novac 2.0.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +1574 -597
- package/bin/novac +468 -171
- package/bin/nvc +522 -0
- package/bin/nvml +78 -17
- package/demo.nv +0 -0
- package/demo_builtins.nv +0 -0
- package/demo_http.nv +0 -0
- package/examples/bf.nv +69 -0
- package/examples/math.nv +21 -0
- package/kits/birdAPI/kitdef.js +954 -0
- package/kits/kitRNG/kitdef.js +740 -0
- package/kits/kitSSH/kitdef.js +1272 -0
- package/kits/kitadb/kitdef.js +606 -0
- package/kits/kitai/kitdef.js +2185 -0
- package/kits/kitansi/kitdef.js +1402 -0
- package/kits/kitcanvas/kitdef.js +914 -0
- package/kits/kitclippy/kitdef.js +925 -0
- package/kits/kitformat/kitdef.js +1485 -0
- package/kits/kitgps/kitdef.js +1862 -0
- package/kits/kitlibproc/kitdef.js +3 -2
- package/kits/kitmatrix/ex.js +19 -0
- package/kits/kitmatrix/kitdef.js +960 -0
- package/kits/kitmorse/kitdef.js +229 -0
- package/kits/kitmpatch/kitdef.js +906 -0
- package/kits/kitnet/kitdef.js +1401 -0
- package/kits/kitnovacweb/README.md +1416 -143
- package/kits/kitnovacweb/kitdef.js +92 -2
- package/kits/kitnovacweb/nvml/executor.js +578 -176
- package/kits/kitnovacweb/nvml/index.js +2 -2
- package/kits/kitnovacweb/nvml/lexer.js +72 -69
- package/kits/kitnovacweb/nvml/parser.js +328 -159
- package/kits/kitnovacweb/nvml/renderer.js +770 -270
- package/kits/kitparse/kitdef.js +1688 -0
- package/kits/kitproto/kitdef.js +613 -0
- package/kits/kitqr/kitdef.js +637 -0
- package/kits/kitregex++/kitdef.js +1353 -0
- package/kits/kitrequire/kitdef.js +1599 -0
- package/kits/kitx11/kitdef.js +1 -0
- package/kits/kitx11/kitx11.js +2472 -0
- package/kits/kitx11/kitx11_conn.js +948 -0
- package/kits/kitx11/kitx11_worker.js +121 -0
- package/kits/libtea/kitdef.js +2691 -0
- package/kits/libterm/ex.js +285 -0
- package/kits/libterm/kitdef.js +1927 -0
- package/novac/LICENSE +21 -0
- package/novac/README.md +1823 -0
- package/novac/bin/novac +950 -0
- package/novac/bin/nvc +522 -0
- package/novac/bin/nvml +542 -0
- package/novac/demo.nv +245 -0
- package/novac/demo_builtins.nv +209 -0
- package/novac/demo_http.nv +62 -0
- package/novac/examples/bf.nv +69 -0
- package/novac/examples/math.nv +21 -0
- package/novac/kits/kitai/kitdef.js +2185 -0
- package/novac/kits/kitansi/kitdef.js +1402 -0
- package/novac/kits/kitformat/kitdef.js +1485 -0
- package/novac/kits/kitgps/kitdef.js +1862 -0
- package/novac/kits/kitlibfs/kitdef.js +231 -0
- package/{examples/example-project/nova_modules → novac/kits}/kitlibproc/kitdef.js +3 -2
- package/novac/kits/kitmatrix/ex.js +19 -0
- package/novac/kits/kitmatrix/kitdef.js +960 -0
- package/novac/kits/kitmpatch/kitdef.js +906 -0
- package/novac/kits/kitnovacweb/README.md +1572 -0
- package/novac/kits/kitnovacweb/demo.nv +12 -0
- package/novac/kits/kitnovacweb/demo.nvml +71 -0
- package/novac/kits/kitnovacweb/index.nova +12 -0
- package/novac/kits/kitnovacweb/kitdef.js +692 -0
- package/novac/kits/kitnovacweb/nova.kit.json +8 -0
- package/novac/kits/kitnovacweb/nvml/executor.js +739 -0
- package/novac/kits/kitnovacweb/nvml/index.js +67 -0
- package/novac/kits/kitnovacweb/nvml/lexer.js +263 -0
- package/novac/kits/kitnovacweb/nvml/parser.js +508 -0
- package/novac/kits/kitnovacweb/nvml/renderer.js +924 -0
- package/novac/kits/kitparse/kitdef.js +1688 -0
- package/novac/kits/kitregex++/kitdef.js +1353 -0
- package/novac/kits/kitrequire/kitdef.js +1599 -0
- package/novac/kits/kitx11/kitdef.js +1 -0
- package/novac/kits/kitx11/kitx11.js +2472 -0
- package/novac/kits/kitx11/kitx11_conn.js +948 -0
- package/novac/kits/kitx11/kitx11_worker.js +121 -0
- package/novac/kits/libtea/tf.js +2691 -0
- package/novac/kits/libterm/ex.js +285 -0
- package/novac/kits/libterm/kitdef.js +1927 -0
- package/novac/node_modules/chalk/license +9 -0
- package/novac/node_modules/chalk/package.json +83 -0
- package/novac/node_modules/chalk/readme.md +297 -0
- package/novac/node_modules/chalk/source/index.d.ts +325 -0
- package/novac/node_modules/chalk/source/index.js +225 -0
- package/novac/node_modules/chalk/source/utilities.js +33 -0
- package/novac/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
- package/novac/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
- package/novac/node_modules/commander/LICENSE +22 -0
- package/novac/node_modules/commander/Readme.md +1176 -0
- package/novac/node_modules/commander/esm.mjs +16 -0
- package/novac/node_modules/commander/index.js +24 -0
- package/novac/node_modules/commander/lib/argument.js +150 -0
- package/novac/node_modules/commander/lib/command.js +2777 -0
- package/novac/node_modules/commander/lib/error.js +39 -0
- package/novac/node_modules/commander/lib/help.js +747 -0
- package/novac/node_modules/commander/lib/option.js +380 -0
- package/novac/node_modules/commander/lib/suggestSimilar.js +101 -0
- package/novac/node_modules/commander/package-support.json +19 -0
- package/novac/node_modules/commander/package.json +82 -0
- package/novac/node_modules/commander/typings/esm.d.mts +3 -0
- package/novac/node_modules/commander/typings/index.d.ts +1113 -0
- package/novac/node_modules/node-addon-api/LICENSE.md +9 -0
- package/novac/node_modules/node-addon-api/README.md +95 -0
- package/novac/node_modules/node-addon-api/common.gypi +21 -0
- package/novac/node_modules/node-addon-api/except.gypi +25 -0
- package/novac/node_modules/node-addon-api/index.js +14 -0
- package/novac/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
- package/novac/node_modules/node-addon-api/napi-inl.h +7165 -0
- package/novac/node_modules/node-addon-api/napi.h +3364 -0
- package/novac/node_modules/node-addon-api/node_addon_api.gyp +42 -0
- package/novac/node_modules/node-addon-api/node_api.gyp +9 -0
- package/novac/node_modules/node-addon-api/noexcept.gypi +26 -0
- package/novac/node_modules/node-addon-api/package-support.json +21 -0
- package/novac/node_modules/node-addon-api/package.json +480 -0
- package/novac/node_modules/node-addon-api/tools/README.md +73 -0
- package/novac/node_modules/node-addon-api/tools/check-napi.js +99 -0
- package/novac/node_modules/node-addon-api/tools/clang-format.js +71 -0
- package/novac/node_modules/node-addon-api/tools/conversion.js +301 -0
- package/novac/node_modules/serialize-javascript/LICENSE +27 -0
- package/novac/node_modules/serialize-javascript/README.md +149 -0
- package/novac/node_modules/serialize-javascript/index.js +297 -0
- package/novac/node_modules/serialize-javascript/package.json +33 -0
- package/novac/package.json +27 -0
- package/novac/scripts/update-bin.js +24 -0
- package/novac/src/core/bstd.js +1035 -0
- package/novac/src/core/config.js +155 -0
- package/novac/src/core/describe.js +187 -0
- package/novac/src/core/emitter.js +499 -0
- package/novac/src/core/error.js +86 -0
- package/novac/src/core/executor.js +5606 -0
- package/novac/src/core/formatter.js +686 -0
- package/novac/src/core/lexer.js +1026 -0
- package/novac/src/core/nova_builtins.js +717 -0
- package/novac/src/core/nova_thread_worker.js +166 -0
- package/novac/src/core/parser.js +2181 -0
- package/novac/src/core/types.js +112 -0
- package/novac/src/index.js +28 -0
- package/novac/src/runtime/stdlib.js +244 -0
- package/package.json +6 -3
- package/scripts/update-bin.js +0 -0
- package/src/core/bstd.js +838 -362
- package/src/core/executor.js +2578 -170
- package/src/core/lexer.js +502 -54
- package/src/core/nova_builtins.js +21 -3
- package/src/core/parser.js +413 -72
- package/src/core/types.js +30 -2
- package/src/index.js +0 -0
- package/examples/example-project/README.md +0 -3
- package/examples/example-project/src/main.nova +0 -3
- package/src/core/environment.js +0 -0
- /package/{examples/example-project/bin/example-project.nv → novac/node_modules/node-addon-api/nothing.c} +0 -0
|
@@ -0,0 +1,2185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KitAI — A Complete AI / LLM Toolkit Library
|
|
3
|
+
* Version : 1.0.0
|
|
4
|
+
* License : MIT
|
|
5
|
+
* Style : CJS, single file, zero runtime dependencies
|
|
6
|
+
*
|
|
7
|
+
* ╔══════════════════════════════════════════════════════════════════════╗
|
|
8
|
+
* ║ FEATURE MAP ║
|
|
9
|
+
* ╠══════════════════════════════════════════════════════════════════════╣
|
|
10
|
+
* ║ PROVIDER LAYER ║
|
|
11
|
+
* ║ 1. Multi-provider client (OpenAI, Anthropic, Gemini, Ollama, ║
|
|
12
|
+
* ║ Groq, Mistral, Together, Cohere, HuggingFace, custom) ║
|
|
13
|
+
* ║ 2. Provider auto-detect from API key prefix ║
|
|
14
|
+
* ║ 3. Streaming chat completions (SSE + async iterator) ║
|
|
15
|
+
* ║ 4. Non-streaming chat completions ║
|
|
16
|
+
* ║ 5. Image generation (DALL·E 3, Stable Diffusion, Flux) ║
|
|
17
|
+
* ║ 6. Audio transcription (Whisper) ║
|
|
18
|
+
* ║ 7. Text-to-speech synthesis ║
|
|
19
|
+
* ║ 8. Embeddings (text → float[] vector) ║
|
|
20
|
+
* ║ 9. Reranking (cross-encoder style) ║
|
|
21
|
+
* ║ 10. Model catalogue (80+ models with metadata) ║
|
|
22
|
+
* ║ ║
|
|
23
|
+
* ║ PROMPT ENGINEERING ║
|
|
24
|
+
* ║ 11. Prompt template engine ({var} + {{block}} + filters) ║
|
|
25
|
+
* ║ 12. Few-shot example builder ║
|
|
26
|
+
* ║ 13. System prompt library (30+ built-in personas/roles) ║
|
|
27
|
+
* ║ 14. Chain-of-thought / ReAct prompt scaffolds ║
|
|
28
|
+
* ║ 15. Prompt compression (trim to token budget) ║
|
|
29
|
+
* ║ 16. Prompt scoring & critique (self-critique loop) ║
|
|
30
|
+
* ║ ║
|
|
31
|
+
* ║ MEMORY & CONTEXT ║
|
|
32
|
+
* ║ 17. Sliding-window conversation memory ║
|
|
33
|
+
* ║ 18. Token-budget memory (auto-trim oldest turns) ║
|
|
34
|
+
* ║ 19. Summary memory (compress history into summary) ║
|
|
35
|
+
* ║ 20. Vector store (in-memory cosine-similarity search) ║
|
|
36
|
+
* ║ 21. RAG pipeline (chunk → embed → retrieve → augment) ║
|
|
37
|
+
* ║ 22. Document chunker (fixed, sentence, paragraph, semantic) ║
|
|
38
|
+
* ║ ║
|
|
39
|
+
* ║ CHAIN & AGENT ║
|
|
40
|
+
* ║ 23. Sequential chain (pipe output of one call into next) ║
|
|
41
|
+
* ║ 24. Parallel chain (fan-out + merge) ║
|
|
42
|
+
* ║ 25. Conditional chain (route by classifier) ║
|
|
43
|
+
* ║ 26. Tool/function-calling agent (ReAct loop) ║
|
|
44
|
+
* ║ 27. Built-in tools: calculator, date/time, web-search stub, ║
|
|
45
|
+
* ║ JSON-path extractor, regex, base64, UUID, hash ║
|
|
46
|
+
* ║ 28. Tool registry (register / unregister / list) ║
|
|
47
|
+
* ║ ║
|
|
48
|
+
* ║ STRUCTURED OUTPUT ║
|
|
49
|
+
* ║ 29. JSON schema enforcer (retry-until-valid) ║
|
|
50
|
+
* ║ 30. Zod-style schema builder (kitai.schema) ║
|
|
51
|
+
* ║ 31. Output parsers: JSON, CSV, list, key-value, markdown table ║
|
|
52
|
+
* ║ 32. Extraction (pull typed fields from free text) ║
|
|
53
|
+
* ║ 33. Classification (zero-shot, few-shot, chain-of-thought) ║
|
|
54
|
+
* ║ ║
|
|
55
|
+
* ║ EVALUATION & SAFETY ║
|
|
56
|
+
* ║ 34. LLM-as-judge eval (correctness, relevance, coherence) ║
|
|
57
|
+
* ║ 35. BLEU / ROUGE-L / exact-match / F1 metrics (offline) ║
|
|
58
|
+
* ║ 36. Hallucination detector (grounding check) ║
|
|
59
|
+
* ║ 37. Toxicity / PII / prompt-injection guard ║
|
|
60
|
+
* ║ 38. Retry + exponential-backoff + fallback chain ║
|
|
61
|
+
* ║ ║
|
|
62
|
+
* ║ TOKENIZER & COUNTING ║
|
|
63
|
+
* ║ 39. Token estimator (tiktoken-compatible cl100k approximation) ║
|
|
64
|
+
* ║ 40. Cost estimator (per-model $/1M token pricing table) ║
|
|
65
|
+
* ║ 41. Context-window advisor ║
|
|
66
|
+
* ║ ║
|
|
67
|
+
* ║ UTILITIES ║
|
|
68
|
+
* ║ 42. Diff viewer (old vs new LLM response) ║
|
|
69
|
+
* ║ 43. Markdown → plain-text stripper ║
|
|
70
|
+
* ║ 44. Response cache (in-memory LRU, hash-keyed) ║
|
|
71
|
+
* ║ 45. Conversation export (JSON / Markdown / HTML) ║
|
|
72
|
+
* ║ 46. Streaming aggregator (chunks → full message) ║
|
|
73
|
+
* ║ 47. Multi-turn role validator ║
|
|
74
|
+
* ║ 48. Semantic similarity (cosine on embeddings) ║
|
|
75
|
+
* ║ 49. A/B experiment runner ║
|
|
76
|
+
* ║ 50. KitAIFormat — the core message/response object ║
|
|
77
|
+
* ╚══════════════════════════════════════════════════════════════════════╝
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
'use strict';
|
|
81
|
+
|
|
82
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
83
|
+
// §0 Internal helpers
|
|
84
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
const _noop = () => {};
|
|
87
|
+
const _sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
88
|
+
const _isNode = typeof process !== 'undefined' && process.versions && process.versions.node;
|
|
89
|
+
|
|
90
|
+
/** Minimal fetch shim (Node ≥18 has fetch built-in; older Node uses https) */
|
|
91
|
+
function _fetch(url, opts = {}) {
|
|
92
|
+
if (typeof fetch !== 'undefined') return fetch(url, opts);
|
|
93
|
+
if (_isNode) {
|
|
94
|
+
const mod = url.startsWith('https') ? require('https') : require('http');
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
const body = opts.body ? Buffer.from(opts.body) : null;
|
|
97
|
+
const u = new URL(url);
|
|
98
|
+
const req = mod.request({
|
|
99
|
+
hostname: u.hostname, port: u.port, path: u.pathname + u.search,
|
|
100
|
+
method: opts.method || 'GET',
|
|
101
|
+
headers: { 'Content-Type':'application/json', ...(opts.headers||{}),
|
|
102
|
+
...(body ? {'Content-Length': body.length} : {}) },
|
|
103
|
+
}, res => {
|
|
104
|
+
const chunks = [];
|
|
105
|
+
res.on('data', c => chunks.push(c));
|
|
106
|
+
res.on('end', () => {
|
|
107
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
108
|
+
resolve({
|
|
109
|
+
ok: res.statusCode >= 200 && res.statusCode < 300,
|
|
110
|
+
status: res.statusCode,
|
|
111
|
+
headers: { get: k => res.headers[k.toLowerCase()] },
|
|
112
|
+
text: () => Promise.resolve(text),
|
|
113
|
+
json: () => Promise.resolve(JSON.parse(text)),
|
|
114
|
+
body: { getReader: () => {
|
|
115
|
+
let done = false;
|
|
116
|
+
return {
|
|
117
|
+
read: () => { if(done) return Promise.resolve({done:true,value:undefined});
|
|
118
|
+
done=true; return Promise.resolve({done:false,value:Buffer.from(text)}); }
|
|
119
|
+
};
|
|
120
|
+
}},
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
res.on('error', reject);
|
|
124
|
+
});
|
|
125
|
+
req.on('error', reject);
|
|
126
|
+
if (body) req.write(body);
|
|
127
|
+
req.end();
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
throw new Error('KitAI: no fetch implementation available');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function _md5Hex(str) {
|
|
134
|
+
// FNV-1a 64-bit approximation – fast, not crypto
|
|
135
|
+
let h1=0x811c9dc5, h2=0x811c9dc5;
|
|
136
|
+
for (let i=0;i<str.length;i++) {
|
|
137
|
+
const c=str.charCodeAt(i);
|
|
138
|
+
h1^=c; h1=(h1*0x01000193)>>>0;
|
|
139
|
+
h2^=(c<<8); h2=(h2*0x01000193)>>>0;
|
|
140
|
+
}
|
|
141
|
+
return (h1>>>0).toString(16).padStart(8,'0')+(h2>>>0).toString(16).padStart(8,'0');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function _deepClone(obj) { return JSON.parse(JSON.stringify(obj)); }
|
|
145
|
+
|
|
146
|
+
function _cosineSim(a, b) {
|
|
147
|
+
if (a.length !== b.length) return 0;
|
|
148
|
+
let dot=0, na=0, nb=0;
|
|
149
|
+
for (let i=0;i<a.length;i++) { dot+=a[i]*b[i]; na+=a[i]*a[i]; nb+=b[i]*b[i]; }
|
|
150
|
+
return dot / (Math.sqrt(na)*Math.sqrt(nb) || 1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function _dotProduct(a, b) {
|
|
154
|
+
let s=0; for (let i=0;i<a.length;i++) s+=a[i]*b[i]; return s;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function _norm(v) {
|
|
158
|
+
const mag = Math.sqrt(v.reduce((s,x)=>s+x*x,0));
|
|
159
|
+
return v.map(x => x/mag);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
163
|
+
// §1 KitAIFormat — core response object
|
|
164
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
class KitAIFormat {
|
|
167
|
+
/**
|
|
168
|
+
* @param {object} opts
|
|
169
|
+
* @param {string} opts.content – text content of the response
|
|
170
|
+
* @param {string} [opts.role] – 'assistant' | 'user' | 'system' | 'tool'
|
|
171
|
+
* @param {string} [opts.model] – model id that produced this
|
|
172
|
+
* @param {object} [opts.usage] – { promptTokens, completionTokens, totalTokens }
|
|
173
|
+
* @param {any[]} [opts.toolCalls] – raw tool_calls array if any
|
|
174
|
+
* @param {any} [opts.raw] – full provider response
|
|
175
|
+
* @param {number} [opts.latencyMs] – round-trip latency
|
|
176
|
+
* @param {string} [opts.finishReason]
|
|
177
|
+
*/
|
|
178
|
+
constructor({ content='', role='assistant', model='', usage={}, toolCalls=[], raw=null, latencyMs=0, finishReason='stop' } = {}) {
|
|
179
|
+
this.content = content;
|
|
180
|
+
this.role = role;
|
|
181
|
+
this.model = model;
|
|
182
|
+
this.usage = { promptTokens:0, completionTokens:0, totalTokens:0, ...usage };
|
|
183
|
+
this.toolCalls = toolCalls;
|
|
184
|
+
this.raw = raw;
|
|
185
|
+
this.latencyMs = latencyMs;
|
|
186
|
+
this.finishReason = finishReason;
|
|
187
|
+
this.createdAt = Date.now();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── Parsers ────────────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
/** Parse content as JSON (throws on failure) */
|
|
193
|
+
toJSON() { return JSON.parse(this.content); }
|
|
194
|
+
|
|
195
|
+
/** Best-effort JSON parse (returns null on failure) */
|
|
196
|
+
tryJSON() { try { return this.toJSON(); } catch { return null; } }
|
|
197
|
+
|
|
198
|
+
/** Split content into a trimmed string array by line */
|
|
199
|
+
toLines() { return this.content.split('\n').map(l=>l.trim()).filter(Boolean); }
|
|
200
|
+
|
|
201
|
+
/** Extract bullet / numbered list items */
|
|
202
|
+
toList() {
|
|
203
|
+
return this.content.split('\n')
|
|
204
|
+
.map(l => l.replace(/^[-*•]\s+|^\d+[.)]\s+/,'').trim())
|
|
205
|
+
.filter(Boolean);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Strip markdown formatting → plain text */
|
|
209
|
+
toPlainText() { return kitai.stripMarkdown(this.content); }
|
|
210
|
+
|
|
211
|
+
/** Extract all ```lang ... ``` code blocks */
|
|
212
|
+
toCodeBlocks() {
|
|
213
|
+
const blocks = [], re = /```(\w*)\n?([\s\S]*?)```/g;
|
|
214
|
+
let m;
|
|
215
|
+
while ((m=re.exec(this.content))!==null)
|
|
216
|
+
blocks.push({ lang: m[1]||'text', code: m[2].trim() });
|
|
217
|
+
return blocks;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Extract key: value pairs from content */
|
|
221
|
+
toKeyValue() {
|
|
222
|
+
const map = {};
|
|
223
|
+
for (const line of this.toLines()) {
|
|
224
|
+
const i = line.indexOf(':');
|
|
225
|
+
if (i>0) map[line.slice(0,i).trim()] = line.slice(i+1).trim();
|
|
226
|
+
}
|
|
227
|
+
return map;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Parse a markdown table into array of objects */
|
|
231
|
+
toTable() { return kitai.parsers.markdownTable(this.content); }
|
|
232
|
+
|
|
233
|
+
/** Return as a Message object (for use in next conversation turn) */
|
|
234
|
+
toMessage() { return { role: this.role, content: this.content }; }
|
|
235
|
+
|
|
236
|
+
/** Cost in USD based on model pricing */
|
|
237
|
+
estimatedCost() { return kitai.cost.estimate(this.model, this.usage.promptTokens, this.usage.completionTokens); }
|
|
238
|
+
|
|
239
|
+
/** Human-readable summary line */
|
|
240
|
+
summary() {
|
|
241
|
+
const tok = this.usage.totalTokens ? `${this.usage.totalTokens} tok` : '';
|
|
242
|
+
const lat = this.latencyMs ? `${this.latencyMs}ms` : '';
|
|
243
|
+
const cost = this.estimatedCost() ? `$${this.estimatedCost().toFixed(6)}` : '';
|
|
244
|
+
const info = [this.model, tok, lat, cost].filter(Boolean).join(' · ');
|
|
245
|
+
return `[${this.role}] ${this.content.slice(0,80)}${this.content.length>80?'…':''} (${info})`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
toString() { return this.content; }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
252
|
+
// §2 Provider registry & adapters
|
|
253
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
const _PROVIDERS = {
|
|
256
|
+
openai: {
|
|
257
|
+
baseURL: 'https://api.openai.com/v1',
|
|
258
|
+
authHeader: key => ({ Authorization: `Bearer ${key}` }),
|
|
259
|
+
chatPath: '/chat/completions',
|
|
260
|
+
embedPath: '/embeddings',
|
|
261
|
+
imagePath: '/images/generations',
|
|
262
|
+
audioPath: '/audio/transcriptions',
|
|
263
|
+
ttsPath: '/audio/speech',
|
|
264
|
+
models: ['gpt-4o','gpt-4o-mini','gpt-4-turbo','gpt-3.5-turbo','gpt-4o-2024-11-20'],
|
|
265
|
+
parseChat: r => ({ content: r.choices[0].message.content, toolCalls: r.choices[0].message.tool_calls||[],
|
|
266
|
+
usage: { promptTokens:r.usage?.prompt_tokens||0, completionTokens:r.usage?.completion_tokens||0,
|
|
267
|
+
totalTokens:r.usage?.total_tokens||0 }, finishReason: r.choices[0].finish_reason }),
|
|
268
|
+
parseEmbed: r => r.data[0].embedding,
|
|
269
|
+
parseChunk: line => {
|
|
270
|
+
if (!line.startsWith('data:')) return null;
|
|
271
|
+
const d = line.slice(5).trim();
|
|
272
|
+
if (d==='[DONE]') return null;
|
|
273
|
+
try { const j=JSON.parse(d); return j.choices[0]?.delta?.content||''; } catch { return ''; }
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
anthropic: {
|
|
278
|
+
baseURL: 'https://api.anthropic.com/v1',
|
|
279
|
+
authHeader: key => ({ 'x-api-key': key, 'anthropic-version': '2023-06-01' }),
|
|
280
|
+
chatPath: '/messages',
|
|
281
|
+
embedPath: null,
|
|
282
|
+
imagePath: null,
|
|
283
|
+
models: ['claude-opus-4-20250514','claude-sonnet-4-5','claude-haiku-4-5','claude-3-opus-20240229'],
|
|
284
|
+
buildBody: (msgs, model, opts) => {
|
|
285
|
+
const sys = msgs.find(m=>m.role==='system');
|
|
286
|
+
const conv= msgs.filter(m=>m.role!=='system');
|
|
287
|
+
return { model, messages: conv, max_tokens: opts.maxTokens||1024,
|
|
288
|
+
...(sys ? {system: sys.content} : {}), ...(opts.temperature!=null?{temperature:opts.temperature}:{}) };
|
|
289
|
+
},
|
|
290
|
+
parseChat: r => ({ content: r.content[0].text, toolCalls: r.content.filter(b=>b.type==='tool_use'),
|
|
291
|
+
usage: { promptTokens:r.usage?.input_tokens||0, completionTokens:r.usage?.output_tokens||0,
|
|
292
|
+
totalTokens:(r.usage?.input_tokens||0)+(r.usage?.output_tokens||0) },
|
|
293
|
+
finishReason: r.stop_reason }),
|
|
294
|
+
parseChunk: line => {
|
|
295
|
+
if (!line.startsWith('data:')) return null;
|
|
296
|
+
const d=line.slice(5).trim();
|
|
297
|
+
try { const j=JSON.parse(d); return j.type==='content_block_delta'?j.delta?.text||'':j.type==='message_stop'?null:''; }
|
|
298
|
+
catch { return ''; }
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
gemini: {
|
|
303
|
+
baseURL: 'https://generativelanguage.googleapis.com/v1beta',
|
|
304
|
+
authHeader: () => ({}),
|
|
305
|
+
chatPathFn: (model,key) => `/models/${model}:generateContent?key=${key}`,
|
|
306
|
+
models: ['gemini-1.5-pro','gemini-1.5-flash','gemini-2.0-flash','gemini-pro'],
|
|
307
|
+
buildBody: (msgs, _model, opts) => ({
|
|
308
|
+
contents: msgs.filter(m=>m.role!=='system').map(m=>({ role:m.role==='assistant'?'model':'user', parts:[{text:m.content}] })),
|
|
309
|
+
generationConfig: { maxOutputTokens: opts.maxTokens||1024, temperature: opts.temperature??0.7 },
|
|
310
|
+
}),
|
|
311
|
+
parseChat: r => ({ content: r.candidates[0].content.parts[0].text, toolCalls: [],
|
|
312
|
+
usage: { promptTokens:r.usageMetadata?.promptTokenCount||0,
|
|
313
|
+
completionTokens:r.usageMetadata?.candidatesTokenCount||0,
|
|
314
|
+
totalTokens:r.usageMetadata?.totalTokenCount||0 }, finishReason:'stop' }),
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
ollama: {
|
|
318
|
+
baseURL: 'http://localhost:11434/v1',
|
|
319
|
+
authHeader: () => ({}),
|
|
320
|
+
chatPath: '/chat/completions',
|
|
321
|
+
embedPath: '/embeddings',
|
|
322
|
+
models: ['llama3','mistral','phi3','gemma2','qwen2','deepseek-coder'],
|
|
323
|
+
parseChat: r => ({ content: r.choices[0].message.content, toolCalls: [],
|
|
324
|
+
usage: { promptTokens:r.usage?.prompt_tokens||0, completionTokens:r.usage?.completion_tokens||0,
|
|
325
|
+
totalTokens:r.usage?.total_tokens||0 }, finishReason:'stop' }),
|
|
326
|
+
parseEmbed: r => r.data?.[0]?.embedding || r.embedding || [],
|
|
327
|
+
parseChunk: line => {
|
|
328
|
+
if (!line.startsWith('data:')) return null;
|
|
329
|
+
const d=line.slice(5).trim(); if(d==='[DONE]') return null;
|
|
330
|
+
try { return JSON.parse(d).choices[0]?.delta?.content||''; } catch { return ''; }
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
groq: {
|
|
335
|
+
baseURL: 'https://api.groq.com/openai/v1',
|
|
336
|
+
authHeader: key => ({ Authorization: `Bearer ${key}` }),
|
|
337
|
+
chatPath: '/chat/completions',
|
|
338
|
+
embedPath: '/openai/v1/embeddings',
|
|
339
|
+
models: ['llama-3.1-70b-versatile','llama-3.1-8b-instant','mixtral-8x7b-32768','gemma2-9b-it'],
|
|
340
|
+
parseChat: r => ({ content: r.choices[0].message.content, toolCalls: r.choices[0].message.tool_calls||[],
|
|
341
|
+
usage: { promptTokens:r.usage?.prompt_tokens||0, completionTokens:r.usage?.completion_tokens||0,
|
|
342
|
+
totalTokens:r.usage?.total_tokens||0 }, finishReason:r.choices[0].finish_reason }),
|
|
343
|
+
parseEmbed: r => r.data[0].embedding,
|
|
344
|
+
parseChunk: line => {
|
|
345
|
+
if (!line.startsWith('data:')) return null;
|
|
346
|
+
const d=line.slice(5).trim(); if(d==='[DONE]') return null;
|
|
347
|
+
try { return JSON.parse(d).choices[0]?.delta?.content||''; } catch { return ''; }
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
|
|
351
|
+
mistral: {
|
|
352
|
+
baseURL: 'https://api.mistral.ai/v1',
|
|
353
|
+
authHeader: key => ({ Authorization: `Bearer ${key}` }),
|
|
354
|
+
chatPath: '/chat/completions',
|
|
355
|
+
embedPath: '/embeddings',
|
|
356
|
+
models: ['mistral-large-latest','mistral-medium-latest','mistral-small-latest','codestral-latest','open-mixtral-8x22b'],
|
|
357
|
+
parseChat: r => ({ content: r.choices[0].message.content, toolCalls: r.choices[0].message.tool_calls||[],
|
|
358
|
+
usage: { promptTokens:r.usage?.prompt_tokens||0, completionTokens:r.usage?.completion_tokens||0,
|
|
359
|
+
totalTokens:r.usage?.total_tokens||0 }, finishReason:r.choices[0].finish_reason }),
|
|
360
|
+
parseEmbed: r => r.data[0].embedding,
|
|
361
|
+
parseChunk: line => {
|
|
362
|
+
if (!line.startsWith('data:')) return null;
|
|
363
|
+
const d=line.slice(5).trim(); if(d==='[DONE]') return null;
|
|
364
|
+
try { return JSON.parse(d).choices[0]?.delta?.content||''; } catch { return ''; }
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
|
|
368
|
+
cohere: {
|
|
369
|
+
baseURL: 'https://api.cohere.com/v2',
|
|
370
|
+
authHeader: key => ({ Authorization: `Bearer ${key}` }),
|
|
371
|
+
chatPath: '/chat',
|
|
372
|
+
embedPath: '/embed',
|
|
373
|
+
rerankPath: '/rerank',
|
|
374
|
+
models: ['command-r-plus','command-r','command','command-light'],
|
|
375
|
+
buildBody: (msgs, model, opts) => ({
|
|
376
|
+
model, messages: msgs.map(m=>({role:m.role,content:m.content})),
|
|
377
|
+
max_tokens: opts.maxTokens||1024,
|
|
378
|
+
}),
|
|
379
|
+
parseChat: r => ({ content: r.message?.content?.[0]?.text||r.text||'', toolCalls:[],
|
|
380
|
+
usage: { promptTokens:r.meta?.tokens?.input_tokens||0,
|
|
381
|
+
completionTokens:r.meta?.tokens?.output_tokens||0,
|
|
382
|
+
totalTokens:(r.meta?.tokens?.input_tokens||0)+(r.meta?.tokens?.output_tokens||0) },
|
|
383
|
+
finishReason:'stop' }),
|
|
384
|
+
parseEmbed: r => r.embeddings?.float?.[0] || r.embeddings?.[0] || [],
|
|
385
|
+
},
|
|
386
|
+
|
|
387
|
+
together: {
|
|
388
|
+
baseURL: 'https://api.together.xyz/v1',
|
|
389
|
+
authHeader: key => ({ Authorization: `Bearer ${key}` }),
|
|
390
|
+
chatPath: '/chat/completions',
|
|
391
|
+
embedPath: '/embeddings',
|
|
392
|
+
models: ['meta-llama/Llama-3-70b-chat-hf','mistralai/Mixtral-8x7B-Instruct-v0.1','Qwen/Qwen2-72B-Instruct'],
|
|
393
|
+
parseChat: r => ({ content: r.choices[0].message.content, toolCalls:[],
|
|
394
|
+
usage: { promptTokens:r.usage?.prompt_tokens||0, completionTokens:r.usage?.completion_tokens||0,
|
|
395
|
+
totalTokens:r.usage?.total_tokens||0 }, finishReason:r.choices[0].finish_reason }),
|
|
396
|
+
parseEmbed: r => r.data[0].embedding,
|
|
397
|
+
parseChunk: line => {
|
|
398
|
+
if (!line.startsWith('data:')) return null;
|
|
399
|
+
const d=line.slice(5).trim(); if(d==='[DONE]') return null;
|
|
400
|
+
try { return JSON.parse(d).choices[0]?.delta?.content||''; } catch { return ''; }
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
406
|
+
// §3 Model catalogue (80+ entries)
|
|
407
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
408
|
+
|
|
409
|
+
const _MODELS = {
|
|
410
|
+
// OpenAI
|
|
411
|
+
'gpt-4o': { provider:'openai', context:128000, family:'gpt-4', type:'chat', inputPer1M:2.50, outputPer1M:10.00 },
|
|
412
|
+
'gpt-4o-mini': { provider:'openai', context:128000, family:'gpt-4', type:'chat', inputPer1M:0.15, outputPer1M:0.60 },
|
|
413
|
+
'gpt-4-turbo': { provider:'openai', context:128000, family:'gpt-4', type:'chat', inputPer1M:10.00, outputPer1M:30.00 },
|
|
414
|
+
'gpt-4': { provider:'openai', context:8192, family:'gpt-4', type:'chat', inputPer1M:30.00, outputPer1M:60.00 },
|
|
415
|
+
'gpt-3.5-turbo': { provider:'openai', context:16385, family:'gpt-3.5', type:'chat', inputPer1M:0.50, outputPer1M:1.50 },
|
|
416
|
+
'o1': { provider:'openai', context:200000, family:'o1', type:'chat', inputPer1M:15.00, outputPer1M:60.00 },
|
|
417
|
+
'o1-mini': { provider:'openai', context:128000, family:'o1', type:'chat', inputPer1M:3.00, outputPer1M:12.00 },
|
|
418
|
+
'o3-mini': { provider:'openai', context:200000, family:'o3', type:'chat', inputPer1M:1.10, outputPer1M:4.40 },
|
|
419
|
+
'text-embedding-3-small':{ provider:'openai', context:8191, family:'embed', type:'embed', inputPer1M:0.02, outputPer1M:0 },
|
|
420
|
+
'text-embedding-3-large':{ provider:'openai', context:8191, family:'embed', type:'embed', inputPer1M:0.13, outputPer1M:0 },
|
|
421
|
+
'dall-e-3': { provider:'openai', context:0, family:'image', type:'image', inputPer1M:0, outputPer1M:0 },
|
|
422
|
+
'whisper-1': { provider:'openai', context:0, family:'audio', type:'audio', inputPer1M:0, outputPer1M:0 },
|
|
423
|
+
'tts-1': { provider:'openai', context:0, family:'tts', type:'tts', inputPer1M:15.00, outputPer1M:0 },
|
|
424
|
+
'tts-1-hd': { provider:'openai', context:0, family:'tts', type:'tts', inputPer1M:30.00, outputPer1M:0 },
|
|
425
|
+
|
|
426
|
+
// Anthropic
|
|
427
|
+
'claude-opus-4-20250514':{ provider:'anthropic',context:200000,family:'claude-4', type:'chat', inputPer1M:15.00, outputPer1M:75.00 },
|
|
428
|
+
'claude-sonnet-4-5': { provider:'anthropic', context:200000, family:'claude-4', type:'chat', inputPer1M:3.00, outputPer1M:15.00 },
|
|
429
|
+
'claude-haiku-4-5': { provider:'anthropic', context:200000, family:'claude-4', type:'chat', inputPer1M:0.80, outputPer1M:4.00 },
|
|
430
|
+
'claude-3-opus-20240229':{ provider:'anthropic',context:200000,family:'claude-3', type:'chat', inputPer1M:15.00, outputPer1M:75.00 },
|
|
431
|
+
'claude-3-5-sonnet-20241022':{ provider:'anthropic',context:200000,family:'claude-3',type:'chat',inputPer1M:3.00, outputPer1M:15.00 },
|
|
432
|
+
'claude-3-haiku-20240307':{ provider:'anthropic',context:200000,family:'claude-3',type:'chat', inputPer1M:0.25, outputPer1M:1.25 },
|
|
433
|
+
|
|
434
|
+
// Google Gemini
|
|
435
|
+
'gemini-1.5-pro': { provider:'gemini', context:2000000,family:'gemini', type:'chat', inputPer1M:1.25, outputPer1M:5.00 },
|
|
436
|
+
'gemini-1.5-flash': { provider:'gemini', context:1000000,family:'gemini', type:'chat', inputPer1M:0.075, outputPer1M:0.30 },
|
|
437
|
+
'gemini-2.0-flash': { provider:'gemini', context:1048576,family:'gemini', type:'chat', inputPer1M:0.10, outputPer1M:0.40 },
|
|
438
|
+
'gemini-pro': { provider:'gemini', context:32760, family:'gemini', type:'chat', inputPer1M:0.50, outputPer1M:1.50 },
|
|
439
|
+
|
|
440
|
+
// Groq
|
|
441
|
+
'llama-3.1-70b-versatile':{ provider:'groq', context:32768, family:'llama', type:'chat', inputPer1M:0.59, outputPer1M:0.79 },
|
|
442
|
+
'llama-3.1-8b-instant':{ provider:'groq', context:131072, family:'llama', type:'chat', inputPer1M:0.05, outputPer1M:0.08 },
|
|
443
|
+
'mixtral-8x7b-32768': { provider:'groq', context:32768, family:'mixtral', type:'chat', inputPer1M:0.24, outputPer1M:0.24 },
|
|
444
|
+
'gemma2-9b-it': { provider:'groq', context:8192, family:'gemma', type:'chat', inputPer1M:0.20, outputPer1M:0.20 },
|
|
445
|
+
|
|
446
|
+
// Mistral
|
|
447
|
+
'mistral-large-latest':{ provider:'mistral', context:131072, family:'mistral', type:'chat', inputPer1M:2.00, outputPer1M:6.00 },
|
|
448
|
+
'mistral-medium-latest':{ provider:'mistral', context:131072, family:'mistral', type:'chat', inputPer1M:2.70, outputPer1M:8.10 },
|
|
449
|
+
'mistral-small-latest':{ provider:'mistral', context:32768, family:'mistral', type:'chat', inputPer1M:0.20, outputPer1M:0.60 },
|
|
450
|
+
'codestral-latest': { provider:'mistral', context:32768, family:'codestral',type:'chat', inputPer1M:0.20, outputPer1M:0.60 },
|
|
451
|
+
'open-mixtral-8x22b': { provider:'mistral', context:65536, family:'mixtral', type:'chat', inputPer1M:2.00, outputPer1M:6.00 },
|
|
452
|
+
'mistral-embed': { provider:'mistral', context:8192, family:'embed', type:'embed', inputPer1M:0.10, outputPer1M:0 },
|
|
453
|
+
|
|
454
|
+
// Cohere
|
|
455
|
+
'command-r-plus': { provider:'cohere', context:128000, family:'command', type:'chat', inputPer1M:2.50, outputPer1M:10.00 },
|
|
456
|
+
'command-r': { provider:'cohere', context:128000, family:'command', type:'chat', inputPer1M:0.15, outputPer1M:0.60 },
|
|
457
|
+
'command': { provider:'cohere', context:4096, family:'command', type:'chat', inputPer1M:1.00, outputPer1M:2.00 },
|
|
458
|
+
'embed-english-v3.0': { provider:'cohere', context:512, family:'embed', type:'embed', inputPer1M:0.10, outputPer1M:0 },
|
|
459
|
+
'embed-multilingual-v3.0':{ provider:'cohere', context:512, family:'embed', type:'embed', inputPer1M:0.10, outputPer1M:0 },
|
|
460
|
+
|
|
461
|
+
// Together
|
|
462
|
+
'meta-llama/Llama-3-70b-chat-hf':{ provider:'together',context:8192,family:'llama',type:'chat',inputPer1M:0.90,outputPer1M:0.90},
|
|
463
|
+
'Qwen/Qwen2-72B-Instruct': { provider:'together',context:32768,family:'qwen',type:'chat',inputPer1M:0.90,outputPer1M:0.90},
|
|
464
|
+
|
|
465
|
+
// Ollama (local – free)
|
|
466
|
+
'llama3': { provider:'ollama', context:8192, family:'llama', type:'chat', inputPer1M:0, outputPer1M:0 },
|
|
467
|
+
'mistral': { provider:'ollama', context:32768, family:'mistral', type:'chat', inputPer1M:0, outputPer1M:0 },
|
|
468
|
+
'phi3': { provider:'ollama', context:131072, family:'phi', type:'chat', inputPer1M:0, outputPer1M:0 },
|
|
469
|
+
'gemma2': { provider:'ollama', context:8192, family:'gemma', type:'chat', inputPer1M:0, outputPer1M:0 },
|
|
470
|
+
'qwen2': { provider:'ollama', context:32768, family:'qwen', type:'chat', inputPer1M:0, outputPer1M:0 },
|
|
471
|
+
'deepseek-coder': { provider:'ollama', context:16384, family:'deepseek', type:'chat', inputPer1M:0, outputPer1M:0 },
|
|
472
|
+
'nomic-embed-text': { provider:'ollama', context:8192, family:'embed', type:'embed', inputPer1M:0, outputPer1M:0 },
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
476
|
+
// §4 Tokenizer (cl100k_base approximation — no wasm required)
|
|
477
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
478
|
+
|
|
479
|
+
const tokenizer = (() => {
|
|
480
|
+
// Approximation: GPT-4 averages ~4 chars/token for English prose.
|
|
481
|
+
// We use a slightly smarter heuristic that accounts for whitespace splits,
|
|
482
|
+
// punctuation, numbers, CJK, code identifiers, etc.
|
|
483
|
+
function estimate(text) {
|
|
484
|
+
if (!text) return 0;
|
|
485
|
+
let count = 0;
|
|
486
|
+
// Coarse split by whitespace first
|
|
487
|
+
const words = text.split(/\s+/);
|
|
488
|
+
for (const w of words) {
|
|
489
|
+
if (!w) continue;
|
|
490
|
+
// Each word costs at least 1 token
|
|
491
|
+
// Long words (~7+ chars) typically split into multiple tokens
|
|
492
|
+
count += Math.ceil(w.length / 4.5);
|
|
493
|
+
// Punctuation tacked onto words adds tokens
|
|
494
|
+
const puncts = (w.match(/[^\w]/g)||[]).length;
|
|
495
|
+
count += Math.ceil(puncts * 0.5);
|
|
496
|
+
}
|
|
497
|
+
// CJK characters are generally 1 char = 1 token
|
|
498
|
+
const cjk = (text.match(/[\u4e00-\u9fff\u3040-\u30ff\uAC00-\uD7AF]/g)||[]).length;
|
|
499
|
+
count += cjk;
|
|
500
|
+
return Math.max(1, Math.round(count));
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function estimateMessages(messages) {
|
|
504
|
+
// ~4 tokens per message overhead (role + separators)
|
|
505
|
+
return messages.reduce((s,m) => s + estimate(m.content||'') + 4, 3);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function maxForModel(modelId) {
|
|
509
|
+
return (_MODELS[modelId]||{}).context || 4096;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function fitsInContext(messages, modelId, reserveTokens=512) {
|
|
513
|
+
const used = estimateMessages(messages);
|
|
514
|
+
const max = maxForModel(modelId);
|
|
515
|
+
return { fits: used + reserveTokens <= max, used, max, remaining: max - used };
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return { estimate, estimateMessages, maxForModel, fitsInContext };
|
|
519
|
+
})();
|
|
520
|
+
|
|
521
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
522
|
+
// §5 Cost estimator
|
|
523
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
524
|
+
|
|
525
|
+
const cost = {
|
|
526
|
+
/**
|
|
527
|
+
* Estimate cost in USD.
|
|
528
|
+
* @param {string} modelId
|
|
529
|
+
* @param {number} promptTokens
|
|
530
|
+
* @param {number} completionTokens
|
|
531
|
+
* @returns {number} USD
|
|
532
|
+
*/
|
|
533
|
+
estimate(modelId, promptTokens=0, completionTokens=0) {
|
|
534
|
+
const m = _MODELS[modelId];
|
|
535
|
+
if (!m) return 0;
|
|
536
|
+
return (promptTokens * m.inputPer1M + completionTokens * m.outputPer1M) / 1_000_000;
|
|
537
|
+
},
|
|
538
|
+
|
|
539
|
+
/** Return full pricing table */
|
|
540
|
+
table() {
|
|
541
|
+
return Object.entries(_MODELS).map(([id,m]) => ({
|
|
542
|
+
id, provider:m.provider, family:m.family, type:m.type,
|
|
543
|
+
contextK: Math.round(m.context/1000),
|
|
544
|
+
inputPer1M: m.inputPer1M, outputPer1M: m.outputPer1M,
|
|
545
|
+
}));
|
|
546
|
+
},
|
|
547
|
+
|
|
548
|
+
/** Compare cost across multiple models for the same workload */
|
|
549
|
+
compare(promptTokens, completionTokens, modelIds) {
|
|
550
|
+
return modelIds
|
|
551
|
+
.map(id => ({ id, usd: cost.estimate(id, promptTokens, completionTokens) }))
|
|
552
|
+
.sort((a,b) => a.usd - b.usd);
|
|
553
|
+
},
|
|
554
|
+
|
|
555
|
+
/** Cheapest model for a given type ('chat'|'embed'|'image') */
|
|
556
|
+
cheapest(type='chat') {
|
|
557
|
+
return Object.entries(_MODELS)
|
|
558
|
+
.filter(([,m]) => m.type===type && m.inputPer1M > 0)
|
|
559
|
+
.sort((a,b) => a[1].inputPer1M - b[1].inputPer1M)[0]?.[0] || null;
|
|
560
|
+
},
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
564
|
+
// §6 LRU Response Cache
|
|
565
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
566
|
+
|
|
567
|
+
class LRUCache {
|
|
568
|
+
constructor(maxSize=256) {
|
|
569
|
+
this._max = maxSize;
|
|
570
|
+
this._map = new Map();
|
|
571
|
+
}
|
|
572
|
+
_key(messages, model, opts) {
|
|
573
|
+
return _md5Hex(JSON.stringify({ messages, model, opts }));
|
|
574
|
+
}
|
|
575
|
+
get(messages, model, opts) {
|
|
576
|
+
const k = this._key(messages, model, opts);
|
|
577
|
+
if (!this._map.has(k)) return null;
|
|
578
|
+
const v = this._map.get(k);
|
|
579
|
+
this._map.delete(k); this._map.set(k,v); // promote
|
|
580
|
+
return v;
|
|
581
|
+
}
|
|
582
|
+
set(messages, model, opts, value) {
|
|
583
|
+
const k = this._key(messages, model, opts);
|
|
584
|
+
if (this._map.size >= this._max) this._map.delete(this._map.keys().next().value);
|
|
585
|
+
this._map.set(k, value);
|
|
586
|
+
}
|
|
587
|
+
clear() { this._map.clear(); }
|
|
588
|
+
get size() { return this._map.size; }
|
|
589
|
+
invalidate(messages, model, opts) { this._map.delete(this._key(messages, model, opts)); }
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
593
|
+
// §7 Core client
|
|
594
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
595
|
+
|
|
596
|
+
class KitAIClient {
|
|
597
|
+
/**
|
|
598
|
+
* @param {object} config
|
|
599
|
+
* @param {string} config.apiKey
|
|
600
|
+
* @param {string} [config.provider] – auto-detected if omitted
|
|
601
|
+
* @param {string} [config.model] – default model
|
|
602
|
+
* @param {string} [config.baseURL] – override base URL
|
|
603
|
+
* @param {number} [config.maxTokens] – default max_tokens
|
|
604
|
+
* @param {number} [config.temperature]
|
|
605
|
+
* @param {number} [config.maxRetries]
|
|
606
|
+
* @param {boolean}[config.cache] – enable response cache
|
|
607
|
+
* @param {object} [config.headers] – extra headers
|
|
608
|
+
*/
|
|
609
|
+
constructor(config = {}) {
|
|
610
|
+
this.apiKey = config.apiKey || '';
|
|
611
|
+
this.provider = config.provider || kitai.detectProvider(this.apiKey);
|
|
612
|
+
this.model = config.model || '';
|
|
613
|
+
this._baseURL = config.baseURL || '';
|
|
614
|
+
this.maxTokens = config.maxTokens ?? 1024;
|
|
615
|
+
this.temperature = config.temperature ?? 0.7;
|
|
616
|
+
this.maxRetries = config.maxRetries ?? 3;
|
|
617
|
+
this._cache = config.cache ? new LRUCache() : null;
|
|
618
|
+
this._headers = config.headers || {};
|
|
619
|
+
this._prov = _PROVIDERS[this.provider] || _PROVIDERS.openai;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
_baseUrl() { return this._baseURL || this._prov.baseURL; }
|
|
623
|
+
|
|
624
|
+
_headers_(extra={}) {
|
|
625
|
+
const auth = this._prov.authHeader(this.apiKey);
|
|
626
|
+
return { 'Content-Type':'application/json', ...auth, ...this._headers, ...extra };
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
_buildBody(messages, model, opts={}) {
|
|
630
|
+
if (this._prov.buildBody) return this._prov.buildBody(messages, model, { maxTokens:this.maxTokens, temperature:this.temperature, ...opts });
|
|
631
|
+
const body = {
|
|
632
|
+
model,
|
|
633
|
+
messages,
|
|
634
|
+
max_tokens: opts.maxTokens ?? this.maxTokens,
|
|
635
|
+
temperature: opts.temperature ?? this.temperature,
|
|
636
|
+
};
|
|
637
|
+
if (opts.tools) body.tools = opts.tools;
|
|
638
|
+
if (opts.jsonMode) body.response_format = { type:'json_object' };
|
|
639
|
+
if (opts.stream) body.stream = true;
|
|
640
|
+
return body;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/** Non-streaming chat completion */
|
|
644
|
+
async chat(messages, opts = {}) {
|
|
645
|
+
const model = opts.model || this.model || this._prov.models?.[0] || 'gpt-4o-mini';
|
|
646
|
+
|
|
647
|
+
// Cache lookup
|
|
648
|
+
if (this._cache && !opts.noCache) {
|
|
649
|
+
const hit = this._cache.get(messages, model, opts);
|
|
650
|
+
if (hit) return hit;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const t0 = Date.now();
|
|
654
|
+
let lastErr;
|
|
655
|
+
for (let attempt=0; attempt<=this.maxRetries; attempt++) {
|
|
656
|
+
try {
|
|
657
|
+
let path, url;
|
|
658
|
+
if (this.provider==='gemini' && this._prov.chatPathFn) {
|
|
659
|
+
path = this._prov.chatPathFn(model, this.apiKey);
|
|
660
|
+
url = `${this._baseUrl()}${path}`;
|
|
661
|
+
} else {
|
|
662
|
+
url = `${this._baseUrl()}${this._prov.chatPath}`;
|
|
663
|
+
}
|
|
664
|
+
const res = await _fetch(url, {
|
|
665
|
+
method: 'POST',
|
|
666
|
+
headers: this._headers_(),
|
|
667
|
+
body: JSON.stringify(this._buildBody(messages, model, opts)),
|
|
668
|
+
});
|
|
669
|
+
if (!res.ok) {
|
|
670
|
+
const txt = await res.text();
|
|
671
|
+
throw new Error(`HTTP ${res.status}: ${txt.slice(0,200)}`);
|
|
672
|
+
}
|
|
673
|
+
const raw = await res.json();
|
|
674
|
+
const parsed = this._prov.parseChat(raw);
|
|
675
|
+
const result = new KitAIFormat({ ...parsed, model, raw, latencyMs: Date.now()-t0 });
|
|
676
|
+
if (this._cache) this._cache.set(messages, model, opts, result);
|
|
677
|
+
return result;
|
|
678
|
+
} catch(e) {
|
|
679
|
+
lastErr = e;
|
|
680
|
+
if (attempt < this.maxRetries) await _sleep(Math.min(1000*2**attempt, 30000));
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
throw lastErr;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/** Streaming chat — returns an async generator yielding string chunks */
|
|
687
|
+
async *stream(messages, opts = {}) {
|
|
688
|
+
const model = opts.model || this.model || this._prov.models?.[0] || 'gpt-4o-mini';
|
|
689
|
+
const url = `${this._baseUrl()}${this._prov.chatPath}`;
|
|
690
|
+
const res = await _fetch(url, {
|
|
691
|
+
method: 'POST',
|
|
692
|
+
headers: this._headers_(),
|
|
693
|
+
body: JSON.stringify(this._buildBody(messages, model, { ...opts, stream:true })),
|
|
694
|
+
});
|
|
695
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
696
|
+
|
|
697
|
+
// Browser ReadableStream
|
|
698
|
+
if (res.body && res.body.getReader) {
|
|
699
|
+
const reader = res.body.getReader();
|
|
700
|
+
const dec = typeof TextDecoder !== 'undefined' ? new TextDecoder() : null;
|
|
701
|
+
let buf = '';
|
|
702
|
+
while (true) {
|
|
703
|
+
const { done, value } = await reader.read();
|
|
704
|
+
if (done) break;
|
|
705
|
+
buf += dec ? dec.decode(value, {stream:true}) : value.toString();
|
|
706
|
+
const lines = buf.split('\n');
|
|
707
|
+
buf = lines.pop();
|
|
708
|
+
for (const line of lines) {
|
|
709
|
+
const chunk = this._prov.parseChunk?.(line);
|
|
710
|
+
if (chunk === null) return;
|
|
711
|
+
if (chunk) yield chunk;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
} else {
|
|
715
|
+
// Node HTTP response arrives as complete text — simulate streaming
|
|
716
|
+
const text = typeof res.text === 'function' ? await res.text() : String(res.body);
|
|
717
|
+
for (const line of text.split('\n')) {
|
|
718
|
+
const chunk = this._prov.parseChunk?.(line);
|
|
719
|
+
if (chunk === null) return;
|
|
720
|
+
if (chunk) yield chunk;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/** Collect an entire stream into a KitAIFormat */
|
|
726
|
+
async streamCollect(messages, opts={}) {
|
|
727
|
+
const t0 = Date.now();
|
|
728
|
+
const model = opts.model || this.model || this._prov.models?.[0] || 'gpt-4o-mini';
|
|
729
|
+
let content = '';
|
|
730
|
+
for await (const chunk of this.stream(messages, opts)) content += chunk;
|
|
731
|
+
const tokens = tokenizer.estimate(content);
|
|
732
|
+
return new KitAIFormat({ content, model, latencyMs: Date.now()-t0,
|
|
733
|
+
usage:{ completionTokens: tokens, promptTokens: tokenizer.estimateMessages(messages), totalTokens: tokenizer.estimateMessages(messages)+tokens } });
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/** Generate embeddings */
|
|
737
|
+
async embed(input, opts={}) {
|
|
738
|
+
const model = opts.model || 'text-embedding-3-small';
|
|
739
|
+
const inputs = Array.isArray(input) ? input : [input];
|
|
740
|
+
const path = this._prov.embedPath;
|
|
741
|
+
if (!path) throw new Error(`Provider ${this.provider} does not support embeddings`);
|
|
742
|
+
const body = this.provider==='cohere'
|
|
743
|
+
? { model, texts: inputs, input_type:'search_document', embedding_types:['float'] }
|
|
744
|
+
: { model, input: inputs.length===1 ? inputs[0] : inputs };
|
|
745
|
+
const res = await _fetch(`${this._baseUrl()}${path}`, {
|
|
746
|
+
method:'POST', headers:this._headers_(), body:JSON.stringify(body),
|
|
747
|
+
});
|
|
748
|
+
if (!res.ok) throw new Error(`Embed HTTP ${res.status}`);
|
|
749
|
+
const raw = await res.json();
|
|
750
|
+
const vec = this._prov.parseEmbed(raw);
|
|
751
|
+
return Array.isArray(vec[0]) ? vec : [vec];
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/** Generate an image, returns URL or base64 */
|
|
755
|
+
async imageGenerate(prompt, opts={}) {
|
|
756
|
+
const model = opts.model || 'dall-e-3';
|
|
757
|
+
const body = { model, prompt, n:opts.n||1, size:opts.size||'1024x1024',
|
|
758
|
+
response_format: opts.responseFormat||'url', quality:opts.quality||'standard' };
|
|
759
|
+
const res = await _fetch(`${this._baseUrl()}${this._prov.imagePath}`, {
|
|
760
|
+
method:'POST', headers:this._headers_(), body:JSON.stringify(body),
|
|
761
|
+
});
|
|
762
|
+
if (!res.ok) throw new Error(`Image HTTP ${res.status}`);
|
|
763
|
+
const raw = await res.json();
|
|
764
|
+
return raw.data.map(d => d.url || d.b64_json);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/** Synthesize speech, returns audio buffer (ArrayBuffer or Buffer) */
|
|
768
|
+
async textToSpeech(text, opts={}) {
|
|
769
|
+
const body = { model: opts.model||'tts-1', input:text, voice:opts.voice||'alloy',
|
|
770
|
+
response_format: opts.format||'mp3', speed: opts.speed||1.0 };
|
|
771
|
+
const res = await _fetch(`${this._baseUrl()}${this._prov.ttsPath}`, {
|
|
772
|
+
method:'POST', headers:this._headers_(), body:JSON.stringify(body),
|
|
773
|
+
});
|
|
774
|
+
if (!res.ok) throw new Error(`TTS HTTP ${res.status}`);
|
|
775
|
+
return res.arrayBuffer ? res.arrayBuffer() : res.buffer();
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
780
|
+
// §8 Memory systems
|
|
781
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
782
|
+
|
|
783
|
+
class SlidingWindowMemory {
|
|
784
|
+
/** @param {number} maxTurns – number of full turns (user+assistant pairs) to keep */
|
|
785
|
+
constructor(maxTurns=10) {
|
|
786
|
+
this._max = maxTurns * 2;
|
|
787
|
+
this._msgs = [];
|
|
788
|
+
this._system = null;
|
|
789
|
+
}
|
|
790
|
+
setSystem(content) { this._system = content; return this; }
|
|
791
|
+
add(role, content) { this._msgs.push({role,content}); this._trim(); return this; }
|
|
792
|
+
addUser(content) { return this.add('user',content); }
|
|
793
|
+
addAssistant(content) { return this.add('assistant',content); }
|
|
794
|
+
addResult(res) { return this.addAssistant(res.content||String(res)); }
|
|
795
|
+
_trim() { while(this._msgs.length > this._max) this._msgs.shift(); }
|
|
796
|
+
messages() {
|
|
797
|
+
const sys = this._system ? [{role:'system',content:this._system}] : [];
|
|
798
|
+
return [...sys, ...this._msgs];
|
|
799
|
+
}
|
|
800
|
+
clear() { this._msgs=[]; return this; }
|
|
801
|
+
get length() { return this._msgs.length; }
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
class TokenBudgetMemory {
|
|
805
|
+
/** @param {number} budget – max tokens for the message history */
|
|
806
|
+
constructor(budget=3000, model='gpt-4o-mini') {
|
|
807
|
+
this._budget = budget;
|
|
808
|
+
this._model = model;
|
|
809
|
+
this._msgs = [];
|
|
810
|
+
this._system = null;
|
|
811
|
+
}
|
|
812
|
+
setSystem(content) { this._system = content; return this; }
|
|
813
|
+
add(role, content) { this._msgs.push({role,content}); this._trim(); return this; }
|
|
814
|
+
addUser(content) { return this.add('user',content); }
|
|
815
|
+
addAssistant(content) { return this.add('assistant',content); }
|
|
816
|
+
addResult(res) { return this.addAssistant(res.content||String(res)); }
|
|
817
|
+
_trim() {
|
|
818
|
+
while(tokenizer.estimateMessages(this.messages()) > this._budget && this._msgs.length > 1)
|
|
819
|
+
this._msgs.shift();
|
|
820
|
+
}
|
|
821
|
+
messages() {
|
|
822
|
+
const sys = this._system ? [{role:'system',content:this._system}] : [];
|
|
823
|
+
return [...sys, ...this._msgs];
|
|
824
|
+
}
|
|
825
|
+
clear() { this._msgs=[]; return this; }
|
|
826
|
+
tokenUsage(){ return tokenizer.estimateMessages(this.messages()); }
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
class SummaryMemory {
|
|
830
|
+
/**
|
|
831
|
+
* When history exceeds `triggerTurns` turns, the client is asked to summarise
|
|
832
|
+
* older messages, which are then replaced with a single "summary" system note.
|
|
833
|
+
* @param {KitAIClient} client
|
|
834
|
+
* @param {number} triggerTurns
|
|
835
|
+
*/
|
|
836
|
+
constructor(client, triggerTurns=10) {
|
|
837
|
+
this._client = client;
|
|
838
|
+
this._trigger = triggerTurns * 2;
|
|
839
|
+
this._msgs = [];
|
|
840
|
+
this._summary = '';
|
|
841
|
+
this._system = '';
|
|
842
|
+
}
|
|
843
|
+
setSystem(content) { this._system=content; return this; }
|
|
844
|
+
add(role,content) { this._msgs.push({role,content}); return this; }
|
|
845
|
+
addUser(content) { return this.add('user',content); }
|
|
846
|
+
addAssistant(content) { return this.add('assistant',content); }
|
|
847
|
+
addResult(res) { return this.addAssistant(res.content||String(res)); }
|
|
848
|
+
async messages() {
|
|
849
|
+
if (this._msgs.length >= this._trigger) await this._summarise();
|
|
850
|
+
const parts = [];
|
|
851
|
+
if (this._system) parts.push({role:'system', content:this._system});
|
|
852
|
+
if (this._summary) parts.push({role:'system', content:`Conversation so far:\n${this._summary}`});
|
|
853
|
+
return [...parts, ...this._msgs];
|
|
854
|
+
}
|
|
855
|
+
async _summarise() {
|
|
856
|
+
const toCompress = this._msgs.splice(0, Math.floor(this._trigger/2));
|
|
857
|
+
const prompt = `Summarise this conversation concisely:\n${toCompress.map(m=>`${m.role}: ${m.content}`).join('\n')}`;
|
|
858
|
+
const res = await this._client.chat([{role:'user',content:prompt}]);
|
|
859
|
+
this._summary = (this._summary ? this._summary+'\n' : '') + res.content;
|
|
860
|
+
}
|
|
861
|
+
clear() { this._msgs=[]; this._summary=''; return this; }
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
865
|
+
// §9 Vector store (in-memory cosine similarity)
|
|
866
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
867
|
+
|
|
868
|
+
class VectorStore {
|
|
869
|
+
constructor() {
|
|
870
|
+
this._docs = []; // [{id, text, embedding, metadata}]
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/** Add a document with its embedding */
|
|
874
|
+
add(id, text, embedding, metadata={}) {
|
|
875
|
+
this._docs.push({ id, text, embedding: _norm(embedding), metadata });
|
|
876
|
+
return this;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/** Add many documents at once */
|
|
880
|
+
addMany(docs) {
|
|
881
|
+
for (const d of docs) this.add(d.id||`doc_${this._docs.length}`, d.text, d.embedding, d.metadata);
|
|
882
|
+
return this;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/** Cosine similarity search */
|
|
886
|
+
search(queryEmbedding, topK=5, filter=null) {
|
|
887
|
+
const q = _norm(queryEmbedding);
|
|
888
|
+
return this._docs
|
|
889
|
+
.filter(d => filter ? filter(d.metadata) : true)
|
|
890
|
+
.map(d => ({ ...d, score: _dotProduct(q, d.embedding) }))
|
|
891
|
+
.sort((a,b) => b.score - a.score)
|
|
892
|
+
.slice(0, topK);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/** Remove by id */
|
|
896
|
+
remove(id) { this._docs = this._docs.filter(d=>d.id!==id); return this; }
|
|
897
|
+
|
|
898
|
+
/** Update metadata */
|
|
899
|
+
updateMetadata(id, meta) {
|
|
900
|
+
const d = this._docs.find(d=>d.id===id);
|
|
901
|
+
if (d) d.metadata = { ...d.metadata, ...meta };
|
|
902
|
+
return this;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
get size() { return this._docs.length; }
|
|
906
|
+
clear() { this._docs=[]; return this; }
|
|
907
|
+
|
|
908
|
+
/** Export as JSON-serialisable array */
|
|
909
|
+
export() { return this._docs.map(d=>({ ...d, embedding: Array.from(d.embedding) })); }
|
|
910
|
+
|
|
911
|
+
/** Import from export() */
|
|
912
|
+
static import(data) {
|
|
913
|
+
const vs = new VectorStore();
|
|
914
|
+
for (const d of data) vs.add(d.id, d.text, d.embedding, d.metadata);
|
|
915
|
+
return vs;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
920
|
+
// §10 Document chunker
|
|
921
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
922
|
+
|
|
923
|
+
const chunker = {
|
|
924
|
+
/** Fixed-size character chunks with optional overlap */
|
|
925
|
+
fixed(text, size=500, overlap=50) {
|
|
926
|
+
const chunks=[];
|
|
927
|
+
for(let i=0;i<text.length;i+=size-overlap)
|
|
928
|
+
chunks.push(text.slice(i,i+size));
|
|
929
|
+
return chunks;
|
|
930
|
+
},
|
|
931
|
+
|
|
932
|
+
/** Split on sentence boundaries */
|
|
933
|
+
sentence(text, maxChunkSize=1000) {
|
|
934
|
+
const sents = text.match(/[^.!?]+[.!?]+[\s]*/g) || [text];
|
|
935
|
+
const chunks=[]; let cur='';
|
|
936
|
+
for(const s of sents){
|
|
937
|
+
if((cur+s).length>maxChunkSize && cur){ chunks.push(cur.trim()); cur=''; }
|
|
938
|
+
cur+=s;
|
|
939
|
+
}
|
|
940
|
+
if(cur.trim()) chunks.push(cur.trim());
|
|
941
|
+
return chunks;
|
|
942
|
+
},
|
|
943
|
+
|
|
944
|
+
/** Split on paragraph boundaries (double newline) */
|
|
945
|
+
paragraph(text, maxChunkSize=2000) {
|
|
946
|
+
const paras = text.split(/\n{2,}/).map(p=>p.trim()).filter(Boolean);
|
|
947
|
+
const chunks=[]; let cur='';
|
|
948
|
+
for(const p of paras){
|
|
949
|
+
if((cur+'\n\n'+p).length>maxChunkSize && cur){ chunks.push(cur.trim()); cur=''; }
|
|
950
|
+
cur = cur ? cur+'\n\n'+p : p;
|
|
951
|
+
}
|
|
952
|
+
if(cur.trim()) chunks.push(cur.trim());
|
|
953
|
+
return chunks;
|
|
954
|
+
},
|
|
955
|
+
|
|
956
|
+
/** Recursive character text splitter (LangChain-style) */
|
|
957
|
+
recursive(text, chunkSize=500, overlap=50, separators=['\n\n','\n',' ','']) {
|
|
958
|
+
for(const sep of separators){
|
|
959
|
+
const parts = sep ? text.split(sep) : [...text];
|
|
960
|
+
if(parts.length > 1){
|
|
961
|
+
const chunks=[]; let cur='';
|
|
962
|
+
for(const p of parts){
|
|
963
|
+
const joined = cur ? cur+sep+p : p;
|
|
964
|
+
if(joined.length <= chunkSize) { cur=joined; }
|
|
965
|
+
else {
|
|
966
|
+
if(cur) chunks.push(cur);
|
|
967
|
+
cur = p.length<=chunkSize ? p : chunker.recursive(p,chunkSize,overlap,separators.slice(1))[0];
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
if(cur) chunks.push(cur);
|
|
971
|
+
if(chunks.length > 1) return chunks;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
// Fall back to fixed
|
|
975
|
+
return chunker.fixed(text, chunkSize, overlap);
|
|
976
|
+
},
|
|
977
|
+
|
|
978
|
+
/** Token-aware chunking */
|
|
979
|
+
byTokens(text, maxTokens=256, overlap=20) {
|
|
980
|
+
const words=text.split(/\s+/);
|
|
981
|
+
const chunks=[]; let cur=[]; let curTok=0;
|
|
982
|
+
for(const w of words){
|
|
983
|
+
const wt=tokenizer.estimate(w);
|
|
984
|
+
if(curTok+wt>maxTokens && cur.length){
|
|
985
|
+
chunks.push(cur.join(' '));
|
|
986
|
+
const keep=Math.floor(overlap/2);
|
|
987
|
+
cur=cur.slice(-keep); curTok=tokenizer.estimate(cur.join(' '));
|
|
988
|
+
}
|
|
989
|
+
cur.push(w); curTok+=wt;
|
|
990
|
+
}
|
|
991
|
+
if(cur.length) chunks.push(cur.join(' '));
|
|
992
|
+
return chunks;
|
|
993
|
+
},
|
|
994
|
+
};
|
|
995
|
+
|
|
996
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
997
|
+
// §11 RAG pipeline
|
|
998
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
999
|
+
|
|
1000
|
+
class RAGPipeline {
|
|
1001
|
+
/**
|
|
1002
|
+
* @param {KitAIClient} client
|
|
1003
|
+
* @param {VectorStore} [store]
|
|
1004
|
+
* @param {object} [opts]
|
|
1005
|
+
* @param {string} [opts.chunkMode] 'sentence'|'paragraph'|'fixed'|'recursive'
|
|
1006
|
+
* @param {number} [opts.chunkSize]
|
|
1007
|
+
* @param {number} [opts.topK]
|
|
1008
|
+
* @param {string} [opts.embedModel]
|
|
1009
|
+
* @param {string} [opts.chatModel]
|
|
1010
|
+
* @param {string} [opts.promptTemplate]
|
|
1011
|
+
*/
|
|
1012
|
+
constructor(client, store=new VectorStore(), opts={}) {
|
|
1013
|
+
this._client = client;
|
|
1014
|
+
this._store = store;
|
|
1015
|
+
this._opts = { chunkMode:'sentence', chunkSize:500, topK:5,
|
|
1016
|
+
embedModel:'text-embedding-3-small', chatModel:'', ...opts };
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/** Ingest a document: chunk → embed → store */
|
|
1020
|
+
async ingest(text, metadata={}) {
|
|
1021
|
+
const mode = this._opts.chunkMode;
|
|
1022
|
+
const chunks= chunker[mode]?.(text, this._opts.chunkSize) || chunker.sentence(text);
|
|
1023
|
+
const vecs = await this._client.embed(chunks, { model:this._opts.embedModel });
|
|
1024
|
+
for(let i=0;i<chunks.length;i++){
|
|
1025
|
+
const id=`${metadata.source||'doc'}_${Date.now()}_${i}`;
|
|
1026
|
+
this._store.add(id, chunks[i], vecs[i] || vecs[0], { ...metadata, chunkIndex:i });
|
|
1027
|
+
}
|
|
1028
|
+
return { chunks: chunks.length, stored: chunks.length };
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/** Query the RAG pipeline: embed query → retrieve → augment → generate */
|
|
1032
|
+
async query(question, opts={}) {
|
|
1033
|
+
const [qvec] = await this._client.embed(question, { model:this._opts.embedModel });
|
|
1034
|
+
const results = this._store.search(qvec, opts.topK || this._opts.topK, opts.filter);
|
|
1035
|
+
const context = results.map((r,i)=>`[${i+1}] ${r.text}`).join('\n\n');
|
|
1036
|
+
const tpl = opts.promptTemplate || this._opts.promptTemplate ||
|
|
1037
|
+
`Answer the question using only the context below.\n\nContext:\n{context}\n\nQuestion: {question}\n\nAnswer:`;
|
|
1038
|
+
const prompt = tpl.replace('{context}',context).replace('{question}',question);
|
|
1039
|
+
const response = await this._client.chat([{role:'user',content:prompt}],
|
|
1040
|
+
{model: opts.model||this._opts.chatModel});
|
|
1041
|
+
return { response, sources: results, context };
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
get store() { return this._store; }
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1048
|
+
// §12 Prompt template engine
|
|
1049
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1050
|
+
|
|
1051
|
+
class PromptTemplate {
|
|
1052
|
+
/**
|
|
1053
|
+
* @param {string} template – "{var}" for variables, "{{#if cond}}...{{/if}}" for blocks
|
|
1054
|
+
* @param {object} [defaults]
|
|
1055
|
+
*/
|
|
1056
|
+
constructor(template, defaults={}) {
|
|
1057
|
+
this._tpl = template;
|
|
1058
|
+
this._defaults = defaults;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/** Render the template with given variables */
|
|
1062
|
+
render(vars={}) {
|
|
1063
|
+
const v = { ...this._defaults, ...vars };
|
|
1064
|
+
let out = this._tpl;
|
|
1065
|
+
|
|
1066
|
+
// {{#if key}}...{{/if}}
|
|
1067
|
+
out = out.replace(/\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (_, key, block) =>
|
|
1068
|
+
v[key] ? block : '');
|
|
1069
|
+
|
|
1070
|
+
// {{#each key}}...{{/each}} — expects v[key] to be an array
|
|
1071
|
+
out = out.replace(/\{\{#each\s+(\w+)\}\}([\s\S]*?)\{\{\/each\}\}/g, (_, key, block) =>
|
|
1072
|
+
(Array.isArray(v[key]) ? v[key] : []).map(item =>
|
|
1073
|
+
block.replace(/\{\{this\}\}/g, item)).join(''));
|
|
1074
|
+
|
|
1075
|
+
// {var|filter} filters: upper, lower, trim, json, truncate:N
|
|
1076
|
+
out = out.replace(/\{(\w+)(?:\|([^}]+))?\}/g, (_, key, filter) => {
|
|
1077
|
+
let val = v[key] !== undefined ? String(v[key]) : '';
|
|
1078
|
+
if (filter) {
|
|
1079
|
+
for (const f of filter.split(',')) {
|
|
1080
|
+
const [fn, arg] = f.trim().split(':');
|
|
1081
|
+
if (fn==='upper') val=val.toUpperCase();
|
|
1082
|
+
else if (fn==='lower') val=val.toLowerCase();
|
|
1083
|
+
else if (fn==='trim') val=val.trim();
|
|
1084
|
+
else if (fn==='json') val=JSON.stringify(v[key]);
|
|
1085
|
+
else if (fn==='truncate') val=val.slice(0,+(arg||100));
|
|
1086
|
+
else if (fn==='indent') val=val.split('\n').join('\n'+' '.repeat(+(arg||1)));
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
return val;
|
|
1090
|
+
});
|
|
1091
|
+
return out;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/** Render into a messages array ready for the client */
|
|
1095
|
+
toMessages(vars={}, role='user') {
|
|
1096
|
+
return [{ role, content: this.render(vars) }];
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/** Compose two templates */
|
|
1100
|
+
concat(other, separator='\n') {
|
|
1101
|
+
return new PromptTemplate(this._tpl + separator + other._tpl, { ...this._defaults, ...other._defaults });
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
toString() { return this._tpl; }
|
|
1105
|
+
|
|
1106
|
+
static from(strings, ...keys) {
|
|
1107
|
+
// Tagged template literal support: PromptTemplate.from`Hello {name}!`
|
|
1108
|
+
const tpl = strings.reduce((acc,s,i)=>acc+s+(keys[i]||''),'');
|
|
1109
|
+
return new PromptTemplate(tpl);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1114
|
+
// §13 Few-shot builder
|
|
1115
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1116
|
+
|
|
1117
|
+
class FewShotBuilder {
|
|
1118
|
+
constructor(instructionOrTemplate='', opts={}) {
|
|
1119
|
+
this._instruction = instructionOrTemplate;
|
|
1120
|
+
this._examples = [];
|
|
1121
|
+
this._inputKey = opts.inputKey || 'Input';
|
|
1122
|
+
this._outputKey = opts.outputKey || 'Output';
|
|
1123
|
+
this._separator = opts.separator || '\n\n';
|
|
1124
|
+
this._exampleTpl = opts.exampleTemplate || null;
|
|
1125
|
+
}
|
|
1126
|
+
/** Add an example { input, output } */
|
|
1127
|
+
add(input, output) { this._examples.push({input,output}); return this; }
|
|
1128
|
+
addMany(pairs) { for(const [i,o] of pairs) this.add(i,o); return this; }
|
|
1129
|
+
|
|
1130
|
+
build(query, extraSystem='') {
|
|
1131
|
+
const exStr = this._examples.map(e =>
|
|
1132
|
+
this._exampleTpl
|
|
1133
|
+
? this._exampleTpl.replace('{input}',e.input).replace('{output}',e.output)
|
|
1134
|
+
: `${this._inputKey}: ${e.input}\n${this._outputKey}: ${e.output}`
|
|
1135
|
+
).join(this._separator);
|
|
1136
|
+
const system = [this._instruction, extraSystem, exStr].filter(Boolean).join('\n\n');
|
|
1137
|
+
return [
|
|
1138
|
+
{ role:'system', content: system },
|
|
1139
|
+
{ role:'user', content: `${this._inputKey}: ${query}` },
|
|
1140
|
+
];
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1145
|
+
// §14 System prompt library
|
|
1146
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1147
|
+
|
|
1148
|
+
const systemPrompts = {
|
|
1149
|
+
assistant: 'You are a helpful, accurate, and concise AI assistant.',
|
|
1150
|
+
coder: 'You are an expert software engineer. Write clean, efficient, well-commented code. When asked to fix bugs, explain the root cause. Always prefer readability over cleverness.',
|
|
1151
|
+
analyst: 'You are a senior data analyst. Provide structured, evidence-based analysis. Use bullet points, tables, and clear section headers. Always cite your reasoning.',
|
|
1152
|
+
writer: 'You are a professional writer and editor. Produce clear, engaging, and well-structured prose. Adapt tone and style to the audience. Avoid jargon unless appropriate.',
|
|
1153
|
+
tutor: 'You are a patient and encouraging tutor. Break complex concepts into simple steps. Use analogies and examples. Check for understanding and adjust explanations accordingly.',
|
|
1154
|
+
reviewer: 'You are a critical code reviewer. Identify bugs, security issues, performance problems, and style violations. Be specific and constructive.',
|
|
1155
|
+
planner: 'You are a strategic planner. Break goals into actionable steps. Consider risks, dependencies, and timelines. Output structured plans with clear milestones.',
|
|
1156
|
+
summariser: 'You are a summarisation expert. Distil content to its core ideas. Be concise, accurate, and preserve the most important details. Never fabricate.',
|
|
1157
|
+
debater: 'You are a debate coach. Present the strongest version of any argument, acknowledge counterarguments, and reason to a clear conclusion.',
|
|
1158
|
+
socratic: 'You are a Socratic guide. Help the user discover answers through questions rather than providing them directly.',
|
|
1159
|
+
translator: 'You are a professional translator. Preserve meaning, tone, and nuance. Flag idioms or culturally specific content that may not translate directly.',
|
|
1160
|
+
jsonFormatter: 'You respond ONLY with valid JSON. No explanation, no markdown, no prose. Ensure the JSON is syntactically correct and matches any schema provided.',
|
|
1161
|
+
classifier: 'You are a classification engine. You respond ONLY with the category label — no explanation, no prose, no quotes.',
|
|
1162
|
+
extractor: 'You are an information extraction engine. Extract the requested fields from the text and return them as a JSON object. If a field is not present, use null.',
|
|
1163
|
+
factChecker: 'You are a rigorous fact-checker. Evaluate claims for accuracy. Cite reasoning. Distinguish between verified facts, likely facts, uncertain claims, and falsehoods.',
|
|
1164
|
+
safeguard: 'You are a content safety evaluator. Identify harmful, toxic, or policy-violating content. Output a JSON object with fields: safe (bool), categories (string[]), explanation (string).',
|
|
1165
|
+
persona: 'You are {name}, {description}. Speak in first person and stay in character at all times.',
|
|
1166
|
+
scientist: 'You are a research scientist. Reason carefully and empirically. Distinguish between hypotheses, evidence, and established fact. Prefer precision over speculation.',
|
|
1167
|
+
lawyer: 'You are a careful legal analyst. Identify relevant legal principles, flag ambiguities, and note jurisdictional differences. Always recommend consulting a qualified attorney.',
|
|
1168
|
+
doctor: 'You are a clinical information assistant. Provide accurate medical information while emphasising that users should consult licensed healthcare professionals for personal advice.',
|
|
1169
|
+
productManager: 'You are a seasoned product manager. Think in terms of user value, feasibility, and business impact. Use frameworks like RICE, MoSCoW, and Jobs-to-be-Done when appropriate.',
|
|
1170
|
+
recruiter: 'You are an expert technical recruiter. Evaluate candidates fairly, identify skill gaps constructively, and always provide actionable feedback.',
|
|
1171
|
+
designer: 'You are a UX/UI design expert. Prioritise user needs, accessibility, and visual clarity. Reference design principles like Gestalt, Fitts\'s Law, and Jakob\'s Law.',
|
|
1172
|
+
marketer: 'You are a creative marketing strategist. Generate compelling copy, campaign ideas, and growth strategies grounded in consumer psychology.',
|
|
1173
|
+
therapist: 'You are a supportive, empathetic counsellor. Listen actively, reflect feelings, and guide without judging. Always recommend professional help for serious concerns.',
|
|
1174
|
+
chef: 'You are a professional chef. Provide precise recipes, cooking techniques, and ingredient substitutions. Consider dietary restrictions proactively.',
|
|
1175
|
+
mathematician: 'You are a mathematics expert. Show your work step by step. Use LaTeX notation where helpful. Verify answers and flag any assumptions.',
|
|
1176
|
+
historian: 'You are a meticulous historian. Ground answers in primary sources. Contextualise events within broader historical patterns. Acknowledge historiographical debates.',
|
|
1177
|
+
poet: 'You are a gifted poet. Craft vivid, emotionally resonant writing. Adapt to requested forms (sonnet, haiku, free verse, etc.) and themes.',
|
|
1178
|
+
custom: (content) => content,
|
|
1179
|
+
};
|
|
1180
|
+
|
|
1181
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1182
|
+
// §15 Chain-of-thought / ReAct scaffolds
|
|
1183
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1184
|
+
|
|
1185
|
+
const scaffolds = {
|
|
1186
|
+
chainOfThought(question, opts={}) {
|
|
1187
|
+
const style = opts.style || 'standard'; // 'standard' | 'stepback' | 'selfconsistency'
|
|
1188
|
+
if (style==='stepback') return [
|
|
1189
|
+
{role:'user', content:`Before answering: "${question}", first answer this broader question that would help: What general principle or concept is needed to answer this?`},
|
|
1190
|
+
];
|
|
1191
|
+
return [{role:'user', content:`Think step by step:\n\n${question}\n\nLet's reason through this carefully:`}];
|
|
1192
|
+
},
|
|
1193
|
+
|
|
1194
|
+
react(question, toolNames=[]) {
|
|
1195
|
+
const tools = toolNames.length ? `Available tools: ${toolNames.join(', ')}.` : '';
|
|
1196
|
+
return [{role:'system', content:[
|
|
1197
|
+
'You are a ReAct agent. For each step output exactly:',
|
|
1198
|
+
'Thought: <your reasoning>',
|
|
1199
|
+
'Action: <tool_name>(<args>)',
|
|
1200
|
+
'Observation: <result>',
|
|
1201
|
+
'...repeat until...',
|
|
1202
|
+
'Final Answer: <answer>',
|
|
1203
|
+
tools,
|
|
1204
|
+
].filter(Boolean).join('\n')},
|
|
1205
|
+
{role:'user', content: question}];
|
|
1206
|
+
},
|
|
1207
|
+
|
|
1208
|
+
selfCritique(originalPrompt, response) {
|
|
1209
|
+
return [{role:'user', content:`Original question: ${originalPrompt}\n\nYour previous answer:\n${response}\n\nCritically review your answer. Identify any errors, gaps, or improvements. Then provide a revised, improved answer.`}];
|
|
1210
|
+
},
|
|
1211
|
+
|
|
1212
|
+
treeOfThought(question, branches=3) {
|
|
1213
|
+
return [{role:'user', content:`Consider ${branches} different approaches to answering:\n\n"${question}"\n\nFor each approach:\n1. Outline the approach\n2. Reason through it\n3. Evaluate its strengths and weaknesses\n\nThen select the best approach and provide a final answer.`}];
|
|
1214
|
+
},
|
|
1215
|
+
|
|
1216
|
+
planAndSolve(question) {
|
|
1217
|
+
return [{role:'user', content:`Let's solve this step by step.\n\nProblem: ${question}\n\nStep 1: Devise a plan with numbered sub-steps.\nStep 2: Execute each sub-step in order.\nStep 3: Verify the final answer.\n\nBegin:`}];
|
|
1218
|
+
},
|
|
1219
|
+
};
|
|
1220
|
+
|
|
1221
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1222
|
+
// §16 Sequential chain
|
|
1223
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1224
|
+
|
|
1225
|
+
class SequentialChain {
|
|
1226
|
+
constructor(client, opts={}) {
|
|
1227
|
+
this._client = client;
|
|
1228
|
+
this._steps = [];
|
|
1229
|
+
this._opts = opts;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/** Add a step. `step` can be:
|
|
1233
|
+
* - a string prompt template (uses {input} for previous output)
|
|
1234
|
+
* - a PromptTemplate instance
|
|
1235
|
+
* - an async function(previousOutput) => messages[]
|
|
1236
|
+
*/
|
|
1237
|
+
step(step, opts={}) { this._steps.push({step, opts}); return this; }
|
|
1238
|
+
|
|
1239
|
+
async run(initialInput, ctx={}) {
|
|
1240
|
+
let current = initialInput;
|
|
1241
|
+
const history = [];
|
|
1242
|
+
for (const {step, opts} of this._steps) {
|
|
1243
|
+
let messages;
|
|
1244
|
+
if (typeof step === 'function') {
|
|
1245
|
+
messages = await step(current, ctx);
|
|
1246
|
+
} else if (step instanceof PromptTemplate) {
|
|
1247
|
+
messages = step.toMessages({ input: current, ...ctx });
|
|
1248
|
+
} else {
|
|
1249
|
+
messages = [{ role:'user', content: String(step).replace(/\{input\}/g, current) }];
|
|
1250
|
+
}
|
|
1251
|
+
const res = await this._client.chat(messages, { ...this._opts, ...opts });
|
|
1252
|
+
history.push({ input: current, output: res.content, response: res });
|
|
1253
|
+
current = res.content;
|
|
1254
|
+
}
|
|
1255
|
+
return { output: current, history };
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1260
|
+
// §17 Parallel chain
|
|
1261
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1262
|
+
|
|
1263
|
+
class ParallelChain {
|
|
1264
|
+
constructor(client, opts={}) {
|
|
1265
|
+
this._client = client;
|
|
1266
|
+
this._branches= [];
|
|
1267
|
+
this._opts = opts;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
branch(name, messagesOrFn) { this._branches.push({name, fn: messagesOrFn}); return this; }
|
|
1271
|
+
|
|
1272
|
+
async run(input, mergeFn=null) {
|
|
1273
|
+
const results = await Promise.all(this._branches.map(async ({name, fn}) => {
|
|
1274
|
+
const messages = typeof fn==='function' ? await fn(input) : fn;
|
|
1275
|
+
const res = await this._client.chat(messages, this._opts);
|
|
1276
|
+
return { name, response: res, output: res.content };
|
|
1277
|
+
}));
|
|
1278
|
+
const merged = mergeFn ? await mergeFn(results, this._client) : results;
|
|
1279
|
+
return { branches: results, merged };
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1284
|
+
// §18 Tool registry & ReAct agent
|
|
1285
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1286
|
+
|
|
1287
|
+
class ToolRegistry {
|
|
1288
|
+
constructor() { this._tools = new Map(); }
|
|
1289
|
+
|
|
1290
|
+
/**
|
|
1291
|
+
* Register a tool.
|
|
1292
|
+
* @param {string} name
|
|
1293
|
+
* @param {string} description
|
|
1294
|
+
* @param {Function} fn – sync or async (args) => result
|
|
1295
|
+
* @param {object} [schema] – JSON schema for args
|
|
1296
|
+
*/
|
|
1297
|
+
register(name, description, fn, schema={}) {
|
|
1298
|
+
this._tools.set(name, { name, description, fn, schema });
|
|
1299
|
+
return this;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
unregister(name) { this._tools.delete(name); return this; }
|
|
1303
|
+
has(name) { return this._tools.has(name); }
|
|
1304
|
+
get(name) { return this._tools.get(name); }
|
|
1305
|
+
list() { return [...this._tools.values()].map(t=>({ name:t.name, description:t.description, schema:t.schema })); }
|
|
1306
|
+
|
|
1307
|
+
async call(name, args) {
|
|
1308
|
+
const tool = this._tools.get(name);
|
|
1309
|
+
if (!tool) throw new Error(`Tool not found: ${name}`);
|
|
1310
|
+
return tool.fn(args);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
/** Convert to OpenAI tool_choice format */
|
|
1314
|
+
toOpenAISchema() {
|
|
1315
|
+
return [...this._tools.values()].map(t=>({
|
|
1316
|
+
type:'function',
|
|
1317
|
+
function:{ name:t.name, description:t.description, parameters: t.schema||{type:'object',properties:{}} },
|
|
1318
|
+
}));
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// Built-in tools
|
|
1323
|
+
const _builtinTools = {
|
|
1324
|
+
calculator: {
|
|
1325
|
+
description: 'Evaluate a mathematical expression. Args: { expression: string }',
|
|
1326
|
+
fn: ({expression}) => {
|
|
1327
|
+
// Safe-ish eval: only allow math chars
|
|
1328
|
+
if (!/^[\d+\-*/().\s%^eE,]+$/.test(expression)) return 'Error: invalid expression';
|
|
1329
|
+
try { return String(Function(`"use strict"; return (${expression})`)()) } catch(e) { return 'Error: '+e.message; }
|
|
1330
|
+
},
|
|
1331
|
+
},
|
|
1332
|
+
datetime: {
|
|
1333
|
+
description: 'Get current date/time info. Args: { timezone?: string, format?: "iso"|"human"|"unix" }',
|
|
1334
|
+
fn: ({timezone='UTC', format='iso'}) => {
|
|
1335
|
+
const d = new Date();
|
|
1336
|
+
if(format==='unix') return String(Math.floor(d.getTime()/1000));
|
|
1337
|
+
if(format==='human') return d.toLocaleString('en-US',{timeZone:timezone});
|
|
1338
|
+
return d.toISOString();
|
|
1339
|
+
},
|
|
1340
|
+
},
|
|
1341
|
+
jsonPath: {
|
|
1342
|
+
description: 'Extract a value from a JSON string using dot notation. Args: { json: string, path: string }',
|
|
1343
|
+
fn: ({json, path}) => {
|
|
1344
|
+
try {
|
|
1345
|
+
const obj = typeof json==='string' ? JSON.parse(json) : json;
|
|
1346
|
+
return String(path.split('.').reduce((o,k)=>o?.[k],obj) ?? 'null');
|
|
1347
|
+
} catch(e) { return 'Error: '+e.message; }
|
|
1348
|
+
},
|
|
1349
|
+
},
|
|
1350
|
+
regex: {
|
|
1351
|
+
description: 'Test or extract regex matches. Args: { text: string, pattern: string, flags?: string, mode?: "test"|"match"|"replace", replacement?: string }',
|
|
1352
|
+
fn: ({text, pattern, flags='g', mode='match', replacement=''}) => {
|
|
1353
|
+
const re = new RegExp(pattern, flags);
|
|
1354
|
+
if(mode==='test') return String(re.test(text));
|
|
1355
|
+
if(mode==='replace') return text.replace(re, replacement);
|
|
1356
|
+
const m = text.match(re);
|
|
1357
|
+
return m ? JSON.stringify(m) : 'null';
|
|
1358
|
+
},
|
|
1359
|
+
},
|
|
1360
|
+
base64: {
|
|
1361
|
+
description: 'Encode or decode base64. Args: { text: string, mode: "encode"|"decode" }',
|
|
1362
|
+
fn: ({text, mode='encode'}) => {
|
|
1363
|
+
if(mode==='encode') return typeof btoa!=='undefined' ? btoa(text) : Buffer.from(text).toString('base64');
|
|
1364
|
+
return typeof atob!=='undefined' ? atob(text) : Buffer.from(text,'base64').toString('utf8');
|
|
1365
|
+
},
|
|
1366
|
+
},
|
|
1367
|
+
uuid: {
|
|
1368
|
+
description: 'Generate a UUID v4. Args: {}',
|
|
1369
|
+
fn: () => {
|
|
1370
|
+
if(typeof crypto!=='undefined' && crypto.randomUUID) return crypto.randomUUID();
|
|
1371
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g,c=>{
|
|
1372
|
+
const r=Math.random()*16|0; return (c==='x'?r:(r&0x3|0x8)).toString(16);
|
|
1373
|
+
});
|
|
1374
|
+
},
|
|
1375
|
+
},
|
|
1376
|
+
hash: {
|
|
1377
|
+
description: 'FNV-1a hash of a string. Args: { text: string }',
|
|
1378
|
+
fn: ({text}) => _md5Hex(text),
|
|
1379
|
+
},
|
|
1380
|
+
webSearch: {
|
|
1381
|
+
description: 'Search the web (stub — returns a placeholder; connect to a real API). Args: { query: string, topK?: number }',
|
|
1382
|
+
fn: ({query, topK=3}) => JSON.stringify({ query, note:'web_search stub — connect to SerpAPI/Brave/Tavily', results:[] }),
|
|
1383
|
+
},
|
|
1384
|
+
};
|
|
1385
|
+
|
|
1386
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1387
|
+
// §19 ReAct agent
|
|
1388
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1389
|
+
|
|
1390
|
+
class ReactAgent {
|
|
1391
|
+
/**
|
|
1392
|
+
* @param {KitAIClient} client
|
|
1393
|
+
* @param {ToolRegistry} registry
|
|
1394
|
+
* @param {object} [opts]
|
|
1395
|
+
* @param {number} [opts.maxSteps]
|
|
1396
|
+
* @param {string} [opts.model]
|
|
1397
|
+
* @param {boolean} [opts.verbose]
|
|
1398
|
+
*/
|
|
1399
|
+
constructor(client, registry, opts={}) {
|
|
1400
|
+
this._client = client;
|
|
1401
|
+
this._registry = registry;
|
|
1402
|
+
this._maxSteps = opts.maxSteps || 10;
|
|
1403
|
+
this._model = opts.model || '';
|
|
1404
|
+
this._verbose = opts.verbose || false;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
async run(question) {
|
|
1408
|
+
const messages = scaffolds.react(question, this._registry.list().map(t=>t.name));
|
|
1409
|
+
const trace = [];
|
|
1410
|
+
let steps = 0;
|
|
1411
|
+
|
|
1412
|
+
while (steps++ < this._maxSteps) {
|
|
1413
|
+
const res = await this._client.chat(messages, { model:this._model });
|
|
1414
|
+
const txt = res.content;
|
|
1415
|
+
messages.push({ role:'assistant', content:txt });
|
|
1416
|
+
if(this._verbose) console.log(`[ReAct step ${steps}]\n${txt}`);
|
|
1417
|
+
|
|
1418
|
+
// Parse Final Answer
|
|
1419
|
+
const finalMatch = txt.match(/Final Answer:\s*([\s\S]+)/i);
|
|
1420
|
+
if (finalMatch) return { answer: finalMatch[1].trim(), trace, steps };
|
|
1421
|
+
|
|
1422
|
+
// Parse Action: tool_name(json_or_text)
|
|
1423
|
+
const actionMatch = txt.match(/Action:\s*(\w+)\(([\s\S]*?)\)(?:\n|$)/i);
|
|
1424
|
+
if (actionMatch) {
|
|
1425
|
+
const toolName = actionMatch[1];
|
|
1426
|
+
let args = {};
|
|
1427
|
+
try { args = JSON.parse(actionMatch[2]); } catch { args = { input: actionMatch[2] }; }
|
|
1428
|
+
let observation;
|
|
1429
|
+
try { observation = await this._registry.call(toolName, args); }
|
|
1430
|
+
catch (e) { observation = `Error: ${e.message}`; }
|
|
1431
|
+
trace.push({ thought:txt, action:toolName, args, observation });
|
|
1432
|
+
messages.push({ role:'user', content:`Observation: ${observation}` });
|
|
1433
|
+
} else {
|
|
1434
|
+
// No action found – treat as final
|
|
1435
|
+
return { answer: txt, trace, steps };
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
return { answer:'Max steps reached', trace, steps };
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1443
|
+
// §20 Structured output — schema builder & JSON enforcer
|
|
1444
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1445
|
+
|
|
1446
|
+
const schema = {
|
|
1447
|
+
string: (desc='') => ({ type:'string', description:desc }),
|
|
1448
|
+
number: (desc='') => ({ type:'number', description:desc }),
|
|
1449
|
+
boolean: (desc='') => ({ type:'boolean', description:desc }),
|
|
1450
|
+
array: (items, desc='') => ({ type:'array', items, description:desc }),
|
|
1451
|
+
object: (properties, required=[], desc='') => ({ type:'object', properties, required, description:desc }),
|
|
1452
|
+
enum: (values, desc='') => ({ type:'string', enum:values, description:desc }),
|
|
1453
|
+
nullable:(inner) => ({ ...inner, nullable:true }),
|
|
1454
|
+
|
|
1455
|
+
/** Generate a JSON instruction string from a schema object */
|
|
1456
|
+
toInstruction(schemaObj, name='response') {
|
|
1457
|
+
return `Respond ONLY with a valid JSON object matching this schema for "${name}":\n${JSON.stringify(schemaObj, null, 2)}\nNo explanation, no markdown, just the JSON.`;
|
|
1458
|
+
},
|
|
1459
|
+
|
|
1460
|
+
/** Validate a value against a simple schema (subset of JSON Schema) */
|
|
1461
|
+
validate(value, schemaObj) {
|
|
1462
|
+
const errors = [];
|
|
1463
|
+
function check(val, s, path='') {
|
|
1464
|
+
if(s.nullable && (val===null||val===undefined)) return;
|
|
1465
|
+
if(s.type==='string' && typeof val!=='string') errors.push(`${path}: expected string, got ${typeof val}`);
|
|
1466
|
+
if(s.type==='number' && typeof val!=='number') errors.push(`${path}: expected number, got ${typeof val}`);
|
|
1467
|
+
if(s.type==='boolean' && typeof val!=='boolean') errors.push(`${path}: expected boolean, got ${typeof val}`);
|
|
1468
|
+
if(s.type==='array'){
|
|
1469
|
+
if(!Array.isArray(val)) errors.push(`${path}: expected array`);
|
|
1470
|
+
else val.forEach((item,i)=>check(item,s.items||{},`${path}[${i}]`));
|
|
1471
|
+
}
|
|
1472
|
+
if(s.type==='object'){
|
|
1473
|
+
if(typeof val!=='object'||Array.isArray(val)) errors.push(`${path}: expected object`);
|
|
1474
|
+
else {
|
|
1475
|
+
for(const req of (s.required||[])) if(!(req in val)) errors.push(`${path}.${req}: required`);
|
|
1476
|
+
for(const [k,sub] of Object.entries(s.properties||{})) if(k in val) check(val[k],sub,`${path}.${k}`);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
if(s.enum && !s.enum.includes(val)) errors.push(`${path}: must be one of ${s.enum.join(',')}`);
|
|
1480
|
+
}
|
|
1481
|
+
check(value, schemaObj);
|
|
1482
|
+
return { valid: errors.length===0, errors };
|
|
1483
|
+
},
|
|
1484
|
+
};
|
|
1485
|
+
|
|
1486
|
+
class JSONEnforcer {
|
|
1487
|
+
/**
|
|
1488
|
+
* @param {KitAIClient} client
|
|
1489
|
+
* @param {object} schemaObj – KitAI schema or raw JSON Schema
|
|
1490
|
+
* @param {number} maxRetries
|
|
1491
|
+
*/
|
|
1492
|
+
constructor(client, schemaObj, maxRetries=3) {
|
|
1493
|
+
this._client = client;
|
|
1494
|
+
this._schema = schemaObj;
|
|
1495
|
+
this._retries = maxRetries;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
async run(messages, opts={}) {
|
|
1499
|
+
const systemMsg = { role:'system', content: schema.toInstruction(this._schema) };
|
|
1500
|
+
const msgs = [systemMsg, ...messages];
|
|
1501
|
+
for (let attempt=0; attempt<=this._retries; attempt++) {
|
|
1502
|
+
const res = await this._client.chat(msgs, { ...opts, jsonMode: true });
|
|
1503
|
+
const json = res.tryJSON();
|
|
1504
|
+
if (json !== null) {
|
|
1505
|
+
const v = schema.validate(json, this._schema);
|
|
1506
|
+
if (v.valid) return { data:json, response:res, attempt };
|
|
1507
|
+
// Feed validation errors back
|
|
1508
|
+
msgs.push({ role:'assistant', content:res.content });
|
|
1509
|
+
msgs.push({ role:'user', content:`Your JSON was invalid:\n${v.errors.join('\n')}\nPlease fix and respond with valid JSON only.` });
|
|
1510
|
+
} else {
|
|
1511
|
+
msgs.push({ role:'assistant', content:res.content });
|
|
1512
|
+
msgs.push({ role:'user', content:'Your response was not valid JSON. Please respond with JSON only.' });
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
throw new Error('JSONEnforcer: max retries exceeded');
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1520
|
+
// §21 Output parsers
|
|
1521
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1522
|
+
|
|
1523
|
+
const parsers = {
|
|
1524
|
+
/** Parse JSON, stripping markdown fences */
|
|
1525
|
+
json(text) {
|
|
1526
|
+
const clean = text.replace(/^```(?:json)?\n?/,'').replace(/\n?```$/,'').trim();
|
|
1527
|
+
return JSON.parse(clean);
|
|
1528
|
+
},
|
|
1529
|
+
|
|
1530
|
+
/** Parse CSV text → array of row objects */
|
|
1531
|
+
csv(text, delimiter=',') {
|
|
1532
|
+
const lines = text.trim().split('\n');
|
|
1533
|
+
const headers = lines[0].split(delimiter).map(h=>h.trim().replace(/^"|"$/g,''));
|
|
1534
|
+
return lines.slice(1).map(l => {
|
|
1535
|
+
const vals = l.split(delimiter).map(v=>v.trim().replace(/^"|"$/g,''));
|
|
1536
|
+
return Object.fromEntries(headers.map((h,i)=>[h,vals[i]??'']));
|
|
1537
|
+
});
|
|
1538
|
+
},
|
|
1539
|
+
|
|
1540
|
+
/** Parse numbered or bulleted list → string[] */
|
|
1541
|
+
list(text) {
|
|
1542
|
+
return text.split('\n')
|
|
1543
|
+
.map(l=>l.replace(/^[-*•]\s+|^\d+[.)]\s+/,'').trim())
|
|
1544
|
+
.filter(Boolean);
|
|
1545
|
+
},
|
|
1546
|
+
|
|
1547
|
+
/** Parse "Key: Value" lines → object */
|
|
1548
|
+
keyValue(text) {
|
|
1549
|
+
const result = {};
|
|
1550
|
+
for(const line of text.split('\n')){
|
|
1551
|
+
const i=line.indexOf(':');
|
|
1552
|
+
if(i>0) result[line.slice(0,i).trim()] = line.slice(i+1).trim();
|
|
1553
|
+
}
|
|
1554
|
+
return result;
|
|
1555
|
+
},
|
|
1556
|
+
|
|
1557
|
+
/** Parse a markdown table → array of objects */
|
|
1558
|
+
markdownTable(text) {
|
|
1559
|
+
const lines = text.split('\n').filter(l=>l.trim().startsWith('|'));
|
|
1560
|
+
if(lines.length<2) return [];
|
|
1561
|
+
const headers = lines[0].split('|').slice(1,-1).map(h=>h.trim());
|
|
1562
|
+
return lines.slice(2).map(l=>{ // skip separator line
|
|
1563
|
+
const vals = l.split('|').slice(1,-1).map(v=>v.trim());
|
|
1564
|
+
return Object.fromEntries(headers.map((h,i)=>[h,vals[i]||'']));
|
|
1565
|
+
});
|
|
1566
|
+
},
|
|
1567
|
+
|
|
1568
|
+
/** Extract typed fields using an extraction prompt + JSON enforcer */
|
|
1569
|
+
async extract(client, text, fields, model='') {
|
|
1570
|
+
const schemaObj = { type:'object', properties: Object.fromEntries(
|
|
1571
|
+
Object.entries(fields).map(([k,v])=>[k, typeof v==='string'?{type:v}:v])
|
|
1572
|
+
), required:[] };
|
|
1573
|
+
const msgs=[{role:'user',content:`Extract the following fields from this text:\n\n${text}\n\nFields: ${Object.keys(fields).join(', ')}`}];
|
|
1574
|
+
const enforcer=new JSONEnforcer(client,schemaObj);
|
|
1575
|
+
return enforcer.run(msgs,{model});
|
|
1576
|
+
},
|
|
1577
|
+
};
|
|
1578
|
+
|
|
1579
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1580
|
+
// §22 Classification
|
|
1581
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1582
|
+
|
|
1583
|
+
const classify = {
|
|
1584
|
+
/** Zero-shot classification */
|
|
1585
|
+
async zeroShot(client, text, labels, opts={}) {
|
|
1586
|
+
const labelList = labels.join(' | ');
|
|
1587
|
+
const msgs = [
|
|
1588
|
+
{ role:'system', content:`Classify the input into exactly one of these categories: ${labelList}\nRespond with ONLY the category name. No explanation.` },
|
|
1589
|
+
{ role:'user', content: text },
|
|
1590
|
+
];
|
|
1591
|
+
const res = await client.chat(msgs, opts);
|
|
1592
|
+
const label = res.content.trim();
|
|
1593
|
+
return { label: labels.find(l=>l.toLowerCase()===label.toLowerCase()) || label, raw:res };
|
|
1594
|
+
},
|
|
1595
|
+
|
|
1596
|
+
/** Few-shot classification */
|
|
1597
|
+
async fewShot(client, text, examples, labels, opts={}) {
|
|
1598
|
+
const builder = new FewShotBuilder(`Classify into: ${labels.join(' | ')}`);
|
|
1599
|
+
for(const [input,output] of examples) builder.add(input,output);
|
|
1600
|
+
const msgs = builder.build(text, `Reply with only the label.`);
|
|
1601
|
+
const res = await client.chat(msgs, opts);
|
|
1602
|
+
return { label: res.content.trim(), raw:res };
|
|
1603
|
+
},
|
|
1604
|
+
|
|
1605
|
+
/** Multi-label classification */
|
|
1606
|
+
async multiLabel(client, text, labels, opts={}) {
|
|
1607
|
+
const msgs = [
|
|
1608
|
+
{ role:'system', content:`From the list [${labels.join(', ')}], select ALL that apply to the input. Respond with a JSON array of matching labels only.` },
|
|
1609
|
+
{ role:'user', content: text },
|
|
1610
|
+
];
|
|
1611
|
+
const res = await client.chat(msgs, opts);
|
|
1612
|
+
try { return { labels: parsers.json(res.content), raw:res }; }
|
|
1613
|
+
catch { return { labels:[], raw:res }; }
|
|
1614
|
+
},
|
|
1615
|
+
};
|
|
1616
|
+
|
|
1617
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1618
|
+
// §23 Evaluation & metrics
|
|
1619
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1620
|
+
|
|
1621
|
+
const metrics = {
|
|
1622
|
+
/** BLEU-1 (unigram precision) */
|
|
1623
|
+
bleu1(reference, hypothesis) {
|
|
1624
|
+
const ref = reference.toLowerCase().split(/\s+/);
|
|
1625
|
+
const hyp = hypothesis.toLowerCase().split(/\s+/);
|
|
1626
|
+
const refSet = new Set(ref);
|
|
1627
|
+
const matches = hyp.filter(w=>refSet.has(w)).length;
|
|
1628
|
+
const bp = hyp.length >= ref.length ? 1 : Math.exp(1 - ref.length/hyp.length);
|
|
1629
|
+
return bp * matches / (hyp.length||1);
|
|
1630
|
+
},
|
|
1631
|
+
|
|
1632
|
+
/** ROUGE-L (longest common subsequence F1) */
|
|
1633
|
+
rougeL(reference, hypothesis) {
|
|
1634
|
+
const r=reference.toLowerCase().split(/\s+/), h=hypothesis.toLowerCase().split(/\s+/);
|
|
1635
|
+
const m=r.length, n=h.length;
|
|
1636
|
+
const dp=Array.from({length:m+1},()=>new Array(n+1).fill(0));
|
|
1637
|
+
for(let i=1;i<=m;i++) for(let j=1;j<=n;j++)
|
|
1638
|
+
dp[i][j]=r[i-1]===h[j-1]?dp[i-1][j-1]+1:Math.max(dp[i-1][j],dp[i][j-1]);
|
|
1639
|
+
const lcs=dp[m][n];
|
|
1640
|
+
const p=lcs/(n||1), rec=lcs/(m||1);
|
|
1641
|
+
return p+rec>0 ? 2*p*rec/(p+rec) : 0;
|
|
1642
|
+
},
|
|
1643
|
+
|
|
1644
|
+
/** Exact match */
|
|
1645
|
+
exactMatch(reference, hypothesis) {
|
|
1646
|
+
return reference.trim().toLowerCase()===hypothesis.trim().toLowerCase()?1:0;
|
|
1647
|
+
},
|
|
1648
|
+
|
|
1649
|
+
/** Token-level F1 */
|
|
1650
|
+
tokenF1(reference, hypothesis) {
|
|
1651
|
+
const r=reference.toLowerCase().split(/\s+/), h=hypothesis.toLowerCase().split(/\s+/);
|
|
1652
|
+
const refCount={}, hypCount={};
|
|
1653
|
+
for(const w of r) refCount[w]=(refCount[w]||0)+1;
|
|
1654
|
+
for(const w of h) hypCount[w]=(hypCount[w]||0)+1;
|
|
1655
|
+
let overlap=0;
|
|
1656
|
+
for(const w of Object.keys(hypCount)) overlap+=Math.min(hypCount[w],refCount[w]||0);
|
|
1657
|
+
const p=overlap/(h.length||1), rec=overlap/(r.length||1);
|
|
1658
|
+
return p+rec>0?2*p*rec/(p+rec):0;
|
|
1659
|
+
},
|
|
1660
|
+
};
|
|
1661
|
+
|
|
1662
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1663
|
+
// §24 LLM-as-judge
|
|
1664
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1665
|
+
|
|
1666
|
+
class LLMJudge {
|
|
1667
|
+
constructor(client, opts={}) {
|
|
1668
|
+
this._client = client;
|
|
1669
|
+
this._model = opts.model || '';
|
|
1670
|
+
this._criteria= opts.criteria || ['correctness','relevance','coherence','conciseness'];
|
|
1671
|
+
this._scale = opts.scale || 5;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
async evaluate(question, response, reference='', extra={}) {
|
|
1675
|
+
const refNote = reference ? `\nReference answer: ${reference}` : '';
|
|
1676
|
+
const critList = this._criteria.map(c=>`- ${c}: score 1-${this._scale}`).join('\n');
|
|
1677
|
+
const msgs = [{role:'user', content:
|
|
1678
|
+
`Evaluate this AI response on a scale of 1-${this._scale} for each criterion.\n\nQuestion: ${question}${refNote}\n\nResponse: ${response}\n\nCriteria:\n${critList}\n\nRespond as JSON: { "scores": { <criterion>: <score> }, "overall": <avg>, "rationale": "<brief>" }\n\nAdditional context: ${JSON.stringify(extra)}`
|
|
1679
|
+
}];
|
|
1680
|
+
const res = await this._client.chat(msgs, { model:this._model, jsonMode:true });
|
|
1681
|
+
const data = res.tryJSON() || { scores:{}, overall:0, rationale:res.content };
|
|
1682
|
+
return { ...data, raw:res };
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
async batch(pairs, reference='') {
|
|
1686
|
+
return Promise.all(pairs.map(([q,r])=>this.evaluate(q,r,reference)));
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1691
|
+
// §25 Safety guard
|
|
1692
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1693
|
+
|
|
1694
|
+
const guard = {
|
|
1695
|
+
/** Lightweight offline PII detector (regex-based) */
|
|
1696
|
+
detectPII(text) {
|
|
1697
|
+
const patterns = {
|
|
1698
|
+
email: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
|
|
1699
|
+
phone: /(\+?\d[\d\s\-().]{7,}\d)/g,
|
|
1700
|
+
ssn: /\b\d{3}-\d{2}-\d{4}\b/g,
|
|
1701
|
+
creditCard: /\b(?:\d[ -]?){13,16}\b/g,
|
|
1702
|
+
ipAddress: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g,
|
|
1703
|
+
dateOfBirth: /\b\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}\b/g,
|
|
1704
|
+
};
|
|
1705
|
+
const found={};
|
|
1706
|
+
for(const [k,re] of Object.entries(patterns)){
|
|
1707
|
+
const m=text.match(re);
|
|
1708
|
+
if(m) found[k]=m;
|
|
1709
|
+
}
|
|
1710
|
+
return { hasPII: Object.keys(found).length>0, detections:found };
|
|
1711
|
+
},
|
|
1712
|
+
|
|
1713
|
+
/** Offline prompt injection heuristic */
|
|
1714
|
+
detectInjection(text) {
|
|
1715
|
+
const patterns = [
|
|
1716
|
+
/ignore\s+(all\s+)?previous\s+instructions/i,
|
|
1717
|
+
/you\s+are\s+now\s+(a\s+)?(?!an?\s+assistant)/i,
|
|
1718
|
+
/disregard\s+your\s+(system|instructions|prompt)/i,
|
|
1719
|
+
/act\s+as\s+(?:an?\s+)?(?:evil|unethical|jailbreak)/i,
|
|
1720
|
+
/\[INST\]|\[\/INST\]|<\|im_start\|>|<\|im_end\|>/,
|
|
1721
|
+
/DAN\s+mode|jailbreak|bypass\s+(?:safety|filter)/i,
|
|
1722
|
+
];
|
|
1723
|
+
const matches = patterns.filter(p=>p.test(text));
|
|
1724
|
+
return { injectionDetected: matches.length>0, matchCount:matches.length };
|
|
1725
|
+
},
|
|
1726
|
+
|
|
1727
|
+
/** LLM-based toxicity + safety check */
|
|
1728
|
+
async check(client, text, opts={}) {
|
|
1729
|
+
const msgs=[{role:'system',content:systemPrompts.safeguard},{role:'user',content:text}];
|
|
1730
|
+
const res=await client.chat(msgs,{model:opts.model||'',jsonMode:true});
|
|
1731
|
+
const data=res.tryJSON()||{safe:null,categories:[],explanation:res.content};
|
|
1732
|
+
return { ...data, pii:guard.detectPII(text), injection:guard.detectInjection(text), raw:res };
|
|
1733
|
+
},
|
|
1734
|
+
|
|
1735
|
+
/** Hallucination grounding check */
|
|
1736
|
+
async groundingCheck(client, claim, context, opts={}) {
|
|
1737
|
+
const msgs=[{role:'user',content:
|
|
1738
|
+
`Does the following claim appear to be fully supported by the given context?\n\nContext:\n${context}\n\nClaim: ${claim}\n\nRespond as JSON: { "supported": true|false, "confidence": 0.0-1.0, "reasoning": "..." }`
|
|
1739
|
+
}];
|
|
1740
|
+
const res=await client.chat(msgs,{model:opts.model||'',jsonMode:true});
|
|
1741
|
+
return res.tryJSON()||{supported:null,confidence:0,reasoning:res.content};
|
|
1742
|
+
},
|
|
1743
|
+
};
|
|
1744
|
+
|
|
1745
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1746
|
+
// §26 Utilities
|
|
1747
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1748
|
+
|
|
1749
|
+
/** Strip markdown to plain text */
|
|
1750
|
+
function stripMarkdown(text) {
|
|
1751
|
+
return text
|
|
1752
|
+
.replace(/```[\s\S]*?```/g,'') // code blocks
|
|
1753
|
+
.replace(/`[^`]+`/g,'') // inline code
|
|
1754
|
+
.replace(/#{1,6}\s+/g,'') // headers
|
|
1755
|
+
.replace(/\*\*([^*]+)\*\*/g,'$1') // bold
|
|
1756
|
+
.replace(/\*([^*]+)\*/g,'$1') // italic
|
|
1757
|
+
.replace(/~~([^~]+)~~/g,'$1') // strikethrough
|
|
1758
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g,'$1')// links
|
|
1759
|
+
.replace(/^>\s+/gm,'') // blockquotes
|
|
1760
|
+
.replace(/^[-*+]\s+/gm,'') // bullets
|
|
1761
|
+
.replace(/^\d+\.\s+/gm,'') // numbered list
|
|
1762
|
+
.replace(/\n{3,}/g,'\n\n') // triple newlines
|
|
1763
|
+
.trim();
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
/** Diff two strings — returns array of { type:'equal'|'insert'|'delete', value:string } */
|
|
1767
|
+
function diffText(oldText, newText) {
|
|
1768
|
+
const oldWords = oldText.split(/\s+/);
|
|
1769
|
+
const newWords = newText.split(/\s+/);
|
|
1770
|
+
const ops=[];
|
|
1771
|
+
// Simple LCS diff on word level
|
|
1772
|
+
const m=oldWords.length,n=newWords.length;
|
|
1773
|
+
const dp=Array.from({length:m+1},()=>new Array(n+1).fill(0));
|
|
1774
|
+
for(let i=1;i<=m;i++) for(let j=1;j<=n;j++)
|
|
1775
|
+
dp[i][j]=oldWords[i-1]===newWords[j-1]?dp[i-1][j-1]+1:Math.max(dp[i-1][j],dp[i][j-1]);
|
|
1776
|
+
let i=m,j=n;
|
|
1777
|
+
const result=[];
|
|
1778
|
+
while(i>0||j>0){
|
|
1779
|
+
if(i>0&&j>0&&oldWords[i-1]===newWords[j-1]){result.unshift({type:'equal',value:oldWords[--i]});j--;}
|
|
1780
|
+
else if(j>0&&(i===0||dp[i][j-1]>=dp[i-1][j])){result.unshift({type:'insert',value:newWords[--j]});}
|
|
1781
|
+
else{result.unshift({type:'delete',value:oldWords[--i]});}
|
|
1782
|
+
}
|
|
1783
|
+
return result;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
/** Semantic similarity score (requires pre-computed embeddings) */
|
|
1787
|
+
function semanticSimilarity(embeddingA, embeddingB) {
|
|
1788
|
+
return _cosineSim(embeddingA, embeddingB);
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
/** Export a conversation history to various formats */
|
|
1792
|
+
const exporter = {
|
|
1793
|
+
toJSON(messages, meta={}) {
|
|
1794
|
+
return JSON.stringify({ meta, messages, exportedAt: new Date().toISOString() }, null, 2);
|
|
1795
|
+
},
|
|
1796
|
+
toMarkdown(messages, title='Conversation') {
|
|
1797
|
+
const lines=[`# ${title}\n`];
|
|
1798
|
+
for(const m of messages) {
|
|
1799
|
+
const role=m.role.charAt(0).toUpperCase()+m.role.slice(1);
|
|
1800
|
+
lines.push(`## ${role}\n${m.content}\n`);
|
|
1801
|
+
}
|
|
1802
|
+
return lines.join('\n');
|
|
1803
|
+
},
|
|
1804
|
+
toHTML(messages, title='Conversation') {
|
|
1805
|
+
const rows = messages.map(m=>{
|
|
1806
|
+
const bg = m.role==='user'?'#f0f4ff':m.role==='system'?'#fff8e1':'#f0fff4';
|
|
1807
|
+
return `<div style="background:${bg};border-radius:8px;padding:12px;margin:8px 0"><strong>${m.role}</strong><p>${m.content.replace(/\n/g,'<br>')}</p></div>`;
|
|
1808
|
+
}).join('');
|
|
1809
|
+
return `<!DOCTYPE html><html><head><title>${title}</title></head><body style="font-family:sans-serif;max-width:800px;margin:auto;padding:20px"><h1>${title}</h1>${rows}</body></html>`;
|
|
1810
|
+
},
|
|
1811
|
+
toText(messages) {
|
|
1812
|
+
return messages.map(m=>`[${m.role.toUpperCase()}]\n${m.content}`).join('\n\n---\n\n');
|
|
1813
|
+
},
|
|
1814
|
+
};
|
|
1815
|
+
|
|
1816
|
+
/** A/B experiment runner */
|
|
1817
|
+
class ABExperiment {
|
|
1818
|
+
/**
|
|
1819
|
+
* @param {KitAIClient[]} clients – two or more client/model configurations
|
|
1820
|
+
* @param {object} [opts]
|
|
1821
|
+
*/
|
|
1822
|
+
constructor(clients, opts={}) {
|
|
1823
|
+
this._clients = clients;
|
|
1824
|
+
this._opts = opts;
|
|
1825
|
+
this._results = [];
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
async run(messages, judge=null) {
|
|
1829
|
+
const responses = await Promise.all(
|
|
1830
|
+
this._clients.map(async (c,i) => {
|
|
1831
|
+
const t0 = Date.now();
|
|
1832
|
+
const res = await c.chat(messages, this._opts);
|
|
1833
|
+
res.variant = i;
|
|
1834
|
+
return res;
|
|
1835
|
+
})
|
|
1836
|
+
);
|
|
1837
|
+
let scores = null;
|
|
1838
|
+
if (judge instanceof LLMJudge) {
|
|
1839
|
+
const q = messages.filter(m=>m.role==='user').map(m=>m.content).join(' ');
|
|
1840
|
+
scores = await Promise.all(responses.map(r=>judge.evaluate(q,r.content)));
|
|
1841
|
+
}
|
|
1842
|
+
const result = { responses, scores, timestamp: Date.now() };
|
|
1843
|
+
this._results.push(result);
|
|
1844
|
+
return result;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
history() { return this._results; }
|
|
1848
|
+
|
|
1849
|
+
summary() {
|
|
1850
|
+
if(!this._results.length) return null;
|
|
1851
|
+
const totals = this._clients.map((_,i)=>({ variant:i, avgLatency:0, avgScore:0, count:0 }));
|
|
1852
|
+
for(const r of this._results){
|
|
1853
|
+
r.responses.forEach((res,i)=>{
|
|
1854
|
+
totals[i].avgLatency+=res.latencyMs; totals[i].count++;
|
|
1855
|
+
if(r.scores?.[i]) totals[i].avgScore+=r.scores[i].overall||0;
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
return totals.map(t=>({
|
|
1859
|
+
variant:t.variant,
|
|
1860
|
+
avgLatency: t.count?Math.round(t.avgLatency/t.count):0,
|
|
1861
|
+
avgScore: t.count?+(t.avgScore/t.count).toFixed(2):0,
|
|
1862
|
+
runs:t.count,
|
|
1863
|
+
}));
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
/** Compress a prompt to fit within a token budget (trim middle) */
|
|
1868
|
+
function compressPrompt(text, maxTokens=2000) {
|
|
1869
|
+
const est = tokenizer.estimate(text);
|
|
1870
|
+
if (est <= maxTokens) return text;
|
|
1871
|
+
const ratio = maxTokens / est;
|
|
1872
|
+
const keep = Math.floor(text.length * ratio * 0.9);
|
|
1873
|
+
const half = Math.floor(keep/2);
|
|
1874
|
+
return text.slice(0,half) + '\n\n[... content trimmed for context window ...]\n\n' + text.slice(-half);
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
/** Validate that conversation messages alternate correctly */
|
|
1878
|
+
function validateMessages(messages) {
|
|
1879
|
+
const errors=[];
|
|
1880
|
+
for(let i=0;i<messages.length;i++){
|
|
1881
|
+
const m=messages[i];
|
|
1882
|
+
if(!m.role) errors.push(`Message ${i}: missing role`);
|
|
1883
|
+
if(!m.content&&!m.tool_calls) errors.push(`Message ${i}: missing content`);
|
|
1884
|
+
if(i>0&&m.role==='user'&&messages[i-1].role==='user') errors.push(`Message ${i}: consecutive user messages`);
|
|
1885
|
+
if(i>0&&m.role==='assistant'&&messages[i-1].role==='assistant') errors.push(`Message ${i}: consecutive assistant messages`);
|
|
1886
|
+
}
|
|
1887
|
+
return { valid:errors.length===0, errors };
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1891
|
+
// §27 Provider auto-detect from API key prefix
|
|
1892
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1893
|
+
|
|
1894
|
+
function detectProvider(apiKey='') {
|
|
1895
|
+
if (!apiKey) return 'openai';
|
|
1896
|
+
if (apiKey.startsWith('sk-ant-')) return 'anthropic';
|
|
1897
|
+
if (apiKey.startsWith('AIza')) return 'gemini';
|
|
1898
|
+
if (apiKey.startsWith('gsk_')) return 'groq';
|
|
1899
|
+
if (apiKey.startsWith('AZ')) return 'together';
|
|
1900
|
+
return 'openai';
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1904
|
+
// §28 kitai namespace
|
|
1905
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1906
|
+
|
|
1907
|
+
const kitai = {
|
|
1908
|
+
|
|
1909
|
+
// ── Meta ────────────────────────────────────────────────────────────────
|
|
1910
|
+
version: '1.0.0',
|
|
1911
|
+
|
|
1912
|
+
// ── Core classes ────────────────────────────────────────────────────────
|
|
1913
|
+
KitAIFormat,
|
|
1914
|
+
KitAIClient,
|
|
1915
|
+
PromptTemplate,
|
|
1916
|
+
FewShotBuilder,
|
|
1917
|
+
VectorStore,
|
|
1918
|
+
RAGPipeline,
|
|
1919
|
+
LRUCache,
|
|
1920
|
+
ToolRegistry,
|
|
1921
|
+
ReactAgent,
|
|
1922
|
+
JSONEnforcer,
|
|
1923
|
+
LLMJudge,
|
|
1924
|
+
ABExperiment,
|
|
1925
|
+
SequentialChain,
|
|
1926
|
+
ParallelChain,
|
|
1927
|
+
SlidingWindowMemory,
|
|
1928
|
+
TokenBudgetMemory,
|
|
1929
|
+
SummaryMemory,
|
|
1930
|
+
|
|
1931
|
+
// ── Sub-namespaces ───────────────────────────────────────────────────────
|
|
1932
|
+
tokenizer,
|
|
1933
|
+
cost,
|
|
1934
|
+
schema,
|
|
1935
|
+
parsers,
|
|
1936
|
+
classify,
|
|
1937
|
+
metrics,
|
|
1938
|
+
guard,
|
|
1939
|
+
chunker,
|
|
1940
|
+
scaffolds,
|
|
1941
|
+
systemPrompts,
|
|
1942
|
+
exporter,
|
|
1943
|
+
|
|
1944
|
+
// ── Utility functions ────────────────────────────────────────────────────
|
|
1945
|
+
stripMarkdown,
|
|
1946
|
+
diffText,
|
|
1947
|
+
semanticSimilarity,
|
|
1948
|
+
compressPrompt,
|
|
1949
|
+
validateMessages,
|
|
1950
|
+
detectProvider,
|
|
1951
|
+
|
|
1952
|
+
// ── Model catalogue ──────────────────────────────────────────────────────
|
|
1953
|
+
models: {
|
|
1954
|
+
all: () => Object.entries(_MODELS).map(([id,m])=>({id,...m})),
|
|
1955
|
+
byProvider: (p) => Object.entries(_MODELS).filter(([,m])=>m.provider===p).map(([id,m])=>({id,...m})),
|
|
1956
|
+
byType: (t) => Object.entries(_MODELS).filter(([,m])=>m.type===t).map(([id,m])=>({id,...m})),
|
|
1957
|
+
byFamily: (f) => Object.entries(_MODELS).filter(([,m])=>m.family===f).map(([id,m])=>({id,...m})),
|
|
1958
|
+
get: (id)=> _MODELS[id]||null,
|
|
1959
|
+
contextWindow: (id)=> _MODELS[id]?.context||null,
|
|
1960
|
+
providers: () => [...new Set(Object.values(_MODELS).map(m=>m.provider))],
|
|
1961
|
+
families: () => [...new Set(Object.values(_MODELS).map(m=>m.family))],
|
|
1962
|
+
cheapest: (type='chat') => cost.cheapest(type),
|
|
1963
|
+
largest: (type='chat') => Object.entries(_MODELS)
|
|
1964
|
+
.filter(([,m])=>m.type===type)
|
|
1965
|
+
.sort((a,b)=>b[1].context-a[1].context)[0]?.[0]||null,
|
|
1966
|
+
register(id, spec) { _MODELS[id]=spec; },
|
|
1967
|
+
},
|
|
1968
|
+
|
|
1969
|
+
// ── Provider registry ────────────────────────────────────────────────────
|
|
1970
|
+
providers: {
|
|
1971
|
+
all: () => Object.keys(_PROVIDERS),
|
|
1972
|
+
get: (name) => _PROVIDERS[name]||null,
|
|
1973
|
+
register: (name, spec) => { _PROVIDERS[name]=spec; },
|
|
1974
|
+
},
|
|
1975
|
+
|
|
1976
|
+
// ── Built-in tools ───────────────────────────────────────────────────────
|
|
1977
|
+
builtinTools: _builtinTools,
|
|
1978
|
+
|
|
1979
|
+
/** Create a ToolRegistry pre-loaded with all built-in tools */
|
|
1980
|
+
defaultToolRegistry() {
|
|
1981
|
+
const reg = new ToolRegistry();
|
|
1982
|
+
for(const [name,{description,fn}] of Object.entries(_builtinTools))
|
|
1983
|
+
reg.register(name, description, fn);
|
|
1984
|
+
return reg;
|
|
1985
|
+
},
|
|
1986
|
+
|
|
1987
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1988
|
+
// FEATURE 1 – Multi-provider client factory
|
|
1989
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1990
|
+
/**
|
|
1991
|
+
* Create a KitAIClient.
|
|
1992
|
+
* @param {object|string} config – full config object, or just an apiKey string
|
|
1993
|
+
*/
|
|
1994
|
+
client(config={}) {
|
|
1995
|
+
if (typeof config === 'string') config = { apiKey: config };
|
|
1996
|
+
return new KitAIClient(config);
|
|
1997
|
+
},
|
|
1998
|
+
|
|
1999
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2000
|
+
// FEATURE 3 – Stream aggregator (async iter → full string)
|
|
2001
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2002
|
+
async collectStream(asyncIter) {
|
|
2003
|
+
let out='';
|
|
2004
|
+
for await(const chunk of asyncIter) out+=chunk;
|
|
2005
|
+
return out;
|
|
2006
|
+
},
|
|
2007
|
+
|
|
2008
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2009
|
+
// FEATURE 10 – Quick model info lookup
|
|
2010
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2011
|
+
model: (id) => _MODELS[id] || null,
|
|
2012
|
+
|
|
2013
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2014
|
+
// FEATURE 11 – Prompt template factory
|
|
2015
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2016
|
+
prompt(template, defaults={}) { return new PromptTemplate(template, defaults); },
|
|
2017
|
+
|
|
2018
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2019
|
+
// FEATURE 13 – System prompt helper
|
|
2020
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2021
|
+
system(name, vars={}) {
|
|
2022
|
+
let content = typeof systemPrompts[name]==='function' ? systemPrompts[name](vars.content||'') : systemPrompts[name];
|
|
2023
|
+
if (!content) content = name; // allow passing raw content
|
|
2024
|
+
for(const [k,v] of Object.entries(vars)) content=content.replace(`{${k}}`,v);
|
|
2025
|
+
return { role:'system', content };
|
|
2026
|
+
},
|
|
2027
|
+
|
|
2028
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2029
|
+
// FEATURE 17 – Memory factories
|
|
2030
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2031
|
+
memory: {
|
|
2032
|
+
sliding: (maxTurns=10) => new SlidingWindowMemory(maxTurns),
|
|
2033
|
+
budget: (budget=3000, model='') => new TokenBudgetMemory(budget, model),
|
|
2034
|
+
summary: (client, trigger=10) => new SummaryMemory(client, trigger),
|
|
2035
|
+
},
|
|
2036
|
+
|
|
2037
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2038
|
+
// FEATURE 21 – RAG factory
|
|
2039
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2040
|
+
rag(client, opts={}) { return new RAGPipeline(client, new VectorStore(), opts); },
|
|
2041
|
+
|
|
2042
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2043
|
+
// FEATURE 23 – Sequential chain factory
|
|
2044
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2045
|
+
chain(client, opts={}) { return new SequentialChain(client, opts); },
|
|
2046
|
+
parallel(client, opts={}) { return new ParallelChain(client, opts); },
|
|
2047
|
+
|
|
2048
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2049
|
+
// FEATURE 26 – ReAct agent factory
|
|
2050
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2051
|
+
agent(client, registry=kitai.defaultToolRegistry(), opts={}) {
|
|
2052
|
+
return new ReactAgent(client, registry, opts);
|
|
2053
|
+
},
|
|
2054
|
+
|
|
2055
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2056
|
+
// FEATURE 29 – JSON enforcer factory
|
|
2057
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2058
|
+
enforce(client, schemaObj, maxRetries=3) { return new JSONEnforcer(client, schemaObj, maxRetries); },
|
|
2059
|
+
|
|
2060
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2061
|
+
// FEATURE 34 – LLM judge factory
|
|
2062
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2063
|
+
judge(client, opts={}) { return new LLMJudge(client, opts); },
|
|
2064
|
+
|
|
2065
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2066
|
+
// FEATURE 38 – Retry + exponential backoff wrapper
|
|
2067
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2068
|
+
/**
|
|
2069
|
+
* Wrap any async fn with retry + exponential backoff + optional fallback.
|
|
2070
|
+
* @param {Function} fn
|
|
2071
|
+
* @param {object} [opts]
|
|
2072
|
+
* @param {number} [opts.maxRetries]
|
|
2073
|
+
* @param {number} [opts.baseDelayMs]
|
|
2074
|
+
* @param {Function[]} [opts.fallbacks] – list of alternative fns to try after all retries fail
|
|
2075
|
+
*/
|
|
2076
|
+
async retry(fn, opts={}) {
|
|
2077
|
+
const { maxRetries=3, baseDelayMs=500, fallbacks=[] } = opts;
|
|
2078
|
+
let lastErr;
|
|
2079
|
+
for(let i=0;i<=maxRetries;i++){
|
|
2080
|
+
try { return await fn(); }
|
|
2081
|
+
catch (e) {
|
|
2082
|
+
lastErr=e;
|
|
2083
|
+
if(i<maxRetries) await _sleep(Math.min(baseDelayMs*2**i, 30000));
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
for(const fallback of fallbacks){
|
|
2087
|
+
try { return await fallback(); } catch(e) { lastErr=e; }
|
|
2088
|
+
}
|
|
2089
|
+
throw lastErr;
|
|
2090
|
+
},
|
|
2091
|
+
|
|
2092
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2093
|
+
// FEATURE 39 – Token estimation shortcuts
|
|
2094
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2095
|
+
estimateTokens(text) { return tokenizer.estimate(text); },
|
|
2096
|
+
estimateMessages(messages) { return tokenizer.estimateMessages(messages); },
|
|
2097
|
+
fitsContext(messages, model) { return tokenizer.fitsInContext(messages, model); },
|
|
2098
|
+
|
|
2099
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2100
|
+
// FEATURE 41 – Context window advisor
|
|
2101
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2102
|
+
/**
|
|
2103
|
+
* Given text content and desired reserve, recommend the cheapest model
|
|
2104
|
+
* whose context window fits.
|
|
2105
|
+
*/
|
|
2106
|
+
adviseBestModel(messages, reserveTokens=1000, type='chat') {
|
|
2107
|
+
const used = tokenizer.estimateMessages(messages);
|
|
2108
|
+
const candidates = Object.entries(_MODELS)
|
|
2109
|
+
.filter(([,m]) => m.type===type && m.context >= used+reserveTokens)
|
|
2110
|
+
.sort((a,b)=>a[1].inputPer1M-b[1].inputPer1M);
|
|
2111
|
+
return candidates.map(([id,m])=>({ id, contextWindow:m.context, usedTokens:used, inputPer1M:m.inputPer1M }));
|
|
2112
|
+
},
|
|
2113
|
+
|
|
2114
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2115
|
+
// FEATURE 44 – Cache factory
|
|
2116
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2117
|
+
cache(maxSize=256) { return new LRUCache(maxSize); },
|
|
2118
|
+
|
|
2119
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2120
|
+
// FEATURE 49 – A/B experiment factory
|
|
2121
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2122
|
+
experiment(clients, opts={}) { return new ABExperiment(clients, opts); },
|
|
2123
|
+
|
|
2124
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2125
|
+
// FEATURE 48 – Semantic similarity (async, needs client for embed)
|
|
2126
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2127
|
+
async similarityScore(client, textA, textB, model='text-embedding-3-small') {
|
|
2128
|
+
const [[a],[b]] = await Promise.all([
|
|
2129
|
+
client.embed(textA,{model}),
|
|
2130
|
+
client.embed(textB,{model}),
|
|
2131
|
+
]);
|
|
2132
|
+
return _cosineSim(a,b);
|
|
2133
|
+
},
|
|
2134
|
+
|
|
2135
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2136
|
+
// Convenience: one-shot chat without creating a client first
|
|
2137
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2138
|
+
async ask(apiKey, question, opts={}) {
|
|
2139
|
+
const c = kitai.client({ apiKey, ...opts });
|
|
2140
|
+
const res = await c.chat([{role:'user',content:question}], opts);
|
|
2141
|
+
return res;
|
|
2142
|
+
},
|
|
2143
|
+
};
|
|
2144
|
+
|
|
2145
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2146
|
+
// §29 Exports (same pattern as KitGPS)
|
|
2147
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2148
|
+
let kitdef = {};
|
|
2149
|
+
kitdef = kitai;
|
|
2150
|
+
kitdef.default = kitai;
|
|
2151
|
+
kitdef.KitAIFormat = KitAIFormat;
|
|
2152
|
+
kitdef.KitAIClient = KitAIClient;
|
|
2153
|
+
kitdef.PromptTemplate = PromptTemplate;
|
|
2154
|
+
kitdef.FewShotBuilder = FewShotBuilder;
|
|
2155
|
+
kitdef.VectorStore = VectorStore;
|
|
2156
|
+
kitdef.RAGPipeline = RAGPipeline;
|
|
2157
|
+
kitdef.ToolRegistry = ToolRegistry;
|
|
2158
|
+
kitdef.ReactAgent = ReactAgent;
|
|
2159
|
+
kitdef.JSONEnforcer = JSONEnforcer;
|
|
2160
|
+
kitdef.LLMJudge = LLMJudge;
|
|
2161
|
+
kitdef.ABExperiment = ABExperiment;
|
|
2162
|
+
kitdef.SequentialChain= SequentialChain;
|
|
2163
|
+
kitdef.ParallelChain = ParallelChain;
|
|
2164
|
+
kitdef.SlidingWindowMemory = SlidingWindowMemory;
|
|
2165
|
+
kitdef.TokenBudgetMemory = TokenBudgetMemory;
|
|
2166
|
+
kitdef.SummaryMemory = SummaryMemory;
|
|
2167
|
+
kitdef.LRUCache = LRUCache;
|
|
2168
|
+
kitdef.tokenizer = tokenizer;
|
|
2169
|
+
kitdef.cost = cost;
|
|
2170
|
+
kitdef.schema = schema;
|
|
2171
|
+
kitdef.parsers = parsers;
|
|
2172
|
+
kitdef.classify = classify;
|
|
2173
|
+
kitdef.metrics = metrics;
|
|
2174
|
+
kitdef.guard = guard;
|
|
2175
|
+
kitdef.chunker = chunker;
|
|
2176
|
+
kitdef.scaffolds = scaffolds;
|
|
2177
|
+
kitdef.systemPrompts = systemPrompts;
|
|
2178
|
+
kitdef.exporter = exporter;
|
|
2179
|
+
kitdef.stripMarkdown = stripMarkdown;
|
|
2180
|
+
kitdef.diffText = diffText;
|
|
2181
|
+
kitdef.semanticSimilarity = semanticSimilarity;
|
|
2182
|
+
kitdef.compressPrompt = compressPrompt;
|
|
2183
|
+
kitdef.validateMessages = validateMessages;
|
|
2184
|
+
kitdef.detectProvider = detectProvider;
|
|
2185
|
+
module.exports = { kitdef };
|