@shadowforge0/aquifer-memory 1.5.9 → 1.5.12
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/README.md +20 -2
- package/consumers/cli.js +34 -0
- package/consumers/mcp.js +38 -5
- package/consumers/miranda/context-inject.js +1 -0
- package/consumers/openclaw-plugin.js +43 -3
- package/consumers/shared/config.js +20 -0
- package/consumers/shared/factory.js +1 -0
- package/consumers/shared/recall-format.js +26 -0
- package/core/aquifer.js +12 -0
- package/core/insights.js +210 -58
- package/core/mcp-manifest.js +18 -1
- package/core/storage.js +71 -0
- package/package.json +10 -2
- package/scripts/backfill-canonical-key.js +250 -0
- package/scripts/queries.json +0 -45
- package/scripts/retro-recall-bench.js +0 -409
- package/scripts/sample-bench-queries.sql +0 -75
package/README.md
CHANGED
|
@@ -134,14 +134,32 @@ Need LLM summarization, the knowledge graph, OpenAI embeddings, or the reranker?
|
|
|
134
134
|
| `AQUIFER_AGENT_ID` | No | Default agent ID | `main` |
|
|
135
135
|
| `AQUIFER_MIGRATIONS_MODE` | No | Startup handshake mode: `apply` (default), `check`, `off` | `apply` |
|
|
136
136
|
| `AQUIFER_MIGRATION_LOCK_TIMEOUT_MS` | No | Advisory-lock wait before `AQ_MIGRATION_LOCK_TIMEOUT` (default 30000) | `30000` |
|
|
137
|
+
| `AQUIFER_INSIGHTS_DEDUP_MODE` | No | Insights semantic dedup mode: `off` (default), `shadow`, `enforce` — env wins over code for this field only, so operators can kill-switch without redeploy | `shadow` |
|
|
138
|
+
| `AQUIFER_INSIGHTS_DEDUP_COSINE` | No | Cosine threshold for semantic merge (default `0.88`; warn outside `[0.75, 0.95]`) | `0.90` |
|
|
139
|
+
| `AQUIFER_INSIGHTS_DEDUP_CLOSE_BAND_FROM` | No | Lower bound for close-band logging (`dedupNear`); must be below threshold (default `0.85`) | `0.82` |
|
|
137
140
|
|
|
138
141
|
Full env-to-config mapping is in [consumers/shared/config.js](consumers/shared/config.js).
|
|
139
142
|
|
|
143
|
+
### Insights semantic dedup (1.5.10)
|
|
144
|
+
|
|
145
|
+
When a cron extractor (`scripts/extract-insights-from-recent-sessions.js`) or any other caller writes insights via `commitInsight`, the canonical-key layer (1.5.3+) dedupes rows whose `canonicalClaim + entities` hash to the same value. But LLMs don't always produce the same `canonicalClaim` across runs, so 1.5.10 adds a second tier: `title + body` are embedded, matched against `(tenant, agent, type)`-scoped active rows, and a top cosine above `AQUIFER_INSIGHTS_DEDUP_COSINE` triggers supersede (enforce) or metadata-only would-merge logging (shadow). Close-band hits (`closeBandFrom ≤ cos < threshold`) write `metadata.dedupNear` without supersede so operators can tune thresholds without committing.
|
|
146
|
+
|
|
147
|
+
Recommended rollout: `shadow` for one weekly cycle, inspect `SELECT metadata->>'shadowMatch' FROM insights WHERE metadata ? 'shadowMatch'`, then flip to `enforce`. Kill-switch: `AQUIFER_INSIGHTS_DEDUP_MODE=off` and restart.
|
|
148
|
+
|
|
149
|
+
Pre-1.5.3 rows with `canonical_key_v2 IS NULL` are caught by the semantic tier but skip the canonical path; a startup warn points at the one-shot backfill:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
DATABASE_URL=... \
|
|
153
|
+
node scripts/backfill-canonical-key.js --schema <schema> --agent <id>
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The script is idempotent (`WHERE canonical_key_v2 IS NULL` guard) and race-safe with live writers.
|
|
157
|
+
|
|
140
158
|
---
|
|
141
159
|
|
|
142
160
|
## Host Integration
|
|
143
161
|
|
|
144
|
-
MCP is the primary integration surface. Agent hosts connect to the Aquifer MCP server, which exposes
|
|
162
|
+
MCP is the primary integration surface. Agent hosts connect to the Aquifer MCP server, which exposes six tools: `session_recall`, `session_feedback`, `feedback_stats`, `session_bootstrap`, `memory_stats`, `memory_pending`.
|
|
145
163
|
|
|
146
164
|
| Integration | Route | Status | When to use |
|
|
147
165
|
|-------------|-------|--------|-------------|
|
|
@@ -196,7 +214,7 @@ Add to `openclaw.json` under `mcp.servers`:
|
|
|
196
214
|
}
|
|
197
215
|
```
|
|
198
216
|
|
|
199
|
-
Tools materialize as `aquifer__session_recall`, `aquifer__session_feedback`, `aquifer__session_bootstrap`, `aquifer__memory_stats`, `aquifer__memory_pending` (server name prefix added by the host).
|
|
217
|
+
Tools materialize as `aquifer__session_recall`, `aquifer__session_feedback`, `aquifer__feedback_stats`, `aquifer__session_bootstrap`, `aquifer__memory_stats`, `aquifer__memory_pending` (server name prefix added by the host).
|
|
200
218
|
|
|
201
219
|
The OpenClaw plugin (`consumers/openclaw-plugin.js`) is retained for session capture via `before_reset` but is **not** the recommended tool delivery path. Use MCP.
|
|
202
220
|
|
package/consumers/cli.js
CHANGED
|
@@ -99,6 +99,7 @@ async function cmdRecall(aquifer, args) {
|
|
|
99
99
|
return;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
const showExplain = !!args.flags.explain;
|
|
102
103
|
for (let i = 0; i < results.length; i++) {
|
|
103
104
|
const r = results[i];
|
|
104
105
|
const ss = r.structuredSummary || {};
|
|
@@ -107,6 +108,18 @@ async function cmdRecall(aquifer, args) {
|
|
|
107
108
|
console.log(`${i + 1}. [${r.score?.toFixed(3)}] ${title} (${date}, ${r.agentId})`);
|
|
108
109
|
if (ss.overview) console.log(` ${ss.overview.slice(0, 200)}`);
|
|
109
110
|
if (r.matchedTurnText) console.log(` > ${r.matchedTurnText.slice(0, 150)}`);
|
|
111
|
+
if (showExplain && r._debug) {
|
|
112
|
+
const d = r._debug;
|
|
113
|
+
const f = (v) => typeof v === 'number' ? v.toFixed(3) : '?';
|
|
114
|
+
const parts = [
|
|
115
|
+
`rrf=${f(d.rrf)}`, `td=${f(d.timeDecay)}`, `access=${f(d.access)}`,
|
|
116
|
+
`entity=${f(d.entityScore)}`, `trust=${f(d.trustScore)}(\u00d7${f(d.trustMultiplier)})`,
|
|
117
|
+
`ol=${f(d.openLoopBoost)}`, `\u2192 hybrid=${f(d.hybridScore)}`,
|
|
118
|
+
];
|
|
119
|
+
if (d.rerankApplied) parts.push(`rerank=${f(d.rerankScore)}(${d.rerankReason || '?'})`);
|
|
120
|
+
else parts.push(`[rerank: off (${d.rerankReason || '?'})]`);
|
|
121
|
+
console.log(` ${parts.join(' ')}`);
|
|
122
|
+
}
|
|
110
123
|
console.log();
|
|
111
124
|
}
|
|
112
125
|
}
|
|
@@ -133,6 +146,22 @@ async function cmdFeedback(aquifer, args) {
|
|
|
133
146
|
}
|
|
134
147
|
}
|
|
135
148
|
|
|
149
|
+
async function cmdFeedbackStats(aquifer, args) {
|
|
150
|
+
const stats = await aquifer.feedbackStats({
|
|
151
|
+
agentId: args.flags['agent-id'] || undefined,
|
|
152
|
+
dateFrom: args.flags['date-from'] || undefined,
|
|
153
|
+
dateTo: args.flags['date-to'] || undefined,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (args.flags.json) {
|
|
157
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
158
|
+
} else {
|
|
159
|
+
console.log(`Feedback: ${stats.totalFeedback} total (${stats.helpfulCount} helpful, ${stats.unhelpfulCount} unhelpful)`);
|
|
160
|
+
console.log(`Coverage: ${stats.feedbackSessions}/${stats.totalSessions} sessions rated`);
|
|
161
|
+
console.log(`Trust score: avg=${stats.trustScoreAvg} min=${stats.trustScoreMin} max=${stats.trustScoreMax}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
136
165
|
async function cmdBackfill(aquifer, args) {
|
|
137
166
|
const limit = parsePositiveInt(args.flags.limit, 100);
|
|
138
167
|
const dryRun = !!args.flags['dry-run'];
|
|
@@ -318,6 +347,7 @@ Commands:
|
|
|
318
347
|
migrate Run database migrations
|
|
319
348
|
recall <query> Search sessions (requires embed config)
|
|
320
349
|
feedback Record trust feedback on a session
|
|
350
|
+
feedback-stats Show trust feedback statistics and coverage
|
|
321
351
|
backfill Enrich pending sessions
|
|
322
352
|
stats Show database statistics
|
|
323
353
|
export Export sessions as JSONL
|
|
@@ -336,6 +366,7 @@ Options:
|
|
|
336
366
|
--session-id ID Session ID (feedback)
|
|
337
367
|
--verdict helpful|unhelpful Feedback verdict (feedback)
|
|
338
368
|
--note TEXT Feedback note (feedback)
|
|
369
|
+
--explain Show score breakdown per result (recall)
|
|
339
370
|
--json JSON output
|
|
340
371
|
--dry-run Preview only (backfill)
|
|
341
372
|
--output PATH Output file (export)
|
|
@@ -410,6 +441,9 @@ Options:
|
|
|
410
441
|
case 'feedback':
|
|
411
442
|
await cmdFeedback(aquifer, args);
|
|
412
443
|
break;
|
|
444
|
+
case 'feedback-stats':
|
|
445
|
+
await cmdFeedbackStats(aquifer, args);
|
|
446
|
+
break;
|
|
413
447
|
case 'backfill':
|
|
414
448
|
await cmdBackfill(aquifer, args);
|
|
415
449
|
break;
|
package/consumers/mcp.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* This is the primary integration surface for Aquifer. Agent hosts (Claude Code,
|
|
8
8
|
* Codex, OpenCode, etc.) should integrate through this MCP server.
|
|
9
9
|
*
|
|
10
|
-
* Tools: session_recall, session_feedback,
|
|
10
|
+
* Tools: session_recall, session_feedback, feedback_stats,
|
|
11
|
+
* session_bootstrap, memory_stats, memory_pending
|
|
11
12
|
*
|
|
12
13
|
* Usage:
|
|
13
14
|
* npx aquifer mcp
|
|
@@ -32,8 +33,8 @@ function getAquifer() {
|
|
|
32
33
|
|
|
33
34
|
const { formatRecallResults } = require('./shared/recall-format');
|
|
34
35
|
|
|
35
|
-
function formatResults(results, query) {
|
|
36
|
-
return formatRecallResults(results, { query, showScore: true });
|
|
36
|
+
function formatResults(results, query, explain) {
|
|
37
|
+
return formatRecallResults(results, { query, showScore: true, showExplain: !!explain });
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
// ---------------------------------------------------------------------------
|
|
@@ -74,6 +75,7 @@ async function main() {
|
|
|
74
75
|
entities: z.array(z.string()).optional().describe('Entity names to match'),
|
|
75
76
|
entityMode: z.enum(['any', 'all']).optional().describe('"any" (default, boost) or "all" (only sessions with every entity)'),
|
|
76
77
|
mode: z.enum(['fts', 'hybrid', 'vector']).optional().describe('Recall mode: "fts" (keyword only, no embed needed), "hybrid" (default, FTS + vector), "vector" (vector only)'),
|
|
78
|
+
explain: z.boolean().optional().describe('Include per-result score breakdown (diagnostic)'),
|
|
77
79
|
},
|
|
78
80
|
async (params) => {
|
|
79
81
|
try {
|
|
@@ -93,7 +95,7 @@ async function main() {
|
|
|
93
95
|
if (params.mode) recallOpts.mode = params.mode;
|
|
94
96
|
|
|
95
97
|
const results = await aquifer.recall(params.query, recallOpts);
|
|
96
|
-
const text = formatResults(results, params.query);
|
|
98
|
+
const text = formatResults(results, params.query, params.explain);
|
|
97
99
|
return { content: [{ type: 'text', text }] };
|
|
98
100
|
} catch (err) {
|
|
99
101
|
return {
|
|
@@ -106,7 +108,7 @@ async function main() {
|
|
|
106
108
|
|
|
107
109
|
server.tool(
|
|
108
110
|
'session_feedback',
|
|
109
|
-
'
|
|
111
|
+
'After using session_recall, mark the result helpful if it directly informed your answer, or unhelpful if it was irrelevant/outdated. Include a short note. Sessions with more helpful feedback rank higher in future recalls.',
|
|
110
112
|
{
|
|
111
113
|
sessionId: z.string().min(1).describe('Session ID to give feedback on'),
|
|
112
114
|
verdict: z.enum(['helpful', 'unhelpful']).describe('Was the recalled session useful?'),
|
|
@@ -133,6 +135,37 @@ async function main() {
|
|
|
133
135
|
}
|
|
134
136
|
);
|
|
135
137
|
|
|
138
|
+
server.tool(
|
|
139
|
+
'feedback_stats',
|
|
140
|
+
'Return trust feedback statistics: total feedback count, helpful/unhelpful breakdown, trust score distribution, and coverage.',
|
|
141
|
+
{
|
|
142
|
+
agentId: z.string().optional().describe('Filter by agent ID'),
|
|
143
|
+
dateFrom: z.string().optional().describe('Start date YYYY-MM-DD'),
|
|
144
|
+
dateTo: z.string().optional().describe('End date YYYY-MM-DD'),
|
|
145
|
+
},
|
|
146
|
+
async (params) => {
|
|
147
|
+
try {
|
|
148
|
+
const aquifer = getAquifer();
|
|
149
|
+
const stats = await aquifer.feedbackStats({
|
|
150
|
+
agentId: params.agentId || undefined,
|
|
151
|
+
dateFrom: params.dateFrom || undefined,
|
|
152
|
+
dateTo: params.dateTo || undefined,
|
|
153
|
+
});
|
|
154
|
+
const lines = [
|
|
155
|
+
`Feedback: ${stats.totalFeedback} total (${stats.helpfulCount} helpful, ${stats.unhelpfulCount} unhelpful)`,
|
|
156
|
+
`Coverage: ${stats.feedbackSessions}/${stats.totalSessions} sessions rated`,
|
|
157
|
+
`Trust score: avg=${stats.trustScoreAvg} min=${stats.trustScoreMin} max=${stats.trustScoreMax}`,
|
|
158
|
+
];
|
|
159
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
160
|
+
} catch (err) {
|
|
161
|
+
return {
|
|
162
|
+
content: [{ type: 'text', text: `feedback_stats error: ${err.message}` }],
|
|
163
|
+
isError: true,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
|
|
136
169
|
server.tool(
|
|
137
170
|
'memory_stats',
|
|
138
171
|
'Return storage statistics for the Aquifer memory store (session counts by status, summaries, turn embeddings, entities, date range).',
|
|
@@ -14,6 +14,7 @@ function buildSessionContext({ today, agentId, focusText, todoItems, moodLine, h
|
|
|
14
14
|
const parts = [];
|
|
15
15
|
parts.push('你是 Miranda。以下是你已經知道的現況,直接用來回應,不需要讀檔或搜尋。像做 briefing——帶現況也帶判斷和建議。用散文段落,最後一句必須是結論或建議,不能是問句。若草稿有 bullet、標題、表格或問句收尾,改寫再送出。');
|
|
16
16
|
parts.push('回答任何關於過去做過什麼、討論過什麼、決策過什麼的問題時,第一步用 session_recall MCP tool 查,不要用 grep、讀 log、翻檔案。工具在手上就用。');
|
|
17
|
+
parts.push('用完 session_recall 後,如果某筆結果實際幫助了你的回答,呼叫 session_feedback(sessionId, verdict="helpful");如果結果明顯過時或錯誤,呼叫 session_feedback(sessionId, verdict="unhelpful"),帶簡短 note 說明原因。只對實際影響回答的結果回饋,不要每次 recall 都打分。');
|
|
17
18
|
|
|
18
19
|
if (focusText) parts.push(`現在的焦點是${focusText}。`);
|
|
19
20
|
if (handoffText) parts.push(`上一段的交接:${handoffText}`);
|
|
@@ -214,6 +214,7 @@ function register(api) {
|
|
|
214
214
|
entities: { type: 'array', items: { type: 'string' }, description: 'Entity names to match' },
|
|
215
215
|
entityMode: { type: 'string', enum: ['any', 'all'], description: '"any" (default, boost) or "all" (only sessions with every entity)' },
|
|
216
216
|
mode: { type: 'string', enum: ['fts', 'hybrid', 'vector'], description: 'Recall mode: "fts" (keyword only), "hybrid" (default), "vector" (vector only)' },
|
|
217
|
+
explain: { type: 'boolean', description: 'Include per-result score breakdown (diagnostic)' },
|
|
217
218
|
},
|
|
218
219
|
required: ['query'],
|
|
219
220
|
},
|
|
@@ -234,7 +235,7 @@ function register(api) {
|
|
|
234
235
|
if (params.mode) recallOpts.mode = params.mode;
|
|
235
236
|
|
|
236
237
|
const results = await aquifer.recall(params.query, recallOpts);
|
|
237
|
-
const text = formatRecallResults(results);
|
|
238
|
+
const text = formatRecallResults(results, { showScore: true, showExplain: !!params.explain });
|
|
238
239
|
return { content: [{ type: 'text', text }] };
|
|
239
240
|
} catch (err) {
|
|
240
241
|
return {
|
|
@@ -253,7 +254,7 @@ function register(api) {
|
|
|
253
254
|
|
|
254
255
|
return {
|
|
255
256
|
name: 'session_feedback',
|
|
256
|
-
description: '
|
|
257
|
+
description: 'After using session_recall, mark the result helpful if it directly informed your answer, or unhelpful if it was irrelevant/outdated. Include a short note. Sessions with more helpful feedback rank higher in future recalls.',
|
|
257
258
|
parameters: {
|
|
258
259
|
type: 'object',
|
|
259
260
|
properties: {
|
|
@@ -285,5 +286,44 @@ function register(api) {
|
|
|
285
286
|
};
|
|
286
287
|
}, { name: 'session_feedback' });
|
|
287
288
|
|
|
288
|
-
|
|
289
|
+
// --- feedback_stats tool ---
|
|
290
|
+
|
|
291
|
+
api.registerTool((ctx) => {
|
|
292
|
+
if ((ctx?.sessionKey || '').includes('subagent')) return null;
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
name: 'feedback_stats',
|
|
296
|
+
description: 'Return trust feedback statistics: total feedback count, helpful/unhelpful breakdown, trust score distribution, and coverage.',
|
|
297
|
+
parameters: {
|
|
298
|
+
type: 'object',
|
|
299
|
+
properties: {
|
|
300
|
+
agentId: { type: 'string', description: 'Filter by agent ID' },
|
|
301
|
+
dateFrom: { type: 'string', description: 'Start date YYYY-MM-DD' },
|
|
302
|
+
dateTo: { type: 'string', description: 'End date YYYY-MM-DD' },
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
async execute(_toolCallId, params) {
|
|
306
|
+
try {
|
|
307
|
+
const stats = await aquifer.feedbackStats({
|
|
308
|
+
agentId: params.agentId || undefined,
|
|
309
|
+
dateFrom: params.dateFrom || undefined,
|
|
310
|
+
dateTo: params.dateTo || undefined,
|
|
311
|
+
});
|
|
312
|
+
const lines = [
|
|
313
|
+
`Feedback: ${stats.totalFeedback} total (${stats.helpfulCount} helpful, ${stats.unhelpfulCount} unhelpful)`,
|
|
314
|
+
`Coverage: ${stats.feedbackSessions}/${stats.totalSessions} sessions rated`,
|
|
315
|
+
`Trust score: avg=${stats.trustScoreAvg} min=${stats.trustScoreMin} max=${stats.trustScoreMax}`,
|
|
316
|
+
];
|
|
317
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
318
|
+
} catch (err) {
|
|
319
|
+
return {
|
|
320
|
+
content: [{ type: 'text', text: `feedback_stats error: ${err.message}` }],
|
|
321
|
+
isError: true,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
}, { name: 'feedback_stats' });
|
|
327
|
+
|
|
328
|
+
api.logger.info('[aquifer-memory] registered (before_reset + session_recall + session_feedback + feedback_stats)');
|
|
289
329
|
}
|
|
@@ -30,6 +30,15 @@ const DEFAULTS = {
|
|
|
30
30
|
temperature: 0,
|
|
31
31
|
},
|
|
32
32
|
entities: { enabled: false, mergeCall: true, scope: 'default' },
|
|
33
|
+
insights: {
|
|
34
|
+
recallWeights: null,
|
|
35
|
+
recencyWindowDays: null,
|
|
36
|
+
dedup: {
|
|
37
|
+
mode: 'off',
|
|
38
|
+
cosineThreshold: 0.88,
|
|
39
|
+
closeBandFrom: 0.85,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
33
42
|
rank: { rrf: 0.65, timeDecay: 0.25, access: 0.10, entityBoost: 0.18 },
|
|
34
43
|
rerank: {
|
|
35
44
|
enabled: false,
|
|
@@ -75,6 +84,9 @@ const ENV_MAP = [
|
|
|
75
84
|
['AQUIFER_LLM_TEMPERATURE', 'llm.temperature', Number],
|
|
76
85
|
['AQUIFER_ENTITIES_ENABLED', 'entities.enabled', Boolean],
|
|
77
86
|
['AQUIFER_ENTITY_SCOPE', 'entities.scope'],
|
|
87
|
+
['AQUIFER_INSIGHTS_DEDUP_MODE', 'insights.dedup.mode'],
|
|
88
|
+
['AQUIFER_INSIGHTS_DEDUP_COSINE', 'insights.dedup.cosineThreshold', Number],
|
|
89
|
+
['AQUIFER_INSIGHTS_DEDUP_CLOSE_BAND_FROM', 'insights.dedup.closeBandFrom', Number],
|
|
78
90
|
['AQUIFER_RERANK_ENABLED', 'rerank.enabled', Boolean],
|
|
79
91
|
['AQUIFER_RERANK_PROVIDER', 'rerank.provider'],
|
|
80
92
|
['AQUIFER_RERANK_BASE_URL', 'rerank.baseUrl'],
|
|
@@ -165,6 +177,14 @@ function loadConfig(opts = {}) {
|
|
|
165
177
|
config = deepMerge(config, opts.overrides);
|
|
166
178
|
}
|
|
167
179
|
|
|
180
|
+
// insights.dedup shorthand: true → enforce, false → off
|
|
181
|
+
if (config.insights && typeof config.insights.dedup === 'boolean') {
|
|
182
|
+
config.insights.dedup = {
|
|
183
|
+
...DEFAULTS.insights.dedup,
|
|
184
|
+
mode: config.insights.dedup ? 'enforce' : 'off',
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
168
188
|
return config;
|
|
169
189
|
}
|
|
170
190
|
|
|
@@ -66,6 +66,30 @@ const defaultRenderers = {
|
|
|
66
66
|
if (!showScore) return null;
|
|
67
67
|
return `Score: ${typeof result.score === 'number' ? result.score.toFixed(3) : '?'}`;
|
|
68
68
|
},
|
|
69
|
+
explain(result, { showExplain }) {
|
|
70
|
+
if (!showExplain) return null;
|
|
71
|
+
const d = result._debug;
|
|
72
|
+
if (!d) return null;
|
|
73
|
+
const f = (v) => typeof v === 'number' ? v.toFixed(3) : '?';
|
|
74
|
+
const parts = [
|
|
75
|
+
`rrf=${f(d.rrf)}`,
|
|
76
|
+
`td=${f(d.timeDecay)}`,
|
|
77
|
+
`access=${f(d.access)}`,
|
|
78
|
+
`entity=${f(d.entityScore)}`,
|
|
79
|
+
`trust=${f(d.trustScore)}(\u00d7${f(d.trustMultiplier)})`,
|
|
80
|
+
`ol=${f(d.openLoopBoost)}`,
|
|
81
|
+
`\u2192 hybrid=${f(d.hybridScore)}`,
|
|
82
|
+
];
|
|
83
|
+
if (d.rerankApplied) {
|
|
84
|
+
parts.push(`rerank=${f(d.rerankScore)}(${d.rerankReason || '?'})`);
|
|
85
|
+
} else {
|
|
86
|
+
parts.push(`[rerank: off (${d.rerankReason || '?'})]`);
|
|
87
|
+
}
|
|
88
|
+
if (Array.isArray(d.searchErrors) && d.searchErrors.length > 0) {
|
|
89
|
+
parts.push(`errors: ${d.searchErrors.map(e => (e && e.path) || '?').join(',')}`);
|
|
90
|
+
}
|
|
91
|
+
return ` ${parts.join(' ')}`;
|
|
92
|
+
},
|
|
69
93
|
separator() {
|
|
70
94
|
return '';
|
|
71
95
|
},
|
|
@@ -102,6 +126,8 @@ function createRecallFormatter(overrides = {}) {
|
|
|
102
126
|
if (matched) lines.push(matched);
|
|
103
127
|
const score = r.score(res, { showScore: !!opts.showScore, ...ctx });
|
|
104
128
|
if (score) lines.push(score);
|
|
129
|
+
const explain = r.explain(res, { showExplain: !!opts.showExplain, ...ctx });
|
|
130
|
+
if (explain) lines.push(explain);
|
|
105
131
|
const sep = r.separator(i, ctx);
|
|
106
132
|
if (sep !== null && sep !== undefined) lines.push(sep);
|
|
107
133
|
}
|
package/core/aquifer.js
CHANGED
|
@@ -1558,6 +1558,17 @@ function createAquifer(config = {}) {
|
|
|
1558
1558
|
});
|
|
1559
1559
|
},
|
|
1560
1560
|
|
|
1561
|
+
async feedbackStats(opts = {}) {
|
|
1562
|
+
await ensureMigrated();
|
|
1563
|
+
return storage.getFeedbackStats(pool, {
|
|
1564
|
+
schema,
|
|
1565
|
+
tenantId,
|
|
1566
|
+
agentId: opts.agentId || undefined,
|
|
1567
|
+
dateFrom: opts.dateFrom || undefined,
|
|
1568
|
+
dateTo: opts.dateTo || undefined,
|
|
1569
|
+
});
|
|
1570
|
+
},
|
|
1571
|
+
|
|
1561
1572
|
// --- admin ---
|
|
1562
1573
|
|
|
1563
1574
|
async getSession(sessionId, opts = {}) {
|
|
@@ -1837,6 +1848,7 @@ function createAquifer(config = {}) {
|
|
|
1837
1848
|
recallWeights: (config.insights && config.insights.recallWeights) || null,
|
|
1838
1849
|
recencyWindowDays: config.insights && Number.isFinite(config.insights.recencyWindowDays)
|
|
1839
1850
|
? config.insights.recencyWindowDays : undefined,
|
|
1851
|
+
dedup: config.insights && config.insights.dedup ? config.insights.dedup : undefined,
|
|
1840
1852
|
});
|
|
1841
1853
|
|
|
1842
1854
|
return aquifer;
|