@symerian/symi 3.0.18 → 3.0.19
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/dist/build-info.json +3 -3
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/package.json +1 -1
- package/extensions/copilot-proxy/README.md +0 -24
- package/extensions/copilot-proxy/index.ts +0 -154
- package/extensions/copilot-proxy/node_modules/.bin/symi +0 -21
- package/extensions/copilot-proxy/package.json +0 -15
- package/extensions/copilot-proxy/symi.plugin.json +0 -9
- package/extensions/device-pair/index.ts +0 -642
- package/extensions/device-pair/symi.plugin.json +0 -20
- package/extensions/diagnostics-otel/index.ts +0 -15
- package/extensions/diagnostics-otel/node_modules/.bin/acorn +0 -21
- package/extensions/diagnostics-otel/node_modules/.bin/symi +0 -21
- package/extensions/diagnostics-otel/package.json +0 -27
- package/extensions/diagnostics-otel/src/service.test.ts +0 -290
- package/extensions/diagnostics-otel/src/service.ts +0 -666
- package/extensions/diagnostics-otel/symi.plugin.json +0 -8
- package/extensions/google-antigravity-auth/README.md +0 -24
- package/extensions/google-antigravity-auth/index.ts +0 -424
- package/extensions/google-antigravity-auth/node_modules/.bin/symi +0 -21
- package/extensions/google-antigravity-auth/package.json +0 -15
- package/extensions/google-antigravity-auth/symi.plugin.json +0 -9
- package/extensions/google-gemini-cli-auth/README.md +0 -35
- package/extensions/google-gemini-cli-auth/index.ts +0 -75
- package/extensions/google-gemini-cli-auth/node_modules/.bin/symi +0 -21
- package/extensions/google-gemini-cli-auth/oauth.test.ts +0 -162
- package/extensions/google-gemini-cli-auth/oauth.ts +0 -636
- package/extensions/google-gemini-cli-auth/package.json +0 -15
- package/extensions/google-gemini-cli-auth/symi.plugin.json +0 -9
- package/extensions/learning-loop/index.ts +0 -159
- package/extensions/learning-loop/node_modules/.bin/symi +0 -21
- package/extensions/learning-loop/package.json +0 -18
- package/extensions/learning-loop/src/analytics/gateway-methods.ts +0 -230
- package/extensions/learning-loop/src/analytics/metrics-aggregator.ts +0 -153
- package/extensions/learning-loop/src/capture/run-tracker.ts +0 -181
- package/extensions/learning-loop/src/capture/serializer.ts +0 -74
- package/extensions/learning-loop/src/db.ts +0 -583
- package/extensions/learning-loop/src/feedback/explicit-feedback.ts +0 -58
- package/extensions/learning-loop/src/feedback/implicit-signals.ts +0 -89
- package/extensions/learning-loop/src/graph/edge-inference.ts +0 -189
- package/extensions/learning-loop/src/graph/graph-retrieval.ts +0 -144
- package/extensions/learning-loop/src/graph/graph-store.ts +0 -183
- package/extensions/learning-loop/src/hooks.ts +0 -244
- package/extensions/learning-loop/src/injection/cache.ts +0 -73
- package/extensions/learning-loop/src/injection/context-injector.ts +0 -104
- package/extensions/learning-loop/src/injection/prompt-builder.ts +0 -43
- package/extensions/learning-loop/src/learning/embedding-bridge.ts +0 -54
- package/extensions/learning-loop/src/learning/learning-extractor.ts +0 -217
- package/extensions/learning-loop/src/learning/learning-store.ts +0 -158
- package/extensions/learning-loop/src/learning/retrieval.ts +0 -87
- package/extensions/learning-loop/src/math/confidence-intervals.ts +0 -62
- package/extensions/learning-loop/src/math/ewma.ts +0 -51
- package/extensions/learning-loop/src/math/weighted-scorer.ts +0 -42
- package/extensions/learning-loop/src/schema.ts +0 -176
- package/extensions/learning-loop/src/scoring/normalization.ts +0 -32
- package/extensions/learning-loop/src/scoring/quality-engine.ts +0 -78
- package/extensions/learning-loop/src/scoring/signal-extractors.ts +0 -155
- package/extensions/learning-loop/src/test/context-injector.test.ts +0 -142
- package/extensions/learning-loop/src/test/fixes.test.ts +0 -1286
- package/extensions/learning-loop/src/test/graph.test.ts +0 -711
- package/extensions/learning-loop/src/test/integration.test.ts +0 -312
- package/extensions/learning-loop/src/test/learning-store.test.ts +0 -191
- package/extensions/learning-loop/src/test/math.test.ts +0 -148
- package/extensions/learning-loop/src/test/quality-engine.test.ts +0 -231
- package/extensions/learning-loop/src/test/run-tracker.test.ts +0 -143
- package/extensions/learning-loop/src/types.ts +0 -281
- package/extensions/learning-loop/symi.plugin.json +0 -46
- package/extensions/llm-task/README.md +0 -97
- package/extensions/llm-task/index.ts +0 -6
- package/extensions/llm-task/package.json +0 -12
- package/extensions/llm-task/src/llm-task-tool.test.ts +0 -138
- package/extensions/llm-task/src/llm-task-tool.ts +0 -249
- package/extensions/llm-task/symi.plugin.json +0 -21
- package/extensions/memory-lancedb/config.ts +0 -161
- package/extensions/memory-lancedb/index.test.ts +0 -330
- package/extensions/memory-lancedb/index.ts +0 -670
- package/extensions/memory-lancedb/node_modules/.bin/arrow2csv +0 -21
- package/extensions/memory-lancedb/node_modules/.bin/openai +0 -21
- package/extensions/memory-lancedb/node_modules/.bin/symi +0 -21
- package/extensions/memory-lancedb/package.json +0 -20
- package/extensions/memory-lancedb/symi.plugin.json +0 -71
- package/extensions/minimax-portal-auth/README.md +0 -33
- package/extensions/minimax-portal-auth/index.ts +0 -161
- package/extensions/minimax-portal-auth/node_modules/.bin/symi +0 -21
- package/extensions/minimax-portal-auth/oauth.ts +0 -247
- package/extensions/minimax-portal-auth/package.json +0 -15
- package/extensions/minimax-portal-auth/symi.plugin.json +0 -9
- package/extensions/model-equalizer/index.ts +0 -80
- package/extensions/model-equalizer/skills/model-equalizer/SKILL.md +0 -58
- package/extensions/model-equalizer/src/detection.ts +0 -62
- package/extensions/model-equalizer/src/enhancer.ts +0 -63
- package/extensions/model-equalizer/src/test/detection.test.ts +0 -218
- package/extensions/model-equalizer/src/test/enhancer.test.ts +0 -137
- package/extensions/model-equalizer/src/test/integration.test.ts +0 -185
- package/extensions/model-equalizer/src/types.ts +0 -24
- package/extensions/model-equalizer/symi.plugin.json +0 -12
- package/extensions/phone-control/index.ts +0 -421
- package/extensions/phone-control/symi.plugin.json +0 -10
- package/extensions/pipeline/README.md +0 -75
- package/extensions/pipeline/SKILL.md +0 -97
- package/extensions/pipeline/index.ts +0 -18
- package/extensions/pipeline/package.json +0 -11
- package/extensions/pipeline/src/pipeline-tool.test.ts +0 -345
- package/extensions/pipeline/src/pipeline-tool.ts +0 -266
- package/extensions/pipeline/src/windows-spawn.test.ts +0 -148
- package/extensions/pipeline/src/windows-spawn.ts +0 -193
- package/extensions/pipeline/symi.plugin.json +0 -10
- package/extensions/qwen-portal-auth/README.md +0 -24
- package/extensions/qwen-portal-auth/index.ts +0 -134
- package/extensions/qwen-portal-auth/oauth.ts +0 -190
- package/extensions/qwen-portal-auth/symi.plugin.json +0 -9
- package/extensions/talk-voice/index.ts +0 -150
- package/extensions/talk-voice/symi.plugin.json +0 -10
- package/extensions/thread-ownership/index.test.ts +0 -180
- package/extensions/thread-ownership/index.ts +0 -133
- package/extensions/thread-ownership/symi.plugin.json +0 -28
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Implicit feedback detection from user behavior.
|
|
3
|
-
*
|
|
4
|
-
* Analyzes the message_received hook to detect:
|
|
5
|
-
* - Re-ask: next message within 60s with Jaccard similarity > 0.5 => negative signal
|
|
6
|
-
* - Topic change: Jaccard similarity < 0.2 => positive signal (user moved on)
|
|
7
|
-
* - Follow-up continuation: similarity 0.2-0.5 => neutral
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import crypto from "node:crypto";
|
|
11
|
-
import { jaccardSimilarity, tokenize } from "../../../../src/memory/mmr.js";
|
|
12
|
-
import type { DatabaseManager } from "../db.js";
|
|
13
|
-
import type { LearningLoopConfig, FeedbackRecord } from "../types.js";
|
|
14
|
-
|
|
15
|
-
export type ImplicitSignals = ReturnType<typeof createImplicitSignals>;
|
|
16
|
-
|
|
17
|
-
type RecentPrompt = {
|
|
18
|
-
runId: string;
|
|
19
|
-
content: string;
|
|
20
|
-
tokens: Set<string>;
|
|
21
|
-
timestamp: number;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const RE_ASK_WINDOW_MS = 60_000;
|
|
25
|
-
const RE_ASK_SIMILARITY_THRESHOLD = 0.5;
|
|
26
|
-
const TOPIC_CHANGE_THRESHOLD = 0.2;
|
|
27
|
-
|
|
28
|
-
export function createImplicitSignals(params: { db: DatabaseManager; config: LearningLoopConfig }) {
|
|
29
|
-
const { db } = params;
|
|
30
|
-
const recentPrompts: RecentPrompt[] = [];
|
|
31
|
-
const MAX_RECENT = 20;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Record a prompt that was just sent to the LLM, for later comparison.
|
|
35
|
-
*/
|
|
36
|
-
function recordPrompt(runId: string, content: string): void {
|
|
37
|
-
recentPrompts.push({
|
|
38
|
-
runId,
|
|
39
|
-
content,
|
|
40
|
-
tokens: tokenize(content),
|
|
41
|
-
timestamp: Date.now(),
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
// Keep bounded
|
|
45
|
-
while (recentPrompts.length > MAX_RECENT) {
|
|
46
|
-
recentPrompts.shift();
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Analyze an incoming user message against recent prompts.
|
|
52
|
-
* Returns a feedback record if an implicit signal is detected, null otherwise.
|
|
53
|
-
*/
|
|
54
|
-
function analyzeMessage(content: string, timestamp: number): FeedbackRecord | null {
|
|
55
|
-
if (recentPrompts.length === 0) return null;
|
|
56
|
-
|
|
57
|
-
const messageTokens = tokenize(content);
|
|
58
|
-
const lastPrompt = recentPrompts[recentPrompts.length - 1]!;
|
|
59
|
-
const timeDelta = timestamp - lastPrompt.timestamp;
|
|
60
|
-
|
|
61
|
-
const similarity = jaccardSimilarity(messageTokens, lastPrompt.tokens);
|
|
62
|
-
|
|
63
|
-
let score: number | null = null;
|
|
64
|
-
|
|
65
|
-
if (timeDelta <= RE_ASK_WINDOW_MS && similarity > RE_ASK_SIMILARITY_THRESHOLD) {
|
|
66
|
-
// Re-ask: user is repeating themselves => bad signal
|
|
67
|
-
score = 0.0;
|
|
68
|
-
} else if (similarity < TOPIC_CHANGE_THRESHOLD) {
|
|
69
|
-
// Topic change: user moved on => good signal
|
|
70
|
-
score = 1.0;
|
|
71
|
-
} else {
|
|
72
|
-
// Continuation: neutral, no signal
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const feedback: FeedbackRecord = {
|
|
77
|
-
id: `fb_${Date.now()}_${crypto.randomBytes(4).toString("hex")}`,
|
|
78
|
-
runId: lastPrompt.runId,
|
|
79
|
-
source: "implicit",
|
|
80
|
-
score,
|
|
81
|
-
createdAt: timestamp,
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
db.insertFeedback(feedback);
|
|
85
|
-
return feedback;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return { recordPrompt, analyzeMessage };
|
|
89
|
-
}
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Automatic edge creation from learning-loop events.
|
|
3
|
-
* No manual edge management -- edges are inferred from:
|
|
4
|
-
* - Temporal co-occurrence (same run)
|
|
5
|
-
* - Semantic similarity (embedding cosine)
|
|
6
|
-
* - Supersession (same category, higher confidence)
|
|
7
|
-
* - Contradiction (opposing categories with similar content)
|
|
8
|
-
* - Reinforcement (co-applied learnings)
|
|
9
|
-
* - Causation (injected learnings -> extracted learnings)
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { cosineSimilarity } from "../../../../src/memory/internal.js";
|
|
13
|
-
import type { LearningStore } from "../learning/learning-store.js";
|
|
14
|
-
import type { LearningRecord } from "../types.js";
|
|
15
|
-
import type { GraphStore } from "./graph-store.js";
|
|
16
|
-
|
|
17
|
-
export type EdgeInference = ReturnType<typeof createEdgeInference>;
|
|
18
|
-
|
|
19
|
-
const NEGATION_TOKENS = new Set([
|
|
20
|
-
"not",
|
|
21
|
-
"avoid",
|
|
22
|
-
"never",
|
|
23
|
-
"don't",
|
|
24
|
-
"dont",
|
|
25
|
-
"fails",
|
|
26
|
-
"instead",
|
|
27
|
-
"shouldn't",
|
|
28
|
-
"shouldnt",
|
|
29
|
-
"won't",
|
|
30
|
-
"wont",
|
|
31
|
-
"cannot",
|
|
32
|
-
"can't",
|
|
33
|
-
"cant",
|
|
34
|
-
]);
|
|
35
|
-
|
|
36
|
-
function tokenize(text: string): string[] {
|
|
37
|
-
return text
|
|
38
|
-
.toLowerCase()
|
|
39
|
-
.replace(/[^\w\s]/g, " ")
|
|
40
|
-
.split(/\s+/)
|
|
41
|
-
.filter(Boolean);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function jaccardSimilarity(a: Set<string>, b: Set<string>): number {
|
|
45
|
-
let intersection = 0;
|
|
46
|
-
for (const token of a) {
|
|
47
|
-
if (b.has(token)) intersection++;
|
|
48
|
-
}
|
|
49
|
-
const union = a.size + b.size - intersection;
|
|
50
|
-
return union === 0 ? 0 : intersection / union;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function createEdgeInference(params: {
|
|
54
|
-
graphStore: GraphStore;
|
|
55
|
-
learningStore: LearningStore;
|
|
56
|
-
}) {
|
|
57
|
-
const { graphStore, learningStore } = params;
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Called after a new learning is created. Infers T, S, U, X edges.
|
|
61
|
-
*/
|
|
62
|
-
function onLearningCreated(newLearning: LearningRecord, runId: string): void {
|
|
63
|
-
const recentLearnings = learningStore.listLearnings({ limit: 100 });
|
|
64
|
-
|
|
65
|
-
// 1. Temporal edges (T): other learnings from same run
|
|
66
|
-
for (const existing of recentLearnings) {
|
|
67
|
-
if (existing.id === newLearning.id) continue;
|
|
68
|
-
if (existing.runId === runId) {
|
|
69
|
-
graphStore.addEdge(existing.id, newLearning.id, "T", 0.5);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// 2-4 require comparing against recent learnings
|
|
74
|
-
for (const existing of recentLearnings) {
|
|
75
|
-
if (existing.id === newLearning.id) continue;
|
|
76
|
-
|
|
77
|
-
// 2. Semantic edges (S): cosine similarity between embeddings
|
|
78
|
-
if (newLearning.embedding && existing.embedding) {
|
|
79
|
-
const similarity = cosineSimilarity(newLearning.embedding, existing.embedding);
|
|
80
|
-
|
|
81
|
-
if (similarity >= 0.6 && similarity < 0.92) {
|
|
82
|
-
// Canonical ordering for bidirectional edges
|
|
83
|
-
const [a, b] =
|
|
84
|
-
existing.id < newLearning.id
|
|
85
|
-
? [existing.id, newLearning.id]
|
|
86
|
-
: [newLearning.id, existing.id];
|
|
87
|
-
graphStore.addEdge(a, b, "S", similarity);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// 3. Supersession edges (U): same category + high similarity + higher confidence
|
|
91
|
-
if (
|
|
92
|
-
similarity >= 0.8 &&
|
|
93
|
-
existing.category === newLearning.category &&
|
|
94
|
-
newLearning.confidence > existing.confidence
|
|
95
|
-
) {
|
|
96
|
-
graphStore.addEdge(existing.id, newLearning.id, "U", 1.0);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// 4. Contradiction edges (X)
|
|
101
|
-
if (detectContradiction(newLearning, existing)) {
|
|
102
|
-
const [a, b] =
|
|
103
|
-
existing.id < newLearning.id
|
|
104
|
-
? [existing.id, newLearning.id]
|
|
105
|
-
: [newLearning.id, existing.id];
|
|
106
|
-
const weight =
|
|
107
|
-
newLearning.embedding && existing.embedding
|
|
108
|
-
? Math.max(0.5, cosineSimilarity(newLearning.embedding, existing.embedding))
|
|
109
|
-
: 0.7;
|
|
110
|
-
graphStore.addEdge(a, b, "X", weight);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Called after a run is scored. Infers R and C edges.
|
|
117
|
-
* @param runId - The run that was scored
|
|
118
|
-
* @param appliedIds - Learning IDs that were injected into this run
|
|
119
|
-
* @param extractedIds - Learning IDs that were extracted from this run
|
|
120
|
-
*/
|
|
121
|
-
function onRunScored(runId: string, appliedIds: string[], extractedIds: string[]): void {
|
|
122
|
-
// 1. Reinforcement edges (R): between co-applied learnings
|
|
123
|
-
for (let i = 0; i < appliedIds.length; i++) {
|
|
124
|
-
for (let j = i + 1; j < appliedIds.length; j++) {
|
|
125
|
-
const a = appliedIds[i]!;
|
|
126
|
-
const b = appliedIds[j]!;
|
|
127
|
-
const [source, target] = a < b ? [a, b] : [b, a];
|
|
128
|
-
|
|
129
|
-
// Check existing edge weight to increment
|
|
130
|
-
const existing = graphStore
|
|
131
|
-
.getEdges(source, { typeFilter: ["R"] })
|
|
132
|
-
.find((e) => e.sourceId === source && e.targetId === target);
|
|
133
|
-
|
|
134
|
-
if (existing) {
|
|
135
|
-
const newWeight = Math.min(0.8, existing.weight + 0.1);
|
|
136
|
-
graphStore.upsertEdge(source, target, "R", newWeight);
|
|
137
|
-
} else {
|
|
138
|
-
graphStore.addEdge(source, target, "R", 0.3);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// 2. Causal edges (C): from injected learnings to extracted learnings
|
|
144
|
-
for (const injectedId of appliedIds) {
|
|
145
|
-
for (const extractedId of extractedIds) {
|
|
146
|
-
graphStore.addEdge(injectedId, extractedId, "C", 0.6);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Detect contradiction between two learnings via two heuristics:
|
|
153
|
-
* 1. Category opposition (tool_pattern vs anti_pattern) with semantic similarity
|
|
154
|
-
* 2. Negation density with high non-negation token overlap
|
|
155
|
-
*/
|
|
156
|
-
function detectContradiction(a: LearningRecord, b: LearningRecord): boolean {
|
|
157
|
-
// Heuristic 1: Category opposition
|
|
158
|
-
const opposingCategories =
|
|
159
|
-
(a.category === "tool_pattern" && b.category === "anti_pattern") ||
|
|
160
|
-
(a.category === "anti_pattern" && b.category === "tool_pattern");
|
|
161
|
-
|
|
162
|
-
if (opposingCategories && a.embedding && b.embedding) {
|
|
163
|
-
const sim = cosineSimilarity(a.embedding, b.embedding);
|
|
164
|
-
if (sim >= 0.7) return true;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Heuristic 2: Negation density
|
|
168
|
-
const tokensA = tokenize(a.content);
|
|
169
|
-
const tokensB = tokenize(b.content);
|
|
170
|
-
|
|
171
|
-
const negationsA = tokensA.filter((t) => NEGATION_TOKENS.has(t));
|
|
172
|
-
const negationsB = tokensB.filter((t) => NEGATION_TOKENS.has(t));
|
|
173
|
-
|
|
174
|
-
// One has negations, the other doesn't (or significantly fewer)
|
|
175
|
-
const hasNegationAsymmetry =
|
|
176
|
-
(negationsA.length > 0 && negationsB.length === 0) ||
|
|
177
|
-
(negationsB.length > 0 && negationsA.length === 0);
|
|
178
|
-
|
|
179
|
-
if (hasNegationAsymmetry) {
|
|
180
|
-
const nonNegA = new Set(tokensA.filter((t) => !NEGATION_TOKENS.has(t)));
|
|
181
|
-
const nonNegB = new Set(tokensB.filter((t) => !NEGATION_TOKENS.has(t)));
|
|
182
|
-
if (jaccardSimilarity(nonNegA, nonNegB) >= 0.5) return true;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return false;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return { onLearningCreated, onRunScored };
|
|
189
|
-
}
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hop expansion integrated into the retrieval pipeline.
|
|
3
|
-
*
|
|
4
|
-
* Takes seed results from vector/FTS retrieval, expands via graph neighbors,
|
|
5
|
-
* computes expanded scores with hop decay and type multipliers, then merges
|
|
6
|
-
* and re-applies MMR for diversity.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { mmrRerank, type MMRItem } from "../../../../src/memory/mmr.js";
|
|
10
|
-
import type { LearningStore } from "../learning/learning-store.js";
|
|
11
|
-
import type { RetrievalResult } from "../learning/retrieval.js";
|
|
12
|
-
import type { EdgeType, LearningLoopConfig } from "../types.js";
|
|
13
|
-
import type { GraphStore } from "./graph-store.js";
|
|
14
|
-
|
|
15
|
-
export type GraphRetrieval = ReturnType<typeof createGraphRetrieval>;
|
|
16
|
-
|
|
17
|
-
const HOP_DECAY = 0.6;
|
|
18
|
-
const CENTRALITY_HIGH_THRESHOLD = 2.0;
|
|
19
|
-
|
|
20
|
-
const TYPE_MULTIPLIERS: Record<EdgeType, number> = {
|
|
21
|
-
C: 1.0,
|
|
22
|
-
S: 0.8,
|
|
23
|
-
R: 0.7,
|
|
24
|
-
T: 0.5,
|
|
25
|
-
U: 0.3,
|
|
26
|
-
X: 0.0, // Contradictions excluded from expansion
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export function createGraphRetrieval(params: {
|
|
30
|
-
graphStore: GraphStore;
|
|
31
|
-
learningStore: LearningStore;
|
|
32
|
-
config: LearningLoopConfig;
|
|
33
|
-
}) {
|
|
34
|
-
const { graphStore, learningStore } = params;
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Expand seed retrieval results with graph neighbors.
|
|
38
|
-
*
|
|
39
|
-
* 1. Take top-5 seeds
|
|
40
|
-
* 2. 1-hop expansion (2-hop for high-centrality seeds)
|
|
41
|
-
* 3. Compute expanded scores with decay, type multiplier, centrality boost
|
|
42
|
-
* 4. Merge with seeds, dedup, re-apply MMR
|
|
43
|
-
* 5. Annotate contested results (X-edges)
|
|
44
|
-
*/
|
|
45
|
-
function expandWithGraph(seeds: RetrievalResult[], limit: number): RetrievalResult[] {
|
|
46
|
-
if (seeds.length === 0) return seeds;
|
|
47
|
-
|
|
48
|
-
const topSeeds = seeds.slice(0, 5);
|
|
49
|
-
const expanded = new Map<string, RetrievalResult>();
|
|
50
|
-
|
|
51
|
-
// Keep all original seeds
|
|
52
|
-
for (const seed of seeds) {
|
|
53
|
-
expanded.set(seed.learning.id, seed);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Expand each top seed
|
|
57
|
-
for (const seed of topSeeds) {
|
|
58
|
-
const seedId = seed.learning.id;
|
|
59
|
-
const seedCentrality = graphStore.getCentrality(seedId);
|
|
60
|
-
const maxHops = seedCentrality > CENTRALITY_HIGH_THRESHOLD ? 2 : 1;
|
|
61
|
-
|
|
62
|
-
// Check for contradictions and annotate seed
|
|
63
|
-
const contradictions = graphStore.getContradictions(seedId);
|
|
64
|
-
if (contradictions.length > 0) {
|
|
65
|
-
const existing = expanded.get(seedId)!;
|
|
66
|
-
expanded.set(seedId, { ...existing, contested: true });
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Get edges for expansion
|
|
70
|
-
const edges = graphStore.getEdges(seedId);
|
|
71
|
-
|
|
72
|
-
for (const edge of edges) {
|
|
73
|
-
if (TYPE_MULTIPLIERS[edge.edgeType] === 0) continue; // Skip contradictions
|
|
74
|
-
|
|
75
|
-
const neighborId = edge.sourceId === seedId ? edge.targetId : edge.sourceId;
|
|
76
|
-
const neighborLearning = learningStore.getLearning(neighborId);
|
|
77
|
-
if (!neighborLearning) continue;
|
|
78
|
-
|
|
79
|
-
const neighborCentrality = graphStore.getCentrality(neighborId);
|
|
80
|
-
const centralityBoost = 1.0 + 0.1 * Math.log2(1 + neighborCentrality);
|
|
81
|
-
const expandedScore =
|
|
82
|
-
seed.decayedScore * HOP_DECAY * TYPE_MULTIPLIERS[edge.edgeType] * centralityBoost;
|
|
83
|
-
|
|
84
|
-
const existing = expanded.get(neighborId);
|
|
85
|
-
if (!existing || existing.decayedScore < expandedScore) {
|
|
86
|
-
expanded.set(neighborId, {
|
|
87
|
-
learning: neighborLearning,
|
|
88
|
-
score: expandedScore,
|
|
89
|
-
decayedScore: expandedScore,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// 2-hop expansion for high-centrality seeds
|
|
94
|
-
if (maxHops >= 2) {
|
|
95
|
-
const hop2Edges = graphStore.getEdges(neighborId);
|
|
96
|
-
for (const hop2Edge of hop2Edges) {
|
|
97
|
-
if (TYPE_MULTIPLIERS[hop2Edge.edgeType] === 0) continue;
|
|
98
|
-
|
|
99
|
-
const hop2Id = hop2Edge.sourceId === neighborId ? hop2Edge.targetId : hop2Edge.sourceId;
|
|
100
|
-
if (hop2Id === seedId) continue;
|
|
101
|
-
|
|
102
|
-
const hop2Learning = learningStore.getLearning(hop2Id);
|
|
103
|
-
if (!hop2Learning) continue;
|
|
104
|
-
|
|
105
|
-
const hop2Centrality = graphStore.getCentrality(hop2Id);
|
|
106
|
-
const hop2CentralityBoost = 1.0 + 0.1 * Math.log2(1 + hop2Centrality);
|
|
107
|
-
const hop2Score =
|
|
108
|
-
seed.decayedScore *
|
|
109
|
-
HOP_DECAY *
|
|
110
|
-
HOP_DECAY *
|
|
111
|
-
TYPE_MULTIPLIERS[hop2Edge.edgeType] *
|
|
112
|
-
hop2CentralityBoost;
|
|
113
|
-
|
|
114
|
-
const existingHop2 = expanded.get(hop2Id);
|
|
115
|
-
if (!existingHop2 || existingHop2.decayedScore < hop2Score) {
|
|
116
|
-
expanded.set(hop2Id, {
|
|
117
|
-
learning: hop2Learning,
|
|
118
|
-
score: hop2Score,
|
|
119
|
-
decayedScore: hop2Score,
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Convert to array and sort by score
|
|
128
|
-
const allResults = Array.from(expanded.values());
|
|
129
|
-
allResults.sort((a, b) => b.decayedScore - a.decayedScore);
|
|
130
|
-
|
|
131
|
-
// Re-apply MMR reranking
|
|
132
|
-
const mmrItems: (MMRItem & { original: RetrievalResult })[] = allResults.map((r) => ({
|
|
133
|
-
id: r.learning.id,
|
|
134
|
-
score: r.decayedScore,
|
|
135
|
-
content: r.learning.content,
|
|
136
|
-
original: r,
|
|
137
|
-
}));
|
|
138
|
-
|
|
139
|
-
const reranked = mmrRerank(mmrItems, { enabled: true, lambda: 0.7 });
|
|
140
|
-
return reranked.slice(0, limit).map((item) => item.original);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return { expandWithGraph };
|
|
144
|
-
}
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Graph store: edge CRUD + incremental centrality tracking.
|
|
3
|
-
*
|
|
4
|
-
* Centrality model: log-weighted degree centrality.
|
|
5
|
-
* centrality(node) = sum over edges(node): log2(1 + weight)
|
|
6
|
-
*
|
|
7
|
-
* Updated O(1) per edge add via delta. Log dampening prevents
|
|
8
|
-
* high-degree nodes from dominating.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { DatabaseManager } from "../db.js";
|
|
12
|
-
import type { EdgeType, LearningEdge, EdgeRow } from "../types.js";
|
|
13
|
-
|
|
14
|
-
export type GraphStore = ReturnType<typeof createGraphStore>;
|
|
15
|
-
|
|
16
|
-
function rowToEdge(row: EdgeRow): LearningEdge {
|
|
17
|
-
return {
|
|
18
|
-
id: row.id,
|
|
19
|
-
sourceId: row.source_id,
|
|
20
|
-
targetId: row.target_id,
|
|
21
|
-
edgeType: row.edge_type as EdgeType,
|
|
22
|
-
weight: row.weight,
|
|
23
|
-
createdAt: row.created_at,
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function createGraphStore(params: { db: DatabaseManager }) {
|
|
28
|
-
const { db } = params;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Add an edge and incrementally update centrality for both endpoints.
|
|
32
|
-
*/
|
|
33
|
-
function addEdge(source: string, target: string, type: EdgeType, weight: number): void {
|
|
34
|
-
db.insertEdge(source, target, type, weight);
|
|
35
|
-
const delta = Math.log2(1 + weight);
|
|
36
|
-
updateCentralityDelta(source, delta);
|
|
37
|
-
updateCentralityDelta(target, delta);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Upsert an edge (update weight if exists). Adjusts centrality accordingly.
|
|
42
|
-
*/
|
|
43
|
-
function upsertEdge(source: string, target: string, type: EdgeType, newWeight: number): void {
|
|
44
|
-
// Check if edge already exists to compute delta
|
|
45
|
-
const existing = db
|
|
46
|
-
.getEdgesByNode(source)
|
|
47
|
-
.find((e) => e.source_id === source && e.target_id === target && e.edge_type === type);
|
|
48
|
-
|
|
49
|
-
if (existing) {
|
|
50
|
-
const oldDelta = Math.log2(1 + existing.weight);
|
|
51
|
-
const newDelta = Math.log2(1 + newWeight);
|
|
52
|
-
const diff = newDelta - oldDelta;
|
|
53
|
-
db.upsertEdge(source, target, type, newWeight);
|
|
54
|
-
updateCentralityDelta(source, diff);
|
|
55
|
-
updateCentralityDelta(target, diff);
|
|
56
|
-
} else {
|
|
57
|
-
db.upsertEdge(source, target, type, newWeight);
|
|
58
|
-
const delta = Math.log2(1 + newWeight);
|
|
59
|
-
updateCentralityDelta(source, delta);
|
|
60
|
-
updateCentralityDelta(target, delta);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Get all edges for a learning, optionally filtered by type.
|
|
66
|
-
*/
|
|
67
|
-
function getEdges(learningId: string, opts?: { typeFilter?: EdgeType[] }): LearningEdge[] {
|
|
68
|
-
const rows = db.getEdgesByNode(learningId);
|
|
69
|
-
let edges = rows.map(rowToEdge);
|
|
70
|
-
if (opts?.typeFilter && opts.typeFilter.length > 0) {
|
|
71
|
-
const allowed = new Set(opts.typeFilter);
|
|
72
|
-
edges = edges.filter((e) => allowed.has(e.edgeType));
|
|
73
|
-
}
|
|
74
|
-
return edges;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Get N-hop neighbors with accumulated weights.
|
|
79
|
-
* Returns Map<learningId, accumulatedWeight>.
|
|
80
|
-
*/
|
|
81
|
-
function getNeighbors(
|
|
82
|
-
learningId: string,
|
|
83
|
-
hops: number = 1,
|
|
84
|
-
typeFilter?: EdgeType[],
|
|
85
|
-
): Map<string, number> {
|
|
86
|
-
const result = new Map<string, number>();
|
|
87
|
-
const visited = new Set<string>([learningId]);
|
|
88
|
-
let frontier = new Map<string, number>([[learningId, 1.0]]);
|
|
89
|
-
|
|
90
|
-
for (let hop = 0; hop < hops; hop++) {
|
|
91
|
-
const nextFrontier = new Map<string, number>();
|
|
92
|
-
|
|
93
|
-
for (const [nodeId, parentWeight] of frontier) {
|
|
94
|
-
const edges = getEdges(nodeId, { typeFilter });
|
|
95
|
-
for (const edge of edges) {
|
|
96
|
-
const neighborId = edge.sourceId === nodeId ? edge.targetId : edge.sourceId;
|
|
97
|
-
if (visited.has(neighborId)) continue;
|
|
98
|
-
|
|
99
|
-
const weight = parentWeight * edge.weight;
|
|
100
|
-
const existing = nextFrontier.get(neighborId) ?? 0;
|
|
101
|
-
nextFrontier.set(neighborId, Math.max(existing, weight));
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
for (const [nodeId, weight] of nextFrontier) {
|
|
106
|
-
visited.add(nodeId);
|
|
107
|
-
const existing = result.get(nodeId) ?? 0;
|
|
108
|
-
result.set(nodeId, Math.max(existing, weight));
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
frontier = nextFrontier;
|
|
112
|
-
if (frontier.size === 0) break;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return result;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Get centrality score for a learning.
|
|
120
|
-
*/
|
|
121
|
-
function getCentrality(learningId: string): number {
|
|
122
|
-
return db.getCentralityScore(learningId);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Remove all edges for a learning and recompute affected neighbors' centrality.
|
|
127
|
-
*/
|
|
128
|
-
function removeEdgesFor(learningId: string): void {
|
|
129
|
-
const edges = db.getEdgesByNode(learningId);
|
|
130
|
-
|
|
131
|
-
// Collect affected neighbors
|
|
132
|
-
const affected = new Set<string>();
|
|
133
|
-
for (const edge of edges) {
|
|
134
|
-
if (edge.source_id !== learningId) affected.add(edge.source_id);
|
|
135
|
-
if (edge.target_id !== learningId) affected.add(edge.target_id);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Delete all edges
|
|
139
|
-
db.deleteEdgesForLearning(learningId);
|
|
140
|
-
db.deleteCentralityScore(learningId);
|
|
141
|
-
|
|
142
|
-
// Recompute centrality for affected neighbors
|
|
143
|
-
for (const neighborId of affected) {
|
|
144
|
-
recomputeCentrality(neighborId);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Get all contradiction edges, optionally for a specific learning.
|
|
150
|
-
*/
|
|
151
|
-
function getContradictions(learningId?: string): LearningEdge[] {
|
|
152
|
-
if (learningId) {
|
|
153
|
-
return getEdges(learningId, { typeFilter: ["X"] });
|
|
154
|
-
}
|
|
155
|
-
return db.getEdgesByType("X").map(rowToEdge);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// --- Internal helpers ---
|
|
159
|
-
|
|
160
|
-
function updateCentralityDelta(id: string, delta: number): void {
|
|
161
|
-
const current = db.getCentralityScore(id);
|
|
162
|
-
db.upsertCentralityScore(id, current + delta);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function recomputeCentrality(id: string): void {
|
|
166
|
-
const edges = db.getEdgesByNode(id);
|
|
167
|
-
let score = 0;
|
|
168
|
-
for (const edge of edges) {
|
|
169
|
-
score += Math.log2(1 + edge.weight);
|
|
170
|
-
}
|
|
171
|
-
db.upsertCentralityScore(id, score);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return {
|
|
175
|
-
addEdge,
|
|
176
|
-
upsertEdge,
|
|
177
|
-
getEdges,
|
|
178
|
-
getNeighbors,
|
|
179
|
-
getCentrality,
|
|
180
|
-
removeEdgesFor,
|
|
181
|
-
getContradictions,
|
|
182
|
-
};
|
|
183
|
-
}
|