feat(odin): Phase 2a read-only agentic assistant + tool-trace UI

- agentic chat loop (max 6 round-trips, budget re-checked between iterations)
  over 10 read-only, permission-delegated tools in src/services/ai-tools.ts
- persist assistant tool trace in ai_chat_messages.content_json (+ migration)
- consulted-tools chips in OdinThread; header now shows month spend only
  (budget cap hidden from the UI, server-side 402 guard unchanged)
- seed: ai.use permission; Settings exposes the ai permission module
- docs: REVIEW consolidation; deployment-guide.md superseded by docs/release.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-10 23:31:50 +02:00
parent 2dbacc3bec
commit 674b44d047
26 changed files with 13728 additions and 2935 deletions

View File

@@ -122,6 +122,7 @@ export default function OdinChat() {
messagesData.messages.map((m) => ({
role: m.role as "user" | "assistant",
content: m.content,
tools: m.meta?.tools,
})),
);
setReview([]);
@@ -148,6 +149,9 @@ export default function OdinChat() {
messages: msgs.map((m) => ({
role: m.role,
content: m.content.slice(0, MAX_STORE),
...(m.tools && m.tools.length > 0
? { meta: { tools: m.tools } }
: {}),
})),
}),
})
@@ -208,24 +212,8 @@ export default function OdinChat() {
const files = attachments.map((a) => a.file);
if (!text && files.length === 0) return;
// Guard: Odin is scoped to invoice processing only (for now). A text-only
// message makes NO AI call — it gets a canned reply locally, so open-ended
// chat can't burn API credits. The invoice path (attachments) runs normally.
// (Removing this guard re-enables the general /chat path below for Phase 2.)
if (files.length === 0) {
setTurns((t) => [
...t,
{ role: "user", content: text },
{
role: "assistant",
content:
"Momentálně umím zpracovat pouze přijaté faktury. Přiložte prosím fakturu (PDF) a já z ní načtu údaje k uložení.",
},
]);
setInput("");
return;
}
// Phase 2a: text messages go to the agentic /chat endpoint (read-only
// tools over system data); attachments keep the invoice-extract path.
setBusy(true);
const prevTurns = turns;
@@ -301,14 +289,17 @@ export default function OdinChat() {
const body = await res.json();
if (!res.ok) throw new Error(body?.error || "Chyba AI");
const reply: string = body.data.reply;
setTurns((t) => [...t, { role: "assistant", content: reply }]);
const tools: ChatTurn["tools"] = Array.isArray(body.data.tool_trace)
? body.data.tool_trace
: undefined;
setTurns((t) => [...t, { role: "assistant", content: reply, tools }]);
setInput("");
// Create the conversation now (first successful message) and persist.
const convId = await ensureActive();
if (convId != null)
persist(convId, [
{ role: "user", content: userContent },
{ role: "assistant", content: reply },
{ role: "assistant", content: reply, tools },
]);
qc.invalidateQueries({ queryKey: ["ai", "usage"] });
}
@@ -495,8 +486,7 @@ export default function OdinChat() {
color="text.secondary"
sx={{ flexShrink: 0, display: { xs: "none", sm: "block" } }}
>
Utraceno: ${usage.month_spend_usd.toFixed(2)} / $
{usage.budget_usd.toFixed(2)}
Utraceno: ${usage.month_spend_usd.toFixed(2)}
</Typography>
)}
</Box>