kongbrain 0.1.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 +385 -0
- package/openclaw.plugin.json +66 -0
- package/package.json +65 -0
- package/src/acan.ts +309 -0
- package/src/causal.ts +237 -0
- package/src/cognitive-check.ts +330 -0
- package/src/config.ts +64 -0
- package/src/context-engine.ts +487 -0
- package/src/daemon-manager.ts +148 -0
- package/src/daemon-types.ts +65 -0
- package/src/embeddings.ts +77 -0
- package/src/errors.ts +43 -0
- package/src/graph-context.ts +989 -0
- package/src/hooks/after-tool-call.ts +99 -0
- package/src/hooks/before-prompt-build.ts +44 -0
- package/src/hooks/before-tool-call.ts +86 -0
- package/src/hooks/llm-output.ts +173 -0
- package/src/identity.ts +218 -0
- package/src/index.ts +435 -0
- package/src/intent.ts +190 -0
- package/src/memory-daemon.ts +495 -0
- package/src/orchestrator.ts +348 -0
- package/src/prefetch.ts +200 -0
- package/src/reflection.ts +280 -0
- package/src/retrieval-quality.ts +266 -0
- package/src/schema.surql +387 -0
- package/src/skills.ts +343 -0
- package/src/soul.ts +936 -0
- package/src/state.ts +119 -0
- package/src/surreal.ts +1371 -0
- package/src/tools/core-memory.ts +120 -0
- package/src/tools/introspect.ts +329 -0
- package/src/tools/recall.ts +102 -0
- package/src/wakeup.ts +318 -0
- package/src/workspace-migrate.ts +752 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metacognitive Reflection
|
|
3
|
+
*
|
|
4
|
+
* At session end, reviews own performance: tool failures, runaway detections,
|
|
5
|
+
* low retrieval utilization, wasted tokens. If problems exceeded thresholds,
|
|
6
|
+
* generates a structured reflection via Opus, stored as high-importance memory.
|
|
7
|
+
* Retrieved when similar situations arise in future sessions.
|
|
8
|
+
*
|
|
9
|
+
* Ported from kongbrain — takes SurrealStore/EmbeddingService as params.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { CompleteFn } from "./state.js";
|
|
13
|
+
import type { EmbeddingService } from "./embeddings.js";
|
|
14
|
+
import type { SurrealStore } from "./surreal.js";
|
|
15
|
+
import { swallow } from "./errors.js";
|
|
16
|
+
|
|
17
|
+
// --- Types ---
|
|
18
|
+
|
|
19
|
+
export interface ReflectionMetrics {
|
|
20
|
+
avgUtilization: number;
|
|
21
|
+
toolFailureRate: number;
|
|
22
|
+
steeringCandidates: number;
|
|
23
|
+
wastedTokens: number;
|
|
24
|
+
totalToolCalls: number;
|
|
25
|
+
totalTurns: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface Reflection {
|
|
29
|
+
id: string;
|
|
30
|
+
text: string;
|
|
31
|
+
category: string;
|
|
32
|
+
severity: string;
|
|
33
|
+
importance: number;
|
|
34
|
+
score?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// --- Thresholds ---
|
|
38
|
+
|
|
39
|
+
const UTIL_THRESHOLD = 0.2;
|
|
40
|
+
const TOOL_FAILURE_THRESHOLD = 0.2;
|
|
41
|
+
const STEERING_THRESHOLD = 1;
|
|
42
|
+
|
|
43
|
+
let _reflectionContextWindow = 200000;
|
|
44
|
+
|
|
45
|
+
export function setReflectionContextWindow(cw: number): void {
|
|
46
|
+
_reflectionContextWindow = cw;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getWasteThreshold(): number {
|
|
50
|
+
return Math.round(_reflectionContextWindow * 0.005);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// --- Reflection Generation ---
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Gather session metrics and determine if reflection is warranted.
|
|
57
|
+
*/
|
|
58
|
+
export async function gatherSessionMetrics(
|
|
59
|
+
sessionId: string,
|
|
60
|
+
store: SurrealStore,
|
|
61
|
+
): Promise<ReflectionMetrics | null> {
|
|
62
|
+
if (!store.isAvailable()) return null;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const metricsRows = await store.queryFirst<any>(
|
|
66
|
+
`SELECT
|
|
67
|
+
count() AS totalTurns,
|
|
68
|
+
math::sum(actual_tool_calls) AS totalTools,
|
|
69
|
+
math::sum(steering_candidates) AS totalSteering
|
|
70
|
+
FROM orchestrator_metrics WHERE session_id = $sid GROUP ALL`,
|
|
71
|
+
{ sid: sessionId },
|
|
72
|
+
);
|
|
73
|
+
const metrics = metricsRows[0];
|
|
74
|
+
|
|
75
|
+
const qualityRows = await store.queryFirst<any>(
|
|
76
|
+
`SELECT
|
|
77
|
+
count() AS totalRetrievals,
|
|
78
|
+
math::mean(utilization) AS avgUtil,
|
|
79
|
+
math::sum(context_tokens) AS totalContextTokens,
|
|
80
|
+
math::sum(IF tool_success = false THEN 1 ELSE 0 END) AS toolFailures,
|
|
81
|
+
math::sum(IF utilization < 0.1 THEN context_tokens ELSE 0 END) AS wastedTokens
|
|
82
|
+
FROM retrieval_outcome WHERE session_id = $sid GROUP ALL`,
|
|
83
|
+
{ sid: sessionId },
|
|
84
|
+
);
|
|
85
|
+
const quality = qualityRows[0];
|
|
86
|
+
|
|
87
|
+
const totalTurns = Number(metrics?.totalTurns ?? 0);
|
|
88
|
+
const totalTools = Number(metrics?.totalTools ?? 0);
|
|
89
|
+
const totalSteering = Number(metrics?.totalSteering ?? 0);
|
|
90
|
+
const totalRetrievals = Number(quality?.totalRetrievals ?? 0);
|
|
91
|
+
const avgUtilization = Number(quality?.avgUtil ?? 1);
|
|
92
|
+
const toolFailures = Number(quality?.toolFailures ?? 0);
|
|
93
|
+
const wastedTokens = Number(quality?.wastedTokens ?? 0);
|
|
94
|
+
|
|
95
|
+
const toolFailureRate = totalRetrievals > 0 ? toolFailures / totalRetrievals : 0;
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
avgUtilization,
|
|
99
|
+
toolFailureRate,
|
|
100
|
+
steeringCandidates: totalSteering,
|
|
101
|
+
wastedTokens,
|
|
102
|
+
totalToolCalls: totalTools,
|
|
103
|
+
totalTurns,
|
|
104
|
+
};
|
|
105
|
+
} catch (e) {
|
|
106
|
+
swallow.warn("reflection:gatherMetrics", e);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Determine if session performance warrants a reflection.
|
|
113
|
+
*/
|
|
114
|
+
export function shouldReflect(metrics: ReflectionMetrics): { reflect: boolean; reasons: string[] } {
|
|
115
|
+
const reasons: string[] = [];
|
|
116
|
+
|
|
117
|
+
if (metrics.avgUtilization < UTIL_THRESHOLD && metrics.totalTurns > 1) {
|
|
118
|
+
reasons.push(`Low retrieval utilization: ${(metrics.avgUtilization * 100).toFixed(0)}% (threshold: ${UTIL_THRESHOLD * 100}%)`);
|
|
119
|
+
}
|
|
120
|
+
if (metrics.toolFailureRate > TOOL_FAILURE_THRESHOLD) {
|
|
121
|
+
reasons.push(`High tool failure rate: ${(metrics.toolFailureRate * 100).toFixed(0)}% (threshold: ${TOOL_FAILURE_THRESHOLD * 100}%)`);
|
|
122
|
+
}
|
|
123
|
+
if (metrics.steeringCandidates >= STEERING_THRESHOLD) {
|
|
124
|
+
reasons.push(`${metrics.steeringCandidates} steering candidate(s) detected`);
|
|
125
|
+
}
|
|
126
|
+
if (metrics.wastedTokens > getWasteThreshold()) {
|
|
127
|
+
reasons.push(`~${metrics.wastedTokens} wasted context tokens`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { reflect: reasons.length > 0, reasons };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Generate a structured reflection from session performance data.
|
|
135
|
+
* Only called when shouldReflect() returns true.
|
|
136
|
+
*/
|
|
137
|
+
export async function generateReflection(
|
|
138
|
+
sessionId: string,
|
|
139
|
+
store: SurrealStore,
|
|
140
|
+
embeddings: EmbeddingService,
|
|
141
|
+
complete: CompleteFn,
|
|
142
|
+
): Promise<void> {
|
|
143
|
+
const metrics = await gatherSessionMetrics(sessionId, store);
|
|
144
|
+
if (!metrics) return;
|
|
145
|
+
|
|
146
|
+
const { reflect, reasons } = shouldReflect(metrics);
|
|
147
|
+
if (!reflect) return;
|
|
148
|
+
|
|
149
|
+
const severity = reasons.length >= 3 ? "critical" : reasons.length >= 2 ? "moderate" : "minor";
|
|
150
|
+
|
|
151
|
+
let category = "efficiency";
|
|
152
|
+
if (metrics.toolFailureRate > TOOL_FAILURE_THRESHOLD) category = "failure_pattern";
|
|
153
|
+
if (metrics.steeringCandidates >= STEERING_THRESHOLD) category = "approach_strategy";
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const response = await complete({
|
|
157
|
+
system: `Write 2-4 sentences: root cause, error pattern, what to do differently. Be specific. Example: "Spent 8 tool calls reading source before checking error log. For timeout bugs, check logs first."`,
|
|
158
|
+
messages: [{
|
|
159
|
+
role: "user",
|
|
160
|
+
content: `${metrics.totalTurns} turns, ${metrics.totalToolCalls} tools, ${(metrics.avgUtilization * 100).toFixed(0)}% util, ${(metrics.toolFailureRate * 100).toFixed(0)}% fail, ~${metrics.wastedTokens} wasted tokens\nIssues: ${reasons.join("; ")}`,
|
|
161
|
+
}],
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const reflectionText = response.text.trim();
|
|
165
|
+
|
|
166
|
+
if (reflectionText.length < 20) return;
|
|
167
|
+
|
|
168
|
+
let reflEmb: number[] | null = null;
|
|
169
|
+
if (embeddings.isAvailable()) {
|
|
170
|
+
try { reflEmb = await embeddings.embed(reflectionText); } catch (e) { swallow("reflection:ok", e); }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Dedup: skip if a very similar reflection already exists
|
|
174
|
+
if (reflEmb?.length) {
|
|
175
|
+
const existing = await store.queryFirst<{ id: string; importance: number; score: number }>(
|
|
176
|
+
`SELECT id, importance,
|
|
177
|
+
vector::similarity::cosine(embedding, $vec) AS score
|
|
178
|
+
FROM reflection
|
|
179
|
+
WHERE embedding != NONE AND array::len(embedding) > 0
|
|
180
|
+
ORDER BY score DESC LIMIT 1`,
|
|
181
|
+
{ vec: reflEmb },
|
|
182
|
+
);
|
|
183
|
+
const top = existing[0];
|
|
184
|
+
if (top && (top.score ?? 0) > 0.85) {
|
|
185
|
+
const newImportance = Math.min(10, (top.importance ?? 7) + 0.5);
|
|
186
|
+
await store.queryFirst<any>(
|
|
187
|
+
`UPDATE $id SET importance = $imp, updated_at = time::now()`,
|
|
188
|
+
{ id: top.id, imp: newImportance },
|
|
189
|
+
);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const record: Record<string, unknown> = {
|
|
195
|
+
session_id: sessionId,
|
|
196
|
+
text: reflectionText,
|
|
197
|
+
category,
|
|
198
|
+
severity,
|
|
199
|
+
importance: 7.0,
|
|
200
|
+
};
|
|
201
|
+
if (reflEmb?.length) record.embedding = reflEmb;
|
|
202
|
+
|
|
203
|
+
const rows = await store.queryFirst<{ id: string }>(
|
|
204
|
+
`CREATE reflection CONTENT $record RETURN id`,
|
|
205
|
+
{ record },
|
|
206
|
+
);
|
|
207
|
+
const reflectionId = String(rows[0]?.id ?? "");
|
|
208
|
+
|
|
209
|
+
if (reflectionId) {
|
|
210
|
+
await store.relate(reflectionId, "reflects_on", sessionId).catch(e => swallow.warn("reflection:relate", e));
|
|
211
|
+
}
|
|
212
|
+
} catch (e) {
|
|
213
|
+
swallow("reflection:silent", e);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// --- Reflection Retrieval ---
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Vector search on the reflection table.
|
|
221
|
+
*/
|
|
222
|
+
export async function retrieveReflections(
|
|
223
|
+
queryVec: number[],
|
|
224
|
+
limit = 3,
|
|
225
|
+
store?: SurrealStore,
|
|
226
|
+
): Promise<Reflection[]> {
|
|
227
|
+
if (!store?.isAvailable()) return [];
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const rows = await store.queryFirst<any>(
|
|
231
|
+
`SELECT id, text, category, severity, importance,
|
|
232
|
+
vector::similarity::cosine(embedding, $vec) AS score
|
|
233
|
+
FROM reflection
|
|
234
|
+
WHERE embedding != NONE AND array::len(embedding) > 0
|
|
235
|
+
ORDER BY score DESC LIMIT $lim`,
|
|
236
|
+
{ vec: queryVec, lim: limit },
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
return rows
|
|
240
|
+
.filter((r: any) => (r.score ?? 0) > 0.35)
|
|
241
|
+
.map((r: any) => ({
|
|
242
|
+
id: String(r.id),
|
|
243
|
+
text: r.text ?? "",
|
|
244
|
+
category: r.category ?? "efficiency",
|
|
245
|
+
severity: r.severity ?? "minor",
|
|
246
|
+
importance: Number(r.importance ?? 7.0),
|
|
247
|
+
score: r.score,
|
|
248
|
+
}));
|
|
249
|
+
} catch (e) {
|
|
250
|
+
swallow.warn("reflection:retrieve", e);
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Format reflections as a context block for the LLM.
|
|
257
|
+
*/
|
|
258
|
+
export function formatReflectionContext(reflections: Reflection[]): string {
|
|
259
|
+
if (reflections.length === 0) return "";
|
|
260
|
+
|
|
261
|
+
const lines = reflections.map((r) => {
|
|
262
|
+
return `[reflection/${r.category}] ${r.text}`;
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
return `\n<reflection_context>\n[Lessons from past sessions — avoid repeating these mistakes]\n${lines.join("\n\n")}\n</reflection_context>`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Get reflection count (for /stats display).
|
|
270
|
+
*/
|
|
271
|
+
export async function getReflectionCount(store: SurrealStore): Promise<number> {
|
|
272
|
+
try {
|
|
273
|
+
if (!store.isAvailable()) return 0;
|
|
274
|
+
const rows = await store.queryFirst<{ count: number }>(`SELECT count() AS count FROM reflection GROUP ALL`);
|
|
275
|
+
return Number(rows[0]?.count ?? 0);
|
|
276
|
+
} catch (e) {
|
|
277
|
+
swallow.warn("reflection:count", e);
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retrieval Quality Tracker
|
|
3
|
+
*
|
|
4
|
+
* Measures whether retrieved context was actually useful, not just relevant.
|
|
5
|
+
* Tracks 6 signals from research:
|
|
6
|
+
* 1. Referenced in response (text overlap)
|
|
7
|
+
* 2. Task success (tool executions)
|
|
8
|
+
* 3. Retrieval stability
|
|
9
|
+
* 4. Access patterns
|
|
10
|
+
* 5. Context waste
|
|
11
|
+
* 6. Contradiction detection
|
|
12
|
+
*
|
|
13
|
+
* Ported from kongbrain — uses SurrealStore instead of module-level DB.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { SurrealStore, VectorSearchResult } from "./surreal.js";
|
|
17
|
+
import { swallow } from "./errors.js";
|
|
18
|
+
|
|
19
|
+
export type RetrievedItem = VectorSearchResult & {
|
|
20
|
+
finalScore?: number;
|
|
21
|
+
fromNeighbor?: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
interface QualitySignals {
|
|
25
|
+
utilization: number;
|
|
26
|
+
toolSuccess: boolean | null;
|
|
27
|
+
contextTokens: number;
|
|
28
|
+
wasNeighbor: boolean;
|
|
29
|
+
recency: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Per-turn state — module-level since only one turn is active at a time
|
|
33
|
+
let _pendingRetrieval: {
|
|
34
|
+
sessionId: string;
|
|
35
|
+
items: RetrievedItem[];
|
|
36
|
+
toolResults: { success: boolean }[];
|
|
37
|
+
queryEmbedding?: number[];
|
|
38
|
+
} | null = null;
|
|
39
|
+
|
|
40
|
+
export function getStagedItems(): RetrievedItem[] {
|
|
41
|
+
return _pendingRetrieval?.items ? [..._pendingRetrieval.items] : [];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function stageRetrieval(
|
|
45
|
+
sessionId: string,
|
|
46
|
+
items: RetrievedItem[],
|
|
47
|
+
queryEmbedding?: number[],
|
|
48
|
+
): void {
|
|
49
|
+
_pendingRetrieval = {
|
|
50
|
+
sessionId,
|
|
51
|
+
items,
|
|
52
|
+
toolResults: [],
|
|
53
|
+
queryEmbedding,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function recordToolOutcome(success: boolean): void {
|
|
58
|
+
if (_pendingRetrieval) {
|
|
59
|
+
_pendingRetrieval.toolResults.push({ success });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Evaluate retrieval quality after assistant response.
|
|
65
|
+
*/
|
|
66
|
+
export async function evaluateRetrieval(
|
|
67
|
+
responseTurnId: string,
|
|
68
|
+
responseText: string,
|
|
69
|
+
store: SurrealStore,
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
if (!_pendingRetrieval || _pendingRetrieval.items.length === 0) {
|
|
72
|
+
_pendingRetrieval = null;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const { sessionId, items, toolResults, queryEmbedding } = _pendingRetrieval;
|
|
77
|
+
_pendingRetrieval = null;
|
|
78
|
+
|
|
79
|
+
const toolSuccess = toolResults.length > 0
|
|
80
|
+
? toolResults.every((r) => r.success)
|
|
81
|
+
: null;
|
|
82
|
+
|
|
83
|
+
const responseLower = responseText.toLowerCase();
|
|
84
|
+
|
|
85
|
+
for (const item of items) {
|
|
86
|
+
const signals = computeSignals(item, responseLower, toolSuccess);
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const record: Record<string, unknown> = {
|
|
90
|
+
session_id: sessionId,
|
|
91
|
+
turn_id: responseTurnId,
|
|
92
|
+
memory_id: String(item.id),
|
|
93
|
+
memory_table: item.table,
|
|
94
|
+
retrieval_score: item.finalScore ?? 0,
|
|
95
|
+
utilization: signals.utilization,
|
|
96
|
+
context_tokens: signals.contextTokens,
|
|
97
|
+
was_neighbor: signals.wasNeighbor,
|
|
98
|
+
importance: ((item.importance ?? 5) / 10),
|
|
99
|
+
access_count: Math.min((item.accessCount ?? 0) / 50, 1),
|
|
100
|
+
recency: signals.recency,
|
|
101
|
+
};
|
|
102
|
+
if (signals.toolSuccess != null) {
|
|
103
|
+
record.tool_success = signals.toolSuccess;
|
|
104
|
+
}
|
|
105
|
+
if (queryEmbedding) {
|
|
106
|
+
record.query_embedding = queryEmbedding;
|
|
107
|
+
}
|
|
108
|
+
await store.queryExec(`CREATE retrieval_outcome CONTENT $data`, { data: record });
|
|
109
|
+
store.updateUtilityCache(String(item.id), signals.utilization)
|
|
110
|
+
.catch(e => swallow.warn("retrieval-quality:utilityCache", e));
|
|
111
|
+
} catch {
|
|
112
|
+
// non-critical telemetry
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --- Signal computation ---
|
|
118
|
+
|
|
119
|
+
function computeSignals(
|
|
120
|
+
item: RetrievedItem,
|
|
121
|
+
responseLower: string,
|
|
122
|
+
toolSuccess: boolean | null,
|
|
123
|
+
): QualitySignals {
|
|
124
|
+
const rawText = item.text ?? "";
|
|
125
|
+
const memText = rawText.toLowerCase();
|
|
126
|
+
const contextTokens = Math.ceil(rawText.length / 4);
|
|
127
|
+
|
|
128
|
+
const keyTermScore = keyTermOverlap(rawText, responseLower);
|
|
129
|
+
const trigramScore = trigramOverlap(memText, responseLower);
|
|
130
|
+
const unigramScore = unigramOverlap(memText, responseLower);
|
|
131
|
+
const utilization = Math.max(keyTermScore, trigramScore, unigramScore * 0.5);
|
|
132
|
+
|
|
133
|
+
let recency = 0.5;
|
|
134
|
+
if (item.timestamp) {
|
|
135
|
+
const ageHours = (Date.now() - new Date(item.timestamp).getTime()) / 3_600_000;
|
|
136
|
+
recency = Math.exp(-ageHours / 168);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { utilization, toolSuccess, contextTokens, wasNeighbor: item.fromNeighbor ?? false, recency };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function stripPunctuation(text: string): string {
|
|
143
|
+
return text.replace(/[.,;:!?()"'\[\]{}<>—–…]/g, " ");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const KEY_TERM_PATTERNS = [
|
|
147
|
+
/`([^`]{2,60})`/g,
|
|
148
|
+
/\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)\b/g,
|
|
149
|
+
/\b([A-Z]{2,}(?:[-_][A-Z0-9]+)*)\b/g,
|
|
150
|
+
/\b([A-Z][a-z]*[A-Z]\w*)\b/g,
|
|
151
|
+
/\b([A-Z][a-z]{2,})\b/g,
|
|
152
|
+
/\b(\w+(?:[-_]\w+){1,3})\b/g,
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
const STOP_WORDS = new Set([
|
|
156
|
+
"the", "a", "an", "but", "and", "or", "if", "when", "this", "that",
|
|
157
|
+
"for", "with", "from", "into", "not", "are", "was", "were", "has",
|
|
158
|
+
"have", "been", "its", "can", "will", "may", "also", "just", "then",
|
|
159
|
+
"than", "too", "very", "such", "each", "all", "any", "most", "more",
|
|
160
|
+
"some", "other", "about", "over", "only", "new", "used", "how", "where",
|
|
161
|
+
"what", "which", "who", "whom", "does", "did", "had", "could", "would",
|
|
162
|
+
"should", "shall", "let", "get", "got", "set", "put", "run", "see",
|
|
163
|
+
"try", "use", "one", "two", "now", "way", "own", "same", "here",
|
|
164
|
+
"there", "still", "yet", "both", "few", "many", "much", "well",
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
function extractKeyTerms(text: string): Set<string> {
|
|
168
|
+
const terms = new Set<string>();
|
|
169
|
+
for (const pattern of KEY_TERM_PATTERNS) {
|
|
170
|
+
for (const match of text.matchAll(pattern)) {
|
|
171
|
+
const term = match[1].trim().toLowerCase();
|
|
172
|
+
if (term.length >= 3 && !STOP_WORDS.has(term)) terms.add(term);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return terms;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function keyTermOverlap(source: string, targetLower: string): number {
|
|
179
|
+
const terms = extractKeyTerms(source);
|
|
180
|
+
if (terms.size === 0) return 0;
|
|
181
|
+
const cleanTarget = stripPunctuation(targetLower);
|
|
182
|
+
let found = 0;
|
|
183
|
+
for (const term of terms) { if (cleanTarget.includes(term)) found++; }
|
|
184
|
+
return found / terms.size;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function trigramOverlap(source: string, target: string): number {
|
|
188
|
+
const srcGrams = extractNgrams(stripPunctuation(source));
|
|
189
|
+
if (srcGrams.size === 0) return 0;
|
|
190
|
+
const tgtGrams = extractNgrams(stripPunctuation(target));
|
|
191
|
+
let matches = 0;
|
|
192
|
+
for (const gram of srcGrams) { if (tgtGrams.has(gram)) matches++; }
|
|
193
|
+
return matches / srcGrams.size;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function extractNgrams(text: string): Set<string> {
|
|
197
|
+
const words = text.split(/\s+/).filter((w) => w.length > 2);
|
|
198
|
+
const grams = new Set<string>();
|
|
199
|
+
if (words.length >= 3) {
|
|
200
|
+
for (let i = 0; i <= words.length - 3; i++) {
|
|
201
|
+
grams.add(`${words[i]} ${words[i + 1]} ${words[i + 2]}`);
|
|
202
|
+
}
|
|
203
|
+
} else if (words.length === 2) {
|
|
204
|
+
grams.add(`${words[0]} ${words[1]}`);
|
|
205
|
+
} else if (words.length === 1) {
|
|
206
|
+
grams.add(words[0]);
|
|
207
|
+
}
|
|
208
|
+
return grams;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function unigramOverlap(source: string, target: string): number {
|
|
212
|
+
const srcWords = new Set(
|
|
213
|
+
stripPunctuation(source).split(/\s+/)
|
|
214
|
+
.filter((w) => w.length >= 5 && !STOP_WORDS.has(w)),
|
|
215
|
+
);
|
|
216
|
+
if (srcWords.size === 0) return 0;
|
|
217
|
+
const cleanTarget = " " + stripPunctuation(target) + " ";
|
|
218
|
+
let found = 0;
|
|
219
|
+
for (const word of srcWords) {
|
|
220
|
+
if (cleanTarget.includes(` ${word} `) || cleanTarget.includes(` ${word}s `)) found++;
|
|
221
|
+
}
|
|
222
|
+
return found / srcWords.size;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// --- Historical utility queries ---
|
|
226
|
+
|
|
227
|
+
export async function getHistoricalUtilityBatch(
|
|
228
|
+
ids: string[],
|
|
229
|
+
store?: SurrealStore,
|
|
230
|
+
): Promise<Map<string, number>> {
|
|
231
|
+
const result = new Map<string, number>();
|
|
232
|
+
if (ids.length === 0 || !store) return result;
|
|
233
|
+
try {
|
|
234
|
+
const flat = await store.queryFirst<{ memory_id: string; avg: number }>(
|
|
235
|
+
`SELECT memory_id,
|
|
236
|
+
math::mean(IF llm_relevance != NONE THEN llm_relevance ELSE utilization END) AS avg
|
|
237
|
+
FROM retrieval_outcome
|
|
238
|
+
WHERE memory_id IN $ids AND (utilization > 0 OR llm_relevance != NONE)
|
|
239
|
+
GROUP BY memory_id`,
|
|
240
|
+
{ ids },
|
|
241
|
+
);
|
|
242
|
+
for (const row of flat) {
|
|
243
|
+
if (row.avg != null) result.set(String(row.memory_id), row.avg);
|
|
244
|
+
}
|
|
245
|
+
} catch (e) {
|
|
246
|
+
swallow("retrieval-quality:batch", e);
|
|
247
|
+
}
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function getRecentUtilizationAvg(
|
|
252
|
+
sessionId: string,
|
|
253
|
+
windowSize = 10,
|
|
254
|
+
store?: SurrealStore,
|
|
255
|
+
): Promise<number | null> {
|
|
256
|
+
if (!store) return null;
|
|
257
|
+
try {
|
|
258
|
+
const rows = await store.queryFirst<{ avg: number }>(
|
|
259
|
+
`SELECT math::mean(utilization) AS avg FROM (SELECT utilization, created_at FROM retrieval_outcome WHERE session_id = $sid ORDER BY created_at DESC LIMIT $lim)`,
|
|
260
|
+
{ sid: sessionId, lim: windowSize },
|
|
261
|
+
);
|
|
262
|
+
return rows[0]?.avg ?? null;
|
|
263
|
+
} catch {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
}
|