capman 0.6.1 → 0.6.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/CODEBASE.md +6 -5
- package/dist/cjs/concurrent.d.ts +53 -0
- package/dist/cjs/concurrent.d.ts.map +1 -0
- package/dist/cjs/concurrent.js +71 -0
- package/dist/cjs/concurrent.js.map +1 -0
- package/dist/cjs/engine.d.ts +82 -12
- package/dist/cjs/engine.d.ts.map +1 -1
- package/dist/cjs/engine.js +159 -37
- package/dist/cjs/engine.js.map +1 -1
- package/dist/cjs/index.d.ts +2 -1
- 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 +14 -6
- package/dist/cjs/learning.d.ts.map +1 -1
- package/dist/cjs/learning.js +64 -10
- package/dist/cjs/learning.js.map +1 -1
- package/dist/cjs/matcher.d.ts +13 -1
- package/dist/cjs/matcher.d.ts.map +1 -1
- package/dist/cjs/matcher.js +67 -10
- package/dist/cjs/matcher.js.map +1 -1
- package/dist/cjs/schema.js +1 -1
- package/dist/cjs/schema.js.map +1 -1
- package/dist/cjs/types.d.ts +9 -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/concurrent.d.ts +52 -0
- package/dist/esm/concurrent.js +66 -0
- package/dist/esm/engine.d.ts +82 -12
- package/dist/esm/engine.js +159 -37
- package/dist/esm/index.d.ts +2 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/learning.d.ts +14 -6
- package/dist/esm/learning.js +64 -10
- package/dist/esm/matcher.d.ts +13 -1
- package/dist/esm/matcher.js +66 -10
- package/dist/esm/schema.js +1 -1
- package/dist/esm/types.d.ts +9 -0
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +1 -1
package/dist/esm/matcher.js
CHANGED
|
@@ -237,6 +237,20 @@ export function extractBigrams(tokens) {
|
|
|
237
237
|
}
|
|
238
238
|
return bigrams;
|
|
239
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Reciprocal Rank Fusion — fuses multiple ranked lists into a single score map.
|
|
242
|
+
* k=60 is the standard literature default.
|
|
243
|
+
*/
|
|
244
|
+
export function rrf(rankings, k = 60) {
|
|
245
|
+
const scores = new Map();
|
|
246
|
+
for (const ranking of rankings) {
|
|
247
|
+
const sorted = [...ranking].sort((a, b) => b.score - a.score);
|
|
248
|
+
sorted.forEach((item, rank) => {
|
|
249
|
+
scores.set(item.id, (scores.get(item.id) ?? 0) + 1 / (k + rank + 1));
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return scores;
|
|
253
|
+
}
|
|
240
254
|
/**
|
|
241
255
|
* Returns a sub-manifest containing only capabilities that match ALL provided tags.
|
|
242
256
|
* Capabilities without tags are excluded when tags filter is active.
|
|
@@ -528,16 +542,57 @@ export function match(query, manifest, options = {}) {
|
|
|
528
542
|
const b = options.bm25B ?? 0.75;
|
|
529
543
|
// Calibrate ceiling — max self-score for normalization
|
|
530
544
|
const ceiling = options.bm25Ceiling ?? calibrateCeiling(manifest.capabilities, bm25Index, k1, b);
|
|
531
|
-
|
|
545
|
+
// Build per-source ranked lists for RRF fusion
|
|
546
|
+
const keywordRanking = [];
|
|
547
|
+
const fuzzyRanking = [];
|
|
548
|
+
const embeddingRanking = [];
|
|
549
|
+
const keywordScoreMap = new Map();
|
|
532
550
|
for (const cap of manifest.capabilities) {
|
|
533
551
|
const rawBM25 = scoreCapability(qWordSet, cap, bm25Index, k1, b);
|
|
534
552
|
const bm25Score = Math.min(100, Math.round((rawBM25 / ceiling) * 100));
|
|
535
553
|
const bonusPoints = bigramBonus(qBigrams, bm25Index.bigrams[cap.id] ?? new Set());
|
|
536
554
|
const keywordScore = Math.min(100, bm25Score + bonusPoints);
|
|
537
555
|
const fuzzyScore = fuzzyScoreMap.get(cap.id) ?? 0;
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
556
|
+
const embScore = options.embeddingScores?.get(cap.id) ?? 0;
|
|
557
|
+
if (keywordScore > 0)
|
|
558
|
+
keywordRanking.push({ id: cap.id, score: keywordScore });
|
|
559
|
+
keywordScoreMap.set(cap.id, keywordScore);
|
|
560
|
+
if (fuzzyScore > 0)
|
|
561
|
+
fuzzyRanking.push({ id: cap.id, score: fuzzyScore });
|
|
562
|
+
if (embScore > 0)
|
|
563
|
+
embeddingRanking.push({ id: cap.id, score: embScore });
|
|
564
|
+
}
|
|
565
|
+
// RRF fusion. Anchor to theoretical max — a rank-1 entry in all lists scores
|
|
566
|
+
// rankings.length/(k+1). Using observed max instead inflates zero-overlap queries
|
|
567
|
+
// (all capabilities rank equally) to 100%, breaking out-of-scope rejection.
|
|
568
|
+
const rrfK = 60;
|
|
569
|
+
const rankings = [
|
|
570
|
+
keywordRanking,
|
|
571
|
+
...(fuzzyRanking.length > 0 ? [fuzzyRanking] : []),
|
|
572
|
+
...(embeddingRanking.length > 0 ? [embeddingRanking] : []),
|
|
573
|
+
];
|
|
574
|
+
const rrfScores = rrf(rankings, rrfK);
|
|
575
|
+
const theoreticalMax = rankings.length / (rrfK + 1);
|
|
576
|
+
// Pre-compute rank maps — rank 0 = best. Used for accurate via attribution.
|
|
577
|
+
const rankIn = (list, id) => {
|
|
578
|
+
const idx = list.findIndex(e => e.id === id);
|
|
579
|
+
return idx === -1 ? Infinity : idx;
|
|
580
|
+
};
|
|
581
|
+
const allScores = [];
|
|
582
|
+
for (const cap of manifest.capabilities) {
|
|
583
|
+
const rrfScore = rrfScores.get(cap.id) ?? 0;
|
|
584
|
+
const score = Math.min(100, Math.round((rrfScore / theoreticalMax) * 100));
|
|
585
|
+
const keywordScore = keywordScoreMap.get(cap.id) ?? 0;
|
|
586
|
+
const fuzzyScore = fuzzyScoreMap.get(cap.id) ?? 0;
|
|
587
|
+
const embScore = options.embeddingScores?.get(cap.id) ?? 0;
|
|
588
|
+
// via = whichever signal ranked this capability highest (lowest rank index).
|
|
589
|
+
// Uses rank position rather than raw score — RRF is rank-based, not score-based.
|
|
590
|
+
const kRank = rankIn(keywordRanking, cap.id);
|
|
591
|
+
const fRank = rankIn(fuzzyRanking, cap.id);
|
|
592
|
+
const eRank = rankIn(embeddingRanking, cap.id);
|
|
593
|
+
const via = eRank < fRank && eRank < kRank ? 'embedding' :
|
|
594
|
+
fRank < kRank ? 'fuzzy' : 'keyword';
|
|
595
|
+
logger.debug(` scored "${cap.id}": ${score}% (keyword: ${keywordScore}%, fuzzy: ${Math.round(fuzzyScore)}%, emb: ${Math.round(embScore)}%, rrf: ${rrfScore.toFixed(4)})`);
|
|
541
596
|
allScores.push({ cap, score, via });
|
|
542
597
|
if (score > bestScore) {
|
|
543
598
|
bestScore = score;
|
|
@@ -567,7 +622,8 @@ export function match(query, manifest, options = {}) {
|
|
|
567
622
|
logger.debug(`Extracted params: ${JSON.stringify(Object.fromEntries(Object.entries(params).map(([k, v]) => [k, v != null ? '[REDACTED]' : 'null'])))}`);
|
|
568
623
|
// Use the via tag tracked during scoring — avoids redundant scoreCapability call.
|
|
569
624
|
const bestEntry = allScores.find(s => s.cap.id === best.id);
|
|
570
|
-
const winner = bestEntry?.via === '
|
|
625
|
+
const winner = bestEntry?.via === 'embedding' ? 'embedding match' :
|
|
626
|
+
bestEntry?.via === 'fuzzy' ? 'fuzzy match' : 'keyword scoring';
|
|
571
627
|
// Matched return:
|
|
572
628
|
return {
|
|
573
629
|
capability: best,
|
|
@@ -589,17 +645,17 @@ export function match(query, manifest, options = {}) {
|
|
|
589
645
|
* wrapper that maps the prompt to a proper system message, keeping user query
|
|
590
646
|
* data in the user turn only.
|
|
591
647
|
*/
|
|
592
|
-
export async function matchWithLLM(query,
|
|
648
|
+
export async function matchWithLLM(query, topCandidates, options) {
|
|
593
649
|
// Truncate description and examples — prevents context window overflow and
|
|
594
650
|
// reduces prompt injection surface from third-party OpenAPI spec content.
|
|
595
651
|
const MAX_DESC_LEN = 200;
|
|
596
652
|
const MAX_EXAMPLE_LEN = 100;
|
|
597
|
-
const manifestSummary =
|
|
653
|
+
const manifestSummary = topCandidates.map(c => `- ${c.id} (${c.resolver.type}): ${sanitizeForPrompt(c.description, MAX_DESC_LEN)}${c.examples?.length
|
|
598
654
|
? `\n examples: ${c.examples.slice(0, 2).map(e => sanitizeForPrompt(e, MAX_EXAMPLE_LEN)).join(', ')}`
|
|
599
655
|
: ''}`).join('\n');
|
|
600
656
|
// Sanitize app name — strip newlines and control characters that could
|
|
601
657
|
// break the prompt structure or inject additional instructions.
|
|
602
|
-
const safeApp = sanitizeForPrompt(
|
|
658
|
+
const safeApp = sanitizeForPrompt(options.app ?? 'the application', 100);
|
|
603
659
|
const prompt = `You are an intent matcher for an AI agent system.
|
|
604
660
|
|
|
605
661
|
App: ${safeApp}
|
|
@@ -641,7 +697,7 @@ ${JSON.stringify({ user_query: query })}
|
|
|
641
697
|
const isOOS = parsed.matched_capability === 'OUT_OF_SCOPE';
|
|
642
698
|
const capability = isOOS
|
|
643
699
|
? null
|
|
644
|
-
:
|
|
700
|
+
: topCandidates.find(c => c.id === parsed.matched_capability) ?? null;
|
|
645
701
|
// If LLM returned an unknown capability ID, treat as out of scope
|
|
646
702
|
const effectivelyOOS = isOOS || capability === null;
|
|
647
703
|
if (!isOOS && capability === null) {
|
|
@@ -657,7 +713,7 @@ ${JSON.stringify({ user_query: query })}
|
|
|
657
713
|
const llmConfidence = effectivelyOOS
|
|
658
714
|
? 0
|
|
659
715
|
: Math.min(100, Math.max(0, Math.round(parsed.confidence)));
|
|
660
|
-
const allCandidates =
|
|
716
|
+
const allCandidates = topCandidates.map(c => ({
|
|
661
717
|
capabilityId: c.id,
|
|
662
718
|
score: c.id === capability?.id ? llmConfidence : 0,
|
|
663
719
|
matched: c.id === capability?.id,
|
package/dist/esm/schema.js
CHANGED
|
@@ -114,7 +114,7 @@ export const CapmanConfigSchema = z.object({
|
|
|
114
114
|
.refine(caps => new Set(caps.map(c => c.id)).size === caps.length, 'capability ids must be unique'),
|
|
115
115
|
}).refine(cfg => {
|
|
116
116
|
const needsBaseUrl = cfg.capabilities.some(c => c.resolver.type === 'api' || c.resolver.type === 'hybrid');
|
|
117
|
-
return !needsBaseUrl || !!cfg.baseUrl;
|
|
117
|
+
return !needsBaseUrl || !!cfg.baseUrl || (cfg.servers?.length ?? 0) > 0;
|
|
118
118
|
}, { message: 'baseUrl is required when any capability uses an api or hybrid resolver' });
|
|
119
119
|
// ─── Manifest Schema ──────────────────────────────────────────────────────────
|
|
120
120
|
export const ManifestSchema = z.object({
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -81,6 +81,15 @@ export interface LifecycleInfo {
|
|
|
81
81
|
/** Human-readable note for consumers */
|
|
82
82
|
note?: string;
|
|
83
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Optional embedding provider for semantic similarity matching.
|
|
86
|
+
* Zero mandatory dependency — only used when passed to EngineOptions.
|
|
87
|
+
* Implement with any model: OpenAI, local ONNX, Transformers.js, etc.
|
|
88
|
+
*/
|
|
89
|
+
export interface EmbeddingProvider {
|
|
90
|
+
/** Encode a batch of texts into fixed-length float vectors. */
|
|
91
|
+
encode(texts: string[]): Promise<number[][]>;
|
|
92
|
+
}
|
|
84
93
|
export interface MatchHint {
|
|
85
94
|
/**
|
|
86
95
|
* Advisory preferred matching mode for this capability.
|
package/dist/esm/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.6.
|
|
1
|
+
export declare const VERSION = "0.6.1";
|
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.6.
|
|
2
|
+
export const VERSION = '0.6.1';
|
package/package.json
CHANGED