mirror of
https://github.com/harvard-edge/cs249r_book.git
synced 2026-07-20 04:45:11 -05:00
299 lines
8.8 KiB
Python
299 lines
8.8 KiB
Python
"""Report builders for submit-ready MLSysBook lab artifacts."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import html
|
|
from dataclasses import asdict
|
|
from typing import Any
|
|
|
|
from .schemas import ChapterRecap, LabMetadata, LabReport, to_plain
|
|
|
|
|
|
def _bullet_dict(values: dict[str, Any]) -> str:
|
|
if not values:
|
|
return "- Not recorded."
|
|
lines = []
|
|
for key, value in values.items():
|
|
lines.append(f"- {key}: {value}")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def _bullet_list(values: tuple[str, ...] | list[str] | None) -> str:
|
|
if not values:
|
|
return "- Not recorded."
|
|
return "\n".join(f"- {value}" for value in values)
|
|
|
|
|
|
def _section_value(value: Any) -> str:
|
|
if isinstance(value, dict):
|
|
return _bullet_dict(value)
|
|
if isinstance(value, (list, tuple)):
|
|
return _bullet_list(list(value))
|
|
if value:
|
|
return str(value)
|
|
return "Not recorded."
|
|
|
|
|
|
def _missing_fields(
|
|
*,
|
|
track: str,
|
|
scenario: str,
|
|
predictions: dict[str, Any],
|
|
evidence_summary: Any,
|
|
final_decision: Any,
|
|
big_takeaways: tuple[str, ...] | list[str] | None,
|
|
reflections: dict[str, Any],
|
|
residual_risk: str,
|
|
explicit: tuple[str, ...] | list[str] | None,
|
|
) -> list[str]:
|
|
missing = list(explicit or ())
|
|
if track == "not recorded":
|
|
missing.append("Track And Scenario: track")
|
|
if scenario == "not recorded":
|
|
missing.append("Track And Scenario: scenario")
|
|
if not predictions:
|
|
missing.append("Predictions")
|
|
if not evidence_summary:
|
|
missing.append("Evidence Summary")
|
|
if not final_decision:
|
|
missing.append("Final Decision")
|
|
if not big_takeaways:
|
|
missing.append("Big Takeaways")
|
|
if not reflections:
|
|
missing.append("Reflections")
|
|
if not residual_risk:
|
|
missing.append("Residual Risk")
|
|
return missing
|
|
|
|
|
|
def build_lab_report(
|
|
metadata: LabMetadata,
|
|
*,
|
|
student_id: str = "",
|
|
track: str = "not recorded",
|
|
scenario: str = "not recorded",
|
|
recap: ChapterRecap | None = None,
|
|
predictions: dict[str, Any] | None = None,
|
|
knob_settings: dict[str, Any] | None = None,
|
|
binding_constraints: dict[str, Any] | None = None,
|
|
decisions: dict[str, Any] | None = None,
|
|
reflections: dict[str, Any] | None = None,
|
|
residual_risk: str = "",
|
|
result_snapshot: Any | None = None,
|
|
learning_objectives: tuple[str, ...] | list[str] | None = None,
|
|
evidence_summary: dict[str, Any] | str | None = None,
|
|
final_decision: dict[str, Any] | str | None = None,
|
|
big_takeaways: tuple[str, ...] | list[str] | None = None,
|
|
source_trace: dict[str, Any] | str | None = None,
|
|
incomplete_fields: tuple[str, ...] | list[str] | None = None,
|
|
) -> LabReport:
|
|
"""Build a Markdown report and JSON snapshot for a lab submission."""
|
|
predictions = predictions or {}
|
|
knob_settings = knob_settings or {}
|
|
binding_constraints = binding_constraints or {}
|
|
decisions = decisions or {}
|
|
reflections = reflections or {}
|
|
snapshot = to_plain(result_snapshot or {})
|
|
evidence = evidence_summary or {
|
|
"knob_settings": knob_settings or "Not recorded.",
|
|
"binding_constraints": binding_constraints or "Not recorded.",
|
|
"result_snapshot": snapshot or "Not recorded.",
|
|
}
|
|
has_evidence = bool(evidence_summary or knob_settings or binding_constraints or snapshot)
|
|
decision = final_decision or decisions
|
|
trace = source_trace or {
|
|
"mlsysim_version": metadata.mlsysim_version,
|
|
"mlsysbook_labs_version": metadata.mlsysbook_labs_version,
|
|
"report_schema_version": metadata.report_schema_version,
|
|
"book_anchor": metadata.book_anchor,
|
|
}
|
|
|
|
recap_lines = ""
|
|
if recap is not None:
|
|
recap_lines = f"""
|
|
- Emphasis: {recap.emphasis}
|
|
- Key terms: {", ".join(recap.key_terms)}
|
|
- ML concept: {recap.ml_concept}
|
|
- Systems translation: {recap.systems_translation}
|
|
- What to watch: {recap.what_to_watch}
|
|
- Common trap: {recap.common_trap}
|
|
- Suggested reading: {recap.suggested_reading}
|
|
"""
|
|
|
|
missing = _missing_fields(
|
|
track=track,
|
|
scenario=scenario,
|
|
predictions=predictions,
|
|
evidence_summary=has_evidence,
|
|
final_decision=decision,
|
|
big_takeaways=big_takeaways,
|
|
reflections=reflections,
|
|
residual_risk=residual_risk,
|
|
explicit=incomplete_fields,
|
|
)
|
|
incomplete_section = ""
|
|
if missing:
|
|
incomplete_section = f"""
|
|
## Incomplete Fields
|
|
|
|
{_bullet_list(missing)}
|
|
"""
|
|
|
|
markdown = f"""# {metadata.title} Lab Report
|
|
|
|
## Lab
|
|
|
|
- Student: {student_id or "Not provided"}
|
|
- Lab ID: {metadata.lab_id}
|
|
- Lab title: {metadata.title}
|
|
- Lab version: {metadata.lab_version}
|
|
- Book anchor: {metadata.book_anchor}
|
|
- Updated at: {metadata.updated_at}
|
|
- MLSysIM version: {metadata.mlsysim_version}
|
|
- MLSysBook Labs version: {metadata.mlsysbook_labs_version}
|
|
- Report schema version: {metadata.report_schema_version}
|
|
|
|
## Track And Scenario
|
|
|
|
- Track: {track}
|
|
- Scenario: {scenario}
|
|
|
|
## Learning Objectives
|
|
|
|
{_bullet_list(learning_objectives)}
|
|
|
|
## Predictions
|
|
|
|
{_bullet_dict(predictions)}
|
|
|
|
## Evidence Summary
|
|
|
|
{_section_value(evidence)}
|
|
|
|
## Final Decision
|
|
|
|
{_section_value(decision)}
|
|
|
|
## Big Takeaways
|
|
|
|
{_bullet_list(big_takeaways)}
|
|
|
|
## Reflections
|
|
|
|
{_bullet_dict(reflections)}
|
|
|
|
## Residual Risk
|
|
|
|
{residual_risk or "Not recorded."}
|
|
|
|
## Source Trace
|
|
|
|
{_section_value(trace)}
|
|
|
|
{recap_lines}
|
|
|
|
### MLSysIM Result Snapshot
|
|
|
|
```json
|
|
{json.dumps(snapshot, indent=2, sort_keys=True)}
|
|
```
|
|
{incomplete_section}
|
|
"""
|
|
|
|
report_snapshot = {
|
|
"metadata": asdict(metadata),
|
|
"student_id": student_id,
|
|
"track": track,
|
|
"scenario": scenario,
|
|
"learning_objectives": to_plain(learning_objectives or ()),
|
|
"predictions": to_plain(predictions),
|
|
"knob_settings": to_plain(knob_settings),
|
|
"binding_constraints": to_plain(binding_constraints),
|
|
"evidence_summary": to_plain(evidence),
|
|
"final_decision": to_plain(decision),
|
|
"big_takeaways": to_plain(big_takeaways or ()),
|
|
"decisions": to_plain(decisions),
|
|
"reflections": to_plain(reflections),
|
|
"residual_risk": residual_risk,
|
|
"source_trace": to_plain(trace),
|
|
"result_snapshot": snapshot,
|
|
"incomplete_fields": missing,
|
|
}
|
|
return LabReport(
|
|
metadata=metadata,
|
|
student_id=student_id,
|
|
track=track,
|
|
scenario=scenario,
|
|
markdown=markdown,
|
|
snapshot=report_snapshot,
|
|
)
|
|
|
|
|
|
def report_export(report: LabReport, *, include_json: bool = True):
|
|
"""Return Marimo download controls for a lab report."""
|
|
import marimo as mo
|
|
|
|
safe_id = report.metadata.lab_id.replace("/", "_")
|
|
md_download = mo.download(
|
|
report.markdown.encode("utf-8"),
|
|
filename=f"{safe_id}_report.md",
|
|
mimetype="text/markdown",
|
|
label="Download report",
|
|
)
|
|
|
|
if not include_json:
|
|
return md_download
|
|
|
|
json_download = mo.download(
|
|
json.dumps(report.snapshot, indent=2, sort_keys=True).encode("utf-8"),
|
|
filename=f"{safe_id}_snapshot.json",
|
|
mimetype="application/json",
|
|
label="Download JSON snapshot",
|
|
)
|
|
return mo.hstack([md_download, json_download], justify="start")
|
|
|
|
|
|
def report_text_fallback(report: LabReport) -> str:
|
|
"""Return the local Markdown report text for fallback display or copying."""
|
|
return report.markdown
|
|
|
|
|
|
def report_export_panel(report: LabReport, *, include_json: bool = True, include_fallback: bool = True):
|
|
"""Return local download controls plus an optional visible Markdown fallback."""
|
|
import marimo as mo
|
|
|
|
incomplete = tuple(str(item) for item in report.snapshot.get("incomplete_fields", ()) if item)
|
|
if incomplete:
|
|
missing_items = "".join(f"<li>{html.escape(item)}</li>" for item in incomplete)
|
|
return mo.Html(
|
|
f"""
|
|
<div class="mlsysbook-panel mlsysbook-report-panel" style="border-left:4px solid #B42318;">
|
|
<h2>Report Locked</h2>
|
|
<p class="mlsysbook-action-note">
|
|
Complete the required lab work before downloading the local report.
|
|
</p>
|
|
<ul style="margin:10px 0 0 18px; color:#475467; line-height:1.6;">
|
|
{missing_items}
|
|
</ul>
|
|
</div>
|
|
"""
|
|
)
|
|
|
|
controls = report_export(report, include_json=include_json)
|
|
if not include_fallback:
|
|
return mo.vstack([controls], align="center")
|
|
|
|
fallback = mo.Html(
|
|
f"""
|
|
<div class="mlsysbook-panel mlsysbook-report-panel">
|
|
<h2>Report Text Fallback</h2>
|
|
<p class="mlsysbook-action-note">
|
|
This contains the same Markdown artifact as the download button.
|
|
</p>
|
|
<textarea readonly style="width:100%; min-height:260px; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size:0.85rem; line-height:1.45; border:1px solid #D9DEE8; border-radius:8px; padding:12px;">{html.escape(report.markdown)}</textarea>
|
|
</div>
|
|
"""
|
|
)
|
|
return mo.vstack([controls, mo.accordion({"Report text fallback": fallback}, multiple=False)], align="center")
|