@melandlabs/memory-store 1.1.6 → 1.1.7
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 +99 -8
- package/dist/http.js +1 -0
- package/dist/mcp.js +4 -0
- package/dist/server/cli-http.js +435 -2
- package/dist/server/cli-mcp.js +437 -2
- package/dist/server/cli-shared.d.ts +14 -0
- package/dist/server/cli-shared.js +425 -2
- package/package.json +7 -7
package/dist/server/cli-mcp.js
CHANGED
|
@@ -1815,6 +1815,9 @@ async function startMcpServer(options = {}) {
|
|
|
1815
1815
|
tiers: z.array(z.enum(["summary", "raw", "insight", "knowledge"])).optional().describe(
|
|
1816
1816
|
"Per-tier evidence scope for synthesis. Default: ['summary','raw','insight','knowledge']. Ignored when `synthesize` is falsy."
|
|
1817
1817
|
),
|
|
1818
|
+
reasoningStrategy: z.enum(["none", "rewrite", "iterative"]).optional().describe(
|
|
1819
|
+
"LLM reasoning strategy. `rewrite` rephrases the query before embedding; `iterative` runs a planner that searches, notes evidence, and searches again. Requires `unified.reasoning.{queryRewriter,iterativePlanner}` to be wired into the server."
|
|
1820
|
+
),
|
|
1818
1821
|
synthesize: z.union([z.boolean(), z.object({ responseSchema: z.record(z.string(), z.unknown()).optional() })]).optional().describe(
|
|
1819
1822
|
"Opt-in LLM synthesis. `true` returns `{ answer, evidence, results, warnings }`. `synthesize.responseSchema` extracts `answer` from a JSON payload."
|
|
1820
1823
|
),
|
|
@@ -1890,6 +1893,7 @@ async function startMcpServer(options = {}) {
|
|
|
1890
1893
|
documentIds: a.documentIds,
|
|
1891
1894
|
authToken: a.authToken,
|
|
1892
1895
|
includeArchivedInsights: a.includeArchivedInsights,
|
|
1896
|
+
reasoningStrategy: a.reasoningStrategy === "rewrite" || a.reasoningStrategy === "iterative" ? a.reasoningStrategy : void 0,
|
|
1893
1897
|
...a.tiers ? { tiers: a.tiers } : {},
|
|
1894
1898
|
...wantsSynthesis ? {
|
|
1895
1899
|
synthesize: {
|
|
@@ -2179,6 +2183,345 @@ async function startMcpServer(options = {}) {
|
|
|
2179
2183
|
return server;
|
|
2180
2184
|
}
|
|
2181
2185
|
|
|
2186
|
+
// src/search/iterative-recall.ts
|
|
2187
|
+
var SYSTEM_PROMPT = `You are a research assistant collecting evidence from a user's conversation history. You are NOT answering the question; only gather the most useful memory.
|
|
2188
|
+
|
|
2189
|
+
Actions:
|
|
2190
|
+
search \u2014 search by concise keywords. Previously returned chunks are automatically excluded.
|
|
2191
|
+
Action Input: {"keywords":["keyword1","keyword2"],"date_from":"YYYY-MM-DD","date_to":"YYYY-MM-DD"}
|
|
2192
|
+
The date fields are optional; omit them unless an absolute date range is useful.
|
|
2193
|
+
|
|
2194
|
+
note \u2014 save relevant results from the LAST search by their 1-based result number.
|
|
2195
|
+
Action Input: {"indices":[1,3]}
|
|
2196
|
+
|
|
2197
|
+
finish \u2014 finish collecting evidence.
|
|
2198
|
+
Action Input: {}
|
|
2199
|
+
|
|
2200
|
+
Workflow: search -> note -> search again -> note -> finish.
|
|
2201
|
+
Review every result. Call note before another search because the prior result list is replaced. Use diverse keywords on later searches. Never answer the question.
|
|
2202
|
+
|
|
2203
|
+
Output exactly:
|
|
2204
|
+
Thought: one short sentence
|
|
2205
|
+
Action: one action name
|
|
2206
|
+
Action Input: valid JSON object`;
|
|
2207
|
+
function deriveKeywordsFromQuery(query) {
|
|
2208
|
+
return query.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((token) => token.length >= 2);
|
|
2209
|
+
}
|
|
2210
|
+
function formatDateHint(dateFrom, dateTo) {
|
|
2211
|
+
if (!dateFrom && !dateTo) {
|
|
2212
|
+
return void 0;
|
|
2213
|
+
}
|
|
2214
|
+
const fromText = dateFrom ?? "the beginning";
|
|
2215
|
+
const toText = dateTo ?? "now";
|
|
2216
|
+
return `Restrict searches to the date range ${fromText} to ${toText}. You may still emit narrower date_from/date_to bounds when useful.`;
|
|
2217
|
+
}
|
|
2218
|
+
function buildInitialPrompt(query, dateHint) {
|
|
2219
|
+
const hintText = dateHint ? `
|
|
2220
|
+
${dateHint}` : "";
|
|
2221
|
+
return `${SYSTEM_PROMPT}
|
|
2222
|
+
|
|
2223
|
+
Question to research: ${query}${hintText}
|
|
2224
|
+
Search the conversation history and collect all relevant evidence. Start with a broad keyword search.`;
|
|
2225
|
+
}
|
|
2226
|
+
function buildObservationPrompt(hits) {
|
|
2227
|
+
if (hits.length === 0) {
|
|
2228
|
+
return "Observation: No matching memory found.\nRespond with the next action only.";
|
|
2229
|
+
}
|
|
2230
|
+
const blocks = hits.map((hit, index) => {
|
|
2231
|
+
const lines = [`Result ${index + 1} | score=${hit.similarity.toFixed(4)}`];
|
|
2232
|
+
for (const line of hit.content.split("\n")) {
|
|
2233
|
+
if (line.trim()) lines.push(line);
|
|
2234
|
+
}
|
|
2235
|
+
return lines.join("\n");
|
|
2236
|
+
});
|
|
2237
|
+
return `Observation:
|
|
2238
|
+
${blocks.join("\n\n")}
|
|
2239
|
+
|
|
2240
|
+
Respond with the next action only.`;
|
|
2241
|
+
}
|
|
2242
|
+
function stripCodeFences(text) {
|
|
2243
|
+
return text.replace(/```(?:json)?\s*([\s\S]*?)\s*```/g, "$1");
|
|
2244
|
+
}
|
|
2245
|
+
function extractJsonObject(text) {
|
|
2246
|
+
const start = text.indexOf("{");
|
|
2247
|
+
if (start === -1) {
|
|
2248
|
+
return void 0;
|
|
2249
|
+
}
|
|
2250
|
+
let depth = 0;
|
|
2251
|
+
let inString = false;
|
|
2252
|
+
let escaped = false;
|
|
2253
|
+
for (let i = start; i < text.length; i += 1) {
|
|
2254
|
+
const ch = text[i];
|
|
2255
|
+
if (escaped) {
|
|
2256
|
+
escaped = false;
|
|
2257
|
+
continue;
|
|
2258
|
+
}
|
|
2259
|
+
if (inString) {
|
|
2260
|
+
if (ch === "\\") {
|
|
2261
|
+
escaped = true;
|
|
2262
|
+
} else if (ch === '"') {
|
|
2263
|
+
inString = false;
|
|
2264
|
+
}
|
|
2265
|
+
continue;
|
|
2266
|
+
}
|
|
2267
|
+
if (ch === '"') {
|
|
2268
|
+
inString = true;
|
|
2269
|
+
} else if (ch === "{") {
|
|
2270
|
+
depth += 1;
|
|
2271
|
+
} else if (ch === "}") {
|
|
2272
|
+
depth -= 1;
|
|
2273
|
+
if (depth === 0) {
|
|
2274
|
+
return text.slice(start, i + 1);
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
return void 0;
|
|
2279
|
+
}
|
|
2280
|
+
function parseAction(text) {
|
|
2281
|
+
const cleaned = stripCodeFences(text);
|
|
2282
|
+
const actionMatch = cleaned.match(/Action:\s*([\w_]+)/);
|
|
2283
|
+
if (!actionMatch) return null;
|
|
2284
|
+
const action = actionMatch[1].trim().toLowerCase();
|
|
2285
|
+
if (action === "finish") {
|
|
2286
|
+
return { type: "finish" };
|
|
2287
|
+
}
|
|
2288
|
+
const inputMatch = cleaned.match(/Action Input:\s*/s);
|
|
2289
|
+
if (!inputMatch || inputMatch.index === void 0) return null;
|
|
2290
|
+
const jsonText = extractJsonObject(cleaned.slice(inputMatch.index + inputMatch[0].length));
|
|
2291
|
+
if (!jsonText) return null;
|
|
2292
|
+
let parsedJson = null;
|
|
2293
|
+
for (const candidate of [jsonText, jsonText.replace(/'/g, '"')]) {
|
|
2294
|
+
try {
|
|
2295
|
+
parsedJson = JSON.parse(candidate);
|
|
2296
|
+
break;
|
|
2297
|
+
} catch {
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
if (!parsedJson) return null;
|
|
2301
|
+
if (action === "search") {
|
|
2302
|
+
const keywords = parsedJson.keywords;
|
|
2303
|
+
if (!Array.isArray(keywords) || !keywords.every((k) => typeof k === "string")) {
|
|
2304
|
+
return null;
|
|
2305
|
+
}
|
|
2306
|
+
return {
|
|
2307
|
+
type: "search",
|
|
2308
|
+
keywords,
|
|
2309
|
+
dateFrom: typeof parsedJson.date_from === "string" ? parsedJson.date_from : void 0,
|
|
2310
|
+
dateTo: typeof parsedJson.date_to === "string" ? parsedJson.date_to : void 0
|
|
2311
|
+
};
|
|
2312
|
+
}
|
|
2313
|
+
if (action === "note") {
|
|
2314
|
+
const indices = parsedJson.indices;
|
|
2315
|
+
if (!Array.isArray(indices) || !indices.every((i) => typeof i === "number")) {
|
|
2316
|
+
return null;
|
|
2317
|
+
}
|
|
2318
|
+
return { type: "note", indices };
|
|
2319
|
+
}
|
|
2320
|
+
return null;
|
|
2321
|
+
}
|
|
2322
|
+
function createIterativeRecallPlanner(deps) {
|
|
2323
|
+
const { complete, options = {} } = deps;
|
|
2324
|
+
const defaults = {
|
|
2325
|
+
maxIterations: 4,
|
|
2326
|
+
searchTopK: 5,
|
|
2327
|
+
disabled: false,
|
|
2328
|
+
fallbackToBaseline: true
|
|
2329
|
+
};
|
|
2330
|
+
let lastDegraded = false;
|
|
2331
|
+
let iterationsWithValidAction = 0;
|
|
2332
|
+
return {
|
|
2333
|
+
lastDegraded: () => lastDegraded,
|
|
2334
|
+
async plan(input) {
|
|
2335
|
+
const opts = {
|
|
2336
|
+
...defaults,
|
|
2337
|
+
...options,
|
|
2338
|
+
...input.options
|
|
2339
|
+
};
|
|
2340
|
+
lastDegraded = false;
|
|
2341
|
+
iterationsWithValidAction = 0;
|
|
2342
|
+
if (opts.disabled || !input.query.trim()) {
|
|
2343
|
+
return { evidence: [], stats: { iterations: 0, searches: 0, notes: 0 } };
|
|
2344
|
+
}
|
|
2345
|
+
const messages = [
|
|
2346
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
2347
|
+
{
|
|
2348
|
+
role: "user",
|
|
2349
|
+
content: buildInitialPrompt(input.query, formatDateHint(input.dateFrom, input.dateTo))
|
|
2350
|
+
}
|
|
2351
|
+
];
|
|
2352
|
+
const evidence = /* @__PURE__ */ new Map();
|
|
2353
|
+
const excludedIds = /* @__PURE__ */ new Set();
|
|
2354
|
+
let lastHits = [];
|
|
2355
|
+
let searches = 0;
|
|
2356
|
+
let notes = 0;
|
|
2357
|
+
let iterations = 0;
|
|
2358
|
+
let completeThrew = false;
|
|
2359
|
+
for (let i = 0; i < opts.maxIterations; i += 1) {
|
|
2360
|
+
iterations = i + 1;
|
|
2361
|
+
let reply;
|
|
2362
|
+
try {
|
|
2363
|
+
reply = await complete(messages.map((m) => `${m.role}: ${m.content}`).join("\n\n"));
|
|
2364
|
+
} catch {
|
|
2365
|
+
completeThrew = true;
|
|
2366
|
+
break;
|
|
2367
|
+
}
|
|
2368
|
+
messages.push({ role: "assistant", content: reply });
|
|
2369
|
+
const action = parseAction(reply);
|
|
2370
|
+
if (!action) {
|
|
2371
|
+
messages.push({
|
|
2372
|
+
role: "user",
|
|
2373
|
+
content: "Observation: Use exactly one valid action: search, note, or finish. Respond with the next action only."
|
|
2374
|
+
});
|
|
2375
|
+
continue;
|
|
2376
|
+
}
|
|
2377
|
+
iterationsWithValidAction += 1;
|
|
2378
|
+
if (action.type === "finish") {
|
|
2379
|
+
break;
|
|
2380
|
+
}
|
|
2381
|
+
if (action.type === "search") {
|
|
2382
|
+
const result = await input.executor.search({
|
|
2383
|
+
keywords: action.keywords,
|
|
2384
|
+
dateFrom: action.dateFrom ?? input.dateFrom,
|
|
2385
|
+
dateTo: action.dateTo ?? input.dateTo
|
|
2386
|
+
});
|
|
2387
|
+
lastHits = result.candidates.filter((c) => !excludedIds.has(c.id)).slice(0, opts.searchTopK);
|
|
2388
|
+
for (const hit of lastHits) {
|
|
2389
|
+
excludedIds.add(hit.id);
|
|
2390
|
+
}
|
|
2391
|
+
searches += 1;
|
|
2392
|
+
messages.push({ role: "user", content: buildObservationPrompt(lastHits) });
|
|
2393
|
+
continue;
|
|
2394
|
+
}
|
|
2395
|
+
if (action.type === "note") {
|
|
2396
|
+
let saved = 0;
|
|
2397
|
+
for (const index of action.indices) {
|
|
2398
|
+
if (index >= 1 && index <= lastHits.length) {
|
|
2399
|
+
const hit = lastHits[index - 1];
|
|
2400
|
+
if (!evidence.has(hit.id)) {
|
|
2401
|
+
evidence.set(hit.id, hit);
|
|
2402
|
+
saved += 1;
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
notes += saved;
|
|
2407
|
+
messages.push({
|
|
2408
|
+
role: "user",
|
|
2409
|
+
content: `Observation: Saved ${saved} new result(s). Total notes: ${evidence.size}.
|
|
2410
|
+
Respond with the next action only.`
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
let fallbackRan = false;
|
|
2415
|
+
if (evidence.size === 0 && opts.fallbackToBaseline) {
|
|
2416
|
+
fallbackRan = true;
|
|
2417
|
+
if (lastHits.length > 0) {
|
|
2418
|
+
for (const hit of lastHits) {
|
|
2419
|
+
if (!evidence.has(hit.id)) {
|
|
2420
|
+
evidence.set(hit.id, hit);
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
} else {
|
|
2424
|
+
const baseline = await input.executor.search({
|
|
2425
|
+
keywords: deriveKeywordsFromQuery(input.query),
|
|
2426
|
+
dateFrom: input.dateFrom,
|
|
2427
|
+
dateTo: input.dateTo
|
|
2428
|
+
});
|
|
2429
|
+
searches += 1;
|
|
2430
|
+
for (const hit of baseline.candidates.slice(0, opts.searchTopK)) {
|
|
2431
|
+
if (!evidence.has(hit.id)) {
|
|
2432
|
+
evidence.set(hit.id, hit);
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
lastDegraded = completeThrew || iterationsWithValidAction === 0 || fallbackRan;
|
|
2438
|
+
return {
|
|
2439
|
+
evidence: Array.from(evidence.values()),
|
|
2440
|
+
stats: { iterations, searches, notes }
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
};
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
// src/search/query-rewriter.ts
|
|
2447
|
+
var SYSTEM_PROMPT2 = `You are a memory retrieval assistant.
|
|
2448
|
+
|
|
2449
|
+
The memory store contains a user's past messages, written in the first person (e.g. "I told you about my sister's wedding", "I prefer dark mode").
|
|
2450
|
+
|
|
2451
|
+
Your job is NOT to answer the question. Your job is to rewrite the assistant's question into a short, natural question that the USER would ask their own memory log to find the relevant entry.
|
|
2452
|
+
|
|
2453
|
+
Rules:
|
|
2454
|
+
- Use first person: "Did I tell you about ...?", "What did I say about ...?", "Have I mentioned ...?"
|
|
2455
|
+
- Do NOT address the user as "you".
|
|
2456
|
+
- Do NOT answer the question.
|
|
2457
|
+
- Output each rephrasing on its own line, prefixed with "- ".
|
|
2458
|
+
- Keep each rephrasing under 20 words.
|
|
2459
|
+
- Output exactly the requested number of lines.`;
|
|
2460
|
+
function buildPrompt(query, numVariants) {
|
|
2461
|
+
const label = numVariants === 1 ? "1 alternative rephrasing" : `${numVariants} alternative rephrasings`;
|
|
2462
|
+
return `${SYSTEM_PROMPT2}
|
|
2463
|
+
|
|
2464
|
+
Output exactly ${label}.
|
|
2465
|
+
|
|
2466
|
+
Example:
|
|
2467
|
+
Question: What is the name of my cat?
|
|
2468
|
+
- Did I tell you the name of my cat?
|
|
2469
|
+
|
|
2470
|
+
Question: ${query}
|
|
2471
|
+
Output:`;
|
|
2472
|
+
}
|
|
2473
|
+
function sanitizeLine(text) {
|
|
2474
|
+
return text.replace(/^\s*["'`]+|["'`]+\s*$/g, "").replace(/^Rewritten:\s*/i, "").trim();
|
|
2475
|
+
}
|
|
2476
|
+
function parseVariants(raw, maxVariants, original) {
|
|
2477
|
+
const lines = raw.split(/\r?\n/).map((line) => sanitizeLine(line.replace(/^\s*-\s+/, ""))).filter((line) => line.length > 0);
|
|
2478
|
+
if (lines.length === 0) return [];
|
|
2479
|
+
const seen = /* @__PURE__ */ new Set([original.toLowerCase()]);
|
|
2480
|
+
const variants = [];
|
|
2481
|
+
for (const line of lines) {
|
|
2482
|
+
if (variants.length >= maxVariants) break;
|
|
2483
|
+
const key = line.toLowerCase();
|
|
2484
|
+
if (seen.has(key)) continue;
|
|
2485
|
+
seen.add(key);
|
|
2486
|
+
variants.push(line);
|
|
2487
|
+
}
|
|
2488
|
+
return variants;
|
|
2489
|
+
}
|
|
2490
|
+
function createUserVoiceRewriter(options) {
|
|
2491
|
+
const { complete, maxVariants = 1, disabled = false } = options;
|
|
2492
|
+
const wantedVariants = Math.max(0, Math.floor(maxVariants));
|
|
2493
|
+
let lastDegraded = false;
|
|
2494
|
+
return {
|
|
2495
|
+
async rewrite(input) {
|
|
2496
|
+
if (disabled || !input.query || !input.query.trim()) {
|
|
2497
|
+
lastDegraded = false;
|
|
2498
|
+
return [input.query];
|
|
2499
|
+
}
|
|
2500
|
+
const original = input.query.trim();
|
|
2501
|
+
if (wantedVariants === 0) {
|
|
2502
|
+
lastDegraded = false;
|
|
2503
|
+
return [original];
|
|
2504
|
+
}
|
|
2505
|
+
try {
|
|
2506
|
+
const raw = await complete(buildPrompt(original, wantedVariants));
|
|
2507
|
+
const variants = parseVariants(raw, wantedVariants, original);
|
|
2508
|
+
if (variants.length === 0) {
|
|
2509
|
+
lastDegraded = true;
|
|
2510
|
+
return [original];
|
|
2511
|
+
}
|
|
2512
|
+
lastDegraded = false;
|
|
2513
|
+
return [original, ...variants];
|
|
2514
|
+
} catch {
|
|
2515
|
+
lastDegraded = true;
|
|
2516
|
+
return [original];
|
|
2517
|
+
}
|
|
2518
|
+
},
|
|
2519
|
+
lastDegraded() {
|
|
2520
|
+
return lastDegraded;
|
|
2521
|
+
}
|
|
2522
|
+
};
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2182
2525
|
// src/server/cli-shared.ts
|
|
2183
2526
|
init_raw_message_store();
|
|
2184
2527
|
|
|
@@ -2241,7 +2584,11 @@ function parseUnifiedArgs(argv) {
|
|
|
2241
2584
|
insightsBackend: env.INSIGHTS_BACKEND ?? "none",
|
|
2242
2585
|
insightsCollection: env.INSIGHTS_COLLECTION ?? "opencontext_insights",
|
|
2243
2586
|
knowledgeBackend: env.KNOWLEDGE_BACKEND ?? "none",
|
|
2244
|
-
knowledgeCollection: env.KNOWLEDGE_COLLECTION ?? "opencontext_knowledge"
|
|
2587
|
+
knowledgeCollection: env.KNOWLEDGE_COLLECTION ?? "opencontext_knowledge",
|
|
2588
|
+
reasoning: env.REASONING === "1" || env.REASONING === "true",
|
|
2589
|
+
reasoningBaseUrl: env.OPENCONTEXT_LLM_BASE_URL,
|
|
2590
|
+
reasoningModel: env.OPENCONTEXT_LLM_MODEL,
|
|
2591
|
+
reasoningTimeoutMs: env.OPENCONTEXT_LLM_TIMEOUT_MS ? Number.parseInt(env.OPENCONTEXT_LLM_TIMEOUT_MS, 10) : void 0
|
|
2245
2592
|
};
|
|
2246
2593
|
for (let i = 0; i < argv.length; i += 1) {
|
|
2247
2594
|
const arg = argv[i];
|
|
@@ -2276,6 +2623,21 @@ function parseUnifiedArgs(argv) {
|
|
|
2276
2623
|
case "--knowledge-collection":
|
|
2277
2624
|
args.knowledgeCollection = takeValue();
|
|
2278
2625
|
break;
|
|
2626
|
+
case "--reasoning":
|
|
2627
|
+
args.reasoning = true;
|
|
2628
|
+
break;
|
|
2629
|
+
case "--no-reasoning":
|
|
2630
|
+
args.reasoning = false;
|
|
2631
|
+
break;
|
|
2632
|
+
case "--reasoning-base-url":
|
|
2633
|
+
args.reasoningBaseUrl = takeValue();
|
|
2634
|
+
break;
|
|
2635
|
+
case "--reasoning-model":
|
|
2636
|
+
args.reasoningModel = takeValue();
|
|
2637
|
+
break;
|
|
2638
|
+
case "--reasoning-timeout-ms":
|
|
2639
|
+
args.reasoningTimeoutMs = Number.parseInt(takeValue(), 10);
|
|
2640
|
+
break;
|
|
2279
2641
|
}
|
|
2280
2642
|
}
|
|
2281
2643
|
for (const [name, value] of [
|
|
@@ -2449,6 +2811,52 @@ async function buildUnified(args) {
|
|
|
2449
2811
|
};
|
|
2450
2812
|
log(`knowledge backend wired via chroma (collection=${args.knowledgeCollection})`);
|
|
2451
2813
|
}
|
|
2814
|
+
if (args.reasoning) {
|
|
2815
|
+
const apiKey = process.env.OPENCONTEXT_LLM_API_KEY;
|
|
2816
|
+
if (!apiKey) {
|
|
2817
|
+
throw new Error(
|
|
2818
|
+
"--reasoning requires OPENCONTEXT_LLM_API_KEY (and ideally OPENCONTEXT_LLM_BASE_URL / OPENCONTEXT_LLM_MODEL) in the environment"
|
|
2819
|
+
);
|
|
2820
|
+
}
|
|
2821
|
+
const baseUrl = args.reasoningBaseUrl ?? process.env.OPENCONTEXT_LLM_BASE_URL ?? "https://openrouter.ai/api/v1";
|
|
2822
|
+
const model = args.reasoningModel ?? process.env.OPENCONTEXT_LLM_MODEL ?? "openai/gpt-4o-mini";
|
|
2823
|
+
const timeoutMs = args.reasoningTimeoutMs ?? 3e4;
|
|
2824
|
+
const complete = async (prompt) => {
|
|
2825
|
+
const controller = new AbortController();
|
|
2826
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
2827
|
+
timer.unref?.();
|
|
2828
|
+
try {
|
|
2829
|
+
const res = await fetch(`${baseUrl}/chat/completions`, {
|
|
2830
|
+
method: "POST",
|
|
2831
|
+
headers: {
|
|
2832
|
+
"content-type": "application/json",
|
|
2833
|
+
authorization: `Bearer ${apiKey}`,
|
|
2834
|
+
"HTTP-Referer": process.env.NEXT_PUBLIC_APP_URL ?? "https://opencontext.ai",
|
|
2835
|
+
"X-Title": "opencontext AI"
|
|
2836
|
+
},
|
|
2837
|
+
body: JSON.stringify({
|
|
2838
|
+
model,
|
|
2839
|
+
messages: [{ role: "user", content: prompt }],
|
|
2840
|
+
temperature: 0
|
|
2841
|
+
}),
|
|
2842
|
+
signal: controller.signal
|
|
2843
|
+
});
|
|
2844
|
+
if (!res.ok) throw new Error(`reasoning LLM ${res.status}: ${await res.text()}`);
|
|
2845
|
+
const body = await res.json();
|
|
2846
|
+
const text = body.choices?.[0]?.message?.content?.trim();
|
|
2847
|
+
if (!text) throw new Error("reasoning LLM response missing choices[0].message.content");
|
|
2848
|
+
return text;
|
|
2849
|
+
} finally {
|
|
2850
|
+
clearTimeout(timer);
|
|
2851
|
+
}
|
|
2852
|
+
};
|
|
2853
|
+
const queryRewriter = createUserVoiceRewriter({ complete });
|
|
2854
|
+
const iterativePlanner = createIterativeRecallPlanner({ complete });
|
|
2855
|
+
if (!unified.reasoning) unified.reasoning = {};
|
|
2856
|
+
unified.reasoning.queryRewriter = queryRewriter;
|
|
2857
|
+
unified.reasoning.iterativePlanner = iterativePlanner;
|
|
2858
|
+
log(`reasoning wired (model=${model}, baseUrl=${baseUrl})`);
|
|
2859
|
+
}
|
|
2452
2860
|
return unified;
|
|
2453
2861
|
}
|
|
2454
2862
|
function printUnifiedHelp() {
|
|
@@ -2471,7 +2879,26 @@ Cross-source search (wires unified.searchKnowledge / searchInsights / searchRawM
|
|
|
2471
2879
|
--knowledge-backend <name> chroma | none
|
|
2472
2880
|
(env: KNOWLEDGE_BACKEND, default: none)
|
|
2473
2881
|
--knowledge-collection <name> Chroma collection (default: opencontext_knowledge)
|
|
2474
|
-
Note: the chroma backends dynamically import @melandlabs/ai-rag
|
|
2882
|
+
Note: the chroma backends dynamically import @melandlabs/ai-rag.
|
|
2883
|
+
|
|
2884
|
+
Reasoning (wires unified.reasoning.{queryRewriter, iterativePlanner}):
|
|
2885
|
+
--reasoning Enable the LLM reasoning layer so /v1/search and
|
|
2886
|
+
memory.search can honor reasoningStrategy:
|
|
2887
|
+
"rewrite" \u2014 first-person memory-check rephrase
|
|
2888
|
+
"iterative" \u2014 planner that searches, notes evidence,
|
|
2889
|
+
searches again
|
|
2890
|
+
(env: REASONING=1, default: off)
|
|
2891
|
+
--no-reasoning Explicitly disable even if REASONING=1 is set.
|
|
2892
|
+
--reasoning-base-url <url> OpenAI-compatible base URL for the reasoning LLM.
|
|
2893
|
+
(env: OPENCONTEXT_LLM_BASE_URL, default: https://openrouter.ai/api/v1)
|
|
2894
|
+
--reasoning-model <name> Reasoning LLM model identifier.
|
|
2895
|
+
(env: OPENCONTEXT_LLM_MODEL, default: openai/gpt-4o-mini)
|
|
2896
|
+
--reasoning-timeout-ms <int> Per-request timeout (default: 30000).
|
|
2897
|
+
|
|
2898
|
+
Required env when --reasoning is set:
|
|
2899
|
+
OPENCONTEXT_LLM_API_KEY Bearer token (no default)
|
|
2900
|
+
OPENCONTEXT_LLM_BASE_URL (optional) overrides --reasoning-base-url
|
|
2901
|
+
OPENCONTEXT_LLM_MODEL (optional) overrides --reasoning-model`);
|
|
2475
2902
|
}
|
|
2476
2903
|
|
|
2477
2904
|
// src/server/cli-mcp.ts
|
|
@@ -2527,6 +2954,14 @@ Examples:
|
|
|
2527
2954
|
--embedding-provider local \\
|
|
2528
2955
|
--memory-backend sqlite-vec
|
|
2529
2956
|
|
|
2957
|
+
# Same as above + LLM reasoning (memory.search honors
|
|
2958
|
+
# reasoningStrategy: 'rewrite' | 'iterative'). Reads OPENCONTEXT_LLM_*
|
|
2959
|
+
# from the environment so .env Just Works.
|
|
2960
|
+
opencontext-memory-mcp \\
|
|
2961
|
+
--embedding-provider local \\
|
|
2962
|
+
--memory-backend sqlite-vec \\
|
|
2963
|
+
--reasoning
|
|
2964
|
+
|
|
2530
2965
|
# Wire everything via a running Chroma server
|
|
2531
2966
|
opencontext-memory-mcp \\
|
|
2532
2967
|
--embedding-provider openrouter \\
|
|
@@ -29,6 +29,20 @@ interface UnifiedArgs {
|
|
|
29
29
|
insightsCollection: string;
|
|
30
30
|
knowledgeBackend: "chroma" | "none";
|
|
31
31
|
knowledgeCollection: string;
|
|
32
|
+
/**
|
|
33
|
+
* Wire `unified.reasoning.{queryRewriter, iterativePlanner}` from the
|
|
34
|
+
* `OPENCONTEXT_LLM_*` env vars. When false, reasoning providers stay
|
|
35
|
+
* unset and `POST /v1/search` / `memory.search` requests with
|
|
36
|
+
* `reasoningStrategy: "rewrite" | "iterative"` will surface a
|
|
37
|
+
* `_not_configured` warning and fall back to the default search path.
|
|
38
|
+
*/
|
|
39
|
+
reasoning: boolean;
|
|
40
|
+
/** OpenAI-compatible base URL the reasoning LLM is served from. */
|
|
41
|
+
reasoningBaseUrl?: string;
|
|
42
|
+
/** Reasoning LLM model identifier. */
|
|
43
|
+
reasoningModel?: string;
|
|
44
|
+
/** Request timeout for reasoning LLM calls (ms). @default 30000 */
|
|
45
|
+
reasoningTimeoutMs?: number;
|
|
32
46
|
}
|
|
33
47
|
declare function parseUnifiedArgs(argv: string[]): UnifiedArgs;
|
|
34
48
|
declare function buildUnified(args: UnifiedArgs): Promise<UnifiedSearchDeps>;
|