memory-lancedb-pro 1.0.8 → 1.0.10
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/CHANGELOG.md +8 -1
- package/README.md +17 -3
- package/README_CN.md +17 -3
- package/examples/new-session-distill/README.md +19 -0
- package/examples/new-session-distill/hook/enqueue-lesson-extract/HOOK.md +21 -0
- package/examples/new-session-distill/hook/enqueue-lesson-extract/handler.ts +102 -0
- package/examples/new-session-distill/worker/lesson-extract-worker.mjs +412 -0
- package/examples/new-session-distill/worker/systemd/lesson-extract-worker.service +20 -0
- package/index.ts +44 -23
- package/openclaw.plugin.json +4 -4
- package/package.json +1 -1
- package/src/retriever.ts +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.10
|
|
4
|
+
|
|
5
|
+
- Fix: avoid blocking OpenClaw gateway startup on external network calls by running startup self-checks in the background with timeouts.
|
|
6
|
+
|
|
7
|
+
## 1.0.9
|
|
8
|
+
|
|
9
|
+
- Change: update default `retrieval.rerankModel` to `jina-reranker-v3` (still fully configurable).
|
|
10
|
+
|
|
3
11
|
## 1.0.8
|
|
4
12
|
|
|
5
13
|
- Add: JSONL distill extractor supports optional agent allowlist via env var `OPENCLAW_JSONL_DISTILL_ALLOWED_AGENT_IDS` (default off / compatible).
|
|
6
14
|
|
|
7
|
-
|
|
8
15
|
## 1.0.7
|
|
9
16
|
|
|
10
17
|
- Fix: resolve `agentId` from hook context (`ctx?.agentId`) for `before_agent_start` and `agent_end`, restoring per-agent scope isolation when using multi-agent setups.
|
package/README.md
CHANGED
|
@@ -314,7 +314,7 @@ openclaw config get plugins.slots.memory
|
|
|
314
314
|
"minScore": 0.3,
|
|
315
315
|
"rerank": "cross-encoder",
|
|
316
316
|
"rerankApiKey": "${JINA_API_KEY}",
|
|
317
|
-
"rerankModel": "jina-reranker-
|
|
317
|
+
"rerankModel": "jina-reranker-v3",
|
|
318
318
|
"rerankEndpoint": "https://api.jina.ai/v1/rerank",
|
|
319
319
|
"rerankProvider": "jina",
|
|
320
320
|
"candidatePoolSize": 20,
|
|
@@ -362,7 +362,7 @@ Cross-encoder reranking supports multiple providers via `rerankProvider`:
|
|
|
362
362
|
|
|
363
363
|
| Provider | `rerankProvider` | Endpoint | Example Model |
|
|
364
364
|
|----------|-----------------|----------|---------------|
|
|
365
|
-
| **Jina** (default) | `jina` | `https://api.jina.ai/v1/rerank` | `jina-reranker-
|
|
365
|
+
| **Jina** (default) | `jina` | `https://api.jina.ai/v1/rerank` | `jina-reranker-v3` |
|
|
366
366
|
| **SiliconFlow** (free tier available) | `siliconflow` | `https://api.siliconflow.com/v1/rerank` | `BAAI/bge-reranker-v2-m3`, `Qwen/Qwen3-Reranker-8B` |
|
|
367
367
|
| **Pinecone** | `pinecone` | `https://api.pinecone.io/rerank` | `bge-reranker-v2-m3` |
|
|
368
368
|
|
|
@@ -410,7 +410,21 @@ OpenClaw already persists **full session transcripts** as JSONL files:
|
|
|
410
410
|
|
|
411
411
|
This plugin focuses on **high-quality long-term memory**. If you dump raw transcripts into LanceDB, retrieval quality quickly degrades.
|
|
412
412
|
|
|
413
|
-
Instead,
|
|
413
|
+
Instead, **recommended (2026-02+)** is a **non-blocking `/new` pipeline**:
|
|
414
|
+
|
|
415
|
+
- Trigger: `command:new` (you type `/new`)
|
|
416
|
+
- Hook: enqueue a tiny JSON task file (fast; no LLM calls inside the hook)
|
|
417
|
+
- Worker: a user-level systemd service watches the inbox and runs **Gemini Map-Reduce** on the session JSONL transcript
|
|
418
|
+
- Store: writes **0–20** high-signal, atomic lessons into LanceDB Pro via `openclaw memory-pro import`
|
|
419
|
+
- Keywords: each memory includes `Keywords (zh)` with a simple taxonomy (Entity + Action + Symptom). Entity keywords must be copied verbatim from the transcript (no hallucinated project names).
|
|
420
|
+
- Notify: optional Telegram/Discord notification (even if 0 lessons)
|
|
421
|
+
|
|
422
|
+
See the self-contained example files in:
|
|
423
|
+
- `examples/new-session-distill/`
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
Legacy option: an **hourly distiller** cron that:
|
|
414
428
|
|
|
415
429
|
1) Incrementally reads only the **newly appended tail** of each session JSONL (byte-offset cursor)
|
|
416
430
|
2) Filters noise (tool output, injected `<relevant-memories>`, logs, boilerplate)
|
package/README_CN.md
CHANGED
|
@@ -112,7 +112,7 @@ Query → BM25 FTS ─────┘
|
|
|
112
112
|
|
|
113
113
|
### 2. 跨编码器 Rerank
|
|
114
114
|
|
|
115
|
-
- **Jina Reranker API**: `jina-reranker-
|
|
115
|
+
- **Jina Reranker API**: `jina-reranker-v3`(5s 超时保护)
|
|
116
116
|
- **混合评分**: 60% cross-encoder score + 40% 原始融合分
|
|
117
117
|
- **降级策略**: API 失败时回退到 cosine similarity rerank
|
|
118
118
|
|
|
@@ -315,7 +315,7 @@ openclaw config get plugins.slots.memory
|
|
|
315
315
|
"minScore": 0.3,
|
|
316
316
|
"rerank": "cross-encoder",
|
|
317
317
|
"rerankApiKey": "${JINA_API_KEY}",
|
|
318
|
-
"rerankModel": "jina-reranker-
|
|
318
|
+
"rerankModel": "jina-reranker-v3",
|
|
319
319
|
"candidatePoolSize": 20,
|
|
320
320
|
"recencyHalfLifeDays": 14,
|
|
321
321
|
"recencyWeight": 0.1,
|
|
@@ -365,7 +365,21 @@ OpenClaw 会把每个 Agent 的完整会话自动落盘为 JSONL:
|
|
|
365
365
|
|
|
366
366
|
但 JSONL 含大量噪声(tool 输出、系统块、重复回调等),**不建议直接把原文塞进 LanceDB**。
|
|
367
367
|
|
|
368
|
-
|
|
368
|
+
**推荐方案(2026-02+)**:使用 **/new 非阻塞沉淀管线**(Hooks + systemd worker),在你执行 `/new` 时异步提取高价值经验并写入 LanceDB Pro:
|
|
369
|
+
|
|
370
|
+
- 触发:`command:new`(你在聊天里发送 `/new`)
|
|
371
|
+
- Hook:只投递一个很小的 task.json(毫秒级,不调用 LLM,不阻塞 `/new`)
|
|
372
|
+
- Worker:systemd 常驻进程监听队列,读取 session `.jsonl`,用 Gemini **Map-Reduce** 抽取 0~20 条高信噪比记忆
|
|
373
|
+
- 写入:通过 `openclaw memory-pro import` 写入 LanceDB Pro(插件内部仍会 embedding + 查重)
|
|
374
|
+
- 中文关键词:每条记忆包含 `Keywords (zh)`,并遵循三要素(实体/动作/症状)。其中“实体关键词”必须从 transcript 原文逐字拷贝(禁止编造项目名)。
|
|
375
|
+
- 通知:可选(可做到即使 0 条也通知)
|
|
376
|
+
|
|
377
|
+
示例文件:
|
|
378
|
+
- `examples/new-session-distill/`
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
Legacy 方案:本插件也提供一个安全的 extractor 脚本 `scripts/jsonl_distill.py`,配合 OpenClaw 的 `cron` + 独立 distiller agent,实现“增量蒸馏 → 高质量记忆入库”:(适合不依赖 `/new` 的全自动场景)
|
|
369
383
|
|
|
370
384
|
- 只读取每个 JSONL 文件**新增尾巴**(byte offset cursor),避免重复和 token 浪费
|
|
371
385
|
- 生成一个小型 batch JSON
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# New Session Distillation (Recommended)
|
|
2
|
+
|
|
3
|
+
This example shows a **non-blocking /new distillation pipeline**:
|
|
4
|
+
|
|
5
|
+
- Trigger: `command:new` (when you type `/new`)
|
|
6
|
+
- Hook: enqueue a small JSON task file (fast, no LLM calls)
|
|
7
|
+
- Worker: a user-level systemd service watches the inbox and runs **Gemini Map-Reduce** over the session JSONL transcript
|
|
8
|
+
- Storage: write high-signal, atomic lessons into LanceDB Pro via `openclaw memory-pro import`
|
|
9
|
+
- Notify: send a notification message (optional)
|
|
10
|
+
|
|
11
|
+
Files included:
|
|
12
|
+
- `hook/enqueue-lesson-extract/` — OpenClaw workspace hook
|
|
13
|
+
- `worker/lesson-extract-worker.mjs` — Map-Reduce extractor + importer + notifier
|
|
14
|
+
- `worker/systemd/lesson-extract-worker.service` — user systemd unit
|
|
15
|
+
|
|
16
|
+
You must provide:
|
|
17
|
+
- `GEMINI_API_KEY` in an env file loaded by systemd
|
|
18
|
+
|
|
19
|
+
Install steps are documented in the main repo README.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: enqueue-lesson-extract
|
|
3
|
+
description: "Enqueue a lesson-extraction task on /new (async Map-Reduce → LanceDB Pro)"
|
|
4
|
+
metadata:
|
|
5
|
+
{
|
|
6
|
+
"openclaw": {
|
|
7
|
+
"emoji": "🧾",
|
|
8
|
+
"events": ["command:new"]
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Enqueue Lesson Extract Hook
|
|
14
|
+
|
|
15
|
+
Writes a small JSON task file to a queue directory when `/new` is issued.
|
|
16
|
+
|
|
17
|
+
This is intentionally fast and non-blocking. A separate systemd worker consumes tasks and:
|
|
18
|
+
- reads the session JSONL transcript
|
|
19
|
+
- runs Map-Reduce extraction with Gemini Flash
|
|
20
|
+
- writes high-signal, deduped lessons into LanceDB Pro
|
|
21
|
+
- sends a notification (optional)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { HookHandler } from "../../src/hooks/hooks.js";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
function findSessionFile(event: any): string | null {
|
|
8
|
+
const prev = event.context?.previousSessionEntry;
|
|
9
|
+
const curr = event.context?.sessionEntry;
|
|
10
|
+
|
|
11
|
+
const candidates = [prev?.sessionFile, curr?.sessionFile].filter(Boolean);
|
|
12
|
+
|
|
13
|
+
for (const file of candidates) {
|
|
14
|
+
if (file && existsSync(file)) return file;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// If the file was renamed to .reset.*, look for the latest one
|
|
18
|
+
for (const file of candidates) {
|
|
19
|
+
if (!file) continue;
|
|
20
|
+
const dir = dirname(file);
|
|
21
|
+
const base = basename(file);
|
|
22
|
+
const resetPrefix = `${base}.reset.`;
|
|
23
|
+
try {
|
|
24
|
+
const resetFiles = readdirSync(dir)
|
|
25
|
+
.filter((name: string) => name.startsWith(resetPrefix))
|
|
26
|
+
.sort();
|
|
27
|
+
if (resetFiles.length > 0) return join(dir, resetFiles[resetFiles.length - 1]);
|
|
28
|
+
} catch {
|
|
29
|
+
// ignore
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sha1(input: string): string {
|
|
37
|
+
return createHash("sha1").update(input).digest("hex");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function repoRoot(): string {
|
|
41
|
+
// examples/new-session-distill/hook/enqueue-lesson-extract/handler.ts -> up to repo root
|
|
42
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
43
|
+
return join(here, "..", "..", "..", "..", "..");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const handler: HookHandler = async (event) => {
|
|
47
|
+
if (event.type !== "command" || event.action !== "new") return;
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const sessionKey = event.sessionKey || "unknown";
|
|
51
|
+
const ctxEntry = (event.context?.previousSessionEntry || event.context?.sessionEntry) as any;
|
|
52
|
+
const sessionId = (ctxEntry?.sessionId as string) || "unknown";
|
|
53
|
+
const source = (event.context?.commandSource as string) || "unknown";
|
|
54
|
+
|
|
55
|
+
const sessionFile = findSessionFile(event);
|
|
56
|
+
if (!sessionFile) {
|
|
57
|
+
console.error("[enqueue-lesson-extract] No session file found; skipping");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const root = repoRoot();
|
|
62
|
+
const queueDir = join(root, "workspaces", "main", "tasks", "lesson-extract", "inbox");
|
|
63
|
+
mkdirSync(queueDir, { recursive: true });
|
|
64
|
+
|
|
65
|
+
const ts = new Date(event.timestamp || Date.now()).toISOString();
|
|
66
|
+
const taskId = sha1([sessionKey, sessionId, sessionFile, ts].join("|"));
|
|
67
|
+
|
|
68
|
+
const task = {
|
|
69
|
+
taskId,
|
|
70
|
+
agentId: "main",
|
|
71
|
+
scope: "agent:main",
|
|
72
|
+
event: {
|
|
73
|
+
type: "command:new",
|
|
74
|
+
timestamp: ts,
|
|
75
|
+
sessionKey,
|
|
76
|
+
source,
|
|
77
|
+
},
|
|
78
|
+
session: {
|
|
79
|
+
sessionId,
|
|
80
|
+
sessionFile,
|
|
81
|
+
},
|
|
82
|
+
extract: {
|
|
83
|
+
maxFinal: 20,
|
|
84
|
+
mapChunkChars: 12000,
|
|
85
|
+
mapOverlapMsgs: 10,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const safeTs = ts.replace(/[:.]/g, "-");
|
|
90
|
+
const filename = `${safeTs}-${taskId.slice(0, 8)}.json`;
|
|
91
|
+
const outPath = join(queueDir, filename);
|
|
92
|
+
|
|
93
|
+
writeFileSync(outPath, JSON.stringify(task, null, 2) + "\n", "utf-8");
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.error(
|
|
96
|
+
"[enqueue-lesson-extract] Error:",
|
|
97
|
+
err instanceof Error ? err.message : String(err)
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export default handler;
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* lesson-extract-worker (example)
|
|
5
|
+
*
|
|
6
|
+
* Watches a repo-backed inbox for lesson extraction tasks and processes them asynchronously:
|
|
7
|
+
* - reads session JSONL transcript (streaming)
|
|
8
|
+
* - Map: per-chunk extraction via Gemini (native API)
|
|
9
|
+
* - Reduce: merge/dedupe/score -> 0..20 lessons
|
|
10
|
+
* - writes to LanceDB Pro via `openclaw memory-pro import`
|
|
11
|
+
* - sends Telegram notification via `openclaw message send` (optional)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import fsp from "node:fs/promises";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { spawn } from "node:child_process";
|
|
19
|
+
import readline from "node:readline";
|
|
20
|
+
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
+
const __dirname = path.dirname(__filename);
|
|
23
|
+
|
|
24
|
+
// In your deployment, set LESSON_QUEUE_ROOT to your workspace queue.
|
|
25
|
+
// By default we assume repo layout similar to OpenClaw-Memory.
|
|
26
|
+
const REPO_ROOT = path.resolve(__dirname, "..", "..", "..", "..", "..");
|
|
27
|
+
const QUEUE_ROOT = process.env.LESSON_QUEUE_ROOT || path.join(REPO_ROOT, "workspaces", "main", "tasks", "lesson-extract");
|
|
28
|
+
|
|
29
|
+
const INBOX = path.join(QUEUE_ROOT, "inbox");
|
|
30
|
+
const PROCESSING = path.join(QUEUE_ROOT, "processing");
|
|
31
|
+
const DONE = path.join(QUEUE_ROOT, "done");
|
|
32
|
+
const ERROR = path.join(QUEUE_ROOT, "error");
|
|
33
|
+
|
|
34
|
+
const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
|
|
35
|
+
const GEMINI_MODEL = process.env.GEMINI_MODEL || "gemini-3-flash-preview";
|
|
36
|
+
|
|
37
|
+
const ONCE = process.argv.includes("--once");
|
|
38
|
+
|
|
39
|
+
function ensureDirs() {
|
|
40
|
+
for (const d of [INBOX, PROCESSING, DONE, ERROR]) {
|
|
41
|
+
fs.mkdirSync(d, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function nowIso() {
|
|
46
|
+
return new Date().toISOString();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function run(cmd, args, opts = {}) {
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
const child = spawn(cmd, args, { ...opts, stdio: ["ignore", "pipe", "pipe"] });
|
|
52
|
+
let out = "";
|
|
53
|
+
let err = "";
|
|
54
|
+
child.stdout.on("data", (d) => (out += d.toString("utf-8")));
|
|
55
|
+
child.stderr.on("data", (d) => (err += d.toString("utf-8")));
|
|
56
|
+
child.on("close", (code) => resolve({ code: code ?? 0, out, err }));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function safeJsonParse(text) {
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(text);
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeText(s) {
|
|
69
|
+
return (s || "")
|
|
70
|
+
.trim()
|
|
71
|
+
.replace(/\s+/g, " ")
|
|
72
|
+
.replace(/[“”]/g, '"')
|
|
73
|
+
.replace(/[‘’]/g, "'")
|
|
74
|
+
.toLowerCase();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function detectLang(text) {
|
|
78
|
+
const s = text || "";
|
|
79
|
+
const cjk = (s.match(/[\u4e00-\u9fff]/g) || []).length;
|
|
80
|
+
const latin = (s.match(/[A-Za-z]/g) || []).length;
|
|
81
|
+
if (cjk > latin * 0.8) return "zh";
|
|
82
|
+
if (latin > cjk * 0.8) return "en";
|
|
83
|
+
return "mixed";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function* iterJsonlMessages(sessionFile) {
|
|
87
|
+
const stream = fs.createReadStream(sessionFile, { encoding: "utf-8" });
|
|
88
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
89
|
+
|
|
90
|
+
let id = 0;
|
|
91
|
+
for await (const line of rl) {
|
|
92
|
+
if (!line) continue;
|
|
93
|
+
const obj = safeJsonParse(line);
|
|
94
|
+
if (!obj || obj.type !== "message") continue;
|
|
95
|
+
const m = obj.message;
|
|
96
|
+
if (!m || (m.role !== "user" && m.role !== "assistant")) continue;
|
|
97
|
+
|
|
98
|
+
let text = "";
|
|
99
|
+
if (typeof m.content === "string") {
|
|
100
|
+
text = m.content;
|
|
101
|
+
} else if (Array.isArray(m.content)) {
|
|
102
|
+
text = m.content
|
|
103
|
+
.filter((c) => c && c.type === "text" && c.text)
|
|
104
|
+
.map((c) => c.text)
|
|
105
|
+
.join("\n");
|
|
106
|
+
}
|
|
107
|
+
text = (text || "").trim();
|
|
108
|
+
if (!text) continue;
|
|
109
|
+
|
|
110
|
+
id++;
|
|
111
|
+
yield {
|
|
112
|
+
id,
|
|
113
|
+
role: m.role,
|
|
114
|
+
timestamp: obj.timestamp || "",
|
|
115
|
+
text,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function buildChunksFromJsonl(sessionFile, { maxChars = 12000, overlapMsgs = 10, maxChunks = 200 } = {}) {
|
|
121
|
+
const chunks = [];
|
|
122
|
+
let chunk = [];
|
|
123
|
+
let size = 0;
|
|
124
|
+
|
|
125
|
+
const sampleTexts = [];
|
|
126
|
+
|
|
127
|
+
for await (const m of iterJsonlMessages(sessionFile)) {
|
|
128
|
+
if (sampleTexts.length < 200) sampleTexts.push(m.text);
|
|
129
|
+
|
|
130
|
+
const line = `[${m.role === "user" ? "U" : "A"}${m.id}] ${m.text}\n`;
|
|
131
|
+
|
|
132
|
+
if (size + line.length > maxChars && chunk.length > 0) {
|
|
133
|
+
chunks.push(chunk);
|
|
134
|
+
if (chunks.length >= maxChunks) break;
|
|
135
|
+
|
|
136
|
+
chunk = chunk.slice(Math.max(0, chunk.length - overlapMsgs));
|
|
137
|
+
size = chunk.reduce((acc, mm) => acc + (`[${mm.role === "user" ? "U" : "A"}${mm.id}] ${mm.text}\n`).length, 0);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
chunk.push(m);
|
|
141
|
+
size += line.length;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (chunk.length > 0 && chunks.length < maxChunks) chunks.push(chunk);
|
|
145
|
+
|
|
146
|
+
const lang = detectLang(sampleTexts.join("\n"));
|
|
147
|
+
const messageCount = chunk.length === 0 && chunks.length === 0 ? 0 : chunks[chunks.length - 1][chunks[chunks.length - 1].length - 1].id;
|
|
148
|
+
|
|
149
|
+
return { chunks, lang, messageCount };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function buildMapPrompt({ lang, chunk }) {
|
|
153
|
+
const langInstr = lang === "zh" ? "请用中文输出 lessons。" : lang === "en" ? "Output lessons in English." : "Follow the dominant language of the transcript.";
|
|
154
|
+
|
|
155
|
+
return `You are extracting high-signal technical lessons from a chat transcript chunk.\n\nRules:\n- Output STRICT JSON only. No markdown, no backticks.\n- If nothing valuable, output: {\"lessons\":[]}\n- Max 8 lessons.\n- Each lesson.text must be <= 480 characters.\n- Categories: fact | decision | preference | other (use fact/decision primarily).\n- importance: number 0..1 (high-signal: 0.8-0.95).\n- evidence MUST quote exact short snippets from the chunk and include message_ids.\n- Do NOT include secrets/tokens/credentials.\n- Add Keywords (zh) inside each lesson:\n - Include >=1 Entity keyword that appears verbatim in the chunk (project/library/tool/service/config key/error code).\n - Include >=1 Action keyword (e.g., 修复/回滚/重启/迁移/去重/限流).\n - Include >=1 Symptom keyword (e.g., OOM/超时/429/重复/命中率差).\n - Do NOT invent entity names; copy entity keywords from the chunk.\n\n${langInstr}\n\nChunk:\n${chunk.map((m) => `[${m.role === "user" ? "U" : "A"}${m.id}] ${m.text}`).join("\n\n")}\n\nReturn JSON schema:\n{\n \"lessons\": [\n {\n \"category\": \"fact\",\n \"importance\": 0.8,\n \"text\": \"Pitfall: ... Cause: ... Fix: ... Prevention: ...\",\n \"evidence\": [\n {\"message_ids\":[12,13],\"quote\":\"...\"}\n ],\n \"tags\": [\"optional\"]\n }\n ]\n}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function geminiGenerateJson(prompt) {
|
|
159
|
+
if (!GEMINI_API_KEY) throw new Error("GEMINI_API_KEY is not set");
|
|
160
|
+
|
|
161
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent?key=${GEMINI_API_KEY}`;
|
|
162
|
+
|
|
163
|
+
const body = {
|
|
164
|
+
contents: [{ role: "user", parts: [{ text: prompt }] }],
|
|
165
|
+
generationConfig: {
|
|
166
|
+
temperature: 0.2,
|
|
167
|
+
maxOutputTokens: 4096,
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const res = await fetch(url, {
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers: { "Content-Type": "application/json" },
|
|
174
|
+
body: JSON.stringify(body),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const json = await res.json();
|
|
178
|
+
if (!res.ok) {
|
|
179
|
+
throw new Error(`Gemini error ${res.status}: ${JSON.stringify(json).slice(0, 500)}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const text = json?.candidates?.[0]?.content?.parts?.map((p) => p.text).join("") || "";
|
|
183
|
+
return text;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function coerceLessons(obj) {
|
|
187
|
+
const lessons = Array.isArray(obj?.lessons) ? obj.lessons : [];
|
|
188
|
+
return lessons
|
|
189
|
+
.filter((l) => l && typeof l.text === "string" && l.text.trim().length >= 10)
|
|
190
|
+
.map((l) => ({
|
|
191
|
+
category: ["fact", "decision", "preference", "other"].includes(l.category) ? l.category : "other",
|
|
192
|
+
importance: typeof l.importance === "number" ? l.importance : 0.7,
|
|
193
|
+
text: l.text.trim().slice(0, 480),
|
|
194
|
+
evidence: Array.isArray(l.evidence) ? l.evidence : [],
|
|
195
|
+
tags: Array.isArray(l.tags) ? l.tags : [],
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function scoreLesson(l) {
|
|
200
|
+
let s = 0;
|
|
201
|
+
const t = l.text || "";
|
|
202
|
+
if (/pitfall\s*:|cause\s*:|fix\s*:|prevention\s*:/i.test(t)) s += 2;
|
|
203
|
+
if (/decision principle|trigger\s*:|action\s*:/i.test(t)) s += 2;
|
|
204
|
+
if (/\b(openclaw|docker|systemd|ssh|git|api|json|yaml|config)\b/i.test(t)) s += 1;
|
|
205
|
+
if (t.length < 120) s += 0.5;
|
|
206
|
+
if (l.evidence?.length >= 1) s += 1;
|
|
207
|
+
if (l.evidence?.length >= 2) s += 0.5;
|
|
208
|
+
const imp = Math.max(0, Math.min(1, l.importance ?? 0.7));
|
|
209
|
+
s += imp;
|
|
210
|
+
return s;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function reduceLessons(allLessons, maxFinal = 20) {
|
|
214
|
+
const seen = new Set();
|
|
215
|
+
const merged = [];
|
|
216
|
+
|
|
217
|
+
for (const l of allLessons) {
|
|
218
|
+
const key = normalizeText(l.text);
|
|
219
|
+
if (!key) continue;
|
|
220
|
+
if (seen.has(key)) continue;
|
|
221
|
+
seen.add(key);
|
|
222
|
+
merged.push(l);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
merged.sort((a, b) => scoreLesson(b) - scoreLesson(a));
|
|
226
|
+
|
|
227
|
+
const filtered = merged.filter((l) => {
|
|
228
|
+
if (!l.evidence || l.evidence.length === 0) return false;
|
|
229
|
+
const t = normalizeText(l.text);
|
|
230
|
+
if (t.length < 20) return false;
|
|
231
|
+
if (/(be careful|best practice|should|建议|注意)/.test(t) && !/(cause|fix|prevention|trigger|action|原因|修复|预防|触发)/.test(t)) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
return true;
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
return filtered.slice(0, maxFinal);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function importToLanceDb({ lessons, scope }) {
|
|
241
|
+
const importFile = path.join("/tmp", `lesson-import-${Date.now()}.json`);
|
|
242
|
+
const payload = {
|
|
243
|
+
memories: lessons.map((l) => ({
|
|
244
|
+
text: l.text,
|
|
245
|
+
importance: Math.max(0.0, Math.min(1.0, l.importance ?? 0.7)),
|
|
246
|
+
category: l.category,
|
|
247
|
+
})),
|
|
248
|
+
};
|
|
249
|
+
await fsp.writeFile(importFile, JSON.stringify(payload), "utf-8");
|
|
250
|
+
|
|
251
|
+
const { code, out, err } = await run("openclaw", ["memory-pro", "import", importFile, "--scope", scope], { cwd: REPO_ROOT });
|
|
252
|
+
await fsp.unlink(importFile).catch(() => {});
|
|
253
|
+
|
|
254
|
+
return { code, out, err };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function notifyTelegram(chatId, message) {
|
|
258
|
+
const args = ["message", "send", "--channel", "telegram", "--target", String(chatId), "--message", message];
|
|
259
|
+
await run("openclaw", args, { cwd: REPO_ROOT });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function processTaskFile(taskPath) {
|
|
263
|
+
const started = Date.now();
|
|
264
|
+
const taskRaw = await fsp.readFile(taskPath, "utf-8");
|
|
265
|
+
const task = JSON.parse(taskRaw);
|
|
266
|
+
|
|
267
|
+
const baseName = path.basename(taskPath);
|
|
268
|
+
const processingPath = path.join(PROCESSING, baseName);
|
|
269
|
+
await fsp.rename(taskPath, processingPath);
|
|
270
|
+
|
|
271
|
+
const result = {
|
|
272
|
+
taskId: task.taskId,
|
|
273
|
+
startedAt: nowIso(),
|
|
274
|
+
finishedAt: null,
|
|
275
|
+
ok: false,
|
|
276
|
+
sessionId: task.session?.sessionId,
|
|
277
|
+
sessionFile: task.session?.sessionFile,
|
|
278
|
+
stats: {},
|
|
279
|
+
error: null,
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
const sessionFile = task.session?.sessionFile;
|
|
284
|
+
if (!sessionFile || !fs.existsSync(sessionFile)) {
|
|
285
|
+
throw new Error(`sessionFile missing or not found: ${sessionFile}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const { chunks, lang, messageCount } = await buildChunksFromJsonl(sessionFile, {
|
|
289
|
+
maxChars: task.extract?.mapChunkChars ?? 12000,
|
|
290
|
+
overlapMsgs: task.extract?.mapOverlapMsgs ?? 10,
|
|
291
|
+
maxChunks: 200,
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const allLessons = [];
|
|
295
|
+
let mapErrors = 0;
|
|
296
|
+
|
|
297
|
+
for (let idx = 0; idx < chunks.length; idx++) {
|
|
298
|
+
const prompt = buildMapPrompt({ lang, chunk: chunks[idx] });
|
|
299
|
+
try {
|
|
300
|
+
const text = await geminiGenerateJson(prompt);
|
|
301
|
+
const obj = safeJsonParse(text);
|
|
302
|
+
if (!obj) {
|
|
303
|
+
mapErrors++;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const lessons = coerceLessons(obj);
|
|
307
|
+
for (const l of lessons) allLessons.push(l);
|
|
308
|
+
} catch {
|
|
309
|
+
mapErrors++;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const reduced = reduceLessons(allLessons, task.extract?.maxFinal ?? 20);
|
|
314
|
+
|
|
315
|
+
const scope = task.scope || "agent:main";
|
|
316
|
+
const importRes = await importToLanceDb({ lessons: reduced, scope });
|
|
317
|
+
|
|
318
|
+
const durationMs = Date.now() - started;
|
|
319
|
+
result.ok = importRes.code === 0;
|
|
320
|
+
result.finishedAt = nowIso();
|
|
321
|
+
result.stats = {
|
|
322
|
+
lang,
|
|
323
|
+
messages: messageCount,
|
|
324
|
+
chunks: chunks.length,
|
|
325
|
+
mapCandidates: allLessons.length,
|
|
326
|
+
mapErrors,
|
|
327
|
+
reduced: reduced.length,
|
|
328
|
+
importCode: importRes.code,
|
|
329
|
+
durationMs,
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const notifyChatId = task.notify?.telegramChatId;
|
|
333
|
+
if (notifyChatId) {
|
|
334
|
+
const text = [
|
|
335
|
+
`Lesson Extract ✅ (${task.agentId || "main"})`,
|
|
336
|
+
`taskId: ${task.taskId?.slice(0, 8) || "unknown"}`,
|
|
337
|
+
`sessionId: ${task.session?.sessionId || "unknown"}`,
|
|
338
|
+
`lang: ${lang}`,
|
|
339
|
+
`messages: ${messageCount}, chunks: ${chunks.length}`,
|
|
340
|
+
`candidates: ${allLessons.length}, reduced: ${reduced.length}`,
|
|
341
|
+
`import: code=${importRes.code}`,
|
|
342
|
+
`time: ${(durationMs / 1000).toFixed(1)}s`,
|
|
343
|
+
].join("\n");
|
|
344
|
+
await notifyTelegram(notifyChatId, text);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const donePath = path.join(DONE, `${task.taskId}.json`);
|
|
348
|
+
await fsp.writeFile(donePath, JSON.stringify(result, null, 2) + "\n", "utf-8");
|
|
349
|
+
await fsp.unlink(processingPath).catch(() => {});
|
|
350
|
+
} catch (err) {
|
|
351
|
+
result.ok = false;
|
|
352
|
+
result.finishedAt = nowIso();
|
|
353
|
+
result.error = err instanceof Error ? err.message : String(err);
|
|
354
|
+
|
|
355
|
+
const durationMs = Date.now() - started;
|
|
356
|
+
result.stats.durationMs = durationMs;
|
|
357
|
+
|
|
358
|
+
const notifyChatId = task.notify?.telegramChatId;
|
|
359
|
+
if (notifyChatId) {
|
|
360
|
+
await notifyTelegram(
|
|
361
|
+
notifyChatId,
|
|
362
|
+
`Lesson Extract ❌ (${task.agentId || "main"})\n` +
|
|
363
|
+
`taskId: ${task.taskId?.slice(0, 8) || "unknown"}\n` +
|
|
364
|
+
`error: ${result.error}\n` +
|
|
365
|
+
`time: ${(durationMs / 1000).toFixed(1)}s`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const errPath = path.join(ERROR, `${task.taskId}.json`);
|
|
370
|
+
await fsp.writeFile(errPath, JSON.stringify(result, null, 2) + "\n", "utf-8");
|
|
371
|
+
await fsp.unlink(processingPath).catch(() => {});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function drainInboxOnce() {
|
|
376
|
+
ensureDirs();
|
|
377
|
+
const files = (await fsp.readdir(INBOX)).filter((f) => f.endsWith(".json")).sort();
|
|
378
|
+
for (const f of files) {
|
|
379
|
+
await processTaskFile(path.join(INBOX, f));
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function main() {
|
|
384
|
+
ensureDirs();
|
|
385
|
+
await drainInboxOnce();
|
|
386
|
+
if (ONCE) return;
|
|
387
|
+
|
|
388
|
+
const watcher = fs.watch(INBOX, async (_eventType, filename) => {
|
|
389
|
+
if (!filename || !filename.endsWith(".json")) return;
|
|
390
|
+
const full = path.join(INBOX, filename);
|
|
391
|
+
setTimeout(() => {
|
|
392
|
+
processTaskFile(full).catch(() => {});
|
|
393
|
+
}, 150);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
let alive = true;
|
|
397
|
+
const shutdown = () => {
|
|
398
|
+
alive = false;
|
|
399
|
+
watcher.close();
|
|
400
|
+
};
|
|
401
|
+
process.on("SIGINT", shutdown);
|
|
402
|
+
process.on("SIGTERM", shutdown);
|
|
403
|
+
|
|
404
|
+
while (alive) {
|
|
405
|
+
await new Promise((r) => setTimeout(r, 5_000));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
main().catch((err) => {
|
|
410
|
+
console.error(String(err));
|
|
411
|
+
process.exit(1);
|
|
412
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=OpenClaw Lesson Extract Worker (Gemini Map-Reduce -> LanceDB Pro)
|
|
3
|
+
After=network-online.target
|
|
4
|
+
Wants=network-online.target
|
|
5
|
+
|
|
6
|
+
[Service]
|
|
7
|
+
Type=simple
|
|
8
|
+
WorkingDirectory=/home/ubuntu/clawd
|
|
9
|
+
EnvironmentFile=%h/.config/lesson-extract-worker.env
|
|
10
|
+
Environment=NODE_ENV=production
|
|
11
|
+
Environment=LESSON_QUEUE_ROOT=/home/ubuntu/clawd/workspaces/main/tasks/lesson-extract
|
|
12
|
+
Environment=GEMINI_MODEL=gemini-3-flash-preview
|
|
13
|
+
ExecStart=/usr/bin/env node /home/ubuntu/clawd/scripts/lesson-extract-worker.mjs
|
|
14
|
+
Restart=always
|
|
15
|
+
RestartSec=2
|
|
16
|
+
NoNewPrivileges=true
|
|
17
|
+
PrivateTmp=true
|
|
18
|
+
|
|
19
|
+
[Install]
|
|
20
|
+
WantedBy=default.target
|
package/index.ts
CHANGED
|
@@ -639,32 +639,53 @@ const memoryLanceDBProPlugin = {
|
|
|
639
639
|
api.registerService({
|
|
640
640
|
id: "memory-lancedb-pro",
|
|
641
641
|
start: async () => {
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
if (!embedTest.success) {
|
|
656
|
-
api.logger.warn(`memory-lancedb-pro: embedding test failed: ${embedTest.error}`);
|
|
642
|
+
// IMPORTANT: Do not block gateway startup on external network calls.
|
|
643
|
+
// If embedding/retrieval tests hang (bad network / slow provider), the gateway
|
|
644
|
+
// may never bind its HTTP port, causing restart timeouts.
|
|
645
|
+
|
|
646
|
+
const withTimeout = async <T>(p: Promise<T>, ms: number, label: string): Promise<T> => {
|
|
647
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
648
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
649
|
+
timeout = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
650
|
+
});
|
|
651
|
+
try {
|
|
652
|
+
return await Promise.race([p, timeoutPromise]);
|
|
653
|
+
} finally {
|
|
654
|
+
if (timeout) clearTimeout(timeout);
|
|
657
655
|
}
|
|
658
|
-
|
|
659
|
-
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
const runStartupChecks = async () => {
|
|
659
|
+
try {
|
|
660
|
+
// Test components (bounded time)
|
|
661
|
+
const embedTest = await withTimeout(embedder.test(), 8_000, "embedder.test()");
|
|
662
|
+
const retrievalTest = await withTimeout(retriever.test(), 8_000, "retriever.test()");
|
|
663
|
+
|
|
664
|
+
api.logger.info(
|
|
665
|
+
`memory-lancedb-pro: initialized successfully ` +
|
|
666
|
+
`(embedding: ${embedTest.success ? "OK" : "FAIL"}, ` +
|
|
667
|
+
`retrieval: ${retrievalTest.success ? "OK" : "FAIL"}, ` +
|
|
668
|
+
`mode: ${retrievalTest.mode}, ` +
|
|
669
|
+
`FTS: ${retrievalTest.hasFtsSupport ? "enabled" : "disabled"})`
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
if (!embedTest.success) {
|
|
673
|
+
api.logger.warn(`memory-lancedb-pro: embedding test failed: ${embedTest.error}`);
|
|
674
|
+
}
|
|
675
|
+
if (!retrievalTest.success) {
|
|
676
|
+
api.logger.warn(`memory-lancedb-pro: retrieval test failed: ${retrievalTest.error}`);
|
|
677
|
+
}
|
|
678
|
+
} catch (error) {
|
|
679
|
+
api.logger.warn(`memory-lancedb-pro: startup checks failed: ${String(error)}`);
|
|
660
680
|
}
|
|
681
|
+
};
|
|
661
682
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
683
|
+
// Fire-and-forget: allow gateway to start serving immediately.
|
|
684
|
+
setTimeout(() => void runStartupChecks(), 0);
|
|
685
|
+
|
|
686
|
+
// Run initial backup after a short delay, then schedule daily
|
|
687
|
+
setTimeout(() => void runBackup(), 60_000); // 1 min after start
|
|
688
|
+
backupTimer = setInterval(() => void runBackup(), BACKUP_INTERVAL_MS);
|
|
668
689
|
},
|
|
669
690
|
stop: () => {
|
|
670
691
|
if (backupTimer) {
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "memory-lancedb-pro",
|
|
3
3
|
"name": "Memory (LanceDB Pro)",
|
|
4
4
|
"description": "Enhanced LanceDB-backed long-term memory with hybrid retrieval, multi-scope isolation, and management CLI",
|
|
5
|
-
"version": "1.0.
|
|
5
|
+
"version": "1.0.10",
|
|
6
6
|
"kind": "memory",
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
},
|
|
109
109
|
"rerankModel": {
|
|
110
110
|
"type": "string",
|
|
111
|
-
"default": "jina-reranker-
|
|
111
|
+
"default": "jina-reranker-v3",
|
|
112
112
|
"description": "Reranker model name"
|
|
113
113
|
},
|
|
114
114
|
"rerankEndpoint": {
|
|
@@ -304,8 +304,8 @@
|
|
|
304
304
|
},
|
|
305
305
|
"retrieval.rerankModel": {
|
|
306
306
|
"label": "Reranker Model",
|
|
307
|
-
"placeholder": "jina-reranker-
|
|
308
|
-
"help": "Reranker model name (e.g. jina-reranker-
|
|
307
|
+
"placeholder": "jina-reranker-v3",
|
|
308
|
+
"help": "Reranker model name (e.g. jina-reranker-v3, BAAI/bge-reranker-v2-m3)",
|
|
309
309
|
"advanced": true
|
|
310
310
|
},
|
|
311
311
|
"retrieval.rerankEndpoint": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memory-lancedb-pro",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "OpenClaw enhanced LanceDB memory plugin with hybrid retrieval (Vector + BM25), cross-encoder rerank, multi-scope isolation, and management CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
package/src/retriever.ts
CHANGED
|
@@ -26,7 +26,7 @@ export interface RetrievalConfig {
|
|
|
26
26
|
filterNoise: boolean;
|
|
27
27
|
/** Reranker API key (enables cross-encoder reranking) */
|
|
28
28
|
rerankApiKey?: string;
|
|
29
|
-
/** Reranker model (default: jina-reranker-
|
|
29
|
+
/** Reranker model (default: jina-reranker-v3) */
|
|
30
30
|
rerankModel?: string;
|
|
31
31
|
/** Reranker API endpoint (default: https://api.jina.ai/v1/rerank). */
|
|
32
32
|
rerankEndpoint?: string;
|
|
@@ -90,7 +90,7 @@ export const DEFAULT_RETRIEVAL_CONFIG: RetrievalConfig = {
|
|
|
90
90
|
recencyHalfLifeDays: 14,
|
|
91
91
|
recencyWeight: 0.10,
|
|
92
92
|
filterNoise: true,
|
|
93
|
-
rerankModel: "jina-reranker-
|
|
93
|
+
rerankModel: "jina-reranker-v3",
|
|
94
94
|
rerankEndpoint: "https://api.jina.ai/v1/rerank",
|
|
95
95
|
lengthNormAnchor: 500,
|
|
96
96
|
hardMinScore: 0.35,
|
|
@@ -427,7 +427,7 @@ export class MemoryRetriever {
|
|
|
427
427
|
if (this.config.rerank === "cross-encoder" && this.config.rerankApiKey) {
|
|
428
428
|
try {
|
|
429
429
|
const provider = this.config.rerankProvider || "jina";
|
|
430
|
-
const model = this.config.rerankModel || "jina-reranker-
|
|
430
|
+
const model = this.config.rerankModel || "jina-reranker-v3";
|
|
431
431
|
const endpoint = this.config.rerankEndpoint || "https://api.jina.ai/v1/rerank";
|
|
432
432
|
const documents = results.map(r => r.entry.text);
|
|
433
433
|
|