feat: add read-only savings agent tools
This commit is contained in:
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user