Keep/keep-notes/lib/ai/services/notebook-suggestion.service.ts
sepehr 3dd48e248c fix: 8 AI services were not using configured provider
CRITICAL FIX: Auto-labels, notebook summaries, and other AI features
were not working because 8 services were calling getAIProvider() WITHOUT
passing the config parameter.

This caused them to use the default 'ollama' provider instead of
the configured OpenAI provider from the database.

ROOT CAUSE ANALYSIS:
Working features (titles):
- title-suggestions/route.ts: getAIProvider(config) ✓

Broken features (auto-labels, summaries):
- contextual-auto-tag.service.ts: getAIProvider() ✗ (2x)
- notebook-summary.service.ts: getAIProvider() ✗
- auto-label-creation.service.ts: getAIProvider() ✗
- notebook-suggestion.service.ts: getAIProvider() ✗
- batch-organization.service.ts: getAIProvider() ✗
- embedding.service.ts: getAIProvider() ✗ (2x)

FIXED: All 8 services now properly call:
  const config = await getSystemConfig()
  const provider = getAIProvider(config)

This ensures ALL AI features use the provider configured in the admin
interface (OpenAI) instead of defaulting to Ollama.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-12 23:34:16 +01:00

155 lines
4.9 KiB
TypeScript

import { prisma } from '@/lib/prisma'
import { getAIProvider } from '@/lib/ai/factory'
import { getSystemConfig } from '@/lib/config'
import type { Notebook } from '@/lib/types'
export class NotebookSuggestionService {
/**
* Suggest the most appropriate notebook for a note
* @param noteContent - Content of the note
* @param userId - User ID (for fetching user's notebooks)
* @returns Suggested notebook or null (if no good match)
*/
async suggestNotebook(noteContent: string, userId: string): Promise<Notebook | null> {
// 1. Get all notebooks for this user
const notebooks = await prisma.notebook.findMany({
where: { userId },
include: {
labels: true,
_count: {
select: { notes: true },
},
},
orderBy: { order: 'asc' },
})
if (notebooks.length === 0) {
return null // No notebooks to suggest
}
// 2. Build prompt for AI (always in French - interface language)
const prompt = this.buildPrompt(noteContent, notebooks)
// 3. Call AI
try {
const config = await getSystemConfig()
const provider = getAIProvider(config)
const response = await provider.generateText(prompt)
const suggestedName = response.trim().toUpperCase()
// 5. Find matching notebook
const suggestedNotebook = notebooks.find(nb =>
nb.name.toUpperCase() === suggestedName
)
// If AI says "NONE" or no match, return null
if (suggestedName === 'NONE' || !suggestedNotebook) {
return null
}
return suggestedNotebook as Notebook
} catch (error) {
console.error('Failed to suggest notebook:', error)
return null
}
}
/**
* Build the AI prompt for notebook suggestion (always in French - interface language)
*/
private buildPrompt(noteContent: string, notebooks: any[]): string {
const notebookList = notebooks
.map(nb => {
const labels = nb.labels.map((l: any) => l.name).join(', ')
const count = nb._count?.notes || 0
return `- ${nb.name} (${count} notes)${labels ? ` [labels: ${labels}]` : ''}`
})
.join('\n')
return `
Tu es un assistant qui suggère à quel carnet une note devrait appartenir.
CONTENU DE LA NOTE :
${noteContent.substring(0, 500)}
CARNETS DISPONIBLES :
${notebookList}
TÂCHE :
Analyse le contenu de la note (peu importe la langue) et suggère le carnet le PLUS approprié pour cette note.
Considère :
1. Le sujet/thème de la note (LE PLUS IMPORTANT)
2. Les labels existants dans chaque carnet
3. Le nombre de notes (préfère les carnets avec du contenu connexe)
GUIDES DE CLASSIFICATION :
- SPORT/EXERCICE/ACHATS/COURSSES → Carnet Personnel
- LOISIRS/PASSIONS/SORTIES → Carnet Personnel
- SANTÉ/FITNESS/MÉDECIN → Carnet Personnel ou Santé
- FAMILLE/AMIS → Carnet Personnel
- TRAVAIL/RÉUNIONS/PROJETS/CLIENTS → Carnet Travail
- CODING/TECH/DÉVELOPPEMENT → Carnet Travail ou Code
- FINANCES/FACTURES/BANQUE → Carnet Personnel ou Finances
RÈGLES :
- Retourne SEULEMENT le nom du carnet, EXACTEMENT comme indiqué ci-dessus (insensible à la casse)
- Si aucune bonne correspondance n'existe, retourne "NONE"
- Si la note est trop générique/vague, retourne "NONE"
- N'inclus pas d'explications ou de texte supplémentaire
Exemples :
- "Réunion avec Jean sur le planning du projet" → carnet "Travail"
- "Liste de courses ou achat de vêtements" → carnet "Personnel"
- "Script Python pour analyse de données" → carnet "Code"
- "Séance de sport ou fitness" → carnet "Personnel"
- "Achat d'une chemise et d'un jean" → carnet "Personnel"
Ta suggestion :
`.trim()
}
/**
* Batch suggest notebooks for multiple notes (IA3)
* @param noteContents - Array of note contents
* @param userId - User ID
* @returns Map of note index -> suggested notebook
*/
async suggestNotebooksBatch(
noteContents: string[],
userId: string
): Promise<Map<number, Notebook | null>> {
const results = new Map<number, Notebook | null>()
// For efficiency, we could batch this into a single AI call
// For now, process sequentially (could be parallelized)
for (let i = 0; i < noteContents.length; i++) {
const suggestion = await this.suggestNotebook(noteContents[i], userId)
results.set(i, suggestion)
}
return results
}
/**
* Get notebook suggestion confidence score
* (For future UI enhancement: show confidence level)
*/
async suggestNotebookWithConfidence(
noteContent: string,
userId: string
): Promise<{ notebook: Notebook | null; confidence: number }> {
// This could use logprobs from OpenAI API to calculate confidence
// For now, return binary confidence
const notebook = await this.suggestNotebook(noteContent, userId)
return {
notebook,
confidence: notebook ? 0.8 : 0, // Fixed confidence for now
}
}
}
// Export singleton instance
export const notebookSuggestionService = new NotebookSuggestionService()