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>
54 lines
2.0 KiB
TypeScript
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' }
|
|
)
|
|
}
|
|
}
|
|
}
|