Add chat sources and citations

This commit is contained in:
Matthias
2026-06-16 10:58:15 +02:00
parent 85af9d7078
commit 9f17d4d1e1
9 changed files with 356 additions and 13 deletions

View File

@@ -46,8 +46,12 @@ type ChatAskResult = {
usedTransactions: number;
usedBalance: { income: number; expenses: number; balance: number };
toolTrace: ToolTrace[];
sources: ChatSource[];
citations: ChatCitation[];
};
type ToolTrace = { name: string; inputSummary: string; resultSummary: string };
type ChatSource = { id: string; title: string; description?: string };
type ChatCitation = { marker: string; sourceId: string };
type TransactionTypeFilter = "income" | "expense";
type CategoryFilterStatus = "resolved" | "unresolved" | "ambiguous";
type CategoryFilterDiagnostic = {
@@ -287,6 +291,15 @@ const toolTraceValidator = v.object({
inputSummary: v.string(),
resultSummary: v.string(),
});
const sourceValidator = v.object({
id: v.string(),
title: v.string(),
description: v.optional(v.string()),
});
const citationValidator = v.object({
marker: v.string(),
sourceId: v.string(),
});
const toolScopeValidator = v.object(contextArgsValidator);
@@ -1831,6 +1844,29 @@ export function buildToolTraceFromSteps(steps: unknown[]): ToolTrace[] {
return trace;
}
export function buildSourcesFromToolTrace(toolTrace: ToolTrace[]): ChatSource[] {
return toolTrace.map((trace, index) => ({
id: `tool-${index + 1}`,
title: trace.name,
description: trace.resultSummary,
}));
}
export function buildCitationsFromSources(sources: ChatSource[]): ChatCitation[] {
return sources.map((source, index) => ({
marker: `${index + 1}`,
sourceId: source.id,
}));
}
function appendMissingCitationMarkers(answer: string, citations: ChatCitation[]): string {
const missingMarkers = citations
.map((citation) => citation.marker)
.filter((marker) => !answer.includes(`[${marker}]`));
if (missingMarkers.length === 0) return answer;
return `${answer.trimEnd()} ${missingMarkers.map((marker) => `[${marker}]`).join(" ")}`;
}
const transactionToolInputSchema = z.object({
from: z.string().optional().describe("Optionales Startdatum im Format YYYY-MM-DD."),
to: z.string().optional().describe("Optionales Enddatum im Format YYYY-MM-DD."),
@@ -2073,16 +2109,21 @@ async function generateSavingsChatResponse(
tools: savingsTools,
stopWhen: stepCountIs(5),
});
const toolTrace = buildToolTraceFromSteps(result.steps);
const sources = buildSourcesFromToolTrace(toolTrace);
const citations = buildCitationsFromSources(sources);
return {
model: modelName,
answer: result.text,
answer: appendMissingCitationMarkers(result.text, citations),
usedTransactions: selectedSummary.totals.transactionCount,
usedBalance: {
income: selectedSummary.totals.income,
expenses: selectedSummary.totals.expenses,
balance: selectedSummary.totals.balance,
},
toolTrace: buildToolTraceFromSteps(result.steps),
toolTrace,
sources,
citations,
};
} catch (error) {
lastError = error;
@@ -2114,6 +2155,8 @@ export const ask = action({
balance: v.number(),
}),
toolTrace: v.array(toolTraceValidator),
sources: v.array(sourceValidator),
citations: v.array(citationValidator),
}),
handler: async (ctx, args): Promise<ChatAskResult> => {
return await generateSavingsChatResponse(ctx, {
@@ -2145,6 +2188,8 @@ export const sendMessage = action({
balance: v.number(),
}),
toolTrace: v.array(toolTraceValidator),
sources: v.array(sourceValidator),
citations: v.array(citationValidator),
}),
handler: async (ctx, args): Promise<ChatAskResult> => {
const content = args.content.trim();
@@ -2183,6 +2228,8 @@ export const sendMessage = action({
sessionId: args.sessionId,
content: response.answer,
toolTrace: response.toolTrace,
sources: response.sources,
citations: response.citations,
});
return response;
},