Initial Persp generation
This commit is contained in:
+132
-9
@@ -42,11 +42,32 @@ class ExamSolutions(BaseModel):
|
||||
# --- Modèles pour la Requête 3 ---
|
||||
class ExtractedContext(BaseModel):
|
||||
target_question_label: str = Field(description="The exact label of the FIRST question that comes immediately AFTER this information in the exam.")
|
||||
last_question_label: str = Field(description="The exact label of the LAST question that uses or relies on this information.")
|
||||
context_content: str = Field(description="The source text of the definitions, notations, or hypotheses, extracted from the enonce.")
|
||||
|
||||
class ExamContext(BaseModel):
|
||||
contexts: List[ExtractedContext]
|
||||
|
||||
# --- Modèles pour la Requête 4 (Barèmes) ---
|
||||
class RubricItem(BaseModel):
|
||||
label: str = Field(description="The exact label of the question.")
|
||||
rubric_content: str = Field(description="Le barème détaillé en français.")
|
||||
|
||||
class GroupRubrics(BaseModel):
|
||||
rubrics: List[RubricItem]
|
||||
|
||||
PROMPT_4 = """Je te fournis les questions, le contexte éventuel, et les corrections pour un groupe de questions d'un examen.
|
||||
Ta tâche :
|
||||
Établir un barème de correction détaillé en français pour CHAQUE question.
|
||||
Chaque question DOIT être notée sur exactement 4 points. Propose une répartition logique de ces points.
|
||||
Par exemple :
|
||||
- Au moins 2 points si le résultat est correct.
|
||||
- Mettre la moitié des points si le raisonnement est correct mais pas le résultat.
|
||||
- Retirer 1.5 points si les hypothèses d'un théorème ou d'une question précédente ne sont pas vérifiées.
|
||||
|
||||
Renvoie le résultat sous forme de liste JSON correspondant aux labels des questions fournies.
|
||||
"""
|
||||
|
||||
# --- Modèle fusionné (pour le reste du script) ---
|
||||
class QuestionItem(BaseModel):
|
||||
label: str
|
||||
@@ -54,6 +75,8 @@ class QuestionItem(BaseModel):
|
||||
solution_content: str
|
||||
|
||||
class ContextItem(BaseModel):
|
||||
target_question_label: str
|
||||
last_question_label: str
|
||||
content: str # Juste une string encapsulée pour le différencier facilement
|
||||
|
||||
class ExamExtraction(BaseModel):
|
||||
@@ -91,7 +114,9 @@ PROMPT_3 = """I am providing:
|
||||
|
||||
Your task:
|
||||
Extract important information necessary to understand the questions (e.g., definitions of objects, global notations, hypotheses, context) that are NOT part of the question texts themselves.
|
||||
For each extracted piece of information, identify the label of the FIRST question that comes immediately AFTER this information in the exam.
|
||||
For each extracted piece of information, identify:
|
||||
1. The label of the FIRST question that comes immediately AFTER this information in the exam.
|
||||
2. The label of the LAST question that uses or relies on this information.
|
||||
Return the result as a JSON list.
|
||||
"""
|
||||
|
||||
@@ -247,21 +272,45 @@ def process_exam(folder_path: str):
|
||||
# ==========================================
|
||||
sol_map = {s.label: s.solution_content for s in solutions_data.solutions}
|
||||
|
||||
|
||||
# Map labels to their index to validate ordering
|
||||
label_to_idx = {q.label: i for i, q in enumerate(questions_data.questions)}
|
||||
|
||||
for c in context_data.contexts:
|
||||
first_idx = label_to_idx.get(c.target_question_label, -1)
|
||||
last_idx = label_to_idx.get(c.last_question_label, -1)
|
||||
|
||||
# Enforce LAST is after (or equal to) FIRST
|
||||
if first_idx != -1 and last_idx != -1 and last_idx < first_idx:
|
||||
print(f"Warning: LAST question ({c.last_question_label}) is before FIRST ({c.target_question_label}). Fixing.")
|
||||
c.last_question_label = c.target_question_label
|
||||
elif last_idx == -1: # Fallback if invalid
|
||||
c.last_question_label = c.target_question_label
|
||||
|
||||
# Grouper les contextes par label cible
|
||||
ctx_map = {}
|
||||
for c in context_data.contexts:
|
||||
if c.target_question_label in ctx_map:
|
||||
ctx_map[c.target_question_label] += "\n\n" + c.context_content
|
||||
ctx_map[c.target_question_label]['content'] += "\n\n" + c.context_content
|
||||
# Keep the furthest LAST question label
|
||||
curr_last = ctx_map[c.target_question_label]['last']
|
||||
if label_to_idx.get(c.last_question_label, -1) > label_to_idx.get(curr_last, -1):
|
||||
ctx_map[c.target_question_label]['last'] = c.last_question_label
|
||||
else:
|
||||
ctx_map[c.target_question_label] = c.context_content
|
||||
ctx_map[c.target_question_label] = {
|
||||
'content': c.context_content,
|
||||
'last': c.last_question_label
|
||||
}
|
||||
|
||||
merged_items = []
|
||||
for q in questions_data.questions:
|
||||
# 1. S'il y a un contexte pour cette question, on l'insère d'abord dans la liste
|
||||
if q.label in ctx_map:
|
||||
merged_items.append(ContextItem(content=ctx_map[q.label]))
|
||||
merged_items.append(ContextItem(
|
||||
target_question_label=q.label,
|
||||
last_question_label=ctx_map[q.label]['last'],
|
||||
content=ctx_map[q.label]['content']
|
||||
))
|
||||
|
||||
# 2. Puis on ajoute la question
|
||||
sol_content = sol_map.get(q.label, "")
|
||||
merged_items.append(QuestionItem(
|
||||
label=q.label,
|
||||
@@ -328,9 +377,11 @@ def process_exam(folder_path: str):
|
||||
# ---- Transform labels, and check uniqueness
|
||||
|
||||
seen_labels = set()
|
||||
label_updates = {}
|
||||
for group in initial_groups:
|
||||
for item in group:
|
||||
if isinstance(item, QuestionItem):
|
||||
orig_label = item.label
|
||||
# 1. Transform label
|
||||
item.label = item.label.replace("Exercice", "Ex")
|
||||
item.label = item.label.replace(".", ")")
|
||||
@@ -339,7 +390,14 @@ def process_exam(folder_path: str):
|
||||
while item.label in seen_labels:
|
||||
item.label = f"XX{item.label}"
|
||||
seen_labels.add(item.label)
|
||||
label_updates[orig_label] = item.label
|
||||
|
||||
# Sync the modified labels to ContextItem
|
||||
for group in initial_groups:
|
||||
for item in group:
|
||||
if isinstance(item, ContextItem):
|
||||
item.target_question_label = label_updates.get(item.target_question_label, item.target_question_label)
|
||||
item.last_question_label = label_updates.get(item.last_question_label, item.last_question_label)
|
||||
|
||||
# --- WRITE TEXT FILES ---
|
||||
print(f"Writing items files to {items_file.name} and {full_items_file.name}...")
|
||||
@@ -438,7 +496,8 @@ def process_exam(folder_path: str):
|
||||
edited_content = edited_content_raw.replace(' \\n ', '\n')
|
||||
|
||||
if orig_idx < len(extracted_data.items):
|
||||
orig_item = extracted_data.items[orig_idx]
|
||||
orig_item = extracted_data.items[orig_idx] # <-- Add this line
|
||||
|
||||
if isinstance(orig_item, QuestionItem):
|
||||
current_group.append(QuestionItem(
|
||||
label=new_label,
|
||||
@@ -446,7 +505,11 @@ def process_exam(folder_path: str):
|
||||
solution_content=orig_item.solution_content
|
||||
))
|
||||
elif isinstance(orig_item, ContextItem):
|
||||
current_group.append(ContextItem(content=edited_content))
|
||||
current_group.append(ContextItem(
|
||||
target_question_label=orig_item.target_question_label,
|
||||
last_question_label=orig_item.last_question_label,
|
||||
content=edited_content
|
||||
))
|
||||
orig_idx += 1
|
||||
|
||||
if current_group:
|
||||
@@ -467,7 +530,8 @@ def process_exam(folder_path: str):
|
||||
sol_dir = folder / "Sol"
|
||||
text2_dir = folder / "Text2"
|
||||
sol2_dir = folder / "Sol2"
|
||||
dirs = [text_dir, sol_dir, text2_dir, sol2_dir]
|
||||
persp_dir = folder / "Persp"
|
||||
dirs = [text_dir, sol_dir, text2_dir, sol2_dir, persp_dir]
|
||||
|
||||
import shutil
|
||||
# Ask only if any directory already exists
|
||||
@@ -502,6 +566,47 @@ def process_exam(folder_path: str):
|
||||
if not labels:
|
||||
continue # Skip if a group has no questions (only contexts)
|
||||
|
||||
# ==========================================
|
||||
# REQUÊTE 4 : Génération du Barème pour le groupe
|
||||
# ==========================================
|
||||
group_text_parts = []
|
||||
for item in group:
|
||||
if isinstance(item, QuestionItem):
|
||||
group_text_parts.append(f"Question [{item.label}]:\n{item.question_content}\nCorrection [{item.label}]:\n{item.solution_content}")
|
||||
elif isinstance(item, ContextItem):
|
||||
group_text_parts.append(f"Contexte (Cible: {item.target_question_label}):\n{item.content}")
|
||||
|
||||
group_context_text = "\n\n---\n\n".join(group_text_parts)
|
||||
|
||||
contents_4 = [
|
||||
types.Content(
|
||||
role="user",
|
||||
parts=[
|
||||
types.Part.from_text(text=PROMPT_4),
|
||||
types.Part.from_text(text=f"--- CONTENU DU GROUPE ---\n{group_context_text}"),
|
||||
],
|
||||
)
|
||||
]
|
||||
|
||||
config_4 = types.GenerateContentConfig(
|
||||
temperature=0.2,
|
||||
response_mime_type="application/json",
|
||||
response_json_schema=GroupRubrics.model_json_schema(),
|
||||
)
|
||||
|
||||
print(f"Generating rubric (Persp) for group: {', '.join(labels)}...")
|
||||
try:
|
||||
response_r = client.models.generate_content(
|
||||
model=MODEL_ID,
|
||||
contents=contents_4,
|
||||
config=config_4
|
||||
)
|
||||
rubrics_data = GroupRubrics.model_validate_json(response_r.text)
|
||||
rubrics_map = {r.label: r.rubric_content for r in rubrics_data.rubrics}
|
||||
except Exception as e:
|
||||
print(f"Error generating rubric for group {labels[0]}: {e}")
|
||||
rubrics_map = {}
|
||||
|
||||
# 1. Compute the common prefix for the group
|
||||
prefix = labels[0]
|
||||
for lbl in labels[1:]:
|
||||
@@ -536,12 +641,30 @@ def process_exam(folder_path: str):
|
||||
|
||||
with open(sol2_dir / f"{safe_label}.tex", "w", encoding="utf-8") as f_s2:
|
||||
f_s2.write(f"\\textbf{{{item.label}}} {item.solution_content}")
|
||||
|
||||
# --- Écriture du Barème (Persp) ---
|
||||
rubric_text = rubrics_map.get(item.label, "")
|
||||
with open(persp_dir / safe_label, "w", encoding="utf-8") as f_persp:
|
||||
f_persp.write(f"{item.label}\n{rubric_text}")
|
||||
|
||||
elif isinstance(item, ContextItem):
|
||||
raw_ctx = item.content.strip()
|
||||
tabulated_ctx = "\t" + re.sub(r'\n\s*', '\n\t', raw_ctx)
|
||||
text_content_lines.append(f"CONTEXT :")
|
||||
text_content_lines.append(tabulated_ctx)
|
||||
|
||||
# --- Save context to Text2 (Concatenating if exists) ---
|
||||
safe_first = item.target_question_label.replace("/", "_")
|
||||
safe_last = item.last_question_label.replace("/", "_")
|
||||
ctxt_filename = f"CTXT {safe_first} -> {safe_last}.tex"
|
||||
ctxt_path = text2_dir / ctxt_filename
|
||||
|
||||
# If file exists, prepend some spacing before appending
|
||||
prefix = "\n\n" if ctxt_path.exists() else ""
|
||||
|
||||
with open(ctxt_path, "a", encoding="utf-8") as f_c2:
|
||||
f_c2.write(prefix + item.content)
|
||||
|
||||
# 4. Write the grouped Text file
|
||||
with open(text_dir / safe_group_filename, "w", encoding="utf-8") as f_text:
|
||||
f_text.write("\n".join(text_content_lines))
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ unless the distinction is very important.
|
||||
|
||||
In some case, you may find that either
|
||||
- The student didn't answer the right question. Set the score to 0.
|
||||
Since it could be a labeling error, indicate is by setting `error`
|
||||
Since it could be a labeling error, indicate it by setting `error`
|
||||
to \"wrong-label\".
|
||||
- You can find an answer to another question of the exercice (taking
|
||||
more than a couple of lines). Score the question you are supposed
|
||||
|
||||
@@ -9,14 +9,15 @@ import threading
|
||||
|
||||
import annotating
|
||||
|
||||
from utils import natural_key, pdf_image_of_enonce, pdf_image_of_solution
|
||||
from utils import natural_key, pdf_image_of_enonce, pdf_image_of_solution, pdf_images_of_contexts
|
||||
from reading_annotations import detect_checks_and_notes, has_significant_notes
|
||||
|
||||
def get_extra_pdfs_as_images(root_dir, label, annotating_module):
|
||||
def get_extra_pdfs_as_images(root_dir, label, annotating_module, all_labels):
|
||||
"""Fetches Text and Sol pdfs for a given label and converts them to images."""
|
||||
extra_images = []
|
||||
a, b = pdf_image_of_enonce(root_dir, label), pdf_image_of_solution(root_dir, label)
|
||||
for c in [a, b]:
|
||||
e = pdf_images_of_contexts(root_dir, label, all_labels)
|
||||
for c in e + [a, b]:
|
||||
if c:
|
||||
img, _, _ = annotating_module.make_base_image(c)
|
||||
if img:
|
||||
@@ -239,7 +240,7 @@ def apply_actions_and_regenerate_grouped(root_dir, data, student_id,
|
||||
perfect_no_comment = False
|
||||
|
||||
if not perfect_no_comment or has_notes:
|
||||
extras = get_extra_pdfs_as_images(root_dir, label, annotating)
|
||||
extras = get_extra_pdfs_as_images(root_dir, label, annotating, all_labels)
|
||||
extras.append(final_img)
|
||||
concat_list_F.append(extras)
|
||||
|
||||
|
||||
@@ -34,6 +34,43 @@ def pdf_image_of_solution(root_dir, label):
|
||||
if os.path.exists(pdf_path):
|
||||
return pdf_path
|
||||
|
||||
def pdf_images_of_contexts(root_dir, label, all_labels):
|
||||
text2_dir = os.path.join(root_dir, "Text2")
|
||||
if not os.path.isdir(text2_dir):
|
||||
return []
|
||||
|
||||
# Map safe labels (with '/' replaced by '_') to their chronological index
|
||||
safe_to_idx = {l.replace("/", "_"): i for i, l in enumerate(all_labels)}
|
||||
|
||||
safe_target = label.replace("/", "_")
|
||||
target_idx = safe_to_idx.get(safe_target, -1)
|
||||
|
||||
if target_idx == -1:
|
||||
return []
|
||||
|
||||
pertinent_contexts = []
|
||||
|
||||
for filename in os.listdir(text2_dir):
|
||||
if filename.startswith("CTXT ") and filename.endswith(".pdf"):
|
||||
# Extract "first_label -> last_label" from "CTXT first_label -> last_label.pdf"
|
||||
core = filename[5:-4]
|
||||
parts = core.split(" -> ")
|
||||
|
||||
if len(parts) == 2:
|
||||
first_safe, last_safe = parts
|
||||
first_idx = safe_to_idx.get(first_safe, -1)
|
||||
last_idx = safe_to_idx.get(last_safe, -1)
|
||||
|
||||
# Check if the current label falls within the context's validity range
|
||||
if first_idx != -1 and last_idx != -1:
|
||||
if first_idx <= target_idx <= last_idx:
|
||||
pdf_path = os.path.join(text2_dir, filename)
|
||||
pertinent_contexts.append((first_idx, pdf_path))
|
||||
|
||||
# Sort by first_idx to ensure contexts are returned in logical reading order
|
||||
pertinent_contexts.sort(key=lambda x: x[0])
|
||||
return [path for _, path in pertinent_contexts]
|
||||
|
||||
|
||||
def get_exam_file_content(folder_path, mode, label):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user