capman 0.3.0 → 0.4.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/bin/capman.js +70 -0
- package/dist/cjs/cache.d.ts.map +1 -1
- package/dist/cjs/cache.js +15 -17
- package/dist/cjs/cache.js.map +1 -1
- package/dist/cjs/engine.d.ts +3 -3
- package/dist/cjs/engine.d.ts.map +1 -1
- package/dist/cjs/engine.js +87 -8
- 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.map +1 -1
- package/dist/cjs/learning.d.ts.map +1 -1
- package/dist/cjs/learning.js +60 -89
- package/dist/cjs/learning.js.map +1 -1
- package/dist/cjs/matcher.d.ts.map +1 -1
- package/dist/cjs/matcher.js +11 -0
- package/dist/cjs/matcher.js.map +1 -1
- package/dist/cjs/resolver.d.ts +4 -1
- package/dist/cjs/resolver.d.ts.map +1 -1
- package/dist/cjs/resolver.js +36 -38
- package/dist/cjs/resolver.js.map +1 -1
- package/dist/cjs/types.d.ts +26 -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.js +15 -17
- package/dist/esm/engine.js +89 -10
- package/dist/esm/learning.js +60 -89
- package/dist/esm/matcher.js +11 -0
- package/dist/esm/resolver.js +36 -38
- package/dist/esm/version.js +1 -1
- package/package.json +1 -1
package/dist/esm/cache.js
CHANGED
|
@@ -43,36 +43,33 @@ export class FileCache {
|
|
|
43
43
|
this.store = new Map();
|
|
44
44
|
this.loaded = false;
|
|
45
45
|
this.filePath = path.resolve(process.cwd(), filePath);
|
|
46
|
+
logger.info(`FileCache initialized — writing to: ${this.filePath}`);
|
|
46
47
|
}
|
|
47
|
-
load() {
|
|
48
|
+
async load() {
|
|
48
49
|
if (this.loaded)
|
|
49
50
|
return;
|
|
50
51
|
try {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
logger.debug(`File cache loaded: ${this.store.size} entries`);
|
|
55
|
-
}
|
|
52
|
+
const raw = await fs.promises.readFile(this.filePath, 'utf-8');
|
|
53
|
+
this.store = new Map(Object.entries(JSON.parse(raw)));
|
|
54
|
+
logger.debug(`File cache loaded: ${this.store.size} entries`);
|
|
56
55
|
}
|
|
57
56
|
catch {
|
|
58
|
-
|
|
57
|
+
// File doesn't exist yet — start fresh
|
|
59
58
|
}
|
|
60
59
|
this.loaded = true;
|
|
61
60
|
}
|
|
62
|
-
save() {
|
|
61
|
+
async save() {
|
|
63
62
|
try {
|
|
64
63
|
const dir = path.dirname(this.filePath);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const obj = Object.fromEntries(this.store);
|
|
68
|
-
fs.writeFileSync(this.filePath, JSON.stringify(obj, null, 2));
|
|
64
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
65
|
+
await fs.promises.writeFile(this.filePath, JSON.stringify(Object.fromEntries(this.store), null, 2));
|
|
69
66
|
}
|
|
70
67
|
catch {
|
|
71
68
|
logger.warn(`Failed to save file cache to ${this.filePath}`);
|
|
72
69
|
}
|
|
73
70
|
}
|
|
74
71
|
async get(query) {
|
|
75
|
-
this.load();
|
|
72
|
+
await this.load();
|
|
76
73
|
const key = normalizeQuery(query);
|
|
77
74
|
const entry = this.store.get(key);
|
|
78
75
|
if (entry) {
|
|
@@ -83,7 +80,7 @@ export class FileCache {
|
|
|
83
80
|
return null;
|
|
84
81
|
}
|
|
85
82
|
async set(query, result) {
|
|
86
|
-
this.load();
|
|
83
|
+
await this.load();
|
|
87
84
|
const key = normalizeQuery(query);
|
|
88
85
|
this.store.set(key, {
|
|
89
86
|
query,
|
|
@@ -91,15 +88,15 @@ export class FileCache {
|
|
|
91
88
|
cachedAt: new Date().toISOString(),
|
|
92
89
|
hits: 0,
|
|
93
90
|
});
|
|
94
|
-
this.save();
|
|
91
|
+
await this.save();
|
|
95
92
|
logger.debug(`Cache set (file): "${query}"`);
|
|
96
93
|
}
|
|
97
94
|
async clear() {
|
|
98
95
|
this.store.clear();
|
|
99
|
-
this.save();
|
|
96
|
+
await this.save();
|
|
100
97
|
}
|
|
101
98
|
async size() {
|
|
102
|
-
this.load();
|
|
99
|
+
await this.load();
|
|
103
100
|
return this.store.size;
|
|
104
101
|
}
|
|
105
102
|
}
|
|
@@ -119,6 +116,7 @@ export class ComboCache {
|
|
|
119
116
|
if (fileHit) {
|
|
120
117
|
// Promote to memory for next time
|
|
121
118
|
await this.memory.set(query, fileHit.result);
|
|
119
|
+
logger.debug(`Cache promoted to memory: "${query}"`);
|
|
122
120
|
return fileHit;
|
|
123
121
|
}
|
|
124
122
|
return null;
|
package/dist/esm/engine.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { match as _match, matchWithLLM as _matchWithLLM } from './matcher';
|
|
2
2
|
import { resolve as _resolve } from './resolver';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { MemoryCache } from './cache';
|
|
4
|
+
import { MemoryLearningStore } from './learning';
|
|
5
5
|
import { logger } from './logger';
|
|
6
6
|
// ─── CapmanEngine ─────────────────────────────────────────────────────────────
|
|
7
7
|
export class CapmanEngine {
|
|
@@ -13,14 +13,16 @@ export class CapmanEngine {
|
|
|
13
13
|
this.auth = options.auth;
|
|
14
14
|
this.headers = options.headers;
|
|
15
15
|
this.threshold = options.threshold ?? 50;
|
|
16
|
-
// Cache — default
|
|
16
|
+
// Cache — default MemoryCache (no filesystem writes), or disabled with false
|
|
17
|
+
// Use FileCache or ComboCache explicitly for persistence across restarts
|
|
17
18
|
this.cache = options.cache === false
|
|
18
19
|
? null
|
|
19
|
-
: (options.cache ?? new
|
|
20
|
-
// Learning — default
|
|
20
|
+
: (options.cache ?? new MemoryCache());
|
|
21
|
+
// Learning — default MemoryLearningStore (no filesystem writes), or disabled with false
|
|
22
|
+
// Use FileLearningStore explicitly for persistence across restarts
|
|
21
23
|
this.learning = options.learning === false
|
|
22
24
|
? null
|
|
23
|
-
: (options.learning ?? new
|
|
25
|
+
: (options.learning ?? new MemoryLearningStore());
|
|
24
26
|
logger.info(`CapmanEngine initialized — mode: ${this.mode}, cache: ${this.cache ? 'enabled' : 'disabled'}, learning: ${this.learning ? 'enabled' : 'disabled'}`);
|
|
25
27
|
}
|
|
26
28
|
/**
|
|
@@ -36,68 +38,145 @@ export class CapmanEngine {
|
|
|
36
38
|
*/
|
|
37
39
|
async ask(query, overrides = {}) {
|
|
38
40
|
const start = Date.now();
|
|
41
|
+
const steps = [];
|
|
42
|
+
let resolvedVia = 'keyword';
|
|
39
43
|
// ── Step 1: Check cache ──────────────────────────────────────────────────
|
|
44
|
+
const cacheStart = Date.now();
|
|
40
45
|
if (this.cache) {
|
|
41
46
|
const cached = await this.cache.get(query);
|
|
42
47
|
if (cached) {
|
|
48
|
+
steps.push({ type: 'cache_check', status: 'hit', durationMs: Date.now() - cacheStart, detail: 'Served from cache' });
|
|
43
49
|
logger.info(`Cache hit for: "${query}"`);
|
|
44
50
|
const resolution = await _resolve(cached.result, cached.result.extractedParams, this.resolveOptions(overrides));
|
|
51
|
+
const trace = {
|
|
52
|
+
query,
|
|
53
|
+
candidates: cached.result.candidates ?? [],
|
|
54
|
+
reasoning: [`Served from cache (original: ${cached.result.reasoning})`],
|
|
55
|
+
steps,
|
|
56
|
+
resolvedVia: 'cache',
|
|
57
|
+
totalMs: Date.now() - start,
|
|
58
|
+
};
|
|
45
59
|
const result = {
|
|
46
60
|
match: cached.result,
|
|
47
61
|
resolution,
|
|
48
62
|
resolvedVia: 'cache',
|
|
49
63
|
durationMs: Date.now() - start,
|
|
64
|
+
trace,
|
|
50
65
|
};
|
|
51
66
|
await this.recordLearning(query, cached.result, 'cache');
|
|
52
67
|
return result;
|
|
53
68
|
}
|
|
69
|
+
steps.push({ type: 'cache_check', status: 'miss', durationMs: Date.now() - cacheStart });
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
steps.push({ type: 'cache_check', status: 'skip', durationMs: 0, detail: 'Cache disabled' });
|
|
54
73
|
}
|
|
55
74
|
// ── Step 2: Match ────────────────────────────────────────────────────────
|
|
56
75
|
let matchResult;
|
|
57
|
-
let resolvedVia = 'keyword';
|
|
58
76
|
switch (this.mode) {
|
|
59
77
|
case 'cheap': {
|
|
78
|
+
const t = Date.now();
|
|
60
79
|
matchResult = _match(query, this.manifest);
|
|
80
|
+
steps.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: `confidence: ${matchResult.confidence}%` });
|
|
61
81
|
break;
|
|
62
82
|
}
|
|
63
83
|
case 'accurate': {
|
|
64
84
|
if (this.llm) {
|
|
85
|
+
const t = Date.now();
|
|
65
86
|
matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
|
|
66
87
|
resolvedVia = 'llm';
|
|
88
|
+
steps.push({ type: 'llm_match', status: 'pass', durationMs: Date.now() - t, detail: `confidence: ${matchResult.confidence}%` });
|
|
67
89
|
}
|
|
68
90
|
else {
|
|
69
91
|
logger.warn('accurate mode requires llm — falling back to keyword');
|
|
92
|
+
const t = Date.now();
|
|
70
93
|
matchResult = _match(query, this.manifest);
|
|
94
|
+
steps.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: 'llm not provided, used keyword' });
|
|
71
95
|
}
|
|
72
96
|
break;
|
|
73
97
|
}
|
|
74
98
|
case 'balanced':
|
|
75
99
|
default: {
|
|
100
|
+
const t1 = Date.now();
|
|
76
101
|
const keywordResult = _match(query, this.manifest);
|
|
102
|
+
steps.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t1, detail: `confidence: ${keywordResult.confidence}%` });
|
|
77
103
|
if (keywordResult.confidence >= this.threshold || !this.llm) {
|
|
78
104
|
matchResult = keywordResult;
|
|
79
105
|
}
|
|
80
106
|
else {
|
|
81
107
|
logger.info(`Low confidence (${keywordResult.confidence}%) — escalating to LLM`);
|
|
108
|
+
const t2 = Date.now();
|
|
82
109
|
matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
|
|
83
110
|
resolvedVia = 'llm';
|
|
111
|
+
steps.push({ type: 'llm_match', status: 'pass', durationMs: Date.now() - t2, detail: `confidence: ${matchResult.confidence}%` });
|
|
84
112
|
}
|
|
85
113
|
break;
|
|
86
114
|
}
|
|
87
115
|
}
|
|
88
|
-
// ── Step 3:
|
|
116
|
+
// ── Step 3: Privacy check ────────────────────────────────────────────────
|
|
117
|
+
if (matchResult.capability) {
|
|
118
|
+
const privacyLevel = matchResult.capability.privacy.level;
|
|
119
|
+
steps.push({
|
|
120
|
+
type: 'privacy_check',
|
|
121
|
+
status: 'pass',
|
|
122
|
+
durationMs: 0,
|
|
123
|
+
detail: `level: ${privacyLevel}`,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
// ── Step 4: Cache the match result ───────────────────────────────────────
|
|
89
127
|
if (this.cache && matchResult.capability) {
|
|
90
128
|
await this.cache.set(query, matchResult);
|
|
91
129
|
}
|
|
92
|
-
// ── Step
|
|
130
|
+
// ── Step 5: Resolve ──────────────────────────────────────────────────────
|
|
131
|
+
const resolveStart = Date.now();
|
|
93
132
|
const resolution = await _resolve(matchResult, matchResult.extractedParams, this.resolveOptions(overrides));
|
|
94
|
-
|
|
133
|
+
steps.push({
|
|
134
|
+
type: 'resolve',
|
|
135
|
+
status: resolution.success ? 'pass' : 'fail',
|
|
136
|
+
durationMs: Date.now() - resolveStart,
|
|
137
|
+
detail: resolution.error ?? `via ${resolution.resolverType}`,
|
|
138
|
+
});
|
|
139
|
+
// ── Step 6: Build reasoning array ────────────────────────────────────────
|
|
140
|
+
const reasoning = [];
|
|
141
|
+
if (matchResult.candidates?.length) {
|
|
142
|
+
const winner = matchResult.candidates.find(c => c.matched);
|
|
143
|
+
const rejected = matchResult.candidates
|
|
144
|
+
.filter(c => !c.matched && c.score > 0)
|
|
145
|
+
.sort((a, b) => b.score - a.score)
|
|
146
|
+
.slice(0, 3);
|
|
147
|
+
if (winner) {
|
|
148
|
+
reasoning.push(`Matched "${winner.capabilityId}" with ${winner.score}% confidence`);
|
|
149
|
+
}
|
|
150
|
+
if (rejected.length) {
|
|
151
|
+
reasoning.push(`Rejected: ${rejected.map(r => `${r.capabilityId} (${r.score}%)`).join(', ')}`);
|
|
152
|
+
}
|
|
153
|
+
reasoning.push(`Resolved via: ${resolvedVia}`);
|
|
154
|
+
if (matchResult.extractedParams && Object.keys(matchResult.extractedParams).length) {
|
|
155
|
+
const params = Object.entries(matchResult.extractedParams)
|
|
156
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
157
|
+
.join(', ');
|
|
158
|
+
reasoning.push(`Extracted params: ${params}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
reasoning.push(matchResult.reasoning);
|
|
163
|
+
}
|
|
164
|
+
// ── Step 7: Record learning ──────────────────────────────────────────────
|
|
95
165
|
await this.recordLearning(query, matchResult, resolvedVia);
|
|
166
|
+
const trace = {
|
|
167
|
+
query,
|
|
168
|
+
candidates: matchResult.candidates ?? [],
|
|
169
|
+
reasoning,
|
|
170
|
+
steps,
|
|
171
|
+
resolvedVia,
|
|
172
|
+
totalMs: Date.now() - start,
|
|
173
|
+
};
|
|
96
174
|
return {
|
|
97
175
|
match: matchResult,
|
|
98
176
|
resolution,
|
|
99
177
|
resolvedVia,
|
|
100
178
|
durationMs: Date.now() - start,
|
|
179
|
+
trace,
|
|
101
180
|
};
|
|
102
181
|
}
|
|
103
182
|
/**
|
package/dist/esm/learning.js
CHANGED
|
@@ -1,34 +1,74 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { logger } from './logger';
|
|
4
|
+
// ─── Shared computation helpers ───────────────────────────────────────────────
|
|
5
|
+
function computeStats(entries) {
|
|
6
|
+
const index = {};
|
|
7
|
+
let totalQueries = 0;
|
|
8
|
+
let llmQueries = 0;
|
|
9
|
+
let cacheHits = 0;
|
|
10
|
+
let outOfScope = 0;
|
|
11
|
+
for (const entry of entries) {
|
|
12
|
+
totalQueries++;
|
|
13
|
+
if (entry.resolvedVia === 'llm')
|
|
14
|
+
llmQueries++;
|
|
15
|
+
if (entry.resolvedVia === 'cache')
|
|
16
|
+
cacheHits++;
|
|
17
|
+
if (!entry.capabilityId)
|
|
18
|
+
outOfScope++;
|
|
19
|
+
if (entry.capabilityId) {
|
|
20
|
+
const words = entry.query.toLowerCase()
|
|
21
|
+
.split(/\W+/)
|
|
22
|
+
.filter(w => w.length > 2);
|
|
23
|
+
for (const word of words) {
|
|
24
|
+
if (!index[word])
|
|
25
|
+
index[word] = {};
|
|
26
|
+
index[word][entry.capabilityId] =
|
|
27
|
+
(index[word][entry.capabilityId] ?? 0) + 1;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { index, totalQueries, llmQueries, cacheHits, outOfScope };
|
|
32
|
+
}
|
|
33
|
+
function computeTopCapabilities(entries, limit) {
|
|
34
|
+
const counts = {};
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
if (entry.capabilityId) {
|
|
37
|
+
counts[entry.capabilityId] = (counts[entry.capabilityId] ?? 0) + 1;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return Object.entries(counts)
|
|
41
|
+
.sort(([, a], [, b]) => b - a)
|
|
42
|
+
.slice(0, limit)
|
|
43
|
+
.map(([id, hits]) => ({ id, hits }));
|
|
44
|
+
}
|
|
4
45
|
// ─── File Learning Store ──────────────────────────────────────────────────────
|
|
5
46
|
export class FileLearningStore {
|
|
6
47
|
constructor(filePath = '.capman/learning.json') {
|
|
7
48
|
this.entries = [];
|
|
8
49
|
this.loaded = false;
|
|
9
50
|
this.filePath = path.resolve(process.cwd(), filePath);
|
|
51
|
+
logger.info(`FileLearningStore initialized — writing to: ${this.filePath}`);
|
|
10
52
|
}
|
|
11
|
-
load() {
|
|
53
|
+
async load() {
|
|
12
54
|
if (this.loaded)
|
|
13
55
|
return;
|
|
14
56
|
try {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
57
|
+
const raw = await fs.promises.readFile(this.filePath, 'utf-8');
|
|
58
|
+
const parsed = JSON.parse(raw);
|
|
59
|
+
this.entries = parsed.entries ?? [];
|
|
60
|
+
logger.debug(`Learning store loaded: ${this.entries.length} entries`);
|
|
20
61
|
}
|
|
21
62
|
catch {
|
|
22
|
-
|
|
63
|
+
// File doesn't exist yet — start fresh
|
|
23
64
|
}
|
|
24
65
|
this.loaded = true;
|
|
25
66
|
}
|
|
26
|
-
save() {
|
|
67
|
+
async save() {
|
|
27
68
|
try {
|
|
28
69
|
const dir = path.dirname(this.filePath);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
fs.writeFileSync(this.filePath, JSON.stringify({
|
|
70
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
71
|
+
await fs.promises.writeFile(this.filePath, JSON.stringify({
|
|
32
72
|
entries: this.entries,
|
|
33
73
|
updatedAt: new Date().toISOString(),
|
|
34
74
|
}, null, 2));
|
|
@@ -38,57 +78,22 @@ export class FileLearningStore {
|
|
|
38
78
|
}
|
|
39
79
|
}
|
|
40
80
|
async record(entry) {
|
|
41
|
-
this.load();
|
|
81
|
+
await this.load();
|
|
42
82
|
this.entries.push(entry);
|
|
43
|
-
this.save();
|
|
83
|
+
await this.save();
|
|
44
84
|
logger.debug(`Learning recorded: "${entry.query}" → ${entry.capabilityId ?? 'OUT_OF_SCOPE'} via ${entry.resolvedVia}`);
|
|
45
85
|
}
|
|
46
86
|
async getStats() {
|
|
47
|
-
this.load();
|
|
48
|
-
|
|
49
|
-
let totalQueries = 0;
|
|
50
|
-
let llmQueries = 0;
|
|
51
|
-
let cacheHits = 0;
|
|
52
|
-
let outOfScope = 0;
|
|
53
|
-
for (const entry of this.entries) {
|
|
54
|
-
totalQueries++;
|
|
55
|
-
if (entry.resolvedVia === 'llm')
|
|
56
|
-
llmQueries++;
|
|
57
|
-
if (entry.resolvedVia === 'cache')
|
|
58
|
-
cacheHits++;
|
|
59
|
-
if (!entry.capabilityId)
|
|
60
|
-
outOfScope++;
|
|
61
|
-
if (entry.capabilityId) {
|
|
62
|
-
// Index each word of the query against the matched capability
|
|
63
|
-
const words = entry.query.toLowerCase()
|
|
64
|
-
.split(/\W+/)
|
|
65
|
-
.filter(w => w.length > 2);
|
|
66
|
-
for (const word of words) {
|
|
67
|
-
if (!index[word])
|
|
68
|
-
index[word] = {};
|
|
69
|
-
index[word][entry.capabilityId] =
|
|
70
|
-
(index[word][entry.capabilityId] ?? 0) + 1;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return { index, totalQueries, llmQueries, cacheHits, outOfScope };
|
|
87
|
+
await this.load();
|
|
88
|
+
return computeStats(this.entries);
|
|
75
89
|
}
|
|
76
90
|
async getTopCapabilities(limit = 5) {
|
|
77
|
-
this.load();
|
|
78
|
-
|
|
79
|
-
for (const entry of this.entries) {
|
|
80
|
-
if (entry.capabilityId) {
|
|
81
|
-
counts[entry.capabilityId] = (counts[entry.capabilityId] ?? 0) + 1;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return Object.entries(counts)
|
|
85
|
-
.sort(([, a], [, b]) => b - a)
|
|
86
|
-
.slice(0, limit)
|
|
87
|
-
.map(([id, hits]) => ({ id, hits }));
|
|
91
|
+
await this.load();
|
|
92
|
+
return computeTopCapabilities(this.entries, limit);
|
|
88
93
|
}
|
|
89
94
|
async clear() {
|
|
90
95
|
this.entries = [];
|
|
91
|
-
this.save();
|
|
96
|
+
await this.save();
|
|
92
97
|
}
|
|
93
98
|
}
|
|
94
99
|
// ─── Memory Learning Store (for testing) ─────────────────────────────────────
|
|
@@ -100,44 +105,10 @@ export class MemoryLearningStore {
|
|
|
100
105
|
this.entries.push(entry);
|
|
101
106
|
}
|
|
102
107
|
async getStats() {
|
|
103
|
-
|
|
104
|
-
let totalQueries = 0;
|
|
105
|
-
let llmQueries = 0;
|
|
106
|
-
let cacheHits = 0;
|
|
107
|
-
let outOfScope = 0;
|
|
108
|
-
for (const entry of this.entries) {
|
|
109
|
-
totalQueries++;
|
|
110
|
-
if (entry.resolvedVia === 'llm')
|
|
111
|
-
llmQueries++;
|
|
112
|
-
if (entry.resolvedVia === 'cache')
|
|
113
|
-
cacheHits++;
|
|
114
|
-
if (!entry.capabilityId)
|
|
115
|
-
outOfScope++;
|
|
116
|
-
if (entry.capabilityId) {
|
|
117
|
-
const words = entry.query.toLowerCase()
|
|
118
|
-
.split(/\W+/)
|
|
119
|
-
.filter(w => w.length > 2);
|
|
120
|
-
for (const word of words) {
|
|
121
|
-
if (!index[word])
|
|
122
|
-
index[word] = {};
|
|
123
|
-
index[word][entry.capabilityId] =
|
|
124
|
-
(index[word][entry.capabilityId] ?? 0) + 1;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return { index, totalQueries, llmQueries, cacheHits, outOfScope };
|
|
108
|
+
return computeStats(this.entries);
|
|
129
109
|
}
|
|
130
110
|
async getTopCapabilities(limit = 5) {
|
|
131
|
-
|
|
132
|
-
for (const entry of this.entries) {
|
|
133
|
-
if (entry.capabilityId) {
|
|
134
|
-
counts[entry.capabilityId] = (counts[entry.capabilityId] ?? 0) + 1;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
return Object.entries(counts)
|
|
138
|
-
.sort(([, a], [, b]) => b - a)
|
|
139
|
-
.slice(0, limit)
|
|
140
|
-
.map(([id, hits]) => ({ id, hits }));
|
|
111
|
+
return computeTopCapabilities(this.entries, limit);
|
|
141
112
|
}
|
|
142
113
|
async clear() {
|
|
143
114
|
this.entries = [];
|
package/dist/esm/matcher.js
CHANGED
|
@@ -140,33 +140,44 @@ export function match(query, manifest) {
|
|
|
140
140
|
logger.debug(`Manifest has ${manifest.capabilities.length} capabilities`);
|
|
141
141
|
let best = null;
|
|
142
142
|
let bestScore = 0;
|
|
143
|
+
const allScores = [];
|
|
143
144
|
for (const cap of manifest.capabilities) {
|
|
144
145
|
const score = scoreCapability(query, cap);
|
|
145
146
|
logger.debug(` scored "${cap.id}": ${score}%`);
|
|
147
|
+
allScores.push({ cap, score });
|
|
146
148
|
if (score > bestScore) {
|
|
147
149
|
bestScore = score;
|
|
148
150
|
best = cap;
|
|
149
151
|
}
|
|
150
152
|
}
|
|
153
|
+
const candidates = allScores.map(({ cap, score }) => ({
|
|
154
|
+
capabilityId: cap.id,
|
|
155
|
+
score,
|
|
156
|
+
matched: cap.id === best?.id,
|
|
157
|
+
}));
|
|
151
158
|
if (!best || bestScore < 50) {
|
|
152
159
|
logger.info(`No match above threshold (best: ${bestScore}% for "${best?.id ?? 'none'}")`);
|
|
160
|
+
// Out of scope return:
|
|
153
161
|
return {
|
|
154
162
|
capability: null,
|
|
155
163
|
confidence: bestScore,
|
|
156
164
|
intent: 'out_of_scope',
|
|
157
165
|
extractedParams: {},
|
|
158
166
|
reasoning: `No capability matched with sufficient confidence (best score: ${bestScore})`,
|
|
167
|
+
candidates,
|
|
159
168
|
};
|
|
160
169
|
}
|
|
161
170
|
const params = extractParams(query, best);
|
|
162
171
|
logger.info(`Matched "${best.id}" at ${bestScore}% confidence`);
|
|
163
172
|
logger.debug(`Extracted params: ${JSON.stringify(params)}`);
|
|
173
|
+
// Matched return:
|
|
164
174
|
return {
|
|
165
175
|
capability: best,
|
|
166
176
|
confidence: bestScore,
|
|
167
177
|
intent: resolverToIntent(best),
|
|
168
178
|
extractedParams: params,
|
|
169
179
|
reasoning: `Matched "${best.id}" via keyword scoring (score: ${bestScore})`,
|
|
180
|
+
candidates,
|
|
170
181
|
};
|
|
171
182
|
}
|
|
172
183
|
export async function matchWithLLM(query, manifest, options) {
|
package/dist/esm/resolver.js
CHANGED
|
@@ -88,78 +88,76 @@ export async function resolve(matchResult, params = {}, options = {}) {
|
|
|
88
88
|
}
|
|
89
89
|
async function resolveApi(resolver, params, options) {
|
|
90
90
|
const startTime = Date.now();
|
|
91
|
+
const retries = options.retries ?? 0;
|
|
92
|
+
const timeoutMs = options.timeoutMs ?? 5000;
|
|
91
93
|
const apiCalls = resolver.endpoints.map(endpoint => ({
|
|
92
94
|
method: endpoint.method,
|
|
93
95
|
url: buildUrl(options.baseUrl ?? '', endpoint.path, params),
|
|
94
96
|
params,
|
|
95
97
|
}));
|
|
96
98
|
if (options.dryRun) {
|
|
97
|
-
return {
|
|
98
|
-
success: true,
|
|
99
|
-
resolverType: 'api',
|
|
100
|
-
apiCalls,
|
|
101
|
-
durationMs: Date.now() - startTime,
|
|
102
|
-
};
|
|
99
|
+
return { success: true, resolverType: 'api', apiCalls, durationMs: Date.now() - startTime };
|
|
103
100
|
}
|
|
104
101
|
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
105
102
|
if (!fetchFn) {
|
|
106
103
|
return {
|
|
107
|
-
success: true,
|
|
108
|
-
resolverType: 'api',
|
|
109
|
-
apiCalls,
|
|
104
|
+
success: true, resolverType: 'api', apiCalls,
|
|
110
105
|
durationMs: Date.now() - startTime,
|
|
111
106
|
error: 'No fetch available — returning call plan only',
|
|
112
107
|
};
|
|
113
108
|
}
|
|
109
|
+
// ── Fetch with retry + timeout ────────────────────────────────────────────
|
|
110
|
+
async function fetchWithRetry(call, attempt) {
|
|
111
|
+
const controller = new AbortController();
|
|
112
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
113
|
+
try {
|
|
114
|
+
const res = await fetchFn(call.url, {
|
|
115
|
+
method: call.method,
|
|
116
|
+
headers: options.headers ?? {},
|
|
117
|
+
signal: controller.signal,
|
|
118
|
+
body: ['POST', 'PUT', 'PATCH'].includes(call.method)
|
|
119
|
+
? JSON.stringify(call.params)
|
|
120
|
+
: undefined,
|
|
121
|
+
});
|
|
122
|
+
clearTimeout(timer);
|
|
123
|
+
return res;
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
clearTimeout(timer);
|
|
127
|
+
const isTimeout = err instanceof Error && err.name === 'AbortError';
|
|
128
|
+
if (attempt < retries) {
|
|
129
|
+
logger.warn(`Request failed (attempt ${attempt + 1}/${retries + 1}) — retrying: ${isTimeout ? 'timeout' : err}`);
|
|
130
|
+
return fetchWithRetry(call, attempt + 1);
|
|
131
|
+
}
|
|
132
|
+
throw isTimeout ? new Error(`Request timed out after ${timeoutMs}ms`) : err;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
114
135
|
try {
|
|
115
|
-
const responses = await Promise.all(apiCalls.map(c =>
|
|
116
|
-
method: c.method,
|
|
117
|
-
headers: options.headers ?? {},
|
|
118
|
-
body: ['POST', 'PUT', 'PATCH'].includes(c.method)
|
|
119
|
-
? JSON.stringify(c.params)
|
|
120
|
-
: undefined,
|
|
121
|
-
})));
|
|
122
|
-
// Check for HTTP errors
|
|
136
|
+
const responses = await Promise.all(apiCalls.map(c => fetchWithRetry(c, 0)));
|
|
123
137
|
const failedIdx = responses.findIndex(r => !r.ok);
|
|
124
138
|
if (failedIdx !== -1) {
|
|
125
139
|
const failed = responses[failedIdx];
|
|
126
140
|
return {
|
|
127
|
-
success: false,
|
|
128
|
-
resolverType: 'api',
|
|
129
|
-
apiCalls,
|
|
141
|
+
success: false, resolverType: 'api', apiCalls,
|
|
130
142
|
durationMs: Date.now() - startTime,
|
|
131
143
|
error: `API request failed: ${failed.status} ${failed.statusText}`,
|
|
132
144
|
};
|
|
133
145
|
}
|
|
134
|
-
// Parse response bodies
|
|
135
146
|
const enrichedCalls = await Promise.all(responses.map(async (res, i) => {
|
|
136
147
|
let data = undefined;
|
|
137
148
|
try {
|
|
138
149
|
const text = await res.text();
|
|
139
150
|
data = text ? JSON.parse(text) : undefined;
|
|
140
151
|
}
|
|
141
|
-
catch {
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
return {
|
|
145
|
-
...apiCalls[i],
|
|
146
|
-
status: res.status,
|
|
147
|
-
data,
|
|
148
|
-
};
|
|
152
|
+
catch { /* non-JSON response */ }
|
|
153
|
+
return { ...apiCalls[i], status: res.status, data };
|
|
149
154
|
}));
|
|
150
155
|
logger.debug(`API calls completed in ${Date.now() - startTime}ms`);
|
|
151
|
-
return {
|
|
152
|
-
success: true,
|
|
153
|
-
resolverType: 'api',
|
|
154
|
-
apiCalls: enrichedCalls,
|
|
155
|
-
durationMs: Date.now() - startTime,
|
|
156
|
-
};
|
|
156
|
+
return { success: true, resolverType: 'api', apiCalls: enrichedCalls, durationMs: Date.now() - startTime };
|
|
157
157
|
}
|
|
158
158
|
catch (err) {
|
|
159
159
|
return {
|
|
160
|
-
success: false,
|
|
161
|
-
resolverType: 'api',
|
|
162
|
-
apiCalls,
|
|
160
|
+
success: false, resolverType: 'api', apiCalls,
|
|
163
161
|
durationMs: Date.now() - startTime,
|
|
164
162
|
error: err instanceof Error ? err.message : String(err),
|
|
165
163
|
};
|
package/dist/esm/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by scripts/version.js — do not edit manually
|
|
2
|
-
export const VERSION = '0.
|
|
2
|
+
export const VERSION = '0.4.0';
|
package/package.json
CHANGED