feat: POST /corrections/ee — DeepSeek rapport complet — 73/73 tests
This commit is contained in:
parent
a6ee76d4a8
commit
77d5a8373e
6 changed files with 385 additions and 23 deletions
130
src/lib/__tests__/deepseek.test.ts
Normal file
130
src/lib/__tests__/deepseek.test.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
const VALID_RAPPORT = {
|
||||
score: 14.5,
|
||||
nclc: 8,
|
||||
criteres: [
|
||||
{ nom: 'Coherence et cohesion', score: 4, commentaire: 'Bonne organisation.' },
|
||||
{ nom: 'Lexique', score: 3, commentaire: 'Vocabulaire correct mais limite.' },
|
||||
{ nom: 'Morphosyntaxe', score: 4, commentaire: 'Structures variees.' },
|
||||
{ nom: 'Pertinence', score: 3.5, commentaire: 'Adequation partielle a la consigne.' },
|
||||
],
|
||||
erreurs: ['Connecteurs logiques insuffisants', 'Quelques fautes accord'],
|
||||
production_modele: 'Texte modele corrige ici.',
|
||||
suggestions_idees: ['Developper argumentation', 'Ajouter des exemples concrets'],
|
||||
exercices: ['Exercice connecteurs logiques', 'Exercice accords sujet-verbe'],
|
||||
}
|
||||
|
||||
function mockFetchSuccess(rapport: unknown) {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
choices: [{ message: { content: JSON.stringify(rapport) } }],
|
||||
}),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
describe('deepseek.correctEE', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('retourne un rapport avec la bonne structure', async () => {
|
||||
mockFetchSuccess(VALID_RAPPORT)
|
||||
const { correctEE } = await import('../deepseek')
|
||||
|
||||
const rapport = await correctEE('Mon texte de test', 'EE_T1')
|
||||
|
||||
expect(rapport).toHaveProperty('score')
|
||||
expect(rapport).toHaveProperty('nclc')
|
||||
expect(rapport).toHaveProperty('criteres')
|
||||
expect(rapport).toHaveProperty('erreurs')
|
||||
expect(rapport).toHaveProperty('production_modele')
|
||||
expect(rapport).toHaveProperty('suggestions_idees')
|
||||
expect(rapport).toHaveProperty('exercices')
|
||||
expect(rapport.criteres).toHaveLength(4)
|
||||
expect(Array.isArray(rapport.erreurs)).toBe(true)
|
||||
expect(Array.isArray(rapport.suggestions_idees)).toBe(true)
|
||||
expect(Array.isArray(rapport.exercices)).toBe(true)
|
||||
})
|
||||
|
||||
it('score est entre 0 et 20', async () => {
|
||||
mockFetchSuccess(VALID_RAPPORT)
|
||||
const { correctEE } = await import('../deepseek')
|
||||
|
||||
const rapport = await correctEE('Mon texte', 'EE_T1')
|
||||
|
||||
expect(rapport.score).toBeGreaterThanOrEqual(0)
|
||||
expect(rapport.score).toBeLessThanOrEqual(20)
|
||||
})
|
||||
|
||||
it('nclc est entre 4 et 12', async () => {
|
||||
mockFetchSuccess(VALID_RAPPORT)
|
||||
const { correctEE } = await import('../deepseek')
|
||||
|
||||
const rapport = await correctEE('Mon texte', 'EE_T2')
|
||||
|
||||
expect(rapport.nclc).toBeGreaterThanOrEqual(4)
|
||||
expect(rapport.nclc).toBeLessThanOrEqual(12)
|
||||
})
|
||||
|
||||
it('lance une erreur si score hors bornes', async () => {
|
||||
mockFetchSuccess({ ...VALID_RAPPORT, score: 25 })
|
||||
const { correctEE } = await import('../deepseek')
|
||||
|
||||
await expect(correctEE('Texte', 'EE_T1')).rejects.toThrow('Score invalide')
|
||||
})
|
||||
|
||||
it('lance une erreur si nclc hors bornes', async () => {
|
||||
mockFetchSuccess({ ...VALID_RAPPORT, nclc: 2 })
|
||||
const { correctEE } = await import('../deepseek')
|
||||
|
||||
await expect(correctEE('Texte', 'EE_T1')).rejects.toThrow('NCLC invalide')
|
||||
})
|
||||
|
||||
it('erreur HTTP depuis DeepSeek API', async () => {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
})
|
||||
)
|
||||
const { correctEE } = await import('../deepseek')
|
||||
|
||||
await expect(correctEE('Texte', 'EE_T1')).rejects.toThrow('DeepSeek API error')
|
||||
})
|
||||
|
||||
it('erreur si reponse vide', async () => {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ choices: [{ message: { content: '' } }] }),
|
||||
})
|
||||
)
|
||||
const { correctEE } = await import('../deepseek')
|
||||
|
||||
await expect(correctEE('Texte', 'EE_T1')).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('lance une erreur si DeepSeek retourne du JSON invalide', async () => {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
choices: [{ message: { content: 'ceci nest pas du json' } }],
|
||||
}),
|
||||
})
|
||||
)
|
||||
const { correctEE } = await import('../deepseek')
|
||||
|
||||
await expect(correctEE('Texte', 'EE_T1')).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
91
src/lib/deepseek.ts
Normal file
91
src/lib/deepseek.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
const DEEPSEEK_API_KEY = process.env.DEEPSEEK_API_KEY ?? ''
|
||||
const DEEPSEEK_BASE_URL = 'https://api.deepseek.com'
|
||||
|
||||
export interface EECritere {
|
||||
nom: string
|
||||
score: number
|
||||
commentaire: string
|
||||
}
|
||||
|
||||
export interface EERapport {
|
||||
score: number
|
||||
nclc: number
|
||||
criteres: EECritere[]
|
||||
erreurs: string[]
|
||||
production_modele: string
|
||||
suggestions_idees: string[]
|
||||
exercices: string[]
|
||||
}
|
||||
|
||||
const SYSTEM_PROMPT = `Tu es un examinateur officiel du TCF Canada (Test de connaissance du français).
|
||||
Tu évalues une production écrite selon les 4 critères officiels de l'Expression Écrite :
|
||||
1. Cohérence et cohésion
|
||||
2. Lexique (étendue et maîtrise du vocabulaire)
|
||||
3. Morphosyntaxe (grammaire et structures)
|
||||
4. Pertinence (adéquation à la consigne)
|
||||
|
||||
Tu dois retourner un JSON strict avec cette structure exacte :
|
||||
{
|
||||
"score": <number 0-20>,
|
||||
"nclc": <number 4-12>,
|
||||
"criteres": [
|
||||
{ "nom": "Cohérence et cohésion", "score": <number 0-5>, "commentaire": "<string>" },
|
||||
{ "nom": "Lexique", "score": <number 0-5>, "commentaire": "<string>" },
|
||||
{ "nom": "Morphosyntaxe", "score": <number 0-5>, "commentaire": "<string>" },
|
||||
{ "nom": "Pertinence", "score": <number 0-5>, "commentaire": "<string>" }
|
||||
],
|
||||
"erreurs": ["<erreur 1>", "<erreur 2>", ...],
|
||||
"production_modele": "<texte modèle corrigé>",
|
||||
"suggestions_idees": ["<idée 1>", "<idée 2>", ...],
|
||||
"exercices": ["<exercice recommandé 1>", "<exercice recommandé 2>", ...]
|
||||
}
|
||||
|
||||
Règles :
|
||||
- score est la note globale sur 20
|
||||
- nclc est le niveau NCLC estimé (entre 4 et 12)
|
||||
- Chaque critère a un score de 0 à 5
|
||||
- Retourne UNIQUEMENT le JSON, sans texte avant ni après`
|
||||
|
||||
export async function correctEE(contenu: string, tache: string): Promise<EERapport> {
|
||||
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: SYSTEM_PROMPT },
|
||||
{
|
||||
role: 'user',
|
||||
content: `Tâche : ${tache}\n\nProduction de l'étudiant :\n${contenu}`,
|
||||
},
|
||||
],
|
||||
temperature: 0.3,
|
||||
response_format: { type: 'json_object' },
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`DeepSeek API error: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const content = data.choices?.[0]?.message?.content
|
||||
|
||||
if (!content) {
|
||||
throw new Error('DeepSeek API: réponse vide')
|
||||
}
|
||||
|
||||
const rapport: EERapport = JSON.parse(content)
|
||||
|
||||
if (rapport.score < 0 || rapport.score > 20) {
|
||||
throw new Error(`Score invalide: ${rapport.score} (attendu 0-20)`)
|
||||
}
|
||||
if (rapport.nclc < 4 || rapport.nclc > 12) {
|
||||
throw new Error(`NCLC invalide: ${rapport.nclc} (attendu 4-12)`)
|
||||
}
|
||||
|
||||
return rapport
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue