Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8229e02a52 | |||
| a6fb8ae828 | |||
| d1230bcaad | |||
| 2d49c9aa80 | |||
| 9a39f00f94 | |||
| 436757daad | |||
| 69b3297bb3 | |||
| 710c6ce52c | |||
| 4ac3036000 | |||
| 3121d542d4 | |||
| 51ab8f2538 |
21
main.mjs
21
main.mjs
@@ -1,21 +0,0 @@
|
||||
import {Ai} from './dist/index.mjs';
|
||||
|
||||
const ai = new Ai({
|
||||
path: './',
|
||||
llm: {
|
||||
system: 'You are a testbed for developing an AI library',
|
||||
models: {
|
||||
'qwen/qwen3.5-9b': {proto: 'openai', host: 'http://127.0.0.1:1234/v1'}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const skills = [{
|
||||
name: 'Momentum',
|
||||
description: 'Learn how to use the Momentum API',
|
||||
content: 'You can initialize it with: new Momentum(url);'
|
||||
}];
|
||||
|
||||
const history = [], memory = [];
|
||||
console.log(await ai.language.ask('Can you tell me how to use momentum?', {history, skills}));
|
||||
console.log(history, memory);
|
||||
372
package-lock.json
generated
372
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@ztimson/ai-utils",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@ztimson/ai-utils",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.6",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.102.0",
|
||||
@@ -57,21 +57,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||
"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.1",
|
||||
"@emnapi/wasi-threads": "1.2.2",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||
"version": "1.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz",
|
||||
"integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -79,9 +79,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
||||
"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,
|
||||
@@ -681,14 +681,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
|
||||
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
|
||||
"integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@tybys/wasm-util": "^0.10.1"
|
||||
"@tybys/wasm-util": "^0.10.3"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -700,9 +700,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.133.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
|
||||
"integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
|
||||
"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==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -748,12 +748,6 @@
|
||||
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/inquire": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz",
|
||||
"integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/path": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
||||
@@ -767,15 +761,15 @@
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/utf8": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz",
|
||||
"integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==",
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz",
|
||||
"integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
|
||||
"integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -790,9 +784,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
|
||||
"integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz",
|
||||
"integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -807,9 +801,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-x64": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
|
||||
"integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -824,9 +818,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
|
||||
"integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -841,9 +835,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
|
||||
"integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -858,9 +852,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
|
||||
"integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -878,9 +872,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
|
||||
"integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -898,9 +892,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
|
||||
"integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -918,9 +912,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
|
||||
"integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -938,9 +932,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
|
||||
"integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -958,9 +952,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
|
||||
"integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -978,9 +972,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
|
||||
"integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -995,9 +989,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
|
||||
"integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
|
||||
"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"
|
||||
],
|
||||
@@ -1005,18 +999,29 @@
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "1.10.0",
|
||||
"@emnapi/runtime": "1.10.0",
|
||||
"@napi-rs/wasm-runtime": "^1.1.4"
|
||||
"@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/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-win32-arm64-msvc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
|
||||
"integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1031,9 +1036,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
|
||||
"integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1273,9 +1278,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
|
||||
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
|
||||
"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,
|
||||
@@ -1291,9 +1296,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/hast": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
||||
"integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz",
|
||||
"integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1317,9 +1322,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.13.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.1.tgz",
|
||||
"integrity": "sha512-RSpUJGmvsJ1ZeBehQZFhIdpsz+bIpES0nIQXko4Ybq+N+kX6XvOq3Jo+iJ82FWLdblFq85AsMikd3m35jgezYg==",
|
||||
"version": "24.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz",
|
||||
"integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.18.0"
|
||||
@@ -1355,9 +1360,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@ungap/structured-clone": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz",
|
||||
"integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==",
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz",
|
||||
"integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -1403,18 +1408,18 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@ztimson/utils": {
|
||||
"version": "0.29.4",
|
||||
"resolved": "https://registry.npmjs.org/@ztimson/utils/-/utils-0.29.4.tgz",
|
||||
"integrity": "sha512-INIADSrstEfr9sN0xQkBrcfnUNG9cWqRjmk+q4IFq323jIe/xSHBMunpq8HIBumpIk5rYfPmdHxFhutpUxwEFg==",
|
||||
"version": "0.29.5",
|
||||
"resolved": "https://registry.npmjs.org/@ztimson/utils/-/utils-0.29.5.tgz",
|
||||
"integrity": "sha512-8mUuhi//3agwrueR006emOvJu1JXxVEryJmD3nkEmK4yQ9qS24oZijdoaiLRid/6xc75j3Fk6YjmnYmjq61iPQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"var-persist": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"version": "8.17.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
|
||||
"integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -1425,9 +1430,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/adm-zip": {
|
||||
"version": "0.5.17",
|
||||
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz",
|
||||
"integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==",
|
||||
"version": "0.5.18",
|
||||
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.18.tgz",
|
||||
"integrity": "sha512-ufJnssQGbxzLNS1Ho9bCtX4rQKCCvoVuDLHoJyc3F9dOGDB4BkWs2Ci0kv53lqocAEQ/Cbi+I2XCsNYGqVYqng==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0"
|
||||
@@ -1499,9 +1504,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
|
||||
"integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
|
||||
"integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2004,9 +2009,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/exsolve": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
|
||||
"integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.0.tgz",
|
||||
"integrity": "sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -2041,16 +2046,16 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
|
||||
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
"hasown": "^2.0.4",
|
||||
"mime-types": "^2.1.35"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
@@ -2330,9 +2335,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/idb-keyval": {
|
||||
"version": "6.2.5",
|
||||
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.5.tgz",
|
||||
"integrity": "sha512-eKQkTnS0relYsSOYomx8ozIbmdsQCKUdhyuIaQ2DZgKuaxtyQQMkyD/wlnQN32pO3yutN1b1L8uqwcDKaJd7/Q==",
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.3.0.tgz",
|
||||
"integrity": "sha512-um+2dgAWmYsu615EXpWVwSmapJhON0G43t3Ka/EVaohzPQXSMqKEqeDK/oIW3Ow+BXaF2PvSc+oBTFp793A5Ow==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
@@ -2650,9 +2655,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/linkify-it": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.1.tgz",
|
||||
"integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==",
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz",
|
||||
"integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -2711,9 +2716,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-it": {
|
||||
"version": "14.2.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.2.0.tgz",
|
||||
"integrity": "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==",
|
||||
"version": "14.3.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz",
|
||||
"integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -2728,8 +2733,8 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1",
|
||||
"entities": "^4.4.0",
|
||||
"linkify-it": "^5.0.1",
|
||||
"entities": "^4.5.0",
|
||||
"linkify-it": "^5.0.2",
|
||||
"mdurl": "^2.0.0",
|
||||
"punycode.js": "^2.3.1",
|
||||
"uc.micro": "^2.1.0"
|
||||
@@ -2966,9 +2971,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.12",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
||||
"version": "3.3.15",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
|
||||
"integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -3087,15 +3092,27 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/openai": {
|
||||
"version": "6.42.0",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-6.42.0.tgz",
|
||||
"integrity": "sha512-1WFEt/uXMXOLhYRNkgJWo08Y2YNvNwpVU72K7ibrWgWpNOXd4VojXLbe6SQ4bLiUQ3Y8jz4IiyVkylJCL1DtZg==",
|
||||
"version": "6.46.0",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-6.46.0.tgz",
|
||||
"integrity": "sha512-DFg6jEPT2RO+oAyXtddeUJU8zkGy1OQ1AjGzNIJUMQG03TTqvCpy9tBpQ+2VVVnvrl3E56F8GEin2JYtWpITtA==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"@aws-sdk/credential-provider-node": ">=3.972.0 <4",
|
||||
"@smithy/hash-node": ">=4.3.0 <5",
|
||||
"@smithy/signature-v4": ">=5.4.0 <6",
|
||||
"ws": "^8.18.0",
|
||||
"zod": "^3.25 || ^4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@aws-sdk/credential-provider-node": {
|
||||
"optional": true
|
||||
},
|
||||
"@smithy/hash-node": {
|
||||
"optional": true
|
||||
},
|
||||
"@smithy/signature-v4": {
|
||||
"optional": true
|
||||
},
|
||||
"ws": {
|
||||
"optional": true
|
||||
},
|
||||
@@ -3184,9 +3201,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
|
||||
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -3215,9 +3232,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.15",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
||||
"version": "8.5.17",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.17.tgz",
|
||||
"integrity": "sha512-J7EF+8X+CzRPaJPOv9Ck2wNWJvGnnl3PcNPAdGg6GTLjyVpyQ0yATMSXRFRV01BviT/9Gwuc3rjEyJbDJG9a4w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -3255,9 +3272,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/protobufjs": {
|
||||
"version": "7.6.2",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.2.tgz",
|
||||
"integrity": "sha512-N9EiLovGEQOJSPF26Ij7qUGvahfEnq0eeYZ02aigIedkmz1qZSwjnP9SBITHJuF/6MYbIW4HDN8zdYjsjqJKXQ==",
|
||||
"version": "7.6.5",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz",
|
||||
"integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
@@ -3267,7 +3284,6 @@
|
||||
"@protobufjs/eventemitter": "^1.1.1",
|
||||
"@protobufjs/fetch": "^1.1.1",
|
||||
"@protobufjs/float": "^1.0.2",
|
||||
"@protobufjs/inquire": "^1.1.2",
|
||||
"@protobufjs/path": "^1.1.2",
|
||||
"@protobufjs/pool": "^1.1.0",
|
||||
"@protobufjs/utf8": "^1.1.1",
|
||||
@@ -3378,13 +3394,13 @@
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
|
||||
"integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz",
|
||||
"integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.133.0",
|
||||
"@oxc-project/types": "=0.139.0",
|
||||
"@rolldown/pluginutils": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -3394,21 +3410,21 @@
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rolldown/binding-android-arm64": "1.0.3",
|
||||
"@rolldown/binding-darwin-arm64": "1.0.3",
|
||||
"@rolldown/binding-darwin-x64": "1.0.3",
|
||||
"@rolldown/binding-freebsd-x64": "1.0.3",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.0.3",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.0.3",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.3",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.0.3",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.0.3",
|
||||
"@rolldown/binding-linux-x64-musl": "1.0.3",
|
||||
"@rolldown/binding-openharmony-arm64": "1.0.3",
|
||||
"@rolldown/binding-wasm32-wasi": "1.0.3",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.0.3",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.0.3"
|
||||
"@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"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
@@ -3444,9 +3460,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.8.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz",
|
||||
"integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==",
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -3771,9 +3787,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.27.2.tgz",
|
||||
"integrity": "sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==",
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
|
||||
"integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.18.1"
|
||||
@@ -3875,9 +3891,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/unplugin-dts": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/unplugin-dts/-/unplugin-dts-1.0.2.tgz",
|
||||
"integrity": "sha512-VbNiMD0LMl/t6nJueGtrCp79N7ZO1nquxj/FUybJDnKwZGsnW2wjdwBSzA3QEHujoxmxZIptsG43hL7LzXE96w==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/unplugin-dts/-/unplugin-dts-1.0.3.tgz",
|
||||
"integrity": "sha512-/GR887wfG4r1cWyt1UZsLRuMIjsmEbGkS9yJrz+0dsToHAYUD5CTyP3JMGVLv25j9K0mJcwAVvZno/aTuSUvNg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3893,7 +3909,7 @@
|
||||
"peerDependencies": {
|
||||
"@microsoft/api-extractor": ">=7",
|
||||
"@rspack/core": "^1",
|
||||
"@vue/language-core": "~3.1.5",
|
||||
"@vue/language-core": "^3.1.5",
|
||||
"esbuild": "*",
|
||||
"rolldown": "*",
|
||||
"rollup": ">=3",
|
||||
@@ -3965,16 +3981,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.0.16",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
|
||||
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
|
||||
"version": "8.1.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz",
|
||||
"integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"picomatch": "^4.0.4",
|
||||
"postcss": "^8.5.15",
|
||||
"rolldown": "1.0.3",
|
||||
"picomatch": "^4.0.5",
|
||||
"postcss": "^8.5.16",
|
||||
"rolldown": "~1.1.4",
|
||||
"tinyglobby": "^0.2.17"
|
||||
},
|
||||
"bin": {
|
||||
@@ -3991,7 +4007,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"@vitejs/devtools": "^0.1.18",
|
||||
"@vitejs/devtools": "^0.3.0",
|
||||
"esbuild": "^0.27.0 || ^0.28.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "^4.0.0",
|
||||
@@ -4043,13 +4059,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-dts": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-dts/-/vite-plugin-dts-5.0.2.tgz",
|
||||
"integrity": "sha512-lNeHS+dwGju6eRmNvZQt8Shwv9j3m98hbHse/lIbLq9q3yE2DcIOBBYQEVUF6tS0kOmv+VA9Z5FqmzFnGe4U8g==",
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-dts/-/vite-plugin-dts-5.0.3.tgz",
|
||||
"integrity": "sha512-gIth6NdCEHWPiiRMCK3N6C8WjvdsrtEQrmsiG8h6Ov+lFP+b07Y+wcs9H0H7n146l0PDTYK4cQN1vgeG1pMdRQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"unplugin-dts": "1.0.2"
|
||||
"unplugin-dts": "1.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@microsoft/api-extractor": ">=7",
|
||||
@@ -4169,9 +4185,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "16.2.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
|
||||
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
|
||||
"version": "16.2.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.2.tgz",
|
||||
"integrity": "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^7.0.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ztimson/ai-utils",
|
||||
"version": "1.0.2",
|
||||
"version": "1.2.3",
|
||||
"description": "AI Utility library",
|
||||
"author": "Zak Timson",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as os from 'node:os';
|
||||
import LLM, {AnthropicConfig, OllamaConfig, OpenAiConfig, LLMRequest} from './llm';
|
||||
import LLM, {AnthropicConfig, OpenAiConfig, LLMRequest} from './llm';
|
||||
import { Audio } from './audio.ts';
|
||||
import {Vision} from './vision.ts';
|
||||
|
||||
@@ -18,7 +18,7 @@ export type AiOptions = {
|
||||
embedder?: string;
|
||||
/** Large language models, first is default */
|
||||
llm?: Omit<LLMRequest, 'model'> & {
|
||||
models: {[model: string]: AnthropicConfig | OllamaConfig | OpenAiConfig};
|
||||
models: {[model: string]: AnthropicConfig | OpenAiConfig};
|
||||
}
|
||||
/** OCR model: eng, eng_best, eng_fast */
|
||||
ocr?: string;
|
||||
|
||||
@@ -3,6 +3,7 @@ import {findByProp, objectMap, JSONSanitize, JSONAttemptParse} from '@ztimson/ut
|
||||
import {AbortablePromise, Ai} from './ai.ts';
|
||||
import {LLMMessage, LLMRequest} from './llm.ts';
|
||||
import {LLMProvider} from './provider.ts';
|
||||
import {convertSchema} from './tools.ts';
|
||||
|
||||
export class Anthropic extends LLMProvider {
|
||||
client!: anthropic;
|
||||
@@ -48,7 +49,7 @@ export class Anthropic extends LLMProvider {
|
||||
return history.map(({timestamp, ...h}) => h);
|
||||
}
|
||||
|
||||
ask(message: string, options: LLMRequest = {}): AbortablePromise<string> {
|
||||
ask(message: string, options: LLMRequest = {}): AbortablePromise<string | any> {
|
||||
const controller = new AbortController();
|
||||
return Object.assign(new Promise<any>(async (res) => {
|
||||
let history = this.fromStandard([...options.history || [], {role: 'user', content: message, timestamp: Date.now()}]);
|
||||
@@ -57,7 +58,7 @@ export class Anthropic extends LLMProvider {
|
||||
model: options.model || this.model,
|
||||
max_tokens: options.max_tokens || this.ai.options.llm?.max_tokens || 4096,
|
||||
system: options.system || this.ai.options.llm?.system || '',
|
||||
temperature: options.temperature || this.ai.options.llm?.temperature || 0.7,
|
||||
temperature: options.temperature || this.ai.options.llm?.temperature || undefined,
|
||||
tools: tools.map(t => ({
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
@@ -72,6 +73,16 @@ export class Anthropic extends LLMProvider {
|
||||
stream: !!options.stream,
|
||||
};
|
||||
|
||||
// Add structured output support
|
||||
if(options.schema) {
|
||||
requestParams.output_config = {
|
||||
format: {
|
||||
type: 'json_schema',
|
||||
schema: convertSchema(options.schema)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let resp: any, isFirstMessage = true;
|
||||
do {
|
||||
resp = await this.client.messages.create(requestParams).catch(err => {
|
||||
@@ -128,12 +139,17 @@ export class Anthropic extends LLMProvider {
|
||||
requestParams.messages = history;
|
||||
}
|
||||
} while (!controller.signal.aborted && resp.content.some((c: any) => c.type === 'tool_use'));
|
||||
history.push({role: 'assistant', content: resp.content.filter((c: any) => c.type == 'text').map((c: any) => c.text).join('\n\n')});
|
||||
|
||||
const textContent = resp.content.filter((c: any) => c.type == 'text').map((c: any) => c.text).join('\n\n');
|
||||
history.push({role: 'assistant', content: textContent});
|
||||
history = this.toStandard(history);
|
||||
|
||||
if(options.stream) options.stream({done: true});
|
||||
if(options.history) options.history.splice(0, options.history.length, ...history);
|
||||
res(history.at(-1)?.content);
|
||||
|
||||
// Return parsed JSON if schema provided
|
||||
const finalContent = history.at(-1)?.content;
|
||||
res(options.schema ? JSONAttemptParse(finalContent, finalContent) : finalContent);
|
||||
}), {abort: () => controller.abort()});
|
||||
}
|
||||
}
|
||||
|
||||
13
src/audio.ts
13
src/audio.ts
@@ -141,11 +141,18 @@ print(json.dumps(segments))
|
||||
if(!llm) return transcript;
|
||||
let chunks = this.ai.language.chunk(transcript, 500, 0);
|
||||
if(chunks.length > 4) chunks = [...chunks.slice(0, 3), <string>chunks.at(-1)];
|
||||
const names = await this.ai.language.json(chunks.join('\n'), '{1: "Detected Name", 2: "Second Name"}', {
|
||||
system: 'Use the following transcript to identify speakers. Only identify speakers you are positive about, dont mention speakers you are unsure about in your response',
|
||||
await this.ai.language.ask(chunks.join('\n'), {
|
||||
system: 'Read the following transcript and attempt to identify every speaker. For every positively identified speaker, call the \`identify\` tool with the speaker\'s ID number & the identified name exactly once.',
|
||||
temperature: 0.1,
|
||||
tools: [
|
||||
{name: 'identify', description: 'Identify a speaker', args: {
|
||||
speaker: {type: 'number', description: 'Speaker number', required: true},
|
||||
name: {type: 'string', description: 'Inferred name', required: true},
|
||||
}, fn: ({speaker, name}) => {
|
||||
transcript = transcript.replaceAll(`[Speaker ${speaker}]`, `[${name}]`);
|
||||
}}
|
||||
]
|
||||
});
|
||||
Object.entries(names).forEach(([speaker, name]) => transcript = transcript.replaceAll(`[Speaker ${speaker}]`, `[${name}]`));
|
||||
return transcript;
|
||||
}
|
||||
|
||||
|
||||
334
src/kd-tree.ts
Normal file
334
src/kd-tree.ts
Normal file
@@ -0,0 +1,334 @@
|
||||
export type DistanceMetric = "euclidean" | "cosine";
|
||||
|
||||
export interface KDPoint<T = unknown> {
|
||||
vector: number[];
|
||||
payload: T;
|
||||
}
|
||||
|
||||
export interface KNNResult<T = unknown> {
|
||||
point: KDPoint<T>;
|
||||
distance: number;
|
||||
}
|
||||
|
||||
interface KDNode<T> {
|
||||
point: KDPoint<T>;
|
||||
axis: number;
|
||||
left: KDNode<T> | null;
|
||||
right: KDNode<T> | null;
|
||||
}
|
||||
|
||||
// ─── Distance helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
function euclidean(a: number[], b: number[]): number {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
const d = a[i] - b[i];
|
||||
sum += d * d;
|
||||
}
|
||||
return Math.sqrt(sum);
|
||||
}
|
||||
|
||||
function cosine(a: number[], b: number[]): number {
|
||||
let dot = 0, normA = 0, normB = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
dot += a[i] * b[i];
|
||||
normA += a[i] * a[i];
|
||||
normB += b[i] * b[i];
|
||||
}
|
||||
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
||||
return denom === 0 ? 1 : 1 - dot / denom; // distance = 1 - similarity
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps the k closest candidates in memory, evicts the furthest when full
|
||||
*/
|
||||
class BoundedMaxHeap<T> {
|
||||
private heap: KNNResult<T>[] = [];
|
||||
|
||||
constructor(private readonly k: number) {}
|
||||
|
||||
get size(): number { return this.heap.length; }
|
||||
|
||||
get worstDistance(): number {
|
||||
return this.heap.length < this.k ? Infinity : this.heap[0].distance;
|
||||
}
|
||||
|
||||
push(item: KNNResult<T>): void {
|
||||
if (this.heap.length < this.k) {
|
||||
this.heap.push(item);
|
||||
this.bubbleUp(this.heap.length - 1);
|
||||
} else if (item.distance < this.heap[0].distance) {
|
||||
this.heap[0] = item;
|
||||
this.sinkDown(0);
|
||||
}
|
||||
}
|
||||
|
||||
toSortedArray(): KNNResult<T>[] {
|
||||
return [...this.heap].sort((a, b) => a.distance - b.distance);
|
||||
}
|
||||
|
||||
private bubbleUp(i: number): void {
|
||||
while (i > 0) {
|
||||
const parent = (i - 1) >> 1;
|
||||
if (this.heap[parent].distance >= this.heap[i].distance) break;
|
||||
[this.heap[parent], this.heap[i]] = [this.heap[i], this.heap[parent]];
|
||||
i = parent;
|
||||
}
|
||||
}
|
||||
|
||||
private sinkDown(i: number): void {
|
||||
const n = this.heap.length;
|
||||
while (true) {
|
||||
let largest = i;
|
||||
const l = 2 * i + 1, r = 2 * i + 2;
|
||||
if (l < n && this.heap[l].distance > this.heap[largest].distance) largest = l;
|
||||
if (r < n && this.heap[r].distance > this.heap[largest].distance) largest = r;
|
||||
if (largest === i) break;
|
||||
[this.heap[largest], this.heap[i]] = [this.heap[i], this.heap[largest]];
|
||||
i = largest;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* K-D Tree for efficient nearest-neighbor search over high-dimensional vectors / embeddings.
|
||||
*
|
||||
* Supports:
|
||||
* - Insertion of labeled points
|
||||
* - k-nearest-neighbor (KNN) search
|
||||
* - Radius search (all points within a given distance)
|
||||
* - Euclidean and cosine distance metrics
|
||||
* - Bulk construction (balanced tree) for best query performance
|
||||
*/
|
||||
export class KDTree<T = unknown> {
|
||||
private root: KDNode<T> | null = null;
|
||||
private _size = 0;
|
||||
private readonly dims: number;
|
||||
private readonly distanceFn: (a: number[], b: number[]) => number;
|
||||
|
||||
/**
|
||||
* @param dims Dimensionality of all vectors (must be consistent).
|
||||
* @param metric Distance metric to use. Default: "euclidean".
|
||||
* @param points Optional initial set of points. Builds a balanced tree
|
||||
* in O(n log² n) — prefer this over inserting one-by-one
|
||||
* when you have a large corpus.
|
||||
*/
|
||||
constructor(
|
||||
dims: number,
|
||||
metric: DistanceMetric = "euclidean",
|
||||
points?: KDPoint<T>[]
|
||||
) {
|
||||
this.dims = dims;
|
||||
this.distanceFn = metric === "cosine" ? cosine : euclidean;
|
||||
|
||||
if (points && points.length > 0) {
|
||||
this.validateAll(points);
|
||||
this.root = this.buildBalanced([...points], 0);
|
||||
this._size = points.length;
|
||||
}
|
||||
}
|
||||
|
||||
/** Total number of points stored in the tree. */
|
||||
get size(): number { return this._size; }
|
||||
|
||||
// ── Insertion ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Insert a single point. O(log n) average, O(n) worst case on skewed data.
|
||||
* For bulk loading prefer passing points to the constructor.
|
||||
*/
|
||||
insert(point: KDPoint<T>): void {
|
||||
this.validate(point);
|
||||
this.root = this.insertNode(this.root, point, 0);
|
||||
this._size++;
|
||||
}
|
||||
|
||||
// ── KNN search ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Find the k nearest neighbors to `query`.
|
||||
* Returns results sorted by distance ascending.
|
||||
*/
|
||||
knn(query: number[], k: number): KNNResult<T>[] {
|
||||
if (k <= 0) throw new RangeError("k must be a positive integer");
|
||||
this.validateVector(query);
|
||||
|
||||
const heap = new BoundedMaxHeap<T>(k);
|
||||
this.searchKNN(this.root, query, k, heap, 0);
|
||||
return heap.toSortedArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Nearest single neighbor. Convenience wrapper around knn(query, 1).
|
||||
* Returns null if the tree is empty.
|
||||
*/
|
||||
nearest(query: number[]): KNNResult<T> | null {
|
||||
const results = this.knn(query, 1);
|
||||
return results[0] ?? null;
|
||||
}
|
||||
|
||||
// ── Radius search ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Return all points whose distance to `query` is ≤ `radius`,
|
||||
* sorted by distance ascending.
|
||||
*/
|
||||
radiusSearch(query: number[], radius: number): KNNResult<T>[] {
|
||||
if (radius < 0) throw new RangeError("radius must be non-negative");
|
||||
this.validateVector(query);
|
||||
|
||||
const results: KNNResult<T>[] = [];
|
||||
this.searchRadius(this.root, query, radius, results, 0);
|
||||
results.sort((a, b) => a.distance - b.distance);
|
||||
return results;
|
||||
}
|
||||
|
||||
// ── Conversion ─────────────────────────────────────────────────────────────
|
||||
|
||||
/** Collect all points in the tree (order not guaranteed). */
|
||||
toArray(): KDPoint<T>[] {
|
||||
const out: KDPoint<T>[] = [];
|
||||
this.collect(this.root, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the tree from its current points as a balanced tree.
|
||||
* Useful after many individual insertions to restore O(log n) query time.
|
||||
*/
|
||||
rebalance(): void {
|
||||
const points = this.toArray();
|
||||
this.root = points.length ? this.buildBalanced(points, 0) : null;
|
||||
}
|
||||
|
||||
// ── Private: build ─────────────────────────────────────────────────────────
|
||||
|
||||
private buildBalanced(points: KDPoint<T>[], depth: number): KDNode<T> {
|
||||
const axis = depth % this.dims;
|
||||
points.sort((a, b) => a.vector[axis] - b.vector[axis]);
|
||||
|
||||
const mid = Math.floor(points.length / 2);
|
||||
return {
|
||||
point: points[mid],
|
||||
axis,
|
||||
left: points.slice(0, mid).length
|
||||
? this.buildBalanced(points.slice(0, mid), depth + 1)
|
||||
: null,
|
||||
right: points.slice(mid + 1).length
|
||||
? this.buildBalanced(points.slice(mid + 1), depth + 1)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Private: insert ────────────────────────────────────────────────────────
|
||||
|
||||
private insertNode(
|
||||
node: KDNode<T> | null,
|
||||
point: KDPoint<T>,
|
||||
depth: number
|
||||
): KDNode<T> {
|
||||
if (node === null) {
|
||||
return { point, axis: depth % this.dims, left: null, right: null };
|
||||
}
|
||||
const axis = depth % this.dims;
|
||||
if (point.vector[axis] < node.point.vector[axis]) {
|
||||
node.left = this.insertNode(node.left, point, depth + 1);
|
||||
} else {
|
||||
node.right = this.insertNode(node.right, point, depth + 1);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
// ── Private: KNN traversal ─────────────────────────────────────────────────
|
||||
|
||||
private searchKNN(
|
||||
node: KDNode<T> | null,
|
||||
query: number[],
|
||||
k: number,
|
||||
heap: BoundedMaxHeap<T>,
|
||||
depth: number
|
||||
): void {
|
||||
if (node === null) return;
|
||||
|
||||
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];
|
||||
const [near, far] = diff <= 0
|
||||
? [node.left, node.right]
|
||||
: [node.right, node.left];
|
||||
|
||||
this.searchKNN(near, query, k, heap, depth + 1);
|
||||
|
||||
// Only explore the far side if it could contain a closer point.
|
||||
// For cosine distance we can't prune by axis gap alone, so always explore.
|
||||
const shouldExplore =
|
||||
this.distanceFn === cosine
|
||||
? true
|
||||
: Math.abs(diff) < heap.worstDistance;
|
||||
|
||||
if (shouldExplore) {
|
||||
this.searchKNN(far, query, k, heap, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Private: radius traversal ──────────────────────────────────────────────
|
||||
|
||||
private searchRadius(
|
||||
node: KDNode<T> | null,
|
||||
query: number[],
|
||||
radius: number,
|
||||
results: KNNResult<T>[],
|
||||
depth: number
|
||||
): void {
|
||||
if (node === null) return;
|
||||
|
||||
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];
|
||||
const [near, far] = diff <= 0
|
||||
? [node.left, node.right]
|
||||
: [node.right, node.left];
|
||||
|
||||
this.searchRadius(near, query, radius, results, depth + 1);
|
||||
|
||||
const shouldExplore =
|
||||
this.distanceFn === cosine ? true : Math.abs(diff) <= radius;
|
||||
|
||||
if (shouldExplore) {
|
||||
this.searchRadius(far, query, radius, results, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Private: collect ───────────────────────────────────────────────────────
|
||||
|
||||
private collect(node: KDNode<T> | null, out: KDPoint<T>[]): void {
|
||||
if (node === null) return;
|
||||
out.push(node.point);
|
||||
this.collect(node.left, out);
|
||||
this.collect(node.right, out);
|
||||
}
|
||||
|
||||
// ── Private: validation ────────────────────────────────────────────────────
|
||||
|
||||
private validateVector(v: number[]): void {
|
||||
if (v.length !== this.dims) {
|
||||
throw new TypeError(
|
||||
`Vector length ${v.length} does not match tree dimensionality ${this.dims}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private validate(point: KDPoint<T>): void {
|
||||
this.validateVector(point.vector);
|
||||
}
|
||||
|
||||
private validateAll(points: KDPoint<T>[]): void {
|
||||
for (const p of points) this.validate(p);
|
||||
}
|
||||
}
|
||||
93
src/llm.ts
93
src/llm.ts
@@ -2,14 +2,13 @@ import {AbortablePromise, Ai} from './ai.ts';
|
||||
import {Anthropic} from './antrhopic.ts';
|
||||
import {OpenAi} from './open-ai.ts';
|
||||
import {LLMProvider} from './provider.ts';
|
||||
import {AiTool} from './tools.ts';
|
||||
import {AiTool, AiToolArg} from './tools.ts';
|
||||
import {fileURLToPath} from 'url';
|
||||
import {dirname, join} from 'path';
|
||||
import {spawn} from 'node:child_process';
|
||||
import {Memory, MemoryManager} from './memory.ts';
|
||||
import {Memory, MemoryCache, MemoryManager} from './memory.ts';
|
||||
|
||||
export type AnthropicConfig = {proto: 'anthropic', token: string};
|
||||
export type OllamaConfig = {proto: 'ollama', host: string};
|
||||
export type OpenAiConfig = {proto: 'openai', host?: string, token: string};
|
||||
|
||||
export type LLMMessage = {
|
||||
@@ -37,6 +36,8 @@ export type LLMMessage = {
|
||||
}
|
||||
|
||||
export type LLMRequest = {
|
||||
/** Return a parsed JSON object that matches the schema */
|
||||
schema?: AiToolArg;
|
||||
/** System prompt */
|
||||
system?: string;
|
||||
/** Message history */
|
||||
@@ -54,7 +55,7 @@ export type LLMRequest = {
|
||||
/** Compress old messages in the chat to free up context */
|
||||
compress?: {max: number; min: number};
|
||||
/** User's memory documents - RAG injected automatically each turn */
|
||||
memory?: Memory[];
|
||||
memory?: Memory[] | MemoryCache;
|
||||
/** Model to use for memory operations */
|
||||
memoryModel?: string;
|
||||
/** Skill documents the AI can browse and read on demand */
|
||||
@@ -93,7 +94,6 @@ class LLM {
|
||||
Object.entries(ai.options.llm.models).forEach(([model, config]) => {
|
||||
if(!this.defaultModel) this.defaultModel = model;
|
||||
if(config.proto == 'anthropic') this.models[model] = new Anthropic(this.ai, config.token, model);
|
||||
else if(config.proto == 'ollama') this.models[model] = new OpenAi(this.ai, config.host, 'not-needed', model);
|
||||
else if(config.proto == 'openai') this.models[model] = new OpenAi(this.ai, config.host || null, config.token, model);
|
||||
});
|
||||
this.memoryManager = new MemoryManager(this);
|
||||
@@ -160,7 +160,6 @@ class LLM {
|
||||
ask(message: string, options: LLMRequest = {}): AbortablePromise<string> {
|
||||
options = <any>{
|
||||
system: '',
|
||||
temperature: 0.8,
|
||||
...this.ai.options.llm,
|
||||
models: undefined,
|
||||
history: [],
|
||||
@@ -171,8 +170,8 @@ class LLM {
|
||||
let abort = () => {};
|
||||
return Object.assign(new Promise<string>(async res => {
|
||||
let tools: AiTool[] = options.tools || this.ai.options.llm?.tools || [];
|
||||
const prompts: string[] = [options.system || this.ai.options.llm?.system || ''];
|
||||
if(!options.history) options.history = [];
|
||||
const prompts: string[] = [];
|
||||
let history = options.history || [];
|
||||
|
||||
// MCP
|
||||
const mcp = options.mcp || this.ai.options?.llm?.mcp;
|
||||
@@ -191,31 +190,35 @@ class LLM {
|
||||
}
|
||||
|
||||
// Memory
|
||||
if(options.memory) {
|
||||
const relevant = await this.memoryManager.recollect(message, options.memory);
|
||||
if(relevant.length) {
|
||||
const context = relevant.map(m => `### ${m.name}\n${m.content}`).join('\n\n');
|
||||
options.history.push({
|
||||
id: 'auto_recall_' + Math.random().toString(), role: 'tool', name: 'recall', args: {},
|
||||
content: `Knowledge Documents:\n\n${context}`
|
||||
});
|
||||
}
|
||||
prompts.unshift('You have access to a knowledge base. Relevant documents are injected automatically before each message. Use this knowledge to inform your responses.');
|
||||
if (options.memory) {
|
||||
const mems = options.memory instanceof MemoryCache ? options.memory.memories : options.memory;
|
||||
const relevant = await this.memoryManager.recollect(message, options.memory, 5);
|
||||
prompts.unshift(`You have access to the following memory files:
|
||||
${mems.map(m => `- ${m.name}: ${m.description}`).join('\n')}
|
||||
${relevant.length ? `
|
||||
Relevant memories have been preloaded:
|
||||
${relevant.map(r => `
|
||||
**${r.name}**
|
||||
${r.description}
|
||||
${r.content}
|
||||
`).join('\n---\n')}
|
||||
` : ''}`.trim());
|
||||
tools.push(this.memoryManager.tools.read(options.memory));
|
||||
}
|
||||
|
||||
prompts.unshift(options.system || this.ai.options.llm?.system || '');
|
||||
const resp = await this.models[m].ask(message, {...options, tools, system: prompts.filter(Boolean).join('\n\n')});
|
||||
|
||||
// Trim memory injections from history
|
||||
if(options.memory) {
|
||||
options.history.splice(0, options.history.length, ...options.history.filter(h =>
|
||||
h.role !== 'tool' || h.name !== 'recall'));
|
||||
history.splice(0, history.length, ...history.filter(h => h.role !== 'tool' || h.name !== 'recall'));
|
||||
}
|
||||
|
||||
// Auto-memorize before compressing
|
||||
if(options.compress) {
|
||||
if(options.memory) await this.memoryManager.memorize(options.history, options.memory, options);
|
||||
const compressed = await this.compressHistory(options.history, options.compress.max, options.compress.min, options);
|
||||
options.history.splice(0, options.history.length, ...compressed);
|
||||
if(options.compress && this.estimateTokens(history) >= options.compress.max) {
|
||||
if(options.memory) await this.memoryManager.memorize(history, options.memory, {model: options.memoryModel || this.defaultModel, ...options});
|
||||
const compressed = await this.compressHistory(history, options.compress.max, options.compress.min, options);
|
||||
if(options.history) options.history.splice(0, options.history.length, ...compressed);
|
||||
}
|
||||
|
||||
return res(resp);
|
||||
@@ -226,7 +229,7 @@ class LLM {
|
||||
* Digest full conversation history into memory documents.
|
||||
* Call on session end to persist the conversation.
|
||||
*/
|
||||
async updateMemory(history: LLMMessage[], memories: Memory[], options: LLMRequest = {}): Promise<void> {
|
||||
async updateMemory(history: LLMMessage[], memories: Memory[] | MemoryCache, options: LLMRequest = {}): Promise<void> {
|
||||
await this.memoryManager.memorize(history, memories, {model: this.defaultModel, ...options});
|
||||
}
|
||||
|
||||
@@ -391,40 +394,6 @@ class LLM {
|
||||
return {avg: similarities.reduce((acc, s) => acc + s, 0) / similarities.length, max: Math.max(...similarities), similarities};
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask a question with JSON response
|
||||
* @param {string} text Text to process
|
||||
* @param {string} schema JSON schema the AI should match
|
||||
* @param {LLMRequest} options Configuration options and chat history
|
||||
* @returns {Promise<{} | {} | RegExpExecArray | null>}
|
||||
*/
|
||||
async json(text: string, schema: string, options?: LLMRequest): Promise<any> {
|
||||
let system = `Your job is to convert input to JSON using tool calls. Call the \`submit\` tool at least once with JSON matching this schema:\n\`\`\`json\n${schema}\n\`\`\`\n\nResponses are ignored`;
|
||||
if(options?.system) system += '\n\n' + options.system;
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let done = false;
|
||||
const resp = await this.ask(text, {
|
||||
temperature: 0.3,
|
||||
...options,
|
||||
system,
|
||||
tools: [{
|
||||
name: 'submit',
|
||||
description: 'Submit JSON',
|
||||
args: {json: {type: 'string', description: 'Javascript parsable JSON string', required: true}},
|
||||
fn: (args) => {
|
||||
try {
|
||||
const json = JSON.parse(args.json);
|
||||
resolve(json);
|
||||
done = true;
|
||||
} catch { return 'Invalid JSON'; }
|
||||
return 'Saved';
|
||||
}
|
||||
}, ...(options?.tools || [])],
|
||||
});
|
||||
if(!done) reject(`AI failed to create JSON:\n${resp}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a summary of some text
|
||||
* @param {string} text Text to summarize
|
||||
@@ -459,9 +428,8 @@ class LLM {
|
||||
});
|
||||
}
|
||||
|
||||
addModel(name: string, config: AnthropicConfig | OllamaConfig | OpenAiConfig, setDefault = false) {
|
||||
addModel(name: string, config: AnthropicConfig | OpenAiConfig, setDefault = false) {
|
||||
if(config.proto == 'anthropic') this.models[name] = new Anthropic(this.ai, config.token, name);
|
||||
else if(config.proto == 'ollama') this.models[name] = new OpenAi(this.ai, config.host, 'not-needed', name);
|
||||
else if(config.proto == 'openai') this.models[name] = new OpenAi(this.ai, config.host || null, config.token, name);
|
||||
if(setDefault || !this.defaultModel) this.defaultModel = name;
|
||||
}
|
||||
@@ -473,12 +441,11 @@ class LLM {
|
||||
}
|
||||
}
|
||||
|
||||
setModels(models: {[model: string]: AnthropicConfig | OllamaConfig | OpenAiConfig}, replace = true) {
|
||||
setModels(models: {[model: string]: AnthropicConfig | OpenAiConfig}, replace = true) {
|
||||
if(replace) this.models = {};
|
||||
Object.entries(models).forEach(([model, config]) => {
|
||||
if(!this.defaultModel) this.defaultModel = model;
|
||||
if(config.proto == 'anthropic') this.models[model] = new Anthropic(this.ai, config.token, model);
|
||||
else if(config.proto == 'ollama') this.models[model] = new OpenAi(this.ai, config.host, 'not-needed', model);
|
||||
else if(config.proto == 'openai') this.models[model] = new OpenAi(this.ai, config.host || null, config.token, model);
|
||||
});
|
||||
this.defaultModel = Object.keys(this.models)[0] ?? '';
|
||||
|
||||
594
src/memory.ts
594
src/memory.ts
@@ -1,177 +1,501 @@
|
||||
// memory.ts
|
||||
import {LLMRequest, LLMMessage} from './llm.ts';
|
||||
import {AiTool} from './tools.ts';
|
||||
import {KDTree, KDPoint} from './kd-tree.ts';
|
||||
|
||||
/** Background information the AI will be fed as a knowledge document */
|
||||
export type Memory = {
|
||||
/** Memory subject */
|
||||
name: string;
|
||||
/** Short description of what this document contains - used for RAG retrieval */
|
||||
description: string;
|
||||
/** Full markdown content of the document */
|
||||
content: string;
|
||||
/** Embedding vector of the description - used for similarity search */
|
||||
embedding: number[];
|
||||
links: string[];
|
||||
backlinks: string[];
|
||||
}
|
||||
|
||||
export type MemoryCollection = {
|
||||
/** Memory subject */
|
||||
type MemoryRef = {
|
||||
name: string;
|
||||
/** Short description - required if isNew */
|
||||
description?: string;
|
||||
/** Extracted facts to merge */
|
||||
description: string;
|
||||
}
|
||||
|
||||
type FactBucket = {
|
||||
subject: string;
|
||||
facts: string[];
|
||||
}
|
||||
|
||||
export type MemoryNode = {
|
||||
name: string;
|
||||
missing: boolean;
|
||||
links: string[];
|
||||
backlinks: string[];
|
||||
}
|
||||
|
||||
export function buildMemoryGraph(memories: Memory[] | MemoryCache): MemoryNode[] {
|
||||
const mems = memories instanceof MemoryCache ? memories.memories : memories;
|
||||
const nameSet = new Set(mems.map(m => m.name));
|
||||
const ghosts = new Set<string>();
|
||||
|
||||
for (const m of mems) {
|
||||
for (const link of m.links) {
|
||||
if (!nameSet.has(link)) ghosts.add(link);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
...mems.map(m => ({
|
||||
name: m.name,
|
||||
missing: false,
|
||||
links: m.links,
|
||||
backlinks: m.backlinks,
|
||||
})),
|
||||
...[...ghosts].map(name => ({
|
||||
name,
|
||||
missing: true,
|
||||
links: [],
|
||||
backlinks: mems
|
||||
.filter(m => m.links.includes(name))
|
||||
.map(m => m.name),
|
||||
}))
|
||||
];
|
||||
}
|
||||
|
||||
function extractLinks(content: string): string[] {
|
||||
const matches = content.matchAll(/\[\[([^\]]+)\]\]/g);
|
||||
return [...new Set([...matches].map(m => m[1].trim()))];
|
||||
}
|
||||
|
||||
function rebuildBacklinks(memories: Memory[]): void {
|
||||
for (const m of memories) m.backlinks = [];
|
||||
for (const m of memories) {
|
||||
for (const link of m.links) {
|
||||
const target = memories.find(t => t.name === link);
|
||||
if (target) target.backlinks.push(m.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cosineDistance(a: number[], b: number[]): number {
|
||||
let dot = 0, normA = 0, normB = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
dot += a[i] * b[i];
|
||||
normA += a[i] * a[i];
|
||||
normB += b[i] * b[i];
|
||||
}
|
||||
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
||||
return denom === 0 ? 1 : 1 - dot / denom;
|
||||
}
|
||||
|
||||
export class MemoryCache {
|
||||
private tree: KDTree<MemoryRef>;
|
||||
public memories: Memory[];
|
||||
|
||||
constructor(memories: Memory[]) {
|
||||
this.memories = memories;
|
||||
this.tree = this.buildTree();
|
||||
}
|
||||
|
||||
private buildTree(): KDTree<MemoryRef> {
|
||||
const embedded = this.memories.filter(m => m.embedding?.length);
|
||||
if(!embedded.length) return new KDTree<MemoryRef>(0);
|
||||
|
||||
const dims = embedded[0].embedding.length;
|
||||
const points: KDPoint<MemoryRef>[] = embedded.map(m => ({
|
||||
vector: m.embedding,
|
||||
payload: {name: m.name, description: m.description},
|
||||
}));
|
||||
|
||||
return new KDTree<MemoryRef>(dims, 'cosine', points);
|
||||
}
|
||||
|
||||
search(query: number[], limit: number): MemoryRef[] {
|
||||
const results = this.tree.knn(query, limit);
|
||||
return results.map(r => r.point.payload);
|
||||
}
|
||||
|
||||
add(memory: Memory): void {
|
||||
this.memories.push(memory);
|
||||
this.rebuild();
|
||||
}
|
||||
|
||||
update(memory: Memory): void {
|
||||
const idx = this.memories.findIndex(m => m.name === memory.name);
|
||||
if (idx !== -1) {
|
||||
this.memories[idx] = memory;
|
||||
} else {
|
||||
this.memories.push(memory);
|
||||
}
|
||||
this.rebuild();
|
||||
}
|
||||
|
||||
rebuild(): void {
|
||||
this.tree = this.buildTree();
|
||||
}
|
||||
|
||||
rebuildLinks(): void {
|
||||
rebuildBacklinks(this.memories);
|
||||
}
|
||||
}
|
||||
|
||||
export class MemoryManager {
|
||||
|
||||
tools = {
|
||||
edit: (memory: Memory) => ({
|
||||
name: 'edit',
|
||||
description: 'Edit a memory. Omit start/end to append. Pass start only to replace from that line on. Pass start+end to replace a specific range. start=0 replaces the whole document.',
|
||||
args: {
|
||||
content: {type: 'string', description: 'New content', required: true},
|
||||
start: {type: 'number', description: 'First line to replace (0-indexed, inclusive). Omit to append.'},
|
||||
end: {type: 'number', description: 'Last line to replace (0-indexed, inclusive). Omit to replace from start to end of doc.'},
|
||||
},
|
||||
fn: (args: any) => {
|
||||
const lines = memory.content ? memory.content.split('\n') : [];
|
||||
const newLines = args.content.split('\n');
|
||||
if(args.start === undefined) lines.push(...newLines);
|
||||
else if(args.end === undefined) lines.splice(args.start, lines.length - args.start, ...newLines);
|
||||
else lines.splice(args.start, args.end - args.start + 1, ...newLines);
|
||||
memory.content = lines.join('\n');
|
||||
return `Updated memory:\n${memory.content}`;
|
||||
}
|
||||
}),
|
||||
extract: (pools: MemoryCollection[]) => ({
|
||||
name: 'extract',
|
||||
description: 'Extract a list of facts to group into a single memory',
|
||||
args: {
|
||||
name: {type: 'string', description: 'Exact name of an existing memory, or a new name if none fits ([pro]nouns only)', required: true},
|
||||
description: {type: 'string', description: 'One sentence description of the memory subject, only required if new'},
|
||||
facts: {type: 'string', description: 'Comma separated list of extracted facts', required: true},
|
||||
},
|
||||
fn: (args: any) => {
|
||||
pools.push({
|
||||
name: args.name,
|
||||
description: args.description,
|
||||
facts: args.facts.split(',').map((f: string) => f.trim()).filter(Boolean),
|
||||
});
|
||||
return 'Success';
|
||||
}}),
|
||||
read: (memories: Memory[]) => ({
|
||||
name: 'read',
|
||||
description: 'Read entire memory',
|
||||
read: (memories: Memory[] | MemoryCache): AiTool => ({
|
||||
name: 'read_memory',
|
||||
description: 'Read the full content of a memory document',
|
||||
args: {
|
||||
name: {type: 'string', description: 'Exact memory name', required: true},
|
||||
},
|
||||
fn: (args: any) => {
|
||||
const mem = memories.find(m => m.name === args.name);
|
||||
fn:(args: any) => {
|
||||
const mems = memories instanceof MemoryCache ? memories.memories : memories;
|
||||
const mem = mems.find(m => m.name === args.name);
|
||||
if(!mem) return 'Document not found';
|
||||
return `Name: ${mem.name}\nDescription: ${mem.description}\n\n${mem.content}`;
|
||||
return this.formatMemory(mem);
|
||||
}
|
||||
}),
|
||||
};
|
||||
|
||||
constructor(private llm: any) {}
|
||||
|
||||
private cosineSearch(query: number[], memories: Memory[], limit: number): MemoryRef[] {
|
||||
const scored = memories
|
||||
.filter(m => m.embedding?.length)
|
||||
.map(m => ({
|
||||
ref: {name: m.name, description: m.description},
|
||||
distance: cosineDistance(query, m.embedding)
|
||||
}))
|
||||
.sort((a, b) => a.distance - b.distance)
|
||||
.slice(0, limit);
|
||||
return scored.map(s => s.ref);
|
||||
}
|
||||
|
||||
constructor(private llm: any, private model?: string) {}
|
||||
private createNode(name: string, memories: Memory[]): Memory {
|
||||
const existing = memories.find(m => m.name === name);
|
||||
if(existing) return existing;
|
||||
return {
|
||||
name,
|
||||
description: '',
|
||||
content: '',
|
||||
embedding: [],
|
||||
links: [],
|
||||
backlinks: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts facts from conversation and groups them into individual memories
|
||||
* @param {string} conversation Full conversation formatted as [role]: content
|
||||
* @param {Memory[]} memories The user's memory documents
|
||||
* @param {LLMRequest} options LLM options
|
||||
* @returns {Promise<MemoryCollection[]>} Fact pools grouped by target document
|
||||
*/
|
||||
private async extract(conversation: string, memories: Memory[], options: LLMRequest): Promise<MemoryCollection[]> {
|
||||
const existingDocs = memories.map(m => `Name: ${m.name}\nDescription: ${m.description}`).join('\n\n');
|
||||
const pools: MemoryCollection[] = [];
|
||||
await this.llm.ask(conversation, {
|
||||
model: this.model || options.model,
|
||||
temperature: 0.2,
|
||||
system: `You are a fact extractor. Analyze this conversation and extract facts worth remembering long term.
|
||||
Rules:
|
||||
- ONLY extract facts the USER explicitly stated about themselves or their business
|
||||
- ONLY extract decisions that were MADE during this conversation
|
||||
- DO NOT extract anything the AI said, its name, capabilities, or how it introduced itself
|
||||
- DO NOT extract greetings, pleasantries or generic exchanges
|
||||
- If nothing worth remembering was said, call NO tools
|
||||
private formatMemory(mem: Memory): string {
|
||||
return [
|
||||
`# ${mem.name}`,
|
||||
mem.description ? `> ${mem.description}` : '',
|
||||
mem.links.length ? `**Links:** ${mem.links.map(l => `[[${l}]]`).join(', ')}` : '',
|
||||
mem.backlinks.length ? `**Referenced by:** ${mem.backlinks.map(l => `[[${l}]]`).join(', ')}` : '',
|
||||
'',
|
||||
mem.content,
|
||||
].filter(l => l !== undefined).join('\n');
|
||||
}
|
||||
|
||||
For each fact decide whether it belongs in an existing document or needs a new one, then call the \`extract\` tool.
|
||||
private listNodes(memories: Memory[]): MemoryRef[] {
|
||||
return memories.map(m => ({name: m.name, description: m.description}));
|
||||
}
|
||||
|
||||
Existing documents:\n${existingDocs || 'None yet.'}`,
|
||||
tools: [this.tools.extract(pools)]
|
||||
async recollect(query: string, memories: Memory[] | MemoryCache, limit = 5, graphDepth = 1): Promise<Memory[]> {
|
||||
const mem: Memory[] = memories instanceof MemoryCache ? memories.memories : memories;
|
||||
if(!mem.length) return [];
|
||||
const [e] = await this.llm.embedding(query);
|
||||
if(!e) return [];
|
||||
|
||||
let vectorResults: MemoryRef[];
|
||||
if(memories instanceof MemoryCache) vectorResults = memories.search(e.embedding, limit);
|
||||
else vectorResults = this.cosineSearch(e.embedding, mem, limit);
|
||||
const found = new Set<string>(vectorResults.map(r => r.name));
|
||||
|
||||
if(graphDepth > 0) {
|
||||
const frontier = [...found];
|
||||
for(let depth = 0; depth < graphDepth; depth++) {
|
||||
const next: string[] = [];
|
||||
for(const name of frontier) {
|
||||
const node = mem.find(m => m.name === name);
|
||||
if(!node) continue;
|
||||
for(const link of node.links) {
|
||||
if(!found.has(link) && mem.find(m => m.name === link)) {
|
||||
found.add(link);
|
||||
next.push(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
frontier.splice(0, frontier.length, ...next);
|
||||
if(!frontier.length) break;
|
||||
}
|
||||
}
|
||||
|
||||
const vectorOrder = vectorResults.map(r => r.name);
|
||||
const graphExpansions = [...found].filter(n => !vectorOrder.includes(n));
|
||||
const ordered = [...vectorOrder, ...graphExpansions];
|
||||
return ordered.map(n => mem.find(m => m.name === n)!).filter(Boolean);
|
||||
}
|
||||
|
||||
async memorize(history: LLMMessage[], memories: Memory[] | MemoryCache, options: LLMRequest): Promise<void> {
|
||||
const mem = memories instanceof MemoryCache ? memories.memories : memories;
|
||||
const conversation = history
|
||||
.filter(h => h.role === 'user' || h.role === 'assistant')
|
||||
.map(h => `[${h.role}]: ${h.content}`).join('\n\n').trim();
|
||||
|
||||
if(conversation) {
|
||||
const buckets = await this.factAgent(conversation, mem, options);
|
||||
if(buckets.length) {
|
||||
await Promise.all(buckets.map(async bucket => {
|
||||
const node = await this.organizingAgent(bucket, mem, options);
|
||||
if(!mem.find(m => m.name === node.name)) mem.push(node);
|
||||
await this.docAgent(node, bucket, mem, options);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-compress old journals
|
||||
const weekAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
|
||||
const oldDailies = mem.filter(m => {
|
||||
const journal = /^Journal\/(\d{4}-\d{2}-\d{2}$)/.exec(m.name);
|
||||
return journal && new Date(journal[1]).getTime() < weekAgo;
|
||||
});
|
||||
return pools;
|
||||
|
||||
if(oldDailies.length) {
|
||||
const byMonth = new Map<string, Memory[]>();
|
||||
for(const daily of oldDailies) {
|
||||
const match = daily.name.match(/^Journal\/(\d{4}-\d{2})-\d{2}$/);
|
||||
if(!match) continue;
|
||||
const monthKey = match[1];
|
||||
if(!byMonth.has(monthKey)) byMonth.set(monthKey, []);
|
||||
byMonth.get(monthKey)!.push(daily);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bot 2 - Editor: merges a pool of facts into a specific document using surgical line-based edits.
|
||||
* Receives full document content and uses read + amend tools to make precise edits.
|
||||
* @param {MemoryCollection} newMem The fact pool to merge
|
||||
* @param {Memory[]} memories The user's memory documents
|
||||
* @param {LLMRequest} options LLM options
|
||||
*/
|
||||
private async edit(newMem: MemoryCollection, memories: Memory[], options: LLMRequest): Promise<void> {
|
||||
const existing = memories.find(m => m.name === newMem.name);
|
||||
const mem: Memory = existing || {name: newMem.name, description: newMem.description || '', content: '', embedding: []};
|
||||
const isNew = !existing;
|
||||
for(const [monthKey, entries] of byMonth) {
|
||||
const monthlyPath = `Journal/${monthKey}`;
|
||||
let monthly = mem.find(m => m.name === monthlyPath);
|
||||
if(!monthly) {
|
||||
monthly = this.createNode(monthlyPath, mem);
|
||||
mem.push(monthly);
|
||||
}
|
||||
|
||||
await this.llm.ask(newMem.facts.map(f => `- ${f}`).join('\n'),
|
||||
const bucket: FactBucket = {
|
||||
subject: monthlyPath,
|
||||
facts: entries.flatMap(e => e.content.split('\n').filter(line => line.trim())),
|
||||
};
|
||||
|
||||
await this.docAgent(monthly, bucket, mem, options);
|
||||
for(const daily of entries) {
|
||||
const idx = mem.indexOf(daily);
|
||||
if(idx !== -1) mem.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (memories instanceof MemoryCache) {
|
||||
memories.rebuildLinks();
|
||||
memories.rebuild();
|
||||
} else {
|
||||
rebuildBacklinks(mem);
|
||||
}
|
||||
}
|
||||
|
||||
private async docAgent(node: Memory, bucket: FactBucket, memories: Memory[], options: LLMRequest): Promise<void> {
|
||||
let finalContent = node.content;
|
||||
const isJournalCompression = node.name.match(/^Journal\/\d{4}-\d{2}$/);
|
||||
const systemPrompt = isJournalCompression
|
||||
? `You are a journal compressor. Condense the daily entries below into a monthly summary.
|
||||
|
||||
Format:
|
||||
# ${node.name}
|
||||
|
||||
## Themes
|
||||
(Recurring topics, moods, patterns)
|
||||
|
||||
## Key Events
|
||||
(Important moments, decisions, milestones)
|
||||
|
||||
## Notable Conversations
|
||||
(Significant discussions or revelations)
|
||||
|
||||
Rules:
|
||||
- Use [[WikiLinks]] to reference permanent notes using full paths like [[People/Sarah]] or [[Projects/Website]]
|
||||
- Keep it concise but preserve emotional/temporal context
|
||||
- Discard filler but keep things the user vented about or cared about
|
||||
- If a fact belongs in a permanent note, link to it instead of duplicating
|
||||
|
||||
Current monthly summary:
|
||||
\`\`\`markdown
|
||||
${node.content || '(empty — first compression for this month)'}
|
||||
\`\`\``
|
||||
: `You are a knowledge base editor. Integrate the provided facts into the document below.
|
||||
|
||||
Formatting rules:
|
||||
- Use Obsidian-style markdown: # headings, **bold** for key terms, bullet lists for facts
|
||||
- Link related concepts with [[WikiLink]] notation using full paths like [[People/Sarah]] or [[Projects/Website]]
|
||||
- You may create links to nodes that don't exist yet if the concept is important
|
||||
- Keep the document concise, factual, and human-readable
|
||||
- Resolve any contradictions between old content and new facts (new facts win)
|
||||
- Do not add filler, preamble, or AI commentary — just clean knowledge documents
|
||||
|
||||
All nodes:
|
||||
${this.listNodes(memories).map(n => n.name).join(', ') || 'none'}
|
||||
|
||||
Current document:
|
||||
\`\`\`markdown
|
||||
${node.content || '(empty — this is a new document)'}
|
||||
\`\`\``;
|
||||
|
||||
await this.llm.ask(
|
||||
`New facts to integrate:\n${bucket.facts.map(f => `- ${f}`).join('\n')}`,
|
||||
{
|
||||
model: this.model || options.model,
|
||||
temperature: 0.2,
|
||||
system: `You are a document editor. Merge the users list of facts into the following document using the \`edit\` tool; call it as many times as necessary.
|
||||
|
||||
Name: ${mem.name}
|
||||
Description: ${mem.description}
|
||||
${mem.content}`,
|
||||
tools: [this.tools.edit(mem)]
|
||||
model: options.model,
|
||||
temperature: 0.3,
|
||||
system: systemPrompt,
|
||||
tools: [{
|
||||
name: 'update_document',
|
||||
description: 'Write the complete updated document content',
|
||||
args: {
|
||||
description: {type: 'string', description: 'One-line description of what this document covers, no formatting or emojis', required: true},
|
||||
content: {type: 'string', description: 'Fully updated document in markdown', required: true},
|
||||
},
|
||||
fn:(args: any) => {
|
||||
node.description = args.description;
|
||||
finalContent = args.content;
|
||||
return 'Saved';
|
||||
}
|
||||
}]
|
||||
}
|
||||
);
|
||||
|
||||
if(isNew || mem.description !== existing?.description) {
|
||||
const [e] = await this.llm.embedding(mem.description);
|
||||
mem.embedding = e.embedding;
|
||||
}
|
||||
|
||||
if(isNew) memories.push(mem);
|
||||
else {
|
||||
const idx = memories.findIndex(m => m.name === newMem.name);
|
||||
if(idx >= 0) memories[idx] = mem;
|
||||
node.content = finalContent;
|
||||
node.links = extractLinks(finalContent);
|
||||
const needsEmbed = !node.embedding?.length || node.description !== memories.find(m => m.name === node.name)?.description;
|
||||
if (needsEmbed) {
|
||||
const [e] = await this.llm.embedding(node.description);
|
||||
if (e) node.embedding = e.embedding;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find relevant memory documents for a query using description embeddings
|
||||
* @param {string} query The query to search against
|
||||
* @param {Memory[]} memories The user's memory documents
|
||||
* @param {number} limit Max number of results to return
|
||||
* @returns {Promise<Memory[]>} The most relevant memory documents
|
||||
*/
|
||||
async recollect(query: string, memories: Memory[], limit = 5): Promise<Memory[]> {
|
||||
const [e] = await this.llm.embedding(query);
|
||||
return memories
|
||||
.filter(m => m.embedding?.length)
|
||||
.map(m => ({...m, score: this.llm.cosineSimilarity(m.embedding, e.embedding)}))
|
||||
.toSorted((a: any, b: any) => b.score - a.score)
|
||||
.slice(0, limit);
|
||||
private async factAgent(conversation: string, memories: Memory[], options: LLMRequest): Promise<FactBucket[]> {
|
||||
const buckets: FactBucket[] = [];
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
await this.llm.ask(conversation, {
|
||||
model: options.model,
|
||||
temperature: 0.2,
|
||||
system: `You are a fact extractor. Analyze this conversation and extract facts worth remembering long-term.
|
||||
|
||||
Rules:
|
||||
- ONLY extract facts the USER explicitly stated about themselves, their work, or their projects
|
||||
- ONLY extract decisions that were MADE during this conversation
|
||||
- DO NOT extract anything the AI said, its capabilities, or meta-conversation about the AI
|
||||
- DO NOT extract greetings, pleasantries, or generic exchanges
|
||||
- If nothing worth remembering was said, do not call any tools
|
||||
|
||||
**Organizational patterns:**
|
||||
- Journal entries use paths like: Journal/${today}
|
||||
- People use paths like: People/Name
|
||||
- Projects use paths like: Projects/Name
|
||||
- Personal info uses paths like: Personal/Goals, Personal/Tasks, etc.
|
||||
- General knowledge uses paths like: Biology/Topic, History/Topic, etc.
|
||||
|
||||
Learn from existing nodes and follow the same pattern when extracting.
|
||||
|
||||
Group facts by subject. For each group call \`extract_facts\` once with the FULL PATH.
|
||||
|
||||
Known nodes (name: description):
|
||||
${this.listNodes(memories).map(n => `- ${n.name}: ${n.description}`).join('\n') || 'None yet.'}`,
|
||||
tools: [{
|
||||
name: 'extract_facts',
|
||||
description: 'Submit a group of related facts for a specific subject',
|
||||
args: {
|
||||
subject: {type: 'string', description: 'Full path for the subject (e.g., "Journal/2025-01-27", "People/Sarah", "Projects/Website")', required: true},
|
||||
facts: {type: 'string', description: 'Comma-separated list of extracted facts', required: true},
|
||||
},
|
||||
fn: (args: any) => {
|
||||
buckets.push({
|
||||
subject: args.subject,
|
||||
facts: args.facts.split(',').map((f: string) => f.trim()).filter(Boolean),
|
||||
});
|
||||
return 'Recorded';
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
return buckets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Two-stage memory pipeline: classify facts from conversation history then surgically merge them into documents.
|
||||
* Bot 1 (classify) extracts and groups facts cheaply. Bot 2 (edit) runs per-document in parallel with full content access.
|
||||
* @param {LLMMessage[]} history Full conversation history to digest
|
||||
* @param {Memory[]} memories The user's memory documents — mutated in place
|
||||
* @param {LLMRequest} options LLM options
|
||||
*/
|
||||
async memorize(history: LLMMessage[], memories: Memory[], options: LLMRequest): Promise<void> {
|
||||
const conversation = history
|
||||
.filter(h => h.role === 'user' || h.role === 'assistant')
|
||||
.map(h => `[${h.role}]: ${h.content}`)
|
||||
.join('\n\n');
|
||||
if(!conversation.trim()) return;
|
||||
const pools = await this.extract(conversation, memories, options);
|
||||
if(!pools.length) return;
|
||||
await Promise.all(pools.map(pool => this.edit(pool, memories, options)));
|
||||
private async organizingAgent(bucket: FactBucket, memories: Memory[], options: LLMRequest): Promise<Memory> {
|
||||
let candidates = this.listNodes(memories);
|
||||
let attempts = 0;
|
||||
const maxAttempts = 3;
|
||||
|
||||
while (attempts++ < maxAttempts) {
|
||||
let home = '', mode: string | null = null;
|
||||
|
||||
const resp = await this.llm.ask(`Subject: ${bucket.subject}\n\nFacts:\n${bucket.facts.map(f => `- ${f}`).join('\n')}`, {
|
||||
model: options.model,
|
||||
temperature: 0.1,
|
||||
system: `You are a knowledge organizer. Your job is to find the correct home for the supplied facts.
|
||||
|
||||
1. Review the facts and the node list below. Pick the most likely match or decide if a new node is needed.
|
||||
2. If you picked an existing node, use \`read\` to verify it's the right place.
|
||||
- After reading, call either \`confirm\` (correct node) or \`mismatched\` (wrong node).
|
||||
3. If none of the nodes match, call \`create\` to make a new node.
|
||||
|
||||
**Organizational patterns:**
|
||||
- Journal entries: Journal/YYYY-MM-DD
|
||||
- People: People/Name
|
||||
- Projects: Projects/Name
|
||||
- Personal: Personal/Goals, Personal/Tasks, etc.
|
||||
- Knowledge: Biology/Topic, History/Topic, etc.
|
||||
|
||||
Available nodes:
|
||||
${candidates.map(n => `- ${n.name}: ${n.description}`).join('\n') || 'None — create a new node.'}`,
|
||||
tools: [{
|
||||
name: 'read',
|
||||
description: 'Read a node file to verify it is the right home for these facts',
|
||||
args: {name: {type: 'string', description: 'Exact node name (full path)', required: true}},
|
||||
fn: ({name}) => {
|
||||
const mem = memories.find(m => m.name === name);
|
||||
if (!mem) return 'Node not found';
|
||||
home = name;
|
||||
return this.formatMemory(mem);
|
||||
}
|
||||
}, {
|
||||
name: 'confirm',
|
||||
description: 'Confirm this is the correct node for the facts',
|
||||
args: {},
|
||||
fn: () => {
|
||||
mode = 'success';
|
||||
resp.abort();
|
||||
}
|
||||
}, {
|
||||
name: 'mismatched',
|
||||
description: 'This is not the node you are looking for',
|
||||
args: {},
|
||||
fn: () => {
|
||||
mode = 'failed';
|
||||
resp.abort();
|
||||
}
|
||||
}, {
|
||||
name: 'create',
|
||||
description: 'No existing node fits — create a new one',
|
||||
args: {
|
||||
name: {type: 'string', description: 'Full path for the new node (e.g., "People/Sarah", "Journal/2025-01-27")', required: true}
|
||||
},
|
||||
fn: ({name}) => {
|
||||
home = name;
|
||||
mode = 'create';
|
||||
resp.abort();
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
if(mode === 'create') {
|
||||
return this.createNode(home, memories);
|
||||
} else if (mode === 'failed') {
|
||||
candidates = candidates.filter(c => c.name !== home);
|
||||
if(!candidates.length) return this.createNode(bucket.subject, memories);
|
||||
} else if (mode === 'success') {
|
||||
const existing = memories.find(m => m.name === home);
|
||||
return existing || this.createNode(home, memories);
|
||||
}
|
||||
}
|
||||
return this.createNode(bucket.subject, memories);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {findByProp, objectMap, JSONSanitize, JSONAttemptParse, clean} from '@zti
|
||||
import {AbortablePromise, Ai} from './ai.ts';
|
||||
import {LLMMessage, LLMRequest} from './llm.ts';
|
||||
import {LLMProvider} from './provider.ts';
|
||||
import {convertSchema} from './tools.ts';
|
||||
|
||||
export class OpenAi extends LLMProvider {
|
||||
client!: openAI;
|
||||
@@ -11,7 +12,7 @@ export class OpenAi extends LLMProvider {
|
||||
super();
|
||||
this.client = new openAI(clean({
|
||||
baseURL: host,
|
||||
apiKey: token || host ? 'ignored' : undefined
|
||||
apiKey: token || (host ? 'ignored' : undefined)
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -64,7 +65,7 @@ export class OpenAi extends LLMProvider {
|
||||
}, [] as any[]);
|
||||
}
|
||||
|
||||
ask(message: string, options: LLMRequest = {}): AbortablePromise<string> {
|
||||
ask(message: string, options: LLMRequest = {}): AbortablePromise<string | any> {
|
||||
const controller = new AbortController();
|
||||
return Object.assign(new Promise<any>(async (res, rej) => {
|
||||
if(options.system) {
|
||||
@@ -77,8 +78,8 @@ export class OpenAi extends LLMProvider {
|
||||
model: options.model || this.model,
|
||||
messages: history,
|
||||
stream: !!options.stream,
|
||||
max_tokens: options.max_tokens || this.ai.options.llm?.max_tokens || 4096,
|
||||
temperature: options.temperature || this.ai.options.llm?.temperature || 0.7,
|
||||
max_completion_tokens: options.max_tokens || this.ai.options.llm?.max_tokens || undefined,
|
||||
temperature: options.temperature || this.ai.options.llm?.temperature || undefined,
|
||||
tools: tools.map(t => ({
|
||||
type: 'function',
|
||||
function: {
|
||||
@@ -93,6 +94,18 @@ export class OpenAi extends LLMProvider {
|
||||
}))
|
||||
};
|
||||
|
||||
if(options.schema) {
|
||||
const schema = convertSchema(options.schema);
|
||||
requestParams.response_format = {
|
||||
type: 'json_schema',
|
||||
json_schema: {
|
||||
name: 'response',
|
||||
strict: true,
|
||||
schema
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let resp: any, isFirstMessage = true;
|
||||
do {
|
||||
resp = await this.client.chat.completions.create(requestParams).catch(err => {
|
||||
@@ -158,12 +171,17 @@ export class OpenAi extends LLMProvider {
|
||||
requestParams.messages = history;
|
||||
}
|
||||
} while (!controller.signal.aborted && resp.choices?.[0]?.message?.tool_calls?.length);
|
||||
history.push({role: 'assistant', content: resp.choices[0].message.content.trim() || ''});
|
||||
|
||||
const textContent = resp.choices[0].message.content?.trim() || '';
|
||||
history.push({role: 'assistant', content: textContent});
|
||||
history = this.toStandard(history);
|
||||
|
||||
if(options.stream) options.stream({done: true});
|
||||
if(options.history) options.history.splice(0, options.history.length, ...history);
|
||||
res(history.at(-1)?.content);
|
||||
|
||||
// Return parsed JSON if schema provided
|
||||
const finalContent = history.at(-1)?.content;
|
||||
res(options.schema ? JSONAttemptParse(finalContent, finalContent) : finalContent);
|
||||
}), {abort: () => controller.abort()});
|
||||
}
|
||||
}
|
||||
|
||||
151
src/tools.ts
151
src/tools.ts
@@ -1,6 +1,6 @@
|
||||
import * as cheerio from 'cheerio';
|
||||
import {$Sync} from '@ztimson/node-utils';
|
||||
import {ASet, consoleInterceptor, Http, fn as Fn, decodeHtml} from '@ztimson/utils';
|
||||
import {ASet, consoleInterceptor, Http, fn as Fn, decodeHtml, objectMap} from '@ztimson/utils';
|
||||
import * as os from 'node:os';
|
||||
import {Ai} from './ai.ts';
|
||||
import {LLMRequest} from './llm.ts';
|
||||
@@ -44,6 +44,53 @@ export type AiTool = {
|
||||
fn: (args: any, stream: LLMRequest['stream'], ai: Ai) => any | Promise<any>,
|
||||
};
|
||||
|
||||
export function convertSchema(schema: any): any {
|
||||
if(!schema) return null;
|
||||
|
||||
const convertProp = (prop: any): any => {
|
||||
const converted: any = {
|
||||
type: prop.type || 'string',
|
||||
};
|
||||
|
||||
if(prop.description) converted.description = prop.description;
|
||||
if(prop.default !== undefined) converted.default = prop.default;
|
||||
if(prop.enum) converted.enum = prop.enum;
|
||||
if(prop.pattern) converted.pattern = prop.pattern;
|
||||
|
||||
// Handle array items
|
||||
if(prop.type === 'array' && prop.items) {
|
||||
converted.items = convertProp(prop.items);
|
||||
}
|
||||
|
||||
// Handle object properties
|
||||
if(prop.type === 'object' && prop.items) {
|
||||
converted.properties = objectMap(prop.items, (key, value) => convertProp(value));
|
||||
const required = Object.entries(prop.items).filter(([_, v]: any) => v.required).map(([k]) => k);
|
||||
if(required.length) converted.required = required;
|
||||
converted.additionalProperties = false;
|
||||
}
|
||||
|
||||
// Handle min/max based on type
|
||||
if(prop.min !== undefined) {
|
||||
if(prop.type === 'string' || prop.type === 'array') converted.minLength = prop.min;
|
||||
else converted.minimum = prop.min;
|
||||
}
|
||||
if(prop.max !== undefined) {
|
||||
if(prop.type === 'string' || prop.type === 'array') converted.maxLength = prop.max;
|
||||
else converted.maximum = prop.max;
|
||||
}
|
||||
|
||||
return converted;
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'object',
|
||||
properties: objectMap(schema, (key, value) => convertProp(value)),
|
||||
required: Object.entries(schema).filter(([_, v]: any) => v.required).map(([k]) => k),
|
||||
additionalProperties: false
|
||||
};
|
||||
}
|
||||
|
||||
export const CliTool: AiTool = {
|
||||
name: 'cli',
|
||||
description: 'Use the command line interface, returns any output',
|
||||
@@ -53,16 +100,11 @@ export const CliTool: AiTool = {
|
||||
|
||||
export const DateTimeTool: AiTool = {
|
||||
name: 'get_datetime',
|
||||
description: 'Get local date / time',
|
||||
args: {},
|
||||
fn: async () => new Date().toString()
|
||||
}
|
||||
|
||||
export const DateTimeUTCTool: AiTool = {
|
||||
name: 'get_datetime_utc',
|
||||
description: 'Get current UTC date / time',
|
||||
args: {},
|
||||
fn: async () => new Date().toUTCString()
|
||||
description: 'Get local/UTC date/time',
|
||||
args: {
|
||||
timezone: {type: 'string', description: 'Which timezone to return, defaults to local', enum: ['local', 'utc'], default: 'local'}
|
||||
},
|
||||
fn: ({timezone}) => new Date()[timezone === 'local' ? 'toString' : 'toUTCString']()
|
||||
}
|
||||
|
||||
export const ExecTool: AiTool = {
|
||||
@@ -121,7 +163,7 @@ export const JSTool: AiTool = {
|
||||
}
|
||||
|
||||
export const PythonTool: AiTool = {
|
||||
name: 'exec_javascript',
|
||||
name: 'exec_python',
|
||||
description: 'Execute commonjs javascript',
|
||||
args: {
|
||||
code: {type: 'string', description: 'CommonJS javascript', required: true}
|
||||
@@ -259,93 +301,90 @@ export const WebSearchTool: AiTool = {
|
||||
}
|
||||
}
|
||||
|
||||
class WikipediaClient {
|
||||
private async get(url: string): Promise<any> {
|
||||
export const WikipediaTool: AiTool = {
|
||||
name: 'wikipedia_search',
|
||||
description: 'Search Wikipedia for matching articles',
|
||||
args: {
|
||||
query: {type: 'string', description: 'Search term or article title', required: true},
|
||||
mode: {type: 'string', description: 'search - look for articles, summary - intro of first found article (default), full - complete first found article', enum: ['search', 'summary', 'full'], default: 'summary'}
|
||||
},
|
||||
fn: async (args: {query: string, mode: 'search' | 'summary' | 'full'}) => {
|
||||
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)';
|
||||
|
||||
class WikipediaClient {
|
||||
async get(url: string) {
|
||||
const resp = await fetch(url, {headers: {'User-Agent': UA}});
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
private api(params: Record<string, any>): Promise<any> {
|
||||
api(params: any) {
|
||||
const qs = new URLSearchParams({...params, format: 'json', utf8: '1'}).toString();
|
||||
return this.get(`https://en.wikipedia.org/w/api.php?${qs}`);
|
||||
}
|
||||
|
||||
private clean(text: string): string {
|
||||
return text.replace(/\n{3,}/g, '\n\n').replace(/ {2,}/g, ' ').replace(/\[\d+\]/g, '').trim();
|
||||
clean(text: string) {
|
||||
const cutoffs = ['== See also ==', '== References ==', '== Bibliography ==', '== External links =='];
|
||||
for (const marker of cutoffs) {
|
||||
const idx = text.indexOf(marker);
|
||||
if (idx !== -1) text = text.slice(0, idx);
|
||||
}
|
||||
|
||||
private truncate(text: string, max: number): string {
|
||||
if(text.length <= max) return text;
|
||||
const cut = text.slice(0, max);
|
||||
const lastPara = cut.lastIndexOf('\n\n');
|
||||
return lastPara > max * 0.7 ? cut.slice(0, lastPara) : cut;
|
||||
return text
|
||||
.replace(/^={4}\s*(.+?)\s*={4}$/gm, '#### $1')
|
||||
.replace(/^={3}\s*(.+?)\s*={3}$/gm, '### $1')
|
||||
.replace(/^={2}\s*(.+?)\s*={2}$/gm, '## $1')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.replace(/ {2,}/g, ' ')
|
||||
.replace(/\[\d+\]/g, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
private async searchTitles(query: string, limit = 6): Promise<any[]> {
|
||||
async searchTitles(query: string, limit = 6) {
|
||||
const data = await this.api({action: 'query', list: 'search', srsearch: query, srlimit: limit, srprop: 'snippet'});
|
||||
return data.query?.search || [];
|
||||
}
|
||||
|
||||
private async fetchExtract(title: string, intro = false): Promise<string> {
|
||||
async fetchExtract(title: string, introOnly = false) {
|
||||
const params: any = {action: 'query', prop: 'extracts', titles: title, explaintext: 1, redirects: 1};
|
||||
if(intro) params.exintro = 1;
|
||||
if(introOnly) params.exintro = 1;
|
||||
const data = await this.api(params);
|
||||
const page = Object.values(data.query?.pages || {})[0] as any;
|
||||
const page: any = Object.values(data.query?.pages || {})[0];
|
||||
return this.clean(page?.extract || '');
|
||||
}
|
||||
|
||||
private pageUrl(title: string): string {
|
||||
pageUrl(title: string) {
|
||||
return `https://en.wikipedia.org/wiki/${encodeURIComponent(title.replace(/ /g, '_'))}`;
|
||||
}
|
||||
|
||||
private stripHtml(text: string): string {
|
||||
stripHtml(text: string) {
|
||||
return text.replace(/<[^>]+>/g, '');
|
||||
}
|
||||
|
||||
async lookup(query: string, detail: 'intro' | 'full' = 'intro'): Promise<string> {
|
||||
async lookup(query: string, detail = 'summary') {
|
||||
const results = await this.searchTitles(query, 6);
|
||||
if(!results.length) return `❌ No Wikipedia articles found for "${query}"`;
|
||||
const title = results[0].title;
|
||||
const url = this.pageUrl(title);
|
||||
const content = await this.fetchExtract(title, detail === 'intro');
|
||||
const text = this.truncate(content, detail === 'intro' ? 2000 : 8000);
|
||||
return `## ${title}\n🔗 ${url}\n\n${text}`;
|
||||
const introOnly = detail !== 'full';
|
||||
const content = await this.fetchExtract(title, introOnly);
|
||||
return `## ${title}\n🔗 ${url}\n\n${content}`;
|
||||
}
|
||||
|
||||
async search(query: string): Promise<string> {
|
||||
async search(query: string) {
|
||||
const results = await this.searchTitles(query, 8);
|
||||
if(!results.length) return `❌ No results for "${query}"`;
|
||||
const lines = [`### Search results for "${query}"\n`];
|
||||
for(let i = 0; i < results.length; i++) {
|
||||
const r = results[i];
|
||||
const snippet = this.truncate(this.stripHtml(r.snippet || ''), 150);
|
||||
const snippet = this.stripHtml(r.snippet || '').trim();
|
||||
lines.push(`**${i + 1}. ${r.title}**\n${snippet}\n${this.pageUrl(r.title)}`);
|
||||
}
|
||||
return lines.join('\n\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const WikipediaLookupTool: AiTool = {
|
||||
name: 'wikipedia_lookup',
|
||||
description: 'Get Wikipedia article content',
|
||||
args: {
|
||||
query: {type: 'string', description: 'Topic or article title', required: true},
|
||||
detail: {type: 'string', description: 'Content level: "intro" (summary, default) or "full" (complete article)', enum: ['intro', 'full'], default: 'intro'}
|
||||
},
|
||||
fn: async (args: {query: string; detail?: 'intro' | 'full'}) => {
|
||||
const wiki = new WikipediaClient();
|
||||
return wiki.lookup(args.query, args.detail || 'intro');
|
||||
}
|
||||
};
|
||||
|
||||
export const WikipediaSearchTool: AiTool = {
|
||||
name: 'wikipedia_search',
|
||||
description: 'Search Wikipedia for matching articles',
|
||||
args: {
|
||||
query: {type: 'string', description: 'Search terms', required: true}
|
||||
},
|
||||
fn: async (args: {query: string}) => {
|
||||
const wiki = new WikipediaClient();
|
||||
return wiki.search(args.query);
|
||||
if(args.mode == 'search') return wiki.search(args.query);
|
||||
return wiki.lookup(args.query, args.mode || 'summary');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -12,12 +12,31 @@ export class Vision {
|
||||
*/
|
||||
ocr(path: string): AbortablePromise<string | null> {
|
||||
let worker: any;
|
||||
const p = new Promise<string | null>(async res => {
|
||||
let reject: (err: any) => void;
|
||||
|
||||
const handler = (err: Error) => {
|
||||
if(err.stack?.includes('tesseract.js')) {
|
||||
process.off('uncaughtException', handler);
|
||||
reject?.(err);
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
};
|
||||
process.on('uncaughtException', handler);
|
||||
|
||||
const p = (async () => {
|
||||
worker = await createWorker(this.ai.options.ocr || 'eng', 2, {cachePath: this.ai.options.path});
|
||||
const {data} = await worker.recognize(path);
|
||||
await worker.terminate();
|
||||
res(data.text.trim() || null);
|
||||
}).finally(() => worker?.terminate());
|
||||
return await new Promise<string | null>((res, rej) => {
|
||||
reject = rej;
|
||||
worker.recognize(path)
|
||||
.then(({data}: any) => res(data.text.trim() || null))
|
||||
.catch(rej);
|
||||
});
|
||||
})().finally(() => {
|
||||
process.off('uncaughtException', handler);
|
||||
worker?.terminate();
|
||||
});
|
||||
|
||||
return Object.assign(p, {abort: () => worker?.terminate()});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ESNext"],
|
||||
"lib": [
|
||||
"ESNext",
|
||||
"dom"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
|
||||
Reference in New Issue
Block a user