feat(patterns): GET /users/patterns — agrégation erreurs récurrentes + exercices long terme + indice de préparation (Sprint 3.6c)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hermann_Kitio 2026-04-22 22:06:14 +03:00
parent a394ce8429
commit c48ae8d443
6 changed files with 1055 additions and 0 deletions

View file

@ -609,6 +609,149 @@ export async function generateExercices(input: ExercicesInput): Promise<Exercice
}))
}
// ── Sprint 3.6c — Exercices long terme (patterns Premium) ──────────────
export interface PatternInput {
code: string
critere: Critere
frequency: number
description: string | null
}
export interface PatternExerciceItem {
code: string
critere: Critere
diagnostic: string
exercice: {
consigne: string
exemple: string
correction: string
astuce: string
}
}
const PATTERN_EXERCICES_SYSTEM = `Tu es un coach spécialisé dans la préparation au TCF Canada (Test de connaissance du français).
Un candidat commet systématiquement les mêmes erreurs sur ses 5 dernières productions écrites. Tu dois produire UN exercice ciblé par pattern d'erreur récurrent identifié.
CONTEXTE :
- Ces exercices sont des exercices LONG TERME destinés à corriger des faiblesses structurelles récurrentes.
- Ils sont DISTINCTS des exercices individuels générés après chaque correction (qui ciblent une production spécifique).
- Tu n'as PAS accès au texte du candidat. Tes exemples doivent être génériques et représentatifs de l'erreur.
RÈGLES :
1. Un exercice par pattern en entrée, dans le même ordre.
2. Le diagnostic explique en 1-2 phrases POURQUOI cette erreur est problématique pour le TCF Canada.
3. La consigne demande au candidat de corriger ou reformuler une phrase.
4. L'exemple est une phrase incorrecte illustrant le pattern (inventée, pas tirée du candidat).
5. La correction est la version correcte de l'exemple.
6. L'astuce est un procédé mnémotechnique, une règle pratique ou un réflexe de relecture que le candidat doit appliquer APRÈS avoir rédigé son texte pour détecter et corriger cette erreur lui-même. Formulée comme un conseil direct et actionnable.
Exemples d'astuces :
- Subjonctif : "Après 'bien que', 'pourvu que', 'avant que' → le verbe qui suit est TOUJOURS au subjonctif. Relisez votre texte en cherchant ces expressions."
- Accords : "Relisez chaque phrase en pointant du doigt le sujet et son verbe. S'ils sont éloignés, vérifiez l'accord."
- Connecteurs : "Après rédaction, surlignez tous vos connecteurs. Si le même revient plus de 2 fois, remplacez-en un."
7. Niveau de langue : NCLC 7-9 (ni trop simple, ni trop littéraire).
8. Les exemples doivent être en contexte TCF Canada : courriels, lettres formelles, essais argumentatifs, situations professionnelles canadiennes.
FORMAT DE SORTIE JSON strict, aucun texte avant ni après :
{
"exercises": [
{
"code": "<code_taxonomie>",
"critere": "<critere>",
"diagnostic": "<1-2 phrases>",
"exercice": {
"consigne": "<instruction au candidat>",
"exemple": "<phrase incorrecte>",
"correction": "<phrase corrigée>",
"astuce": "<procédé mnémotechnique ou réflexe de relecture>"
}
}
]
}`
function buildPatternExercicesUserPrompt(patterns: PatternInput[]): string {
const lines = patterns.map((p) => {
const desc = p.description ? ` — « ${p.description} »` : ''
return `- ${p.code} (${p.critere}) — apparu ${p.frequency}/5 fois${desc}`
})
return `Voici les patterns d'erreurs récurrents détectés sur les 5 dernières productions du candidat :
${lines.join('\n')}
Produis un exercice ciblé par pattern. JSON strict uniquement.`
}
export async function generatePatternExercices(
patterns: PatternInput[],
): Promise<PatternExerciceItem[]> {
if (patterns.length === 0) return []
const response = await fetch(`${DEEPSEEK_BASE_URL}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${DEEPSEEK_API_KEY}`,
},
body: JSON.stringify({
model: 'deepseek-chat',
messages: [
{ role: 'system', content: PATTERN_EXERCICES_SYSTEM },
{ role: 'user', content: buildPatternExercicesUserPrompt(patterns) },
],
temperature: 0.4,
response_format: { type: 'json_object' },
}),
signal: AbortSignal.timeout(20_000),
})
if (!response.ok) {
throw new Error(`DeepSeek API error: ${response.status} ${response.statusText}`)
}
const data = (await response.json()) as {
choices?: { message?: { content?: string } }[]
}
const content = data.choices?.[0]?.message?.content
if (!content) throw new Error('DeepSeek API: réponse vide')
const parsed = JSON.parse(content) as { exercises?: unknown }
if (!Array.isArray(parsed.exercises)) {
throw new Error('Réponse DeepSeek invalide : exercises doit être un tableau')
}
const out: PatternExerciceItem[] = []
for (const raw of parsed.exercises as unknown[]) {
const item = raw as Record<string, unknown>
const ex = item.exercice as Record<string, unknown> | undefined
if (
typeof item.code !== 'string' ||
typeof item.critere !== 'string' ||
typeof item.diagnostic !== 'string' ||
!ex ||
typeof ex.consigne !== 'string' ||
typeof ex.exemple !== 'string' ||
typeof ex.correction !== 'string' ||
typeof ex.astuce !== 'string'
) {
continue
}
if (!isValidCritere(item.critere)) continue
out.push({
code: item.code,
critere: item.critere,
diagnostic: item.diagnostic,
exercice: {
consigne: ex.consigne,
exemple: ex.exemple,
correction: ex.correction,
astuce: ex.astuce,
},
})
}
return out
}
// ── EO (Expression Orale) — inchangé par Sprint 3.6a ────────────────────
export interface EOCritere {