from __future__ import annotations import html import re from .schemas import EvaluationResult, Rubric def md(value: str) -> str: """Escape model/document text; do not render injected HTML, image URLs or links.""" value = html.escape(str(value), quote=False) return re.sub(r"([\\`*_{}\[\]()#+!|])", r"\\\1", value) def render_feedback(rubric: Rubric, result: EvaluationResult, source_map: dict) -> str: concepts = {c.id: c for c in rubric.concepts} blocks = [] for importance in ("essential", "important", "minor"): blocks.append(f"## {importance.title()} omissions") reports = [c for c in result.checks if c.status in ("partial", "missing", "incorrect") and concepts[c.concept_id].importance == importance] if not reports: blocks.append("No confirmed omissions in this category were identified in this check.") for check in reports: concept = concepts[check.concept_id] label = "**Needs correction:** " if check.status == "incorrect" else "" blocks.append(f"- **{md(concept.name)}** - {label}{md(check.missing_detail)}") if check.status == "partial" and check.answer_evidence: blocks.append(f" Already covered in your answer: \"{md(check.answer_evidence)}\"") refs = [] for e in concept.evidence: c = source_map[e.source_id] refs.append(f"{md(c.filename)}, {md(c.location)}") blocks.append(" *Primary reference: " + "; ".join(dict.fromkeys(refs)) + "*") blocks.append(" **Review hint:** " + md(check.what_to_add)) if check.concept_id in result.supporting_hints: blocks.append(" **Optional supporting explanation:** " + md(result.supporting_hints[check.concept_id])) related = result.supporting_ids.get(check.concept_id, []) if related: labels = [f"{md(source_map[x].filename)}, {md(source_map[x].location)}" for x in related] blocks.append(" *Suggested supporting reading (not an extra requirement): " + "; ".join(labels) + "*") if result.warnings: blocks.append("**Check limitations:** " + " ".join(md(t) for t in result.warnings)) blocks.append("> **Preparation note:** This is an automated, deliberately thorough study aid, not an exam grade. " "Equivalent wording counts. A missing item in the model's output is never treated as an omission in your answer. " "References and feedback can still be imperfect; check the cited primary material.") return "\n\n".join(blocks) def source_list(rubric: Rubric, source_map: dict) -> str: return "\n\n".join(f"**{md(source_map[sid].filename)} - {md(source_map[sid].location)}**\n\n{md(source_map[sid].text)}" for sid in rubric.context_ids if sid in source_map)