/** * Chat Service * Handles conversational AI with context retrieval (RAG) */ import { semanticSearchService } from './semantic-search.service' import { getChatProvider } from '../factory' import { getSystemConfig } from '@/lib/config' import { prisma } from '@/lib/prisma' import { auth } from '@/auth' import { loadTranslations, getTranslationValue, SupportedLanguage } from '@/lib/i18n' // Default untitled text for fallback const DEFAULT_UNTITLED = 'Untitled' export interface ChatMessage { role: 'user' | 'assistant' | 'system' content: string } export interface ChatResponse { message: string conversationId?: string suggestedNotes?: Array<{ id: string; title: string }> } export class ChatService { /** * Main chat entry point with context retrieval */ async chat( message: string, conversationId?: string, notebookId?: string, language: SupportedLanguage = 'en' ): Promise { const session = await auth() if (!session?.user?.id) { throw new Error('Unauthorized') } const userId = session.user.id // Load translations for the requested language const translations = await loadTranslations(language) const untitledText = getTranslationValue(translations, 'notes.untitled') || DEFAULT_UNTITLED const noNotesFoundText = getTranslationValue(translations, 'chat.noNotesFoundForContext') || 'No relevant notes found for this question. Answer with your general knowledge.' // 1. Manage Conversation let conversation: any if (conversationId) { conversation = await prisma.conversation.findUnique({ where: { id: conversationId }, include: { messages: { orderBy: { createdAt: 'asc' }, take: 10 } } }) } if (!conversation) { conversation = await prisma.conversation.create({ data: { userId, notebookId, title: message.substring(0, 50) + '...' }, include: { messages: true } }) } // 2. Retrieval (RAG) // We search for relevant notes based on the current message or notebook context // Lower threshold for notebook-specific searches to ensure we find relevant content const searchResults = await semanticSearchService.search(message, { notebookId, limit: 10, threshold: notebookId ? 0.3 : 0.5 }) const contextNotes = searchResults.map(r => `NOTE [${r.title || untitledText}]: ${r.content}` ).join('\n\n---\n\n') // 3. System Prompt Synthesis const systemPrompt = `Tu es l'Assistant IA de Memento. Tu accompagnes l'utilisateur dans sa réflexion. Tes réponses doivent être concises, premium et utiles. ${contextNotes.length > 0 ? `Voici des extraits de notes de l'utilisateur qui pourraient t'aider à répondre :\n\n${contextNotes}\n\nUtilise ces informations si elles sont pertinentes, mais ne les cite pas mot pour mot sauf si demandé.` : noNotesFoundText} Si l'utilisateur pose une question sur un carnet spécifique, reste focalisé sur ce contexte.` // 4. Call AI Provider const history = (conversation.messages || []).map((m: any) => ({ role: m.role, content: m.content })) const currentMessages = [...history, { role: 'user', content: message }] const config = await getSystemConfig() const provider = getChatProvider(config) const aiResponse = await provider.chat(currentMessages, systemPrompt) // 5. Save Messages to DB await prisma.chatMessage.createMany({ data: [ { conversationId: conversation.id, role: 'user', content: message }, { conversationId: conversation.id, role: 'assistant', content: aiResponse.text } ] }) return { message: aiResponse.text, conversationId: conversation.id, suggestedNotes: searchResults.map(r => ({ id: r.noteId, title: r.title || untitledText })) } } /** * Get conversation history */ async getHistory(conversationId: string) { return prisma.conversation.findUnique({ where: { id: conversationId }, include: { messages: { orderBy: { createdAt: 'asc' } } } }) } /** * List user conversations */ async listConversations(userId: string) { return prisma.conversation.findMany({ where: { userId }, orderBy: { updatedAt: 'desc' }, take: 20 }) } } export const chatService = new ChatService()