Files
finanzen/convex/savingsChatHistory.ts

267 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { paginationOptsValidator, paginationResultValidator } from "convex/server";
import { v } from "convex/values";
import { internalMutation, internalQuery, mutation, query } from "./_generated/server";
import type { Id } from "./_generated/dataModel";
import type { MutationCtx, QueryCtx } from "./_generated/server";
import { assertOwned, requireUserId } from "./lib/helpers";
const initialAssistantMessage =
"Frag mich zu deinen Umsätzen ich werte sie im aktuellen Zeitraum aus.";
const toolTraceValidator = v.object({
name: v.string(),
inputSummary: v.string(),
resultSummary: v.string(),
});
const chatRoleValidator = v.union(v.literal("user"), v.literal("assistant"));
const importMessageValidator = v.object({
role: chatRoleValidator,
content: v.string(),
toolTrace: v.optional(v.array(toolTraceValidator)),
});
const sessionValidator = v.object({
_id: v.id("chatSessions"),
_creationTime: v.number(),
userId: v.id("users"),
title: v.string(),
createdAt: v.number(),
updatedAt: v.number(),
messageCount: v.number(),
legacyLocalId: v.optional(v.string()),
isDeleted: v.boolean(),
});
const messageValidator = v.object({
_id: v.id("chatMessages"),
_creationTime: v.number(),
userId: v.id("users"),
sessionId: v.id("chatSessions"),
role: chatRoleValidator,
content: v.string(),
createdAt: v.number(),
toolTrace: v.optional(v.array(toolTraceValidator)),
});
const promptMessageValidator = v.object({
role: chatRoleValidator,
content: v.string(),
});
function normalizeTitle(title: string | undefined) {
const trimmed = title?.trim();
return trimmed ? trimmed : "Neuer Chat";
}
function titleFromContent(content: string) {
const trimmed = content.trim();
if (!trimmed) return "Neuer Chat";
return trimmed.length > 44 ? `${trimmed.slice(0, 44)}` : trimmed;
}
async function requireOwnedSession(
ctx: QueryCtx | MutationCtx,
sessionId: Id<"chatSessions">,
userId: Id<"users">,
) {
const session = await assertOwned(await ctx.db.get(sessionId), userId, "Chat");
if (session.isDeleted) throw new Error("Chat nicht gefunden");
return session;
}
export const listSessions = query({
args: { paginationOpts: paginationOptsValidator },
returns: paginationResultValidator(sessionValidator),
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
return await ctx.db
.query("chatSessions")
.withIndex("by_user_deleted_updated", (index) =>
index.eq("userId", userId).eq("isDeleted", false),
)
.order("desc")
.paginate(args.paginationOpts);
},
});
export const listMessages = query({
args: {
sessionId: v.id("chatSessions"),
paginationOpts: paginationOptsValidator,
},
returns: paginationResultValidator(messageValidator),
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
await requireOwnedSession(ctx, args.sessionId, userId);
return await ctx.db
.query("chatMessages")
.withIndex("by_user_session_created", (index) =>
index.eq("userId", userId).eq("sessionId", args.sessionId),
)
.order("desc")
.paginate(args.paginationOpts);
},
});
export const createSession = mutation({
args: { title: v.optional(v.string()) },
returns: v.object({ sessionId: v.id("chatSessions") }),
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const now = Date.now();
const sessionId = await ctx.db.insert("chatSessions", {
userId,
title: normalizeTitle(args.title),
createdAt: now,
updatedAt: now,
messageCount: 1,
isDeleted: false,
});
await ctx.db.insert("chatMessages", {
userId,
sessionId,
role: "assistant",
content: initialAssistantMessage,
createdAt: now,
});
return { sessionId };
},
});
export const deleteSession = mutation({
args: { sessionId: v.id("chatSessions") },
returns: v.null(),
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
await requireOwnedSession(ctx, args.sessionId, userId);
await ctx.db.patch(args.sessionId, {
isDeleted: true,
updatedAt: Date.now(),
});
return null;
},
});
export const importLocalSession = mutation({
args: {
legacyLocalId: v.string(),
title: v.string(),
createdAt: v.number(),
updatedAt: v.number(),
messages: v.array(importMessageValidator),
},
returns: v.object({ sessionId: v.id("chatSessions") }),
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const existing = await ctx.db
.query("chatSessions")
.withIndex("by_user_legacyLocalId", (index) =>
index.eq("userId", userId).eq("legacyLocalId", args.legacyLocalId),
)
.unique();
if (existing) return { sessionId: existing._id };
const sessionId = await ctx.db.insert("chatSessions", {
userId,
title: normalizeTitle(args.title),
createdAt: args.createdAt,
updatedAt: args.updatedAt,
messageCount: args.messages.length,
legacyLocalId: args.legacyLocalId,
isDeleted: false,
});
for (const [index, message] of args.messages.entries()) {
await ctx.db.insert("chatMessages", {
userId,
sessionId,
role: message.role,
content: message.content,
createdAt: args.createdAt + index,
...(message.toolTrace ? { toolTrace: message.toolTrace } : {}),
});
}
return { sessionId };
},
});
export const appendUserMessage = internalMutation({
args: {
sessionId: v.id("chatSessions"),
content: v.string(),
},
returns: v.object({ messageId: v.id("chatMessages") }),
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const session = await requireOwnedSession(ctx, args.sessionId, userId);
const now = Date.now();
const messageId = await ctx.db.insert("chatMessages", {
userId,
sessionId: args.sessionId,
role: "user",
content: args.content,
createdAt: now,
});
await ctx.db.patch(args.sessionId, {
title: session.title === "Neuer Chat" ? titleFromContent(args.content) : session.title,
updatedAt: now,
messageCount: session.messageCount + 1,
});
return { messageId };
},
});
export const appendAssistantMessage = internalMutation({
args: {
sessionId: v.id("chatSessions"),
content: v.string(),
toolTrace: v.optional(v.array(toolTraceValidator)),
},
returns: v.object({ messageId: v.id("chatMessages") }),
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const session = await requireOwnedSession(ctx, args.sessionId, userId);
const now = Date.now();
const messageId = await ctx.db.insert("chatMessages", {
userId,
sessionId: args.sessionId,
role: "assistant",
content: args.content,
createdAt: now,
...(args.toolTrace ? { toolTrace: args.toolTrace } : {}),
});
await ctx.db.patch(args.sessionId, {
updatedAt: now,
messageCount: session.messageCount + 1,
});
return { messageId };
},
});
export const getRecentMessagesForPrompt = internalQuery({
args: {
sessionId: v.id("chatSessions"),
limit: v.number(),
},
returns: v.array(promptMessageValidator),
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
await requireOwnedSession(ctx, args.sessionId, userId);
const limit = Math.max(1, Math.min(50, Math.floor(args.limit)));
const messages = await ctx.db
.query("chatMessages")
.withIndex("by_user_session_created", (index) =>
index.eq("userId", userId).eq("sessionId", args.sessionId),
)
.order("desc")
.take(limit);
return messages.reverse().map((message) => ({
role: message.role,
content: message.content,
}));
},
});