hippo-memory 0.36.0 → 0.37.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/README.md +16 -0
- package/dist/api.d.ts +20 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +23 -3
- package/dist/api.js.map +1 -1
- package/dist/benchmarks/e1.3/incident-recall-eval.js +74 -0
- package/dist/benchmarks/e1.3/incident-recall-eval.js.map +1 -0
- package/dist/benchmarks/e1.3/scenarios.json +2587 -0
- package/dist/benchmarks/e1.3/slack-1000-event-smoke.js +102 -0
- package/dist/benchmarks/e1.3/slack-1000-event-smoke.js.map +1 -0
- package/dist/cli.js +82 -0
- package/dist/cli.js.map +1 -1
- package/dist/connectors/slack/backfill.d.ts +42 -0
- package/dist/connectors/slack/backfill.d.ts.map +1 -0
- package/dist/connectors/slack/backfill.js +76 -0
- package/dist/connectors/slack/backfill.js.map +1 -0
- package/dist/connectors/slack/deletion.d.ts +14 -0
- package/dist/connectors/slack/deletion.d.ts.map +1 -0
- package/dist/connectors/slack/deletion.js +46 -0
- package/dist/connectors/slack/deletion.js.map +1 -0
- package/dist/connectors/slack/dlq.d.ts +21 -0
- package/dist/connectors/slack/dlq.d.ts.map +1 -0
- package/dist/connectors/slack/dlq.js +23 -0
- package/dist/connectors/slack/dlq.js.map +1 -0
- package/dist/connectors/slack/idempotency.d.ts +5 -0
- package/dist/connectors/slack/idempotency.d.ts.map +1 -0
- package/dist/connectors/slack/idempotency.js +13 -0
- package/dist/connectors/slack/idempotency.js.map +1 -0
- package/dist/connectors/slack/ingest.d.ts +27 -0
- package/dist/connectors/slack/ingest.d.ts.map +1 -0
- package/dist/connectors/slack/ingest.js +48 -0
- package/dist/connectors/slack/ingest.js.map +1 -0
- package/dist/connectors/slack/ratelimit.d.ts +9 -0
- package/dist/connectors/slack/ratelimit.d.ts.map +1 -0
- package/dist/connectors/slack/ratelimit.js +18 -0
- package/dist/connectors/slack/ratelimit.js.map +1 -0
- package/dist/connectors/slack/scope.d.ts +16 -0
- package/dist/connectors/slack/scope.d.ts.map +1 -0
- package/dist/connectors/slack/scope.js +13 -0
- package/dist/connectors/slack/scope.js.map +1 -0
- package/dist/connectors/slack/signature.d.ts +12 -0
- package/dist/connectors/slack/signature.d.ts.map +1 -0
- package/dist/connectors/slack/signature.js +20 -0
- package/dist/connectors/slack/signature.js.map +1 -0
- package/dist/connectors/slack/tenant-routing.d.ts +13 -0
- package/dist/connectors/slack/tenant-routing.d.ts.map +1 -0
- package/dist/connectors/slack/tenant-routing.js +17 -0
- package/dist/connectors/slack/tenant-routing.js.map +1 -0
- package/dist/connectors/slack/transform.d.ts +20 -0
- package/dist/connectors/slack/transform.d.ts.map +1 -0
- package/dist/connectors/slack/transform.js +31 -0
- package/dist/connectors/slack/transform.js.map +1 -0
- package/dist/connectors/slack/types.d.ts +35 -0
- package/dist/connectors/slack/types.d.ts.map +1 -0
- package/dist/connectors/slack/types.js +23 -0
- package/dist/connectors/slack/types.js.map +1 -0
- package/dist/connectors/slack/web-client.d.ts +12 -0
- package/dist/connectors/slack/web-client.d.ts.map +1 -0
- package/dist/connectors/slack/web-client.js +43 -0
- package/dist/connectors/slack/web-client.js.map +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +46 -1
- package/dist/db.js.map +1 -1
- package/dist/importers.js +3 -3
- package/dist/importers.js.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +174 -2
- package/dist/server.js.map +1 -1
- package/dist/src/ambient.js +147 -0
- package/dist/src/ambient.js.map +1 -0
- package/dist/src/api.js +343 -0
- package/dist/src/api.js.map +1 -0
- package/dist/src/audit.js +152 -0
- package/dist/src/audit.js.map +1 -0
- package/dist/src/auth.js +65 -0
- package/dist/src/auth.js.map +1 -0
- package/dist/src/autolearn.js +143 -0
- package/dist/src/autolearn.js.map +1 -0
- package/dist/src/capture.js +512 -0
- package/dist/src/capture.js.map +1 -0
- package/dist/src/cli.js +4971 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/client.js +181 -0
- package/dist/src/client.js.map +1 -0
- package/dist/src/config.js +108 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/connectors/slack/backfill.js +76 -0
- package/dist/src/connectors/slack/backfill.js.map +1 -0
- package/dist/src/connectors/slack/deletion.js +46 -0
- package/dist/src/connectors/slack/deletion.js.map +1 -0
- package/dist/src/connectors/slack/dlq.js +23 -0
- package/dist/src/connectors/slack/dlq.js.map +1 -0
- package/dist/src/connectors/slack/idempotency.js +13 -0
- package/dist/src/connectors/slack/idempotency.js.map +1 -0
- package/dist/src/connectors/slack/ingest.js +48 -0
- package/dist/src/connectors/slack/ingest.js.map +1 -0
- package/dist/src/connectors/slack/ratelimit.js +18 -0
- package/dist/src/connectors/slack/ratelimit.js.map +1 -0
- package/dist/src/connectors/slack/scope.js +13 -0
- package/dist/src/connectors/slack/scope.js.map +1 -0
- package/dist/src/connectors/slack/signature.js +20 -0
- package/dist/src/connectors/slack/signature.js.map +1 -0
- package/dist/src/connectors/slack/tenant-routing.js +17 -0
- package/dist/src/connectors/slack/tenant-routing.js.map +1 -0
- package/dist/src/connectors/slack/transform.js +31 -0
- package/dist/src/connectors/slack/transform.js.map +1 -0
- package/dist/src/connectors/slack/types.js +23 -0
- package/dist/src/connectors/slack/types.js.map +1 -0
- package/dist/src/connectors/slack/web-client.js +43 -0
- package/dist/src/connectors/slack/web-client.js.map +1 -0
- package/dist/src/consolidate.js +517 -0
- package/dist/src/consolidate.js.map +1 -0
- package/dist/src/dag.js +104 -0
- package/dist/src/dag.js.map +1 -0
- package/dist/src/dashboard.js +409 -0
- package/dist/src/dashboard.js.map +1 -0
- package/dist/src/db.js +584 -0
- package/dist/src/db.js.map +1 -0
- package/dist/src/embeddings.js +344 -0
- package/dist/src/embeddings.js.map +1 -0
- package/dist/src/eval-suite.js +289 -0
- package/dist/src/eval-suite.js.map +1 -0
- package/dist/src/eval.js +187 -0
- package/dist/src/eval.js.map +1 -0
- package/dist/src/extract.js +87 -0
- package/dist/src/extract.js.map +1 -0
- package/dist/src/handoff.js +30 -0
- package/dist/src/handoff.js.map +1 -0
- package/dist/src/hooks.js +582 -0
- package/dist/src/hooks.js.map +1 -0
- package/dist/src/importers.js +399 -0
- package/dist/src/importers.js.map +1 -0
- package/dist/src/index.js +25 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/invalidation.js +94 -0
- package/dist/src/invalidation.js.map +1 -0
- package/dist/src/mcp/framing.js +45 -0
- package/dist/src/mcp/framing.js.map +1 -0
- package/dist/src/mcp/server.js +510 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/dist/src/memory.js +280 -0
- package/dist/src/memory.js.map +1 -0
- package/dist/src/multihop.js +32 -0
- package/dist/src/multihop.js.map +1 -0
- package/dist/src/path-context.js +32 -0
- package/dist/src/path-context.js.map +1 -0
- package/dist/src/physics-config.js +26 -0
- package/dist/src/physics-config.js.map +1 -0
- package/dist/src/physics-state.js +163 -0
- package/dist/src/physics-state.js.map +1 -0
- package/dist/src/physics.js +361 -0
- package/dist/src/physics.js.map +1 -0
- package/dist/src/postinstall.js +68 -0
- package/dist/src/postinstall.js.map +1 -0
- package/dist/src/raw-archive.js +72 -0
- package/dist/src/raw-archive.js.map +1 -0
- package/dist/src/refine-llm.js +147 -0
- package/dist/src/refine-llm.js.map +1 -0
- package/dist/src/replay.js +117 -0
- package/dist/src/replay.js.map +1 -0
- package/dist/src/salience.js +74 -0
- package/dist/src/salience.js.map +1 -0
- package/dist/src/scheduler.js +67 -0
- package/dist/src/scheduler.js.map +1 -0
- package/dist/src/scope.js +35 -0
- package/dist/src/scope.js.map +1 -0
- package/dist/src/search.js +801 -0
- package/dist/src/search.js.map +1 -0
- package/dist/src/server-detect.js +70 -0
- package/dist/src/server-detect.js.map +1 -0
- package/dist/src/server.js +784 -0
- package/dist/src/server.js.map +1 -0
- package/dist/src/shared.js +309 -0
- package/dist/src/shared.js.map +1 -0
- package/dist/src/sso.js +22 -0
- package/dist/src/sso.js.map +1 -0
- package/dist/src/store.js +1390 -0
- package/dist/src/store.js.map +1 -0
- package/dist/src/tenant.js +17 -0
- package/dist/src/tenant.js.map +1 -0
- package/dist/src/trace.js +64 -0
- package/dist/src/trace.js.map +1 -0
- package/dist/src/working-memory.js +149 -0
- package/dist/src/working-memory.js.map +1 -0
- package/dist/src/yaml.js +98 -0
- package/dist/src/yaml.js.map +1 -0
- package/dist/store.d.ts +9 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +30 -2
- package/dist/store.js.map +1 -1
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
- package/dist/import.d.ts +0 -31
- package/dist/import.d.ts.map +0 -1
- package/dist/import.js +0 -307
- package/dist/import.js.map +0 -1
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional embedding-based semantic search for Hippo.
|
|
3
|
+
* Uses @xenova/transformers (local, zero API keys, ~22MB model).
|
|
4
|
+
* Falls back silently if the library is not installed.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import { createRequire } from 'module';
|
|
9
|
+
import { loadAllEntries } from './store.js';
|
|
10
|
+
import { openHippoDb, closeHippoDb, getMeta, setMeta } from './db.js';
|
|
11
|
+
import { initializeParticle, savePhysicsState, loadPhysicsState, resetAllPhysicsState } from './physics-state.js';
|
|
12
|
+
import { loadConfig } from './config.js';
|
|
13
|
+
// Use createRequire for synchronous module resolution check in ESM
|
|
14
|
+
const _require = createRequire(import.meta.url);
|
|
15
|
+
// Cached availability check
|
|
16
|
+
let _embeddingAvailable = null;
|
|
17
|
+
// Lazy-loaded pipeline (expensive to initialize)
|
|
18
|
+
const _pipelineInstances = new Map();
|
|
19
|
+
const _pipelineLoading = new Map();
|
|
20
|
+
const DEFAULT_EMBEDDING_MODEL = 'Xenova/all-MiniLM-L6-v2';
|
|
21
|
+
const EMBEDDING_MODEL_META_KEY = 'embedding_model';
|
|
22
|
+
// Use Function constructor to bypass TypeScript static module resolution
|
|
23
|
+
// for optional peer dependencies that may not be installed.
|
|
24
|
+
const _dynImport = new Function('s', 'return import(s)');
|
|
25
|
+
/**
|
|
26
|
+
* Check (synchronously) if @xenova/transformers or @huggingface/transformers is installed.
|
|
27
|
+
*/
|
|
28
|
+
export function isEmbeddingAvailable() {
|
|
29
|
+
if (_embeddingAvailable !== null)
|
|
30
|
+
return _embeddingAvailable;
|
|
31
|
+
try {
|
|
32
|
+
_require.resolve('@xenova/transformers');
|
|
33
|
+
_embeddingAvailable = true;
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// fall through
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
_require.resolve('@huggingface/transformers');
|
|
41
|
+
_embeddingAvailable = true;
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// fall through
|
|
46
|
+
}
|
|
47
|
+
_embeddingAvailable = false;
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
51
|
+
async function loadPipeline(model) {
|
|
52
|
+
if (_pipelineInstances.has(model))
|
|
53
|
+
return _pipelineInstances.get(model);
|
|
54
|
+
if (_pipelineLoading.has(model))
|
|
55
|
+
return _pipelineLoading.get(model);
|
|
56
|
+
const loading = (async () => {
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
58
|
+
let pipelineFn = null;
|
|
59
|
+
try {
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
61
|
+
const mod = await _dynImport('@xenova/transformers');
|
|
62
|
+
pipelineFn = mod.pipeline ?? mod.default?.pipeline;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
try {
|
|
66
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
67
|
+
const mod = await _dynImport('@huggingface/transformers');
|
|
68
|
+
pipelineFn = mod.pipeline ?? mod.default?.pipeline;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (!pipelineFn)
|
|
75
|
+
return null;
|
|
76
|
+
try {
|
|
77
|
+
const instance = await pipelineFn('feature-extraction', model, { quantized: true });
|
|
78
|
+
_pipelineInstances.set(model, instance);
|
|
79
|
+
return instance;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
_pipelineLoading.delete(model);
|
|
86
|
+
}
|
|
87
|
+
})();
|
|
88
|
+
_pipelineLoading.set(model, loading);
|
|
89
|
+
return loading;
|
|
90
|
+
}
|
|
91
|
+
export function resolveEmbeddingModel(hippoRoot, explicitModel) {
|
|
92
|
+
const direct = explicitModel?.trim();
|
|
93
|
+
if (direct)
|
|
94
|
+
return direct;
|
|
95
|
+
try {
|
|
96
|
+
const configured = loadConfig(hippoRoot).embeddings.model?.trim();
|
|
97
|
+
if (configured)
|
|
98
|
+
return configured;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Fall back to the default model when config cannot be read.
|
|
102
|
+
}
|
|
103
|
+
return DEFAULT_EMBEDDING_MODEL;
|
|
104
|
+
}
|
|
105
|
+
function loadStoredEmbeddingModel(hippoRoot) {
|
|
106
|
+
try {
|
|
107
|
+
const db = openHippoDb(hippoRoot);
|
|
108
|
+
try {
|
|
109
|
+
const model = getMeta(db, EMBEDDING_MODEL_META_KEY, '').trim();
|
|
110
|
+
return model || null;
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
closeHippoDb(db);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function saveStoredEmbeddingModel(hippoRoot, model) {
|
|
121
|
+
const db = openHippoDb(hippoRoot);
|
|
122
|
+
try {
|
|
123
|
+
setMeta(db, EMBEDDING_MODEL_META_KEY, model);
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
closeHippoDb(db);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export function resolveIndexedEmbeddingModel(hippoRoot, index = loadEmbeddingIndex(hippoRoot)) {
|
|
130
|
+
const stored = loadStoredEmbeddingModel(hippoRoot);
|
|
131
|
+
if (stored)
|
|
132
|
+
return stored;
|
|
133
|
+
return Object.keys(index).length > 0 ? DEFAULT_EMBEDDING_MODEL : null;
|
|
134
|
+
}
|
|
135
|
+
export function embeddingModelRequiresReindex(hippoRoot, model, index = loadEmbeddingIndex(hippoRoot)) {
|
|
136
|
+
const indexedModel = resolveIndexedEmbeddingModel(hippoRoot, index);
|
|
137
|
+
return indexedModel !== null && indexedModel !== model;
|
|
138
|
+
}
|
|
139
|
+
async function rebuildEmbeddingIndex(entries, model) {
|
|
140
|
+
const rebuilt = {};
|
|
141
|
+
for (const entry of entries) {
|
|
142
|
+
const text = `${entry.content} ${entry.tags.join(' ')}`.trim();
|
|
143
|
+
const vector = await getEmbedding(text, model);
|
|
144
|
+
if (vector.length > 0) {
|
|
145
|
+
rebuilt[entry.id] = vector;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return rebuilt;
|
|
149
|
+
}
|
|
150
|
+
function resetPhysicsFromIndex(hippoRoot, entries, index) {
|
|
151
|
+
try {
|
|
152
|
+
const db = openHippoDb(hippoRoot);
|
|
153
|
+
try {
|
|
154
|
+
resetAllPhysicsState(db, entries, index);
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
closeHippoDb(db);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Physics reset is best-effort; retrieval will still fall back gracefully.
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Get an embedding vector for a piece of text.
|
|
166
|
+
* Returns an empty array if transformers is not available or fails.
|
|
167
|
+
*/
|
|
168
|
+
export async function getEmbedding(text, model = DEFAULT_EMBEDDING_MODEL) {
|
|
169
|
+
if (!isEmbeddingAvailable())
|
|
170
|
+
return [];
|
|
171
|
+
try {
|
|
172
|
+
const pipe = await loadPipeline(model);
|
|
173
|
+
if (!pipe)
|
|
174
|
+
return [];
|
|
175
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
176
|
+
const output = await pipe(text, { pooling: 'mean', normalize: true });
|
|
177
|
+
return Array.from(output.data);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Cosine similarity between two vectors. Handles unnormalized vectors.
|
|
185
|
+
* Returns 0 for empty or mismatched vectors.
|
|
186
|
+
*/
|
|
187
|
+
export function cosineSimilarity(a, b) {
|
|
188
|
+
if (a.length === 0 || b.length === 0 || a.length !== b.length)
|
|
189
|
+
return 0;
|
|
190
|
+
let dot = 0;
|
|
191
|
+
let normA = 0;
|
|
192
|
+
let normB = 0;
|
|
193
|
+
for (let i = 0; i < a.length; i++) {
|
|
194
|
+
dot += a[i] * b[i];
|
|
195
|
+
normA += a[i] * a[i];
|
|
196
|
+
normB += b[i] * b[i];
|
|
197
|
+
}
|
|
198
|
+
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
199
|
+
if (denom < 1e-10)
|
|
200
|
+
return 0;
|
|
201
|
+
// Clamp to [-1, 1] to handle floating point drift
|
|
202
|
+
return Math.min(1, Math.max(-1, dot / denom));
|
|
203
|
+
}
|
|
204
|
+
const EMBEDDINGS_FILE = 'embeddings.json';
|
|
205
|
+
/**
|
|
206
|
+
* Load the cached embedding index from disk.
|
|
207
|
+
* Returns an empty object if the file doesn't exist or is corrupt.
|
|
208
|
+
*/
|
|
209
|
+
export function loadEmbeddingIndex(hippoRoot) {
|
|
210
|
+
const fp = path.join(hippoRoot, EMBEDDINGS_FILE);
|
|
211
|
+
if (!fs.existsSync(fp))
|
|
212
|
+
return {};
|
|
213
|
+
try {
|
|
214
|
+
return JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return {};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Save the embedding index to disk.
|
|
222
|
+
*/
|
|
223
|
+
export function saveEmbeddingIndex(hippoRoot, index) {
|
|
224
|
+
const fp = path.join(hippoRoot, EMBEDDINGS_FILE);
|
|
225
|
+
const tmp = fp + '.tmp';
|
|
226
|
+
fs.writeFileSync(tmp, JSON.stringify(index), 'utf8');
|
|
227
|
+
try {
|
|
228
|
+
fs.renameSync(tmp, fp);
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
try {
|
|
232
|
+
fs.unlinkSync(tmp);
|
|
233
|
+
}
|
|
234
|
+
catch { /* best-effort cleanup */ }
|
|
235
|
+
throw err;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Mutex to serialize embedding writes and prevent read-modify-write races
|
|
239
|
+
let _embedWriteLock = Promise.resolve();
|
|
240
|
+
async function withEmbedLock(fn) {
|
|
241
|
+
let resolve;
|
|
242
|
+
const next = new Promise(r => { resolve = r; });
|
|
243
|
+
const prev = _embedWriteLock;
|
|
244
|
+
_embedWriteLock = next;
|
|
245
|
+
await prev;
|
|
246
|
+
try {
|
|
247
|
+
return await fn();
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
resolve();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Embed a single memory entry and cache the result in the embedding index.
|
|
255
|
+
*/
|
|
256
|
+
export async function embedMemory(hippoRoot, entry, model) {
|
|
257
|
+
if (!isEmbeddingAvailable())
|
|
258
|
+
return;
|
|
259
|
+
return withEmbedLock(async () => {
|
|
260
|
+
const effectiveModel = resolveEmbeddingModel(hippoRoot, model);
|
|
261
|
+
const existingIndex = loadEmbeddingIndex(hippoRoot);
|
|
262
|
+
if (embeddingModelRequiresReindex(hippoRoot, effectiveModel, existingIndex)) {
|
|
263
|
+
const entries = loadAllEntries(hippoRoot);
|
|
264
|
+
const rebuiltIndex = await rebuildEmbeddingIndex(entries, effectiveModel);
|
|
265
|
+
saveEmbeddingIndex(hippoRoot, rebuiltIndex);
|
|
266
|
+
saveStoredEmbeddingModel(hippoRoot, effectiveModel);
|
|
267
|
+
resetPhysicsFromIndex(hippoRoot, entries, rebuiltIndex);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const text = `${entry.content} ${entry.tags.join(' ')}`.trim();
|
|
271
|
+
const vector = await getEmbedding(text, effectiveModel);
|
|
272
|
+
if (vector.length === 0)
|
|
273
|
+
return;
|
|
274
|
+
const index = existingIndex;
|
|
275
|
+
index[entry.id] = vector;
|
|
276
|
+
saveEmbeddingIndex(hippoRoot, index);
|
|
277
|
+
saveStoredEmbeddingModel(hippoRoot, effectiveModel);
|
|
278
|
+
// Initialize physics state for this memory
|
|
279
|
+
try {
|
|
280
|
+
const db = openHippoDb(hippoRoot);
|
|
281
|
+
try {
|
|
282
|
+
const existing = loadPhysicsState(db, [entry.id]);
|
|
283
|
+
if (!existing.has(entry.id)) {
|
|
284
|
+
const particle = initializeParticle(entry, vector);
|
|
285
|
+
savePhysicsState(db, [particle]);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
finally {
|
|
289
|
+
closeHippoDb(db);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
// Physics init is best-effort — don't break embedding
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Embed all entries in hippoRoot that don't already have cached vectors.
|
|
299
|
+
* Prunes orphaned embeddings for memories that no longer exist.
|
|
300
|
+
* Returns the count of newly embedded entries.
|
|
301
|
+
*/
|
|
302
|
+
export async function embedAll(hippoRoot, model) {
|
|
303
|
+
if (!isEmbeddingAvailable())
|
|
304
|
+
return 0;
|
|
305
|
+
return withEmbedLock(async () => {
|
|
306
|
+
const effectiveModel = resolveEmbeddingModel(hippoRoot, model);
|
|
307
|
+
const entries = loadAllEntries(hippoRoot);
|
|
308
|
+
const index = loadEmbeddingIndex(hippoRoot);
|
|
309
|
+
if (embeddingModelRequiresReindex(hippoRoot, effectiveModel, index)) {
|
|
310
|
+
const rebuiltIndex = await rebuildEmbeddingIndex(entries, effectiveModel);
|
|
311
|
+
saveEmbeddingIndex(hippoRoot, rebuiltIndex);
|
|
312
|
+
saveStoredEmbeddingModel(hippoRoot, effectiveModel);
|
|
313
|
+
resetPhysicsFromIndex(hippoRoot, entries, rebuiltIndex);
|
|
314
|
+
return Object.keys(rebuiltIndex).length;
|
|
315
|
+
}
|
|
316
|
+
let count = 0;
|
|
317
|
+
let dirty = false;
|
|
318
|
+
// Prune orphaned embeddings for deleted memories
|
|
319
|
+
const activeIds = new Set(entries.map((e) => e.id));
|
|
320
|
+
for (const id of Object.keys(index)) {
|
|
321
|
+
if (!activeIds.has(id)) {
|
|
322
|
+
delete index[id];
|
|
323
|
+
dirty = true;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
for (const entry of entries) {
|
|
327
|
+
if (index[entry.id])
|
|
328
|
+
continue; // already embedded
|
|
329
|
+
const text = `${entry.content} ${entry.tags.join(' ')}`.trim();
|
|
330
|
+
const vector = await getEmbedding(text, effectiveModel);
|
|
331
|
+
if (vector.length > 0) {
|
|
332
|
+
index[entry.id] = vector;
|
|
333
|
+
count++;
|
|
334
|
+
dirty = true;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (dirty) {
|
|
338
|
+
saveEmbeddingIndex(hippoRoot, index);
|
|
339
|
+
}
|
|
340
|
+
saveStoredEmbeddingModel(hippoRoot, effectiveModel);
|
|
341
|
+
return count;
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
//# sourceMappingURL=embeddings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embeddings.js","sourceRoot":"","sources":["../../src/embeddings.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-contained feature evaluation suite for hippo.
|
|
3
|
+
*
|
|
4
|
+
* Creates a synthetic memory corpus with known ground truth, runs searches
|
|
5
|
+
* per feature category, and reports per-feature metrics with regression
|
|
6
|
+
* detection against a saved baseline.
|
|
7
|
+
*
|
|
8
|
+
* Design goals:
|
|
9
|
+
* - Zero API calls (no LLM judge, no embeddings)
|
|
10
|
+
* - Deterministic (fixed timestamps, content, IDs)
|
|
11
|
+
* - Fast (<60s for full suite)
|
|
12
|
+
* - Per-feature breakdown so you see exactly what a change helped/hurt
|
|
13
|
+
*/
|
|
14
|
+
import { createMemory, Layer } from './memory.js';
|
|
15
|
+
import { search } from './search.js';
|
|
16
|
+
import { multihopSearch } from './multihop.js';
|
|
17
|
+
import { mrr, recallAtK, ndcgAtK } from './eval.js';
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Synthetic corpus — deterministic, no API calls
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
const BASE_DATE = new Date('2026-01-15T10:00:00Z');
|
|
22
|
+
function dateOffset(days) {
|
|
23
|
+
const d = new Date(BASE_DATE.getTime() + days * 86400000);
|
|
24
|
+
return d.toISOString();
|
|
25
|
+
}
|
|
26
|
+
function mem(id, content, opts = {}) {
|
|
27
|
+
const entry = createMemory(content, {
|
|
28
|
+
layer: opts.layer ?? Layer.Episodic,
|
|
29
|
+
tags: opts.tags ?? [],
|
|
30
|
+
baseHalfLifeDays: 30,
|
|
31
|
+
extracted_from: opts.extracted_from,
|
|
32
|
+
dag_level: opts.dag_level,
|
|
33
|
+
dag_parent_id: opts.dag_parent_id,
|
|
34
|
+
});
|
|
35
|
+
entry.id = id;
|
|
36
|
+
if (opts.created)
|
|
37
|
+
entry.created = opts.created;
|
|
38
|
+
return entry;
|
|
39
|
+
}
|
|
40
|
+
export function buildSyntheticCorpus() {
|
|
41
|
+
const entries = [];
|
|
42
|
+
const cases = [];
|
|
43
|
+
// =========================================================================
|
|
44
|
+
// 1. DIRECT RECALL — basic keyword matching
|
|
45
|
+
// =========================================================================
|
|
46
|
+
entries.push(mem('dr-1', 'The PostgreSQL database migration failed because the users table had a NOT NULL constraint on the email column', { created: dateOffset(1), tags: ['topic:database'] }), mem('dr-2', 'React component rendering performance improved by 40% after memoizing the expensive computation in useMemo', { created: dateOffset(2), tags: ['topic:frontend'] }), mem('dr-3', 'The API rate limiter should use a sliding window algorithm instead of fixed window to prevent burst traffic', { created: dateOffset(3), tags: ['topic:api'] }), mem('dr-4', 'Docker container memory limits need to be set to 512MB for the worker service to prevent OOM kills', { created: dateOffset(4), tags: ['topic:devops'] }), mem('dr-5', 'The JWT token expiration was set to 24 hours but should be reduced to 1 hour for security compliance', { created: dateOffset(5), tags: ['topic:security'] }), mem('dr-6', 'Webpack bundle size grew to 2.3MB because lodash was imported as a whole instead of cherry-picking', { created: dateOffset(6), tags: ['topic:frontend'] }), mem('dr-7', 'The Redis cache TTL for user sessions should match the JWT expiration to prevent stale sessions', { created: dateOffset(7), tags: ['topic:caching'] }), mem('dr-8', 'GraphQL resolver for nested comments has N+1 query problem solved by DataLoader batching', { created: dateOffset(8), tags: ['topic:api'] }));
|
|
47
|
+
cases.push({ id: 'dr-q1', category: 'direct-recall', query: 'PostgreSQL migration NOT NULL constraint', expectedIds: ['dr-1'], description: 'exact keyword match on DB migration' }, { id: 'dr-q2', category: 'direct-recall', query: 'React useMemo performance memoizing', expectedIds: ['dr-2'], description: 'React performance optimization' }, { id: 'dr-q3', category: 'direct-recall', query: 'sliding window rate limiter API', expectedIds: ['dr-3'], description: 'API rate limiting approach' }, { id: 'dr-q4', category: 'direct-recall', query: 'Docker OOM memory limits worker', expectedIds: ['dr-4'], description: 'Docker memory config' }, { id: 'dr-q5', category: 'direct-recall', query: 'JWT token expiration security', expectedIds: ['dr-5'], description: 'JWT security setting' }, { id: 'dr-q6', category: 'direct-recall', query: 'webpack bundle size lodash', expectedIds: ['dr-6'], description: 'bundle size issue' }, { id: 'dr-q7', category: 'direct-recall', query: 'Redis cache TTL session expiration', expectedIds: ['dr-7'], description: 'cache TTL config' }, { id: 'dr-q8', category: 'direct-recall', query: 'GraphQL N+1 DataLoader batching', expectedIds: ['dr-8'], description: 'GraphQL N+1 fix' });
|
|
48
|
+
// =========================================================================
|
|
49
|
+
// 2. EXTRACTION PREFERENCE — extracted facts should rank above raw source
|
|
50
|
+
// =========================================================================
|
|
51
|
+
entries.push(mem('ep-src-1', 'speaker:Alice: So we had this big meeting yesterday about the deployment pipeline and Bob mentioned that the staging environment is using Kubernetes 1.28 and we should upgrade to 1.30 before the end of Q2 because of the security patches', {
|
|
52
|
+
created: dateOffset(10), tags: ['speaker:Alice', 'topic:infrastructure', 'session:meeting-1'],
|
|
53
|
+
}), mem('ep-ext-1', 'The staging environment runs Kubernetes 1.28 and needs to be upgraded to 1.30 before end of Q2 for security patches', {
|
|
54
|
+
created: dateOffset(10), layer: Layer.Semantic, tags: ['speaker:Alice', 'topic:infrastructure', 'extracted'],
|
|
55
|
+
extracted_from: 'ep-src-1',
|
|
56
|
+
}), mem('ep-src-2', 'speaker:Bob: Yeah and the thing about the monitoring is that we switched from Datadog to Grafana last month and the alerting rules still need to be migrated, Carol was supposed to handle that but she has been busy with the frontend rewrite', {
|
|
57
|
+
created: dateOffset(11), tags: ['speaker:Bob', 'topic:monitoring', 'session:meeting-1'],
|
|
58
|
+
}), mem('ep-ext-2', 'The team switched from Datadog to Grafana last month but alerting rules have not been migrated yet. Carol is responsible but blocked by the frontend rewrite.', {
|
|
59
|
+
created: dateOffset(11), layer: Layer.Semantic, tags: ['speaker:Bob', 'topic:monitoring', 'extracted'],
|
|
60
|
+
extracted_from: 'ep-src-2',
|
|
61
|
+
}), mem('ep-src-3', 'speaker:Carol: The login page redesign is almost done, I just need to wire up the OAuth2 PKCE flow with the new identity provider and write the integration tests', {
|
|
62
|
+
created: dateOffset(12), tags: ['speaker:Carol', 'topic:auth', 'session:meeting-2'],
|
|
63
|
+
}), mem('ep-ext-3', 'Carol is nearly done with the login page redesign. Remaining work: wire up OAuth2 PKCE flow with the new identity provider and write integration tests.', {
|
|
64
|
+
created: dateOffset(12), layer: Layer.Semantic, tags: ['speaker:Carol', 'topic:auth', 'extracted'],
|
|
65
|
+
extracted_from: 'ep-src-3',
|
|
66
|
+
}));
|
|
67
|
+
cases.push({ id: 'ep-q1', category: 'extraction-preference', query: 'Kubernetes upgrade staging environment', expectedIds: ['ep-ext-1'], description: 'extracted fact about K8s upgrade should rank above raw utterance' }, { id: 'ep-q2', category: 'extraction-preference', query: 'Datadog Grafana alerting migration', expectedIds: ['ep-ext-2'], description: 'extracted fact about monitoring switch' }, { id: 'ep-q3', category: 'extraction-preference', query: 'OAuth2 PKCE login page redesign', expectedIds: ['ep-ext-3'], description: 'extracted fact about auth work' });
|
|
68
|
+
// =========================================================================
|
|
69
|
+
// 3. DAG DRILL-DOWN — summary nodes should surface children
|
|
70
|
+
// =========================================================================
|
|
71
|
+
entries.push(mem('dag-child-1', 'The API response time for /users endpoint degraded from 50ms to 300ms after adding the permissions check', {
|
|
72
|
+
created: dateOffset(15), layer: Layer.Semantic, tags: ['topic:api-performance', 'extracted'],
|
|
73
|
+
dag_level: 1,
|
|
74
|
+
}), mem('dag-child-2', 'The /orders endpoint latency spiked to 500ms because of a missing index on the orders.created_at column', {
|
|
75
|
+
created: dateOffset(15), layer: Layer.Semantic, tags: ['topic:api-performance', 'extracted'],
|
|
76
|
+
dag_level: 1,
|
|
77
|
+
}), mem('dag-child-3', 'Batch API endpoint /reports/generate takes 8 seconds because it runs synchronously instead of using a job queue', {
|
|
78
|
+
created: dateOffset(15), layer: Layer.Semantic, tags: ['topic:api-performance', 'extracted'],
|
|
79
|
+
dag_level: 1,
|
|
80
|
+
}), mem('dag-summary-1', 'API performance issues: /users degraded to 300ms (permissions check), /orders spiked to 500ms (missing index), /reports takes 8s (needs job queue)', {
|
|
81
|
+
created: dateOffset(16), layer: Layer.Semantic, tags: ['topic:api-performance'],
|
|
82
|
+
dag_level: 2, dag_parent_id: undefined,
|
|
83
|
+
}));
|
|
84
|
+
// Link children to parent
|
|
85
|
+
for (const child of [entries.find(e => e.id === 'dag-child-1'), entries.find(e => e.id === 'dag-child-2'), entries.find(e => e.id === 'dag-child-3')]) {
|
|
86
|
+
child.dag_parent_id = 'dag-summary-1';
|
|
87
|
+
}
|
|
88
|
+
cases.push({ id: 'dag-q1', category: 'dag-drilldown', query: 'API performance problems latency', expectedIds: ['dag-child-1', 'dag-child-2', 'dag-child-3', 'dag-summary-1'], description: 'summary should drill down to all children' }, { id: 'dag-q2', category: 'dag-drilldown', query: 'endpoint response time degradation', expectedIds: ['dag-child-1', 'dag-child-2', 'dag-summary-1'], description: 'query matching summary should surface relevant children' });
|
|
89
|
+
// =========================================================================
|
|
90
|
+
// 4. TEMPORAL — recency/oldest cues should affect ranking
|
|
91
|
+
// =========================================================================
|
|
92
|
+
entries.push(mem('tmp-1', 'The team decided to use TypeScript for the new service', { created: dateOffset(-30), tags: ['topic:architecture'] }), mem('tmp-2', 'The team evaluated Rust as an alternative language for the service', { created: dateOffset(-20), tags: ['topic:architecture'] }), mem('tmp-3', 'The team added Go as a candidate language for the service rewrite', { created: dateOffset(-10), tags: ['topic:architecture'] }), mem('tmp-4', 'The team finalized the language choice as Go for the service rewrite', { created: dateOffset(-1), tags: ['topic:architecture'] }));
|
|
93
|
+
cases.push({ id: 'tmp-q1', category: 'temporal', query: 'what did the team recently decide about the service language', expectedIds: ['tmp-4'], description: 'recent cue should boost newest entry' }, { id: 'tmp-q2', category: 'temporal', query: 'what was the first language choice for the service', expectedIds: ['tmp-1'], description: 'oldest cue should boost earliest entry' }, { id: 'tmp-q3', category: 'temporal', query: 'latest update on the service rewrite language', expectedIds: ['tmp-4'], description: 'latest should boost most recent' }, { id: 'tmp-q4', category: 'temporal', query: 'original architecture decision for the service', expectedIds: ['tmp-1'], description: 'original should boost earliest' });
|
|
94
|
+
// =========================================================================
|
|
95
|
+
// 5. NOISE RESISTANCE — relevant memories found despite noise
|
|
96
|
+
// =========================================================================
|
|
97
|
+
// Add 30 noise entries
|
|
98
|
+
const noiseTopics = [
|
|
99
|
+
'breakfast meeting catering ordered sandwiches', 'office temperature thermostat adjusted',
|
|
100
|
+
'printer paper refill third floor supply', 'parking lot gate code changed 4521',
|
|
101
|
+
'fire drill scheduled next Thursday morning', 'coffee machine broken maintenance called',
|
|
102
|
+
'desk booking system new policy hybrid', 'meeting room projector HDMI adapter missing',
|
|
103
|
+
'birthday celebration for Dave next Friday', 'recycling bins moved to kitchen area',
|
|
104
|
+
'badge access updated for new hires', 'elevator maintenance scheduled weekend',
|
|
105
|
+
'lunch order from Italian place confirmed', 'desk plants watering schedule posted',
|
|
106
|
+
'air conditioning unit serviced last week', 'office carpet cleaning Friday evening',
|
|
107
|
+
'new microwave installed in kitchen', 'visitor parking available spots three',
|
|
108
|
+
'holiday schedule posted on intranet', 'team photo session Thursday afternoon',
|
|
109
|
+
'chair ergonomics assessment signup sheet', 'standing desk adjustment instructions',
|
|
110
|
+
'kitchen fridge cleanup policy reminder', 'window blinds replaced on south side',
|
|
111
|
+
'bicycle rack installed in parking garage', 'team lunch budget increased quarterly',
|
|
112
|
+
'noise canceling headphones approved expense', 'monitor arm request form updated',
|
|
113
|
+
'desk drawer key replacement procedure', 'building security hours extended',
|
|
114
|
+
];
|
|
115
|
+
for (let i = 0; i < noiseTopics.length; i++) {
|
|
116
|
+
entries.push(mem(`noise-${i}`, noiseTopics[i], { created: dateOffset(i), tags: ['topic:office'] }));
|
|
117
|
+
}
|
|
118
|
+
entries.push(mem('nr-1', 'The database connection pool was exhausted because max_connections was set to 10 but the application had 25 concurrent requests', {
|
|
119
|
+
created: dateOffset(20), tags: ['topic:database', 'topic:performance'],
|
|
120
|
+
}), mem('nr-2', 'The S3 bucket policy was misconfigured allowing public read access to customer data uploads', {
|
|
121
|
+
created: dateOffset(21), tags: ['topic:security', 'topic:s3'],
|
|
122
|
+
}));
|
|
123
|
+
cases.push({ id: 'nr-q1', category: 'noise-resistance', query: 'database connection pool exhausted max connections', expectedIds: ['nr-1'], description: 'find DB issue despite 30 noise entries' }, { id: 'nr-q2', category: 'noise-resistance', query: 'S3 bucket public access security misconfiguration', expectedIds: ['nr-2'], description: 'find S3 issue despite noise' }, { id: 'nr-q3', category: 'noise-resistance', query: 'PostgreSQL migration constraint', expectedIds: ['dr-1'], description: 'find earlier DB entry despite noise' }, { id: 'nr-q4', category: 'noise-resistance', query: 'JWT token security expiration', expectedIds: ['dr-5'], description: 'find security entry despite noise' });
|
|
124
|
+
// =========================================================================
|
|
125
|
+
// 6. MULTI-HOP — entity chaining across sessions
|
|
126
|
+
// =========================================================================
|
|
127
|
+
entries.push(mem('mh-1', 'speaker:Alice works on the payment gateway integration with Stripe. She found a webhook signature validation bug.', {
|
|
128
|
+
created: dateOffset(25), tags: ['speaker:Alice', 'topic:payments'],
|
|
129
|
+
}), mem('mh-2', 'speaker:Bob reviewed the Stripe webhook code and confirmed the signature validation uses the wrong secret key from the test environment.', {
|
|
130
|
+
created: dateOffset(26), tags: ['speaker:Bob', 'topic:payments'],
|
|
131
|
+
}), mem('mh-3', 'speaker:Alice fixed the Stripe webhook by switching to the production secret key. Payment confirmations now arrive within 2 seconds.', {
|
|
132
|
+
created: dateOffset(27), tags: ['speaker:Alice', 'topic:payments'],
|
|
133
|
+
}), mem('mh-4', 'speaker:Carol reported that the billing dashboard shows incorrect revenue numbers because it reads from the payments_raw table instead of payments_reconciled.', {
|
|
134
|
+
created: dateOffset(28), tags: ['speaker:Carol', 'topic:billing'],
|
|
135
|
+
}));
|
|
136
|
+
cases.push({ id: 'mh-q1', category: 'multi-hop', query: 'Who fixed the Stripe webhook bug and what was the root cause?', expectedIds: ['mh-1', 'mh-2', 'mh-3'], description: 'chain Alice -> Stripe -> Bob -> fix' }, { id: 'mh-q2', category: 'multi-hop', query: 'What are all the payment-related issues the team discussed?', expectedIds: ['mh-1', 'mh-2', 'mh-3', 'mh-4'], description: 'find all payment topics across speakers' });
|
|
137
|
+
return { entries, cases };
|
|
138
|
+
}
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Runner — evaluates each case against the synthetic corpus
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
export async function runFeatureEval(version) {
|
|
143
|
+
const start = Date.now();
|
|
144
|
+
const { entries, cases } = buildSyntheticCorpus();
|
|
145
|
+
const caseResults = [];
|
|
146
|
+
for (const c of cases) {
|
|
147
|
+
let results;
|
|
148
|
+
if (c.category === 'multi-hop') {
|
|
149
|
+
results = multihopSearch(c.query, entries, { budget: 50000 });
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
results = search(c.query, entries, { budget: 50000 });
|
|
153
|
+
}
|
|
154
|
+
const returnedIds = results.map(r => r.entry.id);
|
|
155
|
+
caseResults.push({
|
|
156
|
+
case: c,
|
|
157
|
+
returnedIds,
|
|
158
|
+
mrrVal: mrr(returnedIds, c.expectedIds),
|
|
159
|
+
r5: recallAtK(returnedIds, c.expectedIds, 5),
|
|
160
|
+
ndcg5: ndcgAtK(returnedIds, c.expectedIds, 5),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
const categories = [...new Set(cases.map(c => c.category))];
|
|
164
|
+
const features = categories.map(cat => {
|
|
165
|
+
const catCases = caseResults.filter(r => r.case.category === cat);
|
|
166
|
+
const n = catCases.length;
|
|
167
|
+
const avgMrr = catCases.reduce((s, r) => s + r.mrrVal, 0) / n;
|
|
168
|
+
const avgR5 = catCases.reduce((s, r) => s + r.r5, 0) / n;
|
|
169
|
+
const avgNdcg5 = catCases.reduce((s, r) => s + r.ndcg5, 0) / n;
|
|
170
|
+
return {
|
|
171
|
+
category: cat,
|
|
172
|
+
cases: n,
|
|
173
|
+
mrr: avgMrr,
|
|
174
|
+
recallAt5: avgR5,
|
|
175
|
+
ndcgAt5: avgNdcg5,
|
|
176
|
+
passed: true,
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
const totalCases = caseResults.length;
|
|
180
|
+
const overallMrr = caseResults.reduce((s, r) => s + r.mrrVal, 0) / totalCases;
|
|
181
|
+
const overallR5 = caseResults.reduce((s, r) => s + r.r5, 0) / totalCases;
|
|
182
|
+
const overallNdcg5 = caseResults.reduce((s, r) => s + r.ndcg5, 0) / totalCases;
|
|
183
|
+
return {
|
|
184
|
+
version,
|
|
185
|
+
timestamp: new Date().toISOString(),
|
|
186
|
+
features,
|
|
187
|
+
overall: { mrr: overallMrr, recallAt5: overallR5, ndcgAt5: overallNdcg5 },
|
|
188
|
+
totalCases,
|
|
189
|
+
durationMs: Date.now() - start,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// Regression detection
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
const REGRESSION_THRESHOLD = 0.05;
|
|
196
|
+
export function detectRegressions(baseline, current) {
|
|
197
|
+
const regressions = [];
|
|
198
|
+
const improvements = [];
|
|
199
|
+
for (const feat of current.features) {
|
|
200
|
+
const base = baseline.features[feat.category];
|
|
201
|
+
if (!base)
|
|
202
|
+
continue;
|
|
203
|
+
for (const metric of ['mrr', 'recallAt5', 'ndcgAt5']) {
|
|
204
|
+
const baseVal = base[metric];
|
|
205
|
+
const curVal = feat[metric];
|
|
206
|
+
const delta = curVal - baseVal;
|
|
207
|
+
if (delta < -REGRESSION_THRESHOLD) {
|
|
208
|
+
regressions.push({ category: feat.category, metric, baseline: baseVal, current: curVal, delta });
|
|
209
|
+
}
|
|
210
|
+
else if (delta > REGRESSION_THRESHOLD) {
|
|
211
|
+
improvements.push({ category: feat.category, metric, baseline: baseVal, current: curVal, delta });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
regressions,
|
|
217
|
+
improvements,
|
|
218
|
+
verdict: regressions.length > 0 ? 'REGRESSION' : 'PASS',
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
export function resultToBaseline(result) {
|
|
222
|
+
const features = {};
|
|
223
|
+
for (const f of result.features) {
|
|
224
|
+
features[f.category] = { mrr: f.mrr, recallAt5: f.recallAt5, ndcgAt5: f.ndcgAt5 };
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
version: result.version,
|
|
228
|
+
timestamp: result.timestamp,
|
|
229
|
+
features,
|
|
230
|
+
overall: result.overall,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// Formatters
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
function pct(n) {
|
|
237
|
+
return (n * 100).toFixed(1) + '%';
|
|
238
|
+
}
|
|
239
|
+
function pad(s, w) {
|
|
240
|
+
return s.padEnd(w);
|
|
241
|
+
}
|
|
242
|
+
function deltaStr(d) {
|
|
243
|
+
const sign = d >= 0 ? '+' : '';
|
|
244
|
+
return sign + pct(d);
|
|
245
|
+
}
|
|
246
|
+
export function formatResult(result, baseline) {
|
|
247
|
+
const lines = [];
|
|
248
|
+
lines.push(`Hippo Eval v${result.version} — ${result.totalCases} queries, ${result.durationMs}ms`);
|
|
249
|
+
lines.push('');
|
|
250
|
+
lines.push(`${pad('Feature', 24)} | ${pad('MRR', 8)} | ${pad('R@5', 8)} | ${pad('NDCG@5', 8)} | ${pad('vs baseline', 12)} | verdict`);
|
|
251
|
+
lines.push(`${'─'.repeat(24)}-|-${'─'.repeat(8)}-|-${'─'.repeat(8)}-|-${'─'.repeat(8)}-|-${'─'.repeat(12)}-|${'─'.repeat(8)}`);
|
|
252
|
+
let report;
|
|
253
|
+
if (baseline) {
|
|
254
|
+
report = detectRegressions(baseline, result);
|
|
255
|
+
}
|
|
256
|
+
for (const feat of result.features) {
|
|
257
|
+
const base = baseline?.features[feat.category];
|
|
258
|
+
let vsBaseline = 'NEW';
|
|
259
|
+
let verdict = 'baseline';
|
|
260
|
+
if (base) {
|
|
261
|
+
const delta = feat.ndcgAt5 - base.ndcgAt5;
|
|
262
|
+
vsBaseline = deltaStr(delta);
|
|
263
|
+
verdict = delta < -REGRESSION_THRESHOLD ? 'REGRESS' : 'PASS';
|
|
264
|
+
}
|
|
265
|
+
lines.push(`${pad(feat.category, 24)} | ${pad(pct(feat.mrr), 8)} | ${pad(pct(feat.recallAt5), 8)} | ${pad(pct(feat.ndcgAt5), 8)} | ${pad(vsBaseline, 12)} | ${verdict}`);
|
|
266
|
+
}
|
|
267
|
+
lines.push('');
|
|
268
|
+
lines.push(`Overall: MRR ${pct(result.overall.mrr)} | R@5 ${pct(result.overall.recallAt5)} | NDCG@5 ${pct(result.overall.ndcgAt5)}`);
|
|
269
|
+
if (report) {
|
|
270
|
+
if (report.verdict === 'REGRESSION') {
|
|
271
|
+
lines.push('');
|
|
272
|
+
lines.push(`REGRESSIONS DETECTED (>${pct(REGRESSION_THRESHOLD)} drop):`);
|
|
273
|
+
for (const r of report.regressions) {
|
|
274
|
+
lines.push(` ${r.category}.${r.metric}: ${pct(r.baseline)} -> ${pct(r.current)} (${deltaStr(r.delta)})`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (report.improvements.length > 0) {
|
|
278
|
+
lines.push('');
|
|
279
|
+
lines.push('Improvements:');
|
|
280
|
+
for (const imp of report.improvements) {
|
|
281
|
+
lines.push(` ${imp.category}.${imp.metric}: ${pct(imp.baseline)} -> ${pct(imp.current)} (${deltaStr(imp.delta)})`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
lines.push('');
|
|
285
|
+
lines.push(`Verdict: ${report.verdict}${report.regressions.length > 0 ? ` (${report.regressions.length} regressions)` : ''}`);
|
|
286
|
+
}
|
|
287
|
+
return lines.join('\n');
|
|
288
|
+
}
|
|
289
|
+
//# sourceMappingURL=eval-suite.js.map
|