@memtensor/memos-local-openclaw-plugin 0.3.20 → 1.0.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/README.md +239 -22
- package/dist/capture/index.d.ts +1 -1
- package/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +33 -8
- package/dist/capture/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/ingest/providers/anthropic.d.ts.map +1 -1
- package/dist/ingest/providers/anthropic.js +22 -8
- package/dist/ingest/providers/anthropic.js.map +1 -1
- package/dist/ingest/providers/bedrock.d.ts.map +1 -1
- package/dist/ingest/providers/bedrock.js +22 -8
- package/dist/ingest/providers/bedrock.js.map +1 -1
- package/dist/ingest/providers/gemini.d.ts.map +1 -1
- package/dist/ingest/providers/gemini.js +22 -8
- package/dist/ingest/providers/gemini.js.map +1 -1
- package/dist/ingest/providers/index.d.ts +13 -18
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +213 -139
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/ingest/providers/openai.d.ts +1 -1
- package/dist/ingest/providers/openai.d.ts.map +1 -1
- package/dist/ingest/providers/openai.js +37 -17
- package/dist/ingest/providers/openai.js.map +1 -1
- package/dist/ingest/task-processor.d.ts +28 -3
- package/dist/ingest/task-processor.d.ts.map +1 -1
- package/dist/ingest/task-processor.js +166 -67
- package/dist/ingest/task-processor.js.map +1 -1
- package/dist/ingest/worker.d.ts.map +1 -1
- package/dist/ingest/worker.js +97 -75
- package/dist/ingest/worker.js.map +1 -1
- package/dist/shared/llm-call.d.ts +26 -0
- package/dist/shared/llm-call.d.ts.map +1 -0
- package/dist/shared/llm-call.js +163 -0
- package/dist/shared/llm-call.js.map +1 -0
- package/dist/skill/evaluator.d.ts +0 -3
- package/dist/skill/evaluator.d.ts.map +1 -1
- package/dist/skill/evaluator.js +34 -59
- package/dist/skill/evaluator.js.map +1 -1
- package/dist/skill/evolver.d.ts +22 -1
- package/dist/skill/evolver.d.ts.map +1 -1
- package/dist/skill/evolver.js +191 -32
- package/dist/skill/evolver.js.map +1 -1
- package/dist/skill/generator.d.ts +0 -3
- package/dist/skill/generator.d.ts.map +1 -1
- package/dist/skill/generator.js +15 -50
- package/dist/skill/generator.js.map +1 -1
- package/dist/skill/upgrader.d.ts +0 -2
- package/dist/skill/upgrader.d.ts.map +1 -1
- package/dist/skill/upgrader.js +4 -39
- package/dist/skill/upgrader.js.map +1 -1
- package/dist/skill/validator.d.ts +0 -2
- package/dist/skill/validator.d.ts.map +1 -1
- package/dist/skill/validator.js +14 -44
- package/dist/skill/validator.js.map +1 -1
- package/dist/storage/sqlite.d.ts +13 -2
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +92 -15
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/tools/memory-get.d.ts.map +1 -1
- package/dist/tools/memory-get.js +5 -1
- package/dist/tools/memory-get.js.map +1 -1
- package/dist/tools/memory-search.d.ts.map +1 -1
- package/dist/tools/memory-search.js +5 -0
- package/dist/tools/memory-search.js.map +1 -1
- package/dist/tools/memory-timeline.d.ts.map +1 -1
- package/dist/tools/memory-timeline.js +11 -2
- package/dist/tools/memory-timeline.js.map +1 -1
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/dist/viewer/html.d.ts +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +380 -26
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +9 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +549 -184
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +9 -3
- package/package.json +2 -1
- package/src/capture/index.ts +39 -10
- package/src/index.ts +3 -2
- package/src/ingest/providers/anthropic.ts +22 -8
- package/src/ingest/providers/bedrock.ts +22 -8
- package/src/ingest/providers/gemini.ts +22 -8
- package/src/ingest/providers/index.ts +192 -142
- package/src/ingest/providers/openai.ts +37 -17
- package/src/ingest/task-processor.ts +183 -65
- package/src/ingest/worker.ts +98 -77
- package/src/shared/llm-call.ts +144 -0
- package/src/skill/evaluator.ts +35 -64
- package/src/skill/evolver.ts +201 -33
- package/src/skill/generator.ts +16 -59
- package/src/skill/upgrader.ts +5 -43
- package/src/skill/validator.ts +15 -47
- package/src/storage/sqlite.ts +107 -15
- package/src/tools/memory-get.ts +6 -1
- package/src/tools/memory-search.ts +6 -0
- package/src/tools/memory-timeline.ts +13 -1
- package/src/types.ts +2 -1
- package/src/viewer/html.ts +380 -26
- package/src/viewer/server.ts +535 -197
|
@@ -76,16 +76,30 @@ export async function summarizeTaskBedrock(
|
|
|
76
76
|
return json.output?.message?.content?.[0]?.text?.trim() ?? "";
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
const TOPIC_JUDGE_PROMPT = `You are a conversation topic boundary detector. Given
|
|
79
|
+
const TOPIC_JUDGE_PROMPT = `You are a conversation topic boundary detector. Given the CURRENT task context (may include opening topic + recent exchanges) and a single NEW user message, decide if the new message belongs to the SAME task or starts a NEW one.
|
|
80
80
|
|
|
81
81
|
Answer ONLY "NEW" or "SAME".
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
-
|
|
85
|
-
-
|
|
86
|
-
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
83
|
+
SAME — the new message:
|
|
84
|
+
- Continues, follows up on, refines, or corrects the same subject/project/task
|
|
85
|
+
- Asks a clarification or next-step question about what was just discussed
|
|
86
|
+
- Reports a result, error, or feedback about the current task
|
|
87
|
+
- Discusses different tools, methods, or approaches for the SAME goal (e.g., learning English via BBC → via ChatGPT → via AI tools = all SAME "learning English" task)
|
|
88
|
+
- Mentions a related technology or platform in the context of the current goal
|
|
89
|
+
- Is a short acknowledgment (ok, thanks, 好的, 嗯) in direct response to the current flow
|
|
90
|
+
|
|
91
|
+
NEW — the new message:
|
|
92
|
+
- Introduces a clearly UNRELATED subject with NO logical connection to the current task
|
|
93
|
+
- The topic has ZERO overlap with any aspect of the current conversation (e.g., from "learning English" to "what's the weather tomorrow")
|
|
94
|
+
- Starts a request about a completely different domain or life area
|
|
95
|
+
- Begins with a new greeting/reset followed by a different topic
|
|
96
|
+
|
|
97
|
+
Key principles:
|
|
98
|
+
- STRONGLY lean toward SAME — only mark NEW for obvious, unambiguous topic shifts
|
|
99
|
+
- Different aspects, tools, or methods related to the same overall goal are SAME
|
|
100
|
+
- If the new message could reasonably be interpreted as part of the ongoing discussion, choose SAME
|
|
101
|
+
- Only choose NEW when there is absolutely no thematic connection to the current task
|
|
102
|
+
- Examples: "学英语" → "用AI工具学英语" = SAME; "学英语" → "明天天气" = NEW
|
|
89
103
|
|
|
90
104
|
Output exactly one word: NEW or SAME`;
|
|
91
105
|
|
|
@@ -107,7 +121,7 @@ export async function judgeNewTopicBedrock(
|
|
|
107
121
|
...cfg.headers,
|
|
108
122
|
};
|
|
109
123
|
|
|
110
|
-
const userContent = `CURRENT
|
|
124
|
+
const userContent = `CURRENT TASK CONTEXT:\n${currentContext}\n\n---\n\nNEW USER MESSAGE:\n${newMessage}`;
|
|
111
125
|
|
|
112
126
|
const resp = await fetch(url, {
|
|
113
127
|
method: "POST",
|
|
@@ -75,16 +75,30 @@ export async function summarizeTaskGemini(
|
|
|
75
75
|
return json.candidates?.[0]?.content?.parts?.[0]?.text?.trim() ?? "";
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
const TOPIC_JUDGE_PROMPT = `You are a conversation topic boundary detector. Given
|
|
78
|
+
const TOPIC_JUDGE_PROMPT = `You are a conversation topic boundary detector. Given the CURRENT task context (may include opening topic + recent exchanges) and a single NEW user message, decide if the new message belongs to the SAME task or starts a NEW one.
|
|
79
79
|
|
|
80
80
|
Answer ONLY "NEW" or "SAME".
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
-
|
|
84
|
-
-
|
|
85
|
-
-
|
|
86
|
-
-
|
|
87
|
-
-
|
|
82
|
+
SAME — the new message:
|
|
83
|
+
- Continues, follows up on, refines, or corrects the same subject/project/task
|
|
84
|
+
- Asks a clarification or next-step question about what was just discussed
|
|
85
|
+
- Reports a result, error, or feedback about the current task
|
|
86
|
+
- Discusses different tools, methods, or approaches for the SAME goal (e.g., learning English via BBC → via ChatGPT → via AI tools = all SAME "learning English" task)
|
|
87
|
+
- Mentions a related technology or platform in the context of the current goal
|
|
88
|
+
- Is a short acknowledgment (ok, thanks, 好的, 嗯) in direct response to the current flow
|
|
89
|
+
|
|
90
|
+
NEW — the new message:
|
|
91
|
+
- Introduces a clearly UNRELATED subject with NO logical connection to the current task
|
|
92
|
+
- The topic has ZERO overlap with any aspect of the current conversation (e.g., from "learning English" to "what's the weather tomorrow")
|
|
93
|
+
- Starts a request about a completely different domain or life area
|
|
94
|
+
- Begins with a new greeting/reset followed by a different topic
|
|
95
|
+
|
|
96
|
+
Key principles:
|
|
97
|
+
- STRONGLY lean toward SAME — only mark NEW for obvious, unambiguous topic shifts
|
|
98
|
+
- Different aspects, tools, or methods related to the same overall goal are SAME
|
|
99
|
+
- If the new message could reasonably be interpreted as part of the ongoing discussion, choose SAME
|
|
100
|
+
- Only choose NEW when there is absolutely no thematic connection to the current task
|
|
101
|
+
- Examples: "学英语" → "用AI工具学英语" = SAME; "学英语" → "明天天气" = NEW
|
|
88
102
|
|
|
89
103
|
Output exactly one word: NEW or SAME`;
|
|
90
104
|
|
|
@@ -105,7 +119,7 @@ export async function judgeNewTopicGemini(
|
|
|
105
119
|
...cfg.headers,
|
|
106
120
|
};
|
|
107
121
|
|
|
108
|
-
const userContent = `CURRENT
|
|
122
|
+
const userContent = `CURRENT TASK CONTEXT:\n${currentContext}\n\n---\n\nNEW USER MESSAGE:\n${newMessage}`;
|
|
109
123
|
|
|
110
124
|
const resp = await fetch(url, {
|
|
111
125
|
method: "POST",
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
1
3
|
import type { SummarizerConfig, Logger } from "../../types";
|
|
2
4
|
import { summarizeOpenAI, summarizeTaskOpenAI, judgeNewTopicOpenAI, filterRelevantOpenAI, judgeDedupOpenAI } from "./openai";
|
|
3
5
|
import type { FilterResult, DedupResult } from "./openai";
|
|
@@ -6,190 +8,238 @@ import { summarizeAnthropic, summarizeTaskAnthropic, judgeNewTopicAnthropic, fil
|
|
|
6
8
|
import { summarizeGemini, summarizeTaskGemini, judgeNewTopicGemini, filterRelevantGemini, judgeDedupGemini } from "./gemini";
|
|
7
9
|
import { summarizeBedrock, summarizeTaskBedrock, judgeNewTopicBedrock, filterRelevantBedrock, judgeDedupBedrock } from "./bedrock";
|
|
8
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Build a SummarizerConfig from OpenClaw's native model configuration (openclaw.json).
|
|
13
|
+
* This serves as the final fallback when both strongCfg and plugin summarizer fail or are absent.
|
|
14
|
+
*/
|
|
15
|
+
function loadOpenClawFallbackConfig(log: Logger): SummarizerConfig | undefined {
|
|
16
|
+
try {
|
|
17
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
18
|
+
const cfgPath = path.join(home, ".openclaw", "openclaw.json");
|
|
19
|
+
if (!fs.existsSync(cfgPath)) return undefined;
|
|
20
|
+
|
|
21
|
+
const raw = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
|
|
22
|
+
|
|
23
|
+
const agentModel: string | undefined = raw?.agents?.defaults?.model?.primary;
|
|
24
|
+
if (!agentModel) return undefined;
|
|
25
|
+
|
|
26
|
+
const [providerKey, modelId] = agentModel.includes("/")
|
|
27
|
+
? agentModel.split("/", 2)
|
|
28
|
+
: [undefined, agentModel];
|
|
29
|
+
|
|
30
|
+
const providerCfg = providerKey
|
|
31
|
+
? raw?.models?.providers?.[providerKey]
|
|
32
|
+
: Object.values(raw?.models?.providers ?? {})[0] as any;
|
|
33
|
+
if (!providerCfg) return undefined;
|
|
34
|
+
|
|
35
|
+
const baseUrl: string | undefined = providerCfg.baseUrl;
|
|
36
|
+
const apiKey: string | undefined = providerCfg.apiKey;
|
|
37
|
+
if (!baseUrl || !apiKey) return undefined;
|
|
38
|
+
|
|
39
|
+
const endpoint = baseUrl.endsWith("/chat/completions")
|
|
40
|
+
? baseUrl
|
|
41
|
+
: baseUrl.replace(/\/+$/, "") + "/chat/completions";
|
|
42
|
+
|
|
43
|
+
log.debug(`OpenClaw fallback model: ${modelId} via ${baseUrl}`);
|
|
44
|
+
return {
|
|
45
|
+
provider: "openai_compatible",
|
|
46
|
+
endpoint,
|
|
47
|
+
apiKey,
|
|
48
|
+
model: modelId,
|
|
49
|
+
};
|
|
50
|
+
} catch (err) {
|
|
51
|
+
log.debug(`Failed to load OpenClaw fallback config: ${err}`);
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
9
56
|
export class Summarizer {
|
|
57
|
+
private strongCfg: SummarizerConfig | undefined;
|
|
58
|
+
private fallbackCfg: SummarizerConfig | undefined;
|
|
59
|
+
|
|
10
60
|
constructor(
|
|
11
61
|
private cfg: SummarizerConfig | undefined,
|
|
12
62
|
private log: Logger,
|
|
13
|
-
|
|
63
|
+
strongCfg?: SummarizerConfig,
|
|
64
|
+
) {
|
|
65
|
+
this.strongCfg = strongCfg;
|
|
66
|
+
this.fallbackCfg = loadOpenClawFallbackConfig(log);
|
|
67
|
+
}
|
|
14
68
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Ordered config chain: strongCfg → cfg → fallbackCfg (OpenClaw native model).
|
|
71
|
+
* Returns configs that are defined, in priority order.
|
|
72
|
+
*/
|
|
73
|
+
private getConfigChain(): SummarizerConfig[] {
|
|
74
|
+
const chain: SummarizerConfig[] = [];
|
|
75
|
+
if (this.strongCfg) chain.push(this.strongCfg);
|
|
76
|
+
if (this.cfg) chain.push(this.cfg);
|
|
77
|
+
if (this.fallbackCfg) chain.push(this.fallbackCfg);
|
|
78
|
+
return chain;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Try calling fn with each config in the chain until one succeeds.
|
|
83
|
+
* Returns undefined if all fail.
|
|
84
|
+
*/
|
|
85
|
+
private async tryChain<T>(
|
|
86
|
+
label: string,
|
|
87
|
+
fn: (cfg: SummarizerConfig) => Promise<T>,
|
|
88
|
+
): Promise<T | undefined> {
|
|
89
|
+
const chain = this.getConfigChain();
|
|
90
|
+
for (let i = 0; i < chain.length; i++) {
|
|
91
|
+
try {
|
|
92
|
+
return await fn(chain[i]);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
const level = i < chain.length - 1 ? "warn" : "error";
|
|
95
|
+
const modelInfo = `${chain[i].provider}/${chain[i].model ?? "?"}`;
|
|
96
|
+
this.log[level](`${label} failed (${modelInfo}), ${i < chain.length - 1 ? "trying next" : "no more fallbacks"}: ${err}`);
|
|
97
|
+
}
|
|
18
98
|
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
19
101
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} catch (err) {
|
|
23
|
-
this.log.warn(`Summarizer provider failed, using rule fallback: ${err}`);
|
|
102
|
+
async summarize(text: string): Promise<string> {
|
|
103
|
+
if (!this.cfg && !this.fallbackCfg) {
|
|
24
104
|
return ruleFallback(text);
|
|
25
105
|
}
|
|
106
|
+
|
|
107
|
+
const result = await this.tryChain("summarize", (cfg) => callSummarize(cfg, text, this.log));
|
|
108
|
+
return result ?? ruleFallback(text);
|
|
26
109
|
}
|
|
27
110
|
|
|
28
111
|
async summarizeTask(text: string): Promise<string> {
|
|
29
|
-
if (!this.cfg) {
|
|
30
|
-
return taskFallback(text);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
return await this.callTaskProvider(text);
|
|
35
|
-
} catch (err) {
|
|
36
|
-
this.log.warn(`Task summarizer failed, using fallback: ${err}`);
|
|
112
|
+
if (!this.cfg && !this.fallbackCfg) {
|
|
37
113
|
return taskFallback(text);
|
|
38
114
|
}
|
|
39
|
-
}
|
|
40
115
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
switch (cfg.provider) {
|
|
44
|
-
case "openai":
|
|
45
|
-
case "openai_compatible":
|
|
46
|
-
return summarizeOpenAI(text, cfg, this.log);
|
|
47
|
-
case "anthropic":
|
|
48
|
-
return summarizeAnthropic(text, cfg, this.log);
|
|
49
|
-
case "gemini":
|
|
50
|
-
return summarizeGemini(text, cfg, this.log);
|
|
51
|
-
case "azure_openai":
|
|
52
|
-
return summarizeOpenAI(text, cfg, this.log);
|
|
53
|
-
case "bedrock":
|
|
54
|
-
return summarizeBedrock(text, cfg, this.log);
|
|
55
|
-
default:
|
|
56
|
-
throw new Error(`Unknown summarizer provider: ${cfg.provider}`);
|
|
57
|
-
}
|
|
116
|
+
const result = await this.tryChain("summarizeTask", (cfg) => callSummarizeTask(cfg, text, this.log));
|
|
117
|
+
return result ?? taskFallback(text);
|
|
58
118
|
}
|
|
59
119
|
|
|
60
|
-
/**
|
|
61
|
-
* Ask the LLM whether the new message starts a different topic from the current conversation.
|
|
62
|
-
* Returns true if it's a new topic, false if it continues the current one.
|
|
63
|
-
* Returns null if no summarizer is configured (caller should fall back to heuristic).
|
|
64
|
-
*/
|
|
65
120
|
async judgeNewTopic(currentContext: string, newMessage: string): Promise<boolean | null> {
|
|
66
|
-
if (!this.cfg) return null;
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
return await this.callTopicJudge(currentContext, newMessage);
|
|
70
|
-
} catch (err) {
|
|
71
|
-
this.log.warn(`Topic judge failed: ${err}`);
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
121
|
+
if (!this.cfg && !this.fallbackCfg) return null;
|
|
75
122
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
switch (cfg.provider) {
|
|
79
|
-
case "openai":
|
|
80
|
-
case "openai_compatible":
|
|
81
|
-
case "azure_openai":
|
|
82
|
-
return judgeNewTopicOpenAI(currentContext, newMessage, cfg, this.log);
|
|
83
|
-
case "anthropic":
|
|
84
|
-
return judgeNewTopicAnthropic(currentContext, newMessage, cfg, this.log);
|
|
85
|
-
case "gemini":
|
|
86
|
-
return judgeNewTopicGemini(currentContext, newMessage, cfg, this.log);
|
|
87
|
-
case "bedrock":
|
|
88
|
-
return judgeNewTopicBedrock(currentContext, newMessage, cfg, this.log);
|
|
89
|
-
default:
|
|
90
|
-
throw new Error(`Unknown summarizer provider: ${cfg.provider}`);
|
|
91
|
-
}
|
|
123
|
+
const result = await this.tryChain("judgeNewTopic", (cfg) => callTopicJudge(cfg, currentContext, newMessage, this.log));
|
|
124
|
+
return result ?? null;
|
|
92
125
|
}
|
|
93
126
|
|
|
94
|
-
/**
|
|
95
|
-
* Filter search results by LLM relevance judgment.
|
|
96
|
-
* Returns { relevant: number[], sufficient: boolean } or null if no summarizer configured.
|
|
97
|
-
*/
|
|
98
127
|
async filterRelevant(
|
|
99
128
|
query: string,
|
|
100
129
|
candidates: Array<{ index: number; summary: string; role: string }>,
|
|
101
130
|
): Promise<FilterResult | null> {
|
|
102
|
-
if (!this.cfg) return null;
|
|
131
|
+
if (!this.cfg && !this.fallbackCfg) return null;
|
|
103
132
|
if (candidates.length === 0) return { relevant: [], sufficient: true };
|
|
104
133
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
} catch (err) {
|
|
108
|
-
this.log.warn(`filterRelevant failed, returning all candidates: ${err}`);
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
private async callFilterRelevant(
|
|
114
|
-
query: string,
|
|
115
|
-
candidates: Array<{ index: number; summary: string; role: string }>,
|
|
116
|
-
): Promise<FilterResult> {
|
|
117
|
-
const cfg = this.cfg!;
|
|
118
|
-
switch (cfg.provider) {
|
|
119
|
-
case "openai":
|
|
120
|
-
case "openai_compatible":
|
|
121
|
-
case "azure_openai":
|
|
122
|
-
return filterRelevantOpenAI(query, candidates, cfg, this.log);
|
|
123
|
-
case "anthropic":
|
|
124
|
-
return filterRelevantAnthropic(query, candidates, cfg, this.log);
|
|
125
|
-
case "gemini":
|
|
126
|
-
return filterRelevantGemini(query, candidates, cfg, this.log);
|
|
127
|
-
case "bedrock":
|
|
128
|
-
return filterRelevantBedrock(query, candidates, cfg, this.log);
|
|
129
|
-
default:
|
|
130
|
-
throw new Error(`Unknown summarizer provider: ${cfg.provider}`);
|
|
131
|
-
}
|
|
134
|
+
const result = await this.tryChain("filterRelevant", (cfg) => callFilterRelevant(cfg, query, candidates, this.log));
|
|
135
|
+
return result ?? null;
|
|
132
136
|
}
|
|
133
137
|
|
|
134
|
-
/**
|
|
135
|
-
* Judge whether a new memory is DUPLICATE / UPDATE / NEW relative to similar existing memories.
|
|
136
|
-
* Returns null if no summarizer configured (caller should treat as NEW).
|
|
137
|
-
*/
|
|
138
138
|
async judgeDedup(
|
|
139
139
|
newSummary: string,
|
|
140
140
|
candidates: Array<{ index: number; summary: string; chunkId: string }>,
|
|
141
141
|
): Promise<DedupResult | null> {
|
|
142
|
-
if (!this.cfg) return null;
|
|
142
|
+
if (!this.cfg && !this.fallbackCfg) return null;
|
|
143
143
|
if (candidates.length === 0) return null;
|
|
144
144
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
} catch (err) {
|
|
148
|
-
this.log.warn(`judgeDedup failed, treating as NEW: ${err}`);
|
|
149
|
-
return { action: "NEW", reason: "llm_error" };
|
|
150
|
-
}
|
|
145
|
+
const result = await this.tryChain("judgeDedup", (cfg) => callJudgeDedup(cfg, newSummary, candidates, this.log));
|
|
146
|
+
return result ?? { action: "NEW", reason: "all_models_failed" };
|
|
151
147
|
}
|
|
152
148
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
candidates: Array<{ index: number; summary: string; chunkId: string }>,
|
|
156
|
-
): Promise<DedupResult> {
|
|
157
|
-
const cfg = this.cfg!;
|
|
158
|
-
switch (cfg.provider) {
|
|
159
|
-
case "openai":
|
|
160
|
-
case "openai_compatible":
|
|
161
|
-
case "azure_openai":
|
|
162
|
-
return judgeDedupOpenAI(newSummary, candidates, cfg, this.log);
|
|
163
|
-
case "anthropic":
|
|
164
|
-
return judgeDedupAnthropic(newSummary, candidates, cfg, this.log);
|
|
165
|
-
case "gemini":
|
|
166
|
-
return judgeDedupGemini(newSummary, candidates, cfg, this.log);
|
|
167
|
-
case "bedrock":
|
|
168
|
-
return judgeDedupBedrock(newSummary, candidates, cfg, this.log);
|
|
169
|
-
default:
|
|
170
|
-
throw new Error(`Unknown summarizer provider: ${cfg.provider}`);
|
|
171
|
-
}
|
|
149
|
+
getStrongConfig(): SummarizerConfig | undefined {
|
|
150
|
+
return this.strongCfg;
|
|
172
151
|
}
|
|
152
|
+
}
|
|
173
153
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
154
|
+
// ─── Dispatch helpers ───
|
|
155
|
+
|
|
156
|
+
function callSummarize(cfg: SummarizerConfig, text: string, log: Logger): Promise<string> {
|
|
157
|
+
switch (cfg.provider) {
|
|
158
|
+
case "openai":
|
|
159
|
+
case "openai_compatible":
|
|
160
|
+
case "azure_openai":
|
|
161
|
+
return summarizeOpenAI(text, cfg, log);
|
|
162
|
+
case "anthropic":
|
|
163
|
+
return summarizeAnthropic(text, cfg, log);
|
|
164
|
+
case "gemini":
|
|
165
|
+
return summarizeGemini(text, cfg, log);
|
|
166
|
+
case "bedrock":
|
|
167
|
+
return summarizeBedrock(text, cfg, log);
|
|
168
|
+
default:
|
|
169
|
+
throw new Error(`Unknown summarizer provider: ${cfg.provider}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function callSummarizeTask(cfg: SummarizerConfig, text: string, log: Logger): Promise<string> {
|
|
174
|
+
switch (cfg.provider) {
|
|
175
|
+
case "openai":
|
|
176
|
+
case "openai_compatible":
|
|
177
|
+
case "azure_openai":
|
|
178
|
+
return summarizeTaskOpenAI(text, cfg, log);
|
|
179
|
+
case "anthropic":
|
|
180
|
+
return summarizeTaskAnthropic(text, cfg, log);
|
|
181
|
+
case "gemini":
|
|
182
|
+
return summarizeTaskGemini(text, cfg, log);
|
|
183
|
+
case "bedrock":
|
|
184
|
+
return summarizeTaskBedrock(text, cfg, log);
|
|
185
|
+
default:
|
|
186
|
+
throw new Error(`Unknown summarizer provider: ${cfg.provider}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function callTopicJudge(cfg: SummarizerConfig, currentContext: string, newMessage: string, log: Logger): Promise<boolean> {
|
|
191
|
+
switch (cfg.provider) {
|
|
192
|
+
case "openai":
|
|
193
|
+
case "openai_compatible":
|
|
194
|
+
case "azure_openai":
|
|
195
|
+
return judgeNewTopicOpenAI(currentContext, newMessage, cfg, log);
|
|
196
|
+
case "anthropic":
|
|
197
|
+
return judgeNewTopicAnthropic(currentContext, newMessage, cfg, log);
|
|
198
|
+
case "gemini":
|
|
199
|
+
return judgeNewTopicGemini(currentContext, newMessage, cfg, log);
|
|
200
|
+
case "bedrock":
|
|
201
|
+
return judgeNewTopicBedrock(currentContext, newMessage, cfg, log);
|
|
202
|
+
default:
|
|
203
|
+
throw new Error(`Unknown summarizer provider: ${cfg.provider}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function callFilterRelevant(cfg: SummarizerConfig, query: string, candidates: Array<{ index: number; summary: string; role: string }>, log: Logger): Promise<FilterResult> {
|
|
208
|
+
switch (cfg.provider) {
|
|
209
|
+
case "openai":
|
|
210
|
+
case "openai_compatible":
|
|
211
|
+
case "azure_openai":
|
|
212
|
+
return filterRelevantOpenAI(query, candidates, cfg, log);
|
|
213
|
+
case "anthropic":
|
|
214
|
+
return filterRelevantAnthropic(query, candidates, cfg, log);
|
|
215
|
+
case "gemini":
|
|
216
|
+
return filterRelevantGemini(query, candidates, cfg, log);
|
|
217
|
+
case "bedrock":
|
|
218
|
+
return filterRelevantBedrock(query, candidates, cfg, log);
|
|
219
|
+
default:
|
|
220
|
+
throw new Error(`Unknown summarizer provider: ${cfg.provider}`);
|
|
190
221
|
}
|
|
191
222
|
}
|
|
192
223
|
|
|
224
|
+
function callJudgeDedup(cfg: SummarizerConfig, newSummary: string, candidates: Array<{ index: number; summary: string; chunkId: string }>, log: Logger): Promise<DedupResult> {
|
|
225
|
+
switch (cfg.provider) {
|
|
226
|
+
case "openai":
|
|
227
|
+
case "openai_compatible":
|
|
228
|
+
case "azure_openai":
|
|
229
|
+
return judgeDedupOpenAI(newSummary, candidates, cfg, log);
|
|
230
|
+
case "anthropic":
|
|
231
|
+
return judgeDedupAnthropic(newSummary, candidates, cfg, log);
|
|
232
|
+
case "gemini":
|
|
233
|
+
return judgeDedupGemini(newSummary, candidates, cfg, log);
|
|
234
|
+
case "bedrock":
|
|
235
|
+
return judgeDedupBedrock(newSummary, candidates, cfg, log);
|
|
236
|
+
default:
|
|
237
|
+
throw new Error(`Unknown summarizer provider: ${cfg.provider}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ─── Fallbacks ───
|
|
242
|
+
|
|
193
243
|
function taskFallback(text: string): string {
|
|
194
244
|
const lines = text.split("\n").filter((l) => l.trim().length > 10);
|
|
195
245
|
return lines.slice(0, 30).join("\n").slice(0, 2000);
|
|
@@ -114,16 +114,30 @@ export async function summarizeOpenAI(
|
|
|
114
114
|
return json.choices[0]?.message?.content?.trim() ?? "";
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
const TOPIC_JUDGE_PROMPT = `You are a conversation topic boundary detector. Given
|
|
117
|
+
const TOPIC_JUDGE_PROMPT = `You are a conversation topic boundary detector. Given the CURRENT task context (may include opening topic + recent exchanges) and a single NEW user message, decide if the new message belongs to the SAME task or starts a NEW one.
|
|
118
118
|
|
|
119
119
|
Answer ONLY "NEW" or "SAME".
|
|
120
120
|
|
|
121
|
-
|
|
122
|
-
-
|
|
123
|
-
-
|
|
124
|
-
-
|
|
125
|
-
-
|
|
126
|
-
-
|
|
121
|
+
SAME — the new message:
|
|
122
|
+
- Continues, follows up on, refines, or corrects the same subject/project/task
|
|
123
|
+
- Asks a clarification or next-step question about what was just discussed
|
|
124
|
+
- Reports a result, error, or feedback about the current task
|
|
125
|
+
- Discusses different tools, methods, or approaches for the SAME goal (e.g., learning English via BBC → via ChatGPT → via AI tools = all SAME "learning English" task)
|
|
126
|
+
- Mentions a related technology or platform in the context of the current goal
|
|
127
|
+
- Is a short acknowledgment (ok, thanks, 好的, 嗯) in direct response to the current flow
|
|
128
|
+
|
|
129
|
+
NEW — the new message:
|
|
130
|
+
- Introduces a clearly UNRELATED subject with NO logical connection to the current task
|
|
131
|
+
- The topic has ZERO overlap with any aspect of the current conversation (e.g., from "learning English" to "what's the weather tomorrow")
|
|
132
|
+
- Starts a request about a completely different domain or life area
|
|
133
|
+
- Begins with a new greeting/reset followed by a different topic
|
|
134
|
+
|
|
135
|
+
Key principles:
|
|
136
|
+
- STRONGLY lean toward SAME — only mark NEW for obvious, unambiguous topic shifts
|
|
137
|
+
- Different aspects, tools, or methods related to the same overall goal are SAME
|
|
138
|
+
- If the new message could reasonably be interpreted as part of the ongoing discussion, choose SAME
|
|
139
|
+
- Only choose NEW when there is absolutely no thematic connection to the current task
|
|
140
|
+
- Examples: "学英语" → "用AI工具学英语" = SAME; "学英语" → "明天天气" = NEW
|
|
127
141
|
|
|
128
142
|
Output exactly one word: NEW or SAME`;
|
|
129
143
|
|
|
@@ -141,7 +155,7 @@ export async function judgeNewTopicOpenAI(
|
|
|
141
155
|
...cfg.headers,
|
|
142
156
|
};
|
|
143
157
|
|
|
144
|
-
const userContent = `CURRENT
|
|
158
|
+
const userContent = `CURRENT TASK CONTEXT:\n${currentContext}\n\n---\n\nNEW USER MESSAGE:\n${newMessage}`;
|
|
145
159
|
|
|
146
160
|
const resp = await fetch(endpoint, {
|
|
147
161
|
method: "POST",
|
|
@@ -258,21 +272,27 @@ function parseFilterResult(raw: string, log: Logger): FilterResult {
|
|
|
258
272
|
|
|
259
273
|
// ─── Smart Dedup: judge whether new memory is DUPLICATE / UPDATE / NEW ───
|
|
260
274
|
|
|
261
|
-
export const DEDUP_JUDGE_PROMPT = `You are a memory deduplication system.
|
|
275
|
+
export const DEDUP_JUDGE_PROMPT = `You are a memory deduplication system.
|
|
276
|
+
|
|
277
|
+
LANGUAGE RULE (MUST FOLLOW): You MUST reply in the SAME language as the input memories. 如果输入是中文,reason 和 mergedSummary 必须用中文。If input is English, reply in English. This applies to ALL text fields in your JSON output.
|
|
278
|
+
|
|
279
|
+
Given a NEW memory summary and several EXISTING memory summaries, determine the relationship.
|
|
262
280
|
|
|
263
281
|
For each EXISTING memory, the NEW memory is either:
|
|
264
|
-
- "DUPLICATE": NEW
|
|
265
|
-
- "UPDATE": NEW contains information that supplements
|
|
266
|
-
- "NEW": NEW
|
|
282
|
+
- "DUPLICATE": NEW conveys the same intent/meaning as an EXISTING memory, even if worded differently. Examples: "请告诉我你的名字" vs "你希望我怎么称呼你"; "新会话已开始" vs "New session started"; greetings with minor variations. If the core information/intent is the same, it IS a duplicate.
|
|
283
|
+
- "UPDATE": NEW contains meaningful additional information that supplements an EXISTING memory (new data, status change, concrete detail not present before)
|
|
284
|
+
- "NEW": NEW covers a genuinely different topic/event with no semantic overlap
|
|
285
|
+
|
|
286
|
+
IMPORTANT: Lean toward DUPLICATE when memories share the same intent, topic, or factual content. Only choose NEW when the topics are truly unrelated. Repetitive conversational patterns (greetings, session starts, identity questions, capability descriptions) across different sessions should be treated as DUPLICATE.
|
|
267
287
|
|
|
268
288
|
Pick the BEST match among all candidates. If none match well, choose "NEW".
|
|
269
289
|
|
|
270
|
-
Output a single JSON object:
|
|
271
|
-
- If DUPLICATE: {"action":"DUPLICATE","targetIndex":2,"reason":"
|
|
272
|
-
- If UPDATE: {"action":"UPDATE","targetIndex":3,"reason":"
|
|
273
|
-
- If NEW: {"action":"NEW","reason":"
|
|
290
|
+
Output a single JSON object (reason and mergedSummary MUST match input language):
|
|
291
|
+
- If DUPLICATE: {"action":"DUPLICATE","targetIndex":2,"reason":"与已有记忆意图相同"}
|
|
292
|
+
- If UPDATE: {"action":"UPDATE","targetIndex":3,"reason":"新记忆补充了额外细节","mergedSummary":"合并后的完整摘要,保留新旧所有信息"}
|
|
293
|
+
- If NEW: {"action":"NEW","reason":"不同主题,无关联"}
|
|
274
294
|
|
|
275
|
-
|
|
295
|
+
Output ONLY the JSON object, no other text.`;
|
|
276
296
|
|
|
277
297
|
export interface DedupResult {
|
|
278
298
|
action: "DUPLICATE" | "UPDATE" | "NEW";
|