notoken-core 1.6.0 → 2.0.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.
Files changed (99) hide show
  1. package/config/chat-responses.json +767 -0
  2. package/config/concept-clusters.json +31 -0
  3. package/config/entities.json +93 -0
  4. package/config/image-prompts.json +20 -0
  5. package/config/intent-vectors.json +1 -0
  6. package/config/intents.json +4946 -83
  7. package/config/ollama-models.json +193 -0
  8. package/config/rules.json +32 -1
  9. package/dist/automation/discordPatchright.d.ts +35 -0
  10. package/dist/automation/discordPatchright.js +424 -0
  11. package/dist/automation/discordSetup.d.ts +31 -0
  12. package/dist/automation/discordSetup.js +338 -0
  13. package/dist/conversation/coreference.js +44 -4
  14. package/dist/conversation/pendingActions.d.ts +55 -0
  15. package/dist/conversation/pendingActions.js +127 -0
  16. package/dist/conversation/store.d.ts +72 -0
  17. package/dist/conversation/store.js +140 -1
  18. package/dist/conversation/topicTracker.d.ts +36 -0
  19. package/dist/conversation/topicTracker.js +141 -0
  20. package/dist/execution/ssh.d.ts +42 -1
  21. package/dist/execution/ssh.js +532 -3
  22. package/dist/handlers/executor.js +3981 -16
  23. package/dist/index.d.ts +25 -3
  24. package/dist/index.js +36 -2
  25. package/dist/nlp/batchParser.d.ts +30 -0
  26. package/dist/nlp/batchParser.js +77 -0
  27. package/dist/nlp/conceptExpansion.d.ts +54 -0
  28. package/dist/nlp/conceptExpansion.js +136 -0
  29. package/dist/nlp/conceptRouter.d.ts +49 -0
  30. package/dist/nlp/conceptRouter.js +302 -0
  31. package/dist/nlp/confidenceCalibrator.d.ts +62 -0
  32. package/dist/nlp/confidenceCalibrator.js +116 -0
  33. package/dist/nlp/correctionLearner.d.ts +45 -0
  34. package/dist/nlp/correctionLearner.js +207 -0
  35. package/dist/nlp/entitySpellCorrect.d.ts +35 -0
  36. package/dist/nlp/entitySpellCorrect.js +141 -0
  37. package/dist/nlp/knowledgeGraph.d.ts +70 -0
  38. package/dist/nlp/knowledgeGraph.js +380 -0
  39. package/dist/nlp/llmFallback.js +28 -1
  40. package/dist/nlp/multiClassifier.js +91 -6
  41. package/dist/nlp/multiIntent.d.ts +43 -0
  42. package/dist/nlp/multiIntent.js +154 -0
  43. package/dist/nlp/parseIntent.d.ts +6 -1
  44. package/dist/nlp/parseIntent.js +180 -5
  45. package/dist/nlp/ruleParser.js +315 -0
  46. package/dist/nlp/semanticSimilarity.d.ts +30 -0
  47. package/dist/nlp/semanticSimilarity.js +174 -0
  48. package/dist/nlp/vocabularyBuilder.d.ts +43 -0
  49. package/dist/nlp/vocabularyBuilder.js +224 -0
  50. package/dist/nlp/wikidata.d.ts +49 -0
  51. package/dist/nlp/wikidata.js +228 -0
  52. package/dist/policy/confirm.d.ts +10 -0
  53. package/dist/policy/confirm.js +39 -0
  54. package/dist/policy/safety.js +6 -4
  55. package/dist/utils/aliases.d.ts +5 -0
  56. package/dist/utils/aliases.js +39 -0
  57. package/dist/utils/analysis.js +71 -15
  58. package/dist/utils/browser.d.ts +64 -0
  59. package/dist/utils/browser.js +364 -0
  60. package/dist/utils/commandHistory.d.ts +20 -0
  61. package/dist/utils/commandHistory.js +108 -0
  62. package/dist/utils/completer.d.ts +17 -0
  63. package/dist/utils/completer.js +79 -0
  64. package/dist/utils/config.js +32 -2
  65. package/dist/utils/dbQuery.d.ts +25 -0
  66. package/dist/utils/dbQuery.js +248 -0
  67. package/dist/utils/discordDiag.d.ts +35 -0
  68. package/dist/utils/discordDiag.js +826 -0
  69. package/dist/utils/diskCleanup.d.ts +36 -0
  70. package/dist/utils/diskCleanup.js +775 -0
  71. package/dist/utils/entityResolver.d.ts +107 -0
  72. package/dist/utils/entityResolver.js +468 -0
  73. package/dist/utils/imageGen.d.ts +92 -0
  74. package/dist/utils/imageGen.js +2031 -0
  75. package/dist/utils/installTracker.d.ts +57 -0
  76. package/dist/utils/installTracker.js +160 -0
  77. package/dist/utils/multiExec.d.ts +21 -0
  78. package/dist/utils/multiExec.js +141 -0
  79. package/dist/utils/openclawDiag.d.ts +29 -0
  80. package/dist/utils/openclawDiag.js +1035 -0
  81. package/dist/utils/output.js +4 -0
  82. package/dist/utils/platform.js +2 -1
  83. package/dist/utils/progressReporter.d.ts +50 -0
  84. package/dist/utils/progressReporter.js +58 -0
  85. package/dist/utils/projectDetect.d.ts +44 -0
  86. package/dist/utils/projectDetect.js +319 -0
  87. package/dist/utils/projectScanner.d.ts +44 -0
  88. package/dist/utils/projectScanner.js +312 -0
  89. package/dist/utils/shellCompat.d.ts +78 -0
  90. package/dist/utils/shellCompat.js +186 -0
  91. package/dist/utils/smartArchive.d.ts +16 -0
  92. package/dist/utils/smartArchive.js +172 -0
  93. package/dist/utils/smartRetry.d.ts +26 -0
  94. package/dist/utils/smartRetry.js +114 -0
  95. package/dist/utils/updater.d.ts +1 -0
  96. package/dist/utils/updater.js +1 -1
  97. package/dist/utils/version.d.ts +20 -0
  98. package/dist/utils/version.js +212 -0
  99. package/package.json +6 -3
