@memtensor/memos-local-openclaw-plugin 1.0.4 → 1.0.6-beta.1
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/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +24 -0
- package/dist/capture/index.js.map +1 -1
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +23 -1
- package/dist/client/connector.js.map +1 -1
- package/dist/client/hub.d.ts.map +1 -1
- package/dist/client/hub.js +4 -0
- package/dist/client/hub.js.map +1 -1
- package/dist/hub/server.d.ts +1 -1
- package/dist/hub/server.d.ts.map +1 -1
- package/dist/hub/server.js +39 -31
- package/dist/hub/server.js.map +1 -1
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +16 -86
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/ingest/providers/openai.d.ts +3 -0
- package/dist/ingest/providers/openai.d.ts.map +1 -1
- package/dist/ingest/providers/openai.js +34 -19
- package/dist/ingest/providers/openai.js.map +1 -1
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +28 -19
- package/dist/recall/engine.js.map +1 -1
- package/dist/storage/sqlite.d.ts +30 -7
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +139 -60
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/telemetry.d.ts +4 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +26 -18
- package/dist/telemetry.js.map +1 -1
- package/dist/tools/memory-get.d.ts.map +1 -1
- package/dist/tools/memory-get.js +4 -1
- package/dist/tools/memory-get.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/viewer/server.d.ts +24 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +332 -130
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +66 -30
- package/package.json +1 -1
- package/scripts/postinstall.cjs +21 -5
- package/src/capture/index.ts +36 -0
- package/src/client/connector.ts +22 -1
- package/src/client/hub.ts +4 -0
- package/src/hub/server.ts +42 -26
- package/src/ingest/providers/index.ts +30 -93
- package/src/ingest/providers/openai.ts +32 -15
- package/src/recall/engine.ts +28 -19
- package/src/storage/sqlite.ts +156 -65
- package/src/telemetry.ts +25 -18
- package/src/tools/memory-get.ts +4 -1
- package/src/types.ts +2 -0
- package/src/viewer/server.ts +313 -125
- package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
- package/prebuilds/darwin-x64/better_sqlite3.node +0 -0
- package/prebuilds/linux-x64/better_sqlite3.node +0 -0
- package/prebuilds/win32-x64/better_sqlite3.node +0 -0
- package/telemetry.credentials.json +0 -5
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import type { SummarizerConfig, SummaryProvider, Logger, OpenClawAPI } from "../../types";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
summarizeOpenAI,
|
|
6
|
+
summarizeTaskOpenAI,
|
|
7
|
+
generateTaskTitleOpenAI,
|
|
8
|
+
judgeNewTopicOpenAI,
|
|
9
|
+
filterRelevantOpenAI,
|
|
10
|
+
judgeDedupOpenAI,
|
|
11
|
+
parseFilterResult,
|
|
12
|
+
parseDedupResult,
|
|
13
|
+
TASK_SUMMARY_PROMPT,
|
|
14
|
+
TOPIC_JUDGE_PROMPT,
|
|
15
|
+
FILTER_RELEVANT_PROMPT,
|
|
16
|
+
DEDUP_JUDGE_PROMPT,
|
|
17
|
+
} from "./openai";
|
|
5
18
|
import type { FilterResult, DedupResult } from "./openai";
|
|
6
19
|
export type { FilterResult, DedupResult } from "./openai";
|
|
7
20
|
import { summarizeAnthropic, summarizeTaskAnthropic, generateTaskTitleAnthropic, judgeNewTopicAnthropic, filterRelevantAnthropic, judgeDedupAnthropic } from "./anthropic";
|
|
@@ -360,7 +373,7 @@ export class Summarizer {
|
|
|
360
373
|
private async summarizeTaskOpenClaw(text: string): Promise<string> {
|
|
361
374
|
this.requireOpenClawAPI();
|
|
362
375
|
const prompt = [
|
|
363
|
-
|
|
376
|
+
TASK_SUMMARY_PROMPT,
|
|
364
377
|
``,
|
|
365
378
|
text,
|
|
366
379
|
].join("\n");
|
|
@@ -378,7 +391,7 @@ export class Summarizer {
|
|
|
378
391
|
private async judgeNewTopicOpenClaw(currentContext: string, newMessage: string): Promise<boolean> {
|
|
379
392
|
this.requireOpenClawAPI();
|
|
380
393
|
const prompt = [
|
|
381
|
-
|
|
394
|
+
TOPIC_JUDGE_PROMPT,
|
|
382
395
|
``,
|
|
383
396
|
`CURRENT CONVERSATION SUMMARY:`,
|
|
384
397
|
currentContext,
|
|
@@ -409,7 +422,7 @@ export class Summarizer {
|
|
|
409
422
|
.join("\n");
|
|
410
423
|
|
|
411
424
|
const prompt = [
|
|
412
|
-
|
|
425
|
+
FILTER_RELEVANT_PROMPT,
|
|
413
426
|
``,
|
|
414
427
|
`QUERY: ${query}`,
|
|
415
428
|
``,
|
|
@@ -437,7 +450,7 @@ export class Summarizer {
|
|
|
437
450
|
.join("\n");
|
|
438
451
|
|
|
439
452
|
const prompt = [
|
|
440
|
-
|
|
453
|
+
DEDUP_JUDGE_PROMPT,
|
|
441
454
|
``,
|
|
442
455
|
`NEW MEMORY:`,
|
|
443
456
|
newSummary,
|
|
@@ -466,6 +479,8 @@ function callSummarize(cfg: SummarizerConfig, text: string, log: Logger): Promis
|
|
|
466
479
|
case "azure_openai":
|
|
467
480
|
case "zhipu":
|
|
468
481
|
case "siliconflow":
|
|
482
|
+
case "deepseek":
|
|
483
|
+
case "moonshot":
|
|
469
484
|
case "bailian":
|
|
470
485
|
case "cohere":
|
|
471
486
|
case "mistral":
|
|
@@ -489,6 +504,8 @@ function callSummarizeTask(cfg: SummarizerConfig, text: string, log: Logger): Pr
|
|
|
489
504
|
case "azure_openai":
|
|
490
505
|
case "zhipu":
|
|
491
506
|
case "siliconflow":
|
|
507
|
+
case "deepseek":
|
|
508
|
+
case "moonshot":
|
|
492
509
|
case "bailian":
|
|
493
510
|
case "cohere":
|
|
494
511
|
case "mistral":
|
|
@@ -512,6 +529,8 @@ function callGenerateTaskTitle(cfg: SummarizerConfig, text: string, log: Logger)
|
|
|
512
529
|
case "azure_openai":
|
|
513
530
|
case "zhipu":
|
|
514
531
|
case "siliconflow":
|
|
532
|
+
case "deepseek":
|
|
533
|
+
case "moonshot":
|
|
515
534
|
case "bailian":
|
|
516
535
|
case "cohere":
|
|
517
536
|
case "mistral":
|
|
@@ -535,6 +554,8 @@ function callTopicJudge(cfg: SummarizerConfig, currentContext: string, newMessag
|
|
|
535
554
|
case "azure_openai":
|
|
536
555
|
case "zhipu":
|
|
537
556
|
case "siliconflow":
|
|
557
|
+
case "deepseek":
|
|
558
|
+
case "moonshot":
|
|
538
559
|
case "bailian":
|
|
539
560
|
case "cohere":
|
|
540
561
|
case "mistral":
|
|
@@ -558,6 +579,8 @@ function callFilterRelevant(cfg: SummarizerConfig, query: string, candidates: Ar
|
|
|
558
579
|
case "azure_openai":
|
|
559
580
|
case "zhipu":
|
|
560
581
|
case "siliconflow":
|
|
582
|
+
case "deepseek":
|
|
583
|
+
case "moonshot":
|
|
561
584
|
case "bailian":
|
|
562
585
|
case "cohere":
|
|
563
586
|
case "mistral":
|
|
@@ -581,6 +604,8 @@ function callJudgeDedup(cfg: SummarizerConfig, newSummary: string, candidates: A
|
|
|
581
604
|
case "azure_openai":
|
|
582
605
|
case "zhipu":
|
|
583
606
|
case "siliconflow":
|
|
607
|
+
case "deepseek":
|
|
608
|
+
case "moonshot":
|
|
584
609
|
case "bailian":
|
|
585
610
|
case "cohere":
|
|
586
611
|
case "mistral":
|
|
@@ -629,91 +654,3 @@ function wordCount(text: string): number {
|
|
|
629
654
|
if (noCjk) count += noCjk.split(/\s+/).filter(Boolean).length;
|
|
630
655
|
return count;
|
|
631
656
|
}
|
|
632
|
-
|
|
633
|
-
// ─── OpenClaw Prompt Templates ───
|
|
634
|
-
|
|
635
|
-
const OPENCLAW_TASK_SUMMARY_PROMPT = `You create a DETAILED task summary from a multi-turn conversation. This summary will be the ONLY record of this conversation, so it must preserve ALL important information.
|
|
636
|
-
|
|
637
|
-
CRITICAL LANGUAGE RULE: You MUST write in the SAME language as the user's messages. Chinese input → Chinese output. English input → English output. NEVER mix languages.
|
|
638
|
-
|
|
639
|
-
Output EXACTLY this structure:
|
|
640
|
-
|
|
641
|
-
📌 Title
|
|
642
|
-
A short, descriptive title (10-30 characters). Like a chat group name.
|
|
643
|
-
|
|
644
|
-
🎯 Goal
|
|
645
|
-
One sentence: what the user wanted to accomplish.
|
|
646
|
-
|
|
647
|
-
📋 Key Steps
|
|
648
|
-
- Describe each meaningful step in detail
|
|
649
|
-
- Include the ACTUAL content produced: code snippets, commands, config blocks, formulas, key paragraphs
|
|
650
|
-
- For code: include the function signature and core logic (up to ~30 lines per block), use fenced code blocks
|
|
651
|
-
- For configs: include the actual config values and structure
|
|
652
|
-
- For lists/instructions: include the actual items, not just "provided a list"
|
|
653
|
-
- Merge only truly trivial back-and-forth (like "ok" / "sure")
|
|
654
|
-
- Do NOT over-summarize: "provided a function" is BAD; show the actual function
|
|
655
|
-
|
|
656
|
-
✅ Result
|
|
657
|
-
What was the final outcome? Include the final version of any code/config/content produced.
|
|
658
|
-
|
|
659
|
-
💡 Key Details
|
|
660
|
-
- Decisions made, trade-offs discussed, caveats noted, alternative approaches mentioned
|
|
661
|
-
- Specific values: numbers, versions, thresholds, URLs, file paths, model names
|
|
662
|
-
- Omit this section only if there truly are no noteworthy details
|
|
663
|
-
|
|
664
|
-
RULES:
|
|
665
|
-
- This summary is a KNOWLEDGE BASE ENTRY, not a brief note. Be thorough.
|
|
666
|
-
- PRESERVE verbatim: code, commands, URLs, file paths, error messages, config values, version numbers, names, amounts
|
|
667
|
-
- DISCARD only: greetings, filler, the assistant explaining what it will do before doing it
|
|
668
|
-
- Replace secrets (API keys, tokens, passwords) with [REDACTED]
|
|
669
|
-
- Target length: 30-50% of the original conversation length. Longer conversations need longer summaries.
|
|
670
|
-
- Output summary only, no preamble.`;
|
|
671
|
-
|
|
672
|
-
const OPENCLAW_TOPIC_JUDGE_PROMPT = `You are a conversation topic boundary detector. Given a summary of the CURRENT conversation and a NEW user message, determine if the new message starts a DIFFERENT topic/task.
|
|
673
|
-
|
|
674
|
-
Answer ONLY "NEW" or "SAME".
|
|
675
|
-
|
|
676
|
-
Rules:
|
|
677
|
-
- "NEW" = the new message is about a completely different subject, project, or task
|
|
678
|
-
- "SAME" = the new message continues, follows up on, or is closely related to the current topic
|
|
679
|
-
- Follow-up questions, clarifications, refinements, bug fixes, or next steps on the same task = SAME
|
|
680
|
-
- Greetings or meta-questions like "你好" or "谢谢" without new substance = SAME
|
|
681
|
-
- A clearly unrelated request (e.g., current topic is deployment, new message asks about cooking) = NEW
|
|
682
|
-
|
|
683
|
-
Output exactly one word: NEW or SAME`;
|
|
684
|
-
|
|
685
|
-
const OPENCLAW_FILTER_RELEVANT_PROMPT = `You are a memory relevance judge. Given a user's QUERY and a list of CANDIDATE memory summaries, do two things:
|
|
686
|
-
|
|
687
|
-
1. Select ALL candidates that could be useful for answering the query. When in doubt, INCLUDE the candidate.
|
|
688
|
-
- For questions about lists, history, or "what/where/who" across multiple items, include ALL matching items.
|
|
689
|
-
- For factual lookups, a single direct answer is enough.
|
|
690
|
-
2. Judge whether the selected memories are SUFFICIENT to fully answer the query WITHOUT fetching additional context.
|
|
691
|
-
|
|
692
|
-
IMPORTANT for "sufficient" judgment:
|
|
693
|
-
- sufficient=true ONLY when the memories contain a concrete ANSWER, fact, decision, or actionable information that directly addresses the query.
|
|
694
|
-
- sufficient=false when the memories only repeat the question, show related topics but lack the specific detail, or contain partial information.
|
|
695
|
-
|
|
696
|
-
Output a JSON object with exactly two fields:
|
|
697
|
-
{"relevant":[1,3,5],"sufficient":true}
|
|
698
|
-
|
|
699
|
-
- "relevant": array of candidate numbers that are useful. Empty array [] if none are relevant.
|
|
700
|
-
- "sufficient": true ONLY if the memories contain a direct answer; false otherwise.
|
|
701
|
-
|
|
702
|
-
Output ONLY the JSON object, nothing else.`;
|
|
703
|
-
|
|
704
|
-
const OPENCLAW_DEDUP_JUDGE_PROMPT = `You are a memory deduplication system. Given a NEW memory summary and several EXISTING memory summaries, determine the relationship.
|
|
705
|
-
|
|
706
|
-
For each EXISTING memory, the NEW memory is either:
|
|
707
|
-
- "DUPLICATE": NEW is fully covered by an EXISTING memory — no new information at all
|
|
708
|
-
- "UPDATE": NEW contains information that supplements or updates an EXISTING memory (new data, status change, additional detail)
|
|
709
|
-
- "NEW": NEW is a different topic/event despite surface similarity
|
|
710
|
-
|
|
711
|
-
Pick the BEST match among all candidates. If none match well, choose "NEW".
|
|
712
|
-
|
|
713
|
-
Output a single JSON object:
|
|
714
|
-
- If DUPLICATE: {"action":"DUPLICATE","targetIndex":2,"reason":"..."}
|
|
715
|
-
- If UPDATE: {"action":"UPDATE","targetIndex":3,"reason":"...","mergedSummary":"a combined summary preserving all info from both old and new, same language as input"}
|
|
716
|
-
- If NEW: {"action":"NEW","reason":"..."}
|
|
717
|
-
|
|
718
|
-
CRITICAL: mergedSummary must use the SAME language as the input. Output ONLY the JSON object.`;
|
|
719
|
-
|
|
@@ -16,7 +16,7 @@ Requirements:
|
|
|
16
16
|
- Non-Chinese: 5-15 words (aim for 8-12)
|
|
17
17
|
- Output title only`;
|
|
18
18
|
|
|
19
|
-
const TASK_SUMMARY_PROMPT = `You create a DETAILED task summary from a multi-turn conversation. This summary will be the ONLY record of this conversation, so it must preserve ALL important information.
|
|
19
|
+
export const TASK_SUMMARY_PROMPT = `You create a DETAILED task summary from a multi-turn conversation. This summary will be the ONLY record of this conversation, so it must preserve ALL important information.
|
|
20
20
|
|
|
21
21
|
## LANGUAGE RULE (HIGHEST PRIORITY)
|
|
22
22
|
Detect the PRIMARY language of the user's messages. If most user messages are Chinese, ALL output (title, goal, steps, result, details) MUST be in Chinese. If English, output in English. NEVER mix. This rule overrides everything below.
|
|
@@ -70,7 +70,7 @@ export async function summarizeTaskOpenAI(
|
|
|
70
70
|
const resp = await fetch(endpoint, {
|
|
71
71
|
method: "POST",
|
|
72
72
|
headers,
|
|
73
|
-
body: JSON.stringify({
|
|
73
|
+
body: JSON.stringify(buildRequestBody(cfg, {
|
|
74
74
|
model,
|
|
75
75
|
temperature: cfg.temperature ?? 0.1,
|
|
76
76
|
max_tokens: 4096,
|
|
@@ -78,7 +78,7 @@ export async function summarizeTaskOpenAI(
|
|
|
78
78
|
{ role: "system", content: TASK_SUMMARY_PROMPT },
|
|
79
79
|
{ role: "user", content: text },
|
|
80
80
|
],
|
|
81
|
-
}),
|
|
81
|
+
})),
|
|
82
82
|
signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
|
|
83
83
|
});
|
|
84
84
|
|
|
@@ -119,7 +119,7 @@ export async function generateTaskTitleOpenAI(
|
|
|
119
119
|
const resp = await fetch(endpoint, {
|
|
120
120
|
method: "POST",
|
|
121
121
|
headers,
|
|
122
|
-
body: JSON.stringify({
|
|
122
|
+
body: JSON.stringify(buildRequestBody(cfg, {
|
|
123
123
|
model,
|
|
124
124
|
temperature: 0,
|
|
125
125
|
max_tokens: 100,
|
|
@@ -127,7 +127,7 @@ export async function generateTaskTitleOpenAI(
|
|
|
127
127
|
{ role: "system", content: TASK_TITLE_PROMPT },
|
|
128
128
|
{ role: "user", content: text },
|
|
129
129
|
],
|
|
130
|
-
}),
|
|
130
|
+
})),
|
|
131
131
|
signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
|
|
132
132
|
});
|
|
133
133
|
|
|
@@ -156,14 +156,14 @@ export async function summarizeOpenAI(
|
|
|
156
156
|
const resp = await fetch(endpoint, {
|
|
157
157
|
method: "POST",
|
|
158
158
|
headers,
|
|
159
|
-
body: JSON.stringify({
|
|
159
|
+
body: JSON.stringify(buildRequestBody(cfg, {
|
|
160
160
|
model,
|
|
161
161
|
temperature: cfg.temperature ?? 0,
|
|
162
162
|
messages: [
|
|
163
163
|
{ role: "system", content: SYSTEM_PROMPT },
|
|
164
164
|
{ role: "user", content: `[TEXT TO SUMMARIZE]\n${text}\n[/TEXT TO SUMMARIZE]` },
|
|
165
165
|
],
|
|
166
|
-
}),
|
|
166
|
+
})),
|
|
167
167
|
signal: AbortSignal.timeout(cfg.timeoutMs ?? 30_000),
|
|
168
168
|
});
|
|
169
169
|
|
|
@@ -178,7 +178,7 @@ export async function summarizeOpenAI(
|
|
|
178
178
|
return json.choices[0]?.message?.content?.trim() ?? "";
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
const TOPIC_JUDGE_PROMPT = `You are a conversation topic boundary detector. Given the CURRENT task context and a NEW user message, decide if the new message belongs to the SAME task or starts a NEW one.
|
|
181
|
+
export const TOPIC_JUDGE_PROMPT = `You are a conversation topic boundary detector. Given the CURRENT task context and a NEW user message, decide if the new message belongs to the SAME task or starts a NEW one.
|
|
182
182
|
|
|
183
183
|
Answer ONLY "NEW" or "SAME".
|
|
184
184
|
|
|
@@ -223,7 +223,7 @@ export async function judgeNewTopicOpenAI(
|
|
|
223
223
|
const resp = await fetch(endpoint, {
|
|
224
224
|
method: "POST",
|
|
225
225
|
headers,
|
|
226
|
-
body: JSON.stringify({
|
|
226
|
+
body: JSON.stringify(buildRequestBody(cfg, {
|
|
227
227
|
model,
|
|
228
228
|
temperature: 0,
|
|
229
229
|
max_tokens: 10,
|
|
@@ -231,7 +231,7 @@ export async function judgeNewTopicOpenAI(
|
|
|
231
231
|
{ role: "system", content: TOPIC_JUDGE_PROMPT },
|
|
232
232
|
{ role: "user", content: userContent },
|
|
233
233
|
],
|
|
234
|
-
}),
|
|
234
|
+
})),
|
|
235
235
|
signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
|
|
236
236
|
});
|
|
237
237
|
|
|
@@ -246,7 +246,7 @@ export async function judgeNewTopicOpenAI(
|
|
|
246
246
|
return answer.startsWith("NEW");
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
const FILTER_RELEVANT_PROMPT = `You are a memory relevance judge.
|
|
249
|
+
export const FILTER_RELEVANT_PROMPT = `You are a memory relevance judge.
|
|
250
250
|
|
|
251
251
|
Given a QUERY and CANDIDATE memories, decide: does each candidate's content contain information that would HELP ANSWER the query?
|
|
252
252
|
|
|
@@ -293,7 +293,7 @@ export async function filterRelevantOpenAI(
|
|
|
293
293
|
const resp = await fetch(endpoint, {
|
|
294
294
|
method: "POST",
|
|
295
295
|
headers,
|
|
296
|
-
body: JSON.stringify({
|
|
296
|
+
body: JSON.stringify(buildRequestBody(cfg, {
|
|
297
297
|
model,
|
|
298
298
|
temperature: 0,
|
|
299
299
|
max_tokens: 200,
|
|
@@ -301,7 +301,7 @@ export async function filterRelevantOpenAI(
|
|
|
301
301
|
{ role: "system", content: FILTER_RELEVANT_PROMPT },
|
|
302
302
|
{ role: "user", content: `QUERY: ${query}\n\nCANDIDATES:\n${candidateText}` },
|
|
303
303
|
],
|
|
304
|
-
}),
|
|
304
|
+
})),
|
|
305
305
|
signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
|
|
306
306
|
});
|
|
307
307
|
|
|
@@ -385,7 +385,7 @@ export async function judgeDedupOpenAI(
|
|
|
385
385
|
const resp = await fetch(endpoint, {
|
|
386
386
|
method: "POST",
|
|
387
387
|
headers,
|
|
388
|
-
body: JSON.stringify({
|
|
388
|
+
body: JSON.stringify(buildRequestBody(cfg, {
|
|
389
389
|
model,
|
|
390
390
|
temperature: 0,
|
|
391
391
|
max_tokens: 300,
|
|
@@ -393,7 +393,7 @@ export async function judgeDedupOpenAI(
|
|
|
393
393
|
{ role: "system", content: DEDUP_JUDGE_PROMPT },
|
|
394
394
|
{ role: "user", content: `NEW MEMORY:\n${newSummary}\n\nEXISTING MEMORIES:\n${candidateText}` },
|
|
395
395
|
],
|
|
396
|
-
}),
|
|
396
|
+
})),
|
|
397
397
|
signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
|
|
398
398
|
});
|
|
399
399
|
|
|
@@ -432,3 +432,20 @@ function normalizeChatEndpoint(url: string): string {
|
|
|
432
432
|
if (stripped.endsWith("/completions")) return stripped;
|
|
433
433
|
return `${stripped}/chat/completions`;
|
|
434
434
|
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Zhipu AI (glm-4.7, glm-5, etc.) uses reasoning tokens that consume max_tokens budget,
|
|
438
|
+
* leaving no room for actual output. This helper injects {"thinking":{"type":"disabled"}}
|
|
439
|
+
* for zhipu endpoints to disable the built-in reasoning mode.
|
|
440
|
+
*/
|
|
441
|
+
function isZhipuEndpoint(endpoint: string): boolean {
|
|
442
|
+
return /bigmodel\.cn|zhipuai/.test(endpoint);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function buildRequestBody(cfg: SummarizerConfig, body: Record<string, unknown>): Record<string, unknown> {
|
|
446
|
+
const endpoint = cfg.endpoint ?? "";
|
|
447
|
+
if (isZhipuEndpoint(endpoint)) {
|
|
448
|
+
body.thinking = { type: "disabled" };
|
|
449
|
+
}
|
|
450
|
+
return body;
|
|
451
|
+
}
|
package/src/recall/engine.ts
CHANGED
|
@@ -74,49 +74,58 @@ export class RecallEngine {
|
|
|
74
74
|
score: 1 / (i + 1),
|
|
75
75
|
}));
|
|
76
76
|
|
|
77
|
-
// Step 1c: Hub memories
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
// embedding mismatch.
|
|
77
|
+
// Step 1c: Hub memories — two-stage retrieval (no cached embeddings).
|
|
78
|
+
// Stage 1: FTS + pattern to get candidates.
|
|
79
|
+
// Stage 2: embed candidates on-the-fly + cosine rerank.
|
|
81
80
|
let hubMemFtsRanked: Array<{ id: string; score: number }> = [];
|
|
82
81
|
let hubMemVecRanked: Array<{ id: string; score: number }> = [];
|
|
83
82
|
let hubMemPatternRanked: Array<{ id: string; score: number }> = [];
|
|
84
83
|
if (query && this.ctx.config.sharing?.enabled && this.ctx.config.sharing.role === "hub") {
|
|
84
|
+
// Stage 1: cheap text retrieval
|
|
85
|
+
const hubCandidateTexts = new Map<string, string>();
|
|
85
86
|
try {
|
|
86
87
|
const hubFtsHits = this.store.searchHubMemories(query, { maxResults: candidatePool });
|
|
87
|
-
hubMemFtsRanked = hubFtsHits.map(({ hit }, i) =>
|
|
88
|
-
id
|
|
89
|
-
|
|
88
|
+
hubMemFtsRanked = hubFtsHits.map(({ hit }, i) => {
|
|
89
|
+
hubCandidateTexts.set(hit.id, (hit.summary || hit.content || "").slice(0, 500));
|
|
90
|
+
return { id: `hubmem:${hit.id}`, score: 1 / (i + 1) };
|
|
91
|
+
});
|
|
90
92
|
} catch { /* hub_memories table may not exist */ }
|
|
91
93
|
if (shortTerms.length > 0) {
|
|
92
94
|
try {
|
|
93
95
|
const hubPatternHits = this.store.hubMemoryPatternSearch(shortTerms, { limit: candidatePool });
|
|
94
|
-
hubMemPatternRanked = hubPatternHits.map((h, i) =>
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
hubMemPatternRanked = hubPatternHits.map((h, i) => {
|
|
97
|
+
hubCandidateTexts.set(h.memoryId, (h.content || "").slice(0, 500));
|
|
98
|
+
return { id: `hubmem:${h.memoryId}`, score: 1 / (i + 1) };
|
|
99
|
+
});
|
|
97
100
|
} catch { /* best-effort */ }
|
|
98
101
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
|
|
103
|
+
// Stage 2: embed candidates on-the-fly and cosine rerank
|
|
104
|
+
if (hubCandidateTexts.size > 0) {
|
|
105
|
+
try {
|
|
102
106
|
const qv = await this.embedder.embedQuery(query).catch(() => null);
|
|
103
107
|
if (qv) {
|
|
108
|
+
const ids = [...hubCandidateTexts.keys()];
|
|
109
|
+
const texts = ids.map(id => hubCandidateTexts.get(id)!);
|
|
110
|
+
const vecs = await this.embedder.embed(texts);
|
|
104
111
|
const scored: Array<{ id: string; score: number }> = [];
|
|
105
|
-
for (
|
|
112
|
+
for (let j = 0; j < ids.length; j++) {
|
|
113
|
+
if (!vecs[j]) continue;
|
|
114
|
+
const v = vecs[j];
|
|
106
115
|
let dot = 0, nA = 0, nB = 0;
|
|
107
|
-
for (let i = 0; i < qv.length && i <
|
|
108
|
-
dot += qv[i] *
|
|
116
|
+
for (let i = 0; i < qv.length && i < v.length; i++) {
|
|
117
|
+
dot += qv[i] * v[i]; nA += qv[i] * qv[i]; nB += v[i] * v[i];
|
|
109
118
|
}
|
|
110
119
|
const sim = nA > 0 && nB > 0 ? dot / (Math.sqrt(nA) * Math.sqrt(nB)) : 0;
|
|
111
120
|
if (sim > 0.3) {
|
|
112
|
-
scored.push({ id: `hubmem:${
|
|
121
|
+
scored.push({ id: `hubmem:${ids[j]}`, score: sim });
|
|
113
122
|
}
|
|
114
123
|
}
|
|
115
124
|
scored.sort((a, b) => b.score - a.score);
|
|
116
125
|
hubMemVecRanked = scored.slice(0, candidatePool);
|
|
117
126
|
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
127
|
+
} catch { /* best-effort */ }
|
|
128
|
+
}
|
|
120
129
|
const hubTotal = hubMemFtsRanked.length + hubMemVecRanked.length + hubMemPatternRanked.length;
|
|
121
130
|
if (hubTotal > 0) {
|
|
122
131
|
this.ctx.log.debug(`recall: hub_memories candidates: fts=${hubMemFtsRanked.length}, vec=${hubMemVecRanked.length}, pattern=${hubMemPatternRanked.length}`);
|