@memtensor/memos-local-openclaw-plugin 0.3.20 → 1.0.1
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 +239 -22
- package/dist/capture/index.d.ts +1 -1
- package/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +33 -8
- package/dist/capture/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/ingest/providers/anthropic.d.ts.map +1 -1
- package/dist/ingest/providers/anthropic.js +22 -8
- package/dist/ingest/providers/anthropic.js.map +1 -1
- package/dist/ingest/providers/bedrock.d.ts.map +1 -1
- package/dist/ingest/providers/bedrock.js +22 -8
- package/dist/ingest/providers/bedrock.js.map +1 -1
- package/dist/ingest/providers/gemini.d.ts.map +1 -1
- package/dist/ingest/providers/gemini.js +22 -8
- package/dist/ingest/providers/gemini.js.map +1 -1
- package/dist/ingest/providers/index.d.ts +13 -18
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +213 -139
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/ingest/providers/openai.d.ts +1 -1
- package/dist/ingest/providers/openai.d.ts.map +1 -1
- package/dist/ingest/providers/openai.js +37 -17
- package/dist/ingest/providers/openai.js.map +1 -1
- package/dist/ingest/task-processor.d.ts +28 -3
- package/dist/ingest/task-processor.d.ts.map +1 -1
- package/dist/ingest/task-processor.js +166 -67
- package/dist/ingest/task-processor.js.map +1 -1
- package/dist/ingest/worker.d.ts.map +1 -1
- package/dist/ingest/worker.js +97 -75
- package/dist/ingest/worker.js.map +1 -1
- package/dist/shared/llm-call.d.ts +26 -0
- package/dist/shared/llm-call.d.ts.map +1 -0
- package/dist/shared/llm-call.js +163 -0
- package/dist/shared/llm-call.js.map +1 -0
- package/dist/skill/evaluator.d.ts +0 -3
- package/dist/skill/evaluator.d.ts.map +1 -1
- package/dist/skill/evaluator.js +34 -59
- package/dist/skill/evaluator.js.map +1 -1
- package/dist/skill/evolver.d.ts +22 -1
- package/dist/skill/evolver.d.ts.map +1 -1
- package/dist/skill/evolver.js +191 -32
- package/dist/skill/evolver.js.map +1 -1
- package/dist/skill/generator.d.ts +0 -3
- package/dist/skill/generator.d.ts.map +1 -1
- package/dist/skill/generator.js +15 -50
- package/dist/skill/generator.js.map +1 -1
- package/dist/skill/upgrader.d.ts +0 -2
- package/dist/skill/upgrader.d.ts.map +1 -1
- package/dist/skill/upgrader.js +4 -39
- package/dist/skill/upgrader.js.map +1 -1
- package/dist/skill/validator.d.ts +0 -2
- package/dist/skill/validator.d.ts.map +1 -1
- package/dist/skill/validator.js +14 -44
- package/dist/skill/validator.js.map +1 -1
- package/dist/storage/sqlite.d.ts +13 -2
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +92 -15
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/tools/memory-get.d.ts.map +1 -1
- package/dist/tools/memory-get.js +5 -1
- package/dist/tools/memory-get.js.map +1 -1
- package/dist/tools/memory-search.d.ts.map +1 -1
- package/dist/tools/memory-search.js +5 -0
- package/dist/tools/memory-search.js.map +1 -1
- package/dist/tools/memory-timeline.d.ts.map +1 -1
- package/dist/tools/memory-timeline.js +11 -2
- package/dist/tools/memory-timeline.js.map +1 -1
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/dist/viewer/html.d.ts +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +380 -26
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +9 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +549 -184
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +9 -3
- package/package.json +2 -1
- package/src/capture/index.ts +39 -10
- package/src/index.ts +3 -2
- package/src/ingest/providers/anthropic.ts +22 -8
- package/src/ingest/providers/bedrock.ts +22 -8
- package/src/ingest/providers/gemini.ts +22 -8
- package/src/ingest/providers/index.ts +192 -142
- package/src/ingest/providers/openai.ts +37 -17
- package/src/ingest/task-processor.ts +183 -65
- package/src/ingest/worker.ts +98 -77
- package/src/shared/llm-call.ts +144 -0
- package/src/skill/evaluator.ts +35 -64
- package/src/skill/evolver.ts +201 -33
- package/src/skill/generator.ts +16 -59
- package/src/skill/upgrader.ts +5 -43
- package/src/skill/validator.ts +15 -47
- package/src/storage/sqlite.ts +107 -15
- package/src/tools/memory-get.ts +6 -1
- package/src/tools/memory-search.ts +6 -0
- package/src/tools/memory-timeline.ts +13 -1
- package/src/types.ts +2 -1
- package/src/viewer/html.ts +380 -26
- package/src/viewer/server.ts +535 -197
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import type { SummarizerConfig, Logger, PluginContext } from "../types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Build a SummarizerConfig from OpenClaw's native model configuration (openclaw.json).
|
|
7
|
+
* Final fallback when both strongCfg and plugin summarizer fail or are absent.
|
|
8
|
+
*/
|
|
9
|
+
export function loadOpenClawFallbackConfig(log: Logger): SummarizerConfig | undefined {
|
|
10
|
+
try {
|
|
11
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
12
|
+
const cfgPath = path.join(home, ".openclaw", "openclaw.json");
|
|
13
|
+
if (!fs.existsSync(cfgPath)) return undefined;
|
|
14
|
+
|
|
15
|
+
const raw = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
|
|
16
|
+
|
|
17
|
+
const agentModel: string | undefined = raw?.agents?.defaults?.model?.primary;
|
|
18
|
+
if (!agentModel) return undefined;
|
|
19
|
+
|
|
20
|
+
const [providerKey, modelId] = agentModel.includes("/")
|
|
21
|
+
? agentModel.split("/", 2)
|
|
22
|
+
: [undefined, agentModel];
|
|
23
|
+
|
|
24
|
+
const providerCfg = providerKey
|
|
25
|
+
? raw?.models?.providers?.[providerKey]
|
|
26
|
+
: Object.values(raw?.models?.providers ?? {})[0] as any;
|
|
27
|
+
if (!providerCfg) return undefined;
|
|
28
|
+
|
|
29
|
+
const baseUrl: string | undefined = providerCfg.baseUrl;
|
|
30
|
+
const apiKey: string | undefined = providerCfg.apiKey;
|
|
31
|
+
if (!baseUrl || !apiKey) return undefined;
|
|
32
|
+
|
|
33
|
+
const endpoint = baseUrl.endsWith("/chat/completions")
|
|
34
|
+
? baseUrl
|
|
35
|
+
: baseUrl.replace(/\/+$/, "") + "/chat/completions";
|
|
36
|
+
|
|
37
|
+
log.debug(`OpenClaw fallback model: ${modelId} via ${baseUrl}`);
|
|
38
|
+
return {
|
|
39
|
+
provider: "openai_compatible",
|
|
40
|
+
endpoint,
|
|
41
|
+
apiKey,
|
|
42
|
+
model: modelId,
|
|
43
|
+
};
|
|
44
|
+
} catch (err) {
|
|
45
|
+
log.debug(`Failed to load OpenClaw fallback config: ${err}`);
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build the ordered fallback chain for skill-related LLM calls:
|
|
52
|
+
* skillEvolution.summarizer → plugin summarizer → OpenClaw native model
|
|
53
|
+
*/
|
|
54
|
+
export function buildSkillConfigChain(ctx: PluginContext): SummarizerConfig[] {
|
|
55
|
+
const chain: SummarizerConfig[] = [];
|
|
56
|
+
const skillCfg = ctx.config.skillEvolution?.summarizer;
|
|
57
|
+
const pluginCfg = ctx.config.summarizer;
|
|
58
|
+
const fallbackCfg = loadOpenClawFallbackConfig(ctx.log);
|
|
59
|
+
if (skillCfg) chain.push(skillCfg);
|
|
60
|
+
if (pluginCfg && pluginCfg !== skillCfg) chain.push(pluginCfg);
|
|
61
|
+
if (fallbackCfg) chain.push(fallbackCfg);
|
|
62
|
+
return chain;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface LLMCallOptions {
|
|
66
|
+
maxTokens?: number;
|
|
67
|
+
temperature?: number;
|
|
68
|
+
timeoutMs?: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeEndpoint(url: string): string {
|
|
72
|
+
const stripped = url.replace(/\/+$/, "");
|
|
73
|
+
if (stripped.endsWith("/chat/completions")) return stripped;
|
|
74
|
+
if (stripped.endsWith("/completions")) return stripped;
|
|
75
|
+
return `${stripped}/chat/completions`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Make a single LLM call with the given config. Throws on failure.
|
|
80
|
+
*/
|
|
81
|
+
export async function callLLMOnce(
|
|
82
|
+
cfg: SummarizerConfig,
|
|
83
|
+
prompt: string,
|
|
84
|
+
opts: LLMCallOptions = {},
|
|
85
|
+
): Promise<string> {
|
|
86
|
+
const endpoint = normalizeEndpoint(cfg.endpoint ?? "https://api.openai.com/v1/chat/completions");
|
|
87
|
+
const model = cfg.model ?? "gpt-4o-mini";
|
|
88
|
+
const headers: Record<string, string> = {
|
|
89
|
+
"Content-Type": "application/json",
|
|
90
|
+
Authorization: `Bearer ${cfg.apiKey}`,
|
|
91
|
+
...cfg.headers,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const resp = await fetch(endpoint, {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers,
|
|
97
|
+
body: JSON.stringify({
|
|
98
|
+
model,
|
|
99
|
+
temperature: opts.temperature ?? 0.1,
|
|
100
|
+
max_tokens: opts.maxTokens ?? 1024,
|
|
101
|
+
messages: [{ role: "user", content: prompt }],
|
|
102
|
+
}),
|
|
103
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? 30_000),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (!resp.ok) {
|
|
107
|
+
const body = await resp.text();
|
|
108
|
+
throw new Error(`LLM call failed (${resp.status}): ${body}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const json = (await resp.json()) as { choices: Array<{ message: { content: string } }> };
|
|
112
|
+
return json.choices[0]?.message?.content?.trim() ?? "";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Call LLM with fallback chain: tries each config in order until one succeeds.
|
|
117
|
+
* Returns the result string, or throws if ALL configs fail.
|
|
118
|
+
*/
|
|
119
|
+
export async function callLLMWithFallback(
|
|
120
|
+
chain: SummarizerConfig[],
|
|
121
|
+
prompt: string,
|
|
122
|
+
log: Logger,
|
|
123
|
+
label: string,
|
|
124
|
+
opts: LLMCallOptions = {},
|
|
125
|
+
): Promise<string> {
|
|
126
|
+
if (chain.length === 0) {
|
|
127
|
+
throw new Error(`${label}: no LLM config available`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (let i = 0; i < chain.length; i++) {
|
|
131
|
+
try {
|
|
132
|
+
return await callLLMOnce(chain[i], prompt, opts);
|
|
133
|
+
} catch (err) {
|
|
134
|
+
const modelInfo = `${chain[i].provider ?? "?"}/${chain[i].model ?? "?"}`;
|
|
135
|
+
if (i < chain.length - 1) {
|
|
136
|
+
log.warn(`${label} failed (${modelInfo}), trying next fallback: ${err}`);
|
|
137
|
+
} else {
|
|
138
|
+
log.error(`${label} failed (${modelInfo}), no more fallbacks: ${err}`);
|
|
139
|
+
throw err;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
throw new Error(`${label}: all models failed`);
|
|
144
|
+
}
|
package/src/skill/evaluator.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { Chunk, Task, Skill, PluginContext
|
|
1
|
+
import type { Chunk, Task, Skill, PluginContext } from "../types";
|
|
2
2
|
import { DEFAULTS } from "../types";
|
|
3
|
+
import { buildSkillConfigChain, callLLMWithFallback } from "../shared/llm-call";
|
|
3
4
|
|
|
4
5
|
export interface CreateEvalResult {
|
|
5
6
|
shouldGenerate: boolean;
|
|
@@ -18,34 +19,45 @@ export interface UpgradeEvalResult {
|
|
|
18
19
|
confidence: number;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
const CREATE_EVAL_PROMPT = `You are
|
|
22
|
+
const CREATE_EVAL_PROMPT = `You are a strict experience evaluation expert. Based on the completed task record below, decide whether this task contains **reusable, transferable** experience worth distilling into a "skill".
|
|
22
23
|
|
|
23
|
-
A skill is a reusable guide that helps an AI agent handle
|
|
24
|
+
A skill is a reusable guide that helps an AI agent handle **the same type of task** better in the future. The key question is: "Will someone likely need to do this exact type of thing again?"
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
STRICT criteria — must meet ALL of:
|
|
27
|
+
1. **Repeatable**: The task type is likely to recur (not a one-off personal conversation)
|
|
28
|
+
2. **Transferable**: The approach/solution would help others facing the same problem
|
|
29
|
+
3. **Technical depth**: Contains non-trivial steps, commands, code, configs, or diagnostic reasoning
|
|
30
|
+
|
|
31
|
+
Worth distilling (must meet criteria above AND at least ONE below):
|
|
32
|
+
- Solves a recurring technical problem with a specific approach/workflow
|
|
33
|
+
- Went through trial-and-error (wrong approach then corrected) — the learning is valuable
|
|
29
34
|
- Involves non-obvious usage of specific tools, APIs, or frameworks
|
|
30
35
|
- Contains debugging/troubleshooting with diagnostic reasoning
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
- Contains a process that required specific parameter tuning or configuration
|
|
36
|
+
- Shows how to combine multiple tools/services to accomplish a technical goal
|
|
37
|
+
- Contains deployment, configuration, or infrastructure setup steps
|
|
38
|
+
- Demonstrates a reusable data processing or automation pipeline
|
|
35
39
|
|
|
36
|
-
NOT worth distilling:
|
|
40
|
+
NOT worth distilling (if ANY matches, return shouldGenerate=false):
|
|
37
41
|
- Pure factual Q&A with no process ("what is TCP", "what's the capital of France")
|
|
38
42
|
- Single-turn simple answers with no workflow
|
|
39
43
|
- Conversation too fragmented or incoherent to extract a clear process
|
|
44
|
+
- One-off personal tasks: identity confirmation, preference setting, self-introduction
|
|
45
|
+
- Casual chat, opinion discussion, news commentary, brainstorming without actionable output
|
|
46
|
+
- Simple information lookup or summarization (e.g. "summarize this article", "explain X concept")
|
|
47
|
+
- Organizing/listing personal information (work history, resume, contacts)
|
|
48
|
+
- Generic product/system overviews without specific operational steps
|
|
49
|
+
- Tasks where the "steps" are just the AI answering questions (no real workflow)
|
|
40
50
|
|
|
41
51
|
Task title: {TITLE}
|
|
42
52
|
Task summary:
|
|
43
53
|
{SUMMARY}
|
|
44
54
|
|
|
55
|
+
LANGUAGE RULE: The "reason" field MUST use the SAME language as the task title/summary. Chinese input → Chinese reason. English input → English reason. "suggestedName" stays in English kebab-case.
|
|
56
|
+
|
|
45
57
|
Reply in JSON only, no extra text:
|
|
46
58
|
{
|
|
47
59
|
"shouldGenerate": boolean,
|
|
48
|
-
"reason": "brief explanation",
|
|
60
|
+
"reason": "brief explanation (same language as input)",
|
|
49
61
|
"suggestedName": "kebab-case-name",
|
|
50
62
|
"suggestedTags": ["tag1", "tag2"],
|
|
51
63
|
"confidence": 0.0-1.0
|
|
@@ -80,13 +92,15 @@ NOT worth upgrading:
|
|
|
80
92
|
- New task's approach is worse than existing skill
|
|
81
93
|
- Differences are trivial
|
|
82
94
|
|
|
95
|
+
LANGUAGE RULE: "reason" and "mergeStrategy" MUST use the SAME language as the task title/summary. Chinese input → Chinese output. English input → English output.
|
|
96
|
+
|
|
83
97
|
Reply in JSON only, no extra text:
|
|
84
98
|
{
|
|
85
99
|
"shouldUpgrade": boolean,
|
|
86
100
|
"upgradeType": "refine" | "extend" | "fix",
|
|
87
101
|
"dimensions": ["faster", "more_elegant", "more_convenient", "fewer_tokens", "more_accurate", "more_robust", "new_scenario", "fix_outdated"],
|
|
88
|
-
"reason": "what new value the task brings",
|
|
89
|
-
"mergeStrategy": "which specific parts need updating",
|
|
102
|
+
"reason": "what new value the task brings (same language as input)",
|
|
103
|
+
"mergeStrategy": "which specific parts need updating (same language as input)",
|
|
90
104
|
"confidence": 0.0-1.0
|
|
91
105
|
}`;
|
|
92
106
|
|
|
@@ -121,8 +135,8 @@ export class SkillEvaluator {
|
|
|
121
135
|
}
|
|
122
136
|
|
|
123
137
|
async evaluateCreate(task: Task): Promise<CreateEvalResult> {
|
|
124
|
-
const
|
|
125
|
-
if (
|
|
138
|
+
const chain = buildSkillConfigChain(this.ctx);
|
|
139
|
+
if (chain.length === 0) {
|
|
126
140
|
return { shouldGenerate: false, reason: "no LLM configured", suggestedName: "", suggestedTags: [], confidence: 0 };
|
|
127
141
|
}
|
|
128
142
|
|
|
@@ -131,7 +145,7 @@ export class SkillEvaluator {
|
|
|
131
145
|
.replace("{SUMMARY}", task.summary.slice(0, 3000));
|
|
132
146
|
|
|
133
147
|
try {
|
|
134
|
-
const raw = await this.
|
|
148
|
+
const raw = await callLLMWithFallback(chain, prompt, this.ctx.log, "SkillEvaluator.create");
|
|
135
149
|
return this.parseJSON<CreateEvalResult>(raw, {
|
|
136
150
|
shouldGenerate: false, reason: "parse failed", suggestedName: "", suggestedTags: [], confidence: 0,
|
|
137
151
|
});
|
|
@@ -142,8 +156,8 @@ export class SkillEvaluator {
|
|
|
142
156
|
}
|
|
143
157
|
|
|
144
158
|
async evaluateUpgrade(task: Task, skill: Skill, skillContent: string): Promise<UpgradeEvalResult> {
|
|
145
|
-
const
|
|
146
|
-
if (
|
|
159
|
+
const chain = buildSkillConfigChain(this.ctx);
|
|
160
|
+
if (chain.length === 0) {
|
|
147
161
|
return { shouldUpgrade: false, upgradeType: "refine", dimensions: [], reason: "no LLM configured", mergeStrategy: "", confidence: 0 };
|
|
148
162
|
}
|
|
149
163
|
|
|
@@ -155,7 +169,7 @@ export class SkillEvaluator {
|
|
|
155
169
|
.replace("{SUMMARY}", task.summary.slice(0, 3000));
|
|
156
170
|
|
|
157
171
|
try {
|
|
158
|
-
const raw = await this.
|
|
172
|
+
const raw = await callLLMWithFallback(chain, prompt, this.ctx.log, "SkillEvaluator.upgrade");
|
|
159
173
|
return this.parseJSON<UpgradeEvalResult>(raw, {
|
|
160
174
|
shouldUpgrade: false, upgradeType: "refine", dimensions: [], reason: "parse failed", mergeStrategy: "", confidence: 0,
|
|
161
175
|
});
|
|
@@ -165,42 +179,6 @@ export class SkillEvaluator {
|
|
|
165
179
|
}
|
|
166
180
|
}
|
|
167
181
|
|
|
168
|
-
private getProviderConfig(): SummarizerConfig | undefined {
|
|
169
|
-
return this.ctx.config.summarizer;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
private async callLLM(cfg: SummarizerConfig, userContent: string): Promise<string> {
|
|
173
|
-
const endpoint = this.normalizeEndpoint(cfg.endpoint ?? "https://api.openai.com/v1/chat/completions");
|
|
174
|
-
const model = cfg.model ?? "gpt-4o-mini";
|
|
175
|
-
const headers: Record<string, string> = {
|
|
176
|
-
"Content-Type": "application/json",
|
|
177
|
-
Authorization: `Bearer ${cfg.apiKey}`,
|
|
178
|
-
...cfg.headers,
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
const resp = await fetch(endpoint, {
|
|
182
|
-
method: "POST",
|
|
183
|
-
headers,
|
|
184
|
-
body: JSON.stringify({
|
|
185
|
-
model,
|
|
186
|
-
temperature: cfg.temperature ?? 0.1,
|
|
187
|
-
max_tokens: 1024,
|
|
188
|
-
messages: [
|
|
189
|
-
{ role: "user", content: userContent },
|
|
190
|
-
],
|
|
191
|
-
}),
|
|
192
|
-
signal: AbortSignal.timeout(cfg.timeoutMs ?? 30_000),
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
if (!resp.ok) {
|
|
196
|
-
const body = await resp.text();
|
|
197
|
-
throw new Error(`LLM call failed (${resp.status}): ${body}`);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const json = (await resp.json()) as { choices: Array<{ message: { content: string } }> };
|
|
201
|
-
return json.choices[0]?.message?.content?.trim() ?? "";
|
|
202
|
-
}
|
|
203
|
-
|
|
204
182
|
private parseJSON<T>(raw: string, fallback: T): T {
|
|
205
183
|
const jsonMatch = raw.match(/\{[\s\S]*\}/);
|
|
206
184
|
if (!jsonMatch) return fallback;
|
|
@@ -210,11 +188,4 @@ export class SkillEvaluator {
|
|
|
210
188
|
return fallback;
|
|
211
189
|
}
|
|
212
190
|
}
|
|
213
|
-
|
|
214
|
-
private normalizeEndpoint(url: string): string {
|
|
215
|
-
const stripped = url.replace(/\/+$/, "");
|
|
216
|
-
if (stripped.endsWith("/chat/completions")) return stripped;
|
|
217
|
-
if (stripped.endsWith("/completions")) return stripped;
|
|
218
|
-
return `${stripped}/chat/completions`;
|
|
219
|
-
}
|
|
220
191
|
}
|
package/src/skill/evolver.ts
CHANGED
|
@@ -3,12 +3,14 @@ import * as path from "path";
|
|
|
3
3
|
import type { SqliteStore } from "../storage/sqlite";
|
|
4
4
|
import type { RecallEngine } from "../recall/engine";
|
|
5
5
|
import type { Embedder } from "../embedding";
|
|
6
|
+
import { cosineSimilarity } from "../storage/vector";
|
|
6
7
|
import type { Task, Skill, Chunk, PluginContext } from "../types";
|
|
7
8
|
import { DEFAULTS } from "../types";
|
|
8
9
|
import { SkillEvaluator } from "./evaluator";
|
|
9
10
|
import { SkillGenerator } from "./generator";
|
|
10
11
|
import { SkillUpgrader } from "./upgrader";
|
|
11
12
|
import { SkillInstaller } from "./installer";
|
|
13
|
+
import { buildSkillConfigChain, callLLMWithFallback } from "../shared/llm-call";
|
|
12
14
|
|
|
13
15
|
export class SkillEvolver {
|
|
14
16
|
private evaluator: SkillEvaluator;
|
|
@@ -16,12 +18,13 @@ export class SkillEvolver {
|
|
|
16
18
|
private upgrader: SkillUpgrader;
|
|
17
19
|
private installer: SkillInstaller;
|
|
18
20
|
private processing = false;
|
|
21
|
+
private queue: Task[] = [];
|
|
19
22
|
|
|
20
23
|
constructor(
|
|
21
24
|
private store: SqliteStore,
|
|
22
25
|
private engine: RecallEngine,
|
|
23
26
|
private ctx: PluginContext,
|
|
24
|
-
embedder?: Embedder,
|
|
27
|
+
private embedder?: Embedder,
|
|
25
28
|
) {
|
|
26
29
|
this.evaluator = new SkillEvaluator(ctx);
|
|
27
30
|
this.generator = new SkillGenerator(store, engine, ctx, embedder);
|
|
@@ -29,25 +32,57 @@ export class SkillEvolver {
|
|
|
29
32
|
this.installer = new SkillInstaller(store, ctx);
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
async recoverOrphanedTasks(): Promise<number> {
|
|
36
|
+
const orphaned = this.store.getTasksBySkillStatus(["queued", "generating"]);
|
|
37
|
+
if (orphaned.length === 0) return 0;
|
|
38
|
+
|
|
39
|
+
this.ctx.log.info(`SkillEvolver: recovering ${orphaned.length} orphaned tasks (queued/generating from previous run)`);
|
|
40
|
+
for (const task of orphaned) {
|
|
41
|
+
try {
|
|
42
|
+
await this.processOne(task);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
this.ctx.log.error(`SkillEvolver: recovery failed for task ${task.id}: ${err}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return orphaned.length;
|
|
48
|
+
}
|
|
49
|
+
|
|
32
50
|
async onTaskCompleted(task: Task): Promise<void> {
|
|
33
51
|
const enabled = this.ctx.config.skillEvolution?.enabled ?? DEFAULTS.skillEvolutionEnabled;
|
|
34
52
|
const autoEval = this.ctx.config.skillEvolution?.autoEvaluate ?? DEFAULTS.skillAutoEvaluate;
|
|
35
53
|
if (!enabled || !autoEval) return;
|
|
36
54
|
|
|
37
55
|
if (this.processing) {
|
|
38
|
-
this.ctx.log.debug(
|
|
56
|
+
this.ctx.log.debug(`SkillEvolver: busy, queuing task ${task.id} (queue=${this.queue.length})`);
|
|
57
|
+
this.store.setTaskSkillMeta(task.id, { skillStatus: "queued", skillReason: `排队中,前方还有 ${this.queue.length + 1} 个任务` });
|
|
58
|
+
this.queue.push(task);
|
|
39
59
|
return;
|
|
40
60
|
}
|
|
61
|
+
await this.drain(task);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private async drain(task: Task): Promise<void> {
|
|
41
65
|
this.processing = true;
|
|
42
66
|
try {
|
|
43
|
-
await this.
|
|
44
|
-
|
|
45
|
-
|
|
67
|
+
await this.processOne(task);
|
|
68
|
+
while (this.queue.length > 0) {
|
|
69
|
+
const next = this.queue.shift()!;
|
|
70
|
+
await this.processOne(next);
|
|
71
|
+
}
|
|
46
72
|
} finally {
|
|
47
73
|
this.processing = false;
|
|
48
74
|
}
|
|
49
75
|
}
|
|
50
76
|
|
|
77
|
+
private async processOne(task: Task): Promise<void> {
|
|
78
|
+
try {
|
|
79
|
+
await this.process(task);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
this.ctx.log.error(`SkillEvolver error for task ${task.id}: ${err}`);
|
|
82
|
+
this.store.setTaskSkillMeta(task.id, { skillStatus: "skipped", skillReason: `Error: ${err}` });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
51
86
|
private async process(task: Task): Promise<void> {
|
|
52
87
|
const chunks = this.store.getChunksByTask(task.id);
|
|
53
88
|
|
|
@@ -67,63 +102,196 @@ export class SkillEvolver {
|
|
|
67
102
|
}
|
|
68
103
|
}
|
|
69
104
|
|
|
105
|
+
/** Max candidates to send to LLM for relevance judgment. */
|
|
106
|
+
private static readonly RELATED_SKILL_CANDIDATE_TOP = 10;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Search for an existing skill that is HIGHLY related to the given task.
|
|
110
|
+
*
|
|
111
|
+
* 1. Collect top 50 skill candidates by FTS + vector similarity (relaxed thresholds).
|
|
112
|
+
* 2. Call LLM with task title/summary and each skill's name/description; strict rule:
|
|
113
|
+
* only output ONE skill index if the task clearly belongs to that skill's domain;
|
|
114
|
+
* otherwise output 0 (do not force a match).
|
|
115
|
+
*/
|
|
70
116
|
private async findRelatedSkill(task: Task): Promise<Skill | null> {
|
|
117
|
+
const query = task.summary.slice(0, 600);
|
|
118
|
+
const owner = task.owner ?? "agent:main";
|
|
119
|
+
// Relaxed thresholds to gather a larger candidate pool; LLM will do strict filtering
|
|
120
|
+
const VEC_FLOOR = 0.35;
|
|
121
|
+
const TOP_N = SkillEvolver.RELATED_SKILL_CANDIDATE_TOP;
|
|
122
|
+
|
|
123
|
+
type Candidate = { skill: Skill; vecScore: number; ftsScore: number; combined: number };
|
|
124
|
+
const candidateMap = new Map<string, Candidate>();
|
|
125
|
+
|
|
126
|
+
// 1. FTS on skill name + description (take more candidates)
|
|
71
127
|
try {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
128
|
+
const ftsHits = this.store.skillFtsSearch(query, TOP_N, "mix", owner);
|
|
129
|
+
for (const hit of ftsHits) {
|
|
130
|
+
const skill = this.store.getSkill(hit.skillId);
|
|
131
|
+
if (skill && (skill.status === "active" || skill.status === "draft")) {
|
|
132
|
+
candidateMap.set(skill.id, { skill, vecScore: 0, ftsScore: hit.score, combined: 0 });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} catch (err) {
|
|
136
|
+
this.ctx.log.warn(`SkillEvolver: skill FTS search failed: ${err}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 2. Vector similarity: include all skills above a low floor to rank them
|
|
140
|
+
if (this.embedder) {
|
|
141
|
+
try {
|
|
142
|
+
const queryVec = await this.embedder.embedQuery(query);
|
|
143
|
+
const allSkillEmb = this.store.getSkillEmbeddings("mix", owner);
|
|
144
|
+
for (const row of allSkillEmb) {
|
|
145
|
+
const sim = cosineSimilarity(queryVec, row.vector);
|
|
146
|
+
if (sim >= VEC_FLOOR) {
|
|
147
|
+
const existing = candidateMap.get(row.skillId);
|
|
148
|
+
if (existing) {
|
|
149
|
+
existing.vecScore = sim;
|
|
150
|
+
} else {
|
|
151
|
+
const skill = this.store.getSkill(row.skillId);
|
|
152
|
+
if (skill && (skill.status === "active" || skill.status === "draft")) {
|
|
153
|
+
candidateMap.set(skill.id, { skill, vecScore: sim, ftsScore: 0, combined: 0 });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
84
156
|
}
|
|
85
157
|
}
|
|
158
|
+
} catch (err) {
|
|
159
|
+
this.ctx.log.warn(`SkillEvolver: skill vector search failed: ${err}`);
|
|
86
160
|
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (candidateMap.size === 0) return null;
|
|
164
|
+
|
|
165
|
+
for (const c of candidateMap.values()) {
|
|
166
|
+
c.combined = c.vecScore * 0.7 + c.ftsScore * 0.3;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const sorted = [...candidateMap.values()]
|
|
170
|
+
.sort((a, b) => b.combined - a.combined)
|
|
171
|
+
.slice(0, TOP_N);
|
|
172
|
+
|
|
173
|
+
if (sorted.length === 0) return null;
|
|
174
|
+
|
|
175
|
+
// 3. LLM strict relevance judgment: only one skill if HIGHLY related, else none
|
|
176
|
+
const selectedSkill = await this.judgeSkillRelatedToTask(task, sorted);
|
|
177
|
+
if (selectedSkill) {
|
|
178
|
+
this.ctx.log.debug(`SkillEvolver: LLM selected related skill "${selectedSkill.name}" for task "${task.title}"`);
|
|
179
|
+
} else {
|
|
180
|
+
this.ctx.log.debug(`SkillEvolver: LLM found no highly related skill for task "${task.title}" (${sorted.length} candidates)`);
|
|
181
|
+
}
|
|
182
|
+
return selectedSkill;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Ask LLM to pick at most ONE skill that is HIGHLY relevant to the task.
|
|
187
|
+
* Strict rule: only return a skill if the task clearly belongs to that skill's domain; otherwise return null.
|
|
188
|
+
*/
|
|
189
|
+
private async judgeSkillRelatedToTask(
|
|
190
|
+
task: Task,
|
|
191
|
+
candidates: Array<{ skill: Skill; vecScore: number; ftsScore: number; combined: number }>,
|
|
192
|
+
): Promise<Skill | null> {
|
|
193
|
+
const chain = buildSkillConfigChain(this.ctx);
|
|
194
|
+
if (chain.length === 0) {
|
|
195
|
+
this.ctx.log.warn("SkillEvolver: no LLM config available, skipping skill relevance judgment");
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const taskTitle = task.title || "(no title)";
|
|
200
|
+
const taskSummary = task.summary.slice(0, 800);
|
|
201
|
+
const skillList = candidates
|
|
202
|
+
.map((c, i) => `${i + 1}. [${c.skill.name}]\n ${(c.skill.description || "").slice(0, 300)}`)
|
|
203
|
+
.join("\n\n");
|
|
204
|
+
|
|
205
|
+
const prompt = `You are a strict judge: decide whether a completed TASK should be merged into an EXISTING SKILL. The task and the skill must be in the SAME domain/topic — e.g. same type of problem, same tool, same workflow. Loose or tangential relevance is NOT enough.
|
|
206
|
+
|
|
207
|
+
TASK TITLE: ${taskTitle}
|
|
208
|
+
|
|
209
|
+
TASK SUMMARY:
|
|
210
|
+
${taskSummary}
|
|
211
|
+
|
|
212
|
+
CANDIDATE SKILLS (index, name, description):
|
|
213
|
+
${skillList}
|
|
214
|
+
|
|
215
|
+
RULES:
|
|
216
|
+
- Output exactly ONE skill index (1 to ${candidates.length}) ONLY if the task's experience clearly belongs to that skill's domain. Same topic, same kind of work.
|
|
217
|
+
- If no skill is clearly relevant (different domain, or only loosely related), output 0. When in doubt, output 0.
|
|
218
|
+
- Do not force a match. "Movie recommendation" task must not match "Weather query" or "Legal discussion" skill even if both exist in the list.
|
|
219
|
+
|
|
220
|
+
LANGUAGE RULE: "reason" MUST use the SAME language as the task title/summary. Chinese input → Chinese reason.
|
|
221
|
+
|
|
222
|
+
Reply with JSON only, no other text:
|
|
223
|
+
{"selectedIndex": 0, "reason": "brief explanation (same language as input)"}
|
|
224
|
+
Use selectedIndex 0 when none is highly relevant.`;
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const raw = await callLLMWithFallback(chain, prompt, this.ctx.log, "SkillEvolver.judgeRelated", { temperature: 0, maxTokens: 256 });
|
|
228
|
+
const parsed = this.parseJudgeSkillResult(raw, candidates.length);
|
|
229
|
+
if (parsed.selectedIndex >= 1 && parsed.selectedIndex <= candidates.length) {
|
|
230
|
+
return candidates[parsed.selectedIndex - 1].skill;
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
87
233
|
} catch (err) {
|
|
88
|
-
this.ctx.log.warn(`SkillEvolver:
|
|
234
|
+
this.ctx.log.warn(`SkillEvolver: LLM skill relevance judgment failed: ${err}`);
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private parseJudgeSkillResult(raw: string, maxIndex: number): { selectedIndex: number; reason: string } {
|
|
240
|
+
const fallback = { selectedIndex: 0, reason: "parse failed" };
|
|
241
|
+
const match = raw.match(/\{[\s\S]*\}/);
|
|
242
|
+
if (!match) return fallback;
|
|
243
|
+
try {
|
|
244
|
+
const obj = JSON.parse(match[0]) as { selectedIndex?: number; reason?: string };
|
|
245
|
+
const idx = typeof obj.selectedIndex === "number" ? obj.selectedIndex : 0;
|
|
246
|
+
const reason = typeof obj.reason === "string" ? obj.reason : "";
|
|
247
|
+
if (idx < 0 || idx > maxIndex) return { selectedIndex: 0, reason: reason || "out of range" };
|
|
248
|
+
return { selectedIndex: idx, reason };
|
|
249
|
+
} catch {
|
|
250
|
+
return fallback;
|
|
89
251
|
}
|
|
90
|
-
return null;
|
|
91
252
|
}
|
|
92
253
|
|
|
93
254
|
private async handleExistingSkill(task: Task, chunks: Chunk[], skill: Skill): Promise<void> {
|
|
94
|
-
|
|
255
|
+
// Verify skill still exists in DB (may have been manually deleted)
|
|
256
|
+
const freshSkill = this.store.getSkill(skill.id);
|
|
257
|
+
if (!freshSkill) {
|
|
258
|
+
this.ctx.log.warn(`SkillEvolver: skill "${skill.name}" (${skill.id}) no longer exists, treating as new`);
|
|
259
|
+
await this.handleNewSkill(task, chunks);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const skillContent = this.readSkillContent(freshSkill);
|
|
95
264
|
if (!skillContent) {
|
|
96
|
-
this.ctx.log.warn(`SkillEvolver: cannot read skill "${
|
|
265
|
+
this.ctx.log.warn(`SkillEvolver: cannot read skill "${freshSkill.name}" content, treating as new`);
|
|
97
266
|
await this.handleNewSkill(task, chunks);
|
|
98
267
|
return;
|
|
99
268
|
}
|
|
100
269
|
|
|
101
270
|
const minConfidence = this.ctx.config.skillEvolution?.minConfidence ?? DEFAULTS.skillMinConfidence;
|
|
102
|
-
const evalResult = await this.evaluator.evaluateUpgrade(task,
|
|
271
|
+
const evalResult = await this.evaluator.evaluateUpgrade(task, freshSkill, skillContent);
|
|
103
272
|
|
|
104
273
|
if (evalResult.shouldUpgrade && evalResult.confidence >= minConfidence) {
|
|
105
|
-
this.ctx.log.info(`SkillEvolver: upgrading skill "${
|
|
106
|
-
const { upgraded } = await this.upgrader.upgrade(task,
|
|
274
|
+
this.ctx.log.info(`SkillEvolver: upgrading skill "${freshSkill.name}" — ${evalResult.reason}`);
|
|
275
|
+
const { upgraded } = await this.upgrader.upgrade(task, freshSkill, evalResult);
|
|
107
276
|
|
|
108
|
-
this.markChunksWithSkill(chunks,
|
|
277
|
+
this.markChunksWithSkill(chunks, freshSkill.id);
|
|
109
278
|
|
|
110
279
|
if (upgraded) {
|
|
111
|
-
this.store.linkTaskSkill(task.id,
|
|
112
|
-
this.installer.syncIfInstalled(
|
|
280
|
+
this.store.linkTaskSkill(task.id, freshSkill.id, "evolved_from", freshSkill.version + 1);
|
|
281
|
+
this.installer.syncIfInstalled(freshSkill.name);
|
|
113
282
|
} else {
|
|
114
|
-
this.store.linkTaskSkill(task.id,
|
|
283
|
+
this.store.linkTaskSkill(task.id, freshSkill.id, "applied_to", freshSkill.version);
|
|
115
284
|
}
|
|
116
285
|
} else if (evalResult.confidence < 0.3) {
|
|
117
|
-
// Low confidence means the matched skill is likely unrelated — try creating a new one
|
|
118
286
|
this.ctx.log.info(
|
|
119
|
-
`SkillEvolver: skill "${
|
|
287
|
+
`SkillEvolver: skill "${freshSkill.name}" has low relevance (confidence=${evalResult.confidence}), ` +
|
|
120
288
|
`falling back to new skill evaluation for task "${task.title}"`,
|
|
121
289
|
);
|
|
122
290
|
await this.handleNewSkill(task, chunks);
|
|
123
291
|
} else {
|
|
124
|
-
this.ctx.log.debug(`SkillEvolver: skill "${
|
|
125
|
-
this.markChunksWithSkill(chunks,
|
|
126
|
-
this.store.linkTaskSkill(task.id,
|
|
292
|
+
this.ctx.log.debug(`SkillEvolver: skill "${freshSkill.name}" not worth upgrading (confidence=${evalResult.confidence})`);
|
|
293
|
+
this.markChunksWithSkill(chunks, freshSkill.id);
|
|
294
|
+
this.store.linkTaskSkill(task.id, freshSkill.id, "applied_to", freshSkill.version);
|
|
127
295
|
}
|
|
128
296
|
}
|
|
129
297
|
|