@unblocklabs/unblock-memory 0.3.30 → 0.5.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 +3 -3
- package/dist/src/config.d.ts +3 -4
- package/dist/src/config.js +31 -11
- package/dist/src/diagnostics.d.ts +1 -3
- package/dist/src/manager.d.ts +4 -8
- package/dist/src/manager.js +17 -45
- package/dist/src/memory-passage.d.ts +26 -0
- package/dist/src/memory-passage.js +48 -0
- package/dist/src/memory-whisperer.d.ts +2 -1
- package/dist/src/memory-whisperer.js +37 -42
- package/dist/src/query-contract.d.ts +31 -0
- package/dist/src/query-contract.js +58 -0
- package/dist/src/query-generator.d.ts +15 -0
- package/dist/src/query-generator.js +136 -0
- package/dist/src/query-tokenizer/contract.json +10 -0
- package/dist/src/response-stages.d.ts +3 -3
- package/dist/src/training-candidates.d.ts +2 -1
- package/dist/src/training-candidates.js +12 -9
- package/dist/src/training-input.d.ts +13 -10
- package/dist/src/training-input.js +22 -18
- package/dist/src/training-judge.d.ts +16 -46
- package/dist/src/training-judge.js +6 -53
- package/dist/src/training-models.d.ts +15 -7
- package/dist/src/training-models.js +36 -38
- package/dist/src/training-queries.d.ts +24 -7
- package/dist/src/training-queries.js +237 -101
- package/dist/src/training-retrieval.d.ts +9 -2
- package/dist/src/training-retrieval.js +36 -12
- package/dist/src/training-runtime.js +8 -7
- package/dist/src/training-store.d.ts +46 -16
- package/dist/src/training-store.js +85 -29
- package/dist/src/training.d.ts +10 -0
- package/dist/src/training.js +7 -5
- package/dist/src/typesafe.d.ts +58 -8
- package/dist/src/typesafe.js +47 -29
- package/dist/src/whisperer-context.d.ts +0 -8
- package/dist/src/whisperer-context.js +0 -23
- package/docs/configuration.md +5 -3
- package/docs/memory-training.md +120 -58
- package/docs/retrieval.md +59 -43
- package/openclaw.plugin.json +15 -6
- package/package.json +4 -1
- package/src/mlx-query-worker.py +5 -8
- package/src/query-tokenizer/LICENSE +71 -0
- package/src/query-tokenizer/README.md +21 -0
- package/src/query-tokenizer/chat_template.jinja +115 -0
- package/src/query-tokenizer/contract.json +10 -0
- package/src/query-tokenizer/tokenizer.json +323830 -0
- package/src/query-tokenizer/tokenizer_config.json +19 -0
- package/src/query_contract.py +60 -0
- package/dist/src/mlx-query.d.ts +0 -22
- package/dist/src/mlx-query.js +0 -155
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"backend": "tokenizers",
|
|
3
|
+
"bos_token": "<|startoftext|>",
|
|
4
|
+
"clean_up_tokenization_spaces": false,
|
|
5
|
+
"eos_token": "<|im_end|>",
|
|
6
|
+
"is_local": true,
|
|
7
|
+
"legacy": false,
|
|
8
|
+
"model_input_names": [
|
|
9
|
+
"input_ids",
|
|
10
|
+
"attention_mask"
|
|
11
|
+
],
|
|
12
|
+
"model_max_length": 1000000000000000019884624838656,
|
|
13
|
+
"pad_token": "<|pad|>",
|
|
14
|
+
"sp_model_kwargs": {},
|
|
15
|
+
"spaces_between_special_tokens": false,
|
|
16
|
+
"tokenizer_class": "TokenizersBackend",
|
|
17
|
+
"use_default_system_prompt": false,
|
|
18
|
+
"use_fast": true
|
|
19
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""One student prompt, pinned tokenizer, serialization and target contract for v2."""
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
ASSETS = Path(__file__).with_name("query-tokenizer")
|
|
7
|
+
CONTRACT = json.loads((ASSETS / "contract.json").read_text())
|
|
8
|
+
MODEL, REVISION, SYSTEM = (CONTRACT[key] for key in ("model", "revision", "system"))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def serialize_conversation(conversation):
|
|
12
|
+
return json.dumps({"history": conversation["history"], "currentRequest": conversation["currentRequest"]},
|
|
13
|
+
ensure_ascii=False, separators=(",", ":")).replace("<", "\\u003c")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def validate_conversation(conversation, tokenizer):
|
|
17
|
+
if not isinstance(conversation, dict) or set(conversation) != {"history", "currentRequest"} or \
|
|
18
|
+
not isinstance(conversation["currentRequest"], str) or not conversation["currentRequest"] or \
|
|
19
|
+
not isinstance(conversation["history"], list):
|
|
20
|
+
raise ValueError("Invalid prepared conversation")
|
|
21
|
+
for message in conversation["history"]:
|
|
22
|
+
if not isinstance(message, dict) or set(message) != {"role", "content"} or \
|
|
23
|
+
message["role"] not in ("user", "assistant") or not isinstance(message["content"], str):
|
|
24
|
+
raise ValueError("Invalid prepared conversation history")
|
|
25
|
+
serialized = serialize_conversation(conversation)
|
|
26
|
+
if len(serialized.encode("utf-8")) > CONTRACT["conversationBytes"] or \
|
|
27
|
+
len(tokenizer.encode(serialized, add_special_tokens=False)) > CONTRACT["conversationTokens"]:
|
|
28
|
+
raise ValueError("Prepared conversation exceeds shared window")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def validate_query_pair(value):
|
|
32
|
+
if not isinstance(value, dict) or set(value) != {"lex", "vec"} or \
|
|
33
|
+
any(not isinstance(value[key], str) or not value[key].strip() for key in ("lex", "vec")):
|
|
34
|
+
raise ValueError("Expected exactly one nonempty lex/vec query pair")
|
|
35
|
+
return {key: value[key].strip() for key in ("lex", "vec")}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def student_messages(conversation):
|
|
39
|
+
return [{"role": "system", "content": SYSTEM},
|
|
40
|
+
{"role": "user", "content": "<conversation_data>\n" + serialize_conversation(conversation) + "\n</conversation_data>"}]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def student_prompt_tokens(tokenizer, conversation):
|
|
44
|
+
validate_conversation(conversation, tokenizer)
|
|
45
|
+
# Transformers 5 may return BatchEncoding for tokenize=True. Encode the
|
|
46
|
+
# rendered template explicitly, exactly as dataset preparation does.
|
|
47
|
+
text = tokenizer.apply_chat_template(student_messages(conversation), tokenize=False, add_generation_prompt=True)
|
|
48
|
+
return tokenizer.encode(text, add_special_tokens=False)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def load_tokenizer():
|
|
52
|
+
from transformers import AutoTokenizer
|
|
53
|
+
return AutoTokenizer.from_pretrained(ASSETS, local_files_only=True, trust_remote_code=False)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def verify_model_tokenizer(root):
|
|
57
|
+
# The model package must carry the exact tokenizer/template used for collection.
|
|
58
|
+
for name in ("tokenizer.json", "tokenizer_config.json", "chat_template.jinja"):
|
|
59
|
+
if hashlib.sha256((root / name).read_bytes()).digest() != hashlib.sha256((ASSETS / name).read_bytes()).digest():
|
|
60
|
+
raise ValueError("Incompatible model tokenizer or chat template")
|
package/dist/src/mlx-query.d.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { UnblockMemoryConfig } from "./config.js";
|
|
2
|
-
type Conversation = {
|
|
3
|
-
history: {
|
|
4
|
-
role: "user" | "assistant";
|
|
5
|
-
content: string;
|
|
6
|
-
}[];
|
|
7
|
-
currentRequest: string;
|
|
8
|
-
};
|
|
9
|
-
/** Preserve whole visible messages and the complete current request, never tool/thinking text. */
|
|
10
|
-
export declare function queryConversation(prompt: string, messages: readonly unknown[], historyMessages: number): Conversation;
|
|
11
|
-
export declare class MlxQueryGenerator {
|
|
12
|
-
#private;
|
|
13
|
-
private readonly config;
|
|
14
|
-
/** Gateway hook and tool registries can evaluate/register this plugin separately. */
|
|
15
|
-
static shared(config: NonNullable<UnblockMemoryConfig["memoryWhisperer"]["mlx"]>): MlxQueryGenerator;
|
|
16
|
-
constructor(config: NonNullable<UnblockMemoryConfig["memoryWhisperer"]["mlx"]>);
|
|
17
|
-
get closed(): boolean;
|
|
18
|
-
start(): Promise<void>;
|
|
19
|
-
generate(conversation: Conversation, signal: AbortSignal): Promise<string[]>;
|
|
20
|
-
close(): void;
|
|
21
|
-
}
|
|
22
|
-
export {};
|
package/dist/src/mlx-query.js
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { createInterface } from "node:readline";
|
|
6
|
-
import { abortable } from "./abortable.js";
|
|
7
|
-
import { messageText } from "./whisperer-context.js";
|
|
8
|
-
import { conversationUserText } from "./response-text.js";
|
|
9
|
-
/** Preserve whole visible messages and the complete current request, never tool/thinking text. */
|
|
10
|
-
export function queryConversation(prompt, messages, historyMessages) {
|
|
11
|
-
const currentRequest = conversationUserText(prompt)?.text ?? "";
|
|
12
|
-
const visible = messages.flatMap(message => {
|
|
13
|
-
const text = messageText(message);
|
|
14
|
-
const content = text?.role === "user" ? conversationUserText(text.text)?.text : text?.text;
|
|
15
|
-
return text && content ? [{ role: text.role, content }] : [];
|
|
16
|
-
});
|
|
17
|
-
if (visible.at(-1)?.role === "user" && visible.at(-1)?.content === currentRequest)
|
|
18
|
-
visible.pop();
|
|
19
|
-
const history = historyMessages ? visible.slice(-historyMessages) : [];
|
|
20
|
-
while (history.length && Buffer.byteLength(JSON.stringify({ history, currentRequest })) > 24_000)
|
|
21
|
-
history.shift();
|
|
22
|
-
if (!currentRequest)
|
|
23
|
-
throw new Error("Missing or unparseable current request");
|
|
24
|
-
if (Buffer.byteLength(JSON.stringify({ history, currentRequest })) > 24_000) {
|
|
25
|
-
throw new Error("Query input exceeds context budget");
|
|
26
|
-
}
|
|
27
|
-
return { history, currentRequest };
|
|
28
|
-
}
|
|
29
|
-
export class MlxQueryGenerator {
|
|
30
|
-
config;
|
|
31
|
-
/** Gateway hook and tool registries can evaluate/register this plugin separately. */
|
|
32
|
-
static shared(config) {
|
|
33
|
-
const scope = globalThis;
|
|
34
|
-
const symbol = Symbol.for("unblock-memory.mlx-workers.v1");
|
|
35
|
-
const workers = (scope[symbol] ??= new Map());
|
|
36
|
-
const key = JSON.stringify([config.pythonPath, config.modelPath]);
|
|
37
|
-
let worker = workers.get(key);
|
|
38
|
-
if (!worker || worker.closed) {
|
|
39
|
-
worker = new MlxQueryGenerator(config);
|
|
40
|
-
workers.set(key, worker);
|
|
41
|
-
}
|
|
42
|
-
return worker;
|
|
43
|
-
}
|
|
44
|
-
#child;
|
|
45
|
-
#ready;
|
|
46
|
-
#closed = false;
|
|
47
|
-
#retryAfter = 0;
|
|
48
|
-
#pending = new Map();
|
|
49
|
-
constructor(config) {
|
|
50
|
-
this.config = config;
|
|
51
|
-
}
|
|
52
|
-
get closed() { return this.#closed; }
|
|
53
|
-
start() {
|
|
54
|
-
if (this.#closed)
|
|
55
|
-
return Promise.reject(new Error("Query worker stopped"));
|
|
56
|
-
if (this.#ready)
|
|
57
|
-
return this.#ready;
|
|
58
|
-
if (Date.now() < this.#retryAfter)
|
|
59
|
-
return Promise.reject(new Error("Query worker restarting"));
|
|
60
|
-
const adjacent = new URL("./mlx-query-worker.py", import.meta.url);
|
|
61
|
-
const script = existsSync(adjacent) ? adjacent : new URL("../../src/mlx-query-worker.py", import.meta.url);
|
|
62
|
-
const child = spawn(this.config.pythonPath, ["-u", fileURLToPath(script), this.config.modelPath], {
|
|
63
|
-
stdio: "pipe", env: { ...process.env, HF_HUB_OFFLINE: "1", TRANSFORMERS_OFFLINE: "1", TOKENIZERS_PARALLELISM: "false" },
|
|
64
|
-
});
|
|
65
|
-
this.#child = child;
|
|
66
|
-
// No source text from Python warnings/exceptions enters Gateway logs.
|
|
67
|
-
child.stderr.resume();
|
|
68
|
-
const lines = createInterface({ input: child.stdout });
|
|
69
|
-
this.#ready = new Promise((resolve, reject) => {
|
|
70
|
-
const timer = setTimeout(() => child.kill(), 60_000);
|
|
71
|
-
const fail = () => {
|
|
72
|
-
clearTimeout(timer);
|
|
73
|
-
lines.close();
|
|
74
|
-
reject(new Error("Query worker unavailable"));
|
|
75
|
-
if (this.#child !== child)
|
|
76
|
-
return;
|
|
77
|
-
this.#child = undefined;
|
|
78
|
-
this.#ready = undefined;
|
|
79
|
-
this.#retryAfter = Date.now() + 2000;
|
|
80
|
-
for (const request of this.#pending.values())
|
|
81
|
-
request.reject(new Error("Query worker unavailable"));
|
|
82
|
-
this.#pending.clear();
|
|
83
|
-
};
|
|
84
|
-
child.once("error", fail);
|
|
85
|
-
child.once("close", fail);
|
|
86
|
-
child.stdin.on("error", () => child.kill());
|
|
87
|
-
lines.on("line", line => {
|
|
88
|
-
try {
|
|
89
|
-
const value = JSON.parse(line);
|
|
90
|
-
if (!value || typeof value !== "object")
|
|
91
|
-
throw new Error("Invalid worker response");
|
|
92
|
-
if ("ready" in value && value.ready === true) {
|
|
93
|
-
clearTimeout(timer);
|
|
94
|
-
resolve();
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
if (!("id" in value) || typeof value.id !== "string")
|
|
98
|
-
throw new Error("Missing worker request ID");
|
|
99
|
-
const request = this.#pending.get(value.id);
|
|
100
|
-
if (!request)
|
|
101
|
-
return; // Cancelled request; never surface late results.
|
|
102
|
-
this.#pending.delete(value.id);
|
|
103
|
-
if (!("text" in value) || typeof value.text !== "string") {
|
|
104
|
-
request.reject(new Error("Query generation failed"));
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
let queries;
|
|
108
|
-
try {
|
|
109
|
-
queries = JSON.parse(value.text).queries;
|
|
110
|
-
}
|
|
111
|
-
catch { /* reject below */ }
|
|
112
|
-
const usable = Array.isArray(queries) ? [...new Set(queries
|
|
113
|
-
.filter((query) => typeof query === "string")
|
|
114
|
-
.map(query => query.trim()).filter(Boolean))] : [];
|
|
115
|
-
if (!usable.length) {
|
|
116
|
-
request.reject(new Error("No usable generated queries"));
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
request.resolve(usable);
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
child.kill();
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
return this.#ready;
|
|
127
|
-
}
|
|
128
|
-
async generate(conversation, signal) {
|
|
129
|
-
await abortable(this.start(), signal);
|
|
130
|
-
signal.throwIfAborted();
|
|
131
|
-
if (this.#pending.size >= 8)
|
|
132
|
-
throw new Error("Query worker busy");
|
|
133
|
-
const id = randomUUID();
|
|
134
|
-
const child = this.#child;
|
|
135
|
-
const result = new Promise((resolve, reject) => this.#pending.set(id, { resolve, reject }));
|
|
136
|
-
const cancel = () => {
|
|
137
|
-
this.#pending.get(id)?.reject(new Error("Query generation cancelled"));
|
|
138
|
-
this.#pending.delete(id);
|
|
139
|
-
if (child.stdin.writable)
|
|
140
|
-
child.stdin.write(JSON.stringify({ id, cancel: true }) + "\n");
|
|
141
|
-
};
|
|
142
|
-
signal.addEventListener("abort", cancel, { once: true });
|
|
143
|
-
child.stdin.write(JSON.stringify({ id, conversation }) + "\n");
|
|
144
|
-
try {
|
|
145
|
-
return await result;
|
|
146
|
-
}
|
|
147
|
-
finally {
|
|
148
|
-
signal.removeEventListener("abort", cancel);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
close() {
|
|
152
|
-
this.#closed = true;
|
|
153
|
-
this.#child?.kill();
|
|
154
|
-
}
|
|
155
|
-
}
|