cortico-world-memory 0.1.0
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 +32 -0
- package/package.json +35 -0
- package/src/ENV_PROMPT.md +15 -0
- package/src/config.ts +77 -0
- package/src/cortico-shim.d.ts +89 -0
- package/src/definition.ts +10 -0
- package/src/embedding.ts +79 -0
- package/src/fact-store.ts +114 -0
- package/src/float16.ts +84 -0
- package/src/index.ts +12 -0
- package/src/knowledge-store.ts +169 -0
- package/src/memory.ts +181 -0
- package/src/observe.ts +60 -0
- package/src/persona-store.ts +100 -0
- package/src/profile-store.ts +102 -0
- package/src/reflection-store.ts +152 -0
- package/src/vector-store.ts +327 -0
- package/src/world.ts +336 -0
package/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# cortico-world-memory
|
|
2
|
+
|
|
3
|
+
Cortico World 扩展:把 `E:/qq_bot` 的记忆系统移植成框架内的一格 World。**qq_bot 源码零改动**。
|
|
4
|
+
|
|
5
|
+
## 移植对照
|
|
6
|
+
|
|
7
|
+
| qq_bot(源) | 本扩展(目标) | 说明 |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `memory.py` `Memory` | `src/memory.ts` `MemoryBase` | 三级记忆:短期(cap 环形)/摘要/话题 + JSON 防抖落盘 + overflow 取半;`user:{id}` 统一键改为调用方 scope(默认 `main`) |
|
|
10
|
+
| `vector_memory.py` | `src/vector-store.ts` `VectorStore` | Hybrid Recall = BM25(CJK 2/3-gram,仅 query 建 DF)+ Cosine(预归一化点积)+ RRF(k=60);矩阵按 scope 缓存、版本号失效;存储同款 `base64(fp16)` JSON,可与 `E:/qq_bot/vector_memory.json` 互导 |
|
|
11
|
+
| `memory_server.py`(sidecar) | `src/embedding.ts` `EmbeddingClient` | 「重负载外置 + 掉线降级」同一语义:`sidecar` 模式直接 POST qq_bot memory_server 的 `/call` 调 `vector_memory.generate_embedding`;另有 `http`(OpenAI 兼容 /embeddings)与 `off`(纯 BM25);任一失败自动降级 BM25 |
|
|
12
|
+
| `memory_context.py` | `src/observe.ts` + `src/ENV_PROMPT.md` | 记忆上下文注入:走 envPrompt 插值(要点/摘要/话题常驻前缀)+ 周期 `memory.context` 事件 |
|
|
13
|
+
| `important_notes.py`(存取部分) | `MemoryBase.addNote/removeNote` + `mem_note` 工具 | 重要信息常驻进提示词,条数上限 `notesInPrompt` |
|
|
14
|
+
|
|
15
|
+
## 工具(人格可调用)
|
|
16
|
+
|
|
17
|
+
`mem_remember` / `mem_recall` / `mem_recent` / `mem_overflow` / `mem_summary` / `mem_topic` / `mem_note` / `mem_notes` / `mem_forget_note` / `mem_stats` / `mem_flush`
|
|
18
|
+
|
|
19
|
+
## 部署
|
|
20
|
+
|
|
21
|
+
1. 包放到 `Cortico/extensions/node_modules/cortico-world-memory/`;
|
|
22
|
+
2. `extensions/package.json` 的 dependencies 加 `"cortico-world-memory": "link:./node_modules/cortico-world-memory"`;
|
|
23
|
+
3. 部署 `config.json` 加 `worlds.memory.enabled = true`(或控制台激活);
|
|
24
|
+
4. **整机重启**(`POST /api/run/restart`)才加载新扩展包。
|
|
25
|
+
|
|
26
|
+
数据落在部署 `data/` 下:`memory-data.json`、`vector-memory.json`(均 gitignored)。
|
|
27
|
+
|
|
28
|
+
## 对接 qq_bot 向量 sidecar(可选)
|
|
29
|
+
|
|
30
|
+
不改 qq_bot 一行:在 `E:/qq_bot` 正常启动 `python memory_server.py`(默认 127.0.0.1:8766),
|
|
31
|
+
部署里设 `worlds.memory.embeddingProvider = "sidecar"` 即可复用它的 all-MiniLM-L6-v2 向量;
|
|
32
|
+
sidecar 不在线时本扩展自动只用 BM25,永不停摆。
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cortico-world-memory",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Cortico World 扩展:三级记忆 + Hybrid Recall 向量记忆(移植自 E:/qq_bot 的 memory 系统与 memory_server 向量记忆 sidecar,qq_bot 源码零改动)。",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"cortico-world",
|
|
9
|
+
"cortico",
|
|
10
|
+
"memory",
|
|
11
|
+
"vector-search",
|
|
12
|
+
"bm25",
|
|
13
|
+
"agent"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=22"
|
|
17
|
+
},
|
|
18
|
+
"main": "./src/index.ts",
|
|
19
|
+
"cortico": {
|
|
20
|
+
"kind": "world",
|
|
21
|
+
"api": 5
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"src",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"dependencies": {},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^22.10.0",
|
|
30
|
+
"typescript": "^5.7.2"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"typecheck": "tsc --noEmit"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
你带有一套自己的记忆(三级记忆 + 混合检索记忆库)。
|
|
2
|
+
|
|
3
|
+
## 记忆机制
|
|
4
|
+
- {{memoryContext}}
|
|
5
|
+
- 记忆域(memory scopes): {{memoryScopes}}。默认都在 `main`;跨场景的事(游戏/聊天)可以分域存放。
|
|
6
|
+
|
|
7
|
+
## 使用纪律
|
|
8
|
+
1. 别人说了「记住这个」「以后别这样」这类话 → 立刻 mem_note(硬约束/偏好)或 mem_remember(一般事实)。
|
|
9
|
+
2. 要找以前聊过/发生过什么 → 先 mem_recall(语义+关键词混合检索),不要凭空编造回忆;检索不到就直说想不起来。
|
|
10
|
+
3. 一次对话里反复出现的新约定,值得顺手 mem_remember 存档,免得上下文滚掉就忘。
|
|
11
|
+
4. 短期记忆快满时用 mem_overflow 取出旧条目,浓缩成一段再用 mem_summary 写回——这相当于"把旧事压进长期记忆"。
|
|
12
|
+
5. 话题切换或每轮收尾时,可以用 mem_topic 更新当前话题,保持连贯。
|
|
13
|
+
6. 记忆是你的私事:不要向用户转述"我调用了什么工具",自然地表现出"我记得"就好。
|
|
14
|
+
|
|
15
|
+
一句话:像真人一样记得该记的、忘掉该忘的;要回忆就查库,别装记得。
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { ConfigGroup } from 'cortico/core/types.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 部署里 `worlds.memory` 配置段的形状。`ctx.cfg` 是它的活引用,热改即时生效
|
|
5
|
+
* (改 embedding 端点等连接类键请走「重启」按钮,见 schema 的 x-hot 标注)。
|
|
6
|
+
*/
|
|
7
|
+
export interface MemoryConfig {
|
|
8
|
+
/** 框架对账用,恒为 true(是否启用由部署的 worlds 列表决定)。 */
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
[k: string]: unknown;
|
|
11
|
+
/** 短期记忆窗口(每个 scope 保留的最近条数)。对齐 qq_bot 的 MAX_HISTORY。 */
|
|
12
|
+
maxHistory: number;
|
|
13
|
+
/** 每个 scope 的向量记录上限,超出截掉最旧的。对齐 MAX_VECTORS_PER_USER。 */
|
|
14
|
+
maxVectorsPerScope: number;
|
|
15
|
+
/** 记忆上下文(话题/摘要/要点)推送到事件流的周期(毫秒)。 */
|
|
16
|
+
observeIntervalMs: number;
|
|
17
|
+
/** 检索默认 top_k。 */
|
|
18
|
+
vectorTopK: number;
|
|
19
|
+
/** 环境提示词里常驻注入的「重要信息」条数上限。 */
|
|
20
|
+
notesInPrompt: number;
|
|
21
|
+
/**
|
|
22
|
+
* 向量来源:
|
|
23
|
+
* - `off` 纯 BM25 关键词检索(零依赖,永远可用);
|
|
24
|
+
* - `http` OpenAI 兼容 /embeddings 端点;
|
|
25
|
+
* - `sidecar` qq_bot 的 memory_server(POST {url}/call 调 vector_memory.generate_embedding)。
|
|
26
|
+
* 任一模式失败都自动降级 BM25,与 qq_bot memory_client 的降级语义一致。
|
|
27
|
+
*/
|
|
28
|
+
embeddingProvider: 'off' | 'http' | 'sidecar';
|
|
29
|
+
/** `http` 模式的端点 URL(如本机 ollama: http://127.0.0.1:11434/v1/embeddings)。 */
|
|
30
|
+
embeddingEndpoint: string;
|
|
31
|
+
/** `http` 模式的模型名(如 nomic-embed-text)。 */
|
|
32
|
+
embeddingModel: string;
|
|
33
|
+
/** `sidecar` 模式的 qq_bot memory_server 地址(默认即其约定端口 8766)。 */
|
|
34
|
+
embeddingSidecarUrl: string;
|
|
35
|
+
/** `sidecar` 模式单次调用的超时(毫秒)。qq_bot 侧模型首载可达数分钟,别调太小。 */
|
|
36
|
+
embeddingTimeoutMs: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const MEMORY_DEFAULTS: MemoryConfig = {
|
|
40
|
+
enabled: true,
|
|
41
|
+
maxHistory: 30,
|
|
42
|
+
maxVectorsPerScope: 500,
|
|
43
|
+
observeIntervalMs: 30000,
|
|
44
|
+
vectorTopK: 5,
|
|
45
|
+
notesInPrompt: 10,
|
|
46
|
+
embeddingProvider: 'off',
|
|
47
|
+
embeddingEndpoint: '',
|
|
48
|
+
embeddingModel: '',
|
|
49
|
+
embeddingSidecarUrl: 'http://127.0.0.1:8766',
|
|
50
|
+
embeddingTimeoutMs: 180_000,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** 控制台可调旋钮。一个属性一条 JSON Schema,没有手写表单。 */
|
|
54
|
+
export const MEMORY_CONFIG_GROUP: ConfigGroup = {
|
|
55
|
+
id: 'world:memory',
|
|
56
|
+
owner: 'world:memory',
|
|
57
|
+
schema: {
|
|
58
|
+
type: 'object',
|
|
59
|
+
title: '记忆系统',
|
|
60
|
+
description: '三级记忆(短期/摘要/话题)与 Hybrid Recall 向量记忆(移植自 qq_bot)。',
|
|
61
|
+
properties: {
|
|
62
|
+
'worlds.memory.maxHistory': { type: 'number', title: '短期窗口(条)', description: '每个 scope 保留的最近对话条数。' },
|
|
63
|
+
'worlds.memory.maxVectorsPerScope': { type: 'number', title: '向量上限(条/scope)', description: '超出截掉最旧的。' },
|
|
64
|
+
'worlds.memory.observeIntervalMs': { type: 'number', title: '记忆推送周期(ms)', description: '把话题/摘要/要点推给人格的周期。', 'x-hot': true },
|
|
65
|
+
'worlds.memory.vectorTopK': { type: 'number', title: '检索条数', description: 'mem_recall 默认返回条数。' },
|
|
66
|
+
'worlds.memory.notesInPrompt': { type: 'number', title: '要点注入条数', description: '环境提示词常驻的「重要信息」上限。' },
|
|
67
|
+
'worlds.memory.embeddingProvider': {
|
|
68
|
+
type: 'string', title: '向量来源', enum: ['off', 'http', 'sidecar'],
|
|
69
|
+
description: 'off=纯 BM25;http=OpenAI 兼容 /embeddings;sidecar=qq_bot memory_server。失败自动降级 BM25。',
|
|
70
|
+
},
|
|
71
|
+
'worlds.memory.embeddingEndpoint': { type: 'string', title: 'Embeddings 端点', description: "http 模式用,如 http://127.0.0.1:11434/v1/embeddings。" },
|
|
72
|
+
'worlds.memory.embeddingModel': { type: 'string', title: 'Embeddings 模型', description: 'http 模式用,如 nomic-embed-text。' },
|
|
73
|
+
'worlds.memory.embeddingSidecarUrl': { type: 'string', title: 'Sidecar 地址', description: 'sidecar 模式用,qq_bot memory_server(默认 http://127.0.0.1:8766)。' },
|
|
74
|
+
'worlds.memory.embeddingTimeoutMs': { type: 'number', title: 'Sidecar 超时(ms)', description: 'qq_bot 向量模型首载慢,默认 3 分钟。' },
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// 运行期真实依赖 + 框架 `cortico/*` 类型都从这里取得,使本扩展可独立类型检查,
|
|
2
|
+
// 不拖入 Cortico 整个源码树(否则会因 lib 差异在框架内部文件上报错)。
|
|
3
|
+
// 形状按 Cortico 实际契约手描(与 templates/extension/world 对照)。运行期这些
|
|
4
|
+
// `import type` 会被完全擦除,框架用自己的模块钩子解析 `cortico/*`。
|
|
5
|
+
declare module 'cortico/core/types.ts' {
|
|
6
|
+
export type ToolTag = 'read' | 'write' | 'speak' | 'act' | 'flow' | 'snapshot';
|
|
7
|
+
export type EventOrigin = 'external' | 'internal';
|
|
8
|
+
|
|
9
|
+
export interface Logger {
|
|
10
|
+
info: (...args: unknown[]) => void;
|
|
11
|
+
warn: (...args: unknown[]) => void;
|
|
12
|
+
error: (...args: unknown[]) => void;
|
|
13
|
+
[k: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ToolOutcome {
|
|
17
|
+
text: string;
|
|
18
|
+
blobs?: unknown[];
|
|
19
|
+
failed?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface ToolDef {
|
|
22
|
+
name: string;
|
|
23
|
+
description: string;
|
|
24
|
+
parameters: Record<string, unknown>;
|
|
25
|
+
tags: readonly ToolTag[];
|
|
26
|
+
barrierAfter?: boolean;
|
|
27
|
+
endsTurn?: boolean;
|
|
28
|
+
handler: (args: Record<string, unknown>, ctx: unknown) => Promise<string | ToolOutcome>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface PromptDocDecl {
|
|
32
|
+
key: string;
|
|
33
|
+
title: string;
|
|
34
|
+
description: string;
|
|
35
|
+
path: string;
|
|
36
|
+
role: string;
|
|
37
|
+
vars?: { name: string; description?: string }[];
|
|
38
|
+
}
|
|
39
|
+
export interface WorldConsoleDecl {
|
|
40
|
+
config?: ConfigGroup[];
|
|
41
|
+
promptDocs?: PromptDocDecl[];
|
|
42
|
+
[k: string]: unknown;
|
|
43
|
+
}
|
|
44
|
+
export interface ConfigGroup {
|
|
45
|
+
id: string;
|
|
46
|
+
owner: string;
|
|
47
|
+
schema: Record<string, unknown>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface WorldSection {
|
|
51
|
+
enabled: boolean;
|
|
52
|
+
[k: string]: unknown;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface WorldHost {
|
|
56
|
+
pushEvent: (e: Record<string, unknown>, opts?: Record<string, unknown>) => Promise<unknown>;
|
|
57
|
+
log: Logger;
|
|
58
|
+
deploymentDir?: string;
|
|
59
|
+
[k: string]: unknown;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface World {
|
|
63
|
+
id: string;
|
|
64
|
+
envPromptVars(): Record<string, string> | null;
|
|
65
|
+
tools(): ToolDef[];
|
|
66
|
+
console?(): WorldConsoleDecl;
|
|
67
|
+
start(host: WorldHost): Promise<void>;
|
|
68
|
+
stop(): void | Promise<void>;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
declare module 'cortico/world.ts' {
|
|
73
|
+
import type { World, WorldSection } from 'cortico/core/types.ts';
|
|
74
|
+
export interface WorldContext<S extends WorldSection = WorldSection> {
|
|
75
|
+
cfg: S;
|
|
76
|
+
timezone: string;
|
|
77
|
+
language?: string;
|
|
78
|
+
/** 部署的数据目录(整片 gitignored)。持久化数据写这里。 */
|
|
79
|
+
dataDir?: string;
|
|
80
|
+
[k: string]: unknown;
|
|
81
|
+
}
|
|
82
|
+
export interface WorldDefinition<S extends WorldSection = WorldSection> {
|
|
83
|
+
id: string;
|
|
84
|
+
label: string;
|
|
85
|
+
defaults(): S;
|
|
86
|
+
preflight?(ctx: WorldContext<S>): void;
|
|
87
|
+
create(ctx: WorldContext<S>): World;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { WorldDefinition } from 'cortico/world.ts';
|
|
2
|
+
import { MEMORY_DEFAULTS, type MemoryConfig } from './config.ts';
|
|
3
|
+
import { MemoryWorld } from './world.ts';
|
|
4
|
+
|
|
5
|
+
export const MEMORY_WORLD: WorldDefinition<MemoryConfig> = {
|
|
6
|
+
id: 'memory',
|
|
7
|
+
label: '记忆系统',
|
|
8
|
+
defaults: () => ({ ...MEMORY_DEFAULTS }),
|
|
9
|
+
create: (ctx) => new MemoryWorld(ctx),
|
|
10
|
+
};
|
package/src/embedding.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Embedding 客户端 —— qq_bot「重负载外置 + 掉线降级」语义的移植:
|
|
3
|
+
* - `http`:POST OpenAI 兼容 `{model,input}` → `{data:[{embedding:[...]}]}`(本机 ollama 等)。
|
|
4
|
+
* - `sidecar`:qq_bot 的 memory_server(HTTP RPC),`POST {url}/call` 调
|
|
5
|
+
* `vector_memory.generate_embedding`,返回 384 维 MiniLM 向量或 null。
|
|
6
|
+
* - `off`:不出网,返回 null(调用方仅走 BM25)。
|
|
7
|
+
* 任一模式失败都静默降级返回 null —— 对齐 memory_client「sidecar 掉线,bot 永不停摆」。
|
|
8
|
+
*/
|
|
9
|
+
import type { MemoryConfig } from './config.ts';
|
|
10
|
+
|
|
11
|
+
export type VectorSource = 'http' | 'sidecar' | 'off';
|
|
12
|
+
|
|
13
|
+
export class EmbeddingClient {
|
|
14
|
+
constructor(private readonly cfg: MemoryConfig) {}
|
|
15
|
+
|
|
16
|
+
get provider(): VectorSource {
|
|
17
|
+
const p = this.cfg.embeddingProvider;
|
|
18
|
+
return p === 'http' || p === 'sidecar' ? p : 'off';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 生成一条文本向量;不可用 / 失败返回 null(绝不抛)。 */
|
|
22
|
+
async embed(text: string): Promise<readonly number[] | null> {
|
|
23
|
+
const trimmed = text.trim();
|
|
24
|
+
if (!trimmed) return null;
|
|
25
|
+
try {
|
|
26
|
+
switch (this.provider) {
|
|
27
|
+
case 'http': return await this.embedHttp(trimmed);
|
|
28
|
+
case 'sidecar': return await this.embedSidecar(trimmed);
|
|
29
|
+
default: return null;
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
return null; // 降级:只影响向量路,BM25 照常
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private async embedHttp(text: string): Promise<readonly number[] | null> {
|
|
37
|
+
const endpoint = this.cfg.embeddingEndpoint;
|
|
38
|
+
if (!endpoint) return null;
|
|
39
|
+
const res = await fetch(endpoint, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: { 'content-type': 'application/json' },
|
|
42
|
+
body: JSON.stringify({ model: this.cfg.embeddingModel || 'default', input: text }),
|
|
43
|
+
signal: AbortSignal.timeout(Math.min(this.cfg.embeddingTimeoutMs, 60_000)),
|
|
44
|
+
});
|
|
45
|
+
if (!res.ok) return null;
|
|
46
|
+
const json = (await res.json()) as { data?: Array<{ embedding?: number[] }> };
|
|
47
|
+
const embedding = json.data?.[0]?.embedding;
|
|
48
|
+
return Array.isArray(embedding) && embedding.length ? embedding : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** qq_bot memory_server 的通用 RPC:`{module, func, args}` → `{ok, result}`。 */
|
|
52
|
+
private async embedSidecar(text: string): Promise<readonly number[] | null> {
|
|
53
|
+
const base = (this.cfg.embeddingSidecarUrl || 'http://127.0.0.1:8766').replace(/\/+$/, '');
|
|
54
|
+
const res = await fetch(`${base}/call`, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: { 'content-type': 'application/json' },
|
|
57
|
+
body: JSON.stringify({ module: 'vector_memory', func: 'generate_embedding', args: [text] }),
|
|
58
|
+
signal: AbortSignal.timeout(this.cfg.embeddingTimeoutMs),
|
|
59
|
+
});
|
|
60
|
+
if (!res.ok) return null;
|
|
61
|
+
const json = (await res.json()) as { ok?: boolean; result?: unknown };
|
|
62
|
+
if (!json.ok) return null;
|
|
63
|
+
const result = json.result;
|
|
64
|
+
if (!Array.isArray(result) || !result.length) return null;
|
|
65
|
+
return result.every((n) => typeof n === 'number' && Number.isFinite(n)) ? (result as number[]) : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 探测 sidecar 是否在线(仅 sidecar 模式;用于启动日志与 mem_stats)。 */
|
|
69
|
+
async sidecarHealthy(): Promise<boolean | null> {
|
|
70
|
+
if (this.provider !== 'sidecar') return null;
|
|
71
|
+
try {
|
|
72
|
+
const base = (this.cfg.embeddingSidecarUrl || 'http://127.0.0.1:8766').replace(/\/+$/, '');
|
|
73
|
+
const res = await fetch(`${base}/health`, { signal: AbortSignal.timeout(3000) });
|
|
74
|
+
return res.ok;
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
export interface Fact {
|
|
6
|
+
hash: string;
|
|
7
|
+
text: string;
|
|
8
|
+
confidence: number;
|
|
9
|
+
createdAt: number;
|
|
10
|
+
updatedAt: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 独立事实库(对齐 qq_bot fact_store 的行为):
|
|
15
|
+
* - per-scope 隔离(user_id 等价)
|
|
16
|
+
* - SHA-256 去重(INSERT OR IGNORE, hash = sha256(`${scope}:${text}`))
|
|
17
|
+
* - 原子落盘(临时文件 + rename,同卷 rename 原子,等价于 fact_store 的 archive-first)
|
|
18
|
+
* - 全文/词频检索(对齐 qq_bot FTS5 全文召回的语义)
|
|
19
|
+
*
|
|
20
|
+
* 纯 JS 实现,不引入 better-sqlite3 原生依赖(world 扩展装原生模块有风险)。
|
|
21
|
+
*/
|
|
22
|
+
export class FactStore {
|
|
23
|
+
private readonly file: string;
|
|
24
|
+
private data: Record<string, Fact[]> = {};
|
|
25
|
+
private dirty = false;
|
|
26
|
+
private readonly flushTimer: ReturnType<typeof setInterval>;
|
|
27
|
+
|
|
28
|
+
constructor(dataDir: string, flushMs = 2000) {
|
|
29
|
+
this.file = join(dataDir, 'fact-store.json');
|
|
30
|
+
this.load();
|
|
31
|
+
this.flushTimer = setInterval(() => this.flush(), flushMs);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private load(): void {
|
|
35
|
+
try {
|
|
36
|
+
if (existsSync(this.file)) {
|
|
37
|
+
const raw = JSON.parse(readFileSync(this.file, 'utf8'));
|
|
38
|
+
if (raw && typeof raw === 'object' && raw.scopes) this.data = raw.scopes;
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
this.data = {};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private saveNow(): void {
|
|
46
|
+
if (!this.dirty) return;
|
|
47
|
+
try {
|
|
48
|
+
mkdirSync(dirname(this.file), { recursive: true });
|
|
49
|
+
const tmp = this.file + '.tmp';
|
|
50
|
+
writeFileSync(tmp, JSON.stringify({ scopes: this.data }, null, 2), 'utf8');
|
|
51
|
+
renameSync(tmp, this.file);
|
|
52
|
+
this.dirty = false;
|
|
53
|
+
} catch {
|
|
54
|
+
/* 落盘失败不影响内存态,下次 flush 重试 */
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 强制落盘(供 stop 调用)。 */
|
|
59
|
+
flush(): void {
|
|
60
|
+
clearInterval(this.flushTimer);
|
|
61
|
+
this.saveNow();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private hash(scope: string, text: string): string {
|
|
65
|
+
return createHash('sha256').update(`${scope}:${text}`).digest('hex');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 写入事实;返回新写入条数(已存在的重复事实跳过)。 */
|
|
69
|
+
add(scope: string, texts: string[]): number {
|
|
70
|
+
let added = 0;
|
|
71
|
+
const list = this.data[scope] ?? (this.data[scope] = []);
|
|
72
|
+
const now = Date.now() / 1000;
|
|
73
|
+
for (const t of texts) {
|
|
74
|
+
const text = (t ?? '').toString().trim();
|
|
75
|
+
if (!text) continue;
|
|
76
|
+
const h = this.hash(scope, text);
|
|
77
|
+
if (list.some((f) => f.hash === h)) continue; // 去重
|
|
78
|
+
list.push({ hash: h, text, confidence: 1.0, createdAt: now, updatedAt: now });
|
|
79
|
+
added++;
|
|
80
|
+
}
|
|
81
|
+
if (added) this.dirty = true;
|
|
82
|
+
return added;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 取某 scope 全部事实(按 updatedAt 倒序)。 */
|
|
86
|
+
get(scope: string): Fact[] {
|
|
87
|
+
return (this.data[scope] ?? []).slice().sort((a, b) => b.updatedAt - a.updatedAt);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 全文/词频召回(对齐 qq_bot recall_facts 的语义)。 */
|
|
91
|
+
recall(scope: string, query: string, limit = 5): Fact[] {
|
|
92
|
+
const q = query.trim().toLowerCase();
|
|
93
|
+
const list = this.data[scope] ?? [];
|
|
94
|
+
if (!q) return list.slice(0, Math.max(1, limit));
|
|
95
|
+
const terms = q.split(/\s+/).filter(Boolean);
|
|
96
|
+
return list
|
|
97
|
+
.map((f) => {
|
|
98
|
+
const text = f.text.toLowerCase();
|
|
99
|
+
let score = 0;
|
|
100
|
+
for (const term of terms) if (text.includes(term)) score += 1;
|
|
101
|
+
return { f, score };
|
|
102
|
+
})
|
|
103
|
+
.filter((x) => x.score > 0)
|
|
104
|
+
.sort((a, b) => b.score - a.score)
|
|
105
|
+
.slice(0, Math.max(1, limit))
|
|
106
|
+
.map((x) => x.f);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** 总条数(scope 省略则全部)。 */
|
|
110
|
+
count(scope?: string): number {
|
|
111
|
+
if (!scope) return Object.values(this.data).reduce((s, l) => s + l.length, 0);
|
|
112
|
+
return (this.data[scope] ?? []).length;
|
|
113
|
+
}
|
|
114
|
+
}
|
package/src/float16.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IEEE 754 半精度(fp16)编解码 —— 对齐 E:/qq_bot/vector_memory.py 的向量存储格式:
|
|
3
|
+
* `base64(fp16 little-endian bytes)`。编解码算法与 numpy 的 float16 互转语义一致
|
|
4
|
+
* (含 ±0、±Inf、NaN、次正规数与溢出饱和到 ±Inf),因此本扩展的 vector-memory.json
|
|
5
|
+
* 可以直接导入 qq_bot 侧的数据,反之亦然。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** fp32 → fp16(Uint16 位型)。非数值保守处理:NaN→NaN,±Inf→±Inf,超范围饱和。 */
|
|
9
|
+
export function f32ToF16(value: number): number {
|
|
10
|
+
const f = new Float32Array(1);
|
|
11
|
+
const u = new Uint32Array(f.buffer);
|
|
12
|
+
f[0] = value;
|
|
13
|
+
const x = u[0]!;
|
|
14
|
+
const sign = (x >>> 16) & 0x8000;
|
|
15
|
+
const rest = x & 0x7fffffff;
|
|
16
|
+
if (rest >= 0x7f800000) {
|
|
17
|
+
// NaN / Inf:保持 NaN 位型(qNaN 尾声截断)
|
|
18
|
+
return sign | 0x7c00 | (rest > 0x7f800000 ? 0x0200 : 0);
|
|
19
|
+
}
|
|
20
|
+
if (rest > 0x477fe000) return sign | 0x7c00; // 溢出 → ±Inf(对齐 numpy 就近舍入上界)
|
|
21
|
+
if (rest < 0x33000000) return sign; // 太小 → ±0
|
|
22
|
+
let exp = (rest >>> 23) - 127 + 15;
|
|
23
|
+
let mant = rest & 0x007fffff;
|
|
24
|
+
if (exp <= 0) {
|
|
25
|
+
// 次正规数:右移并对齐舍入
|
|
26
|
+
mant = (rest & 0x007fffff) | 0x00800000;
|
|
27
|
+
const shift = 14 - exp;
|
|
28
|
+
const half = 1 << (shift - 1);
|
|
29
|
+
mant = (mant + half) >> shift;
|
|
30
|
+
return sign | mant;
|
|
31
|
+
}
|
|
32
|
+
// 就近舍入( ties to even )
|
|
33
|
+
mant = mant + 0x00000fff + ((mant >>> 13) & 1);
|
|
34
|
+
if (mant & 0x00800000) { mant = 0; exp += 1; } // 进位到新指数
|
|
35
|
+
return sign | (exp << 10) | (mant >>> 13);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** fp16(Uint16 位型) → fp32。 */
|
|
39
|
+
export function f16ToF32(h: number): number {
|
|
40
|
+
const sign = (h & 0x8000) << 16;
|
|
41
|
+
const exp = (h & 0x7c00) >>> 10;
|
|
42
|
+
const mant = h & 0x03ff;
|
|
43
|
+
let out: number;
|
|
44
|
+
if (exp === 0) {
|
|
45
|
+
// 次正规 / 零:按规格化公式还原
|
|
46
|
+
if (mant === 0) out = 0;
|
|
47
|
+
else {
|
|
48
|
+
let e = -1;
|
|
49
|
+
let m = mant;
|
|
50
|
+
while (!(m & 0x0400)) { m <<= 1; e -= 1; }
|
|
51
|
+
m &= 0x03ff;
|
|
52
|
+
out = (127 + 15 + e - 10) << 23 | m << 13;
|
|
53
|
+
}
|
|
54
|
+
} else if (exp === 0x1f) {
|
|
55
|
+
out = mant ? 0x7fc00000 : 0x7f800000; // NaN / Inf
|
|
56
|
+
} else {
|
|
57
|
+
out = (exp - 15 + 127) << 23 | mant << 13;
|
|
58
|
+
}
|
|
59
|
+
const f = new Float32Array(1);
|
|
60
|
+
const u = new Uint32Array(f.buffer);
|
|
61
|
+
u[0] = (out | sign) >>> 0;
|
|
62
|
+
return f[0]!;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** float 列表 → base64(fp16 LE)。对齐 `_encode_vector_fp16`。 */
|
|
66
|
+
export function encodeVectorFp16(vec: readonly number[]): string {
|
|
67
|
+
const bytes = new Uint8Array(vec.length * 2);
|
|
68
|
+
for (let i = 0; i < vec.length; i++) {
|
|
69
|
+
const h = f32ToF16(vec[i]!);
|
|
70
|
+
bytes[i * 2] = h & 0xff;
|
|
71
|
+
bytes[i * 2 + 1] = (h >>> 8) & 0xff;
|
|
72
|
+
}
|
|
73
|
+
return Buffer.from(bytes).toString('base64');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** base64(fp16 LE) → Float32Array。对齐 `_decode_vector_fp16`(解码后转 fp32)。 */
|
|
77
|
+
export function decodeVectorFp16(encoded: string): Float32Array {
|
|
78
|
+
const raw = Buffer.from(encoded, 'base64');
|
|
79
|
+
const out = new Float32Array(raw.length >>> 1);
|
|
80
|
+
for (let i = 0; i < out.length; i++) {
|
|
81
|
+
out[i] = f16ToF32(raw[i * 2]! | (raw[i * 2 + 1]! << 8));
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// 框架按 package.json 的 `main` 导入本文件,并读取具名导出 id/label/defaults/create
|
|
2
|
+
// 作为 WorldDefinition(不是默认导出)。所以这里把字段拆成具名导出。
|
|
3
|
+
import { MEMORY_WORLD } from './definition.ts';
|
|
4
|
+
|
|
5
|
+
export const id = MEMORY_WORLD.id;
|
|
6
|
+
export const label = MEMORY_WORLD.label;
|
|
7
|
+
export const defaults = MEMORY_WORLD.defaults;
|
|
8
|
+
export const create = MEMORY_WORLD.create;
|
|
9
|
+
|
|
10
|
+
export { MEMORY_WORLD } from './definition.ts';
|
|
11
|
+
export default MEMORY_WORLD;
|
|
12
|
+
export { MEMORY_DEFAULTS, MEMORY_CONFIG_GROUP, type MemoryConfig } from './config.ts';
|