142 lines
4.3 KiB
TypeScript
142 lines
4.3 KiB
TypeScript
/**
|
|
* 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<ChatResponse> {
|
|
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()
|