wilenPxs commited on
Commit
cce0786
·
1 Parent(s): fbdaead

En-tete {exercise} complet (metadonnees standard, niveau mappe, reprise source) + option Elementaire

Browse files
app/pipeline/generate.py CHANGED
@@ -14,7 +14,7 @@ import logging
14
  import re
15
  from typing import Callable, Optional
16
 
17
- from app.config import USE_REASONING
18
  from app.llm.client import process_with_openrouter
19
  from app.pipeline.postprocess import (
20
  PYTHON_FENCE_RE,
@@ -89,25 +89,106 @@ def split_original_questions(content: str) -> tuple[str, str, list[str]]:
89
  return metadata, enonce, segments
90
 
91
 
92
- def build_exercise_metadata(metadata: str, lists_of_notions: str) -> str:
93
- """En-tête déterministe (aucun LLM) injecte les notions à côté de
94
- :involvedConcepts: TYPE_BAC."""
95
- cleaned = strip_python_block_from_text(metadata)
96
- if lists_of_notions and lists_of_notions not in cleaned:
97
- cleaned = re.sub(
98
- r":involvedConcepts:\s*TYPE_BAC(?![\w,])",
99
- f":involvedConcepts: TYPE_BAC,{lists_of_notions}",
100
- cleaned,
101
- count=1,
102
- )
103
- return cleaned.rstrip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
 
105
 
106
  def assemble_exercise(metadata_header: str, pair_blocks: list[str]) -> str:
107
- """Header + blocs de paires + fence finale 5 backticks. Les fences {python}
108
- sont normalisées à la convention plateforme (4 backticks)."""
 
 
 
 
109
  parts = [metadata_header.rstrip()]
110
- parts.extend(block.strip() for block in pair_blocks)
 
 
 
111
  parts.append("`````")
112
  assembled = "\n\n".join(parts)
113
  assembled = assembled.replace("{align*}", "{equation*}")
 
14
  import re
15
  from typing import Callable, Optional
16
 
17
+ from app.config import EXERCISE_FENCE_BACKTICKS, USE_REASONING
18
  from app.llm.client import process_with_openrouter
19
  from app.pipeline.postprocess import (
20
  PYTHON_FENCE_RE,
 
89
  return metadata, enonce, segments
90
 
91
 
92
+ # Le sélecteur UI envoie ""/"Intermediate"/"Advanced"/"Elementary" ; la
93
+ # plateforme attend Elementary/Intermediary/Advanced (noter Intermediary).
94
+ _LEVEL_MAP = {
95
+ "": "",
96
+ "elementary": "Elementary",
97
+ "intermediate": "Intermediary",
98
+ "intermediary": "Intermediary",
99
+ "advanced": "Advanced",
100
+ }
101
+
102
+ # Ordre canonique des options de l'en-tête {exercise} (gabarit plateforme).
103
+ _HEADER_FIELDS = [
104
+ "id", "title", "modules", "recommendedExecutionTime", "level",
105
+ "chap", "involvedConcepts", "originalSource", "visibility",
106
+ ]
107
+
108
+
109
+ def _parse_source_options(metadata: str) -> dict:
110
+ """Extrait les `:option: valeur` de l'en-tête source (s'il en a un)."""
111
+ opts: dict[str, str] = {}
112
+ for m in re.finditer(r"^[ \t]*:([A-Za-z_][\w-]*):[ \t]*(.*?)[ \t]*$",
113
+ metadata or "", re.MULTILINE):
114
+ opts[m.group(1)] = m.group(2).strip()
115
+ return opts
116
+
117
+
118
+ def _norm_level(value: str) -> str:
119
+ return _LEVEL_MAP.get((value or "").strip().lower(), (value or "").strip())
120
+
121
+
122
+ def build_exercise_metadata(
123
+ metadata: str,
124
+ lists_of_notions: str,
125
+ analysis: dict | None = None,
126
+ level: str = "",
127
+ ) -> str:
128
+ """Construit TOUJOURS un en-tête `{exercise}` complet et bien formé
129
+ (5 backticks englobant tout l'exercice — le bloc Python à 4 backticks vient
130
+ juste après). Règles par champ (consigne utilisateur) :
131
+ • :id: vide (auto-attribution plateforme)
132
+ • :title: titre de la source sinon titre déduit de l'analyse
133
+ • :modules: / :chap: repris de la source si présents, sinon vides
134
+ • :recommendedExecutionTime: source sinon défaut (≈ 3 min/question)
135
+ • :level: selon le niveau SÉLECTIONNÉ dans l'UI (mappé Intermediary),
136
+ sinon niveau de la source, sinon Elementary
137
+ • :involvedConcepts: notions retrouvées (RAG) sinon concepts de la source
138
+ • :originalSource: repris de la source si présent
139
+ • :visibility: All par défaut
140
+ Reprend le MAXIMUM des métadonnées présentes dans la source."""
141
+ analysis = analysis or {}
142
+ src = _parse_source_options(metadata)
143
+
144
+ title = src.get("title") or analysis.get("exercise_title") or "Exercice"
145
+
146
+ nb_q = analysis.get("nb_questions") or 0
147
+ try:
148
+ nb_q = int(nb_q)
149
+ except (TypeError, ValueError):
150
+ nb_q = 0
151
+ ret = src.get("recommendedExecutionTime") or str(max(5, nb_q * 3) if nb_q else 10)
152
+
153
+ lvl = _norm_level(level) or _norm_level(src.get("level", "")) or "Elementary"
154
+
155
+ concepts = (lists_of_notions or "").strip() or src.get("involvedConcepts", "")
156
+ # "TYPE_BAC" est un placeholder de gabarit, pas un vrai concept → on l'ôte.
157
+ concepts = concepts.replace("TYPE_BAC,", "").replace("TYPE_BAC", "").strip().strip(",")
158
+
159
+ values = {
160
+ "id": "", # vide → la plateforme l'attribue
161
+ "title": title,
162
+ "modules": src.get("modules", ""), # repris si présent, sinon vide
163
+ "recommendedExecutionTime": ret,
164
+ "level": lvl,
165
+ "chap": src.get("chap", ""), # repris si présent, sinon vide
166
+ "involvedConcepts": concepts,
167
+ "originalSource": src.get("originalSource", ""),
168
+ "visibility": src.get("visibility") or "All",
169
+ }
170
+
171
+ fence = "`" * EXERCISE_FENCE_BACKTICKS
172
+ lines = [f"{fence}{{exercise}}"]
173
+ lines += [f":{k}: {values[k]}".rstrip() for k in _HEADER_FIELDS]
174
+ return "\n".join(lines)
175
+
176
+
177
+ _ENVELOPE_FENCE_RE = re.compile(r"(?m)^`{5,}(\{exercise\})?[ \t]*$")
178
 
179
 
180
  def assemble_exercise(metadata_header: str, pair_blocks: list[str]) -> str:
181
+ """En-tête `{exercise}` (posé par build_exercise_metadata) + blocs de paires
182
+ + fence finale 5 backticks qui referme l'enveloppe APRÈS la dernière question.
183
+ Les fences {python} sont normalisées à 4 backticks (convention plateforme).
184
+ Filet : on retire toute ligne d'enveloppe 5-backticks que le LLM aurait
185
+ reproduite dans un bloc (on possède l'enveloppe nous-mêmes) — sinon on
186
+ obtiendrait une double enveloppe / des backticks non décroissants."""
187
  parts = [metadata_header.rstrip()]
188
+ for block in pair_blocks:
189
+ cleaned = _ENVELOPE_FENCE_RE.sub("", block).strip()
190
+ if cleaned:
191
+ parts.append(cleaned)
192
  parts.append("`````")
193
  assembled = "\n\n".join(parts)
194
  assembled = assembled.replace("{align*}", "{equation*}")
app/pipeline/orchestrator.py CHANGED
@@ -133,7 +133,7 @@ def run_exercise(
133
 
134
  # ── 2. Génération par paires ─────────────────────────────────────────────
135
  metadata, enonce, question_segments = split_original_questions(content)
136
- exercise_header = build_exercise_metadata(metadata, lists_of_notions)
137
 
138
  # Contexte fonctions = catalogue CURÉ (domaine détecté) + hits RAG FAISS.
139
  # Le catalogue curé donne « quel helper pour quel besoin » + couvre les
 
133
 
134
  # ── 2. Génération par paires ─────────────────────────────────────────────
135
  metadata, enonce, question_segments = split_original_questions(content)
136
+ exercise_header = build_exercise_metadata(metadata, lists_of_notions, analysis, level)
137
 
138
  # Contexte fonctions = catalogue CURÉ (domaine détecté) + hits RAG FAISS.
139
  # Le catalogue curé donne « quel helper pour quel besoin » + couvre les
app/server.py CHANGED
@@ -41,7 +41,7 @@ _JOBS: dict = {}
41
  _JOBS_LOCK = threading.Lock()
42
 
43
  VALID_LANGS = ("fr", "en", "both")
44
- VALID_LEVELS = ("", "Intermediate", "Advanced")
45
 
46
 
47
  def _set_job(job_id: str, **kwargs):
 
41
  _JOBS_LOCK = threading.Lock()
42
 
43
  VALID_LANGS = ("fr", "en", "both")
44
+ VALID_LEVELS = ("", "Elementary", "Intermediate", "Advanced")
45
 
46
 
47
  def _set_job(job_id: str, **kwargs):
app/web/templates/index.html CHANGED
@@ -359,6 +359,7 @@
359
  <label class="field-label" for="level">Niveau</label>
360
  <select id="level">
361
  <option value="">—</option>
 
362
  <option value="Intermediate">Intermédiaire</option>
363
  <option value="Advanced">Avancé</option>
364
  </select>
 
359
  <label class="field-label" for="level">Niveau</label>
360
  <select id="level">
361
  <option value="">—</option>
362
+ <option value="Elementary">Élémentaire</option>
363
  <option value="Intermediate">Intermédiaire</option>
364
  <option value="Advanced">Avancé</option>
365
  </select>