feat(corrections): Sprint 3.6a — nouveaux prompts + taxonomie erreurs + génération parallèle
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
df7ef2cc31
commit
63bc43ddcf
14 changed files with 2319 additions and 282 deletions
|
|
@ -1,7 +1,34 @@
|
|||
/**
|
||||
* Contrôleur corrections — Sprint 3.6a.
|
||||
*
|
||||
* Flux POST /corrections/ee :
|
||||
* 1. Vérifier que la simulation existe, appartient à l'utilisateur, n'est pas déjà corrigée.
|
||||
* 2. Charger le sujet (consigne + documents T3) pour alimenter le prompt maître.
|
||||
* 3. Lancer les appels DeepSeek :
|
||||
* (a) correction — await, bloque la réponse HTTP
|
||||
* (b) modèle — démarré EN MÊME TEMPS que (a) avec `nclcObtenu = nclcCible - 1`
|
||||
* comme estimation provisoire ; fire-and-forget, mise à jour async.
|
||||
* (c) exercices — démarré APRÈS (a) car dépend de `rapport.erreurs_codes` et
|
||||
* `rapport.criteres` ; fire-and-forget également.
|
||||
* 4. À réception de (a) : persister le rapport + champs associés, retourner 200.
|
||||
* 5. Les promesses (b) et (c) continuent en arrière-plan et mettent à jour Supabase.
|
||||
*
|
||||
* ⚠️ Risque connu (cf. TECH_DEBT TD-15) : si le process redémarre pendant (b)/(c),
|
||||
* les colonnes `*_status` restent en 'pending' indéfiniment.
|
||||
*/
|
||||
|
||||
import { supabase } from '../lib/supabase.js'
|
||||
import { correctEE as deepseekCorrectEE, correctEO as deepseekCorrectEO } from '../lib/deepseek.js'
|
||||
import {
|
||||
correctEE as deepseekCorrectEE,
|
||||
correctEO as deepseekCorrectEO,
|
||||
generateProductionModele,
|
||||
generateExercices,
|
||||
type CorrectionRapport,
|
||||
type EORapport,
|
||||
type NclcCible,
|
||||
type TacheEE,
|
||||
} from '../lib/deepseek.js'
|
||||
import { PLANS, type Plan } from '../lib/access.js'
|
||||
import type { EERapport, EORapport } from '../lib/deepseek.js'
|
||||
import type { AuthProfile } from '../middleware/auth.js'
|
||||
|
||||
type CorrectionError = {
|
||||
|
|
@ -11,16 +38,23 @@ type CorrectionError = {
|
|||
status: number
|
||||
}
|
||||
|
||||
export interface CorrectEEInput {
|
||||
simulationId: string
|
||||
contenu: string
|
||||
tache: TacheEE
|
||||
nclcCible: NclcCible
|
||||
}
|
||||
|
||||
export async function correctEE(
|
||||
simulationId: string,
|
||||
contenu: string,
|
||||
tache: string,
|
||||
profile: AuthProfile
|
||||
): Promise<{ data: EERapport & { simulation_id: string } } | CorrectionError> {
|
||||
input: CorrectEEInput,
|
||||
profile: AuthProfile,
|
||||
): Promise<{ data: CorrectionRapport & { simulation_id: string } } | CorrectionError> {
|
||||
const { simulationId, contenu, tache, nclcCible } = input
|
||||
|
||||
// 1. Vérifier que la production existe et appartient à l'utilisateur
|
||||
const { data: production, error: fetchError } = await supabase
|
||||
.from('productions')
|
||||
.select('id, user_id, tache')
|
||||
.select('id, user_id, tache, sujet_id, rapport')
|
||||
.eq('id', simulationId)
|
||||
.single()
|
||||
|
||||
|
|
@ -42,11 +76,59 @@ export async function correctEE(
|
|||
}
|
||||
}
|
||||
|
||||
// 2. Appeler DeepSeek pour la correction
|
||||
let rapport: EERapport
|
||||
// 2. Charger le sujet pour alimenter le prompt maître (consigne + docs T3)
|
||||
let sujetConsigne: string | null = null
|
||||
let sourceDoc1: string | null = null
|
||||
let sourceDoc2: string | null = null
|
||||
if (production.sujet_id) {
|
||||
const { data: sujetRow } = await supabase
|
||||
.from('sujets')
|
||||
.select('consigne, doc1_texte, doc2_texte')
|
||||
.eq('id', production.sujet_id)
|
||||
.single()
|
||||
if (sujetRow) {
|
||||
sujetConsigne = (sujetRow.consigne as string | null) ?? null
|
||||
sourceDoc1 = (sujetRow.doc1_texte as string | null) ?? null
|
||||
sourceDoc2 = (sujetRow.doc2_texte as string | null) ?? null
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Lancer correction + modèle EN MÊME TEMPS.
|
||||
// Le modèle démarre sans attendre la correction : on estime `nclcObtenu`
|
||||
// à `nclcCible - 1` (ordre de grandeur plausible pour un candidat visant
|
||||
// NCLC nclcCible). Cette valeur n'alimente que la phrase pédagogique du
|
||||
// prompt modèle — pas la cible, qui reste fixée à NCLC 9.
|
||||
const correctionPromise = deepseekCorrectEE({
|
||||
tache,
|
||||
contenu,
|
||||
sujet: sujetConsigne,
|
||||
sourceDoc1,
|
||||
sourceDoc2,
|
||||
nclcCible,
|
||||
})
|
||||
|
||||
const nclcObtenuEstime = nclcCible - 1
|
||||
void runModeleJob({
|
||||
simulationId,
|
||||
tache,
|
||||
sujet: sujetConsigne,
|
||||
texte: contenu,
|
||||
nclcObtenu: nclcObtenuEstime,
|
||||
})
|
||||
|
||||
let rapport: CorrectionRapport
|
||||
try {
|
||||
rapport = await deepseekCorrectEE(contenu, tache)
|
||||
} catch {
|
||||
rapport = await correctionPromise
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
const stack = err instanceof Error ? err.stack : undefined
|
||||
console.error('[correctionController.correctEE] correction failed', {
|
||||
simulationId,
|
||||
tache,
|
||||
nclcCible,
|
||||
message,
|
||||
stack,
|
||||
})
|
||||
return {
|
||||
error: true,
|
||||
code: 'INTERNAL_ERROR',
|
||||
|
|
@ -55,13 +137,20 @@ export async function correctEE(
|
|||
}
|
||||
}
|
||||
|
||||
// 3. Mettre à jour la production dans Supabase
|
||||
// 4. Persister la correction (statuts modèle/exercices : pending — les jobs sont lancés juste après)
|
||||
const { error: updateError } = await supabase
|
||||
.from('productions')
|
||||
.update({
|
||||
score: rapport.score,
|
||||
nclc: rapport.nclc,
|
||||
nclc_cible: rapport.nclc_cible,
|
||||
revelation: rapport.revelation,
|
||||
diagnostic: rapport.diagnostic,
|
||||
conseil_nclc: rapport.conseil_nclc,
|
||||
erreurs_codes: rapport.erreurs_codes,
|
||||
rapport: JSON.stringify(rapport),
|
||||
exercices_status: 'pending',
|
||||
modele_status: 'pending',
|
||||
})
|
||||
.eq('id', simulationId)
|
||||
|
||||
|
|
@ -74,7 +163,11 @@ export async function correctEE(
|
|||
}
|
||||
}
|
||||
|
||||
// 4. Incrémenter simulations_used si le plan a une limite (non bloquant).
|
||||
// 5. Lancer les exercices maintenant qu'on a rapport.erreurs_codes + criteres.
|
||||
// Ne JAMAIS await — cette promesse vit après la réponse HTTP.
|
||||
void runExercicesJob({ simulationId, tache, rapport })
|
||||
|
||||
// 6. Incrémenter simulations_used si le plan a une limite (non bloquant).
|
||||
if (PLANS[profile.plan as Plan].simulations_lifetime !== null) {
|
||||
await supabase
|
||||
.from('profiles')
|
||||
|
|
@ -82,17 +175,69 @@ export async function correctEE(
|
|||
.eq('id', profile.id)
|
||||
}
|
||||
|
||||
// 5. Retourner le rapport complet enrichi avec simulation_id
|
||||
return { data: { ...rapport, simulation_id: simulationId } }
|
||||
}
|
||||
|
||||
// ── Jobs asynchrones — modèle + exercices ───────────────────────────────
|
||||
|
||||
interface ModeleJobInput {
|
||||
simulationId: string
|
||||
tache: TacheEE
|
||||
sujet: string | null
|
||||
texte: string
|
||||
nclcObtenu: number
|
||||
}
|
||||
|
||||
async function runModeleJob(input: ModeleJobInput): Promise<void> {
|
||||
const { simulationId, tache, sujet, texte, nclcObtenu } = input
|
||||
try {
|
||||
const modele = await generateProductionModele({ tache, sujet, texte, nclcObtenu })
|
||||
await supabase
|
||||
.from('productions')
|
||||
.update({ modele, modele_status: 'ready' })
|
||||
.eq('id', simulationId)
|
||||
} catch {
|
||||
await supabase
|
||||
.from('productions')
|
||||
.update({ modele_status: 'error' })
|
||||
.eq('id', simulationId)
|
||||
}
|
||||
}
|
||||
|
||||
interface ExercicesJobInput {
|
||||
simulationId: string
|
||||
tache: TacheEE
|
||||
rapport: CorrectionRapport
|
||||
}
|
||||
|
||||
async function runExercicesJob(input: ExercicesJobInput): Promise<void> {
|
||||
const { simulationId, tache, rapport } = input
|
||||
try {
|
||||
const exercices = await generateExercices({
|
||||
tache,
|
||||
erreursCodes: rapport.erreurs_codes,
|
||||
criteres: rapport.criteres,
|
||||
})
|
||||
await supabase
|
||||
.from('productions')
|
||||
.update({ exercices, exercices_status: 'ready' })
|
||||
.eq('id', simulationId)
|
||||
} catch {
|
||||
await supabase
|
||||
.from('productions')
|
||||
.update({ exercices_status: 'error' })
|
||||
.eq('id', simulationId)
|
||||
}
|
||||
}
|
||||
|
||||
// ── EO — inchangé par Sprint 3.6a ───────────────────────────────────────
|
||||
|
||||
export async function correctEO(
|
||||
simulationId: string,
|
||||
transcript: string,
|
||||
tache: string,
|
||||
profile: AuthProfile
|
||||
profile: AuthProfile,
|
||||
): Promise<{ data: EORapport & { simulation_id: string } } | CorrectionError> {
|
||||
// 1. Vérifier que la production existe et appartient à l'utilisateur
|
||||
const { data: production, error: fetchError } = await supabase
|
||||
.from('productions')
|
||||
.select('id, user_id, tache')
|
||||
|
|
@ -117,7 +262,6 @@ export async function correctEO(
|
|||
}
|
||||
}
|
||||
|
||||
// 2. Appeler DeepSeek pour la correction EO
|
||||
let rapport: EORapport
|
||||
try {
|
||||
rapport = await deepseekCorrectEO(transcript, tache)
|
||||
|
|
@ -130,7 +274,6 @@ export async function correctEO(
|
|||
}
|
||||
}
|
||||
|
||||
// 3. Mettre à jour la production dans Supabase
|
||||
const { error: updateError } = await supabase
|
||||
.from('productions')
|
||||
.update({
|
||||
|
|
@ -150,7 +293,6 @@ export async function correctEO(
|
|||
}
|
||||
}
|
||||
|
||||
// 4. Incrémenter simulations_used si le plan a une limite (non bloquant).
|
||||
if (PLANS[profile.plan as Plan].simulations_lifetime !== null) {
|
||||
await supabase
|
||||
.from('profiles')
|
||||
|
|
@ -158,6 +300,5 @@ export async function correctEO(
|
|||
.eq('id', profile.id)
|
||||
}
|
||||
|
||||
// 5. Retourner le rapport complet enrichi avec simulation_id
|
||||
return { data: { ...rapport, simulation_id: simulationId } }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue