diff --git a/memento-note/lib/ai/services/agent-executor.service.ts b/memento-note/lib/ai/services/agent-executor.service.ts index 4e09d2c..c189ac3 100644 --- a/memento-note/lib/ai/services/agent-executor.service.ts +++ b/memento-note/lib/ai/services/agent-executor.service.ts @@ -945,7 +945,7 @@ Appelle generate_slides avec un objet JSON structuré : 12. "summary" → { type:"summary", title:"...", items:["Point clé 1", "Point clé 2", ...] } ═══ RÈGLES ═══ -- 6 à 12 slides par présentation +- Nombre de slides : adapté au contenu réel de la note (la règle ⚠️ dans le message utilisateur est ABSOLUE) - Slide 1 OBLIGATOIREMENT type "title" - Dernière slide OBLIGATOIREMENT type "summary" - Au moins 1 slide "chart" ou "stats" si des chiffres existent dans la note @@ -989,7 +989,7 @@ Call generate_slides with a structured JSON object: 12. "summary" → { type:"summary", title:"...", items:["Key point 1", "Key point 2", ...] } ═══ RULES ═══ -- 6 to 12 slides per presentation +- Slide count: adapt to the real content (the ⚠️ rule in the user message is ABSOLUTE) - Slide 1 MUST be type "title" - Last slide MUST be type "summary" - At least 1 "chart" or "stats" slide if numbers exist in the note @@ -1027,6 +1027,49 @@ You MUST use the task_extract tool. Do NOT respond with text, call the tool dire }, } +/** + * Strip markdown syntax, URLs, metadata blocks, and frontmatter from note content. + * Returns only the semantic plain text, used to count real words before generating slides. + */ +function stripNoteMarkdown(raw: string): string { + return raw + // Remove frontmatter / YAML blocks + .replace(/^---[\s\S]*?---/m, '') + // Remove markdown headers + .replace(/^#{1,6}\s+/gm, '') + // Remove bold/italic + .replace(/[*_]{1,3}([^*_]+)[*_]{1,3}/g, '$1') + // Remove inline links — keep label only + .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') + // Remove bare URLs + .replace(/https?:\/\/\S+/g, '') + // Remove metadata lines (Connection to seed, Novelty score, Source brainstorm, Origin, Derived from, etc.) + .replace(/^\*{0,2}(Connection to seed|Novelty score|Source brainstorm|Source note|Origin|Derived from|## Origin)[^:\n]*:?.*$/gim, '') + // Remove horizontal rules + .replace(/^---+$/gm, '') + // Remove blockquote markers + .replace(/^>\s*/gm, '') + // Remove code blocks + .replace(/```[\s\S]*?```/g, '') + .replace(/`[^`]+`/g, '') + // Collapse whitespace + .replace(/\s+/g, ' ') + .trim() +} + +/** Count real semantic words in a note */ +function countNoteWords(raw: string): number { + return stripNoteMarkdown(raw).split(/\s+/).filter(Boolean).length +} + +/** Return max slides based on real word count */ +function slideLimit(wordCount: number): { max: number; label: string } { + if (wordCount < 50) return { max: 3, label: '3 slides MAX (note très courte)' } + if (wordCount < 150) return { max: 4, label: '4 slides MAX (note courte)' } + if (wordCount < 350) return { max: 6, label: '6 slides MAX (note moyenne)' } + return { max: 8, label: '8 slides MAX (note longue)' } +} + function extractJsonFromText(text: string): any { if (!text) return null @@ -1274,7 +1317,16 @@ async function executeToolUseAgent( const notesContext = notes.map(n => `### ${n.title || untitled} (${n.createdAt.toLocaleDateString(dateLocale)})\n${n.content.substring(0, 2000)}` ).join('\n\n') + + // Compute real word count (stripped of markdown/metadata) to determine slide limit + const totalWords = notes.reduce((sum, n) => sum + countNoteWords(n.content || ''), 0) + const { max: maxSlides, label: slideRule } = slideLimit(totalWords) + prompt += `\n\n${lang === 'fr' ? 'Notes source' : 'Source notes'}:\n\n${notesContext}` + prompt += `\n\n${lang === 'fr' + ? `⚠️ RÈGLE ABSOLUE : Cette note fait ${totalWords} mots réels (hors markdown/URLs/métadonnées). Tu DOIS générer EXACTEMENT ${slideRule}. Respecter cette limite est PLUS IMPORTANT que la diversité ou la densité.` + : `⚠️ ABSOLUTE RULE: This note has ${totalWords} real words (excluding markdown/URLs/metadata). You MUST generate EXACTLY ${slideRule}. This limit is MORE IMPORTANT than diversity or density.` + }` } // ── Executive Template Structure (HTML-compatible) ── @@ -1317,8 +1369,8 @@ async function executeToolUseAgent( : `\n\nMANDATORY DENSITY: Every slide must have RICH CONTENT. Minimum 5 items per "bullets" slide (≥15 words each). Minimum 4 cards when using "cards". Minimum 4 data points for "chart". You MUST include at least 1 "chart" slide with REAL data from the notes. If no numbers in notes, create a maturity radar or qualitative comparison scored out of 5.` prompt += `\n\n${lang === 'fr' - ? 'IMPORTANT : Appelle OBLIGATOIREMENT generate_slides avec le JSON structuré {title, theme, slides:[...]}. Ne réponds JAMAIS avec du texte brut. 6-12 slides variées.' - : 'IMPORTANT: You MUST call generate_slides with structured JSON {title, theme, slides:[...]}. NEVER respond with plain text. 6-12 varied slides.'}` + ? 'IMPORTANT : Appelle OBLIGATOIREMENT generate_slides avec le JSON structuré {title, theme, slides:[...]}. Ne réponds JAMAIS avec du texte brut. Respecte ABSOLUMENT la limite de slides indiquée.' + : 'IMPORTANT: You MUST call generate_slides with structured JSON {title, theme, slides:[...]}. NEVER respond with plain text. STRICTLY respect the slide count limit indicated.'}` if (agent.slideTheme && agent.slideTheme !== 'auto') { prompt += `\n${lang === 'fr'