@memtensor/memos-local-openclaw-plugin 0.1.3 → 0.1.4
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/.env.example +13 -5
- package/README.md +177 -97
- package/dist/capture/index.d.ts +5 -7
- package/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +72 -43
- package/dist/capture/index.js.map +1 -1
- package/dist/ingest/providers/anthropic.d.ts +2 -0
- package/dist/ingest/providers/anthropic.d.ts.map +1 -1
- package/dist/ingest/providers/anthropic.js +110 -1
- package/dist/ingest/providers/anthropic.js.map +1 -1
- package/dist/ingest/providers/bedrock.d.ts +2 -5
- package/dist/ingest/providers/bedrock.d.ts.map +1 -1
- package/dist/ingest/providers/bedrock.js +110 -6
- package/dist/ingest/providers/bedrock.js.map +1 -1
- package/dist/ingest/providers/gemini.d.ts +2 -0
- package/dist/ingest/providers/gemini.d.ts.map +1 -1
- package/dist/ingest/providers/gemini.js +106 -1
- package/dist/ingest/providers/gemini.js.map +1 -1
- package/dist/ingest/providers/index.d.ts +9 -0
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +66 -4
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/ingest/providers/openai.d.ts +2 -0
- package/dist/ingest/providers/openai.d.ts.map +1 -1
- package/dist/ingest/providers/openai.js +112 -1
- package/dist/ingest/providers/openai.js.map +1 -1
- package/dist/ingest/task-processor.d.ts +63 -0
- package/dist/ingest/task-processor.d.ts.map +1 -0
- package/dist/ingest/task-processor.js +339 -0
- package/dist/ingest/task-processor.js.map +1 -0
- package/dist/ingest/worker.d.ts +1 -1
- package/dist/ingest/worker.d.ts.map +1 -1
- package/dist/ingest/worker.js +18 -13
- package/dist/ingest/worker.js.map +1 -1
- package/dist/recall/engine.d.ts +1 -0
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +21 -11
- package/dist/recall/engine.js.map +1 -1
- package/dist/recall/mmr.d.ts.map +1 -1
- package/dist/recall/mmr.js +3 -1
- package/dist/recall/mmr.js.map +1 -1
- package/dist/storage/sqlite.d.ts +67 -1
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +251 -5
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -0
- 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 +919 -123
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +3 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +59 -1
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +217 -42
- package/openclaw.plugin.json +20 -45
- package/package.json +3 -4
- package/skill/SKILL.md +59 -0
- package/src/capture/index.ts +85 -45
- package/src/ingest/providers/anthropic.ts +128 -1
- package/src/ingest/providers/bedrock.ts +130 -6
- package/src/ingest/providers/gemini.ts +128 -1
- package/src/ingest/providers/index.ts +74 -8
- package/src/ingest/providers/openai.ts +130 -1
- package/src/ingest/task-processor.ts +380 -0
- package/src/ingest/worker.ts +21 -15
- package/src/recall/engine.ts +22 -12
- package/src/recall/mmr.ts +3 -1
- package/src/storage/sqlite.ts +298 -5
- package/src/types.ts +19 -0
- package/src/viewer/html.ts +919 -123
- package/src/viewer/server.ts +63 -1
- package/SKILL.md +0 -43
- package/www/index.html +0 -632
package/openclaw.plugin.json
CHANGED
|
@@ -1,57 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "memos-local",
|
|
3
|
+
"name": "MemOS Local Memory",
|
|
4
|
+
"description": "Full-write local conversation memory with hybrid search (RRF + MMR + recency). Provides memory_search, memory_get, task_summary, memory_timeline, memory_viewer for layered retrieval.",
|
|
3
5
|
"kind": "memory",
|
|
6
|
+
"version": "0.1.4",
|
|
7
|
+
"homepage": "https://github.com/MemTensor/MemOS/tree/main/apps/memos-local-openclaw",
|
|
4
8
|
"configSchema": {
|
|
5
9
|
"type": "object",
|
|
6
10
|
"additionalProperties": true,
|
|
11
|
+
"description": "Configuration for MemOS Local Memory. Use Raw mode to edit embedding/summarizer settings.",
|
|
7
12
|
"properties": {
|
|
8
|
-
"
|
|
9
|
-
"type": "
|
|
10
|
-
"
|
|
11
|
-
"provider": { "type": "string" },
|
|
12
|
-
"endpoint": { "type": "string" },
|
|
13
|
-
"apiKey": { "type": "string" },
|
|
14
|
-
"model": { "type": "string" }
|
|
15
|
-
}
|
|
16
|
-
},
|
|
17
|
-
"summarizer": {
|
|
18
|
-
"type": "object",
|
|
19
|
-
"properties": {
|
|
20
|
-
"provider": { "type": "string" },
|
|
21
|
-
"endpoint": { "type": "string" },
|
|
22
|
-
"apiKey": { "type": "string" },
|
|
23
|
-
"model": { "type": "string" },
|
|
24
|
-
"temperature": { "type": "number" }
|
|
25
|
-
}
|
|
13
|
+
"viewerPort": {
|
|
14
|
+
"type": "number",
|
|
15
|
+
"description": "Memory Viewer HTTP port (default 18799)"
|
|
26
16
|
}
|
|
27
17
|
}
|
|
28
18
|
},
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"label": "Embedding Model",
|
|
42
|
-
"placeholder": "bge-m3"
|
|
43
|
-
},
|
|
44
|
-
"summarizer.endpoint": {
|
|
45
|
-
"label": "Summarizer Endpoint",
|
|
46
|
-
"placeholder": "https://api.openai.com/v1"
|
|
47
|
-
},
|
|
48
|
-
"summarizer.apiKey": {
|
|
49
|
-
"label": "Summarizer API Key",
|
|
50
|
-
"sensitive": true
|
|
51
|
-
},
|
|
52
|
-
"summarizer.model": {
|
|
53
|
-
"label": "Summarizer Model",
|
|
54
|
-
"placeholder": "gpt-4o-mini"
|
|
55
|
-
}
|
|
19
|
+
"requirements": {
|
|
20
|
+
"node": ">=18.0.0",
|
|
21
|
+
"openclaw": ">=2026.2.0"
|
|
22
|
+
},
|
|
23
|
+
"setup": {
|
|
24
|
+
"postInstall": "npm install --omit=dev",
|
|
25
|
+
"notes": [
|
|
26
|
+
"After install, add to ~/.openclaw/openclaw.json: plugins.slots.memory = \"memos-local\"",
|
|
27
|
+
"Set agents.defaults.memorySearch.enabled = false to disable OpenClaw's built-in memory",
|
|
28
|
+
"Restart the gateway: openclaw gateway stop && openclaw gateway start",
|
|
29
|
+
"Memory Viewer will be available at http://127.0.0.1:18799"
|
|
30
|
+
]
|
|
56
31
|
}
|
|
57
32
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memtensor/memos-local-openclaw-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "MemOS Local memory plugin for OpenClaw — full-write, hybrid-recall, progressive retrieval",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -9,11 +9,10 @@
|
|
|
9
9
|
"index.ts",
|
|
10
10
|
"src",
|
|
11
11
|
"dist",
|
|
12
|
+
"skill",
|
|
12
13
|
"openclaw.plugin.json",
|
|
13
|
-
"SKILL.md",
|
|
14
14
|
"README.md",
|
|
15
|
-
".env.example"
|
|
16
|
-
"www"
|
|
15
|
+
".env.example"
|
|
17
16
|
],
|
|
18
17
|
"openclaw": {
|
|
19
18
|
"extensions": [
|
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: memos-local
|
|
3
|
+
description: "Local long-term conversation memory (MemOS). All past conversations are recorded and searchable. Use this skill whenever the conversation involves user-specific information such as identity, preferences, project details, past decisions, or personal facts. Also use it when you cannot fully answer a question or would otherwise need to ask the user for more details — always search memory first, because the answer may already exist in a previous conversation. Similarly, when the user references something from the past (e.g. 'last time', 'as before', 'continue'), search memory to retrieve the relevant context. Start with memory_search for lightweight hits, then drill down with memory_get, task_summary, or memory_timeline as needed."
|
|
4
|
+
metadata: { "openclaw": { "emoji": "🧠" } }
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# MemOS — Long-Term Memory (Local)
|
|
8
|
+
|
|
9
|
+
You have a local long-term memory that records conversation history. When you need historical information, use the tools below to search and drill down, aiming to get **enough to answer** with the **shortest context**.
|
|
10
|
+
|
|
11
|
+
## Tools
|
|
12
|
+
|
|
13
|
+
| Tool | What it does |
|
|
14
|
+
| ----------------- | ------------------------------------------------------------------------ |
|
|
15
|
+
| `memory_search` | Lightweight search of conversation history. Returns hit summaries + IDs/refs (information may be truncated/compressed). |
|
|
16
|
+
| `memory_get` | Fetch the **full original text** of a hit by `chunkId` (use when the summary is insufficient / the original is longer but you only need this one entry). |
|
|
17
|
+
| `task_summary` | Get the **full task-level context summary** for the task a hit belongs to by `taskId` (use when you judge that more key information may be in other turns of the same task). |
|
|
18
|
+
| `memory_timeline` | Expand the context before and after a hit using its `ref` (use when you need the cause-and-effect / chronological details of a conversation). |
|
|
19
|
+
| `memory_viewer` | Returns the Memory Viewer URL (http://127.0.0.1:18799). |
|
|
20
|
+
|
|
21
|
+
## When to Trigger a Search (Trigger)
|
|
22
|
+
|
|
23
|
+
Trigger a search when any of the following applies:
|
|
24
|
+
|
|
25
|
+
* The current context is insufficient to answer definitively (missing key parameters/links/paths/versions/decisions, etc.)
|
|
26
|
+
* You are about to ask the user for information (try searching memory first)
|
|
27
|
+
* The user references history ("last time / before / continue / as previously planned")
|
|
28
|
+
* You need user-specific information (preferences, identity, project config, directory structure, deployment method, etc.)
|
|
29
|
+
|
|
30
|
+
## Layered Retrieval Strategy (Minimum Information → Progressive Completion)
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
Step 1: memory_search(query="keywords")
|
|
34
|
+
- Goal: quickly get 1-6 most relevant hits (summary + chunkId/taskId/ref)
|
|
35
|
+
|
|
36
|
+
Step 2: Sufficiency check
|
|
37
|
+
- If hit summaries are enough to support the answer → answer directly, stop drilling down
|
|
38
|
+
- If summary is insufficient/truncated but "looks useful" → memory_get(chunkId)
|
|
39
|
+
|
|
40
|
+
Step 3: Related but missing broader context → task_summary(taskId)
|
|
41
|
+
- Applies when: you judge that missing information may be in other turns of the same task
|
|
42
|
+
- Note: conversations are split into tasks by topic/time; each hit typically belongs to a taskId
|
|
43
|
+
- Example: search hits "steps to apply for GPT key", but the summary lacks the website link/prerequisites
|
|
44
|
+
→ calling task_summary retrieves the more complete context and details for that task
|
|
45
|
+
|
|
46
|
+
Step 4: Still need cause-and-effect / chronological details → memory_timeline(ref)
|
|
47
|
+
- Applies when: you need to expand several turns before and after a hit to fill in details and context
|
|
48
|
+
|
|
49
|
+
Step 5: Still insufficient
|
|
50
|
+
- Identify the specific missing fields and ask the user a minimal follow-up question
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Rules
|
|
54
|
+
|
|
55
|
+
* **If you don't know the answer, or you are about to ask the user for clarification/details, you MUST call `memory_search` first.** The user may have already provided this information in a previous conversation. Never say "I don't know" or ask the user without searching memory first.
|
|
56
|
+
* Always `memory_search` first, then decide whether to use `memory_get` / `task_summary` / `memory_timeline`
|
|
57
|
+
* `memory_get` is for "summary isn't enough but this hit is very likely the answer" — avoid pulling in excessive irrelevant context
|
|
58
|
+
* `task_summary` is for "hit is relevant, but the answer may be scattered across other parts of the same task" — use the task-level summary to fill in at once
|
|
59
|
+
* `memory_timeline` should only be used when you genuinely need surrounding context — avoid unnecessarily expanding the context window
|
package/src/capture/index.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { ConversationMessage, Role, Logger } from "../types";
|
|
2
|
-
import { DEFAULTS } from "../types";
|
|
3
2
|
|
|
4
3
|
const SKIP_ROLES: Set<Role> = new Set(["system"]);
|
|
5
4
|
|
|
@@ -10,20 +9,33 @@ const SELF_TOOLS = new Set([
|
|
|
10
9
|
"memory_viewer",
|
|
11
10
|
]);
|
|
12
11
|
|
|
12
|
+
// OpenClaw inbound metadata sentinels — these are AI-facing prefixes,
|
|
13
|
+
// not user content. Must be stripped before storing as memory.
|
|
14
|
+
const INBOUND_META_SENTINELS = [
|
|
15
|
+
"Conversation info (untrusted metadata):",
|
|
16
|
+
"Sender (untrusted metadata):",
|
|
17
|
+
"Thread starter (untrusted, for context):",
|
|
18
|
+
"Replied message (untrusted, for context):",
|
|
19
|
+
"Forwarded message context (untrusted metadata):",
|
|
20
|
+
"Chat history since last reply (untrusted, for context):",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const SENTINEL_FAST_RE = new RegExp(
|
|
24
|
+
INBOUND_META_SENTINELS.map(s => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"),
|
|
25
|
+
);
|
|
26
|
+
|
|
13
27
|
/**
|
|
14
|
-
*
|
|
28
|
+
* Extract writable messages from a conversation turn.
|
|
15
29
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* - Truncate long tool results to avoid storage bloat
|
|
20
|
-
* - Strip injected evidence blocks wrapped in [STORED_MEMORY]...[/STORED_MEMORY]
|
|
30
|
+
* Stores the user's actual text — strips only OpenClaw's injected metadata
|
|
31
|
+
* prefixes (Sender info, conversation context, etc.) which are not user content.
|
|
32
|
+
* Only skips: system prompts and our own memory tool results (prevents loop).
|
|
21
33
|
*/
|
|
22
34
|
export function captureMessages(
|
|
23
35
|
messages: Array<{ role: string; content: string; toolName?: string }>,
|
|
24
36
|
sessionKey: string,
|
|
25
37
|
turnId: string,
|
|
26
|
-
|
|
38
|
+
_evidenceTag: string,
|
|
27
39
|
log: Logger,
|
|
28
40
|
): ConversationMessage[] {
|
|
29
41
|
const now = Date.now();
|
|
@@ -34,39 +46,24 @@ export function captureMessages(
|
|
|
34
46
|
if (SKIP_ROLES.has(role)) continue;
|
|
35
47
|
if (!msg.content || msg.content.trim().length === 0) continue;
|
|
36
48
|
|
|
37
|
-
if (role === "tool") {
|
|
38
|
-
|
|
39
|
-
log.debug(`Skipping self-tool result: ${msg.toolName}`);
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
let content = msg.content.trim();
|
|
44
|
-
const maxChars = DEFAULTS.toolResultMaxChars;
|
|
45
|
-
if (content.length > maxChars) {
|
|
46
|
-
content = content.slice(0, maxChars) + `\n\n[truncated — original ${content.length} chars]`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const toolLabel = msg.toolName ? `[tool:${msg.toolName}] ` : "[tool] ";
|
|
50
|
-
result.push({
|
|
51
|
-
role: "tool",
|
|
52
|
-
content: toolLabel + content,
|
|
53
|
-
timestamp: now,
|
|
54
|
-
turnId,
|
|
55
|
-
sessionKey,
|
|
56
|
-
toolName: msg.toolName,
|
|
57
|
-
});
|
|
49
|
+
if (role === "tool" && msg.toolName && SELF_TOOLS.has(msg.toolName)) {
|
|
50
|
+
log.debug(`Skipping self-tool result: ${msg.toolName}`);
|
|
58
51
|
continue;
|
|
59
52
|
}
|
|
60
53
|
|
|
61
|
-
|
|
62
|
-
if (
|
|
54
|
+
let content = msg.content;
|
|
55
|
+
if (role === "user") {
|
|
56
|
+
content = stripInboundMetadata(content);
|
|
57
|
+
}
|
|
58
|
+
if (!content.trim()) continue;
|
|
63
59
|
|
|
64
60
|
result.push({
|
|
65
61
|
role,
|
|
66
|
-
content
|
|
62
|
+
content,
|
|
67
63
|
timestamp: now,
|
|
68
64
|
turnId,
|
|
69
65
|
sessionKey,
|
|
66
|
+
toolName: role === "tool" ? msg.toolName : undefined,
|
|
70
67
|
});
|
|
71
68
|
}
|
|
72
69
|
|
|
@@ -74,19 +71,62 @@ export function captureMessages(
|
|
|
74
71
|
return result;
|
|
75
72
|
}
|
|
76
73
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Strip OpenClaw-injected inbound metadata blocks from user messages.
|
|
76
|
+
*
|
|
77
|
+
* These blocks have the shape:
|
|
78
|
+
* Sender (untrusted metadata):
|
|
79
|
+
* ```json
|
|
80
|
+
* { "label": "...", "id": "..." }
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* Also strips the envelope timestamp prefix like "[Tue 2026-03-03 21:58 GMT+8] "
|
|
84
|
+
*/
|
|
85
|
+
function stripInboundMetadata(text: string): string {
|
|
86
|
+
if (!SENTINEL_FAST_RE.test(text)) return text;
|
|
87
|
+
|
|
88
|
+
const lines = text.split("\n");
|
|
89
|
+
const result: string[] = [];
|
|
90
|
+
let inMetaBlock = false;
|
|
91
|
+
let inFencedJson = false;
|
|
92
|
+
|
|
93
|
+
for (let i = 0; i < lines.length; i++) {
|
|
94
|
+
const line = lines[i];
|
|
95
|
+
const trimmed = line.trim();
|
|
96
|
+
|
|
97
|
+
if (!inMetaBlock && INBOUND_META_SENTINELS.some(s => s === trimmed)) {
|
|
98
|
+
if (lines[i + 1]?.trim() === "```json") {
|
|
99
|
+
inMetaBlock = true;
|
|
100
|
+
inFencedJson = false;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
// Sentinel without fenced JSON — skip this line only
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (inMetaBlock) {
|
|
108
|
+
if (!inFencedJson && trimmed === "```json") {
|
|
109
|
+
inFencedJson = true;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (inFencedJson && trimmed === "```") {
|
|
113
|
+
inMetaBlock = false;
|
|
114
|
+
inFencedJson = false;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
result.push(line);
|
|
89
121
|
}
|
|
90
122
|
|
|
91
|
-
|
|
123
|
+
let cleaned = result.join("\n").trim();
|
|
124
|
+
|
|
125
|
+
// Strip envelope timestamp prefix: "[Tue 2026-03-03 21:58 GMT+8] actual message"
|
|
126
|
+
cleaned = cleaned.replace(
|
|
127
|
+
/^\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}(?::\d{2})?\s+[A-Z]{3}[+-]\d{1,2}\]\s*/,
|
|
128
|
+
"",
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
return cleaned;
|
|
92
132
|
}
|
|
@@ -1,6 +1,133 @@
|
|
|
1
1
|
import type { SummarizerConfig, Logger } from "../../types";
|
|
2
2
|
|
|
3
|
-
const SYSTEM_PROMPT = `Summarize the text in ONE concise sentence (max
|
|
3
|
+
const SYSTEM_PROMPT = `Summarize the text in ONE concise sentence (max 120 characters). IMPORTANT: Use the SAME language as the input text — if the input is Chinese, write Chinese; if English, write English. Preserve exact names, commands, error codes. No bullet points, no preamble — output only the sentence.`;
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
|
|
9
|
+
Output EXACTLY this structure:
|
|
10
|
+
|
|
11
|
+
📌 Title
|
|
12
|
+
A short, descriptive title (10-30 characters). Like a chat group name.
|
|
13
|
+
|
|
14
|
+
🎯 Goal
|
|
15
|
+
One sentence: what the user wanted to accomplish.
|
|
16
|
+
|
|
17
|
+
📋 Key Steps
|
|
18
|
+
- Describe each meaningful step in detail
|
|
19
|
+
- Include the ACTUAL content produced: code snippets, commands, config blocks, formulas, key paragraphs
|
|
20
|
+
- For code: include the function signature and core logic (up to ~30 lines per block), use fenced code blocks
|
|
21
|
+
- For configs: include the actual config values and structure
|
|
22
|
+
- For lists/instructions: include the actual items, not just "provided a list"
|
|
23
|
+
- Merge only truly trivial back-and-forth (like "ok" / "sure")
|
|
24
|
+
- Do NOT over-summarize: "provided a function" is BAD; show the actual function
|
|
25
|
+
|
|
26
|
+
✅ Result
|
|
27
|
+
What was the final outcome? Include the final version of any code/config/content produced.
|
|
28
|
+
|
|
29
|
+
💡 Key Details
|
|
30
|
+
- Decisions made, trade-offs discussed, caveats noted, alternative approaches mentioned
|
|
31
|
+
- Specific values: numbers, versions, thresholds, URLs, file paths, model names
|
|
32
|
+
- Omit this section only if there truly are no noteworthy details
|
|
33
|
+
|
|
34
|
+
RULES:
|
|
35
|
+
- This summary is a KNOWLEDGE BASE ENTRY, not a brief note. Be thorough.
|
|
36
|
+
- PRESERVE verbatim: code, commands, URLs, file paths, error messages, config values, version numbers, names, amounts
|
|
37
|
+
- DISCARD only: greetings, filler, the assistant explaining what it will do before doing it
|
|
38
|
+
- Replace secrets (API keys, tokens, passwords) with [REDACTED]
|
|
39
|
+
- Target length: 30-50% of the original conversation length. Longer conversations need longer summaries.
|
|
40
|
+
- Output summary only, no preamble.`;
|
|
41
|
+
|
|
42
|
+
export async function summarizeTaskAnthropic(
|
|
43
|
+
text: string,
|
|
44
|
+
cfg: SummarizerConfig,
|
|
45
|
+
log: Logger,
|
|
46
|
+
): Promise<string> {
|
|
47
|
+
const endpoint = cfg.endpoint ?? "https://api.anthropic.com/v1/messages";
|
|
48
|
+
const model = cfg.model ?? "claude-3-haiku-20240307";
|
|
49
|
+
const headers: Record<string, string> = {
|
|
50
|
+
"Content-Type": "application/json",
|
|
51
|
+
"x-api-key": cfg.apiKey ?? "",
|
|
52
|
+
"anthropic-version": "2023-06-01",
|
|
53
|
+
...cfg.headers,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const resp = await fetch(endpoint, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers,
|
|
59
|
+
body: JSON.stringify({
|
|
60
|
+
model,
|
|
61
|
+
max_tokens: 4096,
|
|
62
|
+
temperature: cfg.temperature ?? 0.1,
|
|
63
|
+
system: TASK_SUMMARY_PROMPT,
|
|
64
|
+
messages: [{ role: "user", content: text }],
|
|
65
|
+
}),
|
|
66
|
+
signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (!resp.ok) {
|
|
70
|
+
const body = await resp.text();
|
|
71
|
+
throw new Error(`Anthropic task-summarize failed (${resp.status}): ${body}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const json = (await resp.json()) as { content: Array<{ type: string; text: string }> };
|
|
75
|
+
return json.content.find((c) => c.type === "text")?.text?.trim() ?? "";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const 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.
|
|
79
|
+
|
|
80
|
+
Answer ONLY "NEW" or "SAME".
|
|
81
|
+
|
|
82
|
+
Rules:
|
|
83
|
+
- "NEW" = the new message is about a completely different subject, project, or task
|
|
84
|
+
- "SAME" = the new message continues, follows up on, or is closely related to the current topic
|
|
85
|
+
- Follow-up questions, clarifications, refinements, bug fixes, or next steps on the same task = SAME
|
|
86
|
+
- Greetings or meta-questions like "你好" or "谢谢" without new substance = SAME
|
|
87
|
+
- A clearly unrelated request (e.g., current topic is deployment, new message asks about cooking) = NEW
|
|
88
|
+
|
|
89
|
+
Output exactly one word: NEW or SAME`;
|
|
90
|
+
|
|
91
|
+
export async function judgeNewTopicAnthropic(
|
|
92
|
+
currentContext: string,
|
|
93
|
+
newMessage: string,
|
|
94
|
+
cfg: SummarizerConfig,
|
|
95
|
+
log: Logger,
|
|
96
|
+
): Promise<boolean> {
|
|
97
|
+
const endpoint = cfg.endpoint ?? "https://api.anthropic.com/v1/messages";
|
|
98
|
+
const model = cfg.model ?? "claude-3-haiku-20240307";
|
|
99
|
+
const headers: Record<string, string> = {
|
|
100
|
+
"Content-Type": "application/json",
|
|
101
|
+
"x-api-key": cfg.apiKey ?? "",
|
|
102
|
+
"anthropic-version": "2023-06-01",
|
|
103
|
+
...cfg.headers,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const userContent = `CURRENT CONVERSATION SUMMARY:\n${currentContext}\n\nNEW USER MESSAGE:\n${newMessage}`;
|
|
107
|
+
|
|
108
|
+
const resp = await fetch(endpoint, {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers,
|
|
111
|
+
body: JSON.stringify({
|
|
112
|
+
model,
|
|
113
|
+
max_tokens: 10,
|
|
114
|
+
temperature: 0,
|
|
115
|
+
system: TOPIC_JUDGE_PROMPT,
|
|
116
|
+
messages: [{ role: "user", content: userContent }],
|
|
117
|
+
}),
|
|
118
|
+
signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
if (!resp.ok) {
|
|
122
|
+
const body = await resp.text();
|
|
123
|
+
throw new Error(`Anthropic topic-judge failed (${resp.status}): ${body}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const json = (await resp.json()) as { content: Array<{ type: string; text: string }> };
|
|
127
|
+
const answer = json.content.find((c) => c.type === "text")?.text?.trim().toUpperCase() ?? "";
|
|
128
|
+
log.debug(`Topic judge result: "${answer}"`);
|
|
129
|
+
return answer.startsWith("NEW");
|
|
130
|
+
}
|
|
4
131
|
|
|
5
132
|
export async function summarizeAnthropic(
|
|
6
133
|
text: string,
|
|
@@ -1,12 +1,136 @@
|
|
|
1
1
|
import type { SummarizerConfig, Logger } from "../../types";
|
|
2
2
|
|
|
3
|
-
const SYSTEM_PROMPT = `Summarize the text in ONE concise sentence (max
|
|
3
|
+
const SYSTEM_PROMPT = `Summarize the text in ONE concise sentence (max 120 characters). IMPORTANT: Use the SAME language as the input text — if the input is Chinese, write Chinese; if English, write English. Preserve exact names, commands, error codes. No bullet points, no preamble — output only the sentence.`;
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
|
|
9
|
+
Output EXACTLY this structure:
|
|
10
|
+
|
|
11
|
+
📌 Title
|
|
12
|
+
A short, descriptive title (10-30 characters). Like a chat group name.
|
|
13
|
+
|
|
14
|
+
🎯 Goal
|
|
15
|
+
One sentence: what the user wanted to accomplish.
|
|
16
|
+
|
|
17
|
+
📋 Key Steps
|
|
18
|
+
- Describe each meaningful step in detail
|
|
19
|
+
- Include the ACTUAL content produced: code snippets, commands, config blocks, formulas, key paragraphs
|
|
20
|
+
- For code: include the function signature and core logic (up to ~30 lines per block), use fenced code blocks
|
|
21
|
+
- For configs: include the actual config values and structure
|
|
22
|
+
- For lists/instructions: include the actual items, not just "provided a list"
|
|
23
|
+
- Merge only truly trivial back-and-forth (like "ok" / "sure")
|
|
24
|
+
- Do NOT over-summarize: "provided a function" is BAD; show the actual function
|
|
25
|
+
|
|
26
|
+
✅ Result
|
|
27
|
+
What was the final outcome? Include the final version of any code/config/content produced.
|
|
28
|
+
|
|
29
|
+
💡 Key Details
|
|
30
|
+
- Decisions made, trade-offs discussed, caveats noted, alternative approaches mentioned
|
|
31
|
+
- Specific values: numbers, versions, thresholds, URLs, file paths, model names
|
|
32
|
+
- Omit this section only if there truly are no noteworthy details
|
|
33
|
+
|
|
34
|
+
RULES:
|
|
35
|
+
- This summary is a KNOWLEDGE BASE ENTRY, not a brief note. Be thorough.
|
|
36
|
+
- PRESERVE verbatim: code, commands, URLs, file paths, error messages, config values, version numbers, names, amounts
|
|
37
|
+
- DISCARD only: greetings, filler, the assistant explaining what it will do before doing it
|
|
38
|
+
- Replace secrets (API keys, tokens, passwords) with [REDACTED]
|
|
39
|
+
- Target length: 30-50% of the original conversation length. Longer conversations need longer summaries.
|
|
40
|
+
- Output summary only, no preamble.`;
|
|
41
|
+
|
|
42
|
+
export async function summarizeTaskBedrock(
|
|
43
|
+
text: string,
|
|
44
|
+
cfg: SummarizerConfig,
|
|
45
|
+
log: Logger,
|
|
46
|
+
): Promise<string> {
|
|
47
|
+
const model = cfg.model ?? "anthropic.claude-3-haiku-20240307-v1:0";
|
|
48
|
+
const endpoint = cfg.endpoint;
|
|
49
|
+
if (!endpoint) {
|
|
50
|
+
throw new Error("Bedrock task-summarizer requires 'endpoint'");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const url = `${endpoint}/model/${model}/converse`;
|
|
54
|
+
const headers: Record<string, string> = {
|
|
55
|
+
"Content-Type": "application/json",
|
|
56
|
+
...cfg.headers,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const resp = await fetch(url, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers,
|
|
62
|
+
body: JSON.stringify({
|
|
63
|
+
system: [{ text: TASK_SUMMARY_PROMPT }],
|
|
64
|
+
messages: [{ role: "user", content: [{ text }] }],
|
|
65
|
+
inferenceConfig: { temperature: cfg.temperature ?? 0.1, maxTokens: 4096 },
|
|
66
|
+
}),
|
|
67
|
+
signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (!resp.ok) {
|
|
71
|
+
const body = await resp.text();
|
|
72
|
+
throw new Error(`Bedrock task-summarize failed (${resp.status}): ${body}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const json = (await resp.json()) as { output: { message: { content: Array<{ text: string }> } } };
|
|
76
|
+
return json.output?.message?.content?.[0]?.text?.trim() ?? "";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const 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.
|
|
80
|
+
|
|
81
|
+
Answer ONLY "NEW" or "SAME".
|
|
82
|
+
|
|
83
|
+
Rules:
|
|
84
|
+
- "NEW" = the new message is about a completely different subject, project, or task
|
|
85
|
+
- "SAME" = the new message continues, follows up on, or is closely related to the current topic
|
|
86
|
+
- Follow-up questions, clarifications, refinements, bug fixes, or next steps on the same task = SAME
|
|
87
|
+
- Greetings or meta-questions like "你好" or "谢谢" without new substance = SAME
|
|
88
|
+
- A clearly unrelated request (e.g., current topic is deployment, new message asks about cooking) = NEW
|
|
89
|
+
|
|
90
|
+
Output exactly one word: NEW or SAME`;
|
|
91
|
+
|
|
92
|
+
export async function judgeNewTopicBedrock(
|
|
93
|
+
currentContext: string,
|
|
94
|
+
newMessage: string,
|
|
95
|
+
cfg: SummarizerConfig,
|
|
96
|
+
log: Logger,
|
|
97
|
+
): Promise<boolean> {
|
|
98
|
+
const model = cfg.model ?? "anthropic.claude-3-haiku-20240307-v1:0";
|
|
99
|
+
const endpoint = cfg.endpoint;
|
|
100
|
+
if (!endpoint) {
|
|
101
|
+
throw new Error("Bedrock topic-judge requires 'endpoint'");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const url = `${endpoint}/model/${model}/converse`;
|
|
105
|
+
const headers: Record<string, string> = {
|
|
106
|
+
"Content-Type": "application/json",
|
|
107
|
+
...cfg.headers,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const userContent = `CURRENT CONVERSATION SUMMARY:\n${currentContext}\n\nNEW USER MESSAGE:\n${newMessage}`;
|
|
111
|
+
|
|
112
|
+
const resp = await fetch(url, {
|
|
113
|
+
method: "POST",
|
|
114
|
+
headers,
|
|
115
|
+
body: JSON.stringify({
|
|
116
|
+
system: [{ text: TOPIC_JUDGE_PROMPT }],
|
|
117
|
+
messages: [{ role: "user", content: [{ text: userContent }] }],
|
|
118
|
+
inferenceConfig: { temperature: 0, maxTokens: 10 },
|
|
119
|
+
}),
|
|
120
|
+
signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (!resp.ok) {
|
|
124
|
+
const body = await resp.text();
|
|
125
|
+
throw new Error(`Bedrock topic-judge failed (${resp.status}): ${body}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const json = (await resp.json()) as { output: { message: { content: Array<{ text: string }> } } };
|
|
129
|
+
const answer = json.output?.message?.content?.[0]?.text?.trim().toUpperCase() ?? "";
|
|
130
|
+
log.debug(`Topic judge result: "${answer}"`);
|
|
131
|
+
return answer.startsWith("NEW");
|
|
132
|
+
}
|
|
4
133
|
|
|
5
|
-
/**
|
|
6
|
-
* AWS Bedrock Converse API adapter.
|
|
7
|
-
* Expects cfg.endpoint to be the full Bedrock invoke URL and
|
|
8
|
-
* authentication handled via AWS SDK credential chain (env vars / IAM role).
|
|
9
|
-
*/
|
|
10
134
|
export async function summarizeBedrock(
|
|
11
135
|
text: string,
|
|
12
136
|
cfg: SummarizerConfig,
|