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 { prisma } from '@/lib/prisma'
|
||||||
import { getTagsProvider } from '@/lib/ai/factory'
|
import { getTagsProvider } from '@/lib/ai/factory'
|
||||||
import { getSystemConfig } from '@/lib/config'
|
import { getSystemConfig } from '@/lib/config'
|
||||||
|
import { LanguageDetectionService } from './language-detection.service'
|
||||||
|
|
||||||
export interface LabelSuggestion {
|
export interface LabelSuggestion {
|
||||||
label: string
|
label: string
|
||||||
@@ -16,6 +17,12 @@ export interface LabelSuggestion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ContextualAutoTagService {
|
export class ContextualAutoTagService {
|
||||||
|
private languageDetection: LanguageDetectionService
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.languageDetection = new LanguageDetectionService()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Suggest labels for a note
|
* Suggest labels for a note
|
||||||
* @param noteContent - Content of the note
|
* @param noteContent - Content of the note
|
||||||
@@ -27,13 +34,16 @@ export class ContextualAutoTagService {
|
|||||||
noteContent: string,
|
noteContent: string,
|
||||||
notebookId: string | null,
|
notebookId: string | null,
|
||||||
userId: string,
|
userId: string,
|
||||||
language: string = 'en'
|
_uiLanguage: string = 'en'
|
||||||
): Promise<LabelSuggestion[]> {
|
): Promise<LabelSuggestion[]> {
|
||||||
// If no notebook, return empty (no context)
|
// If no notebook, return empty (no context)
|
||||||
if (!notebookId) {
|
if (!notebookId) {
|
||||||
return []
|
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
|
// Get notebook with its labels
|
||||||
const notebook = await prisma.notebook.findFirst({
|
const notebook = await prisma.notebook.findFirst({
|
||||||
where: {
|
where: {
|
||||||
@@ -54,18 +64,18 @@ export class ContextualAutoTagService {
|
|||||||
return []
|
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)
|
// CASE 1: Notebook has existing labels → suggest from them (IA2)
|
||||||
if (notebook.labels.length > 0) {
|
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)
|
console.log('[ContextualAutoTag] existing label suggestions:', existing.length)
|
||||||
if (existing.length > 0) return existing
|
if (existing.length > 0) return existing
|
||||||
// Fallback: no existing label matched → suggest new ones too
|
// Fallback: no existing label matched → suggest new ones too
|
||||||
}
|
}
|
||||||
|
|
||||||
// CASE 2: No labels in notebook (or none matched) → suggest NEW labels
|
// 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)
|
* 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 labelList = availableLabels.map(l => `- ${l}`).join('\n')
|
||||||
|
|
||||||
const instructions: Record<string, string> = {
|
return `NOTE CONTENT (language: ${contentLanguage}):
|
||||||
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:
|
|
||||||
${noteContent.substring(0, 1000)}
|
${noteContent.substring(0, 1000)}
|
||||||
|
|
||||||
NOTEBOOK: ${notebookName}
|
NOTEBOOK: ${notebookName}
|
||||||
@@ -287,11 +268,10 @@ NOTEBOOK: ${notebookName}
|
|||||||
AVAILABLE LABELS:
|
AVAILABLE LABELS:
|
||||||
${labelList}
|
${labelList}
|
||||||
|
|
||||||
STRICT RULES:
|
RULES:
|
||||||
- Only suggest a label if it is DIRECTLY related to the content of THIS specific note
|
- 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
|
- 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
|
- Maximum 2 suggestions
|
||||||
- Confidence < 0.7 = do not suggest
|
- Confidence < 0.7 = do not suggest
|
||||||
- If NO label is clearly relevant, return an EMPTY array
|
- If NO label is clearly relevant, return an EMPTY array
|
||||||
@@ -299,252 +279,30 @@ STRICT RULES:
|
|||||||
Respond in JSON:
|
Respond in JSON:
|
||||||
{"suggestions":[{"label":"name","confidence":0.85,"reasoning":"why"}]}
|
{"suggestions":[{"label":"name","confidence":0.85,"reasoning":"why"}]}
|
||||||
|
|
||||||
Your response:
|
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']
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the AI prompt for NEW label suggestions (when notebook is empty) (localized)
|
* Build the AI prompt for NEW label suggestions (when notebook is empty) (localized)
|
||||||
*/
|
*/
|
||||||
private buildNewLabelsPrompt(noteContent: string, notebookName: string, language: string = 'en'): string {
|
private buildNewLabelsPrompt(noteContent: string, notebookName: string, contentLanguage: string): string {
|
||||||
const instructions: Record<string, string> = {
|
return `NOTE CONTENT (language: ${contentLanguage}):
|
||||||
fr: `
|
|
||||||
Tu es un assistant qui suggère de nouveaux labels pour organiser une note.
|
|
||||||
|
|
||||||
CONTENU DE LA NOTE :
|
|
||||||
${noteContent.substring(0, 1000)}
|
${noteContent.substring(0, 1000)}
|
||||||
|
|
||||||
NOTEBOOK ACTUEL :
|
NOTEBOOK: ${notebookName}
|
||||||
${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}"
|
|
||||||
|
|
||||||
RULES:
|
RULES:
|
||||||
|
- Suggest 1-3 labels to organize this note
|
||||||
- Labels must be SHORT (max 1-2 words)
|
- Labels must be SHORT (max 1-2 words)
|
||||||
|
- Labels must be in the SAME LANGUAGE as the note content (${contentLanguage})
|
||||||
- Labels must be lowercase
|
- Labels must be lowercase
|
||||||
- Avoid accents if possible
|
- Maximum 3 suggestions
|
||||||
- Return maximum 3 suggestions
|
- Confidence < 0.6 = do not suggest
|
||||||
- Each suggestion must have confidence > 0.6
|
|
||||||
|
|
||||||
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):
|
Your response:`
|
||||||
{"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']
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user