ruvnet-kb-first 5.0.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/LICENSE +21 -0
- package/README.md +674 -0
- package/SKILL.md +740 -0
- package/bin/kb-first.js +123 -0
- package/install/init-project.sh +435 -0
- package/install/install-global.sh +257 -0
- package/install/kb-first-autodetect.sh +108 -0
- package/install/kb-first-command.md +80 -0
- package/install/kb-first-skill.md +262 -0
- package/package.json +87 -0
- package/phases/00-assessment.md +529 -0
- package/phases/01-storage.md +194 -0
- package/phases/01.5-hooks-setup.md +521 -0
- package/phases/02-kb-creation.md +413 -0
- package/phases/03-persistence.md +125 -0
- package/phases/04-visualization.md +170 -0
- package/phases/05-integration.md +114 -0
- package/phases/06-scaffold.md +130 -0
- package/phases/07-build.md +493 -0
- package/phases/08-verification.md +597 -0
- package/phases/09-security.md +512 -0
- package/phases/10-documentation.md +613 -0
- package/phases/11-deployment.md +670 -0
- package/phases/testing.md +713 -0
- package/scripts/1.5-hooks-verify.sh +252 -0
- package/scripts/8.1-code-scan.sh +58 -0
- package/scripts/8.2-import-check.sh +42 -0
- package/scripts/8.3-source-returns.sh +52 -0
- package/scripts/8.4-startup-verify.sh +65 -0
- package/scripts/8.5-fallback-check.sh +63 -0
- package/scripts/8.6-attribution.sh +56 -0
- package/scripts/8.7-confidence.sh +56 -0
- package/scripts/8.8-gap-logging.sh +70 -0
- package/scripts/9-security-audit.sh +202 -0
- package/scripts/init-project.sh +395 -0
- package/scripts/verify-enforcement.sh +167 -0
- package/src/commands/hooks.js +361 -0
- package/src/commands/init.js +315 -0
- package/src/commands/phase.js +372 -0
- package/src/commands/score.js +380 -0
- package/src/commands/status.js +193 -0
- package/src/commands/verify.js +286 -0
- package/src/index.js +56 -0
- package/src/mcp-server.js +412 -0
- package/templates/attention-router.ts +534 -0
- package/templates/code-analysis.ts +683 -0
- package/templates/federated-kb-learner.ts +649 -0
- package/templates/gnn-engine.ts +1091 -0
- package/templates/intentions.md +277 -0
- package/templates/kb-client.ts +905 -0
- package/templates/schema.sql +303 -0
- package/templates/sona-config.ts +312 -0
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KB-First v3.0 - Attention Router Template
|
|
3
|
+
*
|
|
4
|
+
* Intelligent routing and comparison using attention mechanisms.
|
|
5
|
+
* Use this for applications that need to route queries to experts
|
|
6
|
+
* or compare multiple options.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Pool } from 'pg';
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// TYPES
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
export interface Expert {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
embedding: number[];
|
|
20
|
+
handler: (query: string, context?: any) => Promise<any>;
|
|
21
|
+
domains: string[];
|
|
22
|
+
confidence?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RouteResult {
|
|
26
|
+
expert: Expert;
|
|
27
|
+
score: number;
|
|
28
|
+
reasoning: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface CompareResult {
|
|
32
|
+
winner: string;
|
|
33
|
+
scores: Record<string, number>;
|
|
34
|
+
breakdown: Record<string, Record<string, number>>;
|
|
35
|
+
confidence: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type AttentionMechanism =
|
|
39
|
+
| 'moe' // Mixture of Experts routing
|
|
40
|
+
| 'cross' // Cross-attention for comparison
|
|
41
|
+
| 'flash' // Fast processing of large content
|
|
42
|
+
| 'linear' // O(n) for long sequences
|
|
43
|
+
| 'multi_head' // General purpose
|
|
44
|
+
| 'graph' // Graph-aware attention
|
|
45
|
+
| 'hyperbolic'; // Hierarchical attention
|
|
46
|
+
|
|
47
|
+
export interface AttentionConfig {
|
|
48
|
+
mechanism: AttentionMechanism;
|
|
49
|
+
heads?: number;
|
|
50
|
+
blockSize?: number; // For flash attention
|
|
51
|
+
curvature?: number; // For hyperbolic
|
|
52
|
+
topK?: number; // For MoE routing
|
|
53
|
+
temperature?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// =============================================================================
|
|
57
|
+
// ATTENTION ROUTER
|
|
58
|
+
// =============================================================================
|
|
59
|
+
|
|
60
|
+
export class AttentionRouter {
|
|
61
|
+
private pool: Pool;
|
|
62
|
+
private experts: Map<string, Expert>;
|
|
63
|
+
private defaultConfig: AttentionConfig;
|
|
64
|
+
|
|
65
|
+
constructor(config?: { databaseUrl?: string; defaultMechanism?: AttentionMechanism }) {
|
|
66
|
+
if (config?.databaseUrl) {
|
|
67
|
+
this.pool = new Pool({ connectionString: config.databaseUrl });
|
|
68
|
+
}
|
|
69
|
+
this.experts = new Map();
|
|
70
|
+
this.defaultConfig = {
|
|
71
|
+
mechanism: config?.defaultMechanism || 'multi_head',
|
|
72
|
+
heads: 8
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// EXPERT MANAGEMENT
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Register an expert domain
|
|
82
|
+
*/
|
|
83
|
+
registerExpert(expert: Expert): void {
|
|
84
|
+
this.experts.set(expert.id, expert);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Register multiple experts
|
|
89
|
+
*/
|
|
90
|
+
registerExperts(experts: Expert[]): void {
|
|
91
|
+
for (const expert of experts) {
|
|
92
|
+
this.registerExpert(expert);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get all registered experts
|
|
98
|
+
*/
|
|
99
|
+
getExperts(): Expert[] {
|
|
100
|
+
return Array.from(this.experts.values());
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// ROUTING (MoE)
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Route a query to the most appropriate expert(s)
|
|
109
|
+
*/
|
|
110
|
+
async route(
|
|
111
|
+
query: string,
|
|
112
|
+
queryEmbedding: number[],
|
|
113
|
+
options: { topK?: number; threshold?: number } = {}
|
|
114
|
+
): Promise<RouteResult[]> {
|
|
115
|
+
const { topK = 1, threshold = 0.3 } = options;
|
|
116
|
+
|
|
117
|
+
if (this.experts.size === 0) {
|
|
118
|
+
throw new Error('No experts registered');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Calculate similarity to each expert
|
|
122
|
+
const scores: { expert: Expert; score: number }[] = [];
|
|
123
|
+
|
|
124
|
+
for (const expert of this.experts.values()) {
|
|
125
|
+
const score = this.cosineSimilarity(queryEmbedding, expert.embedding);
|
|
126
|
+
if (score >= threshold) {
|
|
127
|
+
scores.push({ expert, score });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Sort by score and take top K
|
|
132
|
+
scores.sort((a, b) => b.score - a.score);
|
|
133
|
+
const topExperts = scores.slice(0, topK);
|
|
134
|
+
|
|
135
|
+
return topExperts.map(({ expert, score }) => ({
|
|
136
|
+
expert,
|
|
137
|
+
score,
|
|
138
|
+
reasoning: `Query matches ${expert.name} domain with ${(score * 100).toFixed(1)}% confidence`
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Route using SQL (if database available)
|
|
144
|
+
*/
|
|
145
|
+
async routeSQL(
|
|
146
|
+
queryEmbedding: number[],
|
|
147
|
+
options: { topK?: number; namespace?: string } = {}
|
|
148
|
+
): Promise<RouteResult[]> {
|
|
149
|
+
const { topK = 1, namespace } = options;
|
|
150
|
+
const client = await this.pool.connect();
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const result = await client.query(`
|
|
154
|
+
SELECT
|
|
155
|
+
id, name, description, domains,
|
|
156
|
+
1 - (embedding <=> $1) as score
|
|
157
|
+
FROM attention_experts
|
|
158
|
+
WHERE ($2::text IS NULL OR namespace = $2)
|
|
159
|
+
ORDER BY embedding <=> $1
|
|
160
|
+
LIMIT $3
|
|
161
|
+
`, [JSON.stringify(queryEmbedding), namespace, topK]);
|
|
162
|
+
|
|
163
|
+
return result.rows.map(row => ({
|
|
164
|
+
expert: {
|
|
165
|
+
id: row.id,
|
|
166
|
+
name: row.name,
|
|
167
|
+
description: row.description,
|
|
168
|
+
embedding: [], // Not returned for efficiency
|
|
169
|
+
handler: this.experts.get(row.id)?.handler || (async () => null),
|
|
170
|
+
domains: row.domains
|
|
171
|
+
},
|
|
172
|
+
score: row.score,
|
|
173
|
+
reasoning: `Routed to ${row.name} with ${(row.score * 100).toFixed(1)}% match`
|
|
174
|
+
}));
|
|
175
|
+
} finally {
|
|
176
|
+
client.release();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// COMPARISON (Cross-Attention)
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Compare two items using cross-attention
|
|
186
|
+
*/
|
|
187
|
+
async compare(
|
|
188
|
+
itemA: { id: string; embedding: number[]; attributes: Record<string, any> },
|
|
189
|
+
itemB: { id: string; embedding: number[]; attributes: Record<string, any> },
|
|
190
|
+
criteria: string[]
|
|
191
|
+
): Promise<CompareResult> {
|
|
192
|
+
const breakdown: Record<string, Record<string, number>> = {};
|
|
193
|
+
const scores: Record<string, number> = { [itemA.id]: 0, [itemB.id]: 0 };
|
|
194
|
+
|
|
195
|
+
for (const criterion of criteria) {
|
|
196
|
+
const valueA = itemA.attributes[criterion];
|
|
197
|
+
const valueB = itemB.attributes[criterion];
|
|
198
|
+
|
|
199
|
+
// Score based on criterion type
|
|
200
|
+
const criterionScores = this.scoreCriterion(criterion, valueA, valueB);
|
|
201
|
+
|
|
202
|
+
breakdown[criterion] = {
|
|
203
|
+
[itemA.id]: criterionScores.a,
|
|
204
|
+
[itemB.id]: criterionScores.b
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
scores[itemA.id] += criterionScores.a;
|
|
208
|
+
scores[itemB.id] += criterionScores.b;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Normalize scores
|
|
212
|
+
const totalCriteria = criteria.length;
|
|
213
|
+
scores[itemA.id] /= totalCriteria;
|
|
214
|
+
scores[itemB.id] /= totalCriteria;
|
|
215
|
+
|
|
216
|
+
// Add embedding similarity bonus
|
|
217
|
+
const embeddingSimilarity = this.cosineSimilarity(itemA.embedding, itemB.embedding);
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
winner: scores[itemA.id] >= scores[itemB.id] ? itemA.id : itemB.id,
|
|
221
|
+
scores,
|
|
222
|
+
breakdown,
|
|
223
|
+
confidence: Math.abs(scores[itemA.id] - scores[itemB.id]) + 0.5
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Compare multiple items and rank them
|
|
229
|
+
*/
|
|
230
|
+
async rankOptions(
|
|
231
|
+
items: { id: string; embedding: number[]; attributes: Record<string, any> }[],
|
|
232
|
+
criteria: string[],
|
|
233
|
+
weights?: Record<string, number>
|
|
234
|
+
): Promise<{ id: string; score: number; rank: number }[]> {
|
|
235
|
+
const defaultWeight = 1 / criteria.length;
|
|
236
|
+
const criteriaWeights = weights ||
|
|
237
|
+
Object.fromEntries(criteria.map(c => [c, defaultWeight]));
|
|
238
|
+
|
|
239
|
+
const scores = items.map(item => {
|
|
240
|
+
let totalScore = 0;
|
|
241
|
+
|
|
242
|
+
for (const criterion of criteria) {
|
|
243
|
+
const value = item.attributes[criterion];
|
|
244
|
+
const normalizedValue = this.normalizeValue(criterion, value, items);
|
|
245
|
+
totalScore += normalizedValue * (criteriaWeights[criterion] || defaultWeight);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return { id: item.id, score: totalScore };
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Sort by score descending
|
|
252
|
+
scores.sort((a, b) => b.score - a.score);
|
|
253
|
+
|
|
254
|
+
// Add rank
|
|
255
|
+
return scores.map((s, index) => ({ ...s, rank: index + 1 }));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// ATTENTION MECHANISMS (SQL)
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Apply multi-head attention via SQL
|
|
264
|
+
*/
|
|
265
|
+
async multiHeadAttention(
|
|
266
|
+
query: number[][],
|
|
267
|
+
key: number[][],
|
|
268
|
+
value: number[][],
|
|
269
|
+
heads: number = 8
|
|
270
|
+
): Promise<number[][]> {
|
|
271
|
+
const client = await this.pool.connect();
|
|
272
|
+
try {
|
|
273
|
+
const result = await client.query(`
|
|
274
|
+
SELECT ruvector_attention_multi_head($1, $2, $3, $4) as attended
|
|
275
|
+
`, [query, key, value, heads]);
|
|
276
|
+
return result.rows[0].attended;
|
|
277
|
+
} finally {
|
|
278
|
+
client.release();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Apply flash attention for efficiency
|
|
284
|
+
*/
|
|
285
|
+
async flashAttention(
|
|
286
|
+
query: number[][],
|
|
287
|
+
key: number[][],
|
|
288
|
+
value: number[][],
|
|
289
|
+
blockSize: number = 256
|
|
290
|
+
): Promise<number[][]> {
|
|
291
|
+
const client = await this.pool.connect();
|
|
292
|
+
try {
|
|
293
|
+
const result = await client.query(`
|
|
294
|
+
SELECT ruvector_attention_flash($1, $2, $3, $4) as attended
|
|
295
|
+
`, [query, key, value, blockSize]);
|
|
296
|
+
return result.rows[0].attended;
|
|
297
|
+
} finally {
|
|
298
|
+
client.release();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Apply linear attention for long sequences
|
|
304
|
+
*/
|
|
305
|
+
async linearAttention(
|
|
306
|
+
query: number[][],
|
|
307
|
+
key: number[][],
|
|
308
|
+
value: number[][]
|
|
309
|
+
): Promise<number[][]> {
|
|
310
|
+
const client = await this.pool.connect();
|
|
311
|
+
try {
|
|
312
|
+
const result = await client.query(`
|
|
313
|
+
SELECT ruvector_attention_linear($1, $2, $3) as attended
|
|
314
|
+
`, [query, key, value]);
|
|
315
|
+
return result.rows[0].attended;
|
|
316
|
+
} finally {
|
|
317
|
+
client.release();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Apply hyperbolic attention for hierarchies
|
|
323
|
+
*/
|
|
324
|
+
async hyperbolicAttention(
|
|
325
|
+
query: number[][],
|
|
326
|
+
key: number[][],
|
|
327
|
+
value: number[][],
|
|
328
|
+
curvature: number = -1.0
|
|
329
|
+
): Promise<number[][]> {
|
|
330
|
+
const client = await this.pool.connect();
|
|
331
|
+
try {
|
|
332
|
+
const result = await client.query(`
|
|
333
|
+
SELECT ruvector_attention_hyperbolic($1, $2, $3, $4) as attended
|
|
334
|
+
`, [query, key, value, curvature]);
|
|
335
|
+
return result.rows[0].attended;
|
|
336
|
+
} finally {
|
|
337
|
+
client.release();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Apply graph attention
|
|
343
|
+
*/
|
|
344
|
+
async graphAttention(
|
|
345
|
+
nodeFeatures: number[][],
|
|
346
|
+
adjacencyMatrix: number[][],
|
|
347
|
+
heads: number = 4
|
|
348
|
+
): Promise<number[][]> {
|
|
349
|
+
const client = await this.pool.connect();
|
|
350
|
+
try {
|
|
351
|
+
const result = await client.query(`
|
|
352
|
+
SELECT ruvector_attention_graph($1, $2, $3) as attended
|
|
353
|
+
`, [nodeFeatures, adjacencyMatrix, heads]);
|
|
354
|
+
return result.rows[0].attended;
|
|
355
|
+
} finally {
|
|
356
|
+
client.release();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
// MECHANISM SELECTION
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Select the appropriate attention mechanism for a task
|
|
366
|
+
*/
|
|
367
|
+
selectMechanism(task: {
|
|
368
|
+
type: 'route' | 'compare' | 'analyze' | 'navigate';
|
|
369
|
+
tokenCount?: number;
|
|
370
|
+
optionCount?: number;
|
|
371
|
+
isHierarchical?: boolean;
|
|
372
|
+
isGraph?: boolean;
|
|
373
|
+
}): AttentionConfig {
|
|
374
|
+
// Routing tasks use MoE
|
|
375
|
+
if (task.type === 'route') {
|
|
376
|
+
return { mechanism: 'moe', topK: 2, temperature: 0.7 };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Comparison tasks use cross-attention
|
|
380
|
+
if (task.type === 'compare') {
|
|
381
|
+
if (task.optionCount && task.optionCount > 10) {
|
|
382
|
+
return { mechanism: 'cross', heads: 4 }; // Fewer heads for many options
|
|
383
|
+
}
|
|
384
|
+
return { mechanism: 'cross', heads: 8 };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Analysis tasks
|
|
388
|
+
if (task.type === 'analyze') {
|
|
389
|
+
// Long sequences need linear or flash
|
|
390
|
+
if (task.tokenCount && task.tokenCount > 8000) {
|
|
391
|
+
return { mechanism: 'linear' };
|
|
392
|
+
}
|
|
393
|
+
if (task.tokenCount && task.tokenCount > 4000) {
|
|
394
|
+
return { mechanism: 'flash', blockSize: 256 };
|
|
395
|
+
}
|
|
396
|
+
return { mechanism: 'multi_head', heads: 8 };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Navigation tasks
|
|
400
|
+
if (task.type === 'navigate') {
|
|
401
|
+
if (task.isHierarchical) {
|
|
402
|
+
return { mechanism: 'hyperbolic', curvature: -1.0 };
|
|
403
|
+
}
|
|
404
|
+
if (task.isGraph) {
|
|
405
|
+
return { mechanism: 'graph', heads: 4 };
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Default
|
|
410
|
+
return { mechanism: 'multi_head', heads: 8 };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ---------------------------------------------------------------------------
|
|
414
|
+
// HELPERS
|
|
415
|
+
// ---------------------------------------------------------------------------
|
|
416
|
+
|
|
417
|
+
private cosineSimilarity(a: number[], b: number[]): number {
|
|
418
|
+
if (a.length !== b.length) return 0;
|
|
419
|
+
|
|
420
|
+
let dotProduct = 0;
|
|
421
|
+
let normA = 0;
|
|
422
|
+
let normB = 0;
|
|
423
|
+
|
|
424
|
+
for (let i = 0; i < a.length; i++) {
|
|
425
|
+
dotProduct += a[i] * b[i];
|
|
426
|
+
normA += a[i] * a[i];
|
|
427
|
+
normB += b[i] * b[i];
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const denominator = Math.sqrt(normA) * Math.sqrt(normB);
|
|
431
|
+
return denominator === 0 ? 0 : dotProduct / denominator;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private scoreCriterion(
|
|
435
|
+
criterion: string,
|
|
436
|
+
valueA: any,
|
|
437
|
+
valueB: any
|
|
438
|
+
): { a: number; b: number } {
|
|
439
|
+
// Numeric comparison (lower is better for cost, higher for value)
|
|
440
|
+
if (typeof valueA === 'number' && typeof valueB === 'number') {
|
|
441
|
+
const lowerIsBetter = criterion.includes('cost') ||
|
|
442
|
+
criterion.includes('price') ||
|
|
443
|
+
criterion.includes('time') ||
|
|
444
|
+
criterion.includes('risk');
|
|
445
|
+
|
|
446
|
+
if (lowerIsBetter) {
|
|
447
|
+
const total = valueA + valueB;
|
|
448
|
+
return {
|
|
449
|
+
a: total === 0 ? 0.5 : valueB / total,
|
|
450
|
+
b: total === 0 ? 0.5 : valueA / total
|
|
451
|
+
};
|
|
452
|
+
} else {
|
|
453
|
+
const total = valueA + valueB;
|
|
454
|
+
return {
|
|
455
|
+
a: total === 0 ? 0.5 : valueA / total,
|
|
456
|
+
b: total === 0 ? 0.5 : valueB / total
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Boolean comparison
|
|
462
|
+
if (typeof valueA === 'boolean' && typeof valueB === 'boolean') {
|
|
463
|
+
return {
|
|
464
|
+
a: valueA ? 1 : 0,
|
|
465
|
+
b: valueB ? 1 : 0
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Default: equal
|
|
470
|
+
return { a: 0.5, b: 0.5 };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private normalizeValue(
|
|
474
|
+
criterion: string,
|
|
475
|
+
value: any,
|
|
476
|
+
allItems: { attributes: Record<string, any> }[]
|
|
477
|
+
): number {
|
|
478
|
+
if (typeof value !== 'number') return 0.5;
|
|
479
|
+
|
|
480
|
+
const allValues = allItems
|
|
481
|
+
.map(i => i.attributes[criterion])
|
|
482
|
+
.filter(v => typeof v === 'number') as number[];
|
|
483
|
+
|
|
484
|
+
const min = Math.min(...allValues);
|
|
485
|
+
const max = Math.max(...allValues);
|
|
486
|
+
|
|
487
|
+
if (max === min) return 0.5;
|
|
488
|
+
|
|
489
|
+
const lowerIsBetter = criterion.includes('cost') ||
|
|
490
|
+
criterion.includes('price') ||
|
|
491
|
+
criterion.includes('time') ||
|
|
492
|
+
criterion.includes('risk');
|
|
493
|
+
|
|
494
|
+
const normalized = (value - min) / (max - min);
|
|
495
|
+
return lowerIsBetter ? 1 - normalized : normalized;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// =============================================================================
|
|
500
|
+
// SCHEMA FOR ATTENTION TABLES
|
|
501
|
+
// =============================================================================
|
|
502
|
+
|
|
503
|
+
export const ATTENTION_SCHEMA = `
|
|
504
|
+
-- Attention Experts
|
|
505
|
+
CREATE TABLE IF NOT EXISTS attention_experts (
|
|
506
|
+
id TEXT PRIMARY KEY,
|
|
507
|
+
namespace TEXT NOT NULL,
|
|
508
|
+
name TEXT NOT NULL,
|
|
509
|
+
description TEXT,
|
|
510
|
+
domains TEXT[] DEFAULT '{}',
|
|
511
|
+
embedding vector(384),
|
|
512
|
+
handler_config JSONB DEFAULT '{}',
|
|
513
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
514
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
-- Routing History (for learning)
|
|
518
|
+
CREATE TABLE IF NOT EXISTS attention_routing_history (
|
|
519
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
520
|
+
query_embedding vector(384),
|
|
521
|
+
routed_expert TEXT REFERENCES attention_experts(id),
|
|
522
|
+
score REAL,
|
|
523
|
+
outcome_success BOOLEAN,
|
|
524
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
-- Indexes
|
|
528
|
+
CREATE INDEX IF NOT EXISTS attention_experts_namespace_idx ON attention_experts(namespace);
|
|
529
|
+
CREATE INDEX IF NOT EXISTS attention_experts_embedding_idx ON attention_experts
|
|
530
|
+
USING hnsw (embedding vector_cosine_ops);
|
|
531
|
+
CREATE INDEX IF NOT EXISTS attention_routing_expert_idx ON attention_routing_history(routed_expert);
|
|
532
|
+
`;
|
|
533
|
+
|
|
534
|
+
export default AttentionRouter;
|