@prometheus-ai/memory 0.5.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 +107 -0
- package/dist/types/cli.d.ts +35 -0
- package/dist/types/config.d.ts +77 -0
- package/dist/types/core/aaak.d.ts +55 -0
- package/dist/types/core/annotations.d.ts +75 -0
- package/dist/types/core/banks.d.ts +33 -0
- package/dist/types/core/beam/consolidate.d.ts +32 -0
- package/dist/types/core/beam/helpers.d.ts +76 -0
- package/dist/types/core/beam/index.d.ts +59 -0
- package/dist/types/core/beam/recall.d.ts +32 -0
- package/dist/types/core/beam/schema.d.ts +2 -0
- package/dist/types/core/beam/store.d.ts +35 -0
- package/dist/types/core/beam/types.d.ts +233 -0
- package/dist/types/core/binary-vectors.d.ts +54 -0
- package/dist/types/core/chat-normalize.d.ts +13 -0
- package/dist/types/core/content-sanitizer.d.ts +18 -0
- package/dist/types/core/cost-log.d.ts +13 -0
- package/dist/types/core/embeddings.d.ts +44 -0
- package/dist/types/core/entities.d.ts +7 -0
- package/dist/types/core/episodic-graph.d.ts +89 -0
- package/dist/types/core/extraction/client.d.ts +31 -0
- package/dist/types/core/extraction/diagnostics.d.ts +51 -0
- package/dist/types/core/extraction/prompts.d.ts +2 -0
- package/dist/types/core/extraction.d.ts +6 -0
- package/dist/types/core/index.d.ts +4 -0
- package/dist/types/core/llm-backends.d.ts +21 -0
- package/dist/types/core/local-llm.d.ts +15 -0
- package/dist/types/core/memory.d.ts +160 -0
- package/dist/types/core/migrations/e6-triplestore-split.d.ts +17 -0
- package/dist/types/core/migrations/index.d.ts +1 -0
- package/dist/types/core/mmr.d.ts +8 -0
- package/dist/types/core/orchestrator.d.ts +20 -0
- package/dist/types/core/patterns.d.ts +61 -0
- package/dist/types/core/plugins.d.ts +109 -0
- package/dist/types/core/polyphonic-recall.d.ts +66 -0
- package/dist/types/core/query-cache.d.ts +46 -0
- package/dist/types/core/query-intent.d.ts +20 -0
- package/dist/types/core/recall-diagnostics.d.ts +48 -0
- package/dist/types/core/runtime-options.d.ts +68 -0
- package/dist/types/core/shmr.d.ts +56 -0
- package/dist/types/core/streaming.d.ts +136 -0
- package/dist/types/core/synonyms.d.ts +46 -0
- package/dist/types/core/temporal-parser.d.ts +16 -0
- package/dist/types/core/token-counter.d.ts +8 -0
- package/dist/types/core/triples.d.ts +63 -0
- package/dist/types/core/typed-memory.d.ts +39 -0
- package/dist/types/core/vector-math.d.ts +1 -0
- package/dist/types/core/veracity-consolidation.d.ts +60 -0
- package/dist/types/core/weibull.d.ts +96 -0
- package/dist/types/db.d.ts +16 -0
- package/dist/types/diagnose.d.ts +24 -0
- package/dist/types/dr/index.d.ts +1 -0
- package/dist/types/dr/recovery.d.ts +68 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/mcp-server.d.ts +40 -0
- package/dist/types/mcp-tools.d.ts +484 -0
- package/dist/types/migrations/e6-triplestore-split.d.ts +1 -0
- package/dist/types/migrations/index.d.ts +1 -0
- package/dist/types/types.d.ts +145 -0
- package/dist/types/util/datetime.d.ts +8 -0
- package/dist/types/util/env.d.ts +10 -0
- package/dist/types/util/ids.d.ts +3 -0
- package/dist/types/util/lru.d.ts +12 -0
- package/dist/types/util/regex.d.ts +10 -0
- package/package.json +85 -0
- package/src/cli.ts +398 -0
- package/src/config.ts +326 -0
- package/src/core/aaak.ts +142 -0
- package/src/core/annotations.ts +457 -0
- package/src/core/banks.ts +133 -0
- package/src/core/beam/consolidate.ts +965 -0
- package/src/core/beam/helpers.ts +977 -0
- package/src/core/beam/index.ts +353 -0
- package/src/core/beam/recall.ts +1100 -0
- package/src/core/beam/schema.ts +423 -0
- package/src/core/beam/store.ts +829 -0
- package/src/core/beam/types.ts +268 -0
- package/src/core/binary-vectors.ts +317 -0
- package/src/core/chat-normalize.ts +160 -0
- package/src/core/content-sanitizer.ts +136 -0
- package/src/core/cost-log.ts +103 -0
- package/src/core/embeddings.ts +423 -0
- package/src/core/entities.ts +259 -0
- package/src/core/episodic-graph.ts +708 -0
- package/src/core/extraction/client.ts +162 -0
- package/src/core/extraction/diagnostics.ts +193 -0
- package/src/core/extraction/prompts.ts +31 -0
- package/src/core/extraction.ts +335 -0
- package/src/core/index.ts +30 -0
- package/src/core/llm-backends.ts +51 -0
- package/src/core/local-llm.ts +436 -0
- package/src/core/memory.ts +630 -0
- package/src/core/migrations/e6-triplestore-split.ts +211 -0
- package/src/core/migrations/index.ts +1 -0
- package/src/core/mmr.ts +71 -0
- package/src/core/orchestrator.ts +62 -0
- package/src/core/patterns.ts +484 -0
- package/src/core/plugins.ts +375 -0
- package/src/core/polyphonic-recall.ts +563 -0
- package/src/core/query-cache.ts +354 -0
- package/src/core/query-intent.ts +139 -0
- package/src/core/recall-diagnostics.ts +157 -0
- package/src/core/runtime-options.ts +119 -0
- package/src/core/shmr.ts +460 -0
- package/src/core/streaming.ts +419 -0
- package/src/core/synonyms.ts +197 -0
- package/src/core/temporal-parser.ts +363 -0
- package/src/core/token-counter.ts +30 -0
- package/src/core/triples.ts +454 -0
- package/src/core/typed-memory.ts +407 -0
- package/src/core/vector-math.ts +23 -0
- package/src/core/veracity-consolidation.ts +477 -0
- package/src/core/weibull.ts +124 -0
- package/src/db.ts +128 -0
- package/src/diagnose.ts +174 -0
- package/src/dr/index.ts +1 -0
- package/src/dr/recovery.ts +405 -0
- package/src/index.ts +33 -0
- package/src/mcp-server.ts +155 -0
- package/src/mcp-tools.ts +970 -0
- package/src/migrations/e6-triplestore-split.ts +1 -0
- package/src/migrations/index.ts +1 -0
- package/src/types.ts +157 -0
- package/src/util/datetime.ts +69 -0
- package/src/util/env.ts +65 -0
- package/src/util/ids.ts +19 -0
- package/src/util/lru.ts +48 -0
- package/src/util/regex.ts +165 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import {
|
|
3
|
+
$env,
|
|
4
|
+
$flag,
|
|
5
|
+
APP_DISPLAY_NAME,
|
|
6
|
+
extractHttpStatusFromError,
|
|
7
|
+
fetchWithRetry,
|
|
8
|
+
getFastembedCacheDir,
|
|
9
|
+
logger,
|
|
10
|
+
} from "@prometheus-ai/utils";
|
|
11
|
+
import type { EmbeddingModel } from "fastembed";
|
|
12
|
+
import { LRUCache } from "lru-cache/raw";
|
|
13
|
+
import packageJson from "../../package.json" with { type: "json" };
|
|
14
|
+
import { type EmbeddingOutput, getMnemopiRuntimeOptions, resolveEmbeddingProvider } from "./runtime-options";
|
|
15
|
+
|
|
16
|
+
export type { EmbeddingOutput } from "./runtime-options";
|
|
17
|
+
export { cosineSimilarity } from "./vector-math";
|
|
18
|
+
|
|
19
|
+
export type Vector = Float32Array;
|
|
20
|
+
export type EmbeddingMatrix = Vector[];
|
|
21
|
+
|
|
22
|
+
export interface EmbeddingProvider {
|
|
23
|
+
embed(texts: readonly string[]): EmbeddingOutput | Promise<EmbeddingOutput>;
|
|
24
|
+
available?(): boolean | Promise<boolean>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type StandardEmbeddingModel = Exclude<EmbeddingModel, EmbeddingModel.CUSTOM>;
|
|
28
|
+
|
|
29
|
+
interface LocalEmbeddingModel {
|
|
30
|
+
embed(texts: string[], batchSize?: number): EmbeddingOutput;
|
|
31
|
+
queryEmbed?(query: string): Promise<number[]>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type LocalModelInitOptions = {
|
|
35
|
+
model: StandardEmbeddingModel;
|
|
36
|
+
cacheDir?: string;
|
|
37
|
+
showDownloadProgress?: boolean;
|
|
38
|
+
};
|
|
39
|
+
type LocalModelInitializer = (options: LocalModelInitOptions) => Promise<LocalEmbeddingModel>;
|
|
40
|
+
|
|
41
|
+
const QUERY_CACHE_MAX = 512;
|
|
42
|
+
|
|
43
|
+
let providerOverride: EmbeddingProvider | null = null;
|
|
44
|
+
let localModelPromise: Promise<LocalEmbeddingModel> | null = null;
|
|
45
|
+
let localModelInitializer: LocalModelInitializer = defaultLocalModelInitializer;
|
|
46
|
+
let apiCallCount = 0;
|
|
47
|
+
const queryCache = new LRUCache<string, Vector>({ max: QUERY_CACHE_MAX });
|
|
48
|
+
|
|
49
|
+
// Provider identity table for the cache key. Each unique `provider` object/function
|
|
50
|
+
// (configured via `withMnemopiRuntimeOptions`) gets a stable integer id so the cache
|
|
51
|
+
// scope reflects the runtime's actual embedding source. Two Mnemopi instances in the
|
|
52
|
+
// same process using different providers/models hash to disjoint keys and never
|
|
53
|
+
// collide on the same query text. `0` is the sentinel for "env-default fallback".
|
|
54
|
+
const providerIds = new WeakMap<object, number>();
|
|
55
|
+
let nextProviderId = 1;
|
|
56
|
+
|
|
57
|
+
async function defaultLocalModelInitializer(options: LocalModelInitOptions): Promise<LocalEmbeddingModel> {
|
|
58
|
+
// Preload ORT 1.24 before fastembed's bundled ORT 1.21 — only on Windows,
|
|
59
|
+
// where loading the older binding first triggers a DLL-reuse crash. The 1.24
|
|
60
|
+
// line also has no darwin/x64 prebuilt, so importing it unconditionally breaks
|
|
61
|
+
// the darwin-x64 `bun build --compile` (Bun folds process.platform/arch and
|
|
62
|
+
// fails to resolve a binding that doesn't ship). The `win32` literal guard is
|
|
63
|
+
// statically foldable, so Bun dead-code-eliminates this import on every
|
|
64
|
+
// non-Windows target; fastembed loads its own ORT 1.21 binding there.
|
|
65
|
+
if (process.platform === "win32") {
|
|
66
|
+
await import("onnxruntime-node");
|
|
67
|
+
}
|
|
68
|
+
const { FlagEmbedding } = await import("fastembed");
|
|
69
|
+
return FlagEmbedding.init(options);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function activeEmbeddingOptions() {
|
|
73
|
+
return getMnemopiRuntimeOptions()?.embeddings;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Compose the per-query cache key. Includes the active provider's identity, the
|
|
78
|
+
* resolved model name, and the API base URL so two `Mnemopi` instances in the same
|
|
79
|
+
* process that point at different providers/models never share a cached query
|
|
80
|
+
* vector. Provider identity comes from `providerIds` (WeakMap-assigned integer);
|
|
81
|
+
* `0` is the sentinel for "no provider configured, fall back to env defaults".
|
|
82
|
+
*/
|
|
83
|
+
function queryCacheKey(text: string): string {
|
|
84
|
+
const active = activeEmbeddingOptions();
|
|
85
|
+
const provider = active?.provider as object | undefined;
|
|
86
|
+
let providerId = 0;
|
|
87
|
+
if (provider !== undefined) {
|
|
88
|
+
const existing = providerIds.get(provider);
|
|
89
|
+
if (existing === undefined) {
|
|
90
|
+
providerId = nextProviderId++;
|
|
91
|
+
providerIds.set(provider, providerId);
|
|
92
|
+
} else {
|
|
93
|
+
providerId = existing;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const model = defaultModel();
|
|
97
|
+
const apiUrl = active?.apiUrl ?? "";
|
|
98
|
+
return `${providerId}::${model}::${apiUrl}::${text}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function inTestRuntime(): boolean {
|
|
102
|
+
return $env.NODE_ENV === "test" || $env.BUN_ENV === "test";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function embeddingsDisabled(): boolean {
|
|
106
|
+
const active = activeEmbeddingOptions();
|
|
107
|
+
if (active?.disabled !== undefined) {
|
|
108
|
+
return active.disabled;
|
|
109
|
+
}
|
|
110
|
+
return $flag("PROMETHEUS_MEMORY_NO_EMBEDDINGS");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function embeddingApiKey(): string {
|
|
114
|
+
const active = activeEmbeddingOptions();
|
|
115
|
+
if (active?.apiKey !== undefined) {
|
|
116
|
+
return active.apiKey;
|
|
117
|
+
}
|
|
118
|
+
return $env.PROMETHEUS_MEMORY_EMBEDDING_API_KEY || $env.OPENROUTER_API_KEY || $env.OPENAI_API_KEY || "";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function embeddingBaseUrl(): string {
|
|
122
|
+
const active = activeEmbeddingOptions();
|
|
123
|
+
if (active?.apiUrl !== undefined) {
|
|
124
|
+
return active.apiUrl;
|
|
125
|
+
}
|
|
126
|
+
return $env.PROMETHEUS_MEMORY_EMBEDDING_API_URL || $env.OPENROUTER_BASE_URL || "https://openrouter.ai/api/v1";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function defaultModel(): string {
|
|
130
|
+
const active = activeEmbeddingOptions();
|
|
131
|
+
if (active?.model !== undefined) {
|
|
132
|
+
return active.model;
|
|
133
|
+
}
|
|
134
|
+
return $env.PROMETHEUS_MEMORY_EMBEDDING_MODEL || "BAAI/bge-small-en-v1.5";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Resolve the embedding model name for the currently active runtime scope.
|
|
139
|
+
*
|
|
140
|
+
* Reads (in order): the active provider's `model` from `withMnemopiRuntimeOptions`,
|
|
141
|
+
* the `PROMETHEUS_MEMORY_EMBEDDING_MODEL` env var, then the bundled fastembed default. Stored
|
|
142
|
+
* alongside each row in `memory_embeddings.model` so migrations can re-embed when
|
|
143
|
+
* the active model changes.
|
|
144
|
+
*/
|
|
145
|
+
export function currentEmbeddingModel(): string {
|
|
146
|
+
return defaultModel();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function isApiModel(modelName: string): boolean {
|
|
150
|
+
if (
|
|
151
|
+
modelName.startsWith("openai/") ||
|
|
152
|
+
modelName.includes("text-embedding") ||
|
|
153
|
+
modelName.startsWith("text-embedding")
|
|
154
|
+
) {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
const active = activeEmbeddingOptions();
|
|
158
|
+
const baseUrl = active?.apiUrl ?? ($env.PROMETHEUS_MEMORY_EMBEDDING_API_URL || $env.OPENROUTER_BASE_URL);
|
|
159
|
+
if (baseUrl !== undefined && baseUrl !== "" && !baseUrl.includes("openrouter.ai")) {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
return $flag("PROMETHEUS_MEMORY_EMBEDDINGS_VIA_API");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const MODEL_DIMS: Record<string, number> = {
|
|
166
|
+
"BAAI/bge-small-en-v1.5": 384,
|
|
167
|
+
"BAAI/bge-base-en-v1.5": 768,
|
|
168
|
+
"BAAI/bge-large-en-v1.5": 1024,
|
|
169
|
+
"BAAI/bge-small-zh-v1.5": 512,
|
|
170
|
+
"BAAI/bge-base-zh-v1.5": 768,
|
|
171
|
+
"BAAI/bge-large-zh-v1.5": 1024,
|
|
172
|
+
"intfloat/multilingual-e5-small": 384,
|
|
173
|
+
"intfloat/multilingual-e5-base": 768,
|
|
174
|
+
"intfloat/multilingual-e5-large": 1024,
|
|
175
|
+
"BAAI/bge-m3": 1024,
|
|
176
|
+
"BAAI/bge-multilingual-gemma2": 3584,
|
|
177
|
+
"openai/text-embedding-3-small": 1536,
|
|
178
|
+
"openai/text-embedding-3-large": 3072,
|
|
179
|
+
"text-embedding-3-small": 1536,
|
|
180
|
+
"text-embedding-3-large": 3072,
|
|
181
|
+
"jina-embeddings-v5-omni-nano": 768,
|
|
182
|
+
"jina-embeddings-v5-omni-small": 1024,
|
|
183
|
+
};
|
|
184
|
+
export function embeddingDimFor(modelName: string): number {
|
|
185
|
+
const override = Number.parseInt($env.PROMETHEUS_MEMORY_EMBEDDING_DIM ?? "", 10);
|
|
186
|
+
if (Number.isFinite(override)) {
|
|
187
|
+
return override;
|
|
188
|
+
}
|
|
189
|
+
return MODEL_DIMS[modelName] ?? 384;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Drain an embedding stream (a custom provider or fastembed) into a `Float32Array` matrix. */
|
|
193
|
+
async function collectMatrix(batches: EmbeddingOutput): Promise<EmbeddingMatrix> {
|
|
194
|
+
const rows: Vector[] = [];
|
|
195
|
+
for await (const batch of batches) {
|
|
196
|
+
for (const row of batch) {
|
|
197
|
+
rows.push(new Float32Array(row));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return rows;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const KNOWN_MODEL_NAMES: Record<string, string> = {
|
|
204
|
+
"BAAI/bge-small-en-v1.5": "fast-bge-small-en-v1.5",
|
|
205
|
+
"BAAI/bge-base-en-v1.5": "fast-bge-base-en-v1.5",
|
|
206
|
+
"BAAI/bge-small-en": "fast-bge-small-en",
|
|
207
|
+
"BAAI/bge-base-en": "fast-bge-base-en",
|
|
208
|
+
"BAAI/bge-small-zh-v1.5": "fast-bge-small-zh-v1.5",
|
|
209
|
+
"intfloat/multilingual-e5-large": "fast-multilingual-e5-large",
|
|
210
|
+
"sentence-transformers/all-MiniLM-L6-v2": "fast-all-MiniLM-L6-v2",
|
|
211
|
+
};
|
|
212
|
+
function fastembedModelName(modelName: string): StandardEmbeddingModel | null {
|
|
213
|
+
// Fastembed `EmbeddingModel` enum string values, inlined so resolving a model name
|
|
214
|
+
// (and `available()`) never imports `fastembed` — its module eagerly loads the
|
|
215
|
+
// `onnxruntime-node` native addon, which segfaults in some runtimes.
|
|
216
|
+
const id = KNOWN_MODEL_NAMES[modelName];
|
|
217
|
+
return id === undefined ? null : (id as StandardEmbeddingModel);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function getLocalModel(): Promise<LocalEmbeddingModel | null> {
|
|
221
|
+
if (isApiModel(defaultModel()) || embeddingsDisabled() || inTestRuntime()) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
if (localModelPromise !== null) {
|
|
225
|
+
return localModelPromise;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const modelName = fastembedModelName(defaultModel());
|
|
229
|
+
if (modelName === null) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
const cacheDir = getFastembedCacheDir();
|
|
233
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
234
|
+
const loading = localModelInitializer({
|
|
235
|
+
model: modelName,
|
|
236
|
+
cacheDir,
|
|
237
|
+
showDownloadProgress: false,
|
|
238
|
+
});
|
|
239
|
+
localModelPromise = loading;
|
|
240
|
+
try {
|
|
241
|
+
return await loading;
|
|
242
|
+
} catch {
|
|
243
|
+
if (localModelPromise === loading) localModelPromise = null;
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function embedApi(texts: readonly string[]): Promise<EmbeddingMatrix | null> {
|
|
249
|
+
const baseUrl = embeddingBaseUrl();
|
|
250
|
+
const isCustom = !baseUrl.includes("openrouter.ai");
|
|
251
|
+
const apiKey = embeddingApiKey();
|
|
252
|
+
if (!isCustom && apiKey === "") {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const headers: Record<string, string> = {
|
|
257
|
+
"Content-Type": "application/json",
|
|
258
|
+
"User-Agent": `${APP_DISPLAY_NAME}/${packageJson.version}`,
|
|
259
|
+
"HTTP-Referer": "https://prometheus.trivlab.com/",
|
|
260
|
+
"X-OpenRouter-Title": APP_DISPLAY_NAME,
|
|
261
|
+
"X-OpenRouter-Categories": "cli-agent",
|
|
262
|
+
};
|
|
263
|
+
if (apiKey !== "") {
|
|
264
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
const response = await fetchWithRetry(`${baseUrl.replace(/\/+$/, "")}/embeddings`, {
|
|
269
|
+
method: "POST",
|
|
270
|
+
headers,
|
|
271
|
+
body: JSON.stringify({ model: defaultModel(), input: texts }),
|
|
272
|
+
signal: AbortSignal.timeout(30000),
|
|
273
|
+
maxAttempts: 3,
|
|
274
|
+
defaultDelayMs: attempt => 2 ** attempt * 1000,
|
|
275
|
+
});
|
|
276
|
+
if (!response.ok) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
const { data: rows } = (await response.json()) as { data?: Array<{ embedding: number[] }> };
|
|
280
|
+
if (rows === undefined) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
apiCallCount += 1;
|
|
284
|
+
return rows.map(row => new Float32Array(row.embedding));
|
|
285
|
+
} catch (error) {
|
|
286
|
+
logger.debug("mnemopi embedding request failed", { status: extractHttpStatusFromError(error) });
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function providerAvailable(provider: EmbeddingProvider): Promise<boolean> {
|
|
292
|
+
if (provider.available === undefined) {
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
return await provider.available();
|
|
297
|
+
} catch {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function setEmbeddingProviderForTests(provider: EmbeddingProvider | null | undefined): void {
|
|
303
|
+
providerOverride = provider ?? null;
|
|
304
|
+
queryCache.clear();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export const setEmbeddingProvider = setEmbeddingProviderForTests;
|
|
308
|
+
|
|
309
|
+
export function setLocalModelInitializerForTests(initializer: LocalModelInitializer | null | undefined): void {
|
|
310
|
+
localModelInitializer = initializer ?? defaultLocalModelInitializer;
|
|
311
|
+
localModelPromise = null;
|
|
312
|
+
queryCache.clear();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function resetEmbeddingProviderForTests(): void {
|
|
316
|
+
providerOverride = null;
|
|
317
|
+
localModelPromise = null;
|
|
318
|
+
localModelInitializer = defaultLocalModelInitializer;
|
|
319
|
+
apiCallCount = 0;
|
|
320
|
+
queryCache.clear();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export const resetEmbeddingStateForTests = resetEmbeddingProviderForTests;
|
|
324
|
+
|
|
325
|
+
export async function available(): Promise<boolean> {
|
|
326
|
+
if (embeddingsDisabled()) {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
const active = activeEmbeddingOptions();
|
|
330
|
+
const activeProvider = resolveEmbeddingProvider(active?.provider);
|
|
331
|
+
if (activeProvider !== undefined) {
|
|
332
|
+
return providerAvailable(activeProvider);
|
|
333
|
+
}
|
|
334
|
+
if (providerOverride !== null) {
|
|
335
|
+
return providerAvailable(providerOverride);
|
|
336
|
+
}
|
|
337
|
+
if (isApiModel(defaultModel())) {
|
|
338
|
+
const baseUrl = active?.apiUrl ?? ($env.PROMETHEUS_MEMORY_EMBEDDING_API_URL || $env.OPENROUTER_BASE_URL);
|
|
339
|
+
if (baseUrl !== undefined && baseUrl !== "" && !baseUrl.includes("openrouter.ai")) {
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
return embeddingApiKey() !== "";
|
|
343
|
+
}
|
|
344
|
+
if (inTestRuntime()) {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
return fastembedModelName(defaultModel()) !== null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function availableApi(): boolean {
|
|
351
|
+
return embeddingApiKey() !== "";
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export async function embedQuery(text: string): Promise<Vector | null> {
|
|
355
|
+
if (text === "" || embeddingsDisabled()) {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
const key = queryCacheKey(text);
|
|
359
|
+
const cached = queryCache.get(key);
|
|
360
|
+
if (cached !== undefined) {
|
|
361
|
+
return cached;
|
|
362
|
+
}
|
|
363
|
+
const vectors = await embed([text]);
|
|
364
|
+
const vector = vectors?.[0] ?? null;
|
|
365
|
+
if (vector !== null) {
|
|
366
|
+
queryCache.set(key, vector);
|
|
367
|
+
}
|
|
368
|
+
return vector;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export async function embed(texts: readonly string[]): Promise<EmbeddingMatrix | null> {
|
|
372
|
+
if (texts.length === 0 || embeddingsDisabled()) {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
const activeProvider = resolveEmbeddingProvider(activeEmbeddingOptions()?.provider);
|
|
376
|
+
if (activeProvider !== undefined) {
|
|
377
|
+
try {
|
|
378
|
+
return await collectMatrix(await activeProvider.embed(texts));
|
|
379
|
+
} catch {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (providerOverride !== null) {
|
|
384
|
+
try {
|
|
385
|
+
return await collectMatrix(await providerOverride.embed(texts));
|
|
386
|
+
} catch {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (isApiModel(defaultModel())) {
|
|
391
|
+
return embedApi(texts);
|
|
392
|
+
}
|
|
393
|
+
if (texts.length === 1) {
|
|
394
|
+
const key = queryCacheKey(texts[0] ?? "");
|
|
395
|
+
const cached = queryCache.get(key);
|
|
396
|
+
if (cached !== undefined) {
|
|
397
|
+
return [cached];
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const model = await getLocalModel();
|
|
401
|
+
if (model === null) {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
try {
|
|
405
|
+
const vectors = await collectMatrix(model.embed([...texts]));
|
|
406
|
+
if (vectors.length === 1) {
|
|
407
|
+
const vector = vectors[0];
|
|
408
|
+
if (vector !== undefined) {
|
|
409
|
+
queryCache.set(queryCacheKey(texts[0] ?? ""), vector);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return vectors;
|
|
413
|
+
} catch {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function getEmbeddingApiCallCountForTests(): number {
|
|
419
|
+
return apiCallCount;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export const DEFAULT_MODEL = defaultModel();
|
|
423
|
+
export const EMBEDDING_DIM = embeddingDimFor(DEFAULT_MODEL);
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
const ENTITY_EXTRACTION_STOP_WORD_VALUES = [
|
|
2
|
+
"the",
|
|
3
|
+
"a",
|
|
4
|
+
"an",
|
|
5
|
+
"and",
|
|
6
|
+
"or",
|
|
7
|
+
"but",
|
|
8
|
+
"in",
|
|
9
|
+
"on",
|
|
10
|
+
"at",
|
|
11
|
+
"to",
|
|
12
|
+
"for",
|
|
13
|
+
"of",
|
|
14
|
+
"with",
|
|
15
|
+
"by",
|
|
16
|
+
"from",
|
|
17
|
+
"as",
|
|
18
|
+
"is",
|
|
19
|
+
"was",
|
|
20
|
+
"are",
|
|
21
|
+
"were",
|
|
22
|
+
"be",
|
|
23
|
+
"been",
|
|
24
|
+
"being",
|
|
25
|
+
"have",
|
|
26
|
+
"has",
|
|
27
|
+
"had",
|
|
28
|
+
"do",
|
|
29
|
+
"does",
|
|
30
|
+
"did",
|
|
31
|
+
"will",
|
|
32
|
+
"would",
|
|
33
|
+
"could",
|
|
34
|
+
"should",
|
|
35
|
+
"may",
|
|
36
|
+
"might",
|
|
37
|
+
"can",
|
|
38
|
+
"shall",
|
|
39
|
+
"i",
|
|
40
|
+
"you",
|
|
41
|
+
"he",
|
|
42
|
+
"she",
|
|
43
|
+
"it",
|
|
44
|
+
"we",
|
|
45
|
+
"they",
|
|
46
|
+
"me",
|
|
47
|
+
"him",
|
|
48
|
+
"her",
|
|
49
|
+
"us",
|
|
50
|
+
"them",
|
|
51
|
+
"my",
|
|
52
|
+
"your",
|
|
53
|
+
"his",
|
|
54
|
+
"its",
|
|
55
|
+
"our",
|
|
56
|
+
"their",
|
|
57
|
+
"this",
|
|
58
|
+
"that",
|
|
59
|
+
"these",
|
|
60
|
+
"those",
|
|
61
|
+
"here",
|
|
62
|
+
"there",
|
|
63
|
+
"where",
|
|
64
|
+
"when",
|
|
65
|
+
"what",
|
|
66
|
+
"which",
|
|
67
|
+
"who",
|
|
68
|
+
"whom",
|
|
69
|
+
"whose",
|
|
70
|
+
"how",
|
|
71
|
+
"why",
|
|
72
|
+
"assistant",
|
|
73
|
+
"user",
|
|
74
|
+
"skill",
|
|
75
|
+
"review",
|
|
76
|
+
"target",
|
|
77
|
+
"class",
|
|
78
|
+
"level",
|
|
79
|
+
"signals",
|
|
80
|
+
"phase",
|
|
81
|
+
"api",
|
|
82
|
+
"pi",
|
|
83
|
+
"summary",
|
|
84
|
+
"added",
|
|
85
|
+
"active",
|
|
86
|
+
"not",
|
|
87
|
+
"whether",
|
|
88
|
+
"all",
|
|
89
|
+
"no",
|
|
90
|
+
"replying",
|
|
91
|
+
"ai",
|
|
92
|
+
"memory",
|
|
93
|
+
"conversation",
|
|
94
|
+
"fact",
|
|
95
|
+
"false",
|
|
96
|
+
"true",
|
|
97
|
+
"none",
|
|
98
|
+
"null",
|
|
99
|
+
"signal",
|
|
100
|
+
"hermes",
|
|
101
|
+
"agent",
|
|
102
|
+
"model",
|
|
103
|
+
"system",
|
|
104
|
+
"note",
|
|
105
|
+
"task",
|
|
106
|
+
"project",
|
|
107
|
+
"result",
|
|
108
|
+
"output",
|
|
109
|
+
"input",
|
|
110
|
+
"data",
|
|
111
|
+
"step",
|
|
112
|
+
"process",
|
|
113
|
+
"point",
|
|
114
|
+
"way",
|
|
115
|
+
"thing",
|
|
116
|
+
"time",
|
|
117
|
+
"work",
|
|
118
|
+
] as const;
|
|
119
|
+
|
|
120
|
+
export const ENTITY_EXTRACTION_STOP_WORDS: ReadonlySet<string> = new Set(ENTITY_EXTRACTION_STOP_WORD_VALUES);
|
|
121
|
+
const ENTITY_PATTERNS: readonly RegExp[] = [
|
|
122
|
+
/@(\w{2,30})/g,
|
|
123
|
+
/#(\w{2,30})/g,
|
|
124
|
+
/"([^"]{2,50})"/g,
|
|
125
|
+
/'([^']{2,50})'/g,
|
|
126
|
+
/\b([A-Z][a-zA-Z]*(?:\s+[A-Z][a-zA-Z]*){1,4})\b/g,
|
|
127
|
+
/\b([A-Z][a-zA-Z]{1,20})\b/g,
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
function chars(value: string): string[] {
|
|
131
|
+
return Array.from(value);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function levenshteinDistance(s1: string, s2: string): number {
|
|
135
|
+
let left = chars(s1);
|
|
136
|
+
let right = chars(s2);
|
|
137
|
+
if (left.length < right.length) {
|
|
138
|
+
const tmp = left;
|
|
139
|
+
left = right;
|
|
140
|
+
right = tmp;
|
|
141
|
+
}
|
|
142
|
+
if (right.length === 0) return left.length;
|
|
143
|
+
|
|
144
|
+
let previousRow = new Array<number>(right.length + 1);
|
|
145
|
+
let currentRow = new Array<number>(right.length + 1).fill(0);
|
|
146
|
+
for (let i = 0; i <= right.length; i++) previousRow[i] = i;
|
|
147
|
+
|
|
148
|
+
for (let i = 0; i < left.length; i++) {
|
|
149
|
+
currentRow[0] = i + 1;
|
|
150
|
+
const c1 = left[i];
|
|
151
|
+
for (let j = 0; j < right.length; j++) {
|
|
152
|
+
const insertions = (previousRow[j + 1] ?? 0) + 1;
|
|
153
|
+
const deletions = (currentRow[j] ?? 0) + 1;
|
|
154
|
+
const substitutions = (previousRow[j] ?? 0) + (c1 === right[j] ? 0 : 1);
|
|
155
|
+
currentRow[j + 1] = Math.min(insertions, deletions, substitutions);
|
|
156
|
+
}
|
|
157
|
+
const tmp = previousRow;
|
|
158
|
+
previousRow = currentRow;
|
|
159
|
+
currentRow = tmp;
|
|
160
|
+
}
|
|
161
|
+
return previousRow[right.length] ?? 0;
|
|
162
|
+
}
|
|
163
|
+
export function similarity(s1: string, s2: string): number {
|
|
164
|
+
const s1Lower = s1.toLowerCase().trim();
|
|
165
|
+
const s2Lower = s2.toLowerCase().trim();
|
|
166
|
+
if (s1Lower === s2Lower) return 1.0;
|
|
167
|
+
|
|
168
|
+
const maxLen = Math.max(chars(s1Lower).length, chars(s2Lower).length);
|
|
169
|
+
if (maxLen === 0) return 1.0;
|
|
170
|
+
|
|
171
|
+
if (s1Lower.startsWith(s2Lower) || s2Lower.startsWith(s1Lower)) {
|
|
172
|
+
const longer = Math.max(chars(s1Lower).length, chars(s2Lower).length);
|
|
173
|
+
const shorter = Math.min(chars(s1Lower).length, chars(s2Lower).length);
|
|
174
|
+
if (shorter / longer < 0.3) return 0.0;
|
|
175
|
+
return 0.7 + (shorter / longer) * 0.3;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (s1Lower.includes(s2Lower) || s2Lower.includes(s1Lower)) {
|
|
179
|
+
const longer = Math.max(chars(s1Lower).length, chars(s2Lower).length);
|
|
180
|
+
const shorter = Math.min(chars(s1Lower).length, chars(s2Lower).length);
|
|
181
|
+
return 0.5 + (shorter / longer) * 0.3;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const dist = levenshteinDistance(s1Lower, s2Lower);
|
|
185
|
+
return 1.0 - dist / maxLen;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function isPureNumber(entity: string): boolean {
|
|
189
|
+
const normalized = entity.replaceAll(".", "").replaceAll(",", "");
|
|
190
|
+
return normalized.length > 0 && /^\d+$/.test(normalized);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function extractEntitiesRegex(text: string): string[] {
|
|
194
|
+
if (typeof text !== "string" || text.length === 0) return [];
|
|
195
|
+
|
|
196
|
+
const entities = new Set<string>();
|
|
197
|
+
for (const sourcePattern of ENTITY_PATTERNS) {
|
|
198
|
+
const pattern = new RegExp(sourcePattern.source, sourcePattern.flags);
|
|
199
|
+
for (const match of text.matchAll(pattern)) {
|
|
200
|
+
const captured = match[1];
|
|
201
|
+
if (captured === undefined) continue;
|
|
202
|
+
const entity = captured.trim();
|
|
203
|
+
if (entity.length < 2) continue;
|
|
204
|
+
|
|
205
|
+
const words = entity.split(/\s+/).filter(word => word.length > 0);
|
|
206
|
+
if (words.length === 1 && ENTITY_EXTRACTION_STOP_WORDS.has(entity.toLowerCase())) continue;
|
|
207
|
+
if (words.some(word => ENTITY_EXTRACTION_STOP_WORDS.has(word.toLowerCase()))) continue;
|
|
208
|
+
if (isPureNumber(entity)) continue;
|
|
209
|
+
|
|
210
|
+
const first = entity[0];
|
|
211
|
+
if (words.length === 1 && first !== undefined && first >= "a" && first <= "z") {
|
|
212
|
+
const groupStart = match.index + match[0].indexOf(captured);
|
|
213
|
+
const prefixChar = groupStart > 0 ? text[groupStart - 1] : undefined;
|
|
214
|
+
if (prefixChar !== "@" && prefixChar !== "#") continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
entities.add(entity);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const result = Array.from(entities).sort();
|
|
222
|
+
const filtered = new Set<string>();
|
|
223
|
+
for (const entity of result) {
|
|
224
|
+
let isSubstring = false;
|
|
225
|
+
for (const other of result) {
|
|
226
|
+
if (other === entity || !other.includes(entity)) continue;
|
|
227
|
+
if (entity.startsWith("@") || entity.startsWith("#")) continue;
|
|
228
|
+
if (other.startsWith("@") || other.startsWith("#")) continue;
|
|
229
|
+
isSubstring = true;
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
if (!isSubstring) filtered.add(entity);
|
|
233
|
+
}
|
|
234
|
+
return Array.from(filtered).sort();
|
|
235
|
+
}
|
|
236
|
+
export type SimilarEntity = readonly [entity: string, score: number];
|
|
237
|
+
|
|
238
|
+
export function findSimilarEntities(
|
|
239
|
+
entity: string,
|
|
240
|
+
knownEntities: readonly string[],
|
|
241
|
+
threshold = 0.8,
|
|
242
|
+
): SimilarEntity[] {
|
|
243
|
+
const matches: SimilarEntity[] = [];
|
|
244
|
+
for (const known of knownEntities) {
|
|
245
|
+
if (known === entity) {
|
|
246
|
+
matches.push([known, 1.0]);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const score = similarity(entity, known);
|
|
250
|
+
if (score >= threshold) matches.push([known, score]);
|
|
251
|
+
}
|
|
252
|
+
matches.sort((left, right) => right[1] - left[1]);
|
|
253
|
+
return matches;
|
|
254
|
+
}
|
|
255
|
+
export function entityExtractionPerformance(text: string, iterations = 1000): number {
|
|
256
|
+
const start = performance.now();
|
|
257
|
+
for (let i = 0; i < iterations; i++) extractEntitiesRegex(text);
|
|
258
|
+
return (performance.now() - start) / iterations;
|
|
259
|
+
}
|