@unblocklabs/unblock-memory 0.3.24 → 0.3.25
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 -0
- package/dist/src/plugin.js +2 -0
- package/dist/src/training-candidates.d.ts +13 -0
- package/dist/src/training-candidates.js +75 -0
- package/dist/src/training-gate.d.ts +27 -0
- package/dist/src/training-gate.js +33 -0
- package/dist/src/training-input.d.ts +51 -0
- package/dist/src/training-input.js +199 -0
- package/dist/src/training-judge.d.ts +74 -0
- package/dist/src/training-judge.js +57 -0
- package/dist/src/training-models.d.ts +20 -0
- package/dist/src/training-models.js +72 -0
- package/dist/src/training-queries.d.ts +120 -0
- package/dist/src/training-queries.js +281 -0
- package/dist/src/training-retrieval.d.ts +37 -0
- package/dist/src/training-retrieval.js +176 -0
- package/dist/src/training-runtime.d.ts +4 -0
- package/dist/src/training-runtime.js +140 -0
- package/dist/src/training-store.d.ts +160 -0
- package/dist/src/training-store.js +300 -0
- package/dist/src/training.d.ts +57 -0
- package/dist/src/training.js +104 -0
- package/docs/memory-training.md +147 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { readSessionManifest } from "./session-sync.js";
|
|
6
|
+
import { resolveSessionSource } from "./sources.js";
|
|
7
|
+
import { trainingCandidates } from "./training-candidates.js";
|
|
8
|
+
export const TRAINING_RETRIEVAL_VERSION = "qmd-2.10.1-historical-prefix-depth10-v2";
|
|
9
|
+
export const TRAINING_SEARCH_OPTIONS = { vector: 10, bm25: 10, mergedLimit: null, rerank: false };
|
|
10
|
+
/** Never infer dates by parsing message bodies: headings can be quoted or forged. */
|
|
11
|
+
export function historicalPrefix(body, spans, cutoff) {
|
|
12
|
+
if (!spans?.length || !body.startsWith("# Transcript\n\n") || !Number.isFinite(cutoff))
|
|
13
|
+
return;
|
|
14
|
+
let previousEnd = 14;
|
|
15
|
+
const parsed = [];
|
|
16
|
+
for (const span of spans) {
|
|
17
|
+
if (![span.start, span.bodyStart, span.end].every(Number.isSafeInteger) || span.start < previousEnd ||
|
|
18
|
+
span.bodyStart <= span.start || span.end < span.bodyStart || span.end > body.length ||
|
|
19
|
+
body.slice(previousEnd, span.start).trim())
|
|
20
|
+
return;
|
|
21
|
+
const heading = `## ${span.type === "user" ? "User" : "Assistant"} — ${span.name} — ${span.timestamp}\n\n`;
|
|
22
|
+
if (body.slice(span.start, span.bodyStart) !== heading)
|
|
23
|
+
return;
|
|
24
|
+
// Explicit zones only: no process-local timezone interpretation. Unknown zones fail closed.
|
|
25
|
+
const time = typeof span.timestamp === "string" && / (?:UTC|GMT(?:[+-]\d{1,2}(?::\d{2})?)?|[ECMP][SD]T)$/u.test(span.timestamp)
|
|
26
|
+
? Date.parse(span.timestamp) : NaN;
|
|
27
|
+
parsed.push({ span, time });
|
|
28
|
+
previousEnd = span.end;
|
|
29
|
+
}
|
|
30
|
+
if (body.slice(previousEnd).trim())
|
|
31
|
+
return;
|
|
32
|
+
// Projection dates have second precision. Exclude the entire cutoff second,
|
|
33
|
+
// including the current question, equal-time replies and crossing chunks.
|
|
34
|
+
const before = Math.floor(cutoff / 1000) * 1000;
|
|
35
|
+
const safe = [];
|
|
36
|
+
for (const item of parsed) {
|
|
37
|
+
if (!Number.isFinite(item.time) || item.time >= before)
|
|
38
|
+
break;
|
|
39
|
+
safe.push(item);
|
|
40
|
+
}
|
|
41
|
+
if (!safe.length)
|
|
42
|
+
return;
|
|
43
|
+
return { body: body.slice(0, parsed[safe.length]?.span.start ?? body.length), spans: safe.map(s => s.span) };
|
|
44
|
+
}
|
|
45
|
+
/** A read-only source snapshot, copied into a disposable in-memory QMD index.
|
|
46
|
+
* No filesystem projection, live-index mutation, model re-embedding or dependency patch. */
|
|
47
|
+
export async function historicalTrainingSearch(stateDir, chatTypes, cutoff, openStore) {
|
|
48
|
+
const indexPath = join(stateDir, "index.sqlite");
|
|
49
|
+
if (!existsSync(indexPath))
|
|
50
|
+
throw new Error("No QMD index; sync sessions before evaluating training queries");
|
|
51
|
+
const manifest = await readSessionManifest(join(stateDir, "sessions-manifest.json"));
|
|
52
|
+
const source = resolveSessionSource(join(stateDir, "sessions"), chatTypes);
|
|
53
|
+
const createStore = openStore ?? (await import("@unblocklabs/qmd")).createStore;
|
|
54
|
+
const qmd = await createStore({ dbPath: ":memory:", keepModelsWarm: true,
|
|
55
|
+
config: { collections: { [source.collection]: { path: source.root, pattern: "**/*.md" } } } });
|
|
56
|
+
// A snapshot has one native embedding context. Serialize its vector stage only;
|
|
57
|
+
// QMD's remote TypeSafe scoring remains concurrent across all ten queries.
|
|
58
|
+
const searchVec = qmd.internal.searchVec;
|
|
59
|
+
let vectorTail = Promise.resolve();
|
|
60
|
+
qmd.internal.searchVec = (...args) => {
|
|
61
|
+
const result = vectorTail.then(() => searchVec(...args));
|
|
62
|
+
vectorTail = result.then(() => { }, () => { });
|
|
63
|
+
return result;
|
|
64
|
+
};
|
|
65
|
+
const db = qmd.internal.db;
|
|
66
|
+
const maxDate = new Date(cutoff).toISOString();
|
|
67
|
+
const report = { sessions: 0, chunks: 0, excluded: 0, truncated: 0, excludedChunks: 0 };
|
|
68
|
+
const metadata = new Map();
|
|
69
|
+
const fingerprint = createHash("sha256").update(JSON.stringify([TRAINING_RETRIEVAL_VERSION, chatTypes.toSorted(), maxDate]));
|
|
70
|
+
try {
|
|
71
|
+
// QMD's SDK only opens writable stores. Use its exact SQLite binding in
|
|
72
|
+
// read-only mode; mixing node:sqlite with better-sqlite3 corrupts sqlite-vec's
|
|
73
|
+
// process-global SQLite API pointer when both load the native extension.
|
|
74
|
+
const requireQmd = createRequire(import.meta.resolve("@unblocklabs/qmd"));
|
|
75
|
+
const Database = requireQmd("better-sqlite3");
|
|
76
|
+
const sourceDb = new Database(indexPath, { readonly: true, fileMustExist: true });
|
|
77
|
+
try {
|
|
78
|
+
const extension = requireQmd("sqlite-vec");
|
|
79
|
+
if (!extension || typeof extension !== "object" || !("getLoadablePath" in extension) || typeof extension.getLoadablePath !== "function") {
|
|
80
|
+
throw new Error("QMD sqlite-vec extension unavailable");
|
|
81
|
+
}
|
|
82
|
+
const extensionPath = extension.getLoadablePath();
|
|
83
|
+
if (typeof extensionPath !== "string")
|
|
84
|
+
throw new Error("QMD sqlite-vec path unavailable");
|
|
85
|
+
sourceDb.loadExtension(extensionPath);
|
|
86
|
+
sourceDb.exec("PRAGMA query_only=ON; PRAGMA busy_timeout=5000; BEGIN");
|
|
87
|
+
db.exec("BEGIN");
|
|
88
|
+
const settings = sourceDb.prepare("SELECT key,value FROM store_config WHERE key='embedding_chunk_strategy'").all();
|
|
89
|
+
for (const { key, value } of settings)
|
|
90
|
+
db.prepare("INSERT OR REPLACE INTO store_config VALUES (?,?)").run(key, value);
|
|
91
|
+
fingerprint.update(JSON.stringify(settings));
|
|
92
|
+
const document = sourceDb.prepare(`SELECT d.hash,c.doc FROM documents d JOIN content c ON c.hash=d.hash
|
|
93
|
+
WHERE d.active=1 AND d.collection=? AND d.path=?`);
|
|
94
|
+
const chunks = sourceDb.prepare(`SELECT cv.seq,cv.pos,cv.chunk_len,cv.model,cv.embed_fingerprint,v.embedding
|
|
95
|
+
FROM content_vectors cv JOIN vectors_vec v ON v.hash_seq=cv.hash||'_'||cv.seq
|
|
96
|
+
WHERE cv.hash=? ORDER BY cv.seq`);
|
|
97
|
+
let dimensions;
|
|
98
|
+
for (const session of Object.values(manifest.sessions).sort((a, b) => a.documentPath.localeCompare(b.documentPath))) {
|
|
99
|
+
// Loggie projections can retroactively annotate old text using later revisions.
|
|
100
|
+
// Workspace files and meetings lack immutable historical content proof here.
|
|
101
|
+
if (!chatTypes.includes(session.chatType) || session.provider === "loggie" || session.startedAt >= cutoff) {
|
|
102
|
+
report.excluded++;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const row = document.get(source.collection, session.documentPath);
|
|
106
|
+
if (!row || createHash("sha256").update(row.doc).digest("hex") !== session.projectionHash) {
|
|
107
|
+
report.excluded++;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const prefix = historicalPrefix(row.doc, session.messages, cutoff);
|
|
111
|
+
if (!prefix) {
|
|
112
|
+
report.excluded++;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const hash = createHash("sha256").update(prefix.body).digest("hex");
|
|
116
|
+
qmd.internal.insertContent(hash, prefix.body, maxDate);
|
|
117
|
+
qmd.internal.insertDocument(source.collection, session.documentPath, "Transcript", hash, maxDate, maxDate);
|
|
118
|
+
metadata.set(`qmd://${source.collection}/${session.documentPath}`, prefix.spans);
|
|
119
|
+
fingerprint.update(JSON.stringify([session.documentPath, hash]));
|
|
120
|
+
report.sessions++;
|
|
121
|
+
if (prefix.spans.length < (session.messages?.length ?? 0))
|
|
122
|
+
report.truncated++;
|
|
123
|
+
for (const chunk of chunks.all(row.hash)) {
|
|
124
|
+
if (!Number.isSafeInteger(chunk.pos) || !Number.isSafeInteger(chunk.chunk_len) || chunk.pos < 0 || chunk.chunk_len <= 0 ||
|
|
125
|
+
chunk.pos + chunk.chunk_len > prefix.body.length) {
|
|
126
|
+
report.excludedChunks++;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const bytes = Buffer.from(chunk.embedding);
|
|
130
|
+
if (bytes.length % 4 || !bytes.length)
|
|
131
|
+
throw new Error("Invalid historical embedding");
|
|
132
|
+
const size = bytes.length / 4;
|
|
133
|
+
if (dimensions === undefined) {
|
|
134
|
+
dimensions = size;
|
|
135
|
+
qmd.internal.ensureVecTable(size);
|
|
136
|
+
}
|
|
137
|
+
if (dimensions !== size)
|
|
138
|
+
throw new Error("Mixed historical embedding dimensions");
|
|
139
|
+
const vector = new Float32Array(size);
|
|
140
|
+
for (let i = 0; i < size; i++)
|
|
141
|
+
vector[i] = bytes.readFloatLE(i * 4);
|
|
142
|
+
qmd.internal.insertEmbedding(hash, chunk.seq, chunk.pos, vector, chunk.model, maxDate, 1, chunk.embed_fingerprint, chunk.chunk_len);
|
|
143
|
+
fingerprint.update(JSON.stringify([chunk.seq, chunk.pos, chunk.chunk_len, chunk.model, chunk.embed_fingerprint])).update(bytes);
|
|
144
|
+
report.chunks++;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
db.exec("COMMIT");
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
sourceDb.close();
|
|
151
|
+
}
|
|
152
|
+
const corpusHash = fingerprint.digest("hex");
|
|
153
|
+
return { corpusHash, report, maxDate,
|
|
154
|
+
search: async (query) => {
|
|
155
|
+
if (!report.sessions)
|
|
156
|
+
return [];
|
|
157
|
+
if (!report.chunks)
|
|
158
|
+
throw new Error("Historical snapshot has no vectors; refusing a BM25-only evaluation");
|
|
159
|
+
const hits = await trainingCandidates(qmd, query, source.collection, `Historical request made at ${maxDate}. Current, now and latest refer to that timestamp.`);
|
|
160
|
+
return hits.map(hit => trainingHit(hit, metadata, cutoff));
|
|
161
|
+
}, close: () => qmd.close() };
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
await qmd.close();
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function trainingHit(hit, metadata, cutoff) {
|
|
169
|
+
const spans = metadata.get(hit.file)?.filter(s => s.start < hit.bestChunkPos + hit.bestChunk.length && s.end > hit.bestChunkPos);
|
|
170
|
+
if (!spans?.length || spans.some(s => !(Date.parse(s.timestamp) < Math.floor(cutoff / 1000) * 1000)) ||
|
|
171
|
+
hit.body.slice(hit.bestChunkPos, hit.bestChunkPos + hit.bestChunk.length) !== hit.bestChunk) {
|
|
172
|
+
throw new Error("QMD returned evidence outside the historical snapshot");
|
|
173
|
+
}
|
|
174
|
+
return { path: hit.file, position: hit.bestChunkPos, text: hit.bestChunk, dates: [...new Set(spans.map(s => s.timestamp))],
|
|
175
|
+
score: hit.score, methods: hit.explain?.methods ?? [] };
|
|
176
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
|
2
|
+
import type { UnblockMemoryConfig } from "./config.js";
|
|
3
|
+
/** CLI only: no scheduler, tools, hooks, or live memory-index changes. */
|
|
4
|
+
export declare function registerMemoryTraining(api: OpenClawPluginApi, config: UnblockMemoryConfig): void;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { existsSync, openSync, writeSync, closeSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { resolveAgentDir, resolveStateDir } from "openclaw/plugin-sdk/memory-core-host-engine-foundation";
|
|
4
|
+
import { listAgentIds } from "openclaw/plugin-sdk/agent-runtime";
|
|
5
|
+
import { normalizeAgentIdStrict } from "openclaw/plugin-sdk/routing";
|
|
6
|
+
import { TrainingStore } from "./training-store.js";
|
|
7
|
+
import { TRAINING_GATE_THRESHOLD } from "./training-gate.js";
|
|
8
|
+
import { collectTraining, runTraining } from "./training.js";
|
|
9
|
+
import { generateTrainingQueries, evaluateTrainingQueries, exportQueryTraining, TRAINING_EVALUATION_CONCURRENCY } from "./training-queries.js";
|
|
10
|
+
function dateOption(value, fallback) {
|
|
11
|
+
if (value === undefined)
|
|
12
|
+
return fallback;
|
|
13
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value) || !Number.isFinite(Date.parse(value)) || new Date(value).toISOString().slice(0, 10) !== value) {
|
|
14
|
+
throw new Error("Dates must be valid YYYY-MM-DD UTC dates");
|
|
15
|
+
}
|
|
16
|
+
return Date.parse(value);
|
|
17
|
+
}
|
|
18
|
+
function thresholdOption(value) {
|
|
19
|
+
const result = Number(value);
|
|
20
|
+
if (!value.trim() || !Number.isFinite(result) || result < 0 || result > 1)
|
|
21
|
+
throw new Error("Threshold must be between 0 and 1");
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
/** CLI only: no scheduler, tools, hooks, or live memory-index changes. */
|
|
25
|
+
export function registerMemoryTraining(api, config) {
|
|
26
|
+
const paths = (cfg, id) => {
|
|
27
|
+
const normalized = normalizeAgentIdStrict(id);
|
|
28
|
+
if (!normalized.ok || !listAgentIds(cfg).includes(normalized.value))
|
|
29
|
+
throw new Error("Unknown memory-training agent");
|
|
30
|
+
const agentId = normalized.value;
|
|
31
|
+
const stateDir = join(resolveStateDir(), "agents", agentId, "unblock-memory");
|
|
32
|
+
return { agentId, databasePath: join(resolveAgentDir(cfg, agentId), "openclaw-agent.sqlite"),
|
|
33
|
+
stateDir, storePath: join(stateDir, "training.sqlite") };
|
|
34
|
+
};
|
|
35
|
+
api.registerCli(({ program, config: cfg }) => {
|
|
36
|
+
const root = program.command("memory-training").description("Operator-only resumable LFM query-training pipeline");
|
|
37
|
+
const withStore = async (agent, options, fn) => {
|
|
38
|
+
const source = paths(cfg, agent);
|
|
39
|
+
if (!options.create && !existsSync(source.storePath))
|
|
40
|
+
throw new Error("No training database; run memory-training collect first");
|
|
41
|
+
const store = new TrainingStore(source.storePath, source.agentId);
|
|
42
|
+
try {
|
|
43
|
+
return await (options.readOnly ? fn(store, source) : store.locked(() => fn(store, source)));
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
store.close();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
root.command("collect").option("--agent <id>", "Agent id", "main")
|
|
50
|
+
.option("--since <date>", "Inclusive user-message date YYYY-MM-DD UTC")
|
|
51
|
+
.option("--until <date>", "Exclusive user-message date YYYY-MM-DD UTC")
|
|
52
|
+
.option("--dry-run", "Coverage only; no database writes or API calls")
|
|
53
|
+
.action(async (opts) => {
|
|
54
|
+
const since = dateOption(opts.since, 0), until = dateOption(opts.until, Number.MAX_SAFE_INTEGER);
|
|
55
|
+
if (until <= since)
|
|
56
|
+
throw new Error("until must be later than since");
|
|
57
|
+
const result = opts.dryRun ? collectTraining(paths(cfg, opts.agent), undefined, { since, until }) :
|
|
58
|
+
await withStore(opts.agent, { create: true }, (store, source) => collectTraining(source, store, { since, until }));
|
|
59
|
+
console.log(JSON.stringify(result, null, 2));
|
|
60
|
+
});
|
|
61
|
+
root.command("run").description("Judge whether historical recall would help each collected input").option("--agent <id>", "Agent id", "main")
|
|
62
|
+
.option("--max-examples <n>", "Optional maximum new API calls; no example-count limit by default")
|
|
63
|
+
.option("--max-input-bytes <n>", "Maximum total serialized state + question bytes, not tokens", "3000000")
|
|
64
|
+
.option("--concurrency <n>", "Concurrent TypeSafe recall requests", "256")
|
|
65
|
+
.option("--dry-run", "Refresh local checkpoints and preview pending work without API calls")
|
|
66
|
+
.action(async (opts) => {
|
|
67
|
+
const result = await withStore(opts.agent, {}, (store, source) => runTraining(source, store, config, {
|
|
68
|
+
maxExamples: opts.maxExamples === undefined ? undefined : Number(opts.maxExamples),
|
|
69
|
+
maxInputBytes: Number(opts.maxInputBytes), concurrency: Number(opts.concurrency), dryRun: opts.dryRun,
|
|
70
|
+
}));
|
|
71
|
+
console.log(JSON.stringify(result, null, 2));
|
|
72
|
+
if (result.failed || result.ambiguous)
|
|
73
|
+
process.exitCode = 1;
|
|
74
|
+
});
|
|
75
|
+
for (const command of ["generate", "evaluate"]) {
|
|
76
|
+
const stage = root.command(command).description(command === "generate" ? "Generate ten exact queries with isolated gpt-6-luna" : "Rank queries using existing historical QMD results")
|
|
77
|
+
.option("--agent <id>", "Agent id", "main")
|
|
78
|
+
.option("--threshold <p>", "Minimum completed recall probability", String(TRAINING_GATE_THRESHOLD))
|
|
79
|
+
.option("--max-examples <n>", "Optional maximum uncached examples")
|
|
80
|
+
.option("--dry-run", "Refresh local sources and preview work without provider calls");
|
|
81
|
+
if (command === "generate")
|
|
82
|
+
stage.option("--max-input-bytes <n>", "Teacher input byte budget", "3000000")
|
|
83
|
+
.option("--concurrency <n>", "Concurrent isolated teacher completions", "8");
|
|
84
|
+
else
|
|
85
|
+
stage.option("--max-calls <n>", "Maximum new retrieval operations plus uncached passage judgments")
|
|
86
|
+
.option("--exclude-judgment <hash...>", "Explicitly exclude exact judgment hashes; persist exclusions, never score as zero")
|
|
87
|
+
.option("--concurrency <n>", "Concurrent historical inputs; remote passage judgments run concurrently", String(TRAINING_EVALUATION_CONCURRENCY));
|
|
88
|
+
stage.action(async (opts) => {
|
|
89
|
+
const options = { maxExamples: opts.maxExamples === undefined ? undefined : Number(opts.maxExamples),
|
|
90
|
+
dryRun: opts.dryRun, threshold: thresholdOption(opts.threshold) };
|
|
91
|
+
const result = await withStore(opts.agent, {}, async (store, source) => command === "generate"
|
|
92
|
+
? await generateTrainingQueries(source, store, api.runtime, { ...options, maxInputBytes: Number(opts.maxInputBytes), concurrency: Number(opts.concurrency) })
|
|
93
|
+
: await evaluateTrainingQueries(source, store, config, { ...options, maxCalls: opts.maxCalls === undefined ? undefined : Number(opts.maxCalls),
|
|
94
|
+
concurrency: Number(opts.concurrency), excludeJudgments: opts.excludeJudgment }));
|
|
95
|
+
console.log(JSON.stringify(result, null, 2));
|
|
96
|
+
if (result.failed || result.ambiguous || result.blocked)
|
|
97
|
+
process.exitCode = 1;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
root.command("status").option("--agent <id>", "Agent id", "main")
|
|
101
|
+
.option("--threshold <p>", "Minimum completed recall probability for query eligibility", String(TRAINING_GATE_THRESHOLD))
|
|
102
|
+
.action(async (opts) => {
|
|
103
|
+
const threshold = thresholdOption(opts.threshold);
|
|
104
|
+
console.log(JSON.stringify(await withStore(opts.agent, { readOnly: true }, store => store.status(threshold)), null, 2));
|
|
105
|
+
});
|
|
106
|
+
root.command("retry-failed").option("--agent <id>", "Agent id", "main")
|
|
107
|
+
.option("--include-ambiguous", "Explicitly permit retrying requests that may already have been billed")
|
|
108
|
+
.action(async (opts) => {
|
|
109
|
+
const reset = await withStore(opts.agent, {}, store => store.retry(opts.includeAmbiguous === true));
|
|
110
|
+
console.log(JSON.stringify({ reset, calls: 0 }));
|
|
111
|
+
});
|
|
112
|
+
root.command("export").option("--agent <id>", "Agent id", "main")
|
|
113
|
+
.option("--threshold <p>", "Minimum completed recall probability", String(TRAINING_GATE_THRESHOLD))
|
|
114
|
+
.option("--stage <stage>", "query-training or recall-gate", "query-training")
|
|
115
|
+
.requiredOption("--output <path>", "New private JSONL file; refuses overwrite")
|
|
116
|
+
.action(async (opts) => {
|
|
117
|
+
if (!["recall-gate", "query-training"].includes(opts.stage))
|
|
118
|
+
throw new Error("Unknown training export stage");
|
|
119
|
+
const output = resolve(opts.output);
|
|
120
|
+
const threshold = thresholdOption(opts.threshold);
|
|
121
|
+
const result = await withStore(opts.agent, {}, (store, source) => {
|
|
122
|
+
collectTraining(source, store, { existingOnly: true });
|
|
123
|
+
const fd = openSync(output, "wx", 0o600);
|
|
124
|
+
let rows = 0;
|
|
125
|
+
const data = opts.stage === "query-training" ? exportQueryTraining(store, threshold) : store.exportRows(threshold);
|
|
126
|
+
try {
|
|
127
|
+
for (const row of data) {
|
|
128
|
+
writeSync(fd, JSON.stringify(row) + "\n");
|
|
129
|
+
rows++;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
closeSync(fd);
|
|
134
|
+
}
|
|
135
|
+
return { output, rows, stage: opts.stage };
|
|
136
|
+
});
|
|
137
|
+
console.log(JSON.stringify(result, null, 2));
|
|
138
|
+
});
|
|
139
|
+
}, { descriptors: [{ name: "memory-training", description: "Collect, generate and rank resumable LFM query examples", hasSubcommands: true }] });
|
|
140
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { type TrainingExample, type TrainingInput } from "./training-input.js";
|
|
2
|
+
import { type judgeTrainingInput } from "./training-gate.js";
|
|
3
|
+
import type { TeacherResult } from "./training-models.js";
|
|
4
|
+
import type { TrainingHit } from "./training-retrieval.js";
|
|
5
|
+
import type { parseContextJudgment } from "./training-judge.js";
|
|
6
|
+
type GateResult = Awaited<ReturnType<typeof judgeTrainingInput>>;
|
|
7
|
+
type Job = {
|
|
8
|
+
id: string;
|
|
9
|
+
inputHash: string;
|
|
10
|
+
inputJson: string;
|
|
11
|
+
};
|
|
12
|
+
export type TrainingSourceExample = {
|
|
13
|
+
id: string;
|
|
14
|
+
inputHash: string;
|
|
15
|
+
inputJson: string;
|
|
16
|
+
sessionId: string;
|
|
17
|
+
timestamp: number;
|
|
18
|
+
};
|
|
19
|
+
export type QueryEvaluation = {
|
|
20
|
+
query: string;
|
|
21
|
+
retrievalId: string;
|
|
22
|
+
score: number;
|
|
23
|
+
judgments?: {
|
|
24
|
+
id: string;
|
|
25
|
+
excluded: boolean;
|
|
26
|
+
}[];
|
|
27
|
+
};
|
|
28
|
+
type TrainingEvaluation = {
|
|
29
|
+
sourceId: string;
|
|
30
|
+
inputHash: string;
|
|
31
|
+
timestamp: number;
|
|
32
|
+
corpusHash: string;
|
|
33
|
+
teacherId: string;
|
|
34
|
+
corpusReport: {
|
|
35
|
+
sessions: number;
|
|
36
|
+
chunks: number;
|
|
37
|
+
excluded: number;
|
|
38
|
+
truncated: number;
|
|
39
|
+
excludedChunks: number;
|
|
40
|
+
};
|
|
41
|
+
queries: QueryEvaluation[];
|
|
42
|
+
selected: string[];
|
|
43
|
+
};
|
|
44
|
+
export type TrainingStepResults = {
|
|
45
|
+
generate: TeacherResult;
|
|
46
|
+
retrieve: {
|
|
47
|
+
query: string;
|
|
48
|
+
maxDate: string;
|
|
49
|
+
corpusHash: string;
|
|
50
|
+
hits: TrainingHit[];
|
|
51
|
+
};
|
|
52
|
+
judge: ReturnType<typeof parseContextJudgment> | {
|
|
53
|
+
excluded: true;
|
|
54
|
+
reason: "operator-exclusion";
|
|
55
|
+
};
|
|
56
|
+
evaluate: TrainingEvaluation;
|
|
57
|
+
};
|
|
58
|
+
type StepStatus = "pending" | "attempted" | "complete" | "failed" | "ambiguous";
|
|
59
|
+
export declare class TrainingStore {
|
|
60
|
+
#private;
|
|
61
|
+
constructor(path: string, agentId: string);
|
|
62
|
+
locked<T>(fn: () => T | Promise<T>): Promise<T>;
|
|
63
|
+
renew(): void;
|
|
64
|
+
sessions(): string[];
|
|
65
|
+
syncSession(sessionId: string, examples: TrainingExample[], since: number, until: number, existingOnly: boolean): {
|
|
66
|
+
added: number;
|
|
67
|
+
changed: number;
|
|
68
|
+
unchanged: number;
|
|
69
|
+
retired: number;
|
|
70
|
+
};
|
|
71
|
+
unavailable(sessionId: string): void;
|
|
72
|
+
pending(limit?: number): Job[];
|
|
73
|
+
start(id: string): number;
|
|
74
|
+
finish(id: string, attempt: number, result: GateResult | {
|
|
75
|
+
status: "failed" | "ambiguous";
|
|
76
|
+
error: string;
|
|
77
|
+
}): void;
|
|
78
|
+
retry(includeAmbiguous: boolean): number;
|
|
79
|
+
activeExamples(): TrainingSourceExample[];
|
|
80
|
+
queryExamples(threshold?: number): {
|
|
81
|
+
recallProbability: number;
|
|
82
|
+
id: string;
|
|
83
|
+
inputHash: string;
|
|
84
|
+
inputJson: string;
|
|
85
|
+
sessionId: string;
|
|
86
|
+
timestamp: number;
|
|
87
|
+
}[];
|
|
88
|
+
step<S extends keyof TrainingStepResults>(stage: S, request: unknown): {
|
|
89
|
+
id: string;
|
|
90
|
+
stage: S;
|
|
91
|
+
request: unknown;
|
|
92
|
+
status: StepStatus;
|
|
93
|
+
result: TrainingStepResults[S] | undefined;
|
|
94
|
+
};
|
|
95
|
+
judgmentExcluded(identity: string): boolean;
|
|
96
|
+
startStep(stage: keyof TrainingStepResults, id: string, request: unknown): number;
|
|
97
|
+
finishStep<S extends keyof TrainingStepResults>(stage: S, id: string, attempt: number, outcome: {
|
|
98
|
+
result: TrainingStepResults[S];
|
|
99
|
+
} | {
|
|
100
|
+
status: "failed" | "ambiguous";
|
|
101
|
+
error: string;
|
|
102
|
+
}): void;
|
|
103
|
+
completedEvaluations(versions?: {
|
|
104
|
+
selection: string;
|
|
105
|
+
retrieval: string;
|
|
106
|
+
}): TrainingEvaluation[];
|
|
107
|
+
sourceDetails(id: string): {
|
|
108
|
+
nodeId: string;
|
|
109
|
+
agentId: string;
|
|
110
|
+
sourceId: string;
|
|
111
|
+
};
|
|
112
|
+
stepRecord(id: string): {
|
|
113
|
+
id: string;
|
|
114
|
+
stage: import("node:sqlite").SQLOutputValue;
|
|
115
|
+
request: unknown;
|
|
116
|
+
result: unknown;
|
|
117
|
+
completedAt: import("node:sqlite").SQLOutputValue;
|
|
118
|
+
};
|
|
119
|
+
status(threshold: number): {
|
|
120
|
+
nodeId: string;
|
|
121
|
+
agentId: string;
|
|
122
|
+
preparation: string;
|
|
123
|
+
promptVersion: string;
|
|
124
|
+
requestedModel: string;
|
|
125
|
+
threshold: number;
|
|
126
|
+
examples: Record<string, import("node:sqlite").SQLOutputValue>[];
|
|
127
|
+
collectedInputs: number;
|
|
128
|
+
queryInputs: number;
|
|
129
|
+
stages: Record<string, import("node:sqlite").SQLOutputValue>[];
|
|
130
|
+
complete: number;
|
|
131
|
+
positive: number;
|
|
132
|
+
inputTokens: number;
|
|
133
|
+
outputTokens: number;
|
|
134
|
+
negative: number;
|
|
135
|
+
queryStages: Record<string, import("node:sqlite").SQLOutputValue>[];
|
|
136
|
+
queryAttempts: Record<string, import("node:sqlite").SQLOutputValue>[];
|
|
137
|
+
attempts: Record<string, import("node:sqlite").SQLOutputValue>[];
|
|
138
|
+
};
|
|
139
|
+
exportRows(threshold: number): Generator<{
|
|
140
|
+
stage: string;
|
|
141
|
+
inputHash: import("node:sqlite").SQLOutputValue;
|
|
142
|
+
preparation: string;
|
|
143
|
+
input: TrainingInput;
|
|
144
|
+
recallProbability: import("node:sqlite").SQLOutputValue;
|
|
145
|
+
recallNeeded: boolean;
|
|
146
|
+
threshold: number;
|
|
147
|
+
model: import("node:sqlite").SQLOutputValue;
|
|
148
|
+
promptVersion: import("node:sqlite").SQLOutputValue;
|
|
149
|
+
usage: {
|
|
150
|
+
input_tokens: import("node:sqlite").SQLOutputValue;
|
|
151
|
+
output_tokens: import("node:sqlite").SQLOutputValue;
|
|
152
|
+
};
|
|
153
|
+
sources: {
|
|
154
|
+
nodeId: string;
|
|
155
|
+
agentId: string;
|
|
156
|
+
}[];
|
|
157
|
+
}, void, unknown>;
|
|
158
|
+
close(): void;
|
|
159
|
+
}
|
|
160
|
+
export {};
|