Files
adventure/apps/web/src/lib/services/ai-chat.ts
Zaine acea7dcf12
Some checks failed
CI / test (push) Has been cancelled
fix the memory adding
2026-06-26 16:32:34 +01:00

198 lines
6.3 KiB
TypeScript

import { and, eq, desc, isNull } from "drizzle-orm";
import { db, aiChatSessions, aiChatMessages } from "../db";
import { buildMentorContext, formatContextForPrompt } from "./ai-context";
import { generateChatReply, getAiAvailability } from "./ai";
import {
formatLibraryContextForPrompt,
getLibraryContextForTopic,
shouldIncludeLibraryContextInChat,
} from "./library-context";
import { listMemories, getProfileSummary } from "./ai-memory";
import { MEMORY_CATEGORIES, type MemoryCategory } from "@adventureos/shared";
import { extractMemoryCandidates } from "./memory-extraction";
import { buildMemoryActionReply, performChatMemoryAction } from "./chat-memory-actions";
export async function listChatSessions(userId: string) {
return db
.select()
.from(aiChatSessions)
.where(and(eq(aiChatSessions.userId, userId), isNull(aiChatSessions.archivedAt)))
.orderBy(desc(aiChatSessions.updatedAt))
.limit(20);
}
export async function createChatSession(
userId: string,
title?: string,
featureContext?: Record<string, unknown>
) {
const [session] = await db
.insert(aiChatSessions)
.values({
userId,
title: title ?? "New conversation",
featureContext: featureContext ?? {},
})
.returning();
return session;
}
export async function getChatSession(userId: string, sessionId: string) {
const [session] = await db
.select()
.from(aiChatSessions)
.where(and(eq(aiChatSessions.userId, userId), eq(aiChatSessions.id, sessionId)));
if (!session) return null;
const messages = await db
.select()
.from(aiChatMessages)
.where(eq(aiChatMessages.sessionId, sessionId))
.orderBy(aiChatMessages.createdAt);
return { session, messages };
}
export async function sendChatMessage(userId: string, sessionId: string, content: string) {
const sessionData = await getChatSession(userId, sessionId);
if (!sessionData) throw new Error("Session not found");
const { session, messages } = sessionData;
const [userMsg] = await db
.insert(aiChatMessages)
.values({
sessionId,
role: "user",
content: content.trim(),
})
.returning();
const memoryAction = await performChatMemoryAction(userId, sessionId, userMsg, messages);
const authoritativeMemoryReply = buildMemoryActionReply(memoryAction);
const availability = await getAiAvailability(userId);
const ctx = await buildMentorContext(userId, {
userMessage: content,
feature: "mentor",
activeDate: (session.featureContext as Record<string, unknown>)?.date as string | undefined,
logContext: true,
});
let contextBlock = formatContextForPrompt(ctx);
if (shouldIncludeLibraryContextInChat(content)) {
try {
const libraryPayload = await getLibraryContextForTopic(userId, content);
contextBlock = `${contextBlock}\n\n${formatLibraryContextForPrompt(libraryPayload)}`;
} catch {
/* library optional */
}
}
let reply: string;
let offline = false;
const memoryIdsUsed = ctx.memoryIds;
if (authoritativeMemoryReply) {
reply = authoritativeMemoryReply;
} else if (!availability.canUse || !availability.online) {
offline = true;
reply = buildOfflineReply(content, ctx);
} else {
const history = messages
.slice(-6)
.map((m) => `${m.role}: ${m.content}`)
.join("\n");
const generated = await generateChatReply(userId, {
userMessage: content,
contextBlock,
history,
memoryAction: JSON.stringify(memoryAction),
});
reply =
generated ??
"I'm having trouble responding right now. Your memories are still saved — try again when the AI is online.";
if (!generated) offline = true;
}
const [assistantMsg] = await db
.insert(aiChatMessages)
.values({
sessionId,
role: "assistant",
content: reply,
metadata: { memoryIdsUsed, offline, memoryAction },
})
.returning();
const memorySuggestions =
memoryAction.intent === "ordinary_message"
? await extractMemoryCandidates(userId, "chat", content, {
type: "ai_chat",
id: userMsg.id,
})
: memoryAction.type === "memory_suggestion_created"
? [
{
id: memoryAction.id,
title: memoryAction.title,
category: memoryAction.category,
status: memoryAction.status,
},
]
: [];
await db
.update(aiChatSessions)
.set({ updatedAt: new Date(), title: session.title === "New conversation" ? truncateTitle(content) : session.title })
.where(eq(aiChatSessions.id, sessionId));
return { message: assistantMsg, reply, memoryIdsUsed, offline, memorySuggestions, memoryAction };
}
function truncateTitle(text: string): string {
const t = text.trim();
return t.length > 40 ? `${t.slice(0, 37)}` : t;
}
function buildOfflineReply(message: string, ctx: Awaited<ReturnType<typeof buildMentorContext>>): string {
const lower = message.toLowerCase();
if (lower.includes("what do you know") || lower.includes("about me")) {
const memLines = ctx.memories.map((m) => `${m.title}: ${m.content}`).join("\n");
return [
"The AI is offline, but here is what I have saved locally:",
ctx.profileSummary ? `\nSummary: ${ctx.profileSummary}` : "",
memLines ? `\nMemories:\n${memLines}` : "\nNo memories saved yet.",
"\nYou can edit these in Settings → AI Memory.",
].join("");
}
return "The mentor is offline right now. Your adventure data is still here — try again when AI is online, or check Settings → AI Memory.";
}
export async function archiveChatSession(userId: string, sessionId: string) {
await db
.update(aiChatSessions)
.set({ archivedAt: new Date() })
.where(and(eq(aiChatSessions.userId, userId), eq(aiChatSessions.id, sessionId)));
}
export async function getWhatAiKnows(userId: string) {
const summary = await getProfileSummary(userId);
const memories = await listMemories(userId, { enabled: true });
const grouped: Record<string, typeof memories> = {};
for (const m of memories) {
if (!grouped[m.category]) grouped[m.category] = [];
grouped[m.category].push(m);
}
return {
summary: summary?.summary ?? "",
grouped: Object.entries(grouped).map(([category, items]) => ({
category,
label: MEMORY_CATEGORIES[category as MemoryCategory] ?? category,
items,
})),
};
}