capman 0.4.5 → 0.5.1
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/CHANGELOG.md +89 -0
- package/CODEBASE.md +94 -156
- package/README.md +23 -0
- package/bin/lib/cmd-generate.js +20 -3
- package/dist/cjs/cache.d.ts +6 -4
- package/dist/cjs/cache.d.ts.map +1 -1
- package/dist/cjs/cache.js +38 -11
- package/dist/cjs/cache.js.map +1 -1
- package/dist/cjs/engine.d.ts +46 -4
- package/dist/cjs/engine.d.ts.map +1 -1
- package/dist/cjs/engine.js +157 -211
- package/dist/cjs/engine.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/learning.d.ts +16 -1
- package/dist/cjs/learning.d.ts.map +1 -1
- package/dist/cjs/learning.js +161 -10
- package/dist/cjs/learning.js.map +1 -1
- package/dist/cjs/matcher.d.ts +23 -0
- package/dist/cjs/matcher.d.ts.map +1 -1
- package/dist/cjs/matcher.js +53 -18
- package/dist/cjs/matcher.js.map +1 -1
- package/dist/cjs/parser.js +15 -1
- package/dist/cjs/parser.js.map +1 -1
- package/dist/cjs/resolver.d.ts.map +1 -1
- package/dist/cjs/resolver.js +22 -5
- package/dist/cjs/resolver.js.map +1 -1
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/cache.d.ts +6 -4
- package/dist/esm/cache.js +38 -11
- package/dist/esm/engine.d.ts +46 -4
- package/dist/esm/engine.js +158 -212
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/learning.d.ts +16 -1
- package/dist/esm/learning.js +161 -10
- package/dist/esm/matcher.d.ts +23 -0
- package/dist/esm/matcher.js +49 -16
- package/dist/esm/parser.js +15 -1
- package/dist/esm/resolver.js +22 -5
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +1 -1
package/dist/esm/engine.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { match as _match, matchWithLLM as _matchWithLLM, resolverToIntent } from './matcher';
|
|
1
|
+
import { match as _match, matchWithLLM as _matchWithLLM, resolverToIntent, extractParams, STOPWORDS } from './matcher';
|
|
2
2
|
import { resolve as _resolve } from './resolver';
|
|
3
3
|
import { MemoryLearningStore } from './learning';
|
|
4
4
|
import { logger } from './logger';
|
|
@@ -7,9 +7,6 @@ import { VERSION } from './version';
|
|
|
7
7
|
// ─── CapmanEngine ─────────────────────────────────────────────────────────────
|
|
8
8
|
export class CapmanEngine {
|
|
9
9
|
constructor(options) {
|
|
10
|
-
// ── Learning index cache ──────────────────────────────────────────────────
|
|
11
|
-
this.cachedStats = null;
|
|
12
|
-
this.statsInvalidated = true;
|
|
13
10
|
// ── LLM rate limiting state ────────────────────────────────────────────────
|
|
14
11
|
this.llmCallsThisMinute = 0;
|
|
15
12
|
this.llmWindowStart = Date.now();
|
|
@@ -23,6 +20,7 @@ export class CapmanEngine {
|
|
|
23
20
|
this.auth = options.auth;
|
|
24
21
|
this.headers = options.headers;
|
|
25
22
|
this.threshold = options.threshold ?? 50;
|
|
23
|
+
this.cacheTtlMs = options.cacheTtlMs ?? null;
|
|
26
24
|
this.maxLLMCallsPerMinute = options.maxLLMCallsPerMinute ?? 60;
|
|
27
25
|
this.llmCooldownMs = options.llmCooldownMs ?? 0;
|
|
28
26
|
this.llmCircuitBreakerThreshold = options.llmCircuitBreakerThreshold ?? 3;
|
|
@@ -42,8 +40,11 @@ export class CapmanEngine {
|
|
|
42
40
|
const manifestMajorMinor = options.manifest.version?.split('.').slice(0, 2).join('.');
|
|
43
41
|
const engineMajorMinor = VERSION.split('.').slice(0, 2).join('.');
|
|
44
42
|
if (manifestMajorMinor && manifestMajorMinor !== engineMajorMinor) {
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
// Use console.warn directly — must be visible regardless of logger level
|
|
44
|
+
// Default log level is 'silent' so logger.warn would never be seen
|
|
45
|
+
console.warn(`[capman] Manifest version "${options.manifest.version}" was generated with a ` +
|
|
46
|
+
`different engine version than "${VERSION}". If you experience matching issues, ` +
|
|
47
|
+
`regenerate with: npx capman generate`);
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
/**
|
|
@@ -60,12 +61,11 @@ export class CapmanEngine {
|
|
|
60
61
|
async ask(query, overrides = {}) {
|
|
61
62
|
const start = Date.now();
|
|
62
63
|
const steps = [];
|
|
63
|
-
let resolvedVia = 'keyword';
|
|
64
64
|
// ── Step 1: Check cache ──────────────────────────────────────────────────
|
|
65
65
|
const cacheStart = Date.now();
|
|
66
66
|
if (this.cache) {
|
|
67
67
|
const queryKey = normalizeQuery(query);
|
|
68
|
-
const cached = await this.cache.get(queryKey);
|
|
68
|
+
const cached = await this.cache.get(queryKey, this.cacheTtlMs ?? undefined);
|
|
69
69
|
if (cached) {
|
|
70
70
|
steps.push({ type: 'cache_check', status: 'hit', durationMs: Date.now() - cacheStart, detail: 'Served from cache' });
|
|
71
71
|
logger.info(`Cache hit for: "${query}"`);
|
|
@@ -94,116 +94,10 @@ export class CapmanEngine {
|
|
|
94
94
|
steps.push({ type: 'cache_check', status: 'skip', durationMs: 0, detail: 'Cache disabled' });
|
|
95
95
|
}
|
|
96
96
|
// ── Step 2: Match ────────────────────────────────────────────────────────
|
|
97
|
-
let matchResult;
|
|
98
|
-
|
|
99
|
-
case 'cheap': {
|
|
100
|
-
const t = Date.now();
|
|
101
|
-
matchResult = _match(query, this.manifest);
|
|
102
|
-
steps.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: `confidence: ${matchResult.confidence}%` });
|
|
103
|
-
break;
|
|
104
|
-
}
|
|
105
|
-
case 'accurate': {
|
|
106
|
-
if (this.llm) {
|
|
107
|
-
const skipReason = this.checkLLMAllowed();
|
|
108
|
-
if (skipReason) {
|
|
109
|
-
logger.warn(`LLM skipped — ${skipReason} — falling back to keyword`);
|
|
110
|
-
const t = Date.now();
|
|
111
|
-
matchResult = _match(query, this.manifest);
|
|
112
|
-
steps.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: `llm skipped: ${skipReason}` });
|
|
113
|
-
}
|
|
114
|
-
else {
|
|
115
|
-
const t = Date.now();
|
|
116
|
-
try {
|
|
117
|
-
matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
|
|
118
|
-
this.recordLLMSuccess();
|
|
119
|
-
resolvedVia = 'llm';
|
|
120
|
-
steps.push({ type: 'llm_match', status: 'pass', durationMs: Date.now() - t, detail: `confidence: ${matchResult.confidence}%` });
|
|
121
|
-
}
|
|
122
|
-
catch (err) {
|
|
123
|
-
this.recordLLMFailure();
|
|
124
|
-
logger.warn(`LLM call failed — falling back to keyword: ${err}`);
|
|
125
|
-
const t2 = Date.now();
|
|
126
|
-
matchResult = _match(query, this.manifest);
|
|
127
|
-
steps.push({ type: 'llm_match', status: 'fail', durationMs: Date.now() - t, detail: String(err) });
|
|
128
|
-
steps.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t2, detail: 'fallback after llm failure' });
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
else {
|
|
133
|
-
logger.warn('accurate mode requires llm — falling back to keyword');
|
|
134
|
-
const t = Date.now();
|
|
135
|
-
matchResult = _match(query, this.manifest);
|
|
136
|
-
steps.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: 'llm not provided, used keyword' });
|
|
137
|
-
}
|
|
138
|
-
break;
|
|
139
|
-
}
|
|
140
|
-
case 'balanced':
|
|
141
|
-
default: {
|
|
142
|
-
const t1 = Date.now();
|
|
143
|
-
const keywordResult = _match(query, this.manifest);
|
|
144
|
-
steps.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t1, detail: `confidence: ${keywordResult.confidence}%` });
|
|
145
|
-
if (keywordResult.confidence >= this.threshold || !this.llm) {
|
|
146
|
-
matchResult = keywordResult;
|
|
147
|
-
}
|
|
148
|
-
else {
|
|
149
|
-
const skipReason = this.checkLLMAllowed();
|
|
150
|
-
if (skipReason) {
|
|
151
|
-
logger.warn(`LLM skipped — ${skipReason}`);
|
|
152
|
-
steps.push({ type: 'llm_match', status: 'skip', durationMs: 0, detail: skipReason });
|
|
153
|
-
matchResult = keywordResult;
|
|
154
|
-
}
|
|
155
|
-
else {
|
|
156
|
-
logger.info(`Low confidence (${keywordResult.confidence}%) — escalating to LLM`);
|
|
157
|
-
const t2 = Date.now();
|
|
158
|
-
try {
|
|
159
|
-
matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
|
|
160
|
-
this.recordLLMSuccess();
|
|
161
|
-
resolvedVia = 'llm';
|
|
162
|
-
steps.push({ type: 'llm_match', status: 'pass', durationMs: Date.now() - t2, detail: `confidence: ${matchResult.confidence}%` });
|
|
163
|
-
}
|
|
164
|
-
catch (err) {
|
|
165
|
-
this.recordLLMFailure();
|
|
166
|
-
logger.warn(`LLM call failed — falling back to keyword: ${err}`);
|
|
167
|
-
steps.push({ type: 'llm_match', status: 'fail', durationMs: Date.now() - t2, detail: String(err) });
|
|
168
|
-
matchResult = keywordResult;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
break;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
const preBoostMatchResult = matchResult;
|
|
97
|
+
let { matchResult, resolvedVia } = await this._runMatch(query, steps);
|
|
98
|
+
const preBoostMatchResult = matchResult; // kept for learning recording only — prevents feedback loop
|
|
176
99
|
// ── Step 2.5: Apply learning boost ───────────────────────────────────────
|
|
177
|
-
|
|
178
|
-
const boosted = await this.applyLearningBoost(query, matchResult.candidates);
|
|
179
|
-
if (boosted.length > 0) {
|
|
180
|
-
const newWinner = boosted.reduce((a, b) => a.score > b.score ? a : b);
|
|
181
|
-
const oldWinner = matchResult.candidates.find(c => c.matched);
|
|
182
|
-
if (newWinner.capabilityId !== oldWinner?.capabilityId && newWinner.score >= this.threshold) {
|
|
183
|
-
// Boost changed the winner — re-extract params for the new capability
|
|
184
|
-
const newCap = this.manifest.capabilities.find(c => c.id === newWinner.capabilityId) ?? null;
|
|
185
|
-
const newParams = newCap ? _match(query, { ...this.manifest, capabilities: [newCap] }).extractedParams : {};
|
|
186
|
-
matchResult = {
|
|
187
|
-
...matchResult,
|
|
188
|
-
capability: newCap,
|
|
189
|
-
confidence: newWinner.score,
|
|
190
|
-
intent: newCap ? resolverToIntent(newCap) : 'out_of_scope',
|
|
191
|
-
extractedParams: newParams,
|
|
192
|
-
candidates: boosted.map(c => ({ ...c, matched: c.capabilityId === newWinner.capabilityId })),
|
|
193
|
-
reasoning: `Matched "${newWinner.capabilityId}" via learning boost (score: ${newWinner.score})`,
|
|
194
|
-
};
|
|
195
|
-
logger.info(`Learning boost changed winner: "${oldWinner?.capabilityId ?? 'none'}" → "${newWinner.capabilityId}"`);
|
|
196
|
-
}
|
|
197
|
-
else {
|
|
198
|
-
// Same winner — update scores only
|
|
199
|
-
matchResult = {
|
|
200
|
-
...matchResult,
|
|
201
|
-
confidence: newWinner.score,
|
|
202
|
-
candidates: boosted.map(c => ({ ...c, matched: c.capabilityId === (oldWinner?.capabilityId ?? '') })),
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
100
|
+
matchResult = await this.applyBoostToMatchResult(query, matchResult);
|
|
207
101
|
// ── Step 3: Privacy check ────────────────────────────────────────────────
|
|
208
102
|
if (matchResult.capability) {
|
|
209
103
|
const privacyLevel = matchResult.capability.privacy.level;
|
|
@@ -214,10 +108,13 @@ export class CapmanEngine {
|
|
|
214
108
|
detail: `level: ${privacyLevel}`,
|
|
215
109
|
});
|
|
216
110
|
}
|
|
217
|
-
// ── Step 4: Cache the match result
|
|
218
|
-
|
|
111
|
+
// ── Step 4: Cache the match result (public capabilities only) ─────────────
|
|
112
|
+
// Non-public capabilities are never cached — prevents auth bypass where
|
|
113
|
+
// User A's cached match is served to User B without privacy enforcement.
|
|
114
|
+
if (this.cache && matchResult.capability
|
|
115
|
+
&& matchResult.capability.privacy.level === 'public') {
|
|
219
116
|
const queryKey = normalizeQuery(query);
|
|
220
|
-
await this.cache.set(queryKey,
|
|
117
|
+
await this.cache.set(queryKey, matchResult);
|
|
221
118
|
}
|
|
222
119
|
// ── Step 5: Resolve ──────────────────────────────────────────────────────
|
|
223
120
|
const resolveStart = Date.now();
|
|
@@ -254,7 +151,10 @@ export class CapmanEngine {
|
|
|
254
151
|
reasoning.push(matchResult.reasoning);
|
|
255
152
|
}
|
|
256
153
|
// ── Step 7: Record learning ──────────────────────────────────────────────
|
|
257
|
-
|
|
154
|
+
// Record the pre-boost match result — not the boosted one.
|
|
155
|
+
// Recording the boosted winner would reinforce it further on every call,
|
|
156
|
+
// creating a feedback loop that permanently displaces keyword matches.
|
|
157
|
+
await this.recordLearning(query, preBoostMatchResult, resolvedVia);
|
|
258
158
|
const trace = {
|
|
259
159
|
query,
|
|
260
160
|
candidates: matchResult.candidates,
|
|
@@ -314,91 +214,11 @@ export class CapmanEngine {
|
|
|
314
214
|
*/
|
|
315
215
|
async explain(query) {
|
|
316
216
|
const start = Date.now();
|
|
317
|
-
// ── Match —
|
|
318
|
-
let matchResult;
|
|
319
|
-
let resolvedVia =
|
|
320
|
-
if (this.mode === 'accurate') {
|
|
321
|
-
if (this.llm) {
|
|
322
|
-
const skipReason = this.checkLLMAllowed();
|
|
323
|
-
if (skipReason) {
|
|
324
|
-
logger.warn(`explain(): LLM skipped — ${skipReason} — falling back to keyword`);
|
|
325
|
-
matchResult = _match(query, this.manifest);
|
|
326
|
-
}
|
|
327
|
-
else {
|
|
328
|
-
try {
|
|
329
|
-
matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
|
|
330
|
-
this.recordLLMSuccess();
|
|
331
|
-
resolvedVia = 'llm';
|
|
332
|
-
}
|
|
333
|
-
catch (err) {
|
|
334
|
-
this.recordLLMFailure();
|
|
335
|
-
logger.warn(`explain(): LLM call failed — falling back to keyword: ${err}`);
|
|
336
|
-
matchResult = _match(query, this.manifest);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
else {
|
|
341
|
-
matchResult = _match(query, this.manifest);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
else if (this.mode === 'balanced' && this.llm) {
|
|
345
|
-
// Keyword first — escalate to LLM if low confidence (same as ask())
|
|
346
|
-
const keywordResult = _match(query, this.manifest);
|
|
347
|
-
if (keywordResult.confidence >= this.threshold) {
|
|
348
|
-
matchResult = keywordResult;
|
|
349
|
-
}
|
|
350
|
-
else {
|
|
351
|
-
const skipReason = this.checkLLMAllowed();
|
|
352
|
-
if (skipReason) {
|
|
353
|
-
logger.warn(`explain(): LLM skipped — ${skipReason}`);
|
|
354
|
-
matchResult = keywordResult;
|
|
355
|
-
}
|
|
356
|
-
else {
|
|
357
|
-
try {
|
|
358
|
-
matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
|
|
359
|
-
this.recordLLMSuccess();
|
|
360
|
-
resolvedVia = 'llm';
|
|
361
|
-
}
|
|
362
|
-
catch (err) {
|
|
363
|
-
this.recordLLMFailure();
|
|
364
|
-
logger.warn(`explain(): LLM call failed — falling back to keyword: ${err}`);
|
|
365
|
-
matchResult = keywordResult;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
else {
|
|
371
|
-
// cheap mode or no llm — keyword only
|
|
372
|
-
matchResult = _match(query, this.manifest);
|
|
373
|
-
}
|
|
217
|
+
// ── Match — shared with ask() via _runMatch() ─────────────────────────────
|
|
218
|
+
let { matchResult, resolvedVia: _resolvedVia } = await this._runMatch(query);
|
|
219
|
+
let resolvedVia = _resolvedVia;
|
|
374
220
|
// ── Apply learning boost (same as ask()) ─────────────────────────────────
|
|
375
|
-
|
|
376
|
-
const boosted = await this.applyLearningBoost(query, matchResult.candidates);
|
|
377
|
-
if (boosted.length > 0) {
|
|
378
|
-
const newWinner = boosted.reduce((a, b) => a.score > b.score ? a : b);
|
|
379
|
-
const oldWinner = matchResult.candidates.find(c => c.matched);
|
|
380
|
-
if (newWinner.capabilityId !== oldWinner?.capabilityId && newWinner.score >= this.threshold) {
|
|
381
|
-
const newCap = this.manifest.capabilities.find(c => c.id === newWinner.capabilityId) ?? null;
|
|
382
|
-
const newParams = newCap ? _match(query, { ...this.manifest, capabilities: [newCap] }).extractedParams : {};
|
|
383
|
-
matchResult = {
|
|
384
|
-
...matchResult,
|
|
385
|
-
capability: newCap,
|
|
386
|
-
confidence: newWinner.score,
|
|
387
|
-
intent: newCap ? resolverToIntent(newCap) : 'out_of_scope',
|
|
388
|
-
extractedParams: newParams,
|
|
389
|
-
candidates: boosted.map(c => ({ ...c, matched: c.capabilityId === newWinner.capabilityId })),
|
|
390
|
-
reasoning: `Matched "${newWinner.capabilityId}" via learning boost (score: ${newWinner.score})`,
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
else {
|
|
394
|
-
matchResult = {
|
|
395
|
-
...matchResult,
|
|
396
|
-
confidence: newWinner.score,
|
|
397
|
-
candidates: boosted.map(c => ({ ...c, matched: c.capabilityId === (oldWinner?.capabilityId ?? '') })),
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
}
|
|
221
|
+
matchResult = await this.applyBoostToMatchResult(query, matchResult);
|
|
402
222
|
// ── Build candidate explanations ─────────────────────────────────────────
|
|
403
223
|
const candidates = matchResult.candidates
|
|
404
224
|
.sort((a, b) => b.score - a.score)
|
|
@@ -582,6 +402,137 @@ export class CapmanEngine {
|
|
|
582
402
|
logger.warn(`LLM circuit breaker opened after ${this.llmConsecutiveFails} consecutive failures — pausing for ${this.llmCircuitBreakerResetMs / 1000}s`);
|
|
583
403
|
}
|
|
584
404
|
}
|
|
405
|
+
/**
|
|
406
|
+
* Runs the matching pipeline for a query — shared by ask() and explain().
|
|
407
|
+
* Handles cheap / balanced / accurate mode dispatch and LLM rate limiting.
|
|
408
|
+
* Returns the match result and which resolver was used.
|
|
409
|
+
*/
|
|
410
|
+
async _runMatch(query, steps) {
|
|
411
|
+
let matchResult;
|
|
412
|
+
let resolvedVia = 'keyword';
|
|
413
|
+
switch (this.mode) {
|
|
414
|
+
case 'cheap': {
|
|
415
|
+
const t = Date.now();
|
|
416
|
+
matchResult = _match(query, this.manifest);
|
|
417
|
+
steps?.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: `confidence: ${matchResult.confidence}%` });
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
case 'accurate': {
|
|
421
|
+
if (this.llm) {
|
|
422
|
+
const skipReason = this.checkLLMAllowed();
|
|
423
|
+
if (skipReason) {
|
|
424
|
+
logger.warn(`LLM skipped — ${skipReason} — falling back to keyword`);
|
|
425
|
+
const t = Date.now();
|
|
426
|
+
matchResult = _match(query, this.manifest);
|
|
427
|
+
steps?.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: `llm skipped: ${skipReason}` });
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
const t = Date.now();
|
|
431
|
+
try {
|
|
432
|
+
matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
|
|
433
|
+
this.recordLLMSuccess();
|
|
434
|
+
resolvedVia = 'llm';
|
|
435
|
+
steps?.push({ type: 'llm_match', status: 'pass', durationMs: Date.now() - t, detail: `confidence: ${matchResult.confidence}%` });
|
|
436
|
+
}
|
|
437
|
+
catch (err) {
|
|
438
|
+
const isParseError = String(err).startsWith('LLM_PARSE_ERROR');
|
|
439
|
+
if (!isParseError)
|
|
440
|
+
this.recordLLMFailure();
|
|
441
|
+
logger.warn(`LLM call failed — falling back to keyword: ${err instanceof Error ? err.message : String(err)}`);
|
|
442
|
+
const t2 = Date.now();
|
|
443
|
+
matchResult = _match(query, this.manifest);
|
|
444
|
+
steps?.push({ type: 'llm_match', status: 'fail', durationMs: Date.now() - t, detail: String(err) });
|
|
445
|
+
steps?.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t2, detail: 'fallback after llm failure' });
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
logger.warn('accurate mode requires llm — falling back to keyword');
|
|
451
|
+
const t = Date.now();
|
|
452
|
+
matchResult = _match(query, this.manifest);
|
|
453
|
+
steps?.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: 'llm not provided, used keyword' });
|
|
454
|
+
}
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
case 'balanced':
|
|
458
|
+
default: {
|
|
459
|
+
const t1 = Date.now();
|
|
460
|
+
const keywordResult = _match(query, this.manifest);
|
|
461
|
+
steps?.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t1, detail: `confidence: ${keywordResult.confidence}%` });
|
|
462
|
+
if (keywordResult.confidence >= this.threshold || !this.llm) {
|
|
463
|
+
matchResult = keywordResult;
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
const skipReason = this.checkLLMAllowed();
|
|
467
|
+
if (skipReason) {
|
|
468
|
+
logger.warn(`LLM skipped — ${skipReason}`);
|
|
469
|
+
steps?.push({ type: 'llm_match', status: 'skip', durationMs: 0, detail: skipReason });
|
|
470
|
+
matchResult = keywordResult;
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
logger.info(`Low confidence (${keywordResult.confidence}%) — escalating to LLM`);
|
|
474
|
+
const t2 = Date.now();
|
|
475
|
+
try {
|
|
476
|
+
matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
|
|
477
|
+
this.recordLLMSuccess();
|
|
478
|
+
resolvedVia = 'llm';
|
|
479
|
+
steps?.push({ type: 'llm_match', status: 'pass', durationMs: Date.now() - t2, detail: `confidence: ${matchResult.confidence}%` });
|
|
480
|
+
}
|
|
481
|
+
catch (err) {
|
|
482
|
+
const isParseError = String(err).startsWith('LLM_PARSE_ERROR');
|
|
483
|
+
if (!isParseError)
|
|
484
|
+
this.recordLLMFailure();
|
|
485
|
+
logger.warn(`LLM call failed — falling back to keyword: ${err instanceof Error ? err.message : String(err)}`);
|
|
486
|
+
steps?.push({ type: 'llm_match', status: 'fail', durationMs: Date.now() - t2, detail: String(err) });
|
|
487
|
+
matchResult = keywordResult;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return { matchResult: matchResult, resolvedVia };
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Applies learning boost to a MatchResult and returns the updated result.
|
|
498
|
+
* Shared by ask() and explain() to avoid logic divergence.
|
|
499
|
+
*/
|
|
500
|
+
async applyBoostToMatchResult(query, matchResult) {
|
|
501
|
+
const hasKeywordSignal = matchResult.candidates.some(c => c.score > 0);
|
|
502
|
+
if (!hasKeywordSignal || matchResult.candidates.length === 0 || !this.learning || this.mode === 'cheap') {
|
|
503
|
+
return matchResult;
|
|
504
|
+
}
|
|
505
|
+
const boosted = await this.applyLearningBoost(query, matchResult.candidates);
|
|
506
|
+
if (boosted.length === 0)
|
|
507
|
+
return matchResult;
|
|
508
|
+
const newWinner = boosted.reduce((a, b) => {
|
|
509
|
+
if (b.score > a.score)
|
|
510
|
+
return b;
|
|
511
|
+
if (b.score === a.score && b.matched)
|
|
512
|
+
return b; // original winner wins ties
|
|
513
|
+
return a;
|
|
514
|
+
});
|
|
515
|
+
const oldWinner = matchResult.candidates.find(c => c.matched);
|
|
516
|
+
if (newWinner.capabilityId !== oldWinner?.capabilityId && newWinner.score >= this.threshold) {
|
|
517
|
+
const newCap = this.manifest.capabilities.find(c => c.id === newWinner.capabilityId) ?? null;
|
|
518
|
+
const newParams = newCap ? extractParams(query, newCap) : {};
|
|
519
|
+
logger.info(`Learning boost changed winner: "${oldWinner?.capabilityId ?? 'none'}" → "${newWinner.capabilityId}"`);
|
|
520
|
+
return {
|
|
521
|
+
...matchResult,
|
|
522
|
+
capability: newCap,
|
|
523
|
+
confidence: newWinner.score,
|
|
524
|
+
intent: newCap ? resolverToIntent(newCap) : 'out_of_scope',
|
|
525
|
+
extractedParams: newParams,
|
|
526
|
+
candidates: boosted.map(c => ({ ...c, matched: c.capabilityId === newWinner.capabilityId })),
|
|
527
|
+
reasoning: `Matched "${newWinner.capabilityId}" via learning boost (score: ${newWinner.score})`,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
return {
|
|
531
|
+
...matchResult,
|
|
532
|
+
confidence: newWinner.score,
|
|
533
|
+
candidates: boosted.map(c => ({ ...c, matched: c.capabilityId === (oldWinner?.capabilityId ?? '') })),
|
|
534
|
+
};
|
|
535
|
+
}
|
|
585
536
|
/**
|
|
586
537
|
* Applies learning boost to match candidates based on historical usage.
|
|
587
538
|
* Capabilities that have previously matched similar keywords get a small
|
|
@@ -591,14 +542,10 @@ export class CapmanEngine {
|
|
|
591
542
|
if (!this.learning)
|
|
592
543
|
return candidates;
|
|
593
544
|
// Use cached stats — rebuilt only when new entries recorded
|
|
594
|
-
|
|
595
|
-
this.cachedStats = await this.learning.getStats();
|
|
596
|
-
this.statsInvalidated = false;
|
|
597
|
-
}
|
|
598
|
-
const stats = this.cachedStats;
|
|
545
|
+
const stats = await this.learning.getStats();
|
|
599
546
|
if (!stats || Object.keys(stats.index).length === 0)
|
|
600
547
|
return candidates;
|
|
601
|
-
const qWords = query.toLowerCase().split(/\W+/).filter(w => w.length > 2);
|
|
548
|
+
const qWords = query.toLowerCase().split(/\W+/).filter(w => w.length > 2 && !STOPWORDS.has(w));
|
|
602
549
|
if (qWords.length === 0)
|
|
603
550
|
return candidates;
|
|
604
551
|
return candidates.map(candidate => {
|
|
@@ -645,6 +592,5 @@ export class CapmanEngine {
|
|
|
645
592
|
resolvedVia,
|
|
646
593
|
timestamp: new Date().toISOString(),
|
|
647
594
|
});
|
|
648
|
-
this.statsInvalidated = true;
|
|
649
595
|
}
|
|
650
596
|
}
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { setLogLevel } from './logger';
|
|
|
2
2
|
export type { LogLevel } from './logger';
|
|
3
3
|
export type { Capability, CapabilityParam, CapmanConfig, Manifest, MatchResult, ExecutionTrace, TraceStep, MatchCandidate, ResolveResult, ApiCallResult, ValidationResult, Resolver, ApiResolver, NavResolver, HybridResolver, PrivacyScope, ResolverType, HttpMethod, ExplainResult, ExplainCandidate, } from './types';
|
|
4
4
|
export { generate, loadConfig, writeManifest, readManifest, validate, generateStarterConfig, } from './generator';
|
|
5
|
-
export { match, matchWithLLM, } from './matcher';
|
|
5
|
+
export { match, matchWithLLM, extractParams, } from './matcher';
|
|
6
6
|
export type { LLMMatcherOptions } from './matcher';
|
|
7
7
|
export { resolve } from './resolver';
|
|
8
8
|
export type { ResolveOptions, AuthContext } from './resolver';
|
package/dist/esm/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { setLogLevel } from './logger';
|
|
2
2
|
export { generate, loadConfig, writeManifest, readManifest, validate, generateStarterConfig, } from './generator';
|
|
3
|
-
export { match, matchWithLLM, } from './matcher';
|
|
3
|
+
export { match, matchWithLLM, extractParams, } from './matcher';
|
|
4
4
|
export { resolve } from './resolver';
|
|
5
5
|
// ─── Engine (recommended API) ─────────────────────────────────────────────────
|
|
6
6
|
export { CapmanEngine } from './engine';
|
package/dist/esm/learning.d.ts
CHANGED
|
@@ -26,17 +26,26 @@ export interface LearningStore {
|
|
|
26
26
|
id: string;
|
|
27
27
|
hits: number;
|
|
28
28
|
}>>;
|
|
29
|
-
|
|
29
|
+
/** Returns the live keyword index without rebuilding — O(1) */
|
|
30
|
+
getIndex(): Promise<Record<string, Record<string, number>>>;
|
|
30
31
|
}
|
|
31
32
|
export declare class FileLearningStore implements LearningStore {
|
|
32
33
|
private filePath;
|
|
33
34
|
private entries;
|
|
34
35
|
private loaded;
|
|
36
|
+
private saveQueue;
|
|
37
|
+
private index;
|
|
38
|
+
private statsCounter;
|
|
35
39
|
constructor(filePath?: string);
|
|
40
|
+
private updateIndex;
|
|
41
|
+
private subtractFromIndex;
|
|
42
|
+
private rebuildIndex;
|
|
36
43
|
private load;
|
|
37
44
|
private save;
|
|
45
|
+
private _doSave;
|
|
38
46
|
record(entry: LearningEntry): Promise<void>;
|
|
39
47
|
getStats(): Promise<KeywordStats>;
|
|
48
|
+
getIndex(): Promise<Record<string, Record<string, number>>>;
|
|
40
49
|
getTopCapabilities(limit?: number): Promise<Array<{
|
|
41
50
|
id: string;
|
|
42
51
|
hits: number;
|
|
@@ -45,8 +54,14 @@ export declare class FileLearningStore implements LearningStore {
|
|
|
45
54
|
}
|
|
46
55
|
export declare class MemoryLearningStore implements LearningStore {
|
|
47
56
|
private entries;
|
|
57
|
+
private index;
|
|
58
|
+
private statsCounter;
|
|
48
59
|
record(entry: LearningEntry): Promise<void>;
|
|
49
60
|
getStats(): Promise<KeywordStats>;
|
|
61
|
+
getIndex(): Promise<Record<string, Record<string, number>>>;
|
|
62
|
+
private updateIndex;
|
|
63
|
+
private subtractFromIndex;
|
|
64
|
+
private rebuildIndex;
|
|
50
65
|
getTopCapabilities(limit?: number): Promise<Array<{
|
|
51
66
|
id: string;
|
|
52
67
|
hits: number;
|