feat: add read-only savings agent tools

This commit is contained in:
2026-06-15 21:21:57 +02:00
parent 4a1cbd105b
commit 1c88d12f0d
4 changed files with 1274 additions and 46 deletions

View File

@@ -11,7 +11,18 @@ import { Separator } from "@/components/ui/separator";
import { ChatHistory, type ChatHistoryItem } from "@/components/chat/ChatHistory";
import { toast } from "sonner";
type ChatMessage = { role: "user" | "assistant"; content: string };
type ToolTrace = {
name: string;
inputSummary: string;
resultSummary: string;
};
type UserChatMessage = { role: "user"; content: string };
type AssistantChatMessage = {
role: "assistant";
content: string;
toolTrace?: ToolTrace[];
};
type ChatMessage = UserChatMessage | AssistantChatMessage;
type ChatSession = {
id: string;
title: string;
@@ -27,6 +38,77 @@ const initialAssistantMessage: ChatMessage = {
};
const fallbackMessages = [initialAssistantMessage];
function normalizeToolTrace(value: unknown): ToolTrace[] | undefined {
if (!Array.isArray(value)) return undefined;
const trace = value.flatMap((item) => {
if (!item || typeof item !== "object") return [];
const candidate = item as Record<string, unknown>;
if (
typeof candidate.name !== "string" ||
typeof candidate.inputSummary !== "string" ||
typeof candidate.resultSummary !== "string"
) {
return [];
}
return [
{
name: candidate.name,
inputSummary: candidate.inputSummary,
resultSummary: candidate.resultSummary,
},
];
});
return trace.length > 0 ? trace : undefined;
}
function normalizeMessage(value: unknown): ChatMessage | null {
if (!value || typeof value !== "object") return null;
const candidate = value as Record<string, unknown>;
if (typeof candidate.content !== "string") return null;
if (candidate.role === "user") {
return { role: "user", content: candidate.content };
}
if (candidate.role === "assistant") {
const toolTrace = normalizeToolTrace(candidate.toolTrace);
return toolTrace
? { role: "assistant", content: candidate.content, toolTrace }
: { role: "assistant", content: candidate.content };
}
return null;
}
function isChatMessage(value: ChatMessage | null): value is ChatMessage {
return value !== null;
}
function normalizeSession(value: unknown): ChatSession | null {
if (!value || typeof value !== "object") return null;
const candidate = value as Record<string, unknown>;
if (
typeof candidate.id !== "string" ||
typeof candidate.title !== "string" ||
typeof candidate.createdAt !== "number" ||
typeof candidate.updatedAt !== "number" ||
!Array.isArray(candidate.messages)
) {
return null;
}
const messages = candidate.messages.map(normalizeMessage);
if (!messages.every(isChatMessage)) return null;
return {
id: candidate.id,
title: candidate.title,
createdAt: candidate.createdAt,
updatedAt: candidate.updatedAt,
messages,
};
}
function createSession(): ChatSession {
const now = Date.now();
const randomId =
@@ -47,9 +129,11 @@ function loadSessions(): ChatSession[] {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return [createSession()];
const parsed = JSON.parse(raw) as ChatSession[];
const parsed: unknown = JSON.parse(raw);
if (!Array.isArray(parsed) || parsed.length === 0) return [createSession()];
return parsed;
const sessions = parsed.map(normalizeSession);
if (sessions.some((session) => session === null)) return [createSession()];
return sessions as ChatSession[];
} catch {
return [createSession()];
}
@@ -156,16 +240,23 @@ export function SavingsChatPage() {
try {
const response = await ask({
messages: typedNextMessages,
messages: typedNextMessages.map((message) => ({
role: message.role,
content: message.content,
})),
from,
to,
accountId,
basis: monthBasis,
});
}) as { answer: string; toolTrace?: unknown };
updateSession(submittedSessionId, [
...typedNextMessages,
{ role: "assistant", content: response.answer },
{
role: "assistant",
content: response.answer,
toolTrace: normalizeToolTrace(response.toolTrace),
},
]);
} catch (error) {
console.error(error);
@@ -228,6 +319,21 @@ export function SavingsChatPage() {
>
<p className="text-xs uppercase text-muted-foreground">{message.role}</p>
<p className="whitespace-pre-wrap text-sm">{message.content}</p>
{message.role === "assistant" && message.toolTrace && message.toolTrace.length > 0 && (
<div className="mt-3 rounded-md border bg-muted/30 p-2">
<p className="text-xs font-medium text-muted-foreground">
Verwendete Werkzeuge
</p>
<div className="mt-2 space-y-2">
{message.toolTrace.map((tool, toolIndex) => (
<div key={`${tool.name}-${toolIndex}`} className="text-xs">
<p className="font-medium">{tool.name}</p>
<p className="text-muted-foreground">{tool.resultSummary}</p>
</div>
))}
</div>
</div>
)}
</div>
))}
{isSubmitting && (