diff --git a/README.md b/README.md
index 6e78d8d..c77054d 100644
--- a/README.md
+++ b/README.md
@@ -30,6 +30,8 @@ Then open:
- `examples/ocw-information-entropy-run/learner_progress.html`
- `examples/ocw-information-entropy-session.json`
+- `examples/ocw-information-entropy-session.html`
+- `examples/ocw-information-entropy-session.txt`
- `examples/ocw-information-entropy-skill-demo/skill_demo.md`
- `examples/ocw-information-entropy-rolemesh-transcript/rolemesh_transcript.md`
- `skills/ocw-information-entropy-agent/`
@@ -38,6 +40,7 @@ That gives you:
- a generated topic pack
- a graph-grounded mentor/practice/evaluator learner session
+- accessible HTML and text-first learner-session outputs
- a visible learning path
- progress artifacts
- a reusable skill grounded in the exported knowledge
@@ -182,6 +185,11 @@ That demo builds a graph-grounded session from the MIT OCW skill bundle and emit
The point of this module is architectural as much as demonstrational: it is the session core that future accessibility, model-benchmark, and voice-interaction work should build on.
+The learner-session demo also writes accessible companion outputs:
+
+- `examples/ocw-information-entropy-session.html`
+- `examples/ocw-information-entropy-session.txt`
+
The first benchmark harness for that session core is now:
```bash
@@ -441,6 +449,7 @@ What remains heuristic or lightweight:
## Recommended Reading
- [docs/roadmap.md](docs/roadmap.md)
+- [docs/learner-accessibility.md](docs/learner-accessibility.md)
- [docs/local-model-benchmark.md](docs/local-model-benchmark.md)
- [docs/course-to-pack.md](docs/course-to-pack.md)
- [docs/learning-graph.md](docs/learning-graph.md)
diff --git a/docs/learner-accessibility.md b/docs/learner-accessibility.md
new file mode 100644
index 0000000..f9ea883
--- /dev/null
+++ b/docs/learner-accessibility.md
@@ -0,0 +1,57 @@
+# Learner Accessibility
+
+Didactopus should make the learner loop usable without assuming visual graph navigation or silent waiting on slow local models.
+
+The current accessibility baseline is built on the graph-grounded learner session backend.
+
+## Current Outputs
+
+Running:
+
+```bash
+python -m didactopus.learner_session_demo
+```
+
+now writes:
+
+- `examples/ocw-information-entropy-session.json`
+- `examples/ocw-information-entropy-session.html`
+- `examples/ocw-information-entropy-session.txt`
+
+## What The Accessible Outputs Do
+
+The HTML output is meant to be screen-reader-friendly and keyboard-friendly:
+
+- skip link to the main content
+- semantic headings
+- reading-order sections for study plan, conversation, and evaluation
+- grounded source fragments rendered as ordinary text instead of only visual diagrams
+
+The plain-text output is a linearized learner-session transcript that is suitable for:
+
+- terminal reading
+- screen-reader reading
+- low-bandwidth sharing
+- future text-to-speech pipelines
+
+## Why This Matters
+
+Didactopus should help learners work with structure, not just with pictures and dashboards.
+
+This is especially important for:
+
+- blind learners
+- screen-reader users
+- learners on low-power hardware
+- situations where audio or text needs to be generated locally
+
+## Relationship To The Roadmap
+
+This is the accessibility baseline, not the endpoint.
+
+Likely next steps:
+
+- local text-to-speech for mentor, practice, and evaluator turns
+- speech-to-text for learner answers
+- explicit spoken structural cues
+- text-first alternatives for more generated visualizations
diff --git a/examples/ocw-information-entropy-session.html b/examples/ocw-information-entropy-session.html
new file mode 100644
index 0000000..7e32f1f
--- /dev/null
+++ b/examples/ocw-information-entropy-session.html
@@ -0,0 +1,116 @@
+
+
+
+
+
+Didactopus Learner Session
+This page is structured for keyboard and screen-reader use. It presents the learner goal, study plan, grounded source fragments, and conversation turns in reading order.
+Learner goal: Help me understand how Shannon entropy leads into channel capacity and thermodynamic entropy.
+
+
+Study Plan
+
+-
+
Independent Reasoning and Careful Comparison
+Status: mastered
+Prerequisites: Course Notes and Reference Texts
+Supporting lessons: Independent Reasoning and Careful Comparison
+Grounding fragments:
+
+Independent Reasoning and Careful Comparison (lesson_body)
- Objective: Explain why the course requires precise comparison of related but non-identical concepts.
+- Exercise: Write a short note distinguishing Shannon entropy, channel capacity, and thermodynamic entropy.
+The syllabus framing implies a style of work where analogy is useful but dangerous when used loosely. Learners must compare models carefully, state assumptions, and notice where similar mathematics does not imply identical interpretation.
+Independent Reasoning and Careful Comparison (objective)
Explain why the course requires precise comparison of related but non-identical concepts.
+
+
+-
+
Thermodynamics and Entropy
+Status: mastered
+Prerequisites: Cryptography and Information Hiding
+Supporting lessons: Thermodynamics and Entropy
+Grounding fragments:
+
+Thermodynamics and Entropy (lesson_body)
- Objective: Explain how thermodynamic entropy relates to, and differs from, Shannon entropy.
+- Exercise: Compare the two entropy notions and identify what is preserved across the analogy.
+The course uses entropy as a bridge concept between communication theory and physics while insisting on careful interpretation.
+Thermodynamics and Entropy (objective)
Explain how thermodynamic entropy relates to, and differs from, Shannon entropy.
+
+
+-
+
Shannon Entropy
+Status: mastered
+Prerequisites: Counting and Probability
+Supporting lessons: Shannon Entropy
+Grounding fragments:
+
+Shannon Entropy (lesson_body)
- Objective: Explain Shannon entropy as a measure of uncertainty and compare high-entropy and low-entropy sources.
+- Exercise: Compute the entropy of a Bernoulli source and interpret the result.
+The course then introduces entropy as a quantitative measure of uncertainty for a source model and uses it to reason about representation cost and surprise.
+Shannon Entropy (objective)
Explain Shannon entropy as a measure of uncertainty and compare high-entropy and low-entropy sources.
+
+
+
+
+
+Conversation
+
+Learner Goal
+Role: user
+Help me understand how Shannon entropy leads into channel capacity and thermodynamic entropy.
+
+
+Didactopus Mentor
+Role: assistant
+[stubbed-response] [mentor] Concept: Independent Reasoning and Careful Comparison Prerequisites: Course Notes and Reference Texts Supporting lessons
+
+
+Didactopus Practice Designer
+Role: assistant
+[stubbed-response] [practice] Concept: Independent Reasoning and Careful Comparison Prerequisites: Course Notes and Reference Texts Supporting lessons
+
+
+Learner Submission
+Role: user
+Entropy measures uncertainty because more possible outcomes require more information to describe, but one limitation is that thermodynamic entropy is not identical to Shannon entropy.
+
+
+Didactopus Evaluator
+Role: assistant
+[stubbed-response] [evaluator] Concept: Independent Reasoning and Careful Comparison Prerequisites: Course Notes and Reference Texts Supporting lessons
+
+
+Didactopus Mentor
+Role: assistant
+[stubbed-response] [mentor] Concept: Independent Reasoning and Careful Comparison Prerequisites: Course Notes and Reference Texts Supporting lessons
+
+
+
+Evaluation Summary
+Verdict: needs_revision
+Aggregated dimensions: {"correctness": 0.6000000000000001, "critique": 0.6499999999999999, "explanation": 0.85}
+Follow-up: Rework the answer so it states the equality/relationship explicitly and explains why it matters.
+
+
+
+
\ No newline at end of file
diff --git a/examples/ocw-information-entropy-session.txt b/examples/ocw-information-entropy-session.txt
new file mode 100644
index 0000000..f16cfc9
--- /dev/null
+++ b/examples/ocw-information-entropy-session.txt
@@ -0,0 +1,53 @@
+Didactopus Learner Session
+
+Learner goal: Help me understand how Shannon entropy leads into channel capacity and thermodynamic entropy.
+
+Study plan:
+1. Independent Reasoning and Careful Comparison
+ Status: mastered
+ Prerequisites: Course Notes and Reference Texts
+ Supporting lessons: Independent Reasoning and Careful Comparison
+ Source fragment (lesson_body): - Objective: Explain why the course requires precise comparison of related but non-identical concepts.
+- Exercise: Write a short note distinguishing Shannon entropy, channel capacity, and thermodynamic entropy.
+The syllabus framing implies a style of work where analogy is useful but dangerous when used loosely. Learners must compare models carefully, state assumptions, and notice where similar mathematics does not imply identical interpretation.
+ Source fragment (objective): Explain why the course requires precise comparison of related but non-identical concepts.
+2. Thermodynamics and Entropy
+ Status: mastered
+ Prerequisites: Cryptography and Information Hiding
+ Supporting lessons: Thermodynamics and Entropy
+ Source fragment (lesson_body): - Objective: Explain how thermodynamic entropy relates to, and differs from, Shannon entropy.
+- Exercise: Compare the two entropy notions and identify what is preserved across the analogy.
+The course uses entropy as a bridge concept between communication theory and physics while insisting on careful interpretation.
+ Source fragment (objective): Explain how thermodynamic entropy relates to, and differs from, Shannon entropy.
+3. Shannon Entropy
+ Status: mastered
+ Prerequisites: Counting and Probability
+ Supporting lessons: Shannon Entropy
+ Source fragment (lesson_body): - Objective: Explain Shannon entropy as a measure of uncertainty and compare high-entropy and low-entropy sources.
+- Exercise: Compute the entropy of a Bernoulli source and interpret the result.
+The course then introduces entropy as a quantitative measure of uncertainty for a source model and uses it to reason about representation cost and surprise.
+ Source fragment (objective): Explain Shannon entropy as a measure of uncertainty and compare high-entropy and low-entropy sources.
+
+Conversation:
+Learner Goal:
+Help me understand how Shannon entropy leads into channel capacity and thermodynamic entropy.
+
+Didactopus Mentor:
+[stubbed-response] [mentor] Concept: Independent Reasoning and Careful Comparison Prerequisites: Course Notes and Reference Texts Supporting lessons
+
+Didactopus Practice Designer:
+[stubbed-response] [practice] Concept: Independent Reasoning and Careful Comparison Prerequisites: Course Notes and Reference Texts Supporting lessons
+
+Learner Submission:
+Entropy measures uncertainty because more possible outcomes require more information to describe, but one limitation is that thermodynamic entropy is not identical to Shannon entropy.
+
+Didactopus Evaluator:
+[stubbed-response] [evaluator] Concept: Independent Reasoning and Careful Comparison Prerequisites: Course Notes and Reference Texts Supporting lessons
+
+Didactopus Mentor:
+[stubbed-response] [mentor] Concept: Independent Reasoning and Careful Comparison Prerequisites: Course Notes and Reference Texts Supporting lessons
+
+Evaluation summary:
+Verdict: needs_revision
+Aggregated dimensions: {"correctness": 0.6000000000000001, "critique": 0.6499999999999999, "explanation": 0.85}
+Follow-up: Rework the answer so it states the equality/relationship explicitly and explains why it matters.
diff --git a/src/didactopus/learner_accessibility.py b/src/didactopus/learner_accessibility.py
new file mode 100644
index 0000000..4bdce13
--- /dev/null
+++ b/src/didactopus/learner_accessibility.py
@@ -0,0 +1,158 @@
+from __future__ import annotations
+
+import html
+import json
+from pathlib import Path
+
+
+def _escape(value: object) -> str:
+ return html.escape(str(value))
+
+
+def build_accessible_session_text(session: dict) -> str:
+ lines = [
+ "Didactopus Learner Session",
+ "",
+ f"Learner goal: {session.get('goal', '')}",
+ "",
+ "Study plan:",
+ ]
+ for index, step in enumerate(session.get("study_plan", {}).get("steps", []), start=1):
+ lines.extend(
+ [
+ f"{index}. {step.get('title', '')}",
+ f" Status: {step.get('status', '')}",
+ f" Prerequisites: {', '.join(step.get('prerequisite_titles', []) or ['none explicit'])}",
+ f" Supporting lessons: {', '.join(step.get('supporting_lessons', []) or ['none listed'])}",
+ ]
+ )
+ for fragment in step.get("source_fragments", [])[:2]:
+ lines.append(f" Source fragment ({fragment.get('kind', 'fragment')}): {fragment.get('text', '')}")
+ lines.extend(
+ [
+ "",
+ "Conversation:",
+ ]
+ )
+ for turn in session.get("turns", []):
+ lines.extend(
+ [
+ f"{turn.get('label', turn.get('role', 'Turn'))}:",
+ str(turn.get("content", "")),
+ "",
+ ]
+ )
+ evaluation = session.get("evaluation", {})
+ lines.extend(
+ [
+ "Evaluation summary:",
+ f"Verdict: {evaluation.get('verdict', '')}",
+ f"Aggregated dimensions: {json.dumps(evaluation.get('aggregated', {}), sort_keys=True)}",
+ f"Follow-up: {evaluation.get('follow_up', '')}",
+ ]
+ )
+ return "\n".join(lines).strip() + "\n"
+
+
+def build_accessible_session_html(session: dict) -> str:
+ steps = session.get("study_plan", {}).get("steps", [])
+ turns = session.get("turns", [])
+ evaluation = session.get("evaluation", {})
+ body = [
+ "",
+ '',
+ "",
+ '',
+ '',
+ 'Didactopus Learner Session
',
+ 'This page is structured for keyboard and screen-reader use. It presents the learner goal, study plan, grounded source fragments, and conversation turns in reading order.
',
+ f"Learner goal: {_escape(session.get('goal', ''))}
",
+ "",
+ '',
+ 'Study Plan
',
+ '',
+ ]
+ for step in steps:
+ body.append("- ")
+ body.append(f"
{_escape(step.get('title', ''))}
")
+ body.append(f"Status: {_escape(step.get('status', ''))}
")
+ body.append(
+ f"Prerequisites: {_escape(', '.join(step.get('prerequisite_titles', []) or ['none explicit']))}
"
+ )
+ body.append(
+ f"Supporting lessons: {_escape(', '.join(step.get('supporting_lessons', []) or ['none listed']))}
"
+ )
+ fragments = step.get("source_fragments", [])[:2]
+ if fragments:
+ body.append("Grounding fragments:
")
+ body.append("")
+ for fragment in fragments:
+ body.append(
+ f'{_escape(fragment.get("lesson_title", ""))} '
+ f'({_escape(fragment.get("kind", "fragment"))})
{_escape(fragment.get("text", ""))}
'
+ )
+ body.append("
")
+ body.append(" ")
+ body.extend(
+ [
+ "
",
+ "",
+ '',
+ 'Conversation
',
+ ]
+ )
+ for turn in turns:
+ body.append('')
+ body.append(f"{_escape(turn.get('label', turn.get('role', 'Turn')))}
")
+ body.append(f"Role: {_escape(turn.get('role', ''))}
")
+ body.append(f"{_escape(turn.get('content', ''))}
")
+ body.append("")
+ body.extend(
+ [
+ "",
+ '',
+ 'Evaluation Summary
',
+ f"Verdict: {_escape(evaluation.get('verdict', ''))}
",
+ f"Aggregated dimensions: {_escape(json.dumps(evaluation.get('aggregated', {}), sort_keys=True))}
",
+ f"Follow-up: {_escape(evaluation.get('follow_up', ''))}
",
+ "",
+ "",
+ "",
+ "",
+ ]
+ )
+ return "\n".join(body)
+
+
+def render_accessible_session_outputs(
+ session: dict,
+ *,
+ out_html: str | Path,
+ out_text: str | Path,
+) -> dict[str, str]:
+ out_html = Path(out_html)
+ out_text = Path(out_text)
+ out_html.write_text(build_accessible_session_html(session), encoding="utf-8")
+ out_text.write_text(build_accessible_session_text(session), encoding="utf-8")
+ return {"html": str(out_html), "text": str(out_text)}
diff --git a/src/didactopus/learner_session_demo.py b/src/didactopus/learner_session_demo.py
index df44d14..832bef1 100644
--- a/src/didactopus/learner_session_demo.py
+++ b/src/didactopus/learner_session_demo.py
@@ -4,6 +4,7 @@ import json
from pathlib import Path
from .config import load_config
+from .learner_accessibility import render_accessible_session_outputs
from .learner_session import build_graph_grounded_session
from .model_provider import ModelProvider
from .ocw_skill_agent_demo import load_ocw_skill_context
@@ -13,6 +14,8 @@ def run_learner_session_demo(
config_path: str | Path,
skill_dir: str | Path,
out_path: str | Path | None = None,
+ accessible_html_path: str | Path | None = None,
+ accessible_text_path: str | Path | None = None,
) -> dict:
config = load_config(config_path)
provider = ModelProvider(config.model_provider)
@@ -24,7 +27,11 @@ def run_learner_session_demo(
learner_submission="Entropy measures uncertainty because more possible outcomes require more information to describe, but one limitation is that thermodynamic entropy is not identical to Shannon entropy.",
)
if out_path is not None:
- Path(out_path).write_text(json.dumps(payload, indent=2), encoding="utf-8")
+ out_path = Path(out_path)
+ out_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
+ html_path = Path(accessible_html_path) if accessible_html_path is not None else out_path.with_suffix(".html")
+ text_path = Path(accessible_text_path) if accessible_text_path is not None else out_path.with_suffix(".txt")
+ render_accessible_session_outputs(payload, out_html=html_path, out_text=text_path)
return payload
@@ -36,8 +43,16 @@ def main() -> None:
parser.add_argument("--config", default=str(root / "configs" / "config.example.yaml"))
parser.add_argument("--skill-dir", default=str(root / "skills" / "ocw-information-entropy-agent"))
parser.add_argument("--out", default=str(root / "examples" / "ocw-information-entropy-session.json"))
+ parser.add_argument("--accessible-html", default=None)
+ parser.add_argument("--accessible-text", default=None)
args = parser.parse_args()
- payload = run_learner_session_demo(args.config, args.skill_dir, args.out)
+ payload = run_learner_session_demo(
+ args.config,
+ args.skill_dir,
+ args.out,
+ args.accessible_html,
+ args.accessible_text,
+ )
print(json.dumps(payload, indent=2))
diff --git a/tests/test_learner_accessibility.py b/tests/test_learner_accessibility.py
new file mode 100644
index 0000000..7259343
--- /dev/null
+++ b/tests/test_learner_accessibility.py
@@ -0,0 +1,43 @@
+from pathlib import Path
+
+from didactopus.learner_accessibility import (
+ build_accessible_session_html,
+ build_accessible_session_text,
+ render_accessible_session_outputs,
+)
+from didactopus.learner_session_demo import run_learner_session_demo
+
+
+def _session_payload() -> dict:
+ root = Path(__file__).resolve().parents[1]
+ return run_learner_session_demo(
+ root / "configs" / "config.example.yaml",
+ root / "skills" / "ocw-information-entropy-agent",
+ )
+
+
+def test_accessible_session_html_has_landmarks() -> None:
+ html = build_accessible_session_html(_session_payload())
+ assert 'href="#session-main"' in html
+ assert 'aria-label="Didactopus learner session"' in html
+ assert "Study Plan" in html
+ assert "Conversation" in html
+ assert "Evaluation Summary" in html
+
+
+def test_accessible_session_text_is_linearized() -> None:
+ text = build_accessible_session_text(_session_payload())
+ assert "Learner goal:" in text
+ assert "Study plan:" in text
+ assert "Conversation:" in text
+ assert "Evaluation summary:" in text
+
+
+def test_render_accessible_session_outputs_writes_files(tmp_path: Path) -> None:
+ outputs = render_accessible_session_outputs(
+ _session_payload(),
+ out_html=tmp_path / "session.html",
+ out_text=tmp_path / "session.txt",
+ )
+ assert Path(outputs["html"]).exists()
+ assert Path(outputs["text"]).exists()
diff --git a/tests/test_learner_session.py b/tests/test_learner_session.py
index 6b853b7..49c4ffc 100644
--- a/tests/test_learner_session.py
+++ b/tests/test_learner_session.py
@@ -35,5 +35,7 @@ def test_run_learner_session_demo_writes_output(tmp_path: Path) -> None:
)
assert (tmp_path / "session.json").exists()
+ assert (tmp_path / "session.html").exists()
+ assert (tmp_path / "session.txt").exists()
assert payload["practice_task"]
assert payload["evaluation"]["aggregated"]