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:
@@ -18,7 +18,7 @@ import {
|
||||
getMonthSpendUsd,
|
||||
getBudgetUsd,
|
||||
setBudgetUsd,
|
||||
chat,
|
||||
agenticChat,
|
||||
extractInvoice,
|
||||
listConversations,
|
||||
createConversation,
|
||||
@@ -75,7 +75,9 @@ export default async function aiRoutes(app: FastifyInstance): Promise<void> {
|
||||
},
|
||||
);
|
||||
|
||||
// POST /api/admin/ai/chat
|
||||
// POST /api/admin/ai/chat — agentic turn with read-only tools (Phase 2a).
|
||||
// The tools execute AS this user: the authData context (permissions +
|
||||
// admin bypass) is what each tool handler checks — see ai-tools.ts.
|
||||
app.post(
|
||||
"/chat",
|
||||
{ preHandler: requirePermission("ai.use") },
|
||||
@@ -85,16 +87,24 @@ export default async function aiRoutes(app: FastifyInstance): Promise<void> {
|
||||
if ("error" in body) return error(reply, body.error, 400);
|
||||
const budgetErr = await assertBudgetAvailable();
|
||||
if (budgetErr) return error(reply, budgetErr.error, budgetErr.status);
|
||||
const { reply: text } = await chat(
|
||||
body.data.messages,
|
||||
request.authData!.userId,
|
||||
);
|
||||
const auth = request.authData!;
|
||||
const { reply: text, toolTrace } = await agenticChat(body.data.messages, {
|
||||
userId: auth.userId,
|
||||
// AuthData.roleName is nullable; a null role simply never matches the
|
||||
// admin bypass and holds only its explicit permissions.
|
||||
roleName: auth.roleName ?? "",
|
||||
permissions: auth.permissions,
|
||||
});
|
||||
const [budgetAfter, spendAfter] = await Promise.all([
|
||||
getBudgetUsd(),
|
||||
getMonthSpendUsd(),
|
||||
]);
|
||||
const remaining_usd = Math.max(0, budgetAfter - spendAfter);
|
||||
return success(reply, { reply: text, remaining_usd });
|
||||
return success(reply, {
|
||||
reply: text,
|
||||
tool_trace: toolTrace,
|
||||
remaining_usd,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user