rubis/apps/api/app/controllers/ai_controller.ts
ordinarthur 5c7dbc2eba fix(plans/ai): contexte plan + interdiction Mustache sections
Bugs remontés sur les générations IA :
- Le modèle utilisait `{{#var}}...{{/var}}` (sections Mustache) pour
  gérer les fallbacks de prénom — notre interpréteur ne fait que de
  la substitution simple, donc le charabia s'affichait dans l'email.
- La signature était dupliquée : l'IA écrivait le nom à la main puis
  ajoutait `{{signature}}`.
- Le contexte du plan (nom + description) n'était pas transmis, donc
  les générations étaient déconnectées du sens du plan parent.

Corrections du SYSTEM_PROMPT :
- Section "Syntaxe des variables" explicite : substitution simple
  uniquement, INTERDICTION des `{{#...}}` / `{{^...}}` / conditionnels
- Section "Tu n'es PAS obligé d'utiliser toutes les variables"
  → l'IA pioche celles qui rendent le message naturel
- Règle : terminer toujours par {{signature}} sur sa propre ligne,
  ne JAMAIS réécrire le nom de l'expéditeur après (la variable
  contient déjà nom + entreprise + formule de politesse)

Backend
- ai_relance_generator : type GenerateRelanceInput accepte planName
  + planDescription (à la place de l'ancien planContext fourre-tout)
- user message structuré en sections # Plan parent / # Cette relance
  / # Brief de l'utilisateur, plus lisible pour le modèle
- ai_controller validator : accepte planName + planDescription

Frontend
- AiGenerateModal accepte planName + planDescription en props et
  les passe à l'API
- Affiche le nom du plan dans la description de la modale
- Bloc dépliable "Variables que l'IA peut insérer (sans obligation)"
  pour montrer à l'utilisateur ce qui est dispo
- StepMessages passe draft.name + draft.description au modal
- MSW handler aligné sur le nouveau contrat

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 23:48:57 +02:00

54 lines
2.0 KiB
TypeScript

import vine from '@vinejs/vine'
import { Exception } from '@adonisjs/core/exceptions'
import type { HttpContext } from '@adonisjs/core/http'
import { generateRelance } from '#services/ai_relance_generator'
const RELANCE_TONES = ['amical', 'courtois', 'ferme', 'mise_en_demeure'] as const
const generateRelanceValidator = vine.create({
tone: vine.enum(RELANCE_TONES),
offsetDays: vine.number().min(-30).max(180),
// Brief libre. On accepte vide : Mistral génère alors un message standard
// pour la tonalité + timing donnés.
prompt: vine.string().maxLength(1000).optional(),
// Contexte du plan parent — nom + description, pour cohérence inter-étapes.
planName: vine.string().maxLength(80).optional(),
planDescription: vine.string().maxLength(500).optional(),
})
/**
* Endpoints IA. V1 : uniquement génération de templates de relance pour le
* wizard de création de plan custom. Mistral est déjà utilisé pour l'OCR
* (cf. mistral_ocr_provider.ts) — on réutilise la même clé API.
*/
export default class AiController {
/**
* POST /ai/generate-relance
*
* Génère subject + body avec des placeholders Mustache prêts à insérer.
* L'utilisateur peut régénérer pour avoir une variante.
*/
async generateRelance({ auth, request, response }: HttpContext) {
auth.getUserOrFail() // auth requise
const payload = await request.validateUsing(generateRelanceValidator)
try {
const result = await generateRelance({
tone: payload.tone,
offsetDays: payload.offsetDays,
prompt: payload.prompt ?? '',
planName: payload.planName,
planDescription: payload.planDescription,
})
return response.json({ data: result })
} catch (err) {
// On wrap pour passer par le handler global et garder le format
// d'erreur uniforme côté front.
throw new Exception(
err instanceof Error ? err.message : 'Génération IA indisponible',
{ status: 502, code: 'ai_generation_failed' }
)
}
}
}