capman 0.2.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 +221 -5
- package/dist/cjs/cache.d.ts +42 -0
- package/dist/cjs/cache.d.ts.map +1 -0
- package/dist/cjs/cache.js +179 -0
- package/dist/cjs/cache.js.map +1 -0
- package/dist/cjs/engine.d.ts +82 -0
- package/dist/cjs/engine.d.ts.map +1 -0
- package/dist/cjs/engine.js +233 -0
- package/dist/cjs/engine.js.map +1 -0
- package/dist/cjs/generator.js +2 -1
- package/dist/cjs/generator.js.map +1 -1
- package/dist/cjs/index.d.ts +7 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +13 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/learning.d.ts +56 -0
- package/dist/cjs/learning.d.ts.map +1 -0
- package/dist/cjs/learning.js +155 -0
- package/dist/cjs/learning.js.map +1 -0
- package/dist/cjs/matcher.d.ts.map +1 -1
- package/dist/cjs/matcher.js +38 -5
- 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 +51 -21
- package/dist/cjs/resolver.js.map +1 -1
- package/dist/cjs/schema.d.ts +10 -10
- package/dist/cjs/types.d.ts +38 -5
- package/dist/cjs/types.d.ts.map +1 -1
- package/dist/cjs/version.d.ts +2 -0
- package/dist/cjs/version.d.ts.map +1 -0
- package/dist/cjs/version.js +6 -0
- package/dist/cjs/version.js.map +1 -0
- package/dist/esm/cache.js +139 -0
- package/dist/esm/engine.js +228 -0
- package/dist/esm/generator.js +2 -1
- package/dist/esm/index.js +6 -0
- package/dist/esm/learning.js +116 -0
- package/dist/esm/matcher.js +38 -5
- package/dist/esm/resolver.js +51 -21
- package/dist/esm/version.js +2 -0
- package/package.json +18 -12
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { logger } from './logger';
|
|
4
|
+
// ─── Normalize query for cache key ────────────────────────────────────────────
|
|
5
|
+
function normalizeQuery(query) {
|
|
6
|
+
return query.toLowerCase().trim().replace(/\s+/g, ' ');
|
|
7
|
+
}
|
|
8
|
+
// ─── Memory Cache ─────────────────────────────────────────────────────────────
|
|
9
|
+
export class MemoryCache {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.store = new Map();
|
|
12
|
+
}
|
|
13
|
+
async get(query) {
|
|
14
|
+
const key = normalizeQuery(query);
|
|
15
|
+
const entry = this.store.get(key);
|
|
16
|
+
if (entry) {
|
|
17
|
+
entry.hits++;
|
|
18
|
+
logger.debug(`Cache hit (memory): "${query}"`);
|
|
19
|
+
return entry;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
async set(query, result) {
|
|
24
|
+
const key = normalizeQuery(query);
|
|
25
|
+
this.store.set(key, {
|
|
26
|
+
query,
|
|
27
|
+
result,
|
|
28
|
+
cachedAt: new Date().toISOString(),
|
|
29
|
+
hits: 0,
|
|
30
|
+
});
|
|
31
|
+
logger.debug(`Cache set (memory): "${query}"`);
|
|
32
|
+
}
|
|
33
|
+
async clear() {
|
|
34
|
+
this.store.clear();
|
|
35
|
+
}
|
|
36
|
+
async size() {
|
|
37
|
+
return this.store.size;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// ─── File Cache ───────────────────────────────────────────────────────────────
|
|
41
|
+
export class FileCache {
|
|
42
|
+
constructor(filePath = '.capman/cache.json') {
|
|
43
|
+
this.store = new Map();
|
|
44
|
+
this.loaded = false;
|
|
45
|
+
this.filePath = path.resolve(process.cwd(), filePath);
|
|
46
|
+
logger.info(`FileCache initialized — writing to: ${this.filePath}`);
|
|
47
|
+
}
|
|
48
|
+
async load() {
|
|
49
|
+
if (this.loaded)
|
|
50
|
+
return;
|
|
51
|
+
try {
|
|
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`);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// File doesn't exist yet — start fresh
|
|
58
|
+
}
|
|
59
|
+
this.loaded = true;
|
|
60
|
+
}
|
|
61
|
+
async save() {
|
|
62
|
+
try {
|
|
63
|
+
const dir = path.dirname(this.filePath);
|
|
64
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
65
|
+
await fs.promises.writeFile(this.filePath, JSON.stringify(Object.fromEntries(this.store), null, 2));
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
logger.warn(`Failed to save file cache to ${this.filePath}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async get(query) {
|
|
72
|
+
await this.load();
|
|
73
|
+
const key = normalizeQuery(query);
|
|
74
|
+
const entry = this.store.get(key);
|
|
75
|
+
if (entry) {
|
|
76
|
+
entry.hits++;
|
|
77
|
+
logger.debug(`Cache hit (file): "${query}"`);
|
|
78
|
+
return entry;
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
async set(query, result) {
|
|
83
|
+
await this.load();
|
|
84
|
+
const key = normalizeQuery(query);
|
|
85
|
+
this.store.set(key, {
|
|
86
|
+
query,
|
|
87
|
+
result,
|
|
88
|
+
cachedAt: new Date().toISOString(),
|
|
89
|
+
hits: 0,
|
|
90
|
+
});
|
|
91
|
+
await this.save();
|
|
92
|
+
logger.debug(`Cache set (file): "${query}"`);
|
|
93
|
+
}
|
|
94
|
+
async clear() {
|
|
95
|
+
this.store.clear();
|
|
96
|
+
await this.save();
|
|
97
|
+
}
|
|
98
|
+
async size() {
|
|
99
|
+
await this.load();
|
|
100
|
+
return this.store.size;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// ─── Combo Cache (memory first, file fallback) ────────────────────────────────
|
|
104
|
+
export class ComboCache {
|
|
105
|
+
constructor(filePath = '.capman/cache.json') {
|
|
106
|
+
this.memory = new MemoryCache();
|
|
107
|
+
this.file = new FileCache(filePath);
|
|
108
|
+
}
|
|
109
|
+
async get(query) {
|
|
110
|
+
// Memory first — fastest
|
|
111
|
+
const memHit = await this.memory.get(query);
|
|
112
|
+
if (memHit)
|
|
113
|
+
return memHit;
|
|
114
|
+
// File fallback — persists across restarts
|
|
115
|
+
const fileHit = await this.file.get(query);
|
|
116
|
+
if (fileHit) {
|
|
117
|
+
// Promote to memory for next time
|
|
118
|
+
await this.memory.set(query, fileHit.result);
|
|
119
|
+
logger.debug(`Cache promoted to memory: "${query}"`);
|
|
120
|
+
return fileHit;
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
async set(query, result) {
|
|
125
|
+
await Promise.all([
|
|
126
|
+
this.memory.set(query, result),
|
|
127
|
+
this.file.set(query, result),
|
|
128
|
+
]);
|
|
129
|
+
}
|
|
130
|
+
async clear() {
|
|
131
|
+
await Promise.all([
|
|
132
|
+
this.memory.clear(),
|
|
133
|
+
this.file.clear(),
|
|
134
|
+
]);
|
|
135
|
+
}
|
|
136
|
+
async size() {
|
|
137
|
+
return this.file.size();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { match as _match, matchWithLLM as _matchWithLLM } from './matcher';
|
|
2
|
+
import { resolve as _resolve } from './resolver';
|
|
3
|
+
import { MemoryCache } from './cache';
|
|
4
|
+
import { MemoryLearningStore } from './learning';
|
|
5
|
+
import { logger } from './logger';
|
|
6
|
+
// ─── CapmanEngine ─────────────────────────────────────────────────────────────
|
|
7
|
+
export class CapmanEngine {
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.manifest = options.manifest;
|
|
10
|
+
this.mode = options.mode ?? 'balanced';
|
|
11
|
+
this.llm = options.llm;
|
|
12
|
+
this.baseUrl = options.baseUrl;
|
|
13
|
+
this.auth = options.auth;
|
|
14
|
+
this.headers = options.headers;
|
|
15
|
+
this.threshold = options.threshold ?? 50;
|
|
16
|
+
// Cache — default MemoryCache (no filesystem writes), or disabled with false
|
|
17
|
+
// Use FileCache or ComboCache explicitly for persistence across restarts
|
|
18
|
+
this.cache = options.cache === false
|
|
19
|
+
? null
|
|
20
|
+
: (options.cache ?? new MemoryCache());
|
|
21
|
+
// Learning — default MemoryLearningStore (no filesystem writes), or disabled with false
|
|
22
|
+
// Use FileLearningStore explicitly for persistence across restarts
|
|
23
|
+
this.learning = options.learning === false
|
|
24
|
+
? null
|
|
25
|
+
: (options.learning ?? new MemoryLearningStore());
|
|
26
|
+
logger.info(`CapmanEngine initialized — mode: ${this.mode}, cache: ${this.cache ? 'enabled' : 'disabled'}, learning: ${this.learning ? 'enabled' : 'disabled'}`);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Ask the engine a natural language query.
|
|
30
|
+
* Automatically handles caching, matching, resolution, and learning.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* const engine = new CapmanEngine({ manifest, llm: myLLM })
|
|
34
|
+
* const result = await engine.ask("Check availability for blue jacket")
|
|
35
|
+
* console.log(result.match.capability?.id) // check_product_availability
|
|
36
|
+
* console.log(result.resolution.apiCalls) // [{ url: '...', method: 'GET' }]
|
|
37
|
+
* console.log(result.resolvedVia) // 'keyword' | 'llm' | 'cache'
|
|
38
|
+
*/
|
|
39
|
+
async ask(query, overrides = {}) {
|
|
40
|
+
const start = Date.now();
|
|
41
|
+
const steps = [];
|
|
42
|
+
let resolvedVia = 'keyword';
|
|
43
|
+
// ── Step 1: Check cache ──────────────────────────────────────────────────
|
|
44
|
+
const cacheStart = Date.now();
|
|
45
|
+
if (this.cache) {
|
|
46
|
+
const cached = await this.cache.get(query);
|
|
47
|
+
if (cached) {
|
|
48
|
+
steps.push({ type: 'cache_check', status: 'hit', durationMs: Date.now() - cacheStart, detail: 'Served from cache' });
|
|
49
|
+
logger.info(`Cache hit for: "${query}"`);
|
|
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
|
+
};
|
|
59
|
+
const result = {
|
|
60
|
+
match: cached.result,
|
|
61
|
+
resolution,
|
|
62
|
+
resolvedVia: 'cache',
|
|
63
|
+
durationMs: Date.now() - start,
|
|
64
|
+
trace,
|
|
65
|
+
};
|
|
66
|
+
await this.recordLearning(query, cached.result, 'cache');
|
|
67
|
+
return result;
|
|
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' });
|
|
73
|
+
}
|
|
74
|
+
// ── Step 2: Match ────────────────────────────────────────────────────────
|
|
75
|
+
let matchResult;
|
|
76
|
+
switch (this.mode) {
|
|
77
|
+
case 'cheap': {
|
|
78
|
+
const t = Date.now();
|
|
79
|
+
matchResult = _match(query, this.manifest);
|
|
80
|
+
steps.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: `confidence: ${matchResult.confidence}%` });
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
case 'accurate': {
|
|
84
|
+
if (this.llm) {
|
|
85
|
+
const t = Date.now();
|
|
86
|
+
matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
|
|
87
|
+
resolvedVia = 'llm';
|
|
88
|
+
steps.push({ type: 'llm_match', status: 'pass', durationMs: Date.now() - t, detail: `confidence: ${matchResult.confidence}%` });
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
logger.warn('accurate mode requires llm — falling back to keyword');
|
|
92
|
+
const t = Date.now();
|
|
93
|
+
matchResult = _match(query, this.manifest);
|
|
94
|
+
steps.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: 'llm not provided, used keyword' });
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
case 'balanced':
|
|
99
|
+
default: {
|
|
100
|
+
const t1 = Date.now();
|
|
101
|
+
const keywordResult = _match(query, this.manifest);
|
|
102
|
+
steps.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t1, detail: `confidence: ${keywordResult.confidence}%` });
|
|
103
|
+
if (keywordResult.confidence >= this.threshold || !this.llm) {
|
|
104
|
+
matchResult = keywordResult;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
logger.info(`Low confidence (${keywordResult.confidence}%) — escalating to LLM`);
|
|
108
|
+
const t2 = Date.now();
|
|
109
|
+
matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
|
|
110
|
+
resolvedVia = 'llm';
|
|
111
|
+
steps.push({ type: 'llm_match', status: 'pass', durationMs: Date.now() - t2, detail: `confidence: ${matchResult.confidence}%` });
|
|
112
|
+
}
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
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 ───────────────────────────────────────
|
|
127
|
+
if (this.cache && matchResult.capability) {
|
|
128
|
+
await this.cache.set(query, matchResult);
|
|
129
|
+
}
|
|
130
|
+
// ── Step 5: Resolve ──────────────────────────────────────────────────────
|
|
131
|
+
const resolveStart = Date.now();
|
|
132
|
+
const resolution = await _resolve(matchResult, matchResult.extractedParams, this.resolveOptions(overrides));
|
|
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 ──────────────────────────────────────────────
|
|
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
|
+
};
|
|
174
|
+
return {
|
|
175
|
+
match: matchResult,
|
|
176
|
+
resolution,
|
|
177
|
+
resolvedVia,
|
|
178
|
+
durationMs: Date.now() - start,
|
|
179
|
+
trace,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Get stats from the learning store.
|
|
184
|
+
* Shows which capabilities are most used, LLM vs keyword ratio, cache hit rate.
|
|
185
|
+
*/
|
|
186
|
+
async getStats() {
|
|
187
|
+
if (!this.learning)
|
|
188
|
+
return null;
|
|
189
|
+
return this.learning.getStats();
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Get the most frequently matched capabilities.
|
|
193
|
+
*/
|
|
194
|
+
async getTopCapabilities(limit = 5) {
|
|
195
|
+
if (!this.learning)
|
|
196
|
+
return [];
|
|
197
|
+
return this.learning.getTopCapabilities(limit);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Clear the cache.
|
|
201
|
+
*/
|
|
202
|
+
async clearCache() {
|
|
203
|
+
if (this.cache)
|
|
204
|
+
await this.cache.clear();
|
|
205
|
+
}
|
|
206
|
+
// ── Private helpers ────────────────────────────────────────────────────────
|
|
207
|
+
resolveOptions(overrides = {}) {
|
|
208
|
+
return {
|
|
209
|
+
baseUrl: this.baseUrl,
|
|
210
|
+
auth: this.auth,
|
|
211
|
+
headers: this.headers,
|
|
212
|
+
...overrides,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
async recordLearning(query, matchResult, resolvedVia) {
|
|
216
|
+
if (!this.learning)
|
|
217
|
+
return;
|
|
218
|
+
await this.learning.record({
|
|
219
|
+
query,
|
|
220
|
+
capabilityId: matchResult.capability?.id ?? null,
|
|
221
|
+
confidence: matchResult.confidence,
|
|
222
|
+
intent: matchResult.intent,
|
|
223
|
+
extractedParams: matchResult.extractedParams,
|
|
224
|
+
resolvedVia,
|
|
225
|
+
timestamp: new Date().toISOString(),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
package/dist/esm/generator.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { VERSION } from './version';
|
|
1
2
|
import * as fs from 'fs';
|
|
2
3
|
import * as path from 'path';
|
|
3
4
|
import { validateConfig, validateManifest } from './schema';
|
|
4
5
|
import { logger } from './logger';
|
|
5
6
|
export function generate(config) {
|
|
6
7
|
return {
|
|
7
|
-
version:
|
|
8
|
+
version: VERSION,
|
|
8
9
|
app: config.app,
|
|
9
10
|
generatedAt: new Date().toISOString(),
|
|
10
11
|
capabilities: config.capabilities,
|
package/dist/esm/index.js
CHANGED
|
@@ -6,6 +6,12 @@ export { resolve } from './resolver';
|
|
|
6
6
|
// ─── Convenience: ask() — match + resolve in one call ────────────────────────
|
|
7
7
|
import { match as _match, matchWithLLM as _matchWithLLM } from './matcher';
|
|
8
8
|
import { resolve as _resolve } from './resolver';
|
|
9
|
+
// ─── Engine (recommended API) ─────────────────────────────────────────────────
|
|
10
|
+
export { CapmanEngine } from './engine';
|
|
11
|
+
// ─── Cache ────────────────────────────────────────────────────────────────────
|
|
12
|
+
export { MemoryCache, FileCache, ComboCache } from './cache';
|
|
13
|
+
// ─── Learning ─────────────────────────────────────────────────────────────────
|
|
14
|
+
export { FileLearningStore, MemoryLearningStore } from './learning';
|
|
9
15
|
/**
|
|
10
16
|
* One-shot convenience: match + resolve in a single call.
|
|
11
17
|
*
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
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
|
+
}
|
|
45
|
+
// ─── File Learning Store ──────────────────────────────────────────────────────
|
|
46
|
+
export class FileLearningStore {
|
|
47
|
+
constructor(filePath = '.capman/learning.json') {
|
|
48
|
+
this.entries = [];
|
|
49
|
+
this.loaded = false;
|
|
50
|
+
this.filePath = path.resolve(process.cwd(), filePath);
|
|
51
|
+
logger.info(`FileLearningStore initialized — writing to: ${this.filePath}`);
|
|
52
|
+
}
|
|
53
|
+
async load() {
|
|
54
|
+
if (this.loaded)
|
|
55
|
+
return;
|
|
56
|
+
try {
|
|
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`);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// File doesn't exist yet — start fresh
|
|
64
|
+
}
|
|
65
|
+
this.loaded = true;
|
|
66
|
+
}
|
|
67
|
+
async save() {
|
|
68
|
+
try {
|
|
69
|
+
const dir = path.dirname(this.filePath);
|
|
70
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
71
|
+
await fs.promises.writeFile(this.filePath, JSON.stringify({
|
|
72
|
+
entries: this.entries,
|
|
73
|
+
updatedAt: new Date().toISOString(),
|
|
74
|
+
}, null, 2));
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
logger.warn(`Failed to save learning store to ${this.filePath}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async record(entry) {
|
|
81
|
+
await this.load();
|
|
82
|
+
this.entries.push(entry);
|
|
83
|
+
await this.save();
|
|
84
|
+
logger.debug(`Learning recorded: "${entry.query}" → ${entry.capabilityId ?? 'OUT_OF_SCOPE'} via ${entry.resolvedVia}`);
|
|
85
|
+
}
|
|
86
|
+
async getStats() {
|
|
87
|
+
await this.load();
|
|
88
|
+
return computeStats(this.entries);
|
|
89
|
+
}
|
|
90
|
+
async getTopCapabilities(limit = 5) {
|
|
91
|
+
await this.load();
|
|
92
|
+
return computeTopCapabilities(this.entries, limit);
|
|
93
|
+
}
|
|
94
|
+
async clear() {
|
|
95
|
+
this.entries = [];
|
|
96
|
+
await this.save();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// ─── Memory Learning Store (for testing) ─────────────────────────────────────
|
|
100
|
+
export class MemoryLearningStore {
|
|
101
|
+
constructor() {
|
|
102
|
+
this.entries = [];
|
|
103
|
+
}
|
|
104
|
+
async record(entry) {
|
|
105
|
+
this.entries.push(entry);
|
|
106
|
+
}
|
|
107
|
+
async getStats() {
|
|
108
|
+
return computeStats(this.entries);
|
|
109
|
+
}
|
|
110
|
+
async getTopCapabilities(limit = 5) {
|
|
111
|
+
return computeTopCapabilities(this.entries, limit);
|
|
112
|
+
}
|
|
113
|
+
async clear() {
|
|
114
|
+
this.entries = [];
|
|
115
|
+
}
|
|
116
|
+
}
|
package/dist/esm/matcher.js
CHANGED
|
@@ -76,19 +76,41 @@ function extractParams(query, cap) {
|
|
|
76
76
|
// e.g. "profile for johndoe" → johndoe
|
|
77
77
|
// "articles by jane" → jane
|
|
78
78
|
// "tag javascript" → javascript
|
|
79
|
+
// Use param name and description as hints for what to look for
|
|
80
|
+
const paramHints = [param.name, ...param.description.toLowerCase().split(/\s+/)]
|
|
81
|
+
.filter(w => w.length > 2);
|
|
82
|
+
// Try keyword-based extraction first
|
|
79
83
|
const keywords = [
|
|
80
84
|
`for `, `by `, `about `, `named `, `called `,
|
|
81
85
|
`tag `, `user `, `author `, `slug `, `id `,
|
|
82
|
-
`
|
|
86
|
+
`from `, `with `,
|
|
83
87
|
];
|
|
88
|
+
// For nav params — look for destination after navigation verbs
|
|
89
|
+
const navKeywords = [`to `, `open `, `show `];
|
|
90
|
+
const isNavParam = param.name === 'destination' ||
|
|
91
|
+
param.description.toLowerCase().includes('screen') ||
|
|
92
|
+
param.description.toLowerCase().includes('page');
|
|
93
|
+
const activeKeywords = isNavParam
|
|
94
|
+
? [...navKeywords, ...keywords]
|
|
95
|
+
: keywords;
|
|
84
96
|
let extracted = null;
|
|
85
|
-
for (const kw of
|
|
97
|
+
for (const kw of activeKeywords) {
|
|
86
98
|
const idx = q.indexOf(kw);
|
|
87
99
|
if (idx !== -1) {
|
|
88
100
|
const after = query.slice(idx + kw.length).trim();
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
101
|
+
// Get remaining words, filter stopwords, take first meaningful one
|
|
102
|
+
const tokens = after.split(/\s+/)
|
|
103
|
+
.map(t => t.replace(/[^a-zA-Z0-9-_@.]/g, ''))
|
|
104
|
+
.filter(t => t.length > 1 && !STOPWORDS.has(t.toLowerCase()));
|
|
105
|
+
if (tokens.length > 0) {
|
|
106
|
+
// For IDs and numbers — single token is correct
|
|
107
|
+
const isIdParam = param.name === 'id' ||
|
|
108
|
+
param.name.endsWith('_id') ||
|
|
109
|
+
param.name.endsWith('Id') ||
|
|
110
|
+
/^\s*\w+\s+id\b/i.test(param.description) ||
|
|
111
|
+
/^id\b/i.test(param.description);
|
|
112
|
+
// For names, products, destinations — grab multi-word phrase
|
|
113
|
+
extracted = (isIdParam || isNavParam) ? tokens[0] : tokens.join('-').toLowerCase();
|
|
92
114
|
break;
|
|
93
115
|
}
|
|
94
116
|
}
|
|
@@ -118,33 +140,44 @@ export function match(query, manifest) {
|
|
|
118
140
|
logger.debug(`Manifest has ${manifest.capabilities.length} capabilities`);
|
|
119
141
|
let best = null;
|
|
120
142
|
let bestScore = 0;
|
|
143
|
+
const allScores = [];
|
|
121
144
|
for (const cap of manifest.capabilities) {
|
|
122
145
|
const score = scoreCapability(query, cap);
|
|
123
146
|
logger.debug(` scored "${cap.id}": ${score}%`);
|
|
147
|
+
allScores.push({ cap, score });
|
|
124
148
|
if (score > bestScore) {
|
|
125
149
|
bestScore = score;
|
|
126
150
|
best = cap;
|
|
127
151
|
}
|
|
128
152
|
}
|
|
153
|
+
const candidates = allScores.map(({ cap, score }) => ({
|
|
154
|
+
capabilityId: cap.id,
|
|
155
|
+
score,
|
|
156
|
+
matched: cap.id === best?.id,
|
|
157
|
+
}));
|
|
129
158
|
if (!best || bestScore < 50) {
|
|
130
159
|
logger.info(`No match above threshold (best: ${bestScore}% for "${best?.id ?? 'none'}")`);
|
|
160
|
+
// Out of scope return:
|
|
131
161
|
return {
|
|
132
162
|
capability: null,
|
|
133
163
|
confidence: bestScore,
|
|
134
164
|
intent: 'out_of_scope',
|
|
135
165
|
extractedParams: {},
|
|
136
166
|
reasoning: `No capability matched with sufficient confidence (best score: ${bestScore})`,
|
|
167
|
+
candidates,
|
|
137
168
|
};
|
|
138
169
|
}
|
|
139
170
|
const params = extractParams(query, best);
|
|
140
171
|
logger.info(`Matched "${best.id}" at ${bestScore}% confidence`);
|
|
141
172
|
logger.debug(`Extracted params: ${JSON.stringify(params)}`);
|
|
173
|
+
// Matched return:
|
|
142
174
|
return {
|
|
143
175
|
capability: best,
|
|
144
176
|
confidence: bestScore,
|
|
145
177
|
intent: resolverToIntent(best),
|
|
146
178
|
extractedParams: params,
|
|
147
179
|
reasoning: `Matched "${best.id}" via keyword scoring (score: ${bestScore})`,
|
|
180
|
+
candidates,
|
|
148
181
|
};
|
|
149
182
|
}
|
|
150
183
|
export async function matchWithLLM(query, manifest, options) {
|