From c7e654afa61e1c594488c6a9910b21ad9c012954 Mon Sep 17 00:00:00 2001 From: sepehr Date: Mon, 27 Apr 2026 20:56:46 +0200 Subject: [PATCH] fix: auto-detect note language for label suggestions (like title suggestions) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../services/contextual-auto-tag.service.ts | 300 ++---------------- 1 file changed, 29 insertions(+), 271 deletions(-) diff --git a/memento-note/lib/ai/services/contextual-auto-tag.service.ts b/memento-note/lib/ai/services/contextual-auto-tag.service.ts index a23e72c..49429fc 100644 --- a/memento-note/lib/ai/services/contextual-auto-tag.service.ts +++ b/memento-note/lib/ai/services/contextual-auto-tag.service.ts @@ -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 { // 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 = { - 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 = { - 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:` } }