@@ -0,0 +1,302 @@
1
+ /**
2
+ * Concept-based intent router.
3
+ *
4
+ * Instead of matching exact synonym phrases, this extracts concepts
5
+ * from the user's input and routes to the right intent domain.
6
+ *
7
+ * How it works:
8
+ * 1. Tokenize with compromise (POS tags)
9
+ * 2. Extract: action verbs, subject nouns, attribute adjectives, question type
10
+ * 3. Map concepts to intent domains via a concept→domain map
11
+ * 4. Pick the best intent within that domain
12
+ *
13
+ * This handles:
14
+ * "is this happening offline or locally is it free or using cloud?"
15
+ * → concepts: [offline, local, free, cloud] → domain: ai.image_status
16
+ *
17
+ * "can you check what crontabs I have running"
18
+ * → action: check, subject: crontabs → domain: cron.list
19
+ */
20
+ import { tokenize, parseDependencies } from "./semantic.js";
21
+ // ─── Concept → Domain Map ──────────────────────────────────────────────────
22
+ // Maps nouns/concepts to intent domains. When a user mentions these concepts,
23
+ // the router knows which intent area to look in.
24
+ const CONCEPT_DOMAINS = {
25
+ // Image generation
26
+ "image": ["ai.generate_image", "ai.image_status"],
27
+ "picture": ["ai.generate_image", "ai.image_status"],
28
+ "photo": ["ai.generate_image", "ai.image_status"],
29
+ "generate": ["ai.generate_image"],
30
+ "stable diffusion": ["ai.image_status", "ai.install_sd"],
31
+ "offline": ["ai.image_status"],
32
+ "cloud": ["ai.image_status"],
33
+ "local": ["ai.image_status"],
34
+ "private": ["ai.image_status"],
35
+ // Server / system
36
+ "crontab": ["cron.list"],
37
+ "cron": ["cron.list", "cron.add", "cron.remove"],
38
+ "uptime": ["server.uptime"],
39
+ "load": ["server.uptime"],
40
+ "memory": ["server.check_memory"],
41
+ "ram": ["server.check_memory"],
42
+ "disk": ["server.check_disk"],
43
+ "storage": ["server.check_disk"],
44
+ "cpu": ["server.uptime", "hardware.info"],
45
+ // Network
46
+ "ip": ["network.ip"],
47
+ "dns": ["dns.lookup"],
48
+ "port": ["network.ports", "firewall.open"],
49
+ "firewall": ["firewall.list"],
50
+ "traceroute": ["network.traceroute"],
51
+ "speed": ["network.speedtest"],
52
+ "bandwidth": ["network.bandwidth"],
53
+ "connection": ["network.connections"],
54
+ // Docker
55
+ "container": ["docker.ps", "docker.restart"],
56
+ "docker": ["docker.ps", "docker.restart"],
57
+ // Git
58
+ "commit": ["git.status", "git.commit"],
59
+ "branch": ["git.branch"],
60
+ "repo": ["git.status"],
61
+ // Files
62
+ "file": ["dir.list", "files.find"],
63
+ "folder": ["dir.list", "project.scan"],
64
+ "directory": ["dir.list", "project.scan"],
65
+ "project": ["project.scan", "project.info"],
66
+ "media": ["files.find_media"],
67
+ "movie": ["files.find_media"],
68
+ "video": ["files.find_media"],
69
+ "music": ["files.find_media"],
70
+ "photos": ["files.find_media"],
71
+ // Services
72
+ "nginx": ["service.status", "service.restart"],
73
+ "redis": ["service.status", "service.restart"],
74
+ "postgres": ["service.status", "service.restart"],
75
+ // System
76
+ "hostname": ["system.hostname"],
77
+ "timezone": ["system.timezone"],
78
+ "kernel": ["system.kernel"],
79
+ "env": ["system.env"],
80
+ "hardware": ["hardware.info"],
81
+ "reboot": ["system.reboot_history"],
82
+ "update": ["package.audit"],
83
+ "package": ["package.audit"],
84
+ "vulnerability": ["package.audit"],
85
+ // Browser
86
+ "browser": ["browser.status", "browser.open"],
87
+ "browse": ["browser.open"],
88
+ "website": ["browser.open"],
89
+ "url": ["browser.open"],
90
+ };
91
+ /**
92
+ * Merge additional concept→domain mappings into the router at runtime.
93
+ * Used by the vocabulary builder to inject learned concepts.
94
+ * Existing domains for a concept are preserved; new ones are appended.
95
+ */
96
+ export function mergeConceptDomains(mappings) {
97
+ for (const [concept, domains] of Object.entries(mappings)) {
98
+ const existing = CONCEPT_DOMAINS[concept];
99
+ if (existing) {
100
+ // Append only new domains
101
+ const existingSet = new Set(existing);
102
+ for (const d of domains) {
103
+ if (!existingSet.has(d)) {
104
+ existing.push(d);
105
+ }
106
+ }
107
+ }
108
+ else {
109
+ CONCEPT_DOMAINS[concept] = [...domains];
110
+ }
111
+ }
112
+ }
113
+ // Question words that indicate an info/status query (not an action)
114
+ const QUESTION_PATTERNS = [
115
+ "is", "are", "was", "were", "do", "does", "did",
116
+ "what", "which", "where", "how", "why", "when",
117
+ "can", "could", "will", "would",
118
+ ];
119
+ // Action verbs that indicate status/info queries
120
+ const STATUS_VERBS = new Set([
121
+ "check", "show", "list", "view", "display", "see", "tell",
122
+ "status", "info", "information", "report",
123
+ ]);
124
+ // Action verbs that indicate mutation/execution
125
+ const ACTION_VERBS = new Set([
126
+ "restart", "stop", "start", "kill", "install", "uninstall", "remove",
127
+ "create", "make", "generate", "draw", "paint", "build",
128
+ "open", "close", "block", "allow",
129
+ "update", "upgrade", "fix", "repair",
130
+ "send", "copy", "move", "delete", "tar", "zip",
131
+ ]);
132
+ // ─── Verb+Object → Intent Map ─────────────────────────────────────────────
133
+ // Maps verb synonyms to object→intent lookups. Used by dependency-based routing.
134
+ const VERB_OBJECT_MAP = {
135
+ restart: { nginx: "service.restart", redis: "service.restart", postgres: "service.restart", docker: "docker.restart", gateway: "openclaw.restart", server: "system.reboot" },
136
+ reboot: { nginx: "service.restart", server: "system.reboot", docker: "docker.restart", gateway: "openclaw.restart" },
137
+ bounce: { nginx: "service.restart", redis: "service.restart", docker: "docker.restart" },
138
+ check: { disk: "server.check_disk", storage: "server.check_disk", memory: "server.check_memory", ram: "server.check_memory", port: "network.ports", dns: "dns.lookup" },
139
+ verify: { disk: "server.check_disk", memory: "server.check_memory", port: "network.ports", dns: "dns.lookup" },
140
+ test: { disk: "server.check_disk", memory: "server.check_memory", port: "network.ports", speed: "network.speedtest", connection: "network.connections" },
141
+ show: { logs: "logs.tail", log: "logs.tail", processes: "process.list", containers: "docker.ps", ports: "network.ports", crontabs: "cron.list", files: "dir.list" },
142
+ list: { logs: "logs.tail", processes: "process.list", containers: "docker.ps", ports: "network.ports", crontabs: "cron.list", files: "dir.list", services: "service.status" },
143
+ display: { logs: "logs.tail", processes: "process.list", containers: "docker.ps" },
144
+ kill: { process: "process.kill", container: "docker.restart" },
145
+ stop: { process: "process.kill", container: "docker.restart", nginx: "service.restart", redis: "service.restart" },
146
+ terminate: { process: "process.kill" },
147
+ block: { ip: "firewall.block_ip", address: "firewall.block_ip", port: "firewall.block_ip" },
148
+ allow: { ip: "firewall.open", port: "firewall.open" },
149
+ backup: { database: "backup.create", db: "backup.create", files: "backup.create", server: "backup.create" },
150
+ create: { backup: "backup.create", crontab: "cron.add", cron: "cron.add", branch: "git.branch" },
151
+ find: { file: "files.find", files: "files.find", media: "files.find_media", movie: "files.find_media", video: "files.find_media" },
152
+ search: { file: "files.find", files: "files.find", media: "files.find_media" },
153
+ tail: { logs: "logs.tail", log: "logs.tail" },
154
+ open: { browser: "browser.open", url: "browser.open", website: "browser.open" },
155
+ remove: { crontab: "cron.remove", cron: "cron.remove", container: "docker.restart" },
156
+ delete: { crontab: "cron.remove", cron: "cron.remove", file: "files.find" },
157
+ };
158
+ /**
159
+ * Route by parsing dependency structure (SVO triples).
160
+ * Maps verb+object combinations to specific intents with graduated confidence.
161
+ */
162
+ export function routeByDependencies(rawText) {
163
+ const text = rawText.toLowerCase().trim();
164
+ const tokens = tokenize(text, [], []);
165
+ const deps = parseDependencies(tokens);
166
+ const verbToken = tokens.find(t => t.tag === "VERB");
167
+ if (!verbToken)
168
+ return null;
169
+ const verb = verbToken.root ?? verbToken.text;
170
+ const objectMap = VERB_OBJECT_MAP[verb];
171
+ if (!objectMap)
172
+ return null;
173
+ // Extract object and location from dependencies
174
+ const objDep = deps.find(d => d.relation === "object");
175
+ const locDep = deps.find(d => d.relation === "location");
176
+ const objWord = objDep?.dependent.normalized ?? objDep?.dependent.text;
177
+ const locWord = locDep?.dependent.normalized ?? locDep?.dependent.text;
178
+ // Also check all nouns/services as fallback (deps might not tag everything)
179
+ const candidateObjects = objWord
180
+ ? [objWord, objWord.replace(/s$/, "")]
181
+ : tokens
182
+ .filter(t => ["NOUN", "SERVICE"].includes(t.tag) && t.index > verbToken.index)
183
+ .flatMap(t => [t.normalized ?? t.text, (t.normalized ?? t.text).replace(/s$/, "")]);
184
+ // Try to match verb+object
185
+ let matchedIntent;
186
+ let matchedObj;
187
+ for (const candidate of candidateObjects) {
188
+ if (objectMap[candidate]) {
189
+ matchedIntent = objectMap[candidate];
190
+ matchedObj = candidate;
191
+ break;
192
+ }
193
+ }
194
+ if (!matchedIntent) {
195
+ // Verb matched but no object — low confidence
196
+ // Pick first intent from the verb's map as a guess
197
+ const firstIntent = Object.values(objectMap)[0];
198
+ return {
199
+ intent: firstIntent,
200
+ confidence: 0.5,
201
+ verb,
202
+ reason: `Dep: verb "${verb}" (no object match)`,
203
+ };
204
+ }
205
+ // Verb+object matched; boost further if location present
206
+ const confidence = locWord ? 0.85 : 0.7;
207
+ return {
208
+ intent: matchedIntent,
209
+ confidence,
210
+ verb,
211
+ object: matchedObj,
212
+ location: locWord,
213
+ reason: `Dep: "${verb}" + "${matchedObj}"${locWord ? ` on "${locWord}"` : ""}`,
214
+ };
215
+ }
216
+ /**
217
+ * Route user input to intent by understanding concepts, not matching phrases.
218
+ */
219
+ export function routeByConcepts(rawText) {
220
+ const text = rawText.toLowerCase().trim();
221
+ const tokens = tokenize(text, [], []);
222
+ // Extract key parts
223
+ const verbs = tokens.filter(t => t.tag === "VERB").map(t => t.text.toLowerCase());
224
+ const nouns = tokens.filter(t => ["NOUN", "SERVICE", "ADJ"].includes(t.tag)).map(t => t.text.toLowerCase());
225
+ const allWords = text.split(/\s+/);
226
+ // Is this a question?
227
+ const isQuestion = QUESTION_PATTERNS.some(q => allWords[0] === q) || text.endsWith("?");
228
+ const isStatusQuery = isQuestion || verbs.some(v => STATUS_VERBS.has(v));
229
+ // Find matching concepts
230
+ const matchedDomains = new Map();
231
+ const matchedConcepts = [];
232
+ // Check ALL words against concept map (not just POS-tagged nouns/verbs)
233
+ // because domain terms like "crontab", "docker", "nginx" may not be tagged correctly
234
+ for (const word of allWords) {
235
+ const domains = CONCEPT_DOMAINS[word];
236
+ if (domains) {
237
+ matchedConcepts.push(word);
238
+ for (const domain of domains) {
239
+ matchedDomains.set(domain, (matchedDomains.get(domain) ?? 0) + 1);
240
+ }
241
+ }
242
+ // Also check plurals/variants (crontabs → crontab, containers → container)
243
+ const singular = word.replace(/s$/, "");
244
+ if (singular !== word) {
245
+ const sDomains = CONCEPT_DOMAINS[singular];
246
+ if (sDomains) {
247
+ matchedConcepts.push(singular);
248
+ for (const domain of sDomains) {
249
+ matchedDomains.set(domain, (matchedDomains.get(domain) ?? 0) + 1);
250
+ }
251
+ }
252
+ }
253
+ }
254
+ // Check bigrams (two-word concepts like "stable diffusion")
255
+ for (let i = 0; i < allWords.length - 1; i++) {
256
+ const bigram = `${allWords[i]} ${allWords[i + 1]}`;
257
+ const domains = CONCEPT_DOMAINS[bigram];
258
+ if (domains) {
259
+ matchedConcepts.push(bigram);
260
+ for (const domain of domains) {
261
+ matchedDomains.set(domain, (matchedDomains.get(domain) ?? 0) + 2); // bigrams worth more
262
+ }
263
+ }
264
+ }
265
+ if (matchedDomains.size === 0)
266
+ return null;
267
+ // Sort by match count
268
+ const sorted = [...matchedDomains.entries()].sort((a, b) => b[1] - a[1]);
269
+ // For questions/status queries, prefer .status/.list/.check intents
270
+ let bestIntent = sorted[0][0];
271
+ if (isStatusQuery) {
272
+ const statusIntents = sorted.filter(([intent]) => intent.includes("status") || intent.includes("list") || intent.includes("check") || intent.includes("info"));
273
+ if (statusIntents.length > 0)
274
+ bestIntent = statusIntents[0][0];
275
+ }
276
+ // For action verbs, prefer action intents
277
+ const hasActionVerb = verbs.some(v => ACTION_VERBS.has(v));
278
+ if (hasActionVerb && !isStatusQuery) {
279
+ const actionIntents = sorted.filter(([intent]) => !intent.includes("status") && !intent.includes("list") && !intent.includes("info"));
280
+ if (actionIntents.length > 0)
281
+ bestIntent = actionIntents[0][0];
282
+ }
283
+ const confidence = Math.min(0.85, 0.5 + matchedConcepts.length * 0.1 + sorted[0][1] * 0.05);
284
+ // Try dependency-based routing and prefer it if higher confidence
285
+ const depResult = routeByDependencies(rawText);
286
+ if (depResult && depResult.confidence > confidence) {
287
+ return {
288
+ intent: depResult.intent,
289
+ confidence: depResult.confidence,
290
+ concepts: matchedConcepts.concat(depResult.object ? [depResult.verb, depResult.object] : [depResult.verb]),
291
+ isQuestion,
292
+ reason: depResult.reason,
293
+ };
294
+ }
295
+ return {
296
+ intent: bestIntent,
297
+ confidence,
298
+ concepts: matchedConcepts,
299
+ isQuestion,
300
+ reason: `Concepts: [${matchedConcepts.join(", ")}] → ${bestIntent}`,
301
+ };
302
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Confidence Calibrator — tracks classifier accuracy over time.
3
+ *
4
+ * Records whether each classifier's votes were correct (user didn't
5
+ * correct them). Over time, adjusts confidence based on track record:
6
+ * - Classifiers with high accuracy get a boost
7
+ * - Classifiers that frequently vote wrong get penalized
8
+ *
9
+ * Persists to ~/.notoken/classifier-stats.json.
10
+ */
11
+ interface ClassifierStats {
12
+ /** Number of times this classifier voted for the winning intent */
13
+ correct: number;
14
+ /** Total votes from this classifier */
15
+ total: number;
16
+ /** Running accuracy (correct/total) */
17
+ accuracy: number;
18
+ /** Calibration multiplier: >1.0 = boosted, <1.0 = penalized */
19
+ multiplier: number;
20
+ }
21
+ /**
22
+ * Record the outcome of a classification round.
23
+ * Call after execution — classifiers that voted for the winning intent
24
+ * get credit, others don't.
25
+ */
26
+ export declare function recordOutcome(votes: Array<{
27
+ classifier: string;
28
+ intent: string;
29
+ }>, winningIntent: string): void;
30
+ /**
31
+ * Get the calibration multiplier for a classifier.
32
+ * Returns 1.0 if no data (neutral).
33
+ */
34
+ export declare function getMultiplier(classifier: string): number;
35
+ /**
36
+ * Apply calibration to a set of classifier votes.
37
+ * Adjusts each vote's confidence by the classifier's track record.
38
+ */
39
+ export declare function calibrateVotes(votes: Array<{
40
+ classifier: string;
41
+ intent: string;
42
+ confidence: number;
43
+ }>): Array<{
44
+ classifier: string;
45
+ intent: string;
46
+ confidence: number;
47
+ }>;
48
+ /**
49
+ * Record that a user corrected a misroute.
50
+ * Penalizes classifiers that voted for the wrong intent.
51
+ */
52
+ export declare function recordCorrection(votes: Array<{
53
+ classifier: string;
54
+ intent: string;
55
+ }>, wrongIntent: string, correctIntent: string): void;
56
+ /**
57
+ * Get stats for display/debugging.
58
+ */
59
+ export declare function getCalibrationStats(): Record<string, ClassifierStats>;
60
+ /** Flush stats to disk. */
61
+ export declare function flushCalibration(): void;
62
+ export {};
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Confidence Calibrator — tracks classifier accuracy over time.
3
+ *
4
+ * Records whether each classifier's votes were correct (user didn't
5
+ * correct them). Over time, adjusts confidence based on track record:
6
+ * - Classifiers with high accuracy get a boost
7
+ * - Classifiers that frequently vote wrong get penalized
8
+ *
9
+ * Persists to ~/.notoken/classifier-stats.json.
10
+ */
11
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
12
+ import { resolve } from "node:path";
13
+ import { homedir } from "node:os";
14
+ const STATS_PATH = resolve(homedir(), ".notoken", "classifier-stats.json");
15
+ let _data = null;
16
+ function load() {
17
+ if (_data)
18
+ return _data;
19
+ if (existsSync(STATS_PATH)) {
20
+ try {
21
+ _data = JSON.parse(readFileSync(STATS_PATH, "utf-8"));
22
+ return _data;
23
+ }
24
+ catch { }
25
+ }
26
+ _data = { classifiers: {}, totalExecutions: 0, lastUpdated: new Date().toISOString() };
27
+ return _data;
28
+ }
29
+ function save() {
30
+ const data = load();
31
+ data.lastUpdated = new Date().toISOString();
32
+ const dir = resolve(STATS_PATH, "..");
33
+ if (!existsSync(dir))
34
+ mkdirSync(dir, { recursive: true });
35
+ writeFileSync(STATS_PATH, JSON.stringify(data, null, 2));
36
+ }
37
+ /**
38
+ * Record the outcome of a classification round.
39
+ * Call after execution — classifiers that voted for the winning intent
40
+ * get credit, others don't.
41
+ */
42
+ export function recordOutcome(votes, winningIntent) {
43
+ const data = load();
44
+ data.totalExecutions++;
45
+ for (const vote of votes) {
46
+ if (!data.classifiers[vote.classifier]) {
47
+ data.classifiers[vote.classifier] = { correct: 0, total: 0, accuracy: 0.5, multiplier: 1.0 };
48
+ }
49
+ const stats = data.classifiers[vote.classifier];
50
+ stats.total++;
51
+ if (vote.intent === winningIntent)
52
+ stats.correct++;
53
+ // Update accuracy with exponential moving average (recent results weighted more)
54
+ const newAccuracy = stats.correct / stats.total;
55
+ stats.accuracy = stats.total < 10 ? newAccuracy : stats.accuracy * 0.9 + newAccuracy * 0.1;
56
+ // Calibration multiplier: sigmoid-like curve centered at 0.5 accuracy
57
+ // accuracy 0.8 → multiplier 1.15, accuracy 0.3 → multiplier 0.85
58
+ stats.multiplier = 0.7 + (stats.accuracy * 0.6);
59
+ }
60
+ // Save every 10 executions
61
+ if (data.totalExecutions % 10 === 0)
62
+ save();
63
+ }
64
+ /**
65
+ * Get the calibration multiplier for a classifier.
66
+ * Returns 1.0 if no data (neutral).
67
+ */
68
+ export function getMultiplier(classifier) {
69
+ const data = load();
70
+ return data.classifiers[classifier]?.multiplier ?? 1.0;
71
+ }
72
+ /**
73
+ * Apply calibration to a set of classifier votes.
74
+ * Adjusts each vote's confidence by the classifier's track record.
75
+ */
76
+ export function calibrateVotes(votes) {
77
+ return votes.map(v => ({
78
+ ...v,
79
+ confidence: Math.min(1.0, v.confidence * getMultiplier(v.classifier)),
80
+ }));
81
+ }
82
+ /**
83
+ * Record that a user corrected a misroute.
84
+ * Penalizes classifiers that voted for the wrong intent.
85
+ */
86
+ export function recordCorrection(votes, wrongIntent, correctIntent) {
87
+ const data = load();
88
+ for (const vote of votes) {
89
+ if (!data.classifiers[vote.classifier]) {
90
+ data.classifiers[vote.classifier] = { correct: 0, total: 0, accuracy: 0.5, multiplier: 1.0 };
91
+ }
92
+ const stats = data.classifiers[vote.classifier];
93
+ if (vote.intent === wrongIntent) {
94
+ // This classifier voted wrong — penalize more strongly
95
+ stats.total += 2; // Double penalty
96
+ stats.accuracy = stats.accuracy * 0.85;
97
+ stats.multiplier = 0.7 + (stats.accuracy * 0.6);
98
+ }
99
+ else if (vote.intent === correctIntent) {
100
+ // This classifier had the right answer — reward
101
+ stats.correct += 2;
102
+ stats.total += 2;
103
+ stats.accuracy = stats.accuracy * 0.9 + 0.1;
104
+ stats.multiplier = 0.7 + (stats.accuracy * 0.6);
105
+ }
106
+ }
107
+ save();
108
+ }
109
+ /**
110
+ * Get stats for display/debugging.
111
+ */
112
+ export function getCalibrationStats() {
113
+ return { ...load().classifiers };
114
+ }
115
+ /** Flush stats to disk. */
116
+ export function flushCalibration() { save(); }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Correction Learner — learns from user corrections after misrouted intents.
3
+ *
4
+ * When the user says "no I meant X" or "not that, I want Y", this module
5
+ * records the correction and uses it to improve future classifications.
6
+ * Uses fuzzy matching so similar phrases benefit from past corrections.
7
+ */
8
+ export interface LearnedCorrection {
9
+ input: string;
10
+ wrongIntent: string;
11
+ correctIntent: string;
12
+ count: number;
13
+ lastSeen: string;
14
+ }
15
+ interface CorrectionMatch {
16
+ intent: string;
17
+ confidence: number;
18
+ }
19
+ /**
20
+ * Record a user correction. Merges with existing entry if the same
21
+ * input+wrongIntent+correctIntent triple already exists.
22
+ */
23
+ export declare function recordCorrection(input: string, wrongIntent: string, correctIntent: string): void;
24
+ /**
25
+ * Check if a user input matches a previously corrected pattern.
26
+ * Uses fuzzy matching so synonymous phrases benefit from past corrections.
27
+ *
28
+ * Returns the corrected intent with a confidence score, or null if no match.
29
+ */
30
+ export declare function checkCorrections(rawText: string): CorrectionMatch | null;
31
+ /**
32
+ * Detect if the user is issuing a correction to a previous misroute.
33
+ *
34
+ * Patterns detected:
35
+ * "no I meant restart the service"
36
+ * "not that, I want to check disk"
37
+ * "wrong, I wanted to see logs"
38
+ * "no, show me the containers"
39
+ * "I said restart not status"
40
+ *
41
+ * Returns the corrected intent text (the part after the correction marker)
42
+ * or null if this is not a correction.
43
+ */
44
+ export declare function detectCorrection(rawText: string, lastIntent: string | null): string | null;
45
+ export {};