Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d42c240362 | |||
| c1a16096ae | |||
| ff0ee0b60e | |||
| 0a6f1e4d62 | |||
| 08a351e028 | |||
| 85c01d3ef1 | |||
| 5826573d5c | |||
| 797a40a566 | |||
| 7308927a3c | |||
| 04f038ba65 |
@@ -119,7 +119,7 @@ const ai = new Ai({
|
||||
system: 'You are a helpful assistant.',
|
||||
compress: {max: 90_000, min: 50_000}, // Compress chat history to min tokens when max is reached
|
||||
temperature: 0.8,
|
||||
max_tokens: 100_000,
|
||||
maxTokens: 100_000,
|
||||
memoryModel: 'gpt-4o', // Cheap model for managing memories in background, defaults to current model
|
||||
models: {
|
||||
'claude-3-5-sonnet': {proto: 'anthropic', token: process.env.ANTHROPIC_TOKEN},
|
||||
|
||||
526
package-lock.json
generated
526
package-lock.json
generated
@@ -1,21 +1,22 @@
|
||||
{
|
||||
"name": "@ztimson/ai-utils",
|
||||
"version": "1.2.6",
|
||||
"version": "1.6.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@ztimson/ai-utils",
|
||||
"version": "1.2.6",
|
||||
"version": "1.6.6",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.102.0",
|
||||
"@huggingface/transformers": "^4.2.0",
|
||||
"@tensorflow/tfjs": "^4.22.0",
|
||||
"@ztimson/node-utils": "^1.0.7",
|
||||
"@ztimson/utils": "^0.29.4",
|
||||
"@ztimson/utils": "^0.30.8",
|
||||
"cheerio": "^1.2.0",
|
||||
"openai": "^6.42.0",
|
||||
"pdf-parse": "^2.4.5",
|
||||
"tesseract.js": "^7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -56,39 +57,12 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "2.0.0-alpha.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-alpha.3.tgz",
|
||||
"integrity": "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "2.0.1",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "2.0.0-alpha.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-2.0.0-alpha.3.tgz",
|
||||
"integrity": "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==",
|
||||
"dev": true,
|
||||
"version": "1.11.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
|
||||
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.0.1.tgz",
|
||||
"integrity": "sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
@@ -577,16 +551,6 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-wasm32/node_modules/@emnapi/runtime": {
|
||||
"version": "1.11.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
|
||||
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
|
||||
@@ -694,32 +658,209 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.0.tgz",
|
||||
"integrity": "sha512-kDoONqMa+VnZ4vvvu/ZUurpJ4gkZU57e7g69qpNgWhYcZFPUHZM2CEMKm+cG6ufDVALbjMvfmMjFVqaK7uEMnA==",
|
||||
"dev": true,
|
||||
"node_modules/@napi-rs/canvas": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.80.tgz",
|
||||
"integrity": "sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"e2e/*"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@napi-rs/canvas-android-arm64": "0.1.80",
|
||||
"@napi-rs/canvas-darwin-arm64": "0.1.80",
|
||||
"@napi-rs/canvas-darwin-x64": "0.1.80",
|
||||
"@napi-rs/canvas-linux-arm-gnueabihf": "0.1.80",
|
||||
"@napi-rs/canvas-linux-arm64-gnu": "0.1.80",
|
||||
"@napi-rs/canvas-linux-arm64-musl": "0.1.80",
|
||||
"@napi-rs/canvas-linux-riscv64-gnu": "0.1.80",
|
||||
"@napi-rs/canvas-linux-x64-gnu": "0.1.80",
|
||||
"@napi-rs/canvas-linux-x64-musl": "0.1.80",
|
||||
"@napi-rs/canvas-win32-x64-msvc": "0.1.80"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-android-arm64": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz",
|
||||
"integrity": "sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@tybys/wasm-util": "^0.10.3"
|
||||
},
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=23.5.0"
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
"node_modules/@napi-rs/canvas-darwin-arm64": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz",
|
||||
"integrity": "sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emnapi/core": "^2.0.0-alpha.3",
|
||||
"@emnapi/runtime": "^2.0.0-alpha.3"
|
||||
"node_modules/@napi-rs/canvas-darwin-x64": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz",
|
||||
"integrity": "sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz",
|
||||
"integrity": "sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz",
|
||||
"integrity": "sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz",
|
||||
"integrity": "sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz",
|
||||
"integrity": "sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz",
|
||||
"integrity": "sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-x64-musl": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz",
|
||||
"integrity": "sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz",
|
||||
"integrity": "sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.139.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz",
|
||||
"integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==",
|
||||
"version": "0.144.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.144.0.tgz",
|
||||
"integrity": "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -784,9 +925,9 @@
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz",
|
||||
"integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -801,9 +942,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz",
|
||||
"integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -818,9 +959,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-x64": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz",
|
||||
"integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -835,9 +976,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz",
|
||||
"integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -852,9 +993,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz",
|
||||
"integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.4.tgz",
|
||||
"integrity": "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -869,9 +1010,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz",
|
||||
"integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.4.tgz",
|
||||
"integrity": "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -889,9 +1030,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz",
|
||||
"integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.4.tgz",
|
||||
"integrity": "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -909,9 +1050,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz",
|
||||
"integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.4.tgz",
|
||||
"integrity": "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -929,9 +1070,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz",
|
||||
"integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.4.tgz",
|
||||
"integrity": "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -949,9 +1090,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz",
|
||||
"integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.4.tgz",
|
||||
"integrity": "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -969,9 +1110,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz",
|
||||
"integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.4.tgz",
|
||||
"integrity": "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -989,9 +1130,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz",
|
||||
"integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1005,63 +1146,10 @@
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz",
|
||||
"integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "1.11.1",
|
||||
"@emnapi/runtime": "1.11.1",
|
||||
"@napi-rs/wasm-runtime": "^1.1.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
|
||||
"integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.2",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
|
||||
"integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
|
||||
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz",
|
||||
"integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.4.tgz",
|
||||
"integrity": "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1076,9 +1164,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz",
|
||||
"integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.4.tgz",
|
||||
"integrity": "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1317,17 +1405,6 @@
|
||||
"@tensorflow/tfjs-core": "4.22.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
|
||||
"integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||
@@ -1448,9 +1525,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@ztimson/utils": {
|
||||
"version": "0.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@ztimson/utils/-/utils-0.29.7.tgz",
|
||||
"integrity": "sha512-cjQ9+RjC5X7gKNA/hJHDf7OtyYCa+5E0PDc76lIaATwNAxXCSx2IO9r2wHiHtZGV5bldjnFmw7aOV8Jmq7SgKQ==",
|
||||
"version": "0.30.8",
|
||||
"resolved": "https://registry.npmjs.org/@ztimson/utils/-/utils-0.30.8.tgz",
|
||||
"integrity": "sha512-+vBjcinqckqMHkP95xWiQeQz2E7Q1oS0b+Odjp+F9rvQ4z0US4JdodjqhEaqh+RAO/yP77x4Eu0a04yBB4HNdw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"var-persist": "^1.0.1"
|
||||
@@ -1544,9 +1621,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.3.tgz",
|
||||
"integrity": "sha512-DRdx5neNsG/QXbniLFWi2YmC/68oeOOmKz6zOjVk6ZS1ZLXgLIKqVEc6hWsmkjBbgii0SwaBTcJ5XKj5gzY/4A==",
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
|
||||
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3011,9 +3088,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.16",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
|
||||
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
|
||||
"version": "3.3.18",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
|
||||
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -3233,6 +3310,38 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pdf-parse": {
|
||||
"version": "2.4.5",
|
||||
"resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-2.4.5.tgz",
|
||||
"integrity": "sha512-mHU89HGh7v+4u2ubfnevJ03lmPgQ5WU4CxAVmTSh/sxVTEDYd1er/dKS/A6vg77NX47KTEoihq8jZBLr8Cxuwg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@napi-rs/canvas": "0.1.80",
|
||||
"pdfjs-dist": "5.4.296"
|
||||
},
|
||||
"bin": {
|
||||
"pdf-parse": "bin/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.16.0 <21 || >=22.3.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/mehmet-kozan"
|
||||
}
|
||||
},
|
||||
"node_modules/pdfjs-dist": {
|
||||
"version": "5.4.296",
|
||||
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz",
|
||||
"integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=20.16.0 || >=22.3.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@napi-rs/canvas": "^0.1.80"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -3272,9 +3381,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.25",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz",
|
||||
"integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==",
|
||||
"version": "8.5.26",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
|
||||
"integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -3292,7 +3401,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.16",
|
||||
"nanoid": "^3.3.17",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
@@ -3434,13 +3543,13 @@
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz",
|
||||
"integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.4.tgz",
|
||||
"integrity": "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.139.0",
|
||||
"@oxc-project/types": "=0.144.0",
|
||||
"@rolldown/pluginutils": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -3450,21 +3559,20 @@
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rolldown/binding-android-arm64": "1.1.5",
|
||||
"@rolldown/binding-darwin-arm64": "1.1.5",
|
||||
"@rolldown/binding-darwin-x64": "1.1.5",
|
||||
"@rolldown/binding-freebsd-x64": "1.1.5",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.1.5",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.1.5",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.1.5",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.1.5",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.1.5",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.1.5",
|
||||
"@rolldown/binding-linux-x64-musl": "1.1.5",
|
||||
"@rolldown/binding-openharmony-arm64": "1.1.5",
|
||||
"@rolldown/binding-wasm32-wasi": "1.1.5",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.1.5",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.1.5"
|
||||
"@rolldown/binding-android-arm64": "1.2.4",
|
||||
"@rolldown/binding-darwin-arm64": "1.2.4",
|
||||
"@rolldown/binding-darwin-x64": "1.2.4",
|
||||
"@rolldown/binding-freebsd-x64": "1.2.4",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.2.4",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.2.4",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.2.4",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.2.4",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.2.4",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.2.4",
|
||||
"@rolldown/binding-linux-x64-musl": "1.2.4",
|
||||
"@rolldown/binding-openharmony-arm64": "1.2.4",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.2.4",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
@@ -4021,16 +4129,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.1.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz",
|
||||
"integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==",
|
||||
"version": "8.2.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz",
|
||||
"integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"lightningcss": "^1.33.0",
|
||||
"picomatch": "^4.0.5",
|
||||
"postcss": "^8.5.17",
|
||||
"rolldown": "~1.1.5",
|
||||
"postcss": "^8.5.25",
|
||||
"rolldown": "~1.2.1",
|
||||
"tinyglobby": "^0.2.17"
|
||||
},
|
||||
"bin": {
|
||||
@@ -4047,7 +4155,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"@vitejs/devtools": "^0.3.0",
|
||||
"@vitejs/devtools": "^0.4.0",
|
||||
"esbuild": "^0.27.0 || ^0.28.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "^4.0.0",
|
||||
@@ -4132,9 +4240,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wasm-feature-detect": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz",
|
||||
"integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==",
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.9.0.tgz",
|
||||
"integrity": "sha512-zonE+xlIIYtxPy++L24ow0hAD8CICb4+FgPyROd3buyXIqsJvUEDkBgfCCoXOd1Hu3DUr0GOfnPIdcGV+YpNaA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ztimson/ai-utils",
|
||||
"version": "1.4.5",
|
||||
"version": "1.6.7",
|
||||
"description": "AI Utility library",
|
||||
"author": "Zak Timson",
|
||||
"license": "MIT",
|
||||
@@ -26,12 +26,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.102.0",
|
||||
"@tensorflow/tfjs": "^4.22.0",
|
||||
"@huggingface/transformers": "^4.2.0",
|
||||
"@tensorflow/tfjs": "^4.22.0",
|
||||
"@ztimson/node-utils": "^1.0.7",
|
||||
"@ztimson/utils": "^0.29.4",
|
||||
"@ztimson/utils": "^0.30.8",
|
||||
"cheerio": "^1.2.0",
|
||||
"openai": "^6.42.0",
|
||||
"pdf-parse": "^2.4.5",
|
||||
"tesseract.js": "^7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Audio } from './audio.ts';
|
||||
import {Vision} from './vision.ts';
|
||||
|
||||
export type AbortablePromise<T> = Promise<T> & {
|
||||
abort: () => any
|
||||
abort: (keep?: boolean) => any
|
||||
};
|
||||
|
||||
export type AiOptions = {
|
||||
|
||||
@@ -24,6 +24,13 @@ export class Anthropic extends LLMProvider {
|
||||
return client;
|
||||
}
|
||||
|
||||
private toWireContent(content: any): any {
|
||||
if(!Array.isArray(content)) return content;
|
||||
return content.map(c => c.type === 'image'
|
||||
? {type: 'image', source: {type: 'base64', media_type: c.mime, data: c.data}}
|
||||
: {type: 'text', text: c.text});
|
||||
}
|
||||
|
||||
/** Convert standard history -> Anthropic wire format */
|
||||
private toWire(history: LLMMessage[]): any[] {
|
||||
const wire: any[] = [];
|
||||
@@ -34,7 +41,7 @@ export class Anthropic extends LLMProvider {
|
||||
{role: 'user', content: [{type: 'tool_result', tool_use_id: h.id, is_error: !!h.error, content: h.error || h.content || ''}]}
|
||||
);
|
||||
} else {
|
||||
wire.push({role: h.role, content: h.content});
|
||||
wire.push({role: h.role, content: this.toWireContent(h.content)});
|
||||
}
|
||||
}
|
||||
return wire;
|
||||
@@ -50,7 +57,7 @@ export class Anthropic extends LLMProvider {
|
||||
const tools = options.tools || this.ai.options.llm?.tools || [];
|
||||
const requestParams: any = {
|
||||
model: options.model || this.model,
|
||||
max_tokens: options.max_tokens || this.ai.options.llm?.max_tokens || 4096,
|
||||
max_tokens: options.maxTokens || this.ai.options.llm?.maxTokens || 4096,
|
||||
system: options.system || this.ai.options.llm?.system || '',
|
||||
temperature: options.temperature || this.ai.options.llm?.temperature || undefined,
|
||||
tools: tools.map(t => ({
|
||||
|
||||
@@ -13,6 +13,56 @@ export function extractLinks(content: string): string[] {
|
||||
return [...new Set([...matches].map(m => m[1].trim()))];
|
||||
}
|
||||
|
||||
/**
|
||||
* Incrementally patch the graph for a set of changed memories, instead of
|
||||
* re-scanning every document. Only the changed memories' own content is
|
||||
* re-parsed for links; affected targets have their backlinks patched.
|
||||
* Does NOT handle node deletion — full rebuildGraph() is still required
|
||||
* when a memory is removed, since that needs a backlink sweep across
|
||||
* everyone who might reference it.
|
||||
*/
|
||||
export function patchGraph(mems: Memory[], nodes: MemoryNode[], changed: Memory[]): MemoryNode[] {
|
||||
const nameSet = new Set(mems.map(m => m.name));
|
||||
const byName = new Map(nodes.map(n => [n.name, n]));
|
||||
|
||||
const ensureNode = (name: string): MemoryNode => {
|
||||
let n = byName.get(name);
|
||||
if (!n) {
|
||||
n = {name, missing: !nameSet.has(name), links: [], backlinks: []};
|
||||
byName.set(name, n);
|
||||
}
|
||||
return n;
|
||||
};
|
||||
|
||||
for (const m of changed) {
|
||||
const node = ensureNode(m.name);
|
||||
node.missing = false; // real memory, promotes any pre-existing ghost entry
|
||||
const oldLinks = m.links ?? [];
|
||||
const newLinks = extractLinks(m.content).filter(l => l !== m.name);
|
||||
|
||||
for (const target of oldLinks.filter(l => !newLinks.includes(l))) {
|
||||
const t = byName.get(target);
|
||||
if (!t) continue;
|
||||
t.backlinks = t.backlinks.filter(n => n !== m.name);
|
||||
if (t.missing && !t.backlinks.length) byName.delete(target); // fully dereferenced ghost
|
||||
}
|
||||
for (const target of newLinks.filter(l => !oldLinks.includes(l))) {
|
||||
const t = ensureNode(target);
|
||||
if (!t.backlinks.includes(m.name)) t.backlinks.push(m.name);
|
||||
}
|
||||
|
||||
m.links = newLinks;
|
||||
node.links = newLinks;
|
||||
}
|
||||
|
||||
for (const m of mems) {
|
||||
const n = byName.get(m.name);
|
||||
if (n) m.backlinks = n.backlinks;
|
||||
}
|
||||
|
||||
return [...byName.values()];
|
||||
}
|
||||
|
||||
export function rebuildGraph(memories: Memory[] | MemoryCache): MemoryNode[] {
|
||||
const mems = memories instanceof MemoryCache ? memories.memories : memories;
|
||||
const nameSet = new Set(mems.map(m => m.name));
|
||||
|
||||
@@ -15,6 +15,7 @@ interface KDNode<T> {
|
||||
axis: number;
|
||||
left: KDNode<T> | null;
|
||||
right: KDNode<T> | null;
|
||||
deleted?: boolean;
|
||||
}
|
||||
|
||||
// ─── Distance helpers ─────────────────────────────────────────────────────────
|
||||
@@ -95,6 +96,7 @@ class BoundedMaxHeap<T> {
|
||||
*
|
||||
* Supports:
|
||||
* - Insertion of labeled points
|
||||
* - Lazy (tombstone) removal, physically purged on rebalance()
|
||||
* - k-nearest-neighbor (KNN) search
|
||||
* - Radius search (all points within a given distance)
|
||||
* - Euclidean and cosine distance metrics
|
||||
@@ -103,6 +105,7 @@ class BoundedMaxHeap<T> {
|
||||
export class KDTree<T = unknown> {
|
||||
private root: KDNode<T> | null = null;
|
||||
private _size = 0;
|
||||
private _tombstones = 0;
|
||||
private readonly distanceFn: (a: number[], b: number[]) => number;
|
||||
|
||||
readonly dims: number;
|
||||
@@ -129,9 +132,15 @@ export class KDTree<T = unknown> {
|
||||
}
|
||||
}
|
||||
|
||||
/** Total number of points stored in the tree. */
|
||||
/** Total number of live points stored in the tree (excludes tombstoned). */
|
||||
get size(): number { return this._size; }
|
||||
|
||||
/** Fraction of physical nodes that are tombstoned (pending removal on next rebalance). */
|
||||
get tombstoneRatio(): number {
|
||||
const total = this._size + this._tombstones;
|
||||
return total ? this._tombstones / total : 0;
|
||||
}
|
||||
|
||||
// ── Insertion ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -144,10 +153,36 @@ export class KDTree<T = unknown> {
|
||||
this._size++;
|
||||
}
|
||||
|
||||
// ── Removal ────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Lazily remove all live points whose payload matches `predicate`.
|
||||
* O(n) traversal, but avoids a full tree rebuild. Call `rebalance()`
|
||||
* periodically (e.g. once tombstoneRatio crosses ~0.25) to reclaim space
|
||||
* and restore optimal query depth.
|
||||
* @returns number of points removed
|
||||
*/
|
||||
remove(predicate: (payload: T) => boolean): number {
|
||||
let removed = 0;
|
||||
const visit = (node: KDNode<T> | null): void => {
|
||||
if (!node) return;
|
||||
if (!node.deleted && predicate(node.point.payload)) {
|
||||
node.deleted = true;
|
||||
removed++;
|
||||
}
|
||||
visit(node.left);
|
||||
visit(node.right);
|
||||
};
|
||||
visit(this.root);
|
||||
this._size -= removed;
|
||||
this._tombstones += removed;
|
||||
return removed;
|
||||
}
|
||||
|
||||
// ── KNN search ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Find the k nearest neighbors to `query`.
|
||||
* Find the k nearest live neighbors to `query`.
|
||||
* Returns results sorted by distance ascending.
|
||||
*/
|
||||
knn(query: number[], k: number): KNNResult<T>[] {
|
||||
@@ -171,7 +206,7 @@ export class KDTree<T = unknown> {
|
||||
// ── Radius search ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Return all points whose distance to `query` is ≤ `radius`,
|
||||
* Return all live points whose distance to `query` is ≤ `radius`,
|
||||
* sorted by distance ascending.
|
||||
*/
|
||||
radiusSearch(query: number[], radius: number): KNNResult<T>[] {
|
||||
@@ -186,7 +221,7 @@ export class KDTree<T = unknown> {
|
||||
|
||||
// ── Conversion ─────────────────────────────────────────────────────────────
|
||||
|
||||
/** Collect all points in the tree (order not guaranteed). */
|
||||
/** Collect all live points in the tree (order not guaranteed). */
|
||||
toArray(): KDPoint<T>[] {
|
||||
const out: KDPoint<T>[] = [];
|
||||
this.collect(this.root, out);
|
||||
@@ -194,12 +229,14 @@ export class KDTree<T = unknown> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the tree from its current points as a balanced tree.
|
||||
* Useful after many individual insertions to restore O(log n) query time.
|
||||
* Rebuild the tree from its current live points as a balanced tree.
|
||||
* Physically purges tombstones and restores O(log n) query time.
|
||||
*/
|
||||
rebalance(): void {
|
||||
const points = this.toArray();
|
||||
this.root = points.length ? this.buildBalanced(points, 0) : null;
|
||||
this._size = points.length;
|
||||
this._tombstones = 0;
|
||||
}
|
||||
|
||||
// ── Private: build ─────────────────────────────────────────────────────────
|
||||
@@ -251,8 +288,10 @@ export class KDTree<T = unknown> {
|
||||
): void {
|
||||
if (node === null) return;
|
||||
|
||||
if (!node.deleted) {
|
||||
const dist = this.distanceFn(query, node.point.vector);
|
||||
heap.push({ point: node.point, distance: dist });
|
||||
}
|
||||
|
||||
const axis = node.axis;
|
||||
const diff = query[axis] - node.point.vector[axis];
|
||||
@@ -285,10 +324,12 @@ export class KDTree<T = unknown> {
|
||||
): void {
|
||||
if (node === null) return;
|
||||
|
||||
if (!node.deleted) {
|
||||
const dist = this.distanceFn(query, node.point.vector);
|
||||
if (dist <= radius) {
|
||||
results.push({ point: node.point, distance: dist });
|
||||
}
|
||||
}
|
||||
|
||||
const axis = node.axis;
|
||||
const diff = query[axis] - node.point.vector[axis];
|
||||
@@ -310,7 +351,7 @@ export class KDTree<T = unknown> {
|
||||
|
||||
private collect(node: KDNode<T> | null, out: KDPoint<T>[]): void {
|
||||
if (node === null) return;
|
||||
out.push(node.point);
|
||||
if (!node.deleted) out.push(node.point);
|
||||
this.collect(node.left, out);
|
||||
this.collect(node.right, out);
|
||||
}
|
||||
|
||||
239
src/llm.ts
239
src/llm.ts
@@ -1,15 +1,20 @@
|
||||
import {clean, snakeCase} from '@ztimson/utils';
|
||||
import {clean, makeUnique, snakeCase} from '@ztimson/utils';
|
||||
import {AbortablePromise, Ai} from './ai.ts';
|
||||
import {Anthropic} from './antrhopic.ts';
|
||||
import {OpenAi} from './open-ai.ts';
|
||||
import {LLMProvider} from './provider.ts';
|
||||
import {AiTool, AiToolArg} from './tools.ts';
|
||||
import {fileURLToPath} from 'url';
|
||||
import {dirname, join} from 'path';
|
||||
import {spawn} from 'node:child_process';
|
||||
import {Memory, MemoryCache, MemoryManager, MemoryOptions} from './memory.ts';
|
||||
import {Memory, MemoryCache, MemoryManager, MemoryOptions, stripHeader} from './memory.ts';
|
||||
import {mkdtempSync} from 'node:fs';
|
||||
import fs from 'node:fs/promises';
|
||||
import {tmpdir} from 'node:os';
|
||||
import {dirname, join, basename, extname} from 'path';
|
||||
import { PDFParse } from 'pdf-parse';
|
||||
|
||||
const MAX_AGENT_DEPTH = 5;
|
||||
const PDF_OCR_PAGE_THRESHOLD = 12; // above this many pages, OCR scanned pages instead of feeding images to the model
|
||||
|
||||
export type AnthropicConfig = {proto: 'anthropic', token: string | string[]};
|
||||
export type OpenAiConfig = {proto: 'openai', host?: string, token: string | string[]};
|
||||
@@ -27,11 +32,26 @@ export type Agent = {
|
||||
agents?: string[] | null;
|
||||
}
|
||||
|
||||
export type LLMFile = {
|
||||
/** Path to file on disk */
|
||||
path?: string;
|
||||
/** File content: raw text, base64-encoded binary, or a Buffer */
|
||||
content?: string | Buffer;
|
||||
/** Original filename, used to infer type from extension */
|
||||
name?: string;
|
||||
/** Mime type override, inferred from extension if omitted */
|
||||
mime?: string;
|
||||
/** @internal set once extraction has run, skips re-processing next turn */
|
||||
extracted?: boolean;
|
||||
};
|
||||
|
||||
export type LLMMessage = {
|
||||
/** Message originator */
|
||||
role: 'assistant' | 'system' | 'user';
|
||||
/** Message content */
|
||||
content: string | any;
|
||||
/** Files attached to request */
|
||||
files?: LLMFile[];
|
||||
/** Timestamp */
|
||||
timestamp?: number;
|
||||
/** Response duration in ms */
|
||||
@@ -67,7 +87,7 @@ export type LLMRequest = {
|
||||
/** Message history */
|
||||
history?: LLMMessage[];
|
||||
/** Max tokens for request */
|
||||
max_tokens?: number;
|
||||
maxTokens?: number;
|
||||
/** 0 = Rigid Logic, 1 = Balanced, 2 = Hyper Creative **/
|
||||
temperature?: number;
|
||||
/** Available tools */
|
||||
@@ -88,6 +108,8 @@ export type LLMRequest = {
|
||||
mcp?: McpServer[];
|
||||
/** Subagents exposed as delegatable/wrapped tools */
|
||||
agents?: Agent[];
|
||||
/** Attach files to request */
|
||||
files?: LLMFile[];
|
||||
/** @internal recursion guard for nested agent delegation */
|
||||
_agentDepth?: number;
|
||||
}
|
||||
@@ -111,6 +133,11 @@ export type Skill = {
|
||||
}
|
||||
|
||||
class LLM {
|
||||
private static AUDIO_EXT = ['wav','mp3','m4a','flac','ogg','aac','wma'];
|
||||
private static IMAGE_EXT = ['png','jpg','jpeg','bmp','gif','tiff','webp'];
|
||||
private static TEXT_EXT = ['txt','md','csv','json','xml','html','js','ts','py','yaml','yml','log'];
|
||||
private static PDF_EXT = ['pdf'];
|
||||
|
||||
private memoryManager!: MemoryManager;
|
||||
|
||||
defaultModel!: string;
|
||||
@@ -126,7 +153,119 @@ class LLM {
|
||||
this.memoryManager = new MemoryManager(this);
|
||||
}
|
||||
|
||||
private setupAgent(agents: Agent[] = [], allAgents: Agent[], history: LLMMessage[], aborts: (() => void)[], depth = 0, delegateState: {resp: string | null}): AiTool[] {
|
||||
private async loadBuffer(file: LLMFile, asText: boolean): Promise<Buffer> {
|
||||
if(file.path) return fs.readFile(file.path);
|
||||
if(Buffer.isBuffer(file.content)) return file.content;
|
||||
if(typeof file.content === 'string') return Buffer.from(file.content, asText ? 'utf-8' : 'base64');
|
||||
throw new Error('No path or content provided');
|
||||
}
|
||||
|
||||
private async writeTemp(name: string, buffer: Buffer): Promise<string> {
|
||||
const path = join(mkdtempSync(join(tmpdir(), 'ai-file-')), name);
|
||||
await fs.writeFile(path, buffer);
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract text from a PDF. Pages with no text layer (scanned/image-only) are handled as either:
|
||||
* - Rendered to images and returned alongside the text so the (vision-capable) model can read them directly
|
||||
* - OCR'd via Tesseract when the doc is too large to reasonably pass as images
|
||||
*/
|
||||
private async resolvePdf(buffer: Buffer): Promise<{text: string, images: {mime: string, data: string}[]}> {
|
||||
const parser = new PDFParse({data: buffer});
|
||||
try {
|
||||
const {text, pages} = await parser.getText();
|
||||
const scanned = (pages || []).filter(p => !p.text?.trim());
|
||||
if(!scanned.length) return {text: text.trim() || '[Empty PDF]', images: []};
|
||||
const total = pages.length;
|
||||
const pageNums = scanned.map(p => p.num);
|
||||
const {pages: shots} = await parser.getScreenshot({partial: pageNums});
|
||||
if(total <= PDF_OCR_PAGE_THRESHOLD) {
|
||||
return {
|
||||
text: text.trim(),
|
||||
images: shots.map(s => ({mime: 'image/png', data: Buffer.from(s.data).toString('base64')}))
|
||||
};
|
||||
}
|
||||
const ocrText = await Promise.all(shots.map(async (s, i) => {
|
||||
const path = await this.writeTemp(`page-${pageNums[i]}.png`, Buffer.from(s.data));
|
||||
try {
|
||||
return await this.ai.vision.ocr(path) || '';
|
||||
} finally {
|
||||
fs.rm(dirname(path), {recursive: true, force: true}).catch(() => {});
|
||||
}
|
||||
}));
|
||||
return {text: [text.trim(), ...ocrText].filter(Boolean).join('\n\n'), images: []};
|
||||
} finally {
|
||||
await parser.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveFile(file: LLMFile): Promise<{text?: string, images?: {mime: string, data: string}[]}> {
|
||||
const name = file.name || (file.path ? basename(file.path) : 'file');
|
||||
|
||||
// Already resolved on a previous turn, reuse cached text
|
||||
if(file.extracted) return {text: `<file name="${name}">\n${file.content}\n</file>`};
|
||||
|
||||
const ext = extname(name).slice(1).toLowerCase();
|
||||
const mime = file.mime || '';
|
||||
const isAudio = mime.startsWith('audio/') || LLM.AUDIO_EXT.includes(ext);
|
||||
const isImage = mime.startsWith('image/') || LLM.IMAGE_EXT.includes(ext);
|
||||
const isPdf = mime === 'application/pdf' || LLM.PDF_EXT.includes(ext);
|
||||
const isText = mime.startsWith('text/') || LLM.TEXT_EXT.includes(ext);
|
||||
|
||||
let tmpDir: string | null = null;
|
||||
try {
|
||||
if(isImage) {
|
||||
const data = (await this.loadBuffer(file, false)).toString('base64');
|
||||
return {images: [{mime: mime || `image/${ext === 'jpg' ? 'jpeg' : ext}`, data}]};
|
||||
}
|
||||
|
||||
if(isPdf) {
|
||||
const {text, images} = await this.resolvePdf(await this.loadBuffer(file, false));
|
||||
// Only cache/skip re-processing when we didn't need to hand off images (OCR'd or fully text-based)
|
||||
if(!images.length) {
|
||||
file.content = text;
|
||||
file.extracted = true;
|
||||
delete file.path;
|
||||
}
|
||||
return {text: `<file name="${name}">\n${text || '[Scanned PDF - see attached page images]'}\n</file>`, images};
|
||||
}
|
||||
|
||||
let text: string;
|
||||
if(isAudio) {
|
||||
let path = file.path;
|
||||
if(!path) {
|
||||
const buffer = await this.loadBuffer(file, false);
|
||||
path = await this.writeTemp(name, buffer);
|
||||
tmpDir = dirname(path);
|
||||
}
|
||||
text = await this.ai.audio.asr(path) || '';
|
||||
} else if(isText) {
|
||||
text = (await this.loadBuffer(file, true)).toString('utf-8');
|
||||
} else {
|
||||
text = typeof file.content === 'string' ? file.content : `[Binary file, unable to extract: ${name}]`;
|
||||
}
|
||||
file.content = text;
|
||||
file.extracted = true;
|
||||
delete file.path;
|
||||
|
||||
return {text: `<file name="${name}">\n${text}\n</file>`};
|
||||
} catch(err: any) {
|
||||
return {text: `<file name="${name}">Failed to process: ${err.message}</file>`};
|
||||
} finally {
|
||||
if(tmpDir) fs.rm(tmpDir, {recursive: true, force: true}).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveFiles(files: LLMFile[]): Promise<{text: string, images: {mime: string, data: string}[]}> {
|
||||
const resolved = await Promise.all(files.map(f => this.resolveFile(f)));
|
||||
return {
|
||||
text: resolved.filter(r => r.text).map(r => r.text).join('\n\n'),
|
||||
images: resolved.flatMap(r => r.images || [])
|
||||
};
|
||||
}
|
||||
|
||||
private setupAgent(agents: Agent[] = [], allAgents: Agent[], history: LLMMessage[], aborts: ((keep?: boolean) => void)[], depth = 0, delegateState: {resp: string | null}): AiTool[] {
|
||||
return agents.map(a => {
|
||||
const toolName = `${a.delegate ? '' : 'sub'}agent_${snakeCase(a.name)}`;
|
||||
return {
|
||||
@@ -207,7 +346,7 @@ ${a.system}`,
|
||||
|
||||
const list = allTools.map(t => `- ${t.name}: ${t.description}`).join('\n');
|
||||
return {
|
||||
prompt: `You have access to the following MCP tools:\n${list}`,
|
||||
prompt: `## MCP\nYou have access to the following MCP tools:\n${list}`,
|
||||
tools: allTools
|
||||
};
|
||||
}
|
||||
@@ -216,7 +355,7 @@ ${a.system}`,
|
||||
if(!skills?.length) return {prompt: '', tools: []};
|
||||
const list = skills.map(s => `- ${s.name}: ${s.description}`).join('\n');
|
||||
return {
|
||||
prompt: `You have access to the following skill documents, whenever there is overlap between a question and a skill file, use \`skill_read\` to get instructions and background knowledge:\n${list}`,
|
||||
prompt: `## Skills\nYou have access to the following skill documents, whenever there is overlap between a question and a skill file, use \`skill_read\` to get instructions and background knowledge:\n${list}`,
|
||||
tools: [{
|
||||
name: 'skill_read',
|
||||
description: 'Read the full content of a skill/knowledge document',
|
||||
@@ -258,11 +397,13 @@ ${a.system}`,
|
||||
if(!this.models[m]) throw new Error(`Model does not exist: ${m}`);
|
||||
let request: AbortablePromise<string> | null = null;
|
||||
let aborted = false;
|
||||
const nestedAborts: (() => void)[] = [];
|
||||
const abort = () => {
|
||||
let keepOnAbort = true;
|
||||
const nestedAborts: ((keep?: boolean) => void)[] = [];
|
||||
const abort = (keep = true) => {
|
||||
aborted = true;
|
||||
request?.abort?.();
|
||||
nestedAborts.forEach(a => a());
|
||||
keepOnAbort = keep;
|
||||
request?.abort?.(keep);
|
||||
nestedAborts.forEach(a => a(keep));
|
||||
};
|
||||
|
||||
let promise: any;
|
||||
@@ -272,7 +413,24 @@ ${a.system}`,
|
||||
let tools: AiTool[] = options.tools || this.ai.options.llm?.tools || [];
|
||||
const prompts: string[] = [];
|
||||
let history = options.history || [];
|
||||
if(message) history.push({role: 'user', content: message, timestamp: Date.now()});
|
||||
const historyStart = history.length;
|
||||
const files = options.files || [];
|
||||
if(message || files.length) history.push({role: 'user', content: message || '', timestamp: Date.now()});
|
||||
|
||||
// Accumulate streamed text so it can be committed to history if aborted mid-generation
|
||||
let partialText = '';
|
||||
const onStream = options.stream;
|
||||
const stream = (chunk: {text?: string, tool?: string, done?: true}) => {
|
||||
if(chunk.text) partialText += chunk.text;
|
||||
return onStream?.(chunk);
|
||||
};
|
||||
|
||||
/** Commit (keep) or discard this turn's progress on abort, then throw */
|
||||
const abortNow = (): never => {
|
||||
if(keepOnAbort) { if(partialText) history.push({role: 'assistant', content: partialText, timestamp: Date.now()}); }
|
||||
else history.splice(historyStart, history.length - historyStart);
|
||||
throw Object.assign(new Error('Aborted'), {name: 'AbortError'});
|
||||
};
|
||||
|
||||
// MCP
|
||||
const mcp = options.mcp || this.ai.options?.llm?.mcp;
|
||||
@@ -301,8 +459,8 @@ ${a.system}`,
|
||||
const mems = mem.memory instanceof MemoryCache ? mem.memory.memories : mem.memory;
|
||||
if(mems.length) {
|
||||
if(mem.inject) {
|
||||
const pool = 15; // candidates considered, cheap since only refs are listed
|
||||
const budget = mem.maxTokens ?? 2000; // actual content injected
|
||||
const pool = 15;
|
||||
const budget = mem.maxTokens ?? 2000;
|
||||
const relevant = await this.memoryManager.recollect(message, mem.memory, pool);
|
||||
|
||||
let used = 0;
|
||||
@@ -316,45 +474,64 @@ ${a.system}`,
|
||||
} else listed.push(r);
|
||||
}
|
||||
|
||||
prompts.unshift(`You have a background memory process which has prefetched relevant information${mem.update ? ' and will create new memories from this conversation' : ''} for you
|
||||
prompts.unshift(`## Memory
|
||||
You have a background memory process which has prefetched relevant information${mem.update ? ' and will create new memories from this conversation' : ''} for you
|
||||
Assume it is perfect and never mention this process to anyone ever
|
||||
Always use your memories to craft a personalized response, they contain links / [[wiki links]] which you use navigate between them
|
||||
${mem.tool ? `You can access memory files via the \`memory_search\` and \`memory_recall\` tools
|
||||
When you need information about the user, \`memory_recall\` \`People/User\` before asking (fetch if not included bellow)
|
||||
When you need information not provided, attempt 1-3 \`memory_search\` calls with unique queries before asking` : ''}
|
||||
When you need information not provided, attempt 1-3 \`memory_search\` calls with distinct queries before asking` : ''}
|
||||
|
||||
${preloaded.length ? `
|
||||
Prefetched Memories (Most relevant first):
|
||||
${preloaded.length ? `### Prefetched Memories (Most relevant first):
|
||||
|
||||
${preloaded.map(r => `Memory: ${r.name}
|
||||
Description: ${r.description}
|
||||
Linked: ${[r.links, ...r.backlinks].join(', ')}
|
||||
Linked: ${makeUnique([...r.links, ...r.backlinks]).join(', ')}
|
||||
\`\`\`
|
||||
${r.content}
|
||||
${stripHeader(r.content)}
|
||||
\`\`\``).join('\n\n')}` : ''}
|
||||
|
||||
${mem.tool && listed.length ? listed.map(r => `Memory: ${r.name}
|
||||
${mem.tool && listed.length ? '\n' + listed.map(r => `Memory: ${r.name}
|
||||
Description: ${r.description}
|
||||
Linked: ${[r.links, ...r.backlinks].join(', ')}
|
||||
<!-- Truncated -->`).join('\n\n') : ''}
|
||||
|
||||
${mem.tool ? `Full memory list:
|
||||
${mems.map(m => `- ${m.name}: ${m.description}`).join('\n')}` : ''}`.trim())
|
||||
Linked: ${makeUnique([...r.links, ...r.backlinks]).join(', ')}
|
||||
<!-- Truncated -->`).join('\n\n') : ''}`.trim())
|
||||
}
|
||||
if(mem.tool) tools.push(this.memoryManager.tools.read(mem.memory));
|
||||
}
|
||||
}
|
||||
|
||||
if(aborted) throw Object.assign(new Error('Aborted'), {name: 'AbortError'});
|
||||
if(aborted) abortNow();
|
||||
|
||||
const lastMsg = history[history.length - 1];
|
||||
if(files.length && lastMsg?.role === 'user') lastMsg.files = files;
|
||||
const restores: {msg: LLMMessage, content: any}[] = [];
|
||||
for(const msg of history) {
|
||||
if(msg.role !== 'user' || !msg.files?.length) continue;
|
||||
const {text, images} = await this.resolveFiles(msg.files);
|
||||
if(!text && !images.length) continue;
|
||||
restores.push({msg, content: msg.content});
|
||||
const merged = text ? [msg.content, text].filter(Boolean).join('\n\n') : msg.content;
|
||||
msg.content = images.length
|
||||
? [...images.map(i => ({type: 'image', mime: i.mime, data: i.data})), {type: 'text', text: merged}]
|
||||
: merged;
|
||||
}
|
||||
|
||||
const toolTimings = new Map<string, {duration: number, tps: number}>();
|
||||
tools = this.wrapToolTiming(tools, toolTimings);
|
||||
|
||||
if(aborted) throw Object.assign(new Error('Aborted'), {name: 'AbortError'});
|
||||
if(aborted) abortNow();
|
||||
|
||||
prompts.unshift(options.system || this.ai.options.llm?.system || '');
|
||||
request = this.models[m].ask('', {...options, tools, system: prompts.filter(Boolean).join('\n\n')});
|
||||
let resp = await request;
|
||||
request = this.models[m].ask('', {...options, tools, stream, system: prompts.filter(Boolean).join('\n\n')});
|
||||
let resp: string;
|
||||
try {
|
||||
resp = await request;
|
||||
} catch(err: any) {
|
||||
if(aborted) return abortNow();
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Strip the file injection shim
|
||||
restores.forEach(({msg, content}) => msg.content = content);
|
||||
|
||||
// Capture meta (duration / tps)
|
||||
for(const h of history) {
|
||||
|
||||
405
src/memory.ts
405
src/memory.ts
@@ -1,10 +1,13 @@
|
||||
import {MemoryNode, rebuildGraph} from './helpers.ts';
|
||||
import {MemoryNode, patchGraph, rebuildGraph} from './helpers.ts';
|
||||
import {LLMRequest, LLMMessage} from './llm.ts';
|
||||
import {AiTool} from './tools.ts';
|
||||
import {KDPoint, KDTree} from './kd-tree.ts';
|
||||
|
||||
const FACTS_HEADING = '## Facts';
|
||||
import {KDTree} from './kd-tree.ts';
|
||||
import {escapeRegex} from '@ztimson/utils';
|
||||
|
||||
const MERGE_THRESHOLD = 0.12;
|
||||
const PENDING_HEADING = '## Pending';
|
||||
const TREE_TOMBSTONE_LIMIT = 0.25;
|
||||
const ALIAS_MATCH_THRESHOLD = 0.55;
|
||||
const GENERIC_TEMPLATE = `# {{Title}}
|
||||
|
||||
## Summary
|
||||
@@ -17,7 +20,12 @@ export type Memory = {
|
||||
name: string;
|
||||
description: string;
|
||||
content: string;
|
||||
/** Description embedding — indexed in the KD tree, used for merge/ANN candidate lookup */
|
||||
embedding: number[];
|
||||
/** Title-only embedding, weighted heaviest during recall ranking */
|
||||
titleEmbedding?: number[];
|
||||
/** Chunked body embeddings, best-chunk match used during recall ranking */
|
||||
bodyEmbeddings?: number[][];
|
||||
links: string[];
|
||||
backlinks: string[];
|
||||
}
|
||||
@@ -25,6 +33,8 @@ export type Memory = {
|
||||
type MemoryRef = {
|
||||
name: string;
|
||||
description: string;
|
||||
/** Cosine distance from the query, present when returned from a search */
|
||||
distance?: number;
|
||||
}
|
||||
|
||||
type FactBucket = {
|
||||
@@ -32,6 +42,11 @@ type FactBucket = {
|
||||
facts: string[];
|
||||
}
|
||||
|
||||
type FactAgentResult = {
|
||||
buckets: FactBucket[];
|
||||
journal: string;
|
||||
}
|
||||
|
||||
function dedupeFacts(facts: string[]): string[] {
|
||||
const seen = new Map<string, string>();
|
||||
for (const f of facts) {
|
||||
@@ -55,14 +70,30 @@ function cosineDistance(a: number[], b: number[]): number {
|
||||
function cosineSearch(query: number[], memories: Memory[], limit: number): MemoryRef[] {
|
||||
return memories
|
||||
.filter(m => m.embedding?.length)
|
||||
.map(m => ({ref: {name: m.name, description: m.description}, distance: cosineDistance(query, m.embedding)}))
|
||||
.map(m => ({name: m.name, description: m.description, distance: cosineDistance(query, m.embedding)}))
|
||||
.sort((a, b) => a.distance - b.distance)
|
||||
.slice(0, limit)
|
||||
.map(s => s.ref);
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
/** Re-embed a node's title / description / body fields. Description embedding stays the KD-tree index key. */
|
||||
async function embedMemoryFields(node: Memory, llm: any): Promise<void> {
|
||||
const body = stripHeader(node.content);
|
||||
const [titleE] = await llm.embedding(node.name.split('/').pop() || node.name);
|
||||
const [descE] = await llm.embedding(node.description || '');
|
||||
const bodyChunks = body ? await llm.embedding(body) : [];
|
||||
if (titleE) node.titleEmbedding = titleE.embedding;
|
||||
if (descE) node.embedding = descE.embedding;
|
||||
node.bodyEmbeddings = bodyChunks.map((c: any) => c.embedding).filter(Boolean);
|
||||
}
|
||||
|
||||
export function stripHeader(content: string): string {
|
||||
return content.replace(/^---[\s\S]*?\n---\n?/, '').trimStart();
|
||||
}
|
||||
|
||||
export class MemoryCache {
|
||||
private tree!: KDTree<MemoryRef>;
|
||||
/** Tracks which memories are currently indexed in the tree, keyed by name -> embedding reference */
|
||||
private indexed = new Map<string, number[]>();
|
||||
public memories: Memory[];
|
||||
public nodes: MemoryNode[] = [];
|
||||
|
||||
@@ -70,37 +101,48 @@ export class MemoryCache {
|
||||
|
||||
constructor(memories: Memory[]) {
|
||||
this.memories = memories;
|
||||
this.tree = new KDTree<MemoryRef>(0);
|
||||
this.rebuild();
|
||||
}
|
||||
|
||||
private buildTree(): KDTree<MemoryRef> {
|
||||
const embedded = this.memories.filter(m => m.embedding?.length);
|
||||
if (!embedded.length) return new KDTree<MemoryRef>(0);
|
||||
/** Incrementally sync the KD tree against `this.memories` instead of rebuilding from scratch */
|
||||
private syncTree(): void {
|
||||
const current = new Set(this.memories.map(m => m.name));
|
||||
|
||||
const dims = embedded[0].embedding.length;
|
||||
const points: KDPoint<MemoryRef>[] = embedded.map(m => ({
|
||||
vector: m.embedding,
|
||||
payload: {name: m.name, description: m.description},
|
||||
}));
|
||||
for (const [name, emb] of [...this.indexed]) {
|
||||
const mem = this.memories.find(m => m.name === name);
|
||||
if (!mem || !current.has(name) || mem.embedding !== emb) {
|
||||
this.tree.remove(p => p.name === name);
|
||||
this.indexed.delete(name);
|
||||
}
|
||||
}
|
||||
|
||||
return new KDTree<MemoryRef>(dims, 'cosine', points);
|
||||
for (const mem of this.memories) {
|
||||
if (!mem.embedding?.length || this.indexed.has(mem.name)) continue;
|
||||
if (this.tree.dims === 0) this.tree = new KDTree<MemoryRef>(mem.embedding.length, 'cosine');
|
||||
if (mem.embedding.length !== this.tree.dims) continue; // guard against embedding model/dim drift
|
||||
this.tree.insert({vector: mem.embedding, payload: {name: mem.name, description: mem.description}});
|
||||
this.indexed.set(mem.name, mem.embedding);
|
||||
}
|
||||
|
||||
if (this.tree.tombstoneRatio > TREE_TOMBSTONE_LIMIT) this.tree.rebalance();
|
||||
}
|
||||
|
||||
search(query: number[], limit: number): MemoryRef[] {
|
||||
if (!this.tree || this.tree.dims === 0) return [];
|
||||
return this.tree.knn(query, limit).map(r => r.point.payload);
|
||||
return this.tree.knn(query, limit).map(r => ({...r.point.payload, distance: r.distance}));
|
||||
}
|
||||
|
||||
add(memory: Memory): void {
|
||||
this.memories.push(memory);
|
||||
this.rebuild();
|
||||
this.rebuild([memory]);
|
||||
}
|
||||
|
||||
update(memory: Memory): void {
|
||||
const idx = this.memories.findIndex(m => m.name === memory.name);
|
||||
if (idx !== -1) this.memories[idx] = memory;
|
||||
const existing = this.memories.find(m => m.name === memory.name);
|
||||
if (existing) Object.assign(existing, memory);
|
||||
else this.memories.push(memory);
|
||||
this.rebuild();
|
||||
this.rebuild([existing ?? memory]);
|
||||
}
|
||||
|
||||
remove(name: string): void {
|
||||
@@ -111,9 +153,11 @@ export class MemoryCache {
|
||||
}
|
||||
}
|
||||
|
||||
rebuild(): void {
|
||||
this.nodes = rebuildGraph(this.memories);
|
||||
this.tree = this.buildTree();
|
||||
rebuild(changed?: Memory[]): void {
|
||||
this.nodes = (changed?.length && this.nodes.length)
|
||||
? patchGraph(this.memories, this.nodes, changed)
|
||||
: rebuildGraph(this.memories);
|
||||
this.syncTree();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,9 +174,9 @@ class MemoryAccessor {
|
||||
return this.list.find(m => m.name === name);
|
||||
}
|
||||
|
||||
commit(): MemoryNode[] {
|
||||
commit(changed?: Memory[]): MemoryNode[] {
|
||||
if (this.cache) {
|
||||
this.cache.rebuild();
|
||||
this.cache.rebuild(changed);
|
||||
return this.cache.nodes;
|
||||
}
|
||||
return rebuildGraph(this.list);
|
||||
@@ -143,6 +187,7 @@ class MemoryAccessor {
|
||||
return nodes.filter(n => n.missing).map(n => n.name);
|
||||
}
|
||||
|
||||
/** Cache path uses the KD tree's knn(); raw-array path (no cache available) falls back to a linear cosine scan */
|
||||
search(vector: number[], limit: number): MemoryRef[] {
|
||||
return this.cache ? this.cache.search(vector, limit) : cosineSearch(vector, this.list, limit);
|
||||
}
|
||||
@@ -158,10 +203,7 @@ class MemoryAccessor {
|
||||
async backfillEmbeddings(llm: any): Promise<number> {
|
||||
const missing = this.list.filter(m => !m.embedding?.length);
|
||||
if (!missing.length) return 0;
|
||||
await Promise.all(missing.map(async node => {
|
||||
const [e] = await llm.embedding(node.content);
|
||||
if (e) node.embedding = e.embedding;
|
||||
}));
|
||||
await Promise.all(missing.map(node => embedMemoryFields(node, llm)));
|
||||
this.commit();
|
||||
return missing.length;
|
||||
}
|
||||
@@ -181,13 +223,13 @@ export type MemoryOptions = {
|
||||
}
|
||||
|
||||
export class MemoryManager {
|
||||
private recentlyTouched = new Map<string, number>();
|
||||
|
||||
private mergeLock: Promise<any> = Promise.resolve();
|
||||
private queues = new Map<string, {
|
||||
dirty: boolean,
|
||||
request: {abort?: () => void} | null,
|
||||
task: Promise<void>,
|
||||
}>();
|
||||
private recentlyTouched = new Map<string, number>();
|
||||
|
||||
tools = {
|
||||
forget: (memories: Memory[] | MemoryCache): AiTool => ({
|
||||
@@ -247,14 +289,13 @@ ${m.content}
|
||||
return new MemoryAccessor(memories);
|
||||
}
|
||||
|
||||
private appendFacts(node: Memory, facts: string[]): void {
|
||||
private stage(node: Memory, block: string): void {
|
||||
this.ensureDoc(node);
|
||||
const body = this.stripHeader(node.content);
|
||||
const bullets = facts.map(f => `- ${f}`).join('\n');
|
||||
const idx = body.indexOf(FACTS_HEADING);
|
||||
const body = stripHeader(node.content);
|
||||
const idx = body.indexOf(PENDING_HEADING);
|
||||
const newBody = idx === -1
|
||||
? `${body.trimEnd()}\n\n${FACTS_HEADING}\n${bullets}\n`
|
||||
: `${body.slice(0, idx + FACTS_HEADING.length)}\n${bullets}${body.slice(idx + FACTS_HEADING.length)}`;
|
||||
? `${body.trimEnd()}\n\n${PENDING_HEADING}\n${block}\n`
|
||||
: `${body.slice(0, idx + PENDING_HEADING.length)}\n${block}${body.slice(idx + PENDING_HEADING.length)}`;
|
||||
node.content = this.touchHeader(node, newBody);
|
||||
}
|
||||
|
||||
@@ -264,42 +305,85 @@ ${m.content}
|
||||
node.content = this.touchHeader(node, `# ${title}\n`);
|
||||
}
|
||||
|
||||
private async factAgent(conversation: string, store: MemoryAccessor, options: LLMRequest, weekKey: string): Promise<FactBucket[]> {
|
||||
private sanitizeDescription(text: string): string {
|
||||
return (text ?? '').replace(/\s+/g, ' ').trim().slice(0, 240);
|
||||
}
|
||||
|
||||
private relink(memories: Memory[], from: string, to: string): void {
|
||||
const pattern = new RegExp(`\\[\\[${escapeRegex(from)}\\]\\]`, 'g');
|
||||
for (const m of memories) if (pattern.test(m.content)) m.content = m.content.replace(pattern, `[[${to}]]`);
|
||||
}
|
||||
|
||||
private normalizeLeaf(name: string): string {
|
||||
return name.trim().toLowerCase().replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a fact-agent proposed subject to an existing node when it's an alias/rename of one.
|
||||
* Exact match is checked first (cheap, and covers the common case since node names are
|
||||
* already normalized at creation time). Only falls through to fuzzy alias matching against
|
||||
* same-root candidates when there's no existing hit — i.e. only on likely-new-doc creation.
|
||||
*/
|
||||
private resolveSubject(subject: string, store: MemoryAccessor): string {
|
||||
const trimmed = subject.trim();
|
||||
const exact = store.find(trimmed);
|
||||
if (exact) return exact.name;
|
||||
|
||||
const normalized = this.normalizeLeaf(trimmed);
|
||||
const caseInsensitive = store.list.find(m => this.normalizeLeaf(m.name) === normalized);
|
||||
if (caseInsensitive) return caseInsensitive.name;
|
||||
|
||||
const root = trimmed.split('/')[0];
|
||||
const leaf = trimmed.split('/').slice(1).join('/') || trimmed;
|
||||
const candidates = store.list.filter(m => m.name.split('/')[0] === root && m.name !== trimmed);
|
||||
if (!candidates.length) return trimmed;
|
||||
|
||||
// fuzzyMatch requires >=2 terms; pad with an empty string when there's only one candidate
|
||||
const leaves = candidates.map(m => m.name.split('/').slice(1).join('/') || m.name);
|
||||
const probe = leaves.length > 1 ? leaves : [...leaves, ''];
|
||||
const {max, similarities} = this.llm.fuzzyMatch(leaf, ...probe);
|
||||
if (max >= ALIAS_MATCH_THRESHOLD) return candidates[similarities.indexOf(max)].name;
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
private async factAgent(conversation: string, store: MemoryAccessor, options: LLMRequest): Promise<FactAgentResult> {
|
||||
const ghosts = store.ghosts();
|
||||
|
||||
const response = await this.llm.ask(conversation, {
|
||||
model: options.model,
|
||||
temperature: 0.2,
|
||||
system: `You are a fact extractor to build obsidian knowledge vaults.
|
||||
Analyze this conversation and extract facts worth remembering long-term.
|
||||
system: `You are a fact extractor for Obsidian-style knowledge vaults. Analyze the conversation and produce:
|
||||
|
||||
Rules:
|
||||
- Always extract facts that the user explicitly told you to remember
|
||||
- ONLY extract current facts the USER explicitly stated about themselves, their work, projects or decisions that were MADE during this conversation
|
||||
- DO NOT extract greetings, pleasantries, or generic exchanges
|
||||
- DO NOT extract deltas or changes in facts; ONLY the end fact
|
||||
- DO NOT extract anything the AI/assistant itself said
|
||||
- If nothing worth remembering was said, return an empty buckets array
|
||||
1. Journal recap (single paragraph)
|
||||
- "Captains Log" style record keeping
|
||||
- What was discussed/worked on, decisions, user's events/state/mood, general context
|
||||
- Leave empty only for trivial/empty exchanges/small talk
|
||||
|
||||
When extracting facts, you MUST also decide the exact destination path:
|
||||
- Reuse node names (including ghost) as much as possible IF the facts belongs there
|
||||
- All information primarily about the user should go under "People/User"
|
||||
- When required, create a new path following collection/subject format (e.g., People/Sarah, Projects/Oxide) — you are not limited to any fixed list of collections, use whatever fits
|
||||
- For journal entries, use "Journal"
|
||||
2. Fact buckets
|
||||
- ONLY facts the USER explicitly stated about themselves, their work, projects, or decisions made during this conversation
|
||||
- NEVER extract greetings, pleasantries, or anything the assistant itself said
|
||||
- Extract the final/end state, not deltas
|
||||
|
||||
Path assignment rules:
|
||||
- Reuse existing node names whenever possible, including when the subject is an alias/nickname of an existing node (e.g. "Rob" referring to an existing "People/Robert")
|
||||
- Documents should be grouped and named by the root subject
|
||||
- Person → People/Name
|
||||
- Project → Projects/Name
|
||||
- Concept → Concepts/Name
|
||||
- A bug report, its investigation, should be nested and attached to the same root subject node
|
||||
- Tickets/one-off tasks → file under the project/name/component they belong to
|
||||
- Only create a new top-level node when the fact belongs to a genuinely new subject (person/project/concept)\`
|
||||
|
||||
Available nodes:
|
||||
- Journal
|
||||
${this.listNodes(store.list).filter(n => !n.name.includes('Journal')).map(n => `- ${n.name}: ${n.description}`).join('\n') || 'None yet.'}
|
||||
${this.listNodes(store.list).map(n => `- ${n.name}: ${n.description}`).join('\n') || 'None yet.'}
|
||||
${ghosts.length ? `${ghosts.map(g => `- ${g}: (Ghost)`).join('\n')}` : ''}`,
|
||||
schema: {
|
||||
buckets: {type: 'array', description: 'Groups of facts to remember, each assigned to a different node. Return an empty array if there is nothing worth storing in an obsidian vault', items: {
|
||||
journal: {type: 'string', description: 'Short day-to-day recap; empty if nothing happened.', required: false},
|
||||
buckets: {type: 'array', description: 'Groups of facts to remember; empty array if nothing worth storing.', items: {
|
||||
type: 'object', items: {
|
||||
subject: {type: 'string', description: 'Exact existing node name OR new path (e.g. "People/Sarah", "Projects/Oxide"), or "Journal"', required: true},
|
||||
facts: {
|
||||
type: 'array',
|
||||
description: 'Facts to store at this destination',
|
||||
items: {type: 'string', description: 'A single fact'},
|
||||
},
|
||||
subject: {type: 'string', description: 'Exact node name or new path (e.g. "People/Sarah", "Projects/Oxide")', required: true},
|
||||
facts: {type: 'array', description: 'Facts to store here', items: {type: 'string'}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -308,14 +392,16 @@ ${ghosts.length ? `${ghosts.map(g => `- ${g}: (Ghost)`).join('\n')}` : ''}`,
|
||||
|
||||
const buckets = new Map<string, string[]>();
|
||||
for (const bucket of response.buckets ?? []) {
|
||||
const subject = bucket.subject.trim().toLowerCase() === 'journal'
|
||||
? `Journal/${weekKey}` : bucket.subject.trim();
|
||||
const subject = bucket.subject.trim();
|
||||
const facts = buckets.get(subject) ?? [];
|
||||
facts.push(...dedupeFacts(bucket.facts));
|
||||
buckets.set(subject, facts);
|
||||
}
|
||||
|
||||
return buckets.entries().toArray().map(([subject, facts]) => ({subject, facts}));
|
||||
return {
|
||||
buckets: buckets.entries().toArray().map(([subject, facts]) => ({subject, facts})),
|
||||
journal: (response.journal ?? '').trim(),
|
||||
};
|
||||
}
|
||||
|
||||
private getWeekMonday(date: Date = new Date()): string {
|
||||
@@ -330,6 +416,36 @@ ${ghosts.length ? `${ghosts.map(g => `- ${g}: (Ghost)`).join('\n')}` : ''}`,
|
||||
return memories.map(m => ({name: m.name, description: m.description}));
|
||||
}
|
||||
|
||||
/** Finds the closest merge candidate via the KD tree's knn() instead of a manual O(n) cosine scan */
|
||||
private async checkMerge(node: Memory, memories: Memory[] | MemoryCache, options: LLMRequest, threshold = MERGE_THRESHOLD): Promise<Memory | null> {
|
||||
if (!node.embedding?.length || node.name.startsWith('Journal/')) return null;
|
||||
const store = this.access(memories);
|
||||
|
||||
const candidate = store.search(node.embedding, 5)
|
||||
.find(r => r.name !== node.name && !r.name.startsWith('Journal/') && r.distance !== undefined && r.distance <= threshold);
|
||||
if (!candidate) return null;
|
||||
const closest = store.find(candidate.name);
|
||||
if (!closest) return null;
|
||||
|
||||
const result = await this.mergeAgent(node, closest, options);
|
||||
const merged: Memory = {name: result.name, description: this.sanitizeDescription(result.description), content: '', embedding: [], links: [], backlinks: []};
|
||||
merged.content = this.touchHeader(merged, result.content);
|
||||
await embedMemoryFields(merged, this.llm);
|
||||
|
||||
this.relink(store.list, node.name, merged.name);
|
||||
this.relink(store.list, closest.name, merged.name);
|
||||
|
||||
this.queues.get(closest.name)?.request?.abort?.();
|
||||
this.queues.delete(closest.name);
|
||||
|
||||
store.forget(node.name);
|
||||
store.forget(closest.name);
|
||||
store.list.push(merged);
|
||||
store.commit();
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
private reconcile(node: Memory, memories: Memory[] | MemoryCache, options: LLMRequest): Promise<void> {
|
||||
const key = node.name;
|
||||
const existing = this.queues.get(key);
|
||||
@@ -343,19 +459,26 @@ ${ghosts.length ? `${ghosts.map(g => `- ${g}: (Ghost)`).join('\n')}` : ''}`,
|
||||
this.queues.set(key, entry);
|
||||
const store = this.access(memories);
|
||||
entry.task = (async () => {
|
||||
let current = node, merged = false;
|
||||
try {
|
||||
do {
|
||||
entry.dirty = false;
|
||||
await this.docAgent(node, store.list, options, entry);
|
||||
await this.docAgent(current, store.list, options, entry);
|
||||
this.mergeLock = this.mergeLock.then(() => this.checkMerge(current, memories, options));
|
||||
const result = await this.mergeLock;
|
||||
if (result) { current = result; merged = true; }
|
||||
} while (entry.dirty);
|
||||
})().finally(() => {
|
||||
} finally {
|
||||
store.commit(merged ? undefined : [node]);
|
||||
this.queues.delete(key);
|
||||
store.commit();
|
||||
});
|
||||
}
|
||||
})();
|
||||
return entry.task;
|
||||
}
|
||||
|
||||
private async docAgent(node: Memory, memories: Memory[], options: LLMRequest, entry: {request: {abort?: () => void} | null}): Promise<void> {
|
||||
const currentBody = this.stripHeader(node.content);
|
||||
if(!memories.includes(node)) return;
|
||||
const currentBody = stripHeader(node.content);
|
||||
let update;
|
||||
try {
|
||||
for (let i = 0; i < 2 && !update?.content; i++) {
|
||||
@@ -363,27 +486,27 @@ ${ghosts.length ? `${ghosts.map(g => `- ${g}: (Ghost)`).join('\n')}` : ''}`,
|
||||
model: options.model,
|
||||
temperature: 0.3,
|
||||
schema: {
|
||||
description: {type: 'string', description: 'One-line description of what this document covers, no formatting or emojis', required: true},
|
||||
description: {type: 'string', description: 'One factual sentence describing the document\'s ENTIRE SUBJECT MATTER — for use as a search/merge fingerprint', required: true},
|
||||
content: {type: 'string', description: 'Rewritten document body in markdown, without the frontmatter block', required: true},
|
||||
},
|
||||
system: `You are a knowledge base editor maintaining one document in an Obsidian-style vault.
|
||||
system: `You are a knowledge base editor maintaining one Obsidian-style document.
|
||||
|
||||
If the document has a "${FACTS_HEADING}" section, integrate every bullet under it into the appropriate part of the document, then remove the "${FACTS_HEADING}" section entirely. If there is no such section, just tidy the document per the rules below.
|
||||
If it has a "## Pending" section, fold all new material into the appropriate part, resolve overlap, then remove the section entirely. If no section, just tidy per the rules below.
|
||||
|
||||
Structure: follow this generic shape loosely, adapting section names/order to what the content actually needs (e.g. journal-style docs may want a timeline instead of "Details"):
|
||||
Use this loose structure, adapting headings to what the content needs:
|
||||
\`\`\`markdown
|
||||
${GENERIC_TEMPLATE}
|
||||
\`\`\`
|
||||
|
||||
Formatting rules:
|
||||
- Use Obsidian-style markdown: # headings, **bold** for emphasis, bullet & numbered lists for grouped 1D data, tables for 2D data
|
||||
- Link related concepts with [[WikiLink]] notation using full paths like [[People/Sarah]] or [[Projects/Website]]
|
||||
- Create links for specific entities (person, place, project, program) and abstract concepts, but skip generics (car, red, dog)
|
||||
- Keep the document concise, factual, and human-readable
|
||||
- Resolve contradictions: newer facts always win — delete the outdated statement entirely, never keep both
|
||||
- Do not add frontmatter blocks, filler, preamble, or AI commentary
|
||||
Rules:
|
||||
- Contradictions: "## Pending" holds the newest information — bias toward it. Fold it in as the standing fact and drop the outdated statement, unless the old context adds meaningful nuance (e.g. "previously X, now Y"). This document should read as a source of truth, not an audit log
|
||||
- Journals (Journal/...): keep entries as a chronological timeline; clean up grammar within entries but never delete history
|
||||
- Use Obsidian markdown: # headings, **bold**, bullet/numbered lists, tables for 2D data
|
||||
- Link specific entities and concepts with [[WikiLink]] (e.g., [[Projects/KiwixServer]]); skip generics
|
||||
- Keep concise, factual, human-readable
|
||||
- NO frontmatter, filler, preamble, or AI commentary
|
||||
|
||||
Other nodes in the vault (link to these instead of duplicating their content):
|
||||
Available nodes to link to (don't duplicate their content):
|
||||
${this.listNodes(memories).filter(n => n.name !== node.name).map(n => n.name).join(', ') || 'none'}
|
||||
|
||||
Current document:
|
||||
@@ -402,10 +525,41 @@ ${currentBody}
|
||||
}
|
||||
|
||||
if (!update?.content) return;
|
||||
node.description = node.name !== 'People/User' ? update.description : 'All information about the current user';
|
||||
node.description = node.name !== 'People/User' ? this.sanitizeDescription(update.description) : 'All information about the current user';
|
||||
node.content = this.touchHeader(node, update.content);
|
||||
const [e] = await this.llm.embedding(node.content);
|
||||
if (e) node.embedding = e.embedding;
|
||||
await embedMemoryFields(node, this.llm);
|
||||
}
|
||||
|
||||
private async mergeAgent(a: Memory, b: Memory, options: LLMRequest): Promise<{name: string, description: string, content: string}> {
|
||||
const modifiedOf = (m: Memory) => this.parseFrontmatter(m.content).fm.get('modified') || 'unknown';
|
||||
|
||||
return this.llm.ask('', {
|
||||
model: options.model,
|
||||
temperature: 0.3,
|
||||
schema: {
|
||||
name: {type: 'string', description: 'New path for the merged doc, collection/subject format (e.g. Projects/Oxide) — only reuse an old title if it\'s genuinely the best fit', required: true},
|
||||
description: {type: 'string', description: 'One factual sentence describing the merged document\'s subject matter', required: true},
|
||||
content: {type: 'string', description: 'Fully reconciled body in markdown, without frontmatter', required: true},
|
||||
},
|
||||
system: `You are a knowledge base editor merging two overlapping Obsidian documents into one.
|
||||
|
||||
Structure loosely:
|
||||
\`\`\`markdown
|
||||
${GENERIC_TEMPLATE}
|
||||
\`\`\`
|
||||
|
||||
Combine both documents, resolve duplication. On contradictions, bias toward whichever document was modified more recently; drop the outdated statement unless the old context adds meaningful nuance.
|
||||
|
||||
Document A ("${a.name}", last modified ${modifiedOf(a)}):
|
||||
\`\`\`markdown
|
||||
${stripHeader(a.content)}
|
||||
\`\`\`
|
||||
|
||||
Document B ("${b.name}", last modified ${modifiedOf(b)}):
|
||||
\`\`\`markdown
|
||||
${stripHeader(b.content)}
|
||||
\`\`\``,
|
||||
});
|
||||
}
|
||||
|
||||
private parseFrontmatter(content: string): {fm: Map<string, string>, body: string} {
|
||||
@@ -415,25 +569,30 @@ ${currentBody}
|
||||
for (const line of match[1].split('\n')) {
|
||||
const i = line.indexOf(':');
|
||||
if (i === -1) continue;
|
||||
fm.set(line.slice(0, i).trim(), line.slice(i + 1).trim());
|
||||
const key = line.slice(0, i).trim();
|
||||
const raw = line.slice(i + 1).trim();
|
||||
let value = raw;
|
||||
try { value = JSON.parse(raw); } catch { /* legacy unquoted value, keep raw */ }
|
||||
fm.set(key, value);
|
||||
}
|
||||
return {fm, body: match[2]};
|
||||
}
|
||||
|
||||
private stripHeader(content: string): string {
|
||||
return content.replace(/^---[\s\S]*?\n---\n?/, '').trimStart();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the code-owned frontmatter block. `body` is passed through stripHeader() first so a
|
||||
* model that ignores instructions and hallucinates its own `---` block can never corrupt or
|
||||
* duplicate the real frontmatter — the LLM only ever gets to influence the body.
|
||||
*/
|
||||
private touchHeader(node: Memory, body: string): string {
|
||||
const {fm} = this.parseFrontmatter(node.content);
|
||||
fm.set('name', node.name);
|
||||
fm.set('description', node.description || '');
|
||||
fm.set('modified', new Date().toISOString());
|
||||
return this.writeFrontmatter(fm, body);
|
||||
return this.writeFrontmatter(fm, stripHeader(body));
|
||||
}
|
||||
|
||||
private writeFrontmatter(fm: Map<string, string>, body: string): string {
|
||||
const lines = [...fm.entries()].map(([k, v]) => `${k}: ${v}`);
|
||||
const lines = [...fm.entries()].map(([k, v]) => `${k}: ${JSON.stringify(String(v).replace(/\s+/g, ' ').trim())}`);
|
||||
return `---\n${lines.join('\n')}\n---\n\n${body.trimStart()}`;
|
||||
}
|
||||
|
||||
@@ -452,6 +611,19 @@ ${currentBody}
|
||||
return this.access(memories).forget(name);
|
||||
}
|
||||
|
||||
/** Ranks a candidate pool by weighted title/description/body similarity against the query embedding */
|
||||
private rankByFields(query: number[], candidates: Memory[], limit: number): Memory[] {
|
||||
const scored = candidates.map(m => {
|
||||
const titleSim = m.titleEmbedding?.length ? 1 - cosineDistance(query, m.titleEmbedding) : 0;
|
||||
const descSim = m.embedding?.length ? 1 - cosineDistance(query, m.embedding) : 0;
|
||||
const bodySim = m.bodyEmbeddings?.length
|
||||
? Math.max(...m.bodyEmbeddings.map(b => 1 - cosineDistance(query, b)))
|
||||
: 0;
|
||||
return {memory: m, score: titleSim * 0.5 + descSim * 0.35 + bodySim * 0.15};
|
||||
});
|
||||
return scored.sort((a, b) => b.score - a.score).slice(0, limit).map(s => s.memory);
|
||||
}
|
||||
|
||||
async recollect(query: string, memories: Memory[] | MemoryCache, limit = 5, graphDepth = 1): Promise<Memory[]> {
|
||||
const store = this.access(memories);
|
||||
if (!store.list.length) return [];
|
||||
@@ -461,8 +633,11 @@ ${currentBody}
|
||||
const [e] = await this.llm.embedding(query);
|
||||
if (!e) return [];
|
||||
|
||||
const vectorResults = store.search(e.embedding, limit);
|
||||
const found = new Set<string>(vectorResults.map(r => r.name));
|
||||
// Description embedding is the cheap ANN index key; pull a wider pool then re-rank by field weight
|
||||
const pool = store.search(e.embedding, Math.max(limit * 3, limit));
|
||||
const poolMemories = pool.map(r => store.find(r.name)).filter((m): m is Memory => !!m);
|
||||
const ranked = this.rankByFields(e.embedding, poolMemories, limit);
|
||||
const found = new Set<string>(ranked.map(m => m.name));
|
||||
|
||||
if (graphDepth > 0) {
|
||||
let frontier = [...found];
|
||||
@@ -482,9 +657,9 @@ ${currentBody}
|
||||
}
|
||||
}
|
||||
|
||||
const vectorOrder = vectorResults.map(r => r.name);
|
||||
const graphExpansions = [...found].filter(n => !vectorOrder.includes(n));
|
||||
return [...vectorOrder, ...graphExpansions].map(n => store.find(n)!).filter(Boolean);
|
||||
const rankedOrder = ranked.map(m => m.name);
|
||||
const graphExpansions = [...found].filter(n => !rankedOrder.includes(n));
|
||||
return [...rankedOrder, ...graphExpansions].map(n => store.find(n)!).filter(Boolean);
|
||||
}
|
||||
|
||||
async memorize(history: LLMMessage[], memories: Memory[] | MemoryCache, options: LLMRequest): Promise<Memory[]> {
|
||||
@@ -498,26 +673,40 @@ ${currentBody}
|
||||
history.push(pending);
|
||||
|
||||
const store = this.access(memories);
|
||||
const buckets = await this.factAgent(conversation, store, options, this.getWeekMonday());
|
||||
const {buckets, journal} = await this.factAgent(conversation, store, options);
|
||||
const touched: Memory[] = [];
|
||||
|
||||
if (journal) {
|
||||
const journalName = `Journal/${this.getWeekMonday()}`;
|
||||
let jnode = store.find(journalName);
|
||||
if (!jnode) {
|
||||
jnode = {name: journalName, description: '', content: '', embedding: [], links: [], backlinks: []};
|
||||
store.list.push(jnode);
|
||||
}
|
||||
this.stage(jnode, `### ${new Date().toISOString().slice(0, 10)}\n${journal}`);
|
||||
touched.push(jnode);
|
||||
}
|
||||
|
||||
for (const {subject, facts} of buckets) {
|
||||
let node = store.find(subject);
|
||||
const resolved = this.resolveSubject(subject, store);
|
||||
let node = store.find(resolved);
|
||||
if (!node) {
|
||||
node = {name: subject, description: '', content: '', embedding: [], links: [], backlinks: []};
|
||||
node = {name: resolved, description: '', content: '', embedding: [], links: [], backlinks: []};
|
||||
store.list.push(node);
|
||||
}
|
||||
this.appendFacts(node, facts);
|
||||
const [e] = await this.llm.embedding(node.content);
|
||||
if (e) node.embedding = e.embedding;
|
||||
this.touch(node.name);
|
||||
this.stage(node, facts.map(f => `- ${f}`).join('\n'));
|
||||
touched.push(node);
|
||||
}
|
||||
|
||||
await Promise.all(touched.map(async node => {
|
||||
await embedMemoryFields(node, this.llm);
|
||||
this.touch(node.name);
|
||||
}));
|
||||
|
||||
if (touched.length) {
|
||||
store.commit();
|
||||
store.commit(touched);
|
||||
(pending as any).content = `Saved to ${touched.map(n => `[[${n.name}]]`).join(', ')}`;
|
||||
await Promise.all(touched.map(node => this.reconcile(node, memories, options).catch(() => {})));
|
||||
Promise.all(touched.map(node => this.reconcile(node, memories, options).catch(() => {})));
|
||||
} else {
|
||||
(pending as any).content = 'Nothing worth remembering.';
|
||||
}
|
||||
@@ -526,9 +715,9 @@ ${currentBody}
|
||||
return touched;
|
||||
}
|
||||
|
||||
async reconcileVault(memories: Memory[] | MemoryCache, options: LLMRequest, scope: 'touched' | 'all' = 'touched'): Promise<void> {
|
||||
async reconcileAll(memories: Memory[] | MemoryCache, options: LLMRequest, scope: 'touched' | 'all' = 'touched'): Promise<void> {
|
||||
const store = this.access(memories);
|
||||
const targets = scope === 'all' ? store.list : store.list.filter(m => m.content.includes(FACTS_HEADING));
|
||||
const targets = scope === 'all' ? store.list : store.list.filter(m => m.content.includes(PENDING_HEADING));
|
||||
await Promise.all(targets.map(node => this.reconcile(node, memories, options)));
|
||||
store.commit();
|
||||
}
|
||||
|
||||
@@ -25,6 +25,13 @@ export class OpenAi extends LLMProvider {
|
||||
return client;
|
||||
}
|
||||
|
||||
private toWireContent(content: any): any {
|
||||
if(!Array.isArray(content)) return content;
|
||||
return content.map(c => c.type === 'image'
|
||||
? {type: 'image_url', image_url: {url: `data:${c.mime};base64,${c.data}`}}
|
||||
: {type: 'text', text: c.text});
|
||||
}
|
||||
|
||||
/** Convert standard history -> OpenAI wire format */
|
||||
private toWire(history: LLMMessage[], system?: string): any[] {
|
||||
const wire: any[] = [];
|
||||
@@ -41,7 +48,7 @@ export class OpenAi extends LLMProvider {
|
||||
content: h.error || h.content || '',
|
||||
});
|
||||
} else {
|
||||
wire.push({role: h.role, content: h.content});
|
||||
wire.push({role: h.role, content: this.toWireContent(h.content)});
|
||||
}
|
||||
}
|
||||
return wire;
|
||||
@@ -58,7 +65,7 @@ export class OpenAi extends LLMProvider {
|
||||
const requestParams: any = {
|
||||
model: options.model || this.model,
|
||||
stream: !!options.stream,
|
||||
max_completion_tokens: options.max_tokens || this.ai.options.llm?.max_tokens || undefined,
|
||||
max_completion_tokens: options.maxTokens || this.ai.options.llm?.maxTokens || undefined,
|
||||
temperature: options.temperature || this.ai.options.llm?.temperature || undefined,
|
||||
tools: tools.map(t => ({
|
||||
type: 'function',
|
||||
|
||||
Reference in New Issue
Block a user