feat(credits): solde IA unique (option M), packs Stripe et polish billing
Un pot de crédits global (BASIC 100 / PRO 1 000 / BUSINESS 4 000) remplace les seaux par fonction côté débit. Packs one-shot S/M/L, admin sans seaux legacy, usage multi-features, hints de coût, anti-flash thème et hydratation admin. Inclut aussi le pipeline slides renforcé et la page publique polishée.
This commit is contained in:
@@ -18,7 +18,26 @@ import { calculateNextRun } from '@/lib/agents/schedule'
|
||||
import { markdownToHtml } from '@/lib/markdown-to-html'
|
||||
import { createNotification } from '@/app/actions/notifications'
|
||||
import { withAiQuota } from '@/lib/ai-quota'
|
||||
import { releaseCredits, slideGenerateCreditCost } from '@/lib/credits'
|
||||
import type { FeatureName } from '@/lib/quota-utils'
|
||||
import {
|
||||
countNoteWords,
|
||||
prepareNoteTextForSlides,
|
||||
slideLimitFromWordCount,
|
||||
SLIDE_CONTENT_PRINCIPLES_EN,
|
||||
SLIDE_CONTENT_PRINCIPLES_FR,
|
||||
normalizeSlideDeck,
|
||||
} from '@/lib/ai/services/slide-content-quality'
|
||||
import { generateSlideDeck } from '@/lib/ai/services/slide-deck-generator.service'
|
||||
import { decodeSlideIntentDescription } from '@/lib/ai/services/slide-intent'
|
||||
|
||||
export type ExecuteAgentOptions = {
|
||||
/**
|
||||
* When true, caller already reserved quota (e.g. run-for-note multi-unit pre-check).
|
||||
* Avoids double-charging on fire-and-forget generation.
|
||||
*/
|
||||
skipQuota?: boolean
|
||||
}
|
||||
|
||||
// Import tools for side-effect registration
|
||||
import '../tools'
|
||||
@@ -915,94 +934,54 @@ This format AUTOMATICALLY creates shapes, text, AND arrows with correct bindings
|
||||
},
|
||||
|
||||
'slide-generator': {
|
||||
fr: `Tu es un expert en design de présentations exécutives. Tu analyses la note et génères une structure JSON qui sera rendue en HTML automatiquement par le serveur.
|
||||
fr: `Tu es un consultant senior (niveau McKinsey/BCG) qui transforme une note Memento en deck exécutif de classe mondiale.
|
||||
|
||||
APPELLE OBLIGATOIREMENT generate_slides avec le JSON structuré. NE GÉNÈRE JAMAIS de HTML brut.
|
||||
|
||||
═══ ÉTAPE 1 — LECTURE (OBLIGATOIRE) ═══
|
||||
1. Lis la note avec note_read. Extrais TOUTES les données exploitables (chiffres, listes, citations, comparaisons, étapes).
|
||||
2. Identifie les types de slides les plus adaptés au contenu réel.
|
||||
${SLIDE_CONTENT_PRINCIPLES_FR}
|
||||
|
||||
═══ ÉTAPE 2 — APPEL generate_slides ═══
|
||||
Appelle generate_slides avec un objet JSON structuré :
|
||||
═══ MÉTHODE ═══
|
||||
1. Lis la note (contenu fourni + note_read si besoin). Extrais : claims, chiffres, citations, étapes, oppositions, décisions.
|
||||
2. Construis un storyboard mental (max N slides selon le message user) avant d'appeler l'outil.
|
||||
3. Appelle generate_slides :
|
||||
{
|
||||
"title": "Titre court (6 mots max)",
|
||||
"theme": "architectural-saas", // ou midnight-cathedral, venture-pitch, clinical-precision, etc.
|
||||
"slides": [ ... tableau de slides ... ]
|
||||
"title": "Titre du deck (≤8 mots)",
|
||||
"theme": "architectural-saas",
|
||||
"slides": [ ... ]
|
||||
}
|
||||
|
||||
═══ TYPES DE SLIDES DISPONIBLES ═══
|
||||
1. "title" → { type:"title", title:"...", subtitle:"..." }
|
||||
2. "bullets" → { type:"bullets", title:"...", items:["phrase 1 (15+ mots)", "phrase 2", ...] }
|
||||
3. "chart" → { type:"chart", title:"...", chartType:"bar|horizontal-bar|line|donut|radar", data:[{label:"Q1",value:65}, ...], subtitle:"..." }
|
||||
4. "stats" → { type:"stats", title:"...", stats:[{value:"98%", label:"Satisfaction"}, ...] }
|
||||
5. "table" → { type:"table", title:"...", headers:["Col A","Col B"], rows:[["val1","val2"], ...] }
|
||||
6. "cards" → { type:"cards", title:"...", cards:[{title:"Titre", description:"Description détaillée..."}, ...] }
|
||||
7. "timeline" → { type:"timeline", title:"...", events:[{date:"Jan 2024", title:"Lancement", description:"..."}, ...] }
|
||||
8. "quote" → { type:"quote", quote:"Citation exacte", author:"Auteur", context:"Analyse..." }
|
||||
9. "comparison" → { type:"comparison", title:"...", left:{title:"A", points:["..."], score:"8/10"}, right:{title:"B", points:["..."], score:"6/10"} }
|
||||
10. "equation" → { type:"equation", title:"...", equations:[{latex:"E=mc^2", label:"Énergie"}], explanation:"..." }
|
||||
11. "image" → { type:"image", title:"...", url:"https://...", caption:"..." }
|
||||
12. "summary" → { type:"summary", title:"...", items:["Point clé 1", "Point clé 2", ...] }
|
||||
═══ TYPES (choisir selon le contenu, pas pour « remplir ») ═══
|
||||
title | bullets | chart* | stats* | table | cards | timeline | quote | comparison | equation | image | summary
|
||||
* chart/stats seulement avec chiffres réels dans la note.
|
||||
|
||||
═══ RÈGLES ═══
|
||||
- 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
|
||||
- VARIER les types — jamais 2 types identiques consécutifs
|
||||
- Chaque texte (bullet, description) = 15+ mots, phrase complète
|
||||
- TOUTES les données viennent de la note (JAMAIS inventer de chiffres)
|
||||
- Les données chart doivent refléter les vrais chiffres de la note
|
||||
|
||||
═══ THÈMES DISPONIBLES ═══
|
||||
═══ THÈMES ═══
|
||||
Sombres : midnight-cathedral, aurora-borealis, tokyo-neon, venture-pitch, forest-floor, steel-glass, cyberpunk-terminal
|
||||
Clairs : sunlit-gallery, clinical-precision, editorial-ink, coastal-morning, paper-studio, architectural-saas
|
||||
Choisir selon le sujet : business/board → architectural-saas ou midnight-cathedral, tech → cyberpunk-terminal ou clinical-precision, créatif → aurora-borealis ou tokyo-neon`,
|
||||
en: `You are an executive presentation design expert. You analyze the note and generate a structured JSON spec that will be rendered to HTML automatically by the server.
|
||||
Business → architectural-saas / midnight-cathedral ; tech → clinical-precision ; créatif → aurora-borealis`,
|
||||
en: `You are a senior consultant (McKinsey/BCG level) turning a Memento note into a world-class executive deck.
|
||||
|
||||
You MUST call generate_slides with structured JSON. NEVER output raw HTML.
|
||||
|
||||
═══ STEP 1 — READ (MANDATORY) ═══
|
||||
1. Read the note with note_read. Extract ALL usable data (numbers, lists, quotes, comparisons, steps).
|
||||
2. Identify the best slide types matching the actual content.
|
||||
${SLIDE_CONTENT_PRINCIPLES_EN}
|
||||
|
||||
═══ STEP 2 — CALL generate_slides ═══
|
||||
Call generate_slides with a structured JSON object:
|
||||
═══ METHOD ═══
|
||||
1. Read the note (provided content + note_read if needed). Extract: claims, numbers, quotes, steps, trade-offs, decisions.
|
||||
2. Storyboard mentally (respect max N slides from the user message) before calling the tool.
|
||||
3. Call generate_slides:
|
||||
{
|
||||
"title": "Short title (6 words max)",
|
||||
"title": "Deck title (≤8 words)",
|
||||
"theme": "architectural-saas",
|
||||
"slides": [ ... array of slides ... ]
|
||||
"slides": [ ... ]
|
||||
}
|
||||
|
||||
═══ AVAILABLE SLIDE TYPES ═══
|
||||
1. "title" → { type:"title", title:"...", subtitle:"..." }
|
||||
2. "bullets" → { type:"bullets", title:"...", items:["sentence 1 (15+ words)", "sentence 2", ...] }
|
||||
3. "chart" → { type:"chart", title:"...", chartType:"bar|horizontal-bar|line|donut|radar", data:[{label:"Q1",value:65}, ...], subtitle:"..." }
|
||||
4. "stats" → { type:"stats", title:"...", stats:[{value:"98%", label:"Satisfaction"}, ...] }
|
||||
5. "table" → { type:"table", title:"...", headers:["Col A","Col B"], rows:[["val1","val2"], ...] }
|
||||
6. "cards" → { type:"cards", title:"...", cards:[{title:"Title", description:"Detailed description..."}, ...] }
|
||||
7. "timeline" → { type:"timeline", title:"...", events:[{date:"Jan 2024", title:"Launch", description:"..."}, ...] }
|
||||
8. "quote" → { type:"quote", quote:"Exact quote", author:"Author", context:"Analysis..." }
|
||||
9. "comparison" → { type:"comparison", title:"...", left:{title:"A", points:["..."], score:"8/10"}, right:{title:"B", points:["..."], score:"6/10"} }
|
||||
10. "equation" → { type:"equation", title:"...", equations:[{latex:"E=mc^2", label:"Energy"}], explanation:"..." }
|
||||
11. "image" → { type:"image", title:"...", url:"https://...", caption:"..." }
|
||||
12. "summary" → { type:"summary", title:"...", items:["Key point 1", "Key point 2", ...] }
|
||||
═══ TYPES (match content — do not pad) ═══
|
||||
title | bullets | chart* | stats* | table | cards | timeline | quote | comparison | equation | image | summary
|
||||
* chart/stats only with real numbers from the note.
|
||||
|
||||
═══ RULES ═══
|
||||
- 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
|
||||
- VARY types — never 2 identical types in a row
|
||||
- Each text (bullet, description) = 15+ words, complete sentence
|
||||
- ALL data comes from the note (NEVER invent numbers)
|
||||
- Chart data must reflect actual numbers from the note
|
||||
|
||||
═══ AVAILABLE THEMES ═══
|
||||
═══ THEMES ═══
|
||||
Dark: midnight-cathedral, aurora-borealis, tokyo-neon, venture-pitch, forest-floor, steel-glass, cyberpunk-terminal
|
||||
Light: sunlit-gallery, clinical-precision, editorial-ink, coastal-morning, paper-studio, architectural-saas
|
||||
Choose based on topic: business/board → architectural-saas or midnight-cathedral, tech → cyberpunk-terminal or clinical-precision, creative → aurora-borealis or tokyo-neon`,
|
||||
Business → architectural-saas / midnight-cathedral; tech → clinical-precision; creative → aurora-borealis`,
|
||||
},
|
||||
'task-extractor': {
|
||||
fr: `Tu es un expert en gestion de tâches et extraction d'action items. Tu analyses des notes et documents pour identifier toutes les tâches, TODOs, et actions à accomplir.
|
||||
@@ -1028,49 +1007,6 @@ 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
|
||||
|
||||
@@ -1323,52 +1259,54 @@ async function executeToolUseAgent(
|
||||
? LANG_NAMES[noteLanguage]
|
||||
: (lang === 'fr' ? 'French' : 'English')
|
||||
|
||||
prompt = `Create a professional presentation using the content from the notes below. Call generate_slides with a structured JSON object (title, theme, slides[]).`
|
||||
prompt += `\n\n⚠️ LANGUAGE RULE: ALL slide text (titles, bullets, labels, subtitles) MUST be written in ${contentLang}. Do NOT translate to another language.`
|
||||
prompt = lang === 'fr'
|
||||
? `Construis un deck exécutif de haute qualité à partir des notes ci-dessous. Appelle generate_slides avec {title, theme, slides[]}.`
|
||||
: `Build a high-quality executive deck from the notes below. Call generate_slides with {title, theme, slides[]}.`
|
||||
prompt += `\n\n⚠️ LANGUAGE: ALL on-slide text (titles, bullets, labels, subtitles, summary) MUST be in ${contentLang}.`
|
||||
|
||||
if (notes.length > 0) {
|
||||
const notesContext = notes.map(n =>
|
||||
`### ${n.title || untitled} (${n.createdAt.toLocaleDateString(dateLocale)})\n${n.content.substring(0, 2000)}`
|
||||
`### ${n.title || untitled} (${n.createdAt.toLocaleDateString(dateLocale)})\n${prepareNoteTextForSlides(n.content || '', 12_000)}`
|
||||
).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)
|
||||
const limit = slideLimitFromWordCount(totalWords)
|
||||
const slideRule = lang === 'fr' ? limit.labelFr : limit.labelEn
|
||||
|
||||
prompt += `\n\n${lang === 'fr' ? 'Notes source' : 'Source notes'}:\n\n${notesContext}`
|
||||
prompt += `\n\n${lang === 'fr' ? '## Notes source (texte intégral utile)' : '## Source notes (usable full text)'}:\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.`
|
||||
? `⚠️ LIMITE DECK : ${totalWords} mots source → ${slideRule}. Mieux vaut ${limit.min} slides excellentes que ${limit.max} slides diluées. Ne dépasse JAMAIS ${limit.max}.`
|
||||
: `⚠️ DECK LIMIT: ${totalWords} source words → ${slideRule}. Prefer ${limit.min} excellent slides over ${limit.max} diluted ones. NEVER exceed ${limit.max}.`
|
||||
}`
|
||||
}
|
||||
|
||||
// ── Executive Template Structure (HTML-compatible) ──
|
||||
// ── Executive templates (max 8 slides — matches hard cap) ──
|
||||
if (slideTemplate && slideTemplate !== 'auto') {
|
||||
const templates: Record<string, { fr: string; en: string }> = {
|
||||
'board-update': {
|
||||
fr: `Structure imposée "Board Update" (10 slides) :
|
||||
1. TITRE avec date | 2. KPIs majeurs (3-4 gros chiffres) | 3. Bar chart progression objectifs | 4. Grille de métriques (6 cards) | 5. Timeline jalons | 6. Cards réalisations | 7. Line chart tendance | 8. Deux colonnes risques/mitigations | 9. Bullets prochaines étapes | 10. Conclusion`,
|
||||
en: `Mandatory "Board Update" structure (10 slides):
|
||||
1. TITLE with date | 2. Key KPIs (3-4 big numbers) | 3. Bar chart goal progress | 4. Metrics grid (6 cards) | 5. Timeline milestones | 6. Achievement cards | 7. Line chart trend | 8. Two-column risks/mitigations | 9. Next steps bullets | 10. Conclusion`
|
||||
fr: `Structure "Board Update" (≤8 slides, titres d'action) :
|
||||
1. title | 2. stats KPIs réels seulement | 3. insight + bullets sur performance | 4. chart progression SI chiffres | 5. cards réalisations | 6. comparison risques/mitigations | 7. bullets prochaines décisions | 8. summary`,
|
||||
en: `"Board Update" structure (≤8 slides, action titles):
|
||||
1. title | 2. stats real KPIs only | 3. insight + performance bullets | 4. chart ONLY if real numbers | 5. achievement cards | 6. risks/mitigations comparison | 7. next decisions bullets | 8. summary`,
|
||||
},
|
||||
'project-status': {
|
||||
fr: `Structure imposée "Project Status" (10 slides) :
|
||||
1. TITRE + statut 🟢🟡🔴 | 2. KPIs (% avancement, jours, budget) | 3. Bar chart par workstream | 4. Cards livrables | 5. Timeline roadmap | 6. Line chart burn-down | 7. Deux colonnes blockers/actions | 8. Tableau de risques | 9. Bullets décisions requises | 10. Synthèse`,
|
||||
en: `Mandatory "Project Status" structure (10 slides):
|
||||
1. TITLE + status 🟢🟡🔴 | 2. KPIs (% complete, days, budget) | 3. Bar chart by workstream | 4. Deliverable cards | 5. Timeline roadmap | 6. Line chart burn-down | 7. Two-column blockers/actions | 8. Risk table | 9. Required decisions bullets | 10. Summary`
|
||||
fr: `Structure "Project Status" (≤8) :
|
||||
1. title + statut | 2. stats avancement (réels) | 3. cards livrables | 4. timeline jalons | 5. comparison blockers/actions | 6. table risques SI présent | 7. bullets décisions | 8. summary`,
|
||||
en: `"Project Status" structure (≤8):
|
||||
1. title + status | 2. real progress stats | 3. deliverable cards | 4. milestone timeline | 5. blockers/actions comparison | 6. risk table IF present | 7. decisions bullets | 8. summary`,
|
||||
},
|
||||
'strategy-review': {
|
||||
fr: `Structure imposée "Strategy Review" (10 slides) :
|
||||
1. TITRE stratégique | 2. Contexte marché (5+ bullets) | 3. Radar/pie positionnement | 4. SWOT deux colonnes | 5. Area chart tendances | 6. Cards axes stratégiques | 7. Timeline roadmap 12 mois | 8. Bar chart projections | 9. KPIs objectifs cibles | 10. Call to action`,
|
||||
en: `Mandatory "Strategy Review" structure (10 slides):
|
||||
1. Strategic TITLE | 2. Market context (5+ bullets) | 3. Radar/pie positioning | 4. SWOT two-column | 5. Area chart trends | 6. Strategic axes cards | 7. 12-month roadmap timeline | 8. Bar chart projections | 9. Target KPIs | 10. Call to action`
|
||||
fr: `Structure "Strategy Review" (≤8) :
|
||||
1. title stratégique | 2. bullets contexte (claims) | 3. comparison options ou SWOT | 4. cards axes stratégiques | 5. timeline roadmap | 6. stats/chart UNIQUEMENT si données | 7. implications | 8. summary CTA`,
|
||||
en: `"Strategy Review" structure (≤8):
|
||||
1. strategic title | 2. context claim bullets | 3. options/SWOT comparison | 4. strategic pillars cards | 5. roadmap timeline | 6. stats/chart ONLY if data | 7. implications | 8. summary CTA`,
|
||||
},
|
||||
'quarterly-results': {
|
||||
fr: `Structure imposée "Quarterly Results" (10 slides) :
|
||||
1. TITRE "Résultats Q? Année" | 2. 4 KPIs (Revenue, Croissance, Marge, NPS) | 3. Bar chart revenue/mois | 4. Line chart croissance YoY | 5. Cards faits marquants | 6. Grille métriques opérationnelles | 7. Donut répartition clients | 8. Deux colonnes succès/challenges | 9. Area chart forecast | 10. Conclusion outlook`,
|
||||
en: `Mandatory "Quarterly Results" structure (10 slides):
|
||||
1. TITLE "Q? Year Results" | 2. 4 KPIs (Revenue, Growth, Margin, NPS) | 3. Bar chart revenue/month | 4. Line chart YoY growth | 5. Highlights cards | 6. Operational metrics grid | 7. Donut client breakdown | 8. Two-column successes/challenges | 9. Area chart forecast | 10. Conclusion outlook`
|
||||
}
|
||||
fr: `Structure "Quarterly Results" (≤8) :
|
||||
1. title trimestre | 2. stats 3–4 KPIs réels | 3. chart SI séries chiffrées | 4. cards faits marquants | 5. comparison succès/défis | 6. bullets outlook | 7. summary`,
|
||||
en: `"Quarterly Results" structure (≤8):
|
||||
1. quarter title | 2. 3–4 real KPI stats | 3. chart IF numeric series | 4. highlight cards | 5. wins/challenges comparison | 6. outlook bullets | 7. summary`,
|
||||
},
|
||||
}
|
||||
const tmpl = templates[slideTemplate]
|
||||
if (tmpl) {
|
||||
@@ -1376,19 +1314,24 @@ async function executeToolUseAgent(
|
||||
}
|
||||
}
|
||||
|
||||
// ── Density rules ──
|
||||
prompt += lang === 'fr'
|
||||
? `\n\nDENSITÉ OBLIGATOIRE : Chaque slide doit avoir un CONTENU RICHE. Minimum 5 bullets par slide "bullets" (≥15 mots chaque). Minimum 4 cards quand tu fais "cards". Minimum 4 data points quand tu fais "chart". Inclus OBLIGATOIREMENT au moins 1 slide "chart" avec données RÉELLES extraites des notes. Si pas de chiffres dans la note, fais un radar de maturité ou une comparaison qualitative notée sur 5.`
|
||||
: `\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. 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.'}`
|
||||
? `\n\nQUALITÉ > QUANTITÉ :
|
||||
- Titres d'action (insight), une idée par slide
|
||||
- 3–5 bullets courts max ; pas de filler ni de paragraphes
|
||||
- Graphiques UNIQUEMENT avec chiffres de la note — JAMAIS inventer de KPI/radar
|
||||
- Arc : problème → insight → preuve → suite
|
||||
- Appelle generate_slides maintenant (JSON structuré uniquement).`
|
||||
: `\n\nQUALITY > QUANTITY:
|
||||
- Action titles (insights), one idea per slide
|
||||
- 3–5 short bullets max; no filler or paragraphs
|
||||
- Charts ONLY with numbers from the note — NEVER invent KPI/radar scores
|
||||
- Arc: problem → insight → evidence → next steps
|
||||
- Call generate_slides now (structured JSON only).`
|
||||
|
||||
if (agent.slideTheme && agent.slideTheme !== 'auto') {
|
||||
prompt += `\n${lang === 'fr'
|
||||
? `Thème visuel imposé : utilise "theme":"${agent.slideTheme}" dans l'appel generate_slides.`
|
||||
: `Visual theme required: use "theme":"${agent.slideTheme}" in the generate_slides call.`}`
|
||||
? `Thème visuel imposé : "theme":"${agent.slideTheme}" dans generate_slides.`
|
||||
: `Required visual theme: "theme":"${agent.slideTheme}" in the generate_slides call.`}`
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -1510,7 +1453,12 @@ async function executeToolUseAgent(
|
||||
const registered = toolRegistry.get('generate_slides')
|
||||
if (registered) {
|
||||
const slideTool = registered.buildTool(ctx)
|
||||
const executionResult = await slideTool.execute({ title, theme, slides })
|
||||
const normalized = normalizeSlideDeck({ title, theme, slides })
|
||||
const executionResult = await slideTool.execute({
|
||||
title: normalized.title,
|
||||
theme: normalized.theme,
|
||||
slides: normalized.slides as any,
|
||||
})
|
||||
if (executionResult && executionResult.success && executionResult.canvasId) {
|
||||
canvasId = executionResult.canvasId
|
||||
specificToolCalled = true
|
||||
@@ -1747,9 +1695,124 @@ async function sendAgentEmail(agentId: string, userId: string, agentName: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dedicated slide path: structured JSON → normalize → HTML canvas.
|
||||
* Never depends on the model calling generate_slides.
|
||||
*/
|
||||
async function executeSlideGeneratorAgent(
|
||||
agent: {
|
||||
id: string
|
||||
name: string
|
||||
description?: string | null
|
||||
userId: string
|
||||
sourceNoteIds?: string | null
|
||||
sourceNotebookId?: string | null
|
||||
slideTheme?: string | null
|
||||
},
|
||||
actionId: string,
|
||||
lang: Lang,
|
||||
): Promise<AgentExecutionResult> {
|
||||
const startTime = Date.now()
|
||||
|
||||
let noteIds: string[] = []
|
||||
try {
|
||||
noteIds = agent.sourceNoteIds ? JSON.parse(agent.sourceNoteIds) : []
|
||||
} catch {
|
||||
noteIds = []
|
||||
}
|
||||
|
||||
const intent = decodeSlideIntentDescription(agent.description)
|
||||
const template = intent.template || 'auto'
|
||||
|
||||
await prisma.agentAction.update({
|
||||
where: { id: actionId },
|
||||
data: {
|
||||
input: JSON.stringify({
|
||||
pipeline: 'structured-slide-deck',
|
||||
noteIds,
|
||||
sourceNotebookId: agent.sourceNotebookId,
|
||||
theme: agent.slideTheme,
|
||||
template,
|
||||
intent,
|
||||
lang,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
const result = await generateSlideDeck({
|
||||
userId: agent.userId,
|
||||
noteIds: noteIds.length > 0 ? noteIds : undefined,
|
||||
sourceNotebookId: agent.sourceNotebookId,
|
||||
theme: agent.slideTheme,
|
||||
template,
|
||||
intent,
|
||||
lang,
|
||||
actionId,
|
||||
})
|
||||
|
||||
const duration = Date.now() - startTime
|
||||
|
||||
if (!result.success || !result.canvasId) {
|
||||
await prisma.agentAction.update({
|
||||
where: { id: actionId },
|
||||
data: {
|
||||
status: 'failure',
|
||||
log:
|
||||
(result.error ||
|
||||
(lang === 'fr'
|
||||
? 'Échec de la génération de présentation (pipeline structuré).'
|
||||
: 'Slide generation failed (structured pipeline).')) +
|
||||
` (${Math.round(duration / 1000)}s)`,
|
||||
},
|
||||
})
|
||||
return {
|
||||
success: false,
|
||||
actionId,
|
||||
error: result.error || 'Slide generation failed',
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.agentAction.update({
|
||||
where: { id: actionId },
|
||||
data: {
|
||||
status: 'success',
|
||||
result: result.canvasId,
|
||||
log: `Slides OK (${result.mode || 'structured'}): ${result.slideCount} slides — ${result.canvasName} (${Math.round(duration / 1000)}s)`,
|
||||
toolLog: JSON.stringify([
|
||||
{
|
||||
step: 1,
|
||||
text: 'structured-slide-deck pipeline (no tool-calling)',
|
||||
toolCalls: [],
|
||||
toolResults: [
|
||||
{
|
||||
toolName: 'generate_slides_structured',
|
||||
preview: JSON.stringify({
|
||||
canvasId: result.canvasId,
|
||||
slideCount: result.slideCount,
|
||||
mode: result.mode,
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
]),
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
actionId,
|
||||
canvasId: result.canvasId,
|
||||
}
|
||||
}
|
||||
|
||||
// --- Main Executor ---
|
||||
|
||||
export async function executeAgent(agentId: string, userId: string, promptOverride?: string): Promise<AgentExecutionResult> {
|
||||
export async function executeAgent(
|
||||
agentId: string,
|
||||
userId: string,
|
||||
promptOverride?: string,
|
||||
options?: ExecuteAgentOptions,
|
||||
): Promise<AgentExecutionResult> {
|
||||
const agent = await prisma.agent.findUnique({
|
||||
where: { id: agentId }
|
||||
})
|
||||
@@ -1768,44 +1831,53 @@ export async function executeAgent(agentId: string, userId: string, promptOverri
|
||||
// Detect user language
|
||||
const lang = await getUserLanguage(userId)
|
||||
const quotaFeature = quotaFeatureForAgentType(agent.type || 'scraper')
|
||||
const agentType = agent.type || 'scraper'
|
||||
|
||||
try {
|
||||
const result = await withAiQuota(
|
||||
userId,
|
||||
quotaFeature,
|
||||
async () => {
|
||||
let inner: AgentExecutionResult
|
||||
// Multi-step slide pipeline: 1 + N crédits (sauf si déjà réservé par run-for-note)
|
||||
let quotaAmount = 1
|
||||
let slideCountForCost: number | null = null
|
||||
if (agentType === 'slide-generator') {
|
||||
const intent = decodeSlideIntentDescription(agent.description)
|
||||
slideCountForCost = intent.slideCount ?? null
|
||||
quotaAmount = slideGenerateCreditCost(intent.slideCount)
|
||||
}
|
||||
|
||||
const runAgent = async (): Promise<AgentExecutionResult> => {
|
||||
// Slide generation uses a dedicated structured-output pipeline (no tool-calling).
|
||||
// Lite models often fail at function calling; generateObject/JSON is reliable.
|
||||
if (agentType === 'slide-generator') {
|
||||
return executeSlideGeneratorAgent(agent, action.id, lang)
|
||||
}
|
||||
|
||||
const hasTools = agent.tools && agent.tools !== '[]' && agent.tools !== 'null'
|
||||
|
||||
if (hasTools) {
|
||||
inner = await executeToolUseAgent(agent, action.id, lang, promptOverride)
|
||||
} else {
|
||||
switch ((agent.type || 'scraper') as AgentType) {
|
||||
case 'scraper':
|
||||
inner = await executeScraperAgent(agent, action.id, lang)
|
||||
break
|
||||
case 'researcher':
|
||||
inner = await executeResearcherAgent(agent, action.id, lang)
|
||||
break
|
||||
case 'monitor':
|
||||
inner = await executeMonitorAgent(agent, action.id, lang)
|
||||
break
|
||||
case 'custom':
|
||||
inner = await executeCustomAgent(agent, action.id, lang)
|
||||
break
|
||||
case 'slide-generator':
|
||||
case 'excalidraw-generator':
|
||||
inner = await executeToolUseAgent(agent, action.id, lang, promptOverride)
|
||||
break
|
||||
default:
|
||||
inner = await executeScraperAgent(agent, action.id, lang)
|
||||
}
|
||||
return executeToolUseAgent(agent, action.id, lang, promptOverride)
|
||||
}
|
||||
return inner
|
||||
},
|
||||
{ lane: 'chat' },
|
||||
)
|
||||
|
||||
switch (agentType as AgentType) {
|
||||
case 'scraper':
|
||||
return executeScraperAgent(agent, action.id, lang)
|
||||
case 'researcher':
|
||||
return executeResearcherAgent(agent, action.id, lang)
|
||||
case 'monitor':
|
||||
return executeMonitorAgent(agent, action.id, lang)
|
||||
case 'custom':
|
||||
return executeCustomAgent(agent, action.id, lang)
|
||||
case 'excalidraw-generator':
|
||||
return executeToolUseAgent(agent, action.id, lang, promptOverride)
|
||||
default:
|
||||
return executeScraperAgent(agent, action.id, lang)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = options?.skipQuota
|
||||
? await runAgent()
|
||||
: await withAiQuota(userId, quotaFeature, runAgent, {
|
||||
lane: 'chat',
|
||||
amount: quotaAmount,
|
||||
slideCount: slideCountForCost,
|
||||
})
|
||||
|
||||
const nextRunUpdate: Record<string, Date | null> = {}
|
||||
if (agent.frequency !== 'manual') {
|
||||
@@ -1861,6 +1933,11 @@ export async function executeAgent(agentId: string, userId: string, promptOverri
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
console.error(`[AgentExecutor] Agent ${agentId} failed:`, message)
|
||||
|
||||
// Pré-réservé par run-for-note : rembourser les crédits
|
||||
if (options?.skipQuota && quotaAmount > 0) {
|
||||
await releaseCredits(userId, quotaAmount, { feature: quotaFeature }).catch(() => {})
|
||||
}
|
||||
|
||||
await prisma.agentAction.update({
|
||||
where: { id: action.id },
|
||||
data: { status: 'failure', log: message }
|
||||
|
||||
Reference in New Issue
Block a user