mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 02:41:09 +02:00
fix(brain): companion can now act on previous tool results across turns
Five fixes from observed chat where user asked to complete two
tasks by title but the LLM had no way to find their IDs:
1. Tool result history: messagesToLlm() now includes previous
tool_result messages as "[Previous tool result]" entries so
the LLM can reference IDs/data from earlier turns.
2. Bare JSON tool call fallback: extractToolCall() now also
matches bare {"name":..., "params":...} JSON without the
```tool fence — the LLM kept dropping the fence.
3. IDs in list message: list_tasks now formats each entry as
"• [abc123] Title" so the LLM has the ID alongside the title.
4. New complete_tasks_by_title tool: case-insensitive substring
match, completes all matches at once. Handles "erledige beide
sicher sicher tasks" without needing IDs.
5. System prompt updates: explains the [id] bracket convention,
warns the LLM to NEVER show raw IDs to users, and references
the new tool for title-based completion.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
77d455a18d
commit
51c8a52811
2 changed files with 101 additions and 21 deletions
|
|
@ -102,19 +102,30 @@ Wenn der Nutzer fragt:
|
|||
- "Erstell mir einen Task X" → \`create_task\` aufrufen
|
||||
- "Log 200ml Wasser" → \`log_drink\` aufrufen
|
||||
- "Welche Termine heute?" → \`get_todays_events\` aufrufen
|
||||
- "Erledige Task X" (per Name) → \`complete_tasks_by_title\` mit titleMatch
|
||||
|
||||
Tool-Aufruf in genau diesem Format (NUR JSON, keine Erklaerung davor):
|
||||
Tool-Aufruf in genau diesem Format (NUR JSON in einem Code-Block):
|
||||
\`\`\`tool
|
||||
{"name": "tool_name", "params": {"key": "value"}}
|
||||
\`\`\`
|
||||
|
||||
Nach dem Tool-Ergebnis bekommst du die Daten zurueck und kannst dem Nutzer antworten.
|
||||
|
||||
## ID-Konvention
|
||||
|
||||
Listen-Tools (wie \`list_tasks\`) zeigen IDs in eckigen Klammern: \`• [abc123] Task-Titel\`.
|
||||
Wenn der Nutzer eine Aktion auf einem Listen-Eintrag will, nutze diese ID fuer den Tool-Aufruf
|
||||
(z.B. \`complete_task\` mit \`taskId: "abc123"\`).
|
||||
|
||||
Du kannst Tool-Results aus VORHERIGEN Nachrichten referenzieren — sie sind als
|
||||
"[Previous tool result]" markiert.
|
||||
|
||||
## Verhalten
|
||||
|
||||
- Antworte auf Deutsch
|
||||
- Sei kurz und hilfreich
|
||||
- **Erfinde keine Daten** — wenn du Listen oder Werte brauchst, RUFE EIN TOOL AUF
|
||||
- Zeige dem Nutzer NIE die rohen IDs in eckigen Klammern — die sind nur fuer dich
|
||||
- Wenn der Nutzer etwas loggen oder erstellen will, nutze das passende Tool
|
||||
- Ermutige den Nutzer bei Fortschritt und Streaks`;
|
||||
}
|
||||
|
|
@ -122,31 +133,65 @@ Nach dem Tool-Ergebnis bekommst du die Daten zurueck und kannst dem Nutzer antwo
|
|||
function extractToolCall(
|
||||
text: string
|
||||
): { name: string; params: Record<string, unknown>; before: string; after: string } | null {
|
||||
const toolBlockRegex = /```tool\s*\n?([\s\S]*?)\n?```/;
|
||||
const match = text.match(toolBlockRegex);
|
||||
if (!match) return null;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(match[1]) as { name: string; params: Record<string, unknown> };
|
||||
if (!parsed.name) return null;
|
||||
const before = text.slice(0, match.index).trim();
|
||||
const after = text.slice((match.index ?? 0) + match[0].length).trim();
|
||||
return { name: parsed.name, params: parsed.params ?? {}, before, after };
|
||||
} catch {
|
||||
return null;
|
||||
// Try fenced ```tool block first
|
||||
const fenced = /```(?:tool|json)?\s*\n?([\s\S]*?)\n?```/;
|
||||
const fencedMatch = text.match(fenced);
|
||||
if (fencedMatch) {
|
||||
try {
|
||||
const parsed = JSON.parse(fencedMatch[1]) as {
|
||||
name: string;
|
||||
params: Record<string, unknown>;
|
||||
};
|
||||
if (parsed.name) {
|
||||
const before = text.slice(0, fencedMatch.index).trim();
|
||||
const after = text.slice((fencedMatch.index ?? 0) + fencedMatch[0].length).trim();
|
||||
return { name: parsed.name, params: parsed.params ?? {}, before, after };
|
||||
}
|
||||
} catch {
|
||||
// Fall through to bare JSON detection
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: bare JSON object with "name" and "params" keys
|
||||
const bareJson = /\{\s*"name"\s*:\s*"[^"]+"\s*,\s*"params"\s*:\s*\{[^}]*\}\s*\}/;
|
||||
const bareMatch = text.match(bareJson);
|
||||
if (bareMatch) {
|
||||
try {
|
||||
const parsed = JSON.parse(bareMatch[0]) as { name: string; params: Record<string, unknown> };
|
||||
if (parsed.name) {
|
||||
const before = text.slice(0, bareMatch.index).trim();
|
||||
const after = text.slice((bareMatch.index ?? 0) + bareMatch[0].length).trim();
|
||||
return { name: parsed.name, params: parsed.params ?? {}, before, after };
|
||||
}
|
||||
} catch {
|
||||
// Not valid JSON
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function messagesToLlm(
|
||||
messages: LocalMessage[]
|
||||
): { role: 'user' | 'assistant' | 'system'; content: string }[] {
|
||||
return messages
|
||||
.filter((m) => m.role !== 'tool_result')
|
||||
.map((m) => ({
|
||||
role:
|
||||
m.role === 'tool_result' ? ('user' as const) : (m.role as 'user' | 'assistant' | 'system'),
|
||||
content: m.content,
|
||||
}));
|
||||
const result: { role: 'user' | 'assistant' | 'system'; content: string }[] = [];
|
||||
for (const m of messages) {
|
||||
if (m.role === 'tool_result' && m.toolResult) {
|
||||
// Surface previous tool results to the LLM so it can
|
||||
// reference IDs/data from earlier turns.
|
||||
const data = m.toolResult.data ? `\nData: ${JSON.stringify(m.toolResult.data)}` : '';
|
||||
result.push({
|
||||
role: 'user',
|
||||
content: `[Previous tool result]\n${m.toolResult.message}${data}`,
|
||||
});
|
||||
} else if (m.role === 'assistant' && m.toolCall) {
|
||||
// Skip the empty placeholder messages for tool calls
|
||||
continue;
|
||||
} else if (m.role === 'user' || m.role === 'assistant' || m.role === 'system') {
|
||||
if (m.content) result.push({ role: m.role, content: m.content });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -124,9 +124,44 @@ export const todoTools: ModuleTool[] = [
|
|||
list.length === 0
|
||||
? `Keine ${filter} Tasks`
|
||||
: list
|
||||
.map((t) => `• ${t.title}${t.dueDate ? ` (faellig ${t.dueDate})` : ''}`)
|
||||
.map(
|
||||
(t) =>
|
||||
`• [${t.id}] ${t.title}${t.dueDate ? ` (faellig ${t.dueDate})` : ''}${t.priority === 'high' ? ' [HOHE PRIO]' : ''}`
|
||||
)
|
||||
.join('\n'),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'complete_tasks_by_title',
|
||||
module: 'todo',
|
||||
description:
|
||||
'Markiert alle offenen Tasks mit dem gegebenen Titel als erledigt (case-insensitive Substring-Match). Nutze diese, wenn der Nutzer eine Task per Name erledigen will und du nicht ihre ID kennst.',
|
||||
parameters: [
|
||||
{ name: 'titleMatch', type: 'string', description: 'Titel oder Teil davon', required: true },
|
||||
],
|
||||
async execute(params) {
|
||||
const all = await taskTable.toArray();
|
||||
const active = all.filter((t) => !t.deletedAt && !t.isCompleted);
|
||||
const decrypted = await decryptRecords<LocalTask>('tasks', active);
|
||||
const tasks = decrypted.map(toTask);
|
||||
|
||||
const needle = (params.titleMatch as string).toLowerCase().trim();
|
||||
const matches = tasks.filter((t) => t.title.toLowerCase().includes(needle));
|
||||
|
||||
if (matches.length === 0) {
|
||||
return { success: false, message: `Kein offener Task mit "${params.titleMatch}" gefunden` };
|
||||
}
|
||||
|
||||
for (const t of matches) {
|
||||
await tasksStore.completeTask(t.id);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { completed: matches.length, titles: matches.map((m) => m.title) },
|
||||
message: `${matches.length} Task(s) erledigt: ${matches.map((m) => m.title).join(', ')}`,
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue