chore: clean up repo for public release
- Remove BMAD framework, IDE configs, dev screenshots, test files, internal docs, and backup files - Rename keep-notes/ to memento-note/ - Update all references from keep-notes to memento-note - Add Apache 2.0 license with Commons Clause (non-commercial restriction) - Add clean .gitignore and .env.docker.example
This commit is contained in:
297
memento-note/lib/ai/services/notebook-suggestion.service.ts
Normal file
297
memento-note/lib/ai/services/notebook-suggestion.service.ts
Normal file
@@ -0,0 +1,297 @@
|
||||
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, language: string = 'en'): 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, language)
|
||||
|
||||
// 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 (localized)
|
||||
*/
|
||||
private buildPrompt(noteContent: string, notebooks: any[], language: string = 'en'): 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')
|
||||
|
||||
const instructions: Record<string, string> = {
|
||||
fr: `
|
||||
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(),
|
||||
en: `
|
||||
You are an assistant that suggests which notebook a note should belong to.
|
||||
|
||||
NOTE CONTENT:
|
||||
${noteContent.substring(0, 500)}
|
||||
|
||||
AVAILABLE NOTEBOOKS:
|
||||
${notebookList}
|
||||
|
||||
TASK:
|
||||
Analyze the note content (regardless of language) and suggest the MOST appropriate notebook for this note.
|
||||
Consider:
|
||||
1. The subject/theme of the note (MOST IMPORTANT)
|
||||
2. Existing labels in each notebook
|
||||
3. The number of notes (prefer notebooks with related content)
|
||||
|
||||
CLASSIFICATION GUIDE:
|
||||
- SPORT/EXERCISE/SHOPPING/GROCERIES → Personal Notebook
|
||||
- HOBBIES/PASSIONS/OUTINGS → Personal Notebook
|
||||
- HEALTH/FITNESS/DOCTOR → Personal Notebook or Health
|
||||
- FAMILY/FRIENDS → Personal Notebook
|
||||
- WORK/MEETINGS/PROJECTS/CLIENTS → Work Notebook
|
||||
- CODING/TECH/DEVELOPMENT → Work Notebook or Code
|
||||
- FINANCE/BILLS/BANKING → Personal Notebook or Finance
|
||||
|
||||
RULES:
|
||||
- Return ONLY the notebook name, EXACTLY as listed above (case insensitive)
|
||||
- If no good match exists, return "NONE"
|
||||
- If the note is too generic/vague, return "NONE"
|
||||
- Do not include explanations or extra text
|
||||
|
||||
Examples:
|
||||
- "Meeting with John about project planning" → notebook "Work"
|
||||
- "Grocery list or buying clothes" → notebook "Personal"
|
||||
- "Python script for data analysis" → notebook "Code"
|
||||
- "Gym session or fitness" → notebook "Personal"
|
||||
|
||||
Your suggestion:
|
||||
`.trim(),
|
||||
fa: `
|
||||
شما یک دستیار هستید که پیشنهاد میدهد یک یادداشت به کدام دفترچه تعلق داشته باشد.
|
||||
|
||||
محتوای یادداشت:
|
||||
${noteContent.substring(0, 500)}
|
||||
|
||||
دفترچههای موجود:
|
||||
${notebookList}
|
||||
|
||||
وظیفه:
|
||||
محتوای یادداشت را تحلیل کنید (صرف نظر از زبان) و مناسبترین دفترچه را برای این یادداشت پیشنهاد دهید.
|
||||
در نظر بگیرید:
|
||||
1. موضوع/تم یادداشت (مهمترین)
|
||||
2. برچسبهای موجود در هر دفترچه
|
||||
3. تعداد یادداشتها (دفترچههای با محتوای مرتبط را ترجیح دهید)
|
||||
|
||||
راهنمای طبقهبندی:
|
||||
- ورزش/تمرین/خرید → دفترچه شخصی
|
||||
- سرگرمیها/علایق/گردش → دفترچه شخصی
|
||||
- سلامت/تناسب اندام/پزشک → دفترچه شخصی یا سلامت
|
||||
- خانواده/دوستان → دفترچه شخصی
|
||||
- کار/جلسات/پروژهها/مشتریان → دفترچه کار
|
||||
- کدنویسی/تکنولوژی/توسعه → دفترچه کار یا کد
|
||||
- مالی/قبضها/بانک → دفترچه شخصی یا مالی
|
||||
|
||||
قوانین:
|
||||
- فقط نام دفترچه را برگردانید، دقیقاً همانطور که در بالا ذکر شده است (بدون حساسیت به حروف بزرگ و کوچک)
|
||||
- اگر تطابق خوبی وجود ندارد، "NONE" را برگردانید
|
||||
- اگر یادداشت خیلی کلی/مبهم است، "NONE" را برگردانید
|
||||
- توضیحات یا متن اضافی را شامل نکنید
|
||||
|
||||
پیشناد شما:
|
||||
`.trim(),
|
||||
es: `
|
||||
Eres un asistente que sugiere a qué cuaderno debería pertenecer una nota.
|
||||
|
||||
CONTENIDO DE LA NOTA:
|
||||
${noteContent.substring(0, 500)}
|
||||
|
||||
CUADERNOS DISPONIBLES:
|
||||
${notebookList}
|
||||
|
||||
TAREA:
|
||||
Analiza el contenido de la nota (independientemente del idioma) y sugiere el cuaderno MÁS apropiado para esta nota.
|
||||
Considera:
|
||||
1. El tema/asunto de la nota (LO MÁS IMPORTANTE)
|
||||
2. Etiquetas existentes en cada cuaderno
|
||||
3. El número de notas (prefiere cuadernos con contenido relacionado)
|
||||
|
||||
GUÍA DE CLASIFICACIÓN:
|
||||
- DEPORTE/EJERCICIO/COMPRAS → Cuaderno Personal
|
||||
- HOBBIES/PASIONES/SALIDAS → Cuaderno Personal
|
||||
- SALUD/FITNESS/DOCTOR → Cuaderno Personal o Salud
|
||||
- FAMILIA/AMIGOS → Cuaderno Personal
|
||||
- TRABAJO/REUNIONES/PROYECTOS → Cuaderno Trabajo
|
||||
- CODING/TECH/DESARROLLO → Cuaderno Trabajo o Código
|
||||
- FINANZAS/FACTURAS/BANCO → Cuaderno Personal o Finanzas
|
||||
|
||||
REGLAS:
|
||||
- Devuelve SOLO el nombre del cuaderno, EXACTAMENTE como se lista arriba (insensible a mayúsculas/minúsculas)
|
||||
- Si no existe una buena coincidencia, devuelve "NONE"
|
||||
- Si la nota es demasiado genérica/vaga, devuelve "NONE"
|
||||
- No incluyas explicaciones o texto extra
|
||||
|
||||
Tu sugerencia:
|
||||
`.trim(),
|
||||
de: `
|
||||
Du bist ein Assistent, der vorschlägt, zu welchem Notizbuch eine Notiz gehören sollte.
|
||||
|
||||
NOTIZINHALT:
|
||||
${noteContent.substring(0, 500)}
|
||||
|
||||
VERFÜGBARE NOTIZBÜCHER:
|
||||
${notebookList}
|
||||
|
||||
AUFGABE:
|
||||
Analysiere den Notizinhalt (unabhängig von der Sprache) und schlage das AM BESTEN geeignete Notizbuch für diese Notiz vor.
|
||||
Berücksichtige:
|
||||
1. Das Thema/den Inhalt der Notiz (AM WICHTIGSTEN)
|
||||
2. Vorhandene Labels in jedem Notizbuch
|
||||
3. Die Anzahl der Notizen (bevorzuge Notizbücher mit verwandtem Inhalt)
|
||||
|
||||
KLASSIFIZIERUNGSLEITFADEN:
|
||||
- SPORT/ÜBUNG/EINKAUFEN → Persönliches Notizbuch
|
||||
- HOBBYS/LEIDENSCHAFTEN → Persönliches Notizbuch
|
||||
- GESUNDHEIT/FITNESS/ARZT → Persönliches Notizbuch oder Gesundheit
|
||||
- FAMILIE/FREUNDE → Persönliches Notizbuch
|
||||
- ARBEIT/MEETINGS/PROJEKTE → Arbeitsnotizbuch
|
||||
- CODING/TECH/ENTWICKLUNG → Arbeitsnotizbuch oder Code
|
||||
- FINANZEN/RECHNUNGEN/BANK → Persönliches Notizbuch oder Finanzen
|
||||
|
||||
REGELN:
|
||||
- Gib NUR den Namen des Notizbuchs zurück, GENAU wie oben aufgeführt (Groß-/Kleinschreibung egal)
|
||||
- Wenn keine gute Übereinstimmung existiert, gib "NONE" zurück
|
||||
- Wenn die Notiz zu allgemein/vage ist, gib "NONE" zurück
|
||||
- Füge keine Erklärungen oder zusätzlichen Text hinzu
|
||||
|
||||
Dein Vorschlag:
|
||||
`.trim()
|
||||
}
|
||||
|
||||
return instructions[language] || instructions['en'] || instructions['fr']
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
language: string = 'en'
|
||||
): 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, language)
|
||||
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()
|
||||
Reference in New Issue
Block a user