Add chat sources and citations
This commit is contained in:
@@ -253,6 +253,14 @@ describe("savingsChatHistory", () => {
|
||||
resultSummary: "2 Umsätze, Saldo 100.00€, 1 Kategorien",
|
||||
},
|
||||
],
|
||||
sources: [
|
||||
{
|
||||
id: "tool-1",
|
||||
title: "summarize_spending",
|
||||
description: "2 Umsätze, Saldo 100.00€, 1 Kategorien",
|
||||
},
|
||||
],
|
||||
citations: [{ marker: "1", sourceId: "tool-1" }],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -282,6 +290,14 @@ describe("savingsChatHistory", () => {
|
||||
resultSummary: "2 Umsätze, Saldo 100.00€, 1 Kategorien",
|
||||
},
|
||||
],
|
||||
sources: [
|
||||
{
|
||||
id: "tool-1",
|
||||
title: "summarize_spending",
|
||||
description: "2 Umsätze, Saldo 100.00€, 1 Kategorien",
|
||||
},
|
||||
],
|
||||
citations: [{ marker: "1", sourceId: "tool-1" }],
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -440,8 +456,24 @@ describe("savingsChat.sendMessage", () => {
|
||||
basis: "effective",
|
||||
});
|
||||
|
||||
expect(result.answer).toBe("Agenten-Antwort");
|
||||
expect(result.answer).toBe("Agenten-Antwort [1] [2]");
|
||||
expect(result.toolTrace).toHaveLength(2);
|
||||
expect(result.sources).toEqual([
|
||||
{
|
||||
id: "tool-1",
|
||||
title: "get_transactions",
|
||||
description: "2 Umsätze, Saldo 2880.00€, vollständig",
|
||||
},
|
||||
{
|
||||
id: "tool-2",
|
||||
title: "summarize_spending",
|
||||
description: "2 Umsätze, Saldo 2880.00€, 1 Kategorien",
|
||||
},
|
||||
]);
|
||||
expect(result.citations).toEqual([
|
||||
{ marker: "1", sourceId: "tool-1" },
|
||||
{ marker: "2", sourceId: "tool-2" },
|
||||
]);
|
||||
|
||||
const generateCall = vi.mocked(generateText).mock.calls[0][0] as {
|
||||
messages: Array<{ role: string; content: string }>;
|
||||
@@ -461,7 +493,7 @@ describe("savingsChat.sendMessage", () => {
|
||||
{ role: "user", content: "Wie sieht Februar aus?" },
|
||||
{
|
||||
role: "assistant",
|
||||
content: "Agenten-Antwort",
|
||||
content: "Agenten-Antwort [1] [2]",
|
||||
toolTrace: [
|
||||
{
|
||||
name: "get_transactions",
|
||||
@@ -474,6 +506,22 @@ describe("savingsChat.sendMessage", () => {
|
||||
resultSummary: "2 Umsätze, Saldo 2880.00€, 1 Kategorien",
|
||||
},
|
||||
],
|
||||
sources: [
|
||||
{
|
||||
id: "tool-1",
|
||||
title: "get_transactions",
|
||||
description: "2 Umsätze, Saldo 2880.00€, vollständig",
|
||||
},
|
||||
{
|
||||
id: "tool-2",
|
||||
title: "summarize_spending",
|
||||
description: "2 Umsätze, Saldo 2880.00€, 1 Kategorien",
|
||||
},
|
||||
],
|
||||
citations: [
|
||||
{ marker: "1", sourceId: "tool-1" },
|
||||
{ marker: "2", sourceId: "tool-2" },
|
||||
],
|
||||
},
|
||||
]);
|
||||
} finally {
|
||||
@@ -1382,7 +1430,7 @@ describe("savingsChat read-only agent tools", () => {
|
||||
basis: "effective",
|
||||
});
|
||||
|
||||
expect(result.answer).toBe("Agenten-Antwort");
|
||||
expect(result.answer).toBe("Agenten-Antwort [1] [2]");
|
||||
expect(result.model).toBe("gpt-5.4-mini");
|
||||
expect(result.usedTransactions).toBe(2);
|
||||
expect(result.usedBalance).toEqual({ income: 3000, expenses: -120, balance: 2880 });
|
||||
@@ -1398,6 +1446,22 @@ describe("savingsChat read-only agent tools", () => {
|
||||
resultSummary: "2 Umsätze, Saldo 2880.00€, 1 Kategorien",
|
||||
},
|
||||
]);
|
||||
expect(result.sources).toEqual([
|
||||
{
|
||||
id: "tool-1",
|
||||
title: "get_transactions",
|
||||
description: "2 Umsätze, Saldo 2880.00€, vollständig",
|
||||
},
|
||||
{
|
||||
id: "tool-2",
|
||||
title: "summarize_spending",
|
||||
description: "2 Umsätze, Saldo 2880.00€, 1 Kategorien",
|
||||
},
|
||||
]);
|
||||
expect(result.citations).toEqual([
|
||||
{ marker: "1", sourceId: "tool-1" },
|
||||
{ marker: "2", sourceId: "tool-2" },
|
||||
]);
|
||||
expect(JSON.stringify(result.toolTrace)).not.toContain("RAW PAYLOAD");
|
||||
expect(JSON.stringify(result.toolTrace)).not.toContain("private note");
|
||||
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -13,6 +13,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 chatRoleValidator = v.union(v.literal("user"), v.literal("assistant"));
|
||||
|
||||
@@ -20,6 +29,8 @@ const importMessageValidator = v.object({
|
||||
role: chatRoleValidator,
|
||||
content: v.string(),
|
||||
toolTrace: v.optional(v.array(toolTraceValidator)),
|
||||
sources: v.optional(v.array(sourceValidator)),
|
||||
citations: v.optional(v.array(citationValidator)),
|
||||
});
|
||||
|
||||
const sessionValidator = v.object({
|
||||
@@ -43,6 +54,8 @@ const messageValidator = v.object({
|
||||
content: v.string(),
|
||||
createdAt: v.number(),
|
||||
toolTrace: v.optional(v.array(toolTraceValidator)),
|
||||
sources: v.optional(v.array(sourceValidator)),
|
||||
citations: v.optional(v.array(citationValidator)),
|
||||
});
|
||||
|
||||
const promptMessageValidator = v.object({
|
||||
@@ -181,6 +194,8 @@ export const importLocalSession = mutation({
|
||||
content: message.content,
|
||||
createdAt: args.createdAt + index,
|
||||
...(message.toolTrace ? { toolTrace: message.toolTrace } : {}),
|
||||
...(message.sources ? { sources: message.sources } : {}),
|
||||
...(message.citations ? { citations: message.citations } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -219,6 +234,8 @@ export const appendAssistantMessage = internalMutation({
|
||||
sessionId: v.id("chatSessions"),
|
||||
content: v.string(),
|
||||
toolTrace: v.optional(v.array(toolTraceValidator)),
|
||||
sources: v.optional(v.array(sourceValidator)),
|
||||
citations: v.optional(v.array(citationValidator)),
|
||||
},
|
||||
returns: v.object({ messageId: v.id("chatMessages") }),
|
||||
handler: async (ctx, args) => {
|
||||
@@ -232,6 +249,8 @@ export const appendAssistantMessage = internalMutation({
|
||||
content: args.content,
|
||||
createdAt: now,
|
||||
...(args.toolTrace ? { toolTrace: args.toolTrace } : {}),
|
||||
...(args.sources ? { sources: args.sources } : {}),
|
||||
...(args.citations ? { citations: args.citations } : {}),
|
||||
});
|
||||
await ctx.db.patch(args.sessionId, {
|
||||
updatedAt: now,
|
||||
|
||||
@@ -15,6 +15,15 @@ const chatToolTrace = v.object({
|
||||
inputSummary: v.string(),
|
||||
resultSummary: v.string(),
|
||||
});
|
||||
const chatSource = v.object({
|
||||
id: v.string(),
|
||||
title: v.string(),
|
||||
description: v.optional(v.string()),
|
||||
});
|
||||
const chatCitation = v.object({
|
||||
marker: v.string(),
|
||||
sourceId: v.string(),
|
||||
});
|
||||
|
||||
export default defineSchema({
|
||||
...authTables,
|
||||
@@ -201,5 +210,7 @@ export default defineSchema({
|
||||
content: v.string(),
|
||||
createdAt: v.number(),
|
||||
toolTrace: v.optional(v.array(chatToolTrace)),
|
||||
sources: v.optional(v.array(chatSource)),
|
||||
citations: v.optional(v.array(chatCitation)),
|
||||
}).index("by_user_session_created", ["userId", "sessionId", "createdAt"]),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user