assistme 0.2.2 → 0.2.4
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/dist/index.js +87 -356
- package/package.json +1 -1
- package/src/agent/mcp-servers.ts +53 -123
- package/src/agent/processor.test.ts +1 -4
- package/src/agent/processor.ts +6 -30
- package/src/agent/skill-extractor.ts +0 -439
- package/src/agent/skills.ts +23 -21
- package/src/agent/memory-extractor.ts +0 -128
- package/src/utils/validation.test.ts +0 -153
- package/src/utils/validation.ts +0 -101
|
@@ -1,16 +1,3 @@
|
|
|
1
|
-
import { getConfig } from "../utils/config.js";
|
|
2
|
-
import { log } from "../utils/logger.js";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Extracted skill from LLM analysis of a multi-step task.
|
|
6
|
-
*/
|
|
7
|
-
export interface ExtractedSkill {
|
|
8
|
-
name: string; // kebab-case, e.g. "price-comparison-amazon"
|
|
9
|
-
description: string; // One-line description
|
|
10
|
-
steps: string; // Markdown step-by-step instructions
|
|
11
|
-
emoji: string; // Single emoji for this skill
|
|
12
|
-
}
|
|
13
|
-
|
|
14
1
|
/**
|
|
15
2
|
* A record of a tool call made during task execution.
|
|
16
3
|
*/
|
|
@@ -19,429 +6,3 @@ export interface ToolCallRecord {
|
|
|
19
6
|
input: Record<string, unknown>;
|
|
20
7
|
result: string;
|
|
21
8
|
}
|
|
22
|
-
|
|
23
|
-
const EXTRACTION_PROMPT = `You are a skill extraction system for an AI agent that controls a real web browser (Chrome via CDP) and can read/write files. Your job is to analyze completed task conversations and extract REUSABLE WORKFLOWS as skills.
|
|
24
|
-
|
|
25
|
-
A skill is a step-by-step instruction template that teaches the agent how to handle a TYPE of task (not a specific instance).
|
|
26
|
-
|
|
27
|
-
Rules:
|
|
28
|
-
- Only extract skills for tasks that involved 3+ distinct steps (multi-step workflows)
|
|
29
|
-
- The skill must be GENERALIZABLE — it should work for similar future tasks, not just this specific one
|
|
30
|
-
- Replace specific values (URLs, product names, prices) with generic placeholders like {product}, {website}, {query}
|
|
31
|
-
- Focus on the WORKFLOW PATTERN, not the specific data
|
|
32
|
-
- Write clear markdown instructions that another AI agent could follow
|
|
33
|
-
- If the task was too simple or too specific to generalize, return null
|
|
34
|
-
- The name must be kebab-case (lowercase, hyphens)
|
|
35
|
-
- Description should be one concise line
|
|
36
|
-
|
|
37
|
-
DO NOT extract skills for:
|
|
38
|
-
- Simple single-step tasks (just opening one page)
|
|
39
|
-
- Tasks that failed or had errors
|
|
40
|
-
- Tasks that are too specific to generalize (e.g. "buy exactly this item at this price")
|
|
41
|
-
|
|
42
|
-
Respond with ONLY valid JSON — no markdown wrapping, no explanation:
|
|
43
|
-
|
|
44
|
-
If a reusable skill was found:
|
|
45
|
-
{"name": "skill-name", "description": "What this skill does", "steps": "## Workflow\\n\\n1. **Step one**\\n...", "emoji": "🔧"}
|
|
46
|
-
|
|
47
|
-
If nothing worth extracting:
|
|
48
|
-
null`;
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Use Haiku to analyze a completed multi-step task and extract a reusable skill.
|
|
52
|
-
* Only called when a task involved 3+ tool calls (multi-step workflow).
|
|
53
|
-
*
|
|
54
|
-
* Cost: ~$0.002 per extraction (slightly more context than memory extraction).
|
|
55
|
-
*/
|
|
56
|
-
export async function extractSkillWithLLM(
|
|
57
|
-
taskPrompt: string,
|
|
58
|
-
taskResult: string,
|
|
59
|
-
toolCalls: ToolCallRecord[]
|
|
60
|
-
): Promise<ExtractedSkill | null> {
|
|
61
|
-
const config = getConfig();
|
|
62
|
-
const apiKey = config.anthropicApiKey;
|
|
63
|
-
|
|
64
|
-
if (!apiKey) {
|
|
65
|
-
log.debug("No API key, skipping skill extraction");
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Build a summary of tool calls (not the full data, just the flow)
|
|
70
|
-
const toolFlow = toolCalls
|
|
71
|
-
.map((tc, i) => {
|
|
72
|
-
const inputSummary = Object.entries(tc.input)
|
|
73
|
-
.map(([k, v]) => `${k}=${String(v).slice(0, 100)}`)
|
|
74
|
-
.join(", ");
|
|
75
|
-
const resultSummary = tc.result.slice(0, 150);
|
|
76
|
-
return `${i + 1}. ${tc.name}(${inputSummary}) → ${resultSummary}`;
|
|
77
|
-
})
|
|
78
|
-
.slice(0, 20) // Max 20 tool calls in summary
|
|
79
|
-
.join("\n");
|
|
80
|
-
|
|
81
|
-
const userMessage = `<task_prompt>
|
|
82
|
-
${taskPrompt.slice(0, 1500)}
|
|
83
|
-
</task_prompt>
|
|
84
|
-
|
|
85
|
-
<tool_calls_flow>
|
|
86
|
-
${toolFlow.slice(0, 3000)}
|
|
87
|
-
</tool_calls_flow>
|
|
88
|
-
|
|
89
|
-
<final_result>
|
|
90
|
-
${taskResult.slice(0, 2000)}
|
|
91
|
-
</final_result>`;
|
|
92
|
-
|
|
93
|
-
try {
|
|
94
|
-
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
95
|
-
method: "POST",
|
|
96
|
-
headers: {
|
|
97
|
-
"Content-Type": "application/json",
|
|
98
|
-
"x-api-key": apiKey,
|
|
99
|
-
"anthropic-version": "2023-06-01",
|
|
100
|
-
},
|
|
101
|
-
body: JSON.stringify({
|
|
102
|
-
model: "claude-haiku-4-5-20251001",
|
|
103
|
-
max_tokens: 1024,
|
|
104
|
-
system: EXTRACTION_PROMPT,
|
|
105
|
-
messages: [{ role: "user", content: userMessage }],
|
|
106
|
-
}),
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
if (!response.ok) {
|
|
110
|
-
const errText = await response.text();
|
|
111
|
-
log.debug(
|
|
112
|
-
`Skill extraction API error: ${response.status} ${errText.slice(0, 200)}`
|
|
113
|
-
);
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const data = await response.json();
|
|
118
|
-
|
|
119
|
-
const text =
|
|
120
|
-
data.content?.[0]?.type === "text" ? data.content[0].text : "";
|
|
121
|
-
|
|
122
|
-
if (!text || text.trim() === "null") {
|
|
123
|
-
log.debug("Skill extraction: nothing reusable found");
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Parse JSON
|
|
128
|
-
const jsonStr = text
|
|
129
|
-
.replace(/```json\n?/g, "")
|
|
130
|
-
.replace(/```\n?/g, "")
|
|
131
|
-
.trim();
|
|
132
|
-
const skill: ExtractedSkill = JSON.parse(jsonStr);
|
|
133
|
-
|
|
134
|
-
// Validate
|
|
135
|
-
if (!skill || !skill.name || !skill.steps || !skill.description) {
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Sanitize name to kebab-case
|
|
140
|
-
skill.name = skill.name
|
|
141
|
-
.toLowerCase()
|
|
142
|
-
.replace(/[^a-z0-9-]/g, "-")
|
|
143
|
-
.replace(/-+/g, "-")
|
|
144
|
-
.replace(/^-|-$/g, "");
|
|
145
|
-
|
|
146
|
-
if (skill.name.length < 3) return null;
|
|
147
|
-
|
|
148
|
-
return skill;
|
|
149
|
-
} catch (err) {
|
|
150
|
-
log.debug(
|
|
151
|
-
`Skill extraction failed: ${err instanceof Error ? err.message : err}`
|
|
152
|
-
);
|
|
153
|
-
return null;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// ── Proactive Skill Generation (from description, not from completed tasks) ──
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* A skill specification decomposed from a work profile.
|
|
161
|
-
*/
|
|
162
|
-
export interface SkillSpec {
|
|
163
|
-
name: string;
|
|
164
|
-
description: string;
|
|
165
|
-
category: string; // e.g. "data-processing", "communication", "reporting"
|
|
166
|
-
priority: "high" | "medium" | "low";
|
|
167
|
-
automatable: boolean; // whether the agent can fully automate this
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const DECOMPOSE_JOB_PROMPT = `You are a job analysis system. Given a person's job description, decompose their work into individual AUTOMATABLE SKILLS that an AI agent could learn and execute.
|
|
171
|
-
|
|
172
|
-
The AI agent has these capabilities:
|
|
173
|
-
- Control a real Chrome browser (navigate, click, type, read pages, take screenshots)
|
|
174
|
-
- Read/write files on the local machine
|
|
175
|
-
- Execute shell commands
|
|
176
|
-
- Store memories and learn from interactions
|
|
177
|
-
|
|
178
|
-
Rules:
|
|
179
|
-
- Focus on RECURRING, REPETITIVE tasks that benefit from automation
|
|
180
|
-
- Each skill should be a single, well-defined workflow
|
|
181
|
-
- Prioritize tasks the person does frequently
|
|
182
|
-
- Mark tasks as automatable:true only if the agent can handle them end-to-end
|
|
183
|
-
- Name skills in kebab-case
|
|
184
|
-
- Be specific — "check-email" is too vague, "summarize-unread-gmail" is better
|
|
185
|
-
- Generate 5-15 skills depending on job complexity
|
|
186
|
-
- Consider both web-based and file-based workflows
|
|
187
|
-
|
|
188
|
-
Respond with ONLY valid JSON array — no markdown wrapping:
|
|
189
|
-
[{"name": "skill-name", "description": "What this skill does", "category": "category", "priority": "high|medium|low", "automatable": true|false}]`;
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Decompose a job description into individual skill specifications.
|
|
193
|
-
* This is the first step: understand what the person does and break it into automatable units.
|
|
194
|
-
*
|
|
195
|
-
* Cost: ~$0.003 per call (Haiku, moderate context).
|
|
196
|
-
*/
|
|
197
|
-
export async function decomposeJob(
|
|
198
|
-
jobDescription: string,
|
|
199
|
-
existingSkillNames: string[] = []
|
|
200
|
-
): Promise<SkillSpec[]> {
|
|
201
|
-
const config = getConfig();
|
|
202
|
-
const apiKey = config.anthropicApiKey;
|
|
203
|
-
if (!apiKey) return [];
|
|
204
|
-
|
|
205
|
-
const existingNote =
|
|
206
|
-
existingSkillNames.length > 0
|
|
207
|
-
? `\n\nThe agent already has these skills (do NOT duplicate): ${existingSkillNames.join(", ")}`
|
|
208
|
-
: "";
|
|
209
|
-
|
|
210
|
-
try {
|
|
211
|
-
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
212
|
-
method: "POST",
|
|
213
|
-
headers: {
|
|
214
|
-
"Content-Type": "application/json",
|
|
215
|
-
"x-api-key": apiKey,
|
|
216
|
-
"anthropic-version": "2023-06-01",
|
|
217
|
-
},
|
|
218
|
-
body: JSON.stringify({
|
|
219
|
-
model: "claude-haiku-4-5-20251001",
|
|
220
|
-
max_tokens: 2048,
|
|
221
|
-
system: DECOMPOSE_JOB_PROMPT,
|
|
222
|
-
messages: [
|
|
223
|
-
{
|
|
224
|
-
role: "user",
|
|
225
|
-
content: `<job_description>\n${jobDescription.slice(0, 3000)}\n</job_description>${existingNote}`,
|
|
226
|
-
},
|
|
227
|
-
],
|
|
228
|
-
}),
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
if (!response.ok) return [];
|
|
232
|
-
|
|
233
|
-
const data = await response.json();
|
|
234
|
-
const text =
|
|
235
|
-
data.content?.[0]?.type === "text" ? data.content[0].text : "";
|
|
236
|
-
if (!text) return [];
|
|
237
|
-
|
|
238
|
-
const jsonStr = text
|
|
239
|
-
.replace(/```json\n?/g, "")
|
|
240
|
-
.replace(/```\n?/g, "")
|
|
241
|
-
.trim();
|
|
242
|
-
const specs: SkillSpec[] = JSON.parse(jsonStr);
|
|
243
|
-
|
|
244
|
-
if (!Array.isArray(specs)) return [];
|
|
245
|
-
|
|
246
|
-
// Sanitize names
|
|
247
|
-
return specs
|
|
248
|
-
.filter((s) => s.name && s.description)
|
|
249
|
-
.map((s) => ({
|
|
250
|
-
...s,
|
|
251
|
-
name: s.name
|
|
252
|
-
.toLowerCase()
|
|
253
|
-
.replace(/[^a-z0-9-]/g, "-")
|
|
254
|
-
.replace(/-+/g, "-")
|
|
255
|
-
.replace(/^-|-$/g, ""),
|
|
256
|
-
}));
|
|
257
|
-
} catch (err) {
|
|
258
|
-
log.debug(`Job decomposition failed: ${err}`);
|
|
259
|
-
return [];
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const GENERATE_PROMPT = `You are a skill authoring system for an AI agent. Given a skill name, description, and context about the user's work, generate detailed step-by-step instructions that the AI agent can follow to execute this skill.
|
|
264
|
-
|
|
265
|
-
The AI agent has these capabilities:
|
|
266
|
-
- Control a real Chrome browser (navigate, click, type, read pages, take screenshots)
|
|
267
|
-
- Read/write files on the local machine
|
|
268
|
-
- Execute shell commands (Bash)
|
|
269
|
-
- Store memories about the user
|
|
270
|
-
|
|
271
|
-
Rules:
|
|
272
|
-
- Write clear, actionable markdown instructions
|
|
273
|
-
- Use numbered steps with bold action names
|
|
274
|
-
- Include error handling (what to do if a page doesn't load, element not found, etc.)
|
|
275
|
-
- Use placeholders like {query}, {date}, {recipient} for variable inputs
|
|
276
|
-
- Include a $ARGUMENTS line at the top if the skill accepts parameters
|
|
277
|
-
- Be specific about which browser tools to use (browser_navigate, browser_click, etc.)
|
|
278
|
-
- Include validation steps (verify the action worked)
|
|
279
|
-
- Keep instructions thorough but concise (10-25 steps)
|
|
280
|
-
|
|
281
|
-
Respond with ONLY valid JSON — no markdown wrapping:
|
|
282
|
-
{"name": "skill-name", "description": "One-line description", "steps": "## Workflow\\n\\n$ARGUMENTS: describe expected arguments\\n\\n1. **Step one**\\n...", "emoji": "🔧", "keywords": ["keyword1", "keyword2"]}`;
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Generate a complete skill (with full instructions) from a description.
|
|
286
|
-
* Unlike extractSkillWithLLM which requires a completed task, this generates proactively.
|
|
287
|
-
*
|
|
288
|
-
* Cost: ~$0.003 per generation (Haiku, moderate output).
|
|
289
|
-
*/
|
|
290
|
-
export async function generateSkillFromDescription(
|
|
291
|
-
name: string,
|
|
292
|
-
description: string,
|
|
293
|
-
jobContext?: string
|
|
294
|
-
): Promise<ExtractedSkill & { keywords?: string[] } | null> {
|
|
295
|
-
const config = getConfig();
|
|
296
|
-
const apiKey = config.anthropicApiKey;
|
|
297
|
-
if (!apiKey) return null;
|
|
298
|
-
|
|
299
|
-
const contextBlock = jobContext
|
|
300
|
-
? `\n\n<user_job_context>\n${jobContext.slice(0, 1000)}\n</user_job_context>`
|
|
301
|
-
: "";
|
|
302
|
-
|
|
303
|
-
try {
|
|
304
|
-
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
305
|
-
method: "POST",
|
|
306
|
-
headers: {
|
|
307
|
-
"Content-Type": "application/json",
|
|
308
|
-
"x-api-key": apiKey,
|
|
309
|
-
"anthropic-version": "2023-06-01",
|
|
310
|
-
},
|
|
311
|
-
body: JSON.stringify({
|
|
312
|
-
model: "claude-haiku-4-5-20251001",
|
|
313
|
-
max_tokens: 2048,
|
|
314
|
-
system: GENERATE_PROMPT,
|
|
315
|
-
messages: [
|
|
316
|
-
{
|
|
317
|
-
role: "user",
|
|
318
|
-
content: `Generate a skill for:\nName: ${name}\nDescription: ${description}${contextBlock}`,
|
|
319
|
-
},
|
|
320
|
-
],
|
|
321
|
-
}),
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
if (!response.ok) return null;
|
|
325
|
-
|
|
326
|
-
const data = await response.json();
|
|
327
|
-
const text =
|
|
328
|
-
data.content?.[0]?.type === "text" ? data.content[0].text : "";
|
|
329
|
-
if (!text) return null;
|
|
330
|
-
|
|
331
|
-
const jsonStr = text
|
|
332
|
-
.replace(/```json\n?/g, "")
|
|
333
|
-
.replace(/```\n?/g, "")
|
|
334
|
-
.trim();
|
|
335
|
-
const skill = JSON.parse(jsonStr);
|
|
336
|
-
|
|
337
|
-
if (!skill || !skill.name || !skill.steps) return null;
|
|
338
|
-
|
|
339
|
-
skill.name = skill.name
|
|
340
|
-
.toLowerCase()
|
|
341
|
-
.replace(/[^a-z0-9-]/g, "-")
|
|
342
|
-
.replace(/-+/g, "-")
|
|
343
|
-
.replace(/^-|-$/g, "");
|
|
344
|
-
|
|
345
|
-
return skill;
|
|
346
|
-
} catch (err) {
|
|
347
|
-
log.debug(`Skill generation failed: ${err}`);
|
|
348
|
-
return null;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const IMPROVEMENT_PROMPT = `You are a skill improvement system. You have an EXISTING skill (workflow instructions for an AI agent) and a NEW task execution that used this skill. Analyze if the new execution discovered a BETTER approach than what the current skill describes.
|
|
353
|
-
|
|
354
|
-
Rules:
|
|
355
|
-
- Only suggest improvements if the new execution found a genuinely better workflow
|
|
356
|
-
- Merge the best parts of the old skill and new execution
|
|
357
|
-
- Keep instructions general/reusable (use placeholders like {product}, {query})
|
|
358
|
-
- Preserve the overall structure but update specific steps that improved
|
|
359
|
-
- If the existing skill is already optimal, return null
|
|
360
|
-
- The output must be the FULL updated skill content (not a diff)
|
|
361
|
-
|
|
362
|
-
Respond with ONLY valid JSON — no markdown wrapping:
|
|
363
|
-
|
|
364
|
-
If improvement found:
|
|
365
|
-
{"improved_steps": "## Workflow\\n\\n1. **Step one**\\n...", "change_summary": "Brief description of what changed"}
|
|
366
|
-
|
|
367
|
-
If no improvement needed:
|
|
368
|
-
null`;
|
|
369
|
-
|
|
370
|
-
/**
|
|
371
|
-
* Analyze whether a skill should be improved based on a new task execution.
|
|
372
|
-
* Called when a task used an existing skill and completed successfully.
|
|
373
|
-
*/
|
|
374
|
-
export async function analyzeSkillImprovement(
|
|
375
|
-
existingSkillContent: string,
|
|
376
|
-
taskPrompt: string,
|
|
377
|
-
taskResult: string,
|
|
378
|
-
toolCalls: ToolCallRecord[]
|
|
379
|
-
): Promise<{ improved_steps: string; change_summary: string } | null> {
|
|
380
|
-
const config = getConfig();
|
|
381
|
-
const apiKey = config.anthropicApiKey;
|
|
382
|
-
|
|
383
|
-
if (!apiKey) return null;
|
|
384
|
-
|
|
385
|
-
const toolFlow = toolCalls
|
|
386
|
-
.map((tc, i) => {
|
|
387
|
-
const inputSummary = Object.entries(tc.input)
|
|
388
|
-
.map(([k, v]) => `${k}=${String(v).slice(0, 80)}`)
|
|
389
|
-
.join(", ");
|
|
390
|
-
return `${i + 1}. ${tc.name}(${inputSummary})`;
|
|
391
|
-
})
|
|
392
|
-
.slice(0, 15)
|
|
393
|
-
.join("\n");
|
|
394
|
-
|
|
395
|
-
const userMessage = `<existing_skill>
|
|
396
|
-
${existingSkillContent.slice(0, 2000)}
|
|
397
|
-
</existing_skill>
|
|
398
|
-
|
|
399
|
-
<new_task>
|
|
400
|
-
${taskPrompt.slice(0, 1000)}
|
|
401
|
-
</new_task>
|
|
402
|
-
|
|
403
|
-
<actual_tool_flow>
|
|
404
|
-
${toolFlow.slice(0, 2000)}
|
|
405
|
-
</actual_tool_flow>
|
|
406
|
-
|
|
407
|
-
<task_result>
|
|
408
|
-
${taskResult.slice(0, 1500)}
|
|
409
|
-
</task_result>`;
|
|
410
|
-
|
|
411
|
-
try {
|
|
412
|
-
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
413
|
-
method: "POST",
|
|
414
|
-
headers: {
|
|
415
|
-
"Content-Type": "application/json",
|
|
416
|
-
"x-api-key": apiKey,
|
|
417
|
-
"anthropic-version": "2023-06-01",
|
|
418
|
-
},
|
|
419
|
-
body: JSON.stringify({
|
|
420
|
-
model: "claude-haiku-4-5-20251001",
|
|
421
|
-
max_tokens: 1024,
|
|
422
|
-
system: IMPROVEMENT_PROMPT,
|
|
423
|
-
messages: [{ role: "user", content: userMessage }],
|
|
424
|
-
}),
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
if (!response.ok) return null;
|
|
428
|
-
|
|
429
|
-
const data = await response.json();
|
|
430
|
-
const text =
|
|
431
|
-
data.content?.[0]?.type === "text" ? data.content[0].text : "";
|
|
432
|
-
|
|
433
|
-
if (!text || text.trim() === "null") return null;
|
|
434
|
-
|
|
435
|
-
const jsonStr = text
|
|
436
|
-
.replace(/```json\n?/g, "")
|
|
437
|
-
.replace(/```\n?/g, "")
|
|
438
|
-
.trim();
|
|
439
|
-
const result = JSON.parse(jsonStr);
|
|
440
|
-
|
|
441
|
-
if (!result || !result.improved_steps) return null;
|
|
442
|
-
|
|
443
|
-
return result;
|
|
444
|
-
} catch {
|
|
445
|
-
return null;
|
|
446
|
-
}
|
|
447
|
-
}
|
package/src/agent/skills.ts
CHANGED
|
@@ -371,33 +371,35 @@ export class SkillManager {
|
|
|
371
371
|
? { openclaw: { emoji: options.emoji } }
|
|
372
372
|
: {};
|
|
373
373
|
|
|
374
|
-
const { data, error } = await sb
|
|
375
|
-
.
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
metadata,
|
|
386
|
-
source,
|
|
387
|
-
is_public: false,
|
|
388
|
-
},
|
|
389
|
-
{ onConflict: "author_id,name" }
|
|
390
|
-
)
|
|
391
|
-
.select("id, name")
|
|
392
|
-
.single();
|
|
374
|
+
const { data, error } = await sb.rpc("create_skill", {
|
|
375
|
+
p_user_id: this.userId,
|
|
376
|
+
p_name: name,
|
|
377
|
+
p_description: description,
|
|
378
|
+
p_content: content,
|
|
379
|
+
p_version: "1.0.0",
|
|
380
|
+
p_source: source,
|
|
381
|
+
p_emoji: options?.emoji || null,
|
|
382
|
+
p_keywords: options?.keywords || [],
|
|
383
|
+
p_metadata: metadata,
|
|
384
|
+
});
|
|
393
385
|
|
|
394
386
|
if (error) {
|
|
395
387
|
log.debug(`Skill create failed for "${name}": ${error.message}`);
|
|
396
388
|
return null;
|
|
397
389
|
}
|
|
398
390
|
|
|
399
|
-
|
|
400
|
-
|
|
391
|
+
const row = (Array.isArray(data) ? data[0] : data) as Record<string, unknown> | null;
|
|
392
|
+
if (!row) {
|
|
393
|
+
log.debug(`Skill create returned no data for "${name}"`);
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// RPC returns out_id/out_name to avoid column ambiguity
|
|
398
|
+
const id = (row.out_id || row.id) as string;
|
|
399
|
+
const skillName = (row.out_name || row.name) as string;
|
|
400
|
+
|
|
401
|
+
log.info(`Skill "${skillName}" created in skills table (pending approval)`);
|
|
402
|
+
return { id, name: skillName };
|
|
401
403
|
} catch (err) {
|
|
402
404
|
log.debug(`Skill create error: ${err}`);
|
|
403
405
|
return null;
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { getConfig } from "../utils/config.js";
|
|
2
|
-
import { log } from "../utils/logger.js";
|
|
3
|
-
import type { MemoryCategory } from "./memory.js";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Extracted memory from LLM analysis of a conversation.
|
|
7
|
-
*/
|
|
8
|
-
export interface ExtractedMemory {
|
|
9
|
-
content: string;
|
|
10
|
-
category: MemoryCategory;
|
|
11
|
-
importance: number;
|
|
12
|
-
tags: string[];
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const EXTRACTION_PROMPT = `You are a memory extraction system. Analyze the following conversation between a user and an AI assistant. Extract any facts worth remembering about the user for future conversations.
|
|
16
|
-
|
|
17
|
-
Rules:
|
|
18
|
-
- Only extract FACTUAL information about the user, not about the task itself
|
|
19
|
-
- Be concise: each memory should be a single clear statement
|
|
20
|
-
- Don't extract trivial/obvious things (e.g. "user asked a question")
|
|
21
|
-
- Categories: "preference" (likes/dislikes/habits), "instruction" (standing orders like "always do X"), "context" (work/life info), "fact" (specific facts), "general" (other)
|
|
22
|
-
- Importance: 3-4 for minor preferences, 5-6 for useful context, 7-8 for strong preferences/instructions, 9-10 for critical standing instructions
|
|
23
|
-
- If nothing worth remembering, return an empty array
|
|
24
|
-
- Max 3 memories per conversation
|
|
25
|
-
- Support both English and Chinese content
|
|
26
|
-
|
|
27
|
-
Respond with ONLY valid JSON — no markdown, no explanation:
|
|
28
|
-
[{"content": "...", "category": "...", "importance": N, "tags": ["..."]}]
|
|
29
|
-
|
|
30
|
-
If nothing to remember: []`;
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Use Haiku to analyze a task conversation and extract memories.
|
|
34
|
-
* This runs as a fire-and-forget background task after each completed task.
|
|
35
|
-
* Cost: ~$0.001 per extraction (Haiku is very cheap).
|
|
36
|
-
*/
|
|
37
|
-
export async function extractMemoriesWithLLM(
|
|
38
|
-
taskPrompt: string,
|
|
39
|
-
taskResult: string
|
|
40
|
-
): Promise<ExtractedMemory[]> {
|
|
41
|
-
const config = getConfig();
|
|
42
|
-
const apiKey = config.anthropicApiKey;
|
|
43
|
-
|
|
44
|
-
if (!apiKey) {
|
|
45
|
-
log.debug("No API key, skipping LLM memory extraction");
|
|
46
|
-
return [];
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Truncate to keep costs low — Haiku context is cheap but no need to waste
|
|
50
|
-
const truncatedPrompt = taskPrompt.slice(0, 2000);
|
|
51
|
-
const truncatedResult = taskResult.slice(0, 3000);
|
|
52
|
-
|
|
53
|
-
const userMessage = `<user_request>
|
|
54
|
-
${truncatedPrompt}
|
|
55
|
-
</user_request>
|
|
56
|
-
|
|
57
|
-
<assistant_response>
|
|
58
|
-
${truncatedResult}
|
|
59
|
-
</assistant_response>`;
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
63
|
-
method: "POST",
|
|
64
|
-
headers: {
|
|
65
|
-
"Content-Type": "application/json",
|
|
66
|
-
"x-api-key": apiKey,
|
|
67
|
-
"anthropic-version": "2023-06-01",
|
|
68
|
-
},
|
|
69
|
-
body: JSON.stringify({
|
|
70
|
-
model: "claude-haiku-4-5-20251001",
|
|
71
|
-
max_tokens: 512,
|
|
72
|
-
system: EXTRACTION_PROMPT,
|
|
73
|
-
messages: [{ role: "user", content: userMessage }],
|
|
74
|
-
}),
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
if (!response.ok) {
|
|
78
|
-
const errText = await response.text();
|
|
79
|
-
log.debug(`Memory extraction API error: ${response.status} ${errText.slice(0, 200)}`);
|
|
80
|
-
return [];
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const data = await response.json();
|
|
84
|
-
|
|
85
|
-
// Extract text from response
|
|
86
|
-
const text =
|
|
87
|
-
data.content?.[0]?.type === "text" ? data.content[0].text : "";
|
|
88
|
-
|
|
89
|
-
if (!text || text.trim() === "[]") {
|
|
90
|
-
log.debug("LLM memory extraction: nothing to remember");
|
|
91
|
-
return [];
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Parse JSON — handle potential markdown wrapping
|
|
95
|
-
const jsonStr = text.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim();
|
|
96
|
-
const memories: ExtractedMemory[] = JSON.parse(jsonStr);
|
|
97
|
-
|
|
98
|
-
// Validate
|
|
99
|
-
const validCategories: MemoryCategory[] = [
|
|
100
|
-
"general",
|
|
101
|
-
"preference",
|
|
102
|
-
"instruction",
|
|
103
|
-
"context",
|
|
104
|
-
"skill_learned",
|
|
105
|
-
"fact",
|
|
106
|
-
];
|
|
107
|
-
|
|
108
|
-
return memories
|
|
109
|
-
.filter(
|
|
110
|
-
(m) =>
|
|
111
|
-
m.content &&
|
|
112
|
-
m.content.length > 5 &&
|
|
113
|
-
validCategories.includes(m.category)
|
|
114
|
-
)
|
|
115
|
-
.map((m) => ({
|
|
116
|
-
content: m.content.slice(0, 500),
|
|
117
|
-
category: m.category,
|
|
118
|
-
importance: Math.max(1, Math.min(10, m.importance || 5)),
|
|
119
|
-
tags: Array.isArray(m.tags) ? m.tags.slice(0, 5) : [],
|
|
120
|
-
}))
|
|
121
|
-
.slice(0, 3); // Max 3 memories per conversation
|
|
122
|
-
} catch (err) {
|
|
123
|
-
log.debug(
|
|
124
|
-
`LLM memory extraction failed: ${err instanceof Error ? err.message : err}`
|
|
125
|
-
);
|
|
126
|
-
return [];
|
|
127
|
-
}
|
|
128
|
-
}
|