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,649 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KB-First v3.0 - Federated Learning Template
|
|
3
|
+
*
|
|
4
|
+
* Distributed expert knowledge curation using agentic-flow's
|
|
5
|
+
* FederatedLearningCoordinator and EphemeralLearningAgent.
|
|
6
|
+
*
|
|
7
|
+
* Use this for:
|
|
8
|
+
* - Multi-expert knowledge aggregation
|
|
9
|
+
* - Quality-gated KB ingestion
|
|
10
|
+
* - Distributed learning without centralizing data
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// TYPES
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
export interface ExpertAgent {
|
|
18
|
+
id: string;
|
|
19
|
+
domain: string;
|
|
20
|
+
expertise: string[];
|
|
21
|
+
qualityThreshold: number;
|
|
22
|
+
learningRate: number;
|
|
23
|
+
contributionCount: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface KnowledgeContribution {
|
|
27
|
+
agentId: string;
|
|
28
|
+
domain: string;
|
|
29
|
+
title: string;
|
|
30
|
+
content: string;
|
|
31
|
+
sourceUrl: string;
|
|
32
|
+
confidence: number;
|
|
33
|
+
metadata: Record<string, unknown>;
|
|
34
|
+
qualityScore: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface AggregationResult {
|
|
38
|
+
accepted: KnowledgeContribution[];
|
|
39
|
+
rejected: KnowledgeContribution[];
|
|
40
|
+
aggregatedConfidence: number;
|
|
41
|
+
qualityMetrics: {
|
|
42
|
+
avgQuality: number;
|
|
43
|
+
minQuality: number;
|
|
44
|
+
maxQuality: number;
|
|
45
|
+
consensusLevel: number;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface FederatedConfig {
|
|
50
|
+
// Quality gating
|
|
51
|
+
qualityThreshold: number; // Min quality to accept (0.6-0.9)
|
|
52
|
+
consensusRequired: number; // Number of agents that must agree (1-5)
|
|
53
|
+
|
|
54
|
+
// Aggregation strategy
|
|
55
|
+
aggregationStrategy: 'weighted' | 'majority' | 'quality_first' | 'expert_weighted';
|
|
56
|
+
weightByExpertise: boolean;
|
|
57
|
+
|
|
58
|
+
// Scale limits
|
|
59
|
+
maxAgents: number;
|
|
60
|
+
maxContributionsPerRound: number;
|
|
61
|
+
|
|
62
|
+
// Learning parameters
|
|
63
|
+
learningRate: number;
|
|
64
|
+
adaptiveThreshold: boolean; // Adjust threshold based on domain
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// =============================================================================
|
|
68
|
+
// DEFAULT CONFIGURATION
|
|
69
|
+
// =============================================================================
|
|
70
|
+
|
|
71
|
+
const DEFAULT_CONFIG: FederatedConfig = {
|
|
72
|
+
qualityThreshold: 0.7,
|
|
73
|
+
consensusRequired: 2,
|
|
74
|
+
aggregationStrategy: 'quality_first',
|
|
75
|
+
weightByExpertise: true,
|
|
76
|
+
maxAgents: 50,
|
|
77
|
+
maxContributionsPerRound: 100,
|
|
78
|
+
learningRate: 0.1,
|
|
79
|
+
adaptiveThreshold: true
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// =============================================================================
|
|
83
|
+
// FEDERATED KB COORDINATOR
|
|
84
|
+
// =============================================================================
|
|
85
|
+
|
|
86
|
+
export class FederatedKBCoordinator {
|
|
87
|
+
private config: FederatedConfig;
|
|
88
|
+
private agents: Map<string, ExpertAgent>;
|
|
89
|
+
private pendingContributions: KnowledgeContribution[];
|
|
90
|
+
private domainThresholds: Map<string, number>;
|
|
91
|
+
|
|
92
|
+
// Integration with agentic-flow (lazy loaded)
|
|
93
|
+
private agenticCoordinator: any = null;
|
|
94
|
+
|
|
95
|
+
constructor(config: Partial<FederatedConfig> = {}) {
|
|
96
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
97
|
+
this.agents = new Map();
|
|
98
|
+
this.pendingContributions = [];
|
|
99
|
+
this.domainThresholds = new Map();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// INITIALIZATION
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Initialize connection to agentic-flow's FederatedLearningCoordinator
|
|
108
|
+
*/
|
|
109
|
+
async initialize(): Promise<boolean> {
|
|
110
|
+
try {
|
|
111
|
+
const agenticFlow = await import('agentic-flow');
|
|
112
|
+
|
|
113
|
+
if (agenticFlow.FederatedLearningCoordinator) {
|
|
114
|
+
this.agenticCoordinator = new agenticFlow.FederatedLearningCoordinator({
|
|
115
|
+
qualityThreshold: this.config.qualityThreshold,
|
|
116
|
+
aggregationStrategy: this.config.aggregationStrategy,
|
|
117
|
+
maxAgents: this.config.maxAgents
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
console.log('[FEDERATED-KB] Initialized with agentic-flow coordinator');
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log('[FEDERATED-KB] Running in standalone mode');
|
|
125
|
+
return false;
|
|
126
|
+
} catch (e) {
|
|
127
|
+
console.log('[FEDERATED-KB] agentic-flow not available, using standalone mode');
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// AGENT MANAGEMENT
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Register an expert agent for knowledge contribution
|
|
138
|
+
*/
|
|
139
|
+
registerAgent(agent: Omit<ExpertAgent, 'contributionCount'>): ExpertAgent {
|
|
140
|
+
const fullAgent: ExpertAgent = {
|
|
141
|
+
...agent,
|
|
142
|
+
contributionCount: 0
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
this.agents.set(agent.id, fullAgent);
|
|
146
|
+
|
|
147
|
+
// Set domain-specific threshold if adaptive
|
|
148
|
+
if (this.config.adaptiveThreshold && !this.domainThresholds.has(agent.domain)) {
|
|
149
|
+
this.domainThresholds.set(agent.domain, this.config.qualityThreshold);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.log(`[FEDERATED-KB] Registered agent: ${agent.id} (domain: ${agent.domain})`);
|
|
153
|
+
return fullAgent;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Create an ephemeral learning agent for temporary tasks
|
|
158
|
+
*/
|
|
159
|
+
async createEphemeralAgent(
|
|
160
|
+
domain: string,
|
|
161
|
+
task: string,
|
|
162
|
+
options: {
|
|
163
|
+
expertise?: string[];
|
|
164
|
+
qualityThreshold?: number;
|
|
165
|
+
ttlMinutes?: number;
|
|
166
|
+
} = {}
|
|
167
|
+
): Promise<ExpertAgent> {
|
|
168
|
+
const agentId = `ephemeral-${domain}-${Date.now()}`;
|
|
169
|
+
|
|
170
|
+
const agent = this.registerAgent({
|
|
171
|
+
id: agentId,
|
|
172
|
+
domain,
|
|
173
|
+
expertise: options.expertise || [domain],
|
|
174
|
+
qualityThreshold: options.qualityThreshold || this.config.qualityThreshold,
|
|
175
|
+
learningRate: this.config.learningRate
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// If agentic-flow is available, create a real ephemeral agent
|
|
179
|
+
if (this.agenticCoordinator) {
|
|
180
|
+
try {
|
|
181
|
+
const agenticFlow = await import('agentic-flow');
|
|
182
|
+
if (agenticFlow.EphemeralLearningAgent) {
|
|
183
|
+
const ephemeralAgent = new agenticFlow.EphemeralLearningAgent({
|
|
184
|
+
id: agentId,
|
|
185
|
+
domain,
|
|
186
|
+
task,
|
|
187
|
+
ttlMinutes: options.ttlMinutes || 30,
|
|
188
|
+
contributeTo: this.agenticCoordinator
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
console.log(`[FEDERATED-KB] Created ephemeral agent with agentic-flow: ${agentId}`);
|
|
192
|
+
}
|
|
193
|
+
} catch (e) {
|
|
194
|
+
// Standalone mode
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return agent;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get all registered agents
|
|
203
|
+
*/
|
|
204
|
+
getAgents(domain?: string): ExpertAgent[] {
|
|
205
|
+
const agents = [...this.agents.values()];
|
|
206
|
+
return domain ? agents.filter(a => a.domain === domain) : agents;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// CONTRIBUTION SUBMISSION
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Submit a knowledge contribution from an agent
|
|
215
|
+
*/
|
|
216
|
+
async submitContribution(
|
|
217
|
+
agentId: string,
|
|
218
|
+
contribution: Omit<KnowledgeContribution, 'agentId' | 'qualityScore'>
|
|
219
|
+
): Promise<{ accepted: boolean; qualityScore: number; reason?: string }> {
|
|
220
|
+
const agent = this.agents.get(agentId);
|
|
221
|
+
if (!agent) {
|
|
222
|
+
return { accepted: false, qualityScore: 0, reason: 'Agent not registered' };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Calculate quality score
|
|
226
|
+
const qualityScore = this.calculateQualityScore(contribution, agent);
|
|
227
|
+
|
|
228
|
+
const fullContribution: KnowledgeContribution = {
|
|
229
|
+
...contribution,
|
|
230
|
+
agentId,
|
|
231
|
+
qualityScore
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// Check against threshold
|
|
235
|
+
const threshold = this.config.adaptiveThreshold
|
|
236
|
+
? this.domainThresholds.get(contribution.domain) || this.config.qualityThreshold
|
|
237
|
+
: this.config.qualityThreshold;
|
|
238
|
+
|
|
239
|
+
if (qualityScore < threshold) {
|
|
240
|
+
return {
|
|
241
|
+
accepted: false,
|
|
242
|
+
qualityScore,
|
|
243
|
+
reason: `Quality score ${qualityScore.toFixed(2)} below threshold ${threshold}`
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Add to pending contributions
|
|
248
|
+
this.pendingContributions.push(fullContribution);
|
|
249
|
+
agent.contributionCount++;
|
|
250
|
+
|
|
251
|
+
// Auto-aggregate if we have enough contributions
|
|
252
|
+
if (this.pendingContributions.length >= this.config.maxContributionsPerRound) {
|
|
253
|
+
await this.aggregateContributions();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return { accepted: true, qualityScore };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Calculate quality score for a contribution
|
|
261
|
+
*/
|
|
262
|
+
private calculateQualityScore(
|
|
263
|
+
contribution: Omit<KnowledgeContribution, 'agentId' | 'qualityScore'>,
|
|
264
|
+
agent: ExpertAgent
|
|
265
|
+
): number {
|
|
266
|
+
let score = contribution.confidence;
|
|
267
|
+
|
|
268
|
+
// Weight by agent's expertise match
|
|
269
|
+
if (this.config.weightByExpertise) {
|
|
270
|
+
const expertiseMatch = agent.expertise.some(e =>
|
|
271
|
+
contribution.domain.toLowerCase().includes(e.toLowerCase()) ||
|
|
272
|
+
e.toLowerCase().includes(contribution.domain.toLowerCase())
|
|
273
|
+
);
|
|
274
|
+
if (expertiseMatch) {
|
|
275
|
+
score *= 1.2; // 20% boost for expertise match
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Content quality factors
|
|
280
|
+
const contentLength = contribution.content.length;
|
|
281
|
+
if (contentLength > 500 && contentLength < 10000) {
|
|
282
|
+
score *= 1.1; // Boost for good content length
|
|
283
|
+
}
|
|
284
|
+
if (contentLength < 100) {
|
|
285
|
+
score *= 0.7; // Penalty for too short
|
|
286
|
+
}
|
|
287
|
+
if (contentLength > 20000) {
|
|
288
|
+
score *= 0.9; // Slight penalty for too long
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Source URL bonus
|
|
292
|
+
if (contribution.sourceUrl && contribution.sourceUrl.startsWith('https://')) {
|
|
293
|
+
score *= 1.05;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Normalize to 0-1 range
|
|
297
|
+
return Math.min(1, Math.max(0, score));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
// AGGREGATION
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Aggregate pending contributions and ingest to KB
|
|
306
|
+
*/
|
|
307
|
+
async aggregateContributions(): Promise<AggregationResult> {
|
|
308
|
+
const contributions = [...this.pendingContributions];
|
|
309
|
+
this.pendingContributions = [];
|
|
310
|
+
|
|
311
|
+
if (contributions.length === 0) {
|
|
312
|
+
return {
|
|
313
|
+
accepted: [],
|
|
314
|
+
rejected: [],
|
|
315
|
+
aggregatedConfidence: 0,
|
|
316
|
+
qualityMetrics: { avgQuality: 0, minQuality: 0, maxQuality: 0, consensusLevel: 0 }
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Group by domain for consensus checking
|
|
321
|
+
const byDomain = new Map<string, KnowledgeContribution[]>();
|
|
322
|
+
contributions.forEach(c => {
|
|
323
|
+
const list = byDomain.get(c.domain) || [];
|
|
324
|
+
list.push(c);
|
|
325
|
+
byDomain.set(c.domain, list);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const accepted: KnowledgeContribution[] = [];
|
|
329
|
+
const rejected: KnowledgeContribution[] = [];
|
|
330
|
+
|
|
331
|
+
// Apply aggregation strategy
|
|
332
|
+
for (const [domain, domainContributions] of byDomain) {
|
|
333
|
+
const { pass, fail } = this.applyAggregationStrategy(domainContributions);
|
|
334
|
+
accepted.push(...pass);
|
|
335
|
+
rejected.push(...fail);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Calculate quality metrics
|
|
339
|
+
const qualities = accepted.map(c => c.qualityScore);
|
|
340
|
+
const qualityMetrics = {
|
|
341
|
+
avgQuality: qualities.length > 0 ? qualities.reduce((a, b) => a + b, 0) / qualities.length : 0,
|
|
342
|
+
minQuality: qualities.length > 0 ? Math.min(...qualities) : 0,
|
|
343
|
+
maxQuality: qualities.length > 0 ? Math.max(...qualities) : 0,
|
|
344
|
+
consensusLevel: accepted.length / (accepted.length + rejected.length) || 0
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// Update adaptive thresholds based on quality metrics
|
|
348
|
+
if (this.config.adaptiveThreshold) {
|
|
349
|
+
this.updateAdaptiveThresholds(accepted);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Ingest accepted contributions to KB
|
|
353
|
+
if (accepted.length > 0) {
|
|
354
|
+
await this.ingestToKB(accepted);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
console.log(`[FEDERATED-KB] Aggregation complete: ${accepted.length} accepted, ${rejected.length} rejected`);
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
accepted,
|
|
361
|
+
rejected,
|
|
362
|
+
aggregatedConfidence: qualityMetrics.avgQuality,
|
|
363
|
+
qualityMetrics
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Apply the configured aggregation strategy
|
|
369
|
+
*/
|
|
370
|
+
private applyAggregationStrategy(contributions: KnowledgeContribution[]): {
|
|
371
|
+
pass: KnowledgeContribution[];
|
|
372
|
+
fail: KnowledgeContribution[];
|
|
373
|
+
} {
|
|
374
|
+
const pass: KnowledgeContribution[] = [];
|
|
375
|
+
const fail: KnowledgeContribution[] = [];
|
|
376
|
+
|
|
377
|
+
switch (this.config.aggregationStrategy) {
|
|
378
|
+
case 'quality_first':
|
|
379
|
+
// Sort by quality, take top ones above threshold
|
|
380
|
+
contributions
|
|
381
|
+
.sort((a, b) => b.qualityScore - a.qualityScore)
|
|
382
|
+
.forEach(c => {
|
|
383
|
+
if (c.qualityScore >= this.config.qualityThreshold) {
|
|
384
|
+
pass.push(c);
|
|
385
|
+
} else {
|
|
386
|
+
fail.push(c);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
break;
|
|
390
|
+
|
|
391
|
+
case 'weighted':
|
|
392
|
+
// Weight by agent expertise and quality
|
|
393
|
+
contributions.forEach(c => {
|
|
394
|
+
const agent = this.agents.get(c.agentId);
|
|
395
|
+
const expertiseBonus = agent?.expertise.length ? 0.1 * agent.expertise.length : 0;
|
|
396
|
+
const weightedScore = c.qualityScore + expertiseBonus;
|
|
397
|
+
if (weightedScore >= this.config.qualityThreshold) {
|
|
398
|
+
pass.push(c);
|
|
399
|
+
} else {
|
|
400
|
+
fail.push(c);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
break;
|
|
404
|
+
|
|
405
|
+
case 'majority':
|
|
406
|
+
// Group similar contributions and require majority agreement
|
|
407
|
+
const grouped = this.groupSimilarContributions(contributions);
|
|
408
|
+
grouped.forEach(group => {
|
|
409
|
+
if (group.length >= this.config.consensusRequired) {
|
|
410
|
+
// Take the highest quality one from the group
|
|
411
|
+
const best = group.sort((a, b) => b.qualityScore - a.qualityScore)[0];
|
|
412
|
+
pass.push(best);
|
|
413
|
+
group.slice(1).forEach(c => fail.push(c));
|
|
414
|
+
} else {
|
|
415
|
+
group.forEach(c => fail.push(c));
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
break;
|
|
419
|
+
|
|
420
|
+
case 'expert_weighted':
|
|
421
|
+
// Weight heavily by agent contribution history
|
|
422
|
+
contributions.forEach(c => {
|
|
423
|
+
const agent = this.agents.get(c.agentId);
|
|
424
|
+
const contributionBonus = Math.min(0.2, (agent?.contributionCount || 0) * 0.01);
|
|
425
|
+
const weightedScore = c.qualityScore + contributionBonus;
|
|
426
|
+
if (weightedScore >= this.config.qualityThreshold) {
|
|
427
|
+
pass.push(c);
|
|
428
|
+
} else {
|
|
429
|
+
fail.push(c);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return { pass, fail };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Group similar contributions for consensus checking
|
|
440
|
+
*/
|
|
441
|
+
private groupSimilarContributions(contributions: KnowledgeContribution[]): KnowledgeContribution[][] {
|
|
442
|
+
// Simple title-based grouping (could be enhanced with embeddings)
|
|
443
|
+
const groups: KnowledgeContribution[][] = [];
|
|
444
|
+
const used = new Set<number>();
|
|
445
|
+
|
|
446
|
+
contributions.forEach((c, i) => {
|
|
447
|
+
if (used.has(i)) return;
|
|
448
|
+
|
|
449
|
+
const group = [c];
|
|
450
|
+
used.add(i);
|
|
451
|
+
|
|
452
|
+
contributions.forEach((other, j) => {
|
|
453
|
+
if (i === j || used.has(j)) return;
|
|
454
|
+
|
|
455
|
+
// Simple similarity check (could use embeddings for better matching)
|
|
456
|
+
const titleSimilar = this.stringSimilarity(c.title, other.title) > 0.7;
|
|
457
|
+
const domainMatch = c.domain === other.domain;
|
|
458
|
+
|
|
459
|
+
if (titleSimilar && domainMatch) {
|
|
460
|
+
group.push(other);
|
|
461
|
+
used.add(j);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
groups.push(group);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
return groups;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Simple string similarity (Jaccard index on words)
|
|
473
|
+
*/
|
|
474
|
+
private stringSimilarity(a: string, b: string): number {
|
|
475
|
+
const wordsA = new Set(a.toLowerCase().split(/\s+/));
|
|
476
|
+
const wordsB = new Set(b.toLowerCase().split(/\s+/));
|
|
477
|
+
const intersection = new Set([...wordsA].filter(x => wordsB.has(x)));
|
|
478
|
+
const union = new Set([...wordsA, ...wordsB]);
|
|
479
|
+
return intersection.size / union.size;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Update adaptive thresholds based on domain performance
|
|
484
|
+
*/
|
|
485
|
+
private updateAdaptiveThresholds(accepted: KnowledgeContribution[]): void {
|
|
486
|
+
const byDomain = new Map<string, number[]>();
|
|
487
|
+
|
|
488
|
+
accepted.forEach(c => {
|
|
489
|
+
const list = byDomain.get(c.domain) || [];
|
|
490
|
+
list.push(c.qualityScore);
|
|
491
|
+
byDomain.set(c.domain, list);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
byDomain.forEach((scores, domain) => {
|
|
495
|
+
const avg = scores.reduce((a, b) => a + b, 0) / scores.length;
|
|
496
|
+
const currentThreshold = this.domainThresholds.get(domain) || this.config.qualityThreshold;
|
|
497
|
+
|
|
498
|
+
// Slowly adapt threshold toward observed quality
|
|
499
|
+
const newThreshold = currentThreshold + this.config.learningRate * (avg - currentThreshold);
|
|
500
|
+
this.domainThresholds.set(domain, Math.max(0.5, Math.min(0.95, newThreshold)));
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Ingest accepted contributions to the KB
|
|
506
|
+
*/
|
|
507
|
+
private async ingestToKB(contributions: KnowledgeContribution[]): Promise<void> {
|
|
508
|
+
try {
|
|
509
|
+
// Import kb-client dynamically
|
|
510
|
+
const kbClient = await import('./kb-client');
|
|
511
|
+
|
|
512
|
+
for (const contribution of contributions) {
|
|
513
|
+
await kbClient.ingestDocument(
|
|
514
|
+
contribution.domain,
|
|
515
|
+
`federated/${contribution.agentId}/${contribution.title.replace(/[^a-z0-9]/gi, '-').toLowerCase()}`,
|
|
516
|
+
contribution.title,
|
|
517
|
+
contribution.content,
|
|
518
|
+
{
|
|
519
|
+
sourceExpert: `federated:${contribution.agentId}`,
|
|
520
|
+
sourceUrl: contribution.sourceUrl,
|
|
521
|
+
confidence: contribution.qualityScore,
|
|
522
|
+
metadata: {
|
|
523
|
+
...contribution.metadata,
|
|
524
|
+
federatedAgent: contribution.agentId,
|
|
525
|
+
aggregatedAt: new Date().toISOString()
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
console.log(`[FEDERATED-KB] Ingested ${contributions.length} contributions to KB`);
|
|
532
|
+
} catch (e) {
|
|
533
|
+
console.error('[FEDERATED-KB] KB ingestion failed:', e);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// ---------------------------------------------------------------------------
|
|
538
|
+
// STATISTICS & MONITORING
|
|
539
|
+
// ---------------------------------------------------------------------------
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Get coordinator statistics
|
|
543
|
+
*/
|
|
544
|
+
getStats(): {
|
|
545
|
+
totalAgents: number;
|
|
546
|
+
pendingContributions: number;
|
|
547
|
+
domainThresholds: Record<string, number>;
|
|
548
|
+
topContributors: Array<{ id: string; domain: string; contributions: number }>;
|
|
549
|
+
} {
|
|
550
|
+
const agents = [...this.agents.values()];
|
|
551
|
+
|
|
552
|
+
return {
|
|
553
|
+
totalAgents: agents.length,
|
|
554
|
+
pendingContributions: this.pendingContributions.length,
|
|
555
|
+
domainThresholds: Object.fromEntries(this.domainThresholds),
|
|
556
|
+
topContributors: agents
|
|
557
|
+
.sort((a, b) => b.contributionCount - a.contributionCount)
|
|
558
|
+
.slice(0, 10)
|
|
559
|
+
.map(a => ({ id: a.id, domain: a.domain, contributions: a.contributionCount }))
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// =============================================================================
|
|
565
|
+
// PRESET CONFIGURATIONS
|
|
566
|
+
// =============================================================================
|
|
567
|
+
|
|
568
|
+
export const PRESETS = {
|
|
569
|
+
/**
|
|
570
|
+
* Strict quality gating for critical domains (medical, legal, finance)
|
|
571
|
+
*/
|
|
572
|
+
strict: {
|
|
573
|
+
qualityThreshold: 0.85,
|
|
574
|
+
consensusRequired: 3,
|
|
575
|
+
aggregationStrategy: 'majority' as const,
|
|
576
|
+
weightByExpertise: true,
|
|
577
|
+
maxAgents: 20,
|
|
578
|
+
maxContributionsPerRound: 50,
|
|
579
|
+
learningRate: 0.05,
|
|
580
|
+
adaptiveThreshold: false
|
|
581
|
+
},
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Balanced for general knowledge domains
|
|
585
|
+
*/
|
|
586
|
+
balanced: {
|
|
587
|
+
qualityThreshold: 0.7,
|
|
588
|
+
consensusRequired: 2,
|
|
589
|
+
aggregationStrategy: 'quality_first' as const,
|
|
590
|
+
weightByExpertise: true,
|
|
591
|
+
maxAgents: 50,
|
|
592
|
+
maxContributionsPerRound: 100,
|
|
593
|
+
learningRate: 0.1,
|
|
594
|
+
adaptiveThreshold: true
|
|
595
|
+
},
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Permissive for rapidly evolving domains (tech, news)
|
|
599
|
+
*/
|
|
600
|
+
permissive: {
|
|
601
|
+
qualityThreshold: 0.5,
|
|
602
|
+
consensusRequired: 1,
|
|
603
|
+
aggregationStrategy: 'weighted' as const,
|
|
604
|
+
weightByExpertise: false,
|
|
605
|
+
maxAgents: 100,
|
|
606
|
+
maxContributionsPerRound: 200,
|
|
607
|
+
learningRate: 0.2,
|
|
608
|
+
adaptiveThreshold: true
|
|
609
|
+
},
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Expert-focused for specialized domains
|
|
613
|
+
*/
|
|
614
|
+
expertFocused: {
|
|
615
|
+
qualityThreshold: 0.75,
|
|
616
|
+
consensusRequired: 2,
|
|
617
|
+
aggregationStrategy: 'expert_weighted' as const,
|
|
618
|
+
weightByExpertise: true,
|
|
619
|
+
maxAgents: 30,
|
|
620
|
+
maxContributionsPerRound: 75,
|
|
621
|
+
learningRate: 0.1,
|
|
622
|
+
adaptiveThreshold: true
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
// =============================================================================
|
|
627
|
+
// CONVENIENCE FACTORY
|
|
628
|
+
// =============================================================================
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Create a federated coordinator with a preset configuration
|
|
632
|
+
*/
|
|
633
|
+
export function createFederatedCoordinator(
|
|
634
|
+
preset: keyof typeof PRESETS = 'balanced',
|
|
635
|
+
overrides: Partial<FederatedConfig> = {}
|
|
636
|
+
): FederatedKBCoordinator {
|
|
637
|
+
const config = { ...PRESETS[preset], ...overrides };
|
|
638
|
+
return new FederatedKBCoordinator(config);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// =============================================================================
|
|
642
|
+
// EXPORTS
|
|
643
|
+
// =============================================================================
|
|
644
|
+
|
|
645
|
+
export default {
|
|
646
|
+
FederatedKBCoordinator,
|
|
647
|
+
createFederatedCoordinator,
|
|
648
|
+
PRESETS
|
|
649
|
+
};
|