capman 0.5.0 → 0.5.2
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 +55 -0
- package/bin/lib/cmd-demo.js +2 -2
- package/dist/cjs/cache.d.ts +6 -4
- package/dist/cjs/cache.d.ts.map +1 -1
- package/dist/cjs/cache.js +44 -13
- package/dist/cjs/cache.js.map +1 -1
- package/dist/cjs/engine.d.ts +23 -1
- package/dist/cjs/engine.d.ts.map +1 -1
- package/dist/cjs/engine.js +168 -165
- package/dist/cjs/engine.js.map +1 -1
- package/dist/cjs/index.d.ts +3 -2
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/learning.d.ts +2 -1
- package/dist/cjs/learning.d.ts.map +1 -1
- package/dist/cjs/learning.js +104 -20
- package/dist/cjs/learning.js.map +1 -1
- package/dist/cjs/matcher.d.ts +15 -1
- package/dist/cjs/matcher.d.ts.map +1 -1
- package/dist/cjs/matcher.js +45 -14
- package/dist/cjs/matcher.js.map +1 -1
- package/dist/cjs/parser.js +16 -2
- package/dist/cjs/parser.js.map +1 -1
- package/dist/cjs/resolver.d.ts.map +1 -1
- package/dist/cjs/resolver.js +32 -6
- package/dist/cjs/resolver.js.map +1 -1
- package/dist/cjs/schema.d.ts +14 -14
- package/dist/cjs/schema.d.ts.map +1 -1
- package/dist/cjs/schema.js +4 -2
- package/dist/cjs/schema.js.map +1 -1
- package/dist/cjs/types.d.ts +1 -0
- package/dist/cjs/types.d.ts.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 +44 -13
- package/dist/esm/engine.d.ts +23 -1
- package/dist/esm/engine.js +169 -166
- package/dist/esm/index.d.ts +3 -2
- package/dist/esm/index.js +1 -0
- package/dist/esm/learning.d.ts +2 -1
- package/dist/esm/learning.js +104 -20
- package/dist/esm/matcher.d.ts +15 -1
- package/dist/esm/matcher.js +43 -13
- package/dist/esm/parser.js +16 -2
- package/dist/esm/resolver.js +32 -6
- package/dist/esm/schema.d.ts +14 -14
- package/dist/esm/schema.js +4 -2
- package/dist/esm/types.d.ts +1 -0
- 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, extractParams, STOPWORDS } from './matcher';
|
|
1
|
+
import { match as _match, matchWithLLM as _matchWithLLM, resolverToIntent, extractParams, STOPWORDS, LLMParseError } from './matcher';
|
|
2
2
|
import { resolve as _resolve } from './resolver';
|
|
3
3
|
import { MemoryLearningStore } from './learning';
|
|
4
4
|
import { logger } from './logger';
|
|
@@ -20,10 +20,11 @@ export class CapmanEngine {
|
|
|
20
20
|
this.auth = options.auth;
|
|
21
21
|
this.headers = options.headers;
|
|
22
22
|
this.threshold = options.threshold ?? 50;
|
|
23
|
+
this.cacheTtlMs = options.cacheTtlMs ?? null;
|
|
23
24
|
this.maxLLMCallsPerMinute = options.maxLLMCallsPerMinute ?? 60;
|
|
24
25
|
this.llmCooldownMs = options.llmCooldownMs ?? 0;
|
|
25
26
|
this.llmCircuitBreakerThreshold = options.llmCircuitBreakerThreshold ?? 3;
|
|
26
|
-
this.llmCircuitBreakerResetMs = options.llmCircuitBreakerResetMs ??
|
|
27
|
+
this.llmCircuitBreakerResetMs = options.llmCircuitBreakerResetMs ?? 60_000;
|
|
27
28
|
// Cache — default MemoryCache (no filesystem writes), or disabled with false
|
|
28
29
|
// Use FileCache or ComboCache explicitly for persistence across restarts
|
|
29
30
|
this.cache = options.cache === false
|
|
@@ -36,14 +37,14 @@ export class CapmanEngine {
|
|
|
36
37
|
: (options.learning ?? new MemoryLearningStore());
|
|
37
38
|
logger.info(`CapmanEngine initialized — mode: ${this.mode}, cache: ${this.cache ? 'enabled' : 'disabled'}, learning: ${this.learning ? 'enabled' : 'disabled'}`);
|
|
38
39
|
// ── Manifest version compatibility check ─────────────────────────────────
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
if (options.manifest.version) {
|
|
41
|
+
const [mMaj, mMin] = options.manifest.version.split('.').map(Number);
|
|
42
|
+
const [eMaj, eMin] = VERSION.split('.').map(Number);
|
|
43
|
+
if (mMaj !== eMaj || mMin !== eMin) {
|
|
44
|
+
console.warn(`[capman] Manifest version "${options.manifest.version}" was generated with a ` +
|
|
45
|
+
`different engine version than "${VERSION}". This is usually fine across patch versions. ` +
|
|
46
|
+
`If you experience unexpected matching issues, regenerate with: npx capman generate`);
|
|
47
|
+
}
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
/**
|
|
@@ -58,18 +59,34 @@ export class CapmanEngine {
|
|
|
58
59
|
* console.log(result.resolvedVia) // 'keyword' | 'llm' | 'cache'
|
|
59
60
|
*/
|
|
60
61
|
async ask(query, overrides = {}) {
|
|
62
|
+
if (!query || typeof query !== 'string') {
|
|
63
|
+
throw new TypeError('query must be a non-empty string');
|
|
64
|
+
}
|
|
65
|
+
if (query.length > CapmanEngine.MAX_QUERY_LENGTH) {
|
|
66
|
+
throw new RangeError(`query exceeds maximum length of ${CapmanEngine.MAX_QUERY_LENGTH} characters`);
|
|
67
|
+
}
|
|
61
68
|
const start = Date.now();
|
|
62
69
|
const steps = [];
|
|
63
|
-
let resolvedVia = 'keyword';
|
|
64
70
|
// ── Step 1: Check cache ──────────────────────────────────────────────────
|
|
65
71
|
const cacheStart = Date.now();
|
|
66
72
|
if (this.cache) {
|
|
67
73
|
const queryKey = normalizeQuery(query);
|
|
68
|
-
const cached = await this.cache.get(queryKey);
|
|
74
|
+
const cached = await this.cache.get(queryKey, this.cacheTtlMs ?? undefined);
|
|
69
75
|
if (cached) {
|
|
70
76
|
steps.push({ type: 'cache_check', status: 'hit', durationMs: Date.now() - cacheStart, detail: 'Served from cache' });
|
|
71
77
|
logger.info(`Cache hit for: "${query}"`);
|
|
72
|
-
|
|
78
|
+
// Re-extract params from the current query — never re-use cached params.
|
|
79
|
+
// Cached params belong to the original query (potentially from a different user).
|
|
80
|
+
// e.g. User A: "show orders for john" → cached with { customer: 'john' }
|
|
81
|
+
// User B: "show orders for jane" → must get { customer: 'jane' }, not john's
|
|
82
|
+
const freshParams = cached.result.capability
|
|
83
|
+
? extractParams(query, cached.result.capability)
|
|
84
|
+
: {};
|
|
85
|
+
const matchWithFreshParams = {
|
|
86
|
+
...cached.result,
|
|
87
|
+
extractedParams: freshParams,
|
|
88
|
+
};
|
|
89
|
+
const resolution = await _resolve(matchWithFreshParams, freshParams, this.resolveOptions(overrides));
|
|
73
90
|
const trace = {
|
|
74
91
|
query,
|
|
75
92
|
candidates: cached.result.candidates,
|
|
@@ -79,13 +96,13 @@ export class CapmanEngine {
|
|
|
79
96
|
totalMs: Date.now() - start,
|
|
80
97
|
};
|
|
81
98
|
const result = {
|
|
82
|
-
match:
|
|
99
|
+
match: matchWithFreshParams,
|
|
83
100
|
resolution,
|
|
84
101
|
resolvedVia: 'cache',
|
|
85
102
|
durationMs: Date.now() - start,
|
|
86
103
|
trace,
|
|
87
104
|
};
|
|
88
|
-
await this.recordLearning(query,
|
|
105
|
+
await this.recordLearning(query, matchWithFreshParams, 'cache');
|
|
89
106
|
return result;
|
|
90
107
|
}
|
|
91
108
|
steps.push({ type: 'cache_check', status: 'miss', durationMs: Date.now() - cacheStart });
|
|
@@ -94,88 +111,7 @@ export class CapmanEngine {
|
|
|
94
111
|
steps.push({ type: 'cache_check', status: 'skip', durationMs: 0, detail: 'Cache disabled' });
|
|
95
112
|
}
|
|
96
113
|
// ── Step 2: Match ────────────────────────────────────────────────────────
|
|
97
|
-
let matchResult;
|
|
98
|
-
switch (this.mode) {
|
|
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
|
-
const isParseError = String(err).startsWith('LLM_PARSE_ERROR');
|
|
124
|
-
if (!isParseError)
|
|
125
|
-
this.recordLLMFailure();
|
|
126
|
-
logger.warn(`LLM call failed — falling back to keyword: ${err}`);
|
|
127
|
-
const t2 = Date.now();
|
|
128
|
-
matchResult = _match(query, this.manifest);
|
|
129
|
-
steps.push({ type: 'llm_match', status: 'fail', durationMs: Date.now() - t, detail: String(err) });
|
|
130
|
-
steps.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t2, detail: 'fallback after llm failure' });
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
else {
|
|
135
|
-
logger.warn('accurate mode requires llm — falling back to keyword');
|
|
136
|
-
const t = Date.now();
|
|
137
|
-
matchResult = _match(query, this.manifest);
|
|
138
|
-
steps.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: 'llm not provided, used keyword' });
|
|
139
|
-
}
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
case 'balanced':
|
|
143
|
-
default: {
|
|
144
|
-
const t1 = Date.now();
|
|
145
|
-
const keywordResult = _match(query, this.manifest);
|
|
146
|
-
steps.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t1, detail: `confidence: ${keywordResult.confidence}%` });
|
|
147
|
-
if (keywordResult.confidence >= this.threshold || !this.llm) {
|
|
148
|
-
matchResult = keywordResult;
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
151
|
-
const skipReason = this.checkLLMAllowed();
|
|
152
|
-
if (skipReason) {
|
|
153
|
-
logger.warn(`LLM skipped — ${skipReason}`);
|
|
154
|
-
steps.push({ type: 'llm_match', status: 'skip', durationMs: 0, detail: skipReason });
|
|
155
|
-
matchResult = keywordResult;
|
|
156
|
-
}
|
|
157
|
-
else {
|
|
158
|
-
logger.info(`Low confidence (${keywordResult.confidence}%) — escalating to LLM`);
|
|
159
|
-
const t2 = Date.now();
|
|
160
|
-
try {
|
|
161
|
-
matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
|
|
162
|
-
this.recordLLMSuccess();
|
|
163
|
-
resolvedVia = 'llm';
|
|
164
|
-
steps.push({ type: 'llm_match', status: 'pass', durationMs: Date.now() - t2, detail: `confidence: ${matchResult.confidence}%` });
|
|
165
|
-
}
|
|
166
|
-
catch (err) {
|
|
167
|
-
const isParseError = String(err).startsWith('LLM_PARSE_ERROR');
|
|
168
|
-
if (!isParseError)
|
|
169
|
-
this.recordLLMFailure();
|
|
170
|
-
logger.warn(`LLM call failed — falling back to keyword: ${err}`);
|
|
171
|
-
steps.push({ type: 'llm_match', status: 'fail', durationMs: Date.now() - t2, detail: String(err) });
|
|
172
|
-
matchResult = keywordResult;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
break;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
114
|
+
let { matchResult, resolvedVia } = await this._runMatch(query, steps);
|
|
179
115
|
const preBoostMatchResult = matchResult; // kept for learning recording only — prevents feedback loop
|
|
180
116
|
// ── Step 2.5: Apply learning boost ───────────────────────────────────────
|
|
181
117
|
matchResult = await this.applyBoostToMatchResult(query, matchResult);
|
|
@@ -294,68 +230,16 @@ export class CapmanEngine {
|
|
|
294
230
|
* console.log(explanation.candidates)
|
|
295
231
|
*/
|
|
296
232
|
async explain(query) {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
let matchResult;
|
|
300
|
-
let resolvedVia = 'keyword';
|
|
301
|
-
if (this.mode === 'accurate') {
|
|
302
|
-
if (this.llm) {
|
|
303
|
-
const skipReason = this.checkLLMAllowed();
|
|
304
|
-
if (skipReason) {
|
|
305
|
-
logger.warn(`explain(): LLM skipped — ${skipReason} — falling back to keyword`);
|
|
306
|
-
matchResult = _match(query, this.manifest);
|
|
307
|
-
}
|
|
308
|
-
else {
|
|
309
|
-
try {
|
|
310
|
-
matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
|
|
311
|
-
this.recordLLMSuccess();
|
|
312
|
-
resolvedVia = 'llm';
|
|
313
|
-
}
|
|
314
|
-
catch (err) {
|
|
315
|
-
const isParseError = String(err).startsWith('LLM_PARSE_ERROR');
|
|
316
|
-
if (!isParseError)
|
|
317
|
-
this.recordLLMFailure();
|
|
318
|
-
logger.warn(`LLM call failed — falling back to keyword: ${err}`);
|
|
319
|
-
matchResult = _match(query, this.manifest);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
else {
|
|
324
|
-
matchResult = _match(query, this.manifest);
|
|
325
|
-
}
|
|
233
|
+
if (!query || typeof query !== 'string') {
|
|
234
|
+
throw new TypeError('query must be a non-empty string');
|
|
326
235
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const keywordResult = _match(query, this.manifest);
|
|
330
|
-
if (keywordResult.confidence >= this.threshold) {
|
|
331
|
-
matchResult = keywordResult;
|
|
332
|
-
}
|
|
333
|
-
else {
|
|
334
|
-
const skipReason = this.checkLLMAllowed();
|
|
335
|
-
if (skipReason) {
|
|
336
|
-
logger.warn(`explain(): LLM skipped — ${skipReason}`);
|
|
337
|
-
matchResult = keywordResult;
|
|
338
|
-
}
|
|
339
|
-
else {
|
|
340
|
-
try {
|
|
341
|
-
matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
|
|
342
|
-
this.recordLLMSuccess();
|
|
343
|
-
resolvedVia = 'llm';
|
|
344
|
-
}
|
|
345
|
-
catch (err) {
|
|
346
|
-
const isParseError = String(err).startsWith('LLM_PARSE_ERROR');
|
|
347
|
-
if (!isParseError)
|
|
348
|
-
this.recordLLMFailure();
|
|
349
|
-
logger.warn(`LLM call failed — falling back to keyword: ${err}`);
|
|
350
|
-
matchResult = keywordResult;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
else {
|
|
356
|
-
// cheap mode or no llm — keyword only
|
|
357
|
-
matchResult = _match(query, this.manifest);
|
|
236
|
+
if (query.length > CapmanEngine.MAX_QUERY_LENGTH) {
|
|
237
|
+
throw new RangeError(`query exceeds maximum length of ${CapmanEngine.MAX_QUERY_LENGTH} characters`);
|
|
358
238
|
}
|
|
239
|
+
const start = Date.now();
|
|
240
|
+
// ── Match — shared with ask() via _runMatch() ─────────────────────────────
|
|
241
|
+
let { matchResult, resolvedVia: _resolvedVia } = await this._runMatch(query);
|
|
242
|
+
let resolvedVia = _resolvedVia;
|
|
359
243
|
// ── Apply learning boost (same as ask()) ─────────────────────────────────
|
|
360
244
|
matchResult = await this.applyBoostToMatchResult(query, matchResult);
|
|
361
245
|
// ── Build candidate explanations ─────────────────────────────────────────
|
|
@@ -400,12 +284,17 @@ export class CapmanEngine {
|
|
|
400
284
|
}
|
|
401
285
|
reasoning.push(`Resolved via: ${resolvedVia}`);
|
|
402
286
|
if (matchResult.extractedParams && Object.keys(matchResult.extractedParams).length) {
|
|
403
|
-
const
|
|
287
|
+
const extracted = Object.entries(matchResult.extractedParams)
|
|
404
288
|
.filter(([, v]) => v !== null)
|
|
405
289
|
.map(([k, v]) => `${k}=${v}`)
|
|
406
290
|
.join(', ');
|
|
407
|
-
|
|
408
|
-
|
|
291
|
+
const session = matchResult.capability?.params
|
|
292
|
+
.filter(p => p.source === 'session')
|
|
293
|
+
.map(p => `${p.name}=[from auth]`)
|
|
294
|
+
.join(', ');
|
|
295
|
+
const parts = [extracted, session].filter(Boolean).join(', ');
|
|
296
|
+
if (parts)
|
|
297
|
+
reasoning.push(`Would extract params: ${parts}`);
|
|
409
298
|
}
|
|
410
299
|
// ── Build wouldExecute ───────────────────────────────────────────────────
|
|
411
300
|
const cap = matchResult.capability;
|
|
@@ -438,7 +327,7 @@ export class CapmanEngine {
|
|
|
438
327
|
let path = endpoint.path;
|
|
439
328
|
for (const [k, v] of Object.entries(params)) {
|
|
440
329
|
if (v)
|
|
441
|
-
path = path.
|
|
330
|
+
path = path.replaceAll(`{${k}}`, v);
|
|
442
331
|
}
|
|
443
332
|
const base = this.baseUrl ?? '';
|
|
444
333
|
action = `${endpoint.method} ${base}${path}`;
|
|
@@ -447,7 +336,7 @@ export class CapmanEngine {
|
|
|
447
336
|
let dest = cap.resolver.destination;
|
|
448
337
|
for (const [k, v] of Object.entries(params)) {
|
|
449
338
|
if (v)
|
|
450
|
-
dest = dest.
|
|
339
|
+
dest = dest.replaceAll(`{${k}}`, v);
|
|
451
340
|
}
|
|
452
341
|
action = `navigate → ${dest}`;
|
|
453
342
|
}
|
|
@@ -457,12 +346,12 @@ export class CapmanEngine {
|
|
|
457
346
|
let path = endpoint.path;
|
|
458
347
|
for (const [k, v] of Object.entries(params)) {
|
|
459
348
|
if (v)
|
|
460
|
-
path = path.
|
|
349
|
+
path = path.replaceAll(`{${k}}`, v);
|
|
461
350
|
}
|
|
462
351
|
let dest = hybrid.nav.destination;
|
|
463
352
|
for (const [k, v] of Object.entries(params)) {
|
|
464
353
|
if (v)
|
|
465
|
-
dest = dest.
|
|
354
|
+
dest = dest.replaceAll(`{${k}}`, v);
|
|
466
355
|
}
|
|
467
356
|
const base = this.baseUrl ?? '';
|
|
468
357
|
action = `${endpoint.method} ${base}${path} + navigate → ${dest}`;
|
|
@@ -511,13 +400,13 @@ export class CapmanEngine {
|
|
|
511
400
|
}
|
|
512
401
|
// ── Per-minute rate limit ────────────────────────────────────────────────
|
|
513
402
|
const windowElapsed = now - this.llmWindowStart;
|
|
514
|
-
if (windowElapsed >=
|
|
403
|
+
if (windowElapsed >= 60_000) {
|
|
515
404
|
this.llmCallsThisMinute = 0;
|
|
516
405
|
this.llmWindowStart = now;
|
|
517
406
|
}
|
|
518
407
|
if (this.llmCallsThisMinute >= this.maxLLMCallsPerMinute) {
|
|
519
408
|
// Recalculate elapsed after possible window reset above
|
|
520
|
-
const resetIn = Math.ceil((
|
|
409
|
+
const resetIn = Math.ceil((60_000 - (now - this.llmWindowStart)) / 1000);
|
|
521
410
|
return `rate limit reached (${this.maxLLMCallsPerMinute}/min) — resets in ${Math.max(0, resetIn)}s`;
|
|
522
411
|
}
|
|
523
412
|
// Reserve the slot atomically before the call happens
|
|
@@ -541,6 +430,118 @@ export class CapmanEngine {
|
|
|
541
430
|
logger.warn(`LLM circuit breaker opened after ${this.llmConsecutiveFails} consecutive failures — pausing for ${this.llmCircuitBreakerResetMs / 1000}s`);
|
|
542
431
|
}
|
|
543
432
|
}
|
|
433
|
+
/**
|
|
434
|
+
* Runs the matching pipeline for a query — shared by ask() and explain().
|
|
435
|
+
* Handles cheap / balanced / accurate mode dispatch and LLM rate limiting.
|
|
436
|
+
* Returns the match result and which resolver was used.
|
|
437
|
+
*/
|
|
438
|
+
async _runMatch(query, steps) {
|
|
439
|
+
let matchResult;
|
|
440
|
+
let resolvedVia = 'keyword';
|
|
441
|
+
switch (this.mode) {
|
|
442
|
+
case 'cheap': {
|
|
443
|
+
const t = Date.now();
|
|
444
|
+
matchResult = _match(query, this.manifest);
|
|
445
|
+
steps?.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: `confidence: ${matchResult.confidence}%` });
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
case 'accurate': {
|
|
449
|
+
if (this.llm) {
|
|
450
|
+
const skipReason = this.checkLLMAllowed();
|
|
451
|
+
if (skipReason) {
|
|
452
|
+
logger.warn(`LLM skipped — ${skipReason} — falling back to keyword`);
|
|
453
|
+
const t = Date.now();
|
|
454
|
+
matchResult = _match(query, this.manifest);
|
|
455
|
+
steps?.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: `llm skipped: ${skipReason}` });
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
const t = Date.now();
|
|
459
|
+
try {
|
|
460
|
+
matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
|
|
461
|
+
this.recordLLMSuccess();
|
|
462
|
+
resolvedVia = 'llm';
|
|
463
|
+
// Merge keyword scores into LLM candidates so boost has real signal for alternatives
|
|
464
|
+
const kwResult = _match(query, this.manifest);
|
|
465
|
+
matchResult = {
|
|
466
|
+
...matchResult,
|
|
467
|
+
candidates: matchResult.candidates.map(c => ({
|
|
468
|
+
...c,
|
|
469
|
+
score: c.matched
|
|
470
|
+
? c.score // keep LLM confidence for winner
|
|
471
|
+
: (kwResult.candidates.find(kc => kc.capabilityId === c.capabilityId)?.score ?? 0),
|
|
472
|
+
})),
|
|
473
|
+
};
|
|
474
|
+
steps?.push({ type: 'llm_match', status: 'pass', durationMs: Date.now() - t, detail: `confidence: ${matchResult.confidence}%` });
|
|
475
|
+
}
|
|
476
|
+
catch (err) {
|
|
477
|
+
const isParseError = err instanceof LLMParseError;
|
|
478
|
+
if (!isParseError)
|
|
479
|
+
this.recordLLMFailure();
|
|
480
|
+
logger.warn(`LLM call failed — falling back to keyword: ${err instanceof Error ? err.message : String(err)}`);
|
|
481
|
+
const t2 = Date.now();
|
|
482
|
+
matchResult = _match(query, this.manifest);
|
|
483
|
+
steps?.push({ type: 'llm_match', status: 'fail', durationMs: Date.now() - t, detail: String(err) });
|
|
484
|
+
steps?.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t2, detail: 'fallback after llm failure' });
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
logger.warn('accurate mode requires llm — falling back to keyword');
|
|
490
|
+
const t = Date.now();
|
|
491
|
+
matchResult = _match(query, this.manifest);
|
|
492
|
+
steps?.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: 'llm not provided, used keyword' });
|
|
493
|
+
}
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
case 'balanced':
|
|
497
|
+
default: {
|
|
498
|
+
const t1 = Date.now();
|
|
499
|
+
const keywordResult = _match(query, this.manifest);
|
|
500
|
+
steps?.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t1, detail: `confidence: ${keywordResult.confidence}%` });
|
|
501
|
+
if (keywordResult.confidence >= this.threshold || !this.llm) {
|
|
502
|
+
matchResult = keywordResult;
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
const skipReason = this.checkLLMAllowed();
|
|
506
|
+
if (skipReason) {
|
|
507
|
+
logger.warn(`LLM skipped — ${skipReason}`);
|
|
508
|
+
steps?.push({ type: 'llm_match', status: 'skip', durationMs: 0, detail: skipReason });
|
|
509
|
+
matchResult = keywordResult;
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
logger.info(`Low confidence (${keywordResult.confidence}%) — escalating to LLM`);
|
|
513
|
+
const t2 = Date.now();
|
|
514
|
+
try {
|
|
515
|
+
matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
|
|
516
|
+
this.recordLLMSuccess();
|
|
517
|
+
resolvedVia = 'llm';
|
|
518
|
+
// keywordResult already computed above in balanced mode — merge scores
|
|
519
|
+
matchResult = {
|
|
520
|
+
...matchResult,
|
|
521
|
+
candidates: matchResult.candidates.map(c => ({
|
|
522
|
+
...c,
|
|
523
|
+
score: c.matched
|
|
524
|
+
? c.score
|
|
525
|
+
: (keywordResult.candidates.find(kc => kc.capabilityId === c.capabilityId)?.score ?? 0),
|
|
526
|
+
})),
|
|
527
|
+
};
|
|
528
|
+
steps?.push({ type: 'llm_match', status: 'pass', durationMs: Date.now() - t2, detail: `confidence: ${matchResult.confidence}%` });
|
|
529
|
+
}
|
|
530
|
+
catch (err) {
|
|
531
|
+
const isParseError = err instanceof LLMParseError;
|
|
532
|
+
if (!isParseError)
|
|
533
|
+
this.recordLLMFailure();
|
|
534
|
+
logger.warn(`LLM call failed — falling back to keyword: ${err instanceof Error ? err.message : String(err)}`);
|
|
535
|
+
steps?.push({ type: 'llm_match', status: 'fail', durationMs: Date.now() - t2, detail: String(err) });
|
|
536
|
+
matchResult = keywordResult;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return { matchResult: matchResult, resolvedVia };
|
|
544
|
+
}
|
|
544
545
|
/**
|
|
545
546
|
* Applies learning boost to a MatchResult and returns the updated result.
|
|
546
547
|
* Shared by ask() and explain() to avoid logic divergence.
|
|
@@ -642,3 +643,5 @@ export class CapmanEngine {
|
|
|
642
643
|
});
|
|
643
644
|
}
|
|
644
645
|
}
|
|
646
|
+
/** Maximum allowed query length in characters. Queries exceeding this throw RangeError. */
|
|
647
|
+
CapmanEngine.MAX_QUERY_LENGTH = 1000;
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ 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
5
|
export { match, matchWithLLM, extractParams, } from './matcher';
|
|
6
|
+
export { LLMParseError } from './matcher';
|
|
6
7
|
export type { LLMMatcherOptions } from './matcher';
|
|
7
8
|
export { resolve } from './resolver';
|
|
8
9
|
export type { ResolveOptions, AuthContext } from './resolver';
|
|
@@ -14,10 +15,10 @@ export { FileLearningStore, MemoryLearningStore } from './learning';
|
|
|
14
15
|
export type { LearningStore, LearningEntry, KeywordStats } from './learning';
|
|
15
16
|
export { parseOpenAPI } from './parser';
|
|
16
17
|
export type { ParseResult } from './parser';
|
|
17
|
-
import type { Manifest, MatchResult, ResolveResult } from './types';
|
|
18
|
+
import type { Manifest, MatchResult, ResolveResult, MatchMode } from './types';
|
|
18
19
|
import type { LLMMatcherOptions } from './matcher';
|
|
19
20
|
import type { ResolveOptions } from './resolver';
|
|
20
|
-
export type MatchMode
|
|
21
|
+
export type { MatchMode } from './types';
|
|
21
22
|
export interface AskOptions extends ResolveOptions {
|
|
22
23
|
llm?: LLMMatcherOptions['llm'];
|
|
23
24
|
/**
|
package/dist/esm/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { setLogLevel } from './logger';
|
|
2
2
|
export { generate, loadConfig, writeManifest, readManifest, validate, generateStarterConfig, } from './generator';
|
|
3
3
|
export { match, matchWithLLM, extractParams, } from './matcher';
|
|
4
|
+
export { LLMParseError } from './matcher';
|
|
4
5
|
export { resolve } from './resolver';
|
|
5
6
|
// ─── Engine (recommended API) ─────────────────────────────────────────────────
|
|
6
7
|
export { CapmanEngine } from './engine';
|
package/dist/esm/learning.d.ts
CHANGED
|
@@ -38,6 +38,7 @@ export declare class FileLearningStore implements LearningStore {
|
|
|
38
38
|
private statsCounter;
|
|
39
39
|
constructor(filePath?: string);
|
|
40
40
|
private updateIndex;
|
|
41
|
+
private subtractFromIndex;
|
|
41
42
|
private rebuildIndex;
|
|
42
43
|
private load;
|
|
43
44
|
private save;
|
|
@@ -59,7 +60,7 @@ export declare class MemoryLearningStore implements LearningStore {
|
|
|
59
60
|
getStats(): Promise<KeywordStats>;
|
|
60
61
|
getIndex(): Promise<Record<string, Record<string, number>>>;
|
|
61
62
|
private updateIndex;
|
|
62
|
-
private
|
|
63
|
+
private subtractFromIndex;
|
|
63
64
|
getTopCapabilities(limit?: number): Promise<Array<{
|
|
64
65
|
id: string;
|
|
65
66
|
hits: number;
|