fix: auto-detect note language for label suggestions (like title suggestions)
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 44s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 44s
Use LanguageDetectionService (tinyld) to detect the language of note content, same pattern as TitleSuggestionService. No more hardcoded per-language prompts — single prompt with detected language injected. Labels now match the note content language (e.g. Persian note → Persian labels). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { getTagsProvider } from '@/lib/ai/factory'
|
||||
import { getSystemConfig } from '@/lib/config'
|
||||
import { LanguageDetectionService } from './language-detection.service'
|
||||
|
||||
export interface LabelSuggestion {
|
||||
label: string
|
||||
@@ -16,6 +17,12 @@ export interface LabelSuggestion {
|
||||
}
|
||||
|
||||
export class ContextualAutoTagService {
|
||||
private languageDetection: LanguageDetectionService
|
||||
|
||||
constructor() {
|
||||
this.languageDetection = new LanguageDetectionService()
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggest labels for a note
|
||||
* @param noteContent - Content of the note
|
||||
@@ -27,13 +34,16 @@ export class ContextualAutoTagService {
|
||||
noteContent: string,
|
||||
notebookId: string | null,
|
||||
userId: string,
|
||||
language: string = 'en'
|
||||
_uiLanguage: string = 'en'
|
||||
): Promise<LabelSuggestion[]> {
|
||||
// If no notebook, return empty (no context)
|
||||
if (!notebookId) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Detect the language of the note content (same as title-suggestion.service.ts)
|
||||
const { language: contentLanguage } = await this.languageDetection.detectLanguage(noteContent)
|
||||
|
||||
// Get notebook with its labels
|
||||
const notebook = await prisma.notebook.findFirst({
|
||||
where: {
|
||||
@@ -54,18 +64,18 @@ export class ContextualAutoTagService {
|
||||
return []
|
||||
}
|
||||
|
||||
console.log('[ContextualAutoTag] notebook:', notebook.name, 'labels:', notebook.labels.map((l: any) => l.name))
|
||||
console.log('[ContextualAutoTag] notebook:', notebook.name, 'labels:', notebook.labels.map((l: any) => l.name), 'contentLanguage:', contentLanguage)
|
||||
|
||||
// CASE 1: Notebook has existing labels → suggest from them (IA2)
|
||||
if (notebook.labels.length > 0) {
|
||||
const existing = await this.suggestFromExistingLabels(noteContent, notebook, language)
|
||||
const existing = await this.suggestFromExistingLabels(noteContent, notebook, contentLanguage)
|
||||
console.log('[ContextualAutoTag] existing label suggestions:', existing.length)
|
||||
if (existing.length > 0) return existing
|
||||
// Fallback: no existing label matched → suggest new ones too
|
||||
}
|
||||
|
||||
// CASE 2: No labels in notebook (or none matched) → suggest NEW labels
|
||||
return await this.suggestNewLabels(noteContent, notebook, language)
|
||||
return await this.suggestNewLabels(noteContent, notebook, contentLanguage)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -247,39 +257,10 @@ export class ContextualAutoTagService {
|
||||
/**
|
||||
* Build the AI prompt for contextual label suggestion (localized)
|
||||
*/
|
||||
private buildPrompt(noteContent: string, notebookName: string, availableLabels: string[], language: string = 'en'): string {
|
||||
private buildPrompt(noteContent: string, notebookName: string, availableLabels: string[], contentLanguage: string): string {
|
||||
const labelList = availableLabels.map(l => `- ${l}`).join('\n')
|
||||
|
||||
const instructions: Record<string, string> = {
|
||||
fr: `
|
||||
Tu es un assistant qui suggère des labels PERTINENTS pour une note.
|
||||
|
||||
CONTENU DE LA NOTE :
|
||||
${noteContent.substring(0, 1000)}
|
||||
|
||||
CARNET : ${notebookName}
|
||||
|
||||
LABELS DISPONIBLES :
|
||||
${labelList}
|
||||
|
||||
RÈGLES STRICTES :
|
||||
- Ne suggère un label QUE s'il est DIRECTEMENT en rapport avec le contenu de cette note précise
|
||||
- Un label "voyage" n'est pertinent QUE si la note parle de voyage
|
||||
- Un label "projet" n'est pertinent QUE si la note parle d'un projet spécifique
|
||||
- Ne suggère PAS un label juste parce qu'il existe dans le carnet
|
||||
- Maximum 2 suggestions
|
||||
- Confiance < 0.7 = ne pas suggérer
|
||||
- Si AUCUN label n'est clairement pertinent, retourne un tableau VIDE
|
||||
|
||||
Réponds en JSON :
|
||||
{"suggestions":[{"label":"nom","confidence":0.85,"reasoning":"pourquoi"}]}
|
||||
|
||||
Ta réponse :
|
||||
`.trim(),
|
||||
en: `
|
||||
You are an assistant that suggests RELEVANT labels for a note.
|
||||
|
||||
NOTE CONTENT:
|
||||
return `NOTE CONTENT (language: ${contentLanguage}):
|
||||
${noteContent.substring(0, 1000)}
|
||||
|
||||
NOTEBOOK: ${notebookName}
|
||||
@@ -287,11 +268,10 @@ NOTEBOOK: ${notebookName}
|
||||
AVAILABLE LABELS:
|
||||
${labelList}
|
||||
|
||||
STRICT RULES:
|
||||
RULES:
|
||||
- Only suggest a label if it is DIRECTLY related to the content of THIS specific note
|
||||
- A "travel" label is relevant ONLY if the note is about travel
|
||||
- A "project" label is relevant ONLY if the note discusses a specific project
|
||||
- Do NOT suggest a label just because it exists in the notebook
|
||||
- If the note content language (${contentLanguage}) differs from the available labels, only suggest a label if it clearly applies regardless of language
|
||||
- Maximum 2 suggestions
|
||||
- Confidence < 0.7 = do not suggest
|
||||
- If NO label is clearly relevant, return an EMPTY array
|
||||
@@ -299,252 +279,30 @@ STRICT RULES:
|
||||
Respond in JSON:
|
||||
{"suggestions":[{"label":"name","confidence":0.85,"reasoning":"why"}]}
|
||||
|
||||
Your response:
|
||||
`.trim(),
|
||||
fa: `
|
||||
تو دستیاری هستی که برچسبهای مرتبط برای یادداشت پیشنهاد میدهی.
|
||||
|
||||
محتوای یادداشت:
|
||||
${noteContent.substring(0, 1000)}
|
||||
|
||||
دفترچه: ${notebookName}
|
||||
|
||||
برچسبهای موجود:
|
||||
${labelList}
|
||||
|
||||
قوانین سختگیرانه:
|
||||
- فقط برچسبی را پیشنهاد بده که مستقیماً با محتوای همین یادداشت مرتبط باشد
|
||||
- برچسب "سفر" فقط وقتی مرتبط است که یادداشت درباره سفر باشد
|
||||
- حداکثر ۲ پیشنهاد
|
||||
- اطمینان < 0.7 = پیشنهاد نده
|
||||
- اگر هیچ برچسبی مرتبط نیست، آرایه خالی برگردان
|
||||
|
||||
پاسخ JSON:
|
||||
{"suggestions":[{"label":"نام","confidence":0.85,"reasoning":"چرا"}]}
|
||||
|
||||
پاسخ شما:
|
||||
`.trim(),
|
||||
es: `
|
||||
Eres un asistente que sugiere etiquetas RELEVANTES para una nota.
|
||||
|
||||
CONTENIDO DE LA NOTA:
|
||||
${noteContent.substring(0, 1000)}
|
||||
|
||||
CUADERNO: ${notebookName}
|
||||
|
||||
ETIQUETAS DISPONIBLES:
|
||||
${labelList}
|
||||
|
||||
REGLAS ESTRICTAS:
|
||||
- Solo sugiere una etiqueta si está DIRECTAMENTE relacionada con el contenido de ESTA nota
|
||||
- Máximo 2 sugerencias
|
||||
- Confianza < 0.7 = no sugerir
|
||||
- Si NINGUNA etiqueta es relevante, devuelve un array VACÍO
|
||||
|
||||
Responde en JSON:
|
||||
{"suggestions":[{"label":"nombre","confidence":0.85,"reasoning":"por qué"}]}
|
||||
|
||||
Tu respuesta:
|
||||
`.trim(),
|
||||
de: `
|
||||
Du bist ein Assistent, der RELEVANTE Labels für eine Notiz vorschlägt.
|
||||
|
||||
NOTIZINHALT:
|
||||
${noteContent.substring(0, 1000)}
|
||||
|
||||
NOTIZBUCH: ${notebookName}
|
||||
|
||||
VERFÜGBARE LABELS:
|
||||
${labelList}
|
||||
|
||||
STRIKTE REGELN:
|
||||
- Schlage ein Label nur vor, wenn es DIREKT mit dem Inhalt DIESER Notiz zusammenhängt
|
||||
- Maximal 2 Vorschläge
|
||||
- Konfidenz < 0.7 = nicht vorschlagen
|
||||
- Wenn KEIN Label relevant ist, gib ein LEERES Array zurück
|
||||
|
||||
Antworte in JSON:
|
||||
{"suggestions":[{"label":"name","confidence":0.85,"reasoning":"warum"}]}
|
||||
|
||||
Deine Antwort:
|
||||
`.trim()
|
||||
}
|
||||
|
||||
return instructions[language] || instructions['en'] || instructions['fr']
|
||||
Your response:`
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the AI prompt for NEW label suggestions (when notebook is empty) (localized)
|
||||
*/
|
||||
private buildNewLabelsPrompt(noteContent: string, notebookName: string, language: string = 'en'): string {
|
||||
const instructions: Record<string, string> = {
|
||||
fr: `
|
||||
Tu es un assistant qui suggère de nouveaux labels pour organiser une note.
|
||||
|
||||
CONTENU DE LA NOTE :
|
||||
private buildNewLabelsPrompt(noteContent: string, notebookName: string, contentLanguage: string): string {
|
||||
return `NOTE CONTENT (language: ${contentLanguage}):
|
||||
${noteContent.substring(0, 1000)}
|
||||
|
||||
NOTEBOOK ACTUEL :
|
||||
${notebookName}
|
||||
|
||||
CONTEXTE :
|
||||
Ce notebook n'a pas encore de labels. Tu dois suggérer les PREMIERS labels appropriés pour cette note.
|
||||
|
||||
TÂCHE :
|
||||
Analyse le contenu de la note et suggère 1-3 labels qui seraient pertinents pour organiser cette note.
|
||||
Considère :
|
||||
1. Les sujets ou thèmes abordés
|
||||
2. Le type de contenu (idée, tâche, référence, etc.)
|
||||
3. Le contexte du notebook "${notebookName}"
|
||||
|
||||
RÈGLES :
|
||||
- Les labels doivent être COURTS (1-2 mots maximum)
|
||||
- Les labels doivent être en minuscules
|
||||
- Évite les accents si possible (ex: "idee" au lieu de "idée")
|
||||
- Retourne au maximum 3 suggestions
|
||||
- Chaque suggestion doit avoir une confiance > 0.6
|
||||
|
||||
IMPORTANT : Réponds UNIQUEMENT avec du JSON valide, sans texte avant ou après. Pas de markdown, pas de code blocks.
|
||||
|
||||
FORMAT DE RÉPONSE (JSON brut, sans markdown) :
|
||||
{"suggestions":[{"label":"nom_du_label","confidence":0.85,"reasoning":"Pourquoi ce label est pertinent"}]}
|
||||
|
||||
Ta réponse (JSON brut uniquement) :
|
||||
`.trim(),
|
||||
en: `
|
||||
You are an assistant that suggests new labels to organize a note.
|
||||
|
||||
NOTE CONTENT:
|
||||
${noteContent.substring(0, 1000)}
|
||||
|
||||
CURRENT NOTEBOOK:
|
||||
${notebookName}
|
||||
|
||||
CONTEXT:
|
||||
This notebook has no labels yet. You must suggest the FIRST appropriate labels for this note.
|
||||
|
||||
TASK:
|
||||
Analyze the note content and suggest 1-3 labels that would be relevant to organize this note.
|
||||
Consider:
|
||||
1. Topics or themes covered
|
||||
2. Content type (idea, task, reference, etc.)
|
||||
3. Context of the notebook "${notebookName}"
|
||||
NOTEBOOK: ${notebookName}
|
||||
|
||||
RULES:
|
||||
- Suggest 1-3 labels to organize this note
|
||||
- Labels must be SHORT (max 1-2 words)
|
||||
- Labels must be in the SAME LANGUAGE as the note content (${contentLanguage})
|
||||
- Labels must be lowercase
|
||||
- Avoid accents if possible
|
||||
- Return maximum 3 suggestions
|
||||
- Each suggestion must have confidence > 0.6
|
||||
- Maximum 3 suggestions
|
||||
- Confidence < 0.6 = do not suggest
|
||||
|
||||
IMPORTANT: Respond ONLY with valid JSON, without text before or after. No markdown, no code blocks.
|
||||
IMPORTANT: Respond ONLY with valid JSON, no markdown, no code blocks.
|
||||
{"suggestions":[{"label":"label_name","confidence":0.85,"reasoning":"why"}]}
|
||||
|
||||
RESPONSE FORMAT (raw JSON, no markdown):
|
||||
{"suggestions":[{"label":"label_name","confidence":0.85,"reasoning":"Why this label is relevant"}]}
|
||||
|
||||
Your response (raw JSON only):
|
||||
`.trim(),
|
||||
fa: `
|
||||
شما یک دستیار هستید که برچسبهای جدیدی برای سازماندهی یک یادداشت پیشنهاد میدهید.
|
||||
|
||||
محتوای یادداشت:
|
||||
${noteContent.substring(0, 1000)}
|
||||
|
||||
دفترچه فعلی:
|
||||
${notebookName}
|
||||
|
||||
زمینه:
|
||||
این دفترچه هنوز هیچ برچسبی ندارد. شما باید اولین برچسبهای مناسب را برای این یادداشت پیشنهاد دهید.
|
||||
|
||||
وظیفه:
|
||||
محتوای یادداشت را تحلیل کنید و ۱-۳ برچسب پیشنهاد دهید که برای سازماندهی این یادداشت مرتبط باشند.
|
||||
در نظر بگیرید:
|
||||
1. موضوعات یا تمهای پوشش داده شده
|
||||
2. نوع محتوا (ایده، وظیفه، مرجع و غیره)
|
||||
3. زمینه دفترچه "${notebookName}"
|
||||
|
||||
قوانین:
|
||||
- برچسبها باید کوتاه باشند (حداکثر ۱-۲ کلمه)
|
||||
- برچسبها باید با حروف کوچک باشند
|
||||
- حداکثر ۳ پیشنهاد برگردانید
|
||||
- هر پیشنهاد باید دارای اطمینان > 0.6 باشد
|
||||
|
||||
مهم: فقط با یک JSON معتبر پاسخ دهید، بدون متن قبل یا بعد. بدون مارکداون، بدون بلوک کد.
|
||||
|
||||
فرمت پاسخ (JSON خام، بدون مارکداون):
|
||||
{"suggestions":[{"label":"نام_برچسب","confidence":0.85,"reasoning":"چرا این برچسب مرتبط است"}]}
|
||||
|
||||
پاسخ شما (فقط JSON خام):
|
||||
`.trim(),
|
||||
es: `
|
||||
Eres un asistente que sugiere nuevas etiquetas para organizar una nota.
|
||||
|
||||
CONTENIDO DE LA NOTA:
|
||||
${noteContent.substring(0, 1000)}
|
||||
|
||||
CUADERNO ACTUAL:
|
||||
${notebookName}
|
||||
|
||||
CONTEXTO:
|
||||
Este cuaderno aún no tiene etiquetas. Debes sugerir las PRIMERAS etiquetas apropiadas para esta nota.
|
||||
|
||||
TAREA:
|
||||
Analiza el contenido de la nota y sugiere 1-3 etiquetas que serían relevantes para organizar esta nota.
|
||||
Considera:
|
||||
1. Temas o tópicos cubiertos
|
||||
2. Tipo de contenido (idea, tarea, referencia, etc.)
|
||||
3. Contexto del cuaderno "${notebookName}"
|
||||
|
||||
REGLAS:
|
||||
- Las etiquetas deben ser CORTAS (máx 1-2 palabras)
|
||||
- Las etiquetas deben estar en minúsculas
|
||||
- Evita acentos si es posible
|
||||
- Devuelve máximo 3 sugerencias
|
||||
- Cada sugerencia debe tener confianza > 0.6
|
||||
|
||||
IMPORTANTE: Responde SOLO con JSON válido, sin texto antes o después. Sin markdown, sin bloques de código.
|
||||
|
||||
FORMATO DE RESPUESTA (JSON crudo, sin markdown):
|
||||
{"suggestions":[{"label":"nombre_etiqueta","confidence":0.85,"reasoning":"Por qué esta etiqueta es relevante"}]}
|
||||
|
||||
Tu respuesta (solo JSON crudo):
|
||||
`.trim(),
|
||||
de: `
|
||||
Du bist ein Assistent, der neue Labels vorschlägt, um eine Notiz zu organisieren.
|
||||
|
||||
NOTIZINHALT:
|
||||
${noteContent.substring(0, 1000)}
|
||||
|
||||
AKTUELLES NOTIZBUCH:
|
||||
${notebookName}
|
||||
|
||||
KONTEXT:
|
||||
Dieses Notizbuch hat noch keine Labels. Du musst die ERSTEN passenden Labels für diese Notiz vorschlagen.
|
||||
|
||||
AUFGABE:
|
||||
Analysiere den Notizinhalt und schlage 1-3 Labels vor, die relevant wären, um diese Notiz zu organisieren.
|
||||
Berücksichtige:
|
||||
1. Abgedeckte Themen oder Bereiche
|
||||
2. Inhaltstyp (Idee, Aufgabe, Referenz, usw.)
|
||||
3. Kontext des Notizbuchs "${notebookName}"
|
||||
|
||||
REGELN:
|
||||
- Labels müssen KURZ sein (max 1-2 Wörter)
|
||||
- Labels müssen kleingeschrieben sein
|
||||
- Vermeide Akzente wenn möglich
|
||||
- Gib maximal 3 Vorschläge zurück
|
||||
- Jeder Vorschlag muss eine Konfidenz > 0.6 haben
|
||||
|
||||
WICHTIG: Antworte NUR mit gültigem JSON, ohne Text davor oder danach. Kein Markdown, keine Code-Blöcke.
|
||||
|
||||
ANTWORTFORMAT (rohes JSON, kein Markdown):
|
||||
{"suggestions":[{"label":"label_name","confidence":0.85,"reasoning":"Warum dieses Label relevant ist"}]}
|
||||
|
||||
Deine Antwort (nur rohes JSON):
|
||||
`.trim()
|
||||
}
|
||||
|
||||
return instructions[language] || instructions['en'] || instructions['fr']
|
||||
Your response:`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user