diff --git a/docs/evo-edu-evidence-trail-pilot.md b/docs/evo-edu-evidence-trail-pilot.md
new file mode 100755
index 0000000..b52f463
--- /dev/null
+++ b/docs/evo-edu-evidence-trail-pilot.md
@@ -0,0 +1,60 @@
+# evo-edu Evidence Trail Pilot
+
+This note captures the first concrete integration path between `evo-edu.org`
+and Didactopus.
+
+## Why this pack first
+
+`Evidence Trail` is the best first pilot because it already matches the
+strongest current Didactopus behaviors:
+
+- question framing
+- source-grounded study
+- concept sequencing
+- reflective revision
+- bibliography growth
+
+It is a better fit for the first learner-workbench pilot than a more
+model-heavy pack such as `Population Change`.
+
+## Current repo artifacts
+
+This pilot now has a first pack skeleton in:
+
+- `domain-packs/evidence-trail/pack.yaml`
+- `domain-packs/evidence-trail/concepts.yaml`
+- `domain-packs/evidence-trail/pack_compliance_manifest.json`
+- `domain-packs/evidence-trail/pack.frontend.json`
+
+and a learner-facing frontend payload copy in:
+
+- `webui/public/packs/evidence-trail-pack.json`
+
+## Immediate next UI step
+
+The current `webui/` is still review-workbench-first. The next useful step is
+to add a learner-workbench path that can load `evidence-trail-pack.json` and
+show:
+
+- onboarding headline/body/checklist
+- current concept
+- prerequisite path
+- one grounded prompt
+- learner response
+- evaluator feedback
+- next study action
+
+## Integration back to evo-edu.org
+
+Once a learner-workbench route exists, `evo-edu.org` should link to it from:
+
+- `/evo/packs/evidence-trail/`
+- `/evo/packs/evidence-trail/study-log.html`
+- `/evo/pathways/self-learners/`
+
+The framing should remain:
+
+- guided-study workbench
+- source-grounded reflection companion
+
+not generic AI tutor.
diff --git a/domain-packs/evidence-trail/concepts.yaml b/domain-packs/evidence-trail/concepts.yaml
new file mode 100755
index 0000000..833970a
--- /dev/null
+++ b/domain-packs/evidence-trail/concepts.yaml
@@ -0,0 +1,59 @@
+concepts:
+ - id: question-framing
+ title: Question Framing
+ description: >-
+ Start from a question narrow enough to investigate and revise. A good
+ study question gives the learner a way to compare evidence rather than
+ only gather disconnected facts.
+ prerequisites: []
+ mastery_signals:
+ - State a focused question that can be investigated with tools and sources.
+ - Distinguish the main question from broader background curiosity.
+ mastery_profile: {}
+ - id: observation-vs-interpretation
+ title: Observation Versus Interpretation
+ description: >-
+ Separate what the model, species account, or source actually shows from
+ the explanation you currently prefer. This distinction is necessary for
+ trustworthy revision.
+ prerequisites:
+ - question-framing
+ mastery_signals:
+ - Describe a model or source observation before drawing a conclusion.
+ - Mark which parts of an explanation are inference rather than direct observation.
+ mastery_profile: {}
+ - id: source-comparison
+ title: Source Comparison
+ description: >-
+ Compare sources by relevance, grounding, and limits rather than treating
+ all sources as equally strong. The learner should note where a source
+ supports, complicates, or fails to resolve a claim.
+ prerequisites:
+ - observation-vs-interpretation
+ mastery_signals:
+ - Explain why one source is stronger or weaker for the current question.
+ - Identify at least one unresolved conflict or limitation across sources.
+ mastery_profile: {}
+ - id: bibliography-growth
+ title: Bibliography Growth
+ description: >-
+ Extend a question through linked citations, related species records, and
+ literature trails. Bibliography growth should deepen the inquiry rather
+ than inflate the reading list without purpose.
+ prerequisites:
+ - source-comparison
+ mastery_signals:
+ - Add a useful new source that sharpens or tests the current explanation.
+ - Explain how a new citation changes the study path.
+ mastery_profile: {}
+ - id: revision-under-uncertainty
+ title: Revision Under Uncertainty
+ description: >-
+ Treat revision as a normal part of inquiry. The learner should be able to
+ state what changed, why it changed, and what uncertainty still remains.
+ prerequisites:
+ - bibliography-growth
+ mastery_signals:
+ - Write a revised explanation after comparing evidence.
+ - State one uncertainty or next evidence move instead of pretending closure.
+ mastery_profile: {}
diff --git a/domain-packs/evidence-trail/pack.frontend.json b/domain-packs/evidence-trail/pack.frontend.json
new file mode 100755
index 0000000..ae02418
--- /dev/null
+++ b/domain-packs/evidence-trail/pack.frontend.json
@@ -0,0 +1,70 @@
+{
+ "id": "evidence-trail",
+ "title": "Evidence Trail",
+ "subtitle": "Draft evo-edu-aligned pack for guided independent study through question framing, source comparison, bibliography growth, and reflective revision.",
+ "level": "self-learner",
+ "concepts": [
+ {
+ "id": "question-framing",
+ "title": "Question Framing",
+ "prerequisites": [],
+ "masteryDimension": "mastery",
+ "exerciseReward": "Question Framing progress recorded"
+ },
+ {
+ "id": "observation-vs-interpretation",
+ "title": "Observation Versus Interpretation",
+ "prerequisites": [
+ "question-framing"
+ ],
+ "masteryDimension": "mastery",
+ "exerciseReward": "Observation Versus Interpretation progress recorded"
+ },
+ {
+ "id": "source-comparison",
+ "title": "Source Comparison",
+ "prerequisites": [
+ "observation-vs-interpretation"
+ ],
+ "masteryDimension": "mastery",
+ "exerciseReward": "Source Comparison progress recorded"
+ },
+ {
+ "id": "bibliography-growth",
+ "title": "Bibliography Growth",
+ "prerequisites": [
+ "source-comparison"
+ ],
+ "masteryDimension": "mastery",
+ "exerciseReward": "Bibliography Growth progress recorded"
+ },
+ {
+ "id": "revision-under-uncertainty",
+ "title": "Revision Under Uncertainty",
+ "prerequisites": [
+ "bibliography-growth"
+ ],
+ "masteryDimension": "mastery",
+ "exerciseReward": "Revision Under Uncertainty progress recorded"
+ }
+ ],
+ "onboarding": {
+ "headline": "Start with one question, one source trail, and one explicit revision.",
+ "body": "Begin with a focused question, compare model output and source material carefully, then record what changed in your understanding and why.",
+ "checklist": [
+ "State the question you are trying to answer.",
+ "Compare one model observation with one cited source or species account.",
+ "Write one interpretation and one uncertainty.",
+ "Record what evidence would make you revise your current view."
+ ]
+ },
+ "compliance": {
+ "sources": 6,
+ "attributionRequired": true,
+ "shareAlikeRequired": true,
+ "noncommercialOnly": true,
+ "flags": [
+ "derived-from-evo-edu-materials"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/domain-packs/evidence-trail/pack.yaml b/domain-packs/evidence-trail/pack.yaml
new file mode 100755
index 0000000..95e52af
--- /dev/null
+++ b/domain-packs/evidence-trail/pack.yaml
@@ -0,0 +1,23 @@
+name: evidence-trail
+display_name: Evidence Trail
+version: 0.1.0-draft
+schema_version: '1'
+didactopus_min_version: 0.1.0
+didactopus_max_version: 0.9.99
+description: Draft evo-edu-aligned pack for guided independent study through question framing, source comparison, bibliography growth, and reflective revision.
+author: Wesley R. Elsberry
+license: CC BY-NC-SA 4.0
+audience_level: self-learner
+dependencies: []
+overrides: []
+profile_templates: {}
+cross_pack_links: []
+supporting_artifacts:
+ - pack_compliance_manifest.json
+first_session_headline: Start with one question, one source trail, and one explicit revision.
+first_session_body: Begin with a focused question, compare model output and source material carefully, then record what changed in your understanding and why.
+first_session_checklist:
+ - State the question you are trying to answer.
+ - Compare one model observation with one cited source or species account.
+ - Write one interpretation and one uncertainty.
+ - Record what evidence would make you revise your current view.
diff --git a/domain-packs/evidence-trail/pack_compliance_manifest.json b/domain-packs/evidence-trail/pack_compliance_manifest.json
new file mode 100755
index 0000000..beb4942
--- /dev/null
+++ b/domain-packs/evidence-trail/pack_compliance_manifest.json
@@ -0,0 +1,42 @@
+{
+ "pack_id": "evidence-trail",
+ "display_name": "Evidence Trail",
+ "attribution_required": true,
+ "share_alike_required": true,
+ "noncommercial_only": true,
+ "restrictive_flags": [
+ "derived-from-evo-edu-materials"
+ ],
+ "derived_from_sources": [
+ {
+ "title": "evo-edu.org Evidence Trail pack",
+ "kind": "site_pack",
+ "locator": "https://evo-edu.org/evo/packs/evidence-trail/"
+ },
+ {
+ "title": "evo-edu.org Evidence Trail study log",
+ "kind": "site_support_material",
+ "locator": "https://evo-edu.org/evo/packs/evidence-trail/study-log.html"
+ },
+ {
+ "title": "evo-edu.org Scientific Virtues",
+ "kind": "site_background",
+ "locator": "https://evo-edu.org/evo/scientific-virtues/"
+ },
+ {
+ "title": "evo-edu.org Notebook",
+ "kind": "site_background",
+ "locator": "https://evo-edu.org/evo/notebook/"
+ },
+ {
+ "title": "EcoSpecies",
+ "kind": "supporting_resource",
+ "locator": "https://evo-edu.org/apps/ecospecies/"
+ },
+ {
+ "title": "Literature Explorer",
+ "kind": "supporting_resource",
+ "locator": "https://evo-edu.org/apps/literature-explorer/"
+ }
+ ]
+}
diff --git a/src/didactopus/api.py b/src/didactopus/api.py
index 1798efa..7ae2860 100644
--- a/src/didactopus/api.py
+++ b/src/didactopus/api.py
@@ -6,7 +6,7 @@ from .db import Base, engine
from .models import (
LoginRequest, TokenPair, KnowledgeCandidateCreate, KnowledgeCandidateUpdate,
ReviewCreate, PromoteRequest, SynthesisRunRequest, SynthesisPromoteRequest,
- CreateLearnerRequest
+ CreateLearnerRequest, LearnerWorkbenchSessionRequest
)
from .repository import (
authenticate_user, get_user_by_id, create_learner, learner_owned_by_user,
@@ -16,6 +16,7 @@ from .repository import (
)
from .auth import issue_access_token, issue_refresh_token, decode_token, new_token_id
from .synthesis import generate_synthesis_candidates
+from .learner_workbench import build_pack_workbench_session
Base.metadata.create_all(bind=engine)
@@ -58,6 +59,25 @@ def api_create_learner(payload: CreateLearnerRequest, user = Depends(current_use
create_learner(user.id, payload.learner_id, payload.display_name)
return {"ok": True, "learner_id": payload.learner_id}
+
+@app.post("/api/learner-workbench/session")
+def api_learner_workbench_session(payload: LearnerWorkbenchSessionRequest):
+ try:
+ return build_pack_workbench_session(
+ pack_id=payload.pack_id,
+ concept_id=payload.concept_id,
+ learner_goal=payload.learner_goal,
+ question=payload.question,
+ observation=payload.observation,
+ interpretation=payload.interpretation,
+ uncertainty=payload.uncertainty,
+ revision_trigger=payload.revision_trigger,
+ )
+ except FileNotFoundError as exc:
+ raise HTTPException(status_code=404, detail=str(exc))
+ except KeyError as exc:
+ raise HTTPException(status_code=400, detail=str(exc))
+
@app.post("/api/knowledge-candidates")
def api_create_candidate(payload: KnowledgeCandidateCreate, reviewer = Depends(require_reviewer)):
candidate_id = create_candidate(payload)
diff --git a/src/didactopus/learner_session.py b/src/didactopus/learner_session.py
index af0a7ac..090f8e0 100644
--- a/src/didactopus/learner_session.py
+++ b/src/didactopus/learner_session.py
@@ -27,6 +27,16 @@ def _grounding_block(step: dict) -> str:
return "\n".join(lines)
+def _scientific_virtues_block() -> str:
+ return (
+ "Scientific virtues operating guidance:\n"
+ "- Distinguish observation from interpretation.\n"
+ "- Preserve uncertainty where the evidence does not settle the issue.\n"
+ "- Treat revision as progress when better evidence changes the conclusion.\n"
+ "- Prefer source-grounded comparison over unsupported confidence."
+ )
+
+
def _generate_role_text(
provider: ModelProvider,
*,
@@ -71,9 +81,10 @@ def build_graph_grounded_session(
mentor_prompt = (
f"{_grounding_block(primary)}\n\n"
f"{_grounding_block(secondary)}\n\n"
+ f"{_scientific_virtues_block()}\n\n"
f"Learner goal: {learner_goal}\n"
"Respond as Didactopus mentor. Give a short grounded orientation, explain why these concepts come first, "
- "and ask one focused question that keeps the learner doing the reasoning."
+ "ask one focused question that keeps the learner doing the reasoning, and explicitly separate what should be observed from what should be interpreted."
)
mentor_text = _generate_role_text(
provider,
@@ -87,8 +98,10 @@ def build_graph_grounded_session(
practice_prompt = (
f"{_grounding_block(primary)}\n\n"
+ f"{_scientific_virtues_block()}\n\n"
f"Learner goal: {learner_goal}\n"
- "Create one reasoning-heavy practice task for the learner. Keep it grounded in the supporting lessons and do not provide the full solution."
+ "Create one reasoning-heavy practice task for the learner. Keep it grounded in the supporting lessons, do not provide the full solution, "
+ "and require the learner to identify observation, interpretation, and one condition that would justify revision."
)
practice_text = _generate_role_text(
provider,
@@ -103,10 +116,12 @@ def build_graph_grounded_session(
evaluation = evaluate_submission_with_skill(context, primary["concept_key"].split("::", 1)[-1], learner_submission)
evaluator_prompt = (
f"{_grounding_block(primary)}\n\n"
+ f"{_scientific_virtues_block()}\n\n"
f"Practice task: {practice_text}\n"
f"Learner submission: {learner_submission}\n"
f"Deterministic evaluator result: verdict={evaluation['verdict']}, aggregated={evaluation['aggregated']}\n"
- "Respond as Didactopus evaluator. Summarize strengths, real gaps, and one next revision target without pretending supported caveats are missing."
+ "Respond as Didactopus evaluator. Summarize strengths, real gaps, and one next revision target without pretending supported caveats are missing. "
+ "Reward honest uncertainty, careful source-grounded distinction between observation and interpretation, and justified revision."
)
evaluator_text = _generate_role_text(
provider,
@@ -121,8 +136,10 @@ def build_graph_grounded_session(
next_step_prompt = (
f"{_grounding_block(primary)}\n\n"
f"{_grounding_block(secondary)}\n\n"
+ f"{_scientific_virtues_block()}\n\n"
f"Evaluator feedback: {evaluator_text}\n"
- "Respond as Didactopus mentor. Give the next study action and explain why it follows from the grounded concept path."
+ "Respond as Didactopus mentor. Give the next study action, explain why it follows from the grounded concept path, "
+ "and state what new evidence or comparison would most help the learner revise or strengthen the current view."
)
next_step_text = _generate_role_text(
provider,
diff --git a/src/didactopus/learner_workbench.py b/src/didactopus/learner_workbench.py
new file mode 100755
index 0000000..5bc1a32
--- /dev/null
+++ b/src/didactopus/learner_workbench.py
@@ -0,0 +1,196 @@
+from __future__ import annotations
+
+from pathlib import Path
+
+import yaml
+
+from .config import AppConfig
+from .model_provider import ModelProvider
+from .role_prompts import system_prompt_for_role
+
+
+ROOT = Path(__file__).resolve().parents[2]
+DOMAIN_PACKS = ROOT / "domain-packs"
+
+
+def _load_yaml(path: Path) -> dict:
+ return yaml.safe_load(path.read_text(encoding="utf-8")) or {}
+
+
+def load_pack_context(pack_id: str) -> dict:
+ pack_dir = DOMAIN_PACKS / pack_id
+ if not pack_dir.exists():
+ raise FileNotFoundError(f"Unknown pack: {pack_id}")
+ pack = _load_yaml(pack_dir / "pack.yaml")
+ concepts = (_load_yaml(pack_dir / "concepts.yaml")).get("concepts", []) or []
+ by_id = {concept.get("id"): concept for concept in concepts}
+ return {
+ "pack_dir": pack_dir,
+ "pack": pack,
+ "concepts": concepts,
+ "by_id": by_id,
+ }
+
+
+def _scientific_virtues_block() -> str:
+ return (
+ "Scientific virtues operating guidance:\n"
+ "- Distinguish observation from interpretation.\n"
+ "- Preserve uncertainty where the evidence does not settle the issue.\n"
+ "- Treat revision as progress when better evidence changes the conclusion.\n"
+ "- Prefer source-grounded comparison over unsupported confidence."
+ )
+
+
+def _concept_block(concept: dict, by_id: dict[str, dict]) -> str:
+ prerequisite_titles = [by_id.get(pid, {}).get("title", pid) for pid in concept.get("prerequisites", []) or []]
+ mastery_signals = concept.get("mastery_signals", []) or []
+ lines = [
+ f"Concept: {concept.get('title', '')}",
+ f"Prerequisites: {', '.join(prerequisite_titles or ['none explicit'])}",
+ f"Description: {concept.get('description', '')}",
+ ]
+ if mastery_signals:
+ lines.append("Mastery signals:")
+ lines.extend(f"- {item}" for item in mastery_signals)
+ return "\n".join(lines)
+
+
+def _generate(provider: ModelProvider, *, role: str, prompt: str, temperature: float, max_tokens: int) -> dict:
+ response = provider.generate(
+ prompt,
+ role=role,
+ system_prompt=system_prompt_for_role(role),
+ temperature=temperature,
+ max_tokens=max_tokens,
+ )
+ return {
+ "text": response.text.strip(),
+ "provider": response.provider,
+ "model_name": response.model_name,
+ }
+
+
+def _feedback(question: str, observation: str, interpretation: str, uncertainty: str, revision_trigger: str) -> dict:
+ strengths: list[str] = []
+ gaps: list[str] = []
+ if question.strip():
+ strengths.append("You stated a concrete study question.")
+ else:
+ gaps.append("State a clear question before treating the inquiry as settled.")
+ if observation.strip():
+ strengths.append("You recorded an explicit observation.")
+ else:
+ gaps.append("Record what was observed before moving to interpretation.")
+ if interpretation.strip():
+ strengths.append("You proposed an interpretation.")
+ else:
+ gaps.append("State the interpretation you currently think fits best.")
+ if uncertainty.strip():
+ strengths.append("You preserved uncertainty instead of smoothing it away.")
+ else:
+ gaps.append("Name one uncertainty or limitation in the current explanation.")
+ if revision_trigger.strip():
+ strengths.append("You identified what evidence could justify revision.")
+ else:
+ gaps.append("Say what result or source would make you revise your view.")
+ return {
+ "strengths": strengths,
+ "gaps": gaps,
+ "next_revision_target": gaps[0] if gaps else "Compare one more source or example before treating the interpretation as stable.",
+ }
+
+
+def build_pack_workbench_session(
+ *,
+ pack_id: str,
+ concept_id: str,
+ learner_goal: str,
+ question: str,
+ observation: str,
+ interpretation: str,
+ uncertainty: str,
+ revision_trigger: str,
+) -> dict:
+ context = load_pack_context(pack_id)
+ pack = context["pack"]
+ concepts = context["concepts"]
+ by_id = context["by_id"]
+ concept = by_id.get(concept_id) or (concepts[0] if concepts else None)
+ if concept is None:
+ raise KeyError(f"No concepts found for pack: {pack_id}")
+ index = concepts.index(concept)
+ next_concept = concepts[index + 1] if index + 1 < len(concepts) else concept
+
+ provider = ModelProvider(AppConfig().model_provider)
+ pack_block = (
+ f"Pack: {pack.get('display_name', pack_id)}\n"
+ f"Description: {pack.get('description', '')}\n"
+ f"Audience level: {pack.get('audience_level', 'novice-friendly')}"
+ )
+ learner_state_block = (
+ f"Learner goal: {learner_goal}\n"
+ f"Question: {question}\n"
+ f"Observation: {observation}\n"
+ f"Interpretation: {interpretation}\n"
+ f"Uncertainty: {uncertainty}\n"
+ f"Revision trigger: {revision_trigger}"
+ )
+
+ mentor = _generate(
+ provider,
+ role="mentor",
+ prompt=(
+ f"{pack_block}\n\n{_concept_block(concept, by_id)}\n\n{_concept_block(next_concept, by_id)}\n\n"
+ f"{_scientific_virtues_block()}\n\n{learner_state_block}\n"
+ "Respond as Didactopus mentor. Give a short grounded orientation, explain why this concept matters now, "
+ "and ask one focused question that helps the learner distinguish observation from interpretation."
+ ),
+ temperature=0.2,
+ max_tokens=260,
+ )
+ practice = _generate(
+ provider,
+ role="practice",
+ prompt=(
+ f"{pack_block}\n\n{_concept_block(concept, by_id)}\n\n{_scientific_virtues_block()}\n\n{learner_state_block}\n"
+ "Respond as Didactopus practice designer. Create one compact task that asks for evidence comparison, honest uncertainty, and a revision condition."
+ ),
+ temperature=0.25,
+ max_tokens=220,
+ )
+ evaluator = _generate(
+ provider,
+ role="evaluator",
+ prompt=(
+ f"{pack_block}\n\n{_concept_block(concept, by_id)}\n\n{_scientific_virtues_block()}\n\n{learner_state_block}\n"
+ "Respond as Didactopus evaluator. Name strengths first, then real gaps, and give one revision target without pretending stated caveats are absent."
+ ),
+ temperature=0.2,
+ max_tokens=240,
+ )
+ next_step = _generate(
+ provider,
+ role="mentor",
+ prompt=(
+ f"{pack_block}\n\n{_concept_block(concept, by_id)}\n\n{_concept_block(next_concept, by_id)}\n\n"
+ f"{_scientific_virtues_block()}\n\nEvaluator feedback: {evaluator['text']}\n"
+ "Respond as Didactopus mentor. Give the next study action and say what new evidence or comparison would most help the learner revise or strengthen the current view."
+ ),
+ temperature=0.2,
+ max_tokens=220,
+ )
+
+ feedback = _feedback(question, observation, interpretation, uncertainty, revision_trigger)
+ return {
+ "pack_id": pack_id,
+ "concept_id": concept.get("id"),
+ "concept_title": concept.get("title"),
+ "next_concept_id": next_concept.get("id"),
+ "next_concept_title": next_concept.get("title"),
+ "mentor": mentor,
+ "practice": practice,
+ "evaluator": evaluator,
+ "next_step": next_step,
+ "feedback": feedback,
+ }
diff --git a/src/didactopus/models.py b/src/didactopus/models.py
index 16f12be..a92e24c 100644
--- a/src/didactopus/models.py
+++ b/src/didactopus/models.py
@@ -65,3 +65,14 @@ class MasteryRecord(BaseModel):
class LearnerState(BaseModel):
learner_id: str
records: list[MasteryRecord] = Field(default_factory=list)
+
+
+class LearnerWorkbenchSessionRequest(BaseModel):
+ pack_id: str
+ concept_id: str
+ learner_goal: str = ""
+ question: str = ""
+ observation: str = ""
+ interpretation: str = ""
+ uncertainty: str = ""
+ revision_trigger: str = ""
diff --git a/src/didactopus/role_prompts.py b/src/didactopus/role_prompts.py
index 775780b..a0d04bc 100644
--- a/src/didactopus/role_prompts.py
+++ b/src/didactopus/role_prompts.py
@@ -18,10 +18,17 @@ def _variant_suffix(role: str, variant: str) -> str:
"project_advisor": " Emphasize realistic next steps and avoid grandiose scope.",
"evaluator": " Preserve learner trust by naming strengths first, avoiding invented omissions, and framing revisions as specific improvements rather than blanket criticism.",
},
+ "scientific_virtues": {
+ "mentor": " Reinforce curiosity, careful observation, and honest separation of observation from interpretation. Preserve uncertainty where the evidence is incomplete and treat revision as progress.",
+ "practice": " Design tasks that ask the learner to compare evidence, separate observation from interpretation, and state what result would change their current view.",
+ "learner": " Keep an earnest learner voice that distinguishes observation from inference, notes uncertainty honestly, and stays willing to revise.",
+ "project_advisor": " Encourage projects that foreground source quality, evidence comparison, and explicit revision rather than polished unsupported synthesis.",
+ "evaluator": " Evaluate scientific habits as well as correctness: distinguish observation from interpretation, reward justified revision, preserve caveats, and challenge weakly supported claims without overstating certainty.",
+ },
"concise": {
"mentor": " Keep the response compact: no more than four short paragraphs or bullets worth of content.",
"practice": " Keep the task compact and direct.",
- "learner": " Keep the reflection short and direct.",
+ "learner": " Keep reflections concise and concrete.",
"project_advisor": " Keep the advice short and concrete.",
"evaluator": " Keep the evaluation compact and specific.",
},
@@ -37,14 +44,16 @@ def mentor_system_prompt() -> str:
"You are Didactopus in mentor mode. Help the learner think through the topic without doing the work for them. "
"Prefer Socratic questions, prerequisite reminders, and hints over finished solutions. "
"When responding to a learner attempt or evaluator note, acknowledge what the learner already did correctly before naming gaps. "
- "Do not claim a caveat, limitation, or nuance is missing if the learner already stated one; instead say how to sharpen or extend it."
+ "Do not claim a caveat, limitation, or nuance is missing if the learner already stated one; instead say how to sharpen or extend it. "
+ "Separate observation from interpretation, name uncertainty when the evidence is incomplete, and frame revision as a normal part of inquiry."
)
def practice_system_prompt() -> str:
return (
"You are Didactopus in practice-design mode. Generate short, reasoning-heavy tasks that force the learner "
- "to explain, compare, or derive ideas rather than copy answers."
+ "to explain, compare, or derive ideas rather than copy answers. Prefer tasks that ask what was observed, what was inferred, "
+ "what evidence supports the claim, and what result would justify revision."
)
@@ -68,7 +77,8 @@ def evaluator_system_prompt() -> str:
"Point out weak assumptions and missing justification instead of giving the polished final answer. "
"Before saying something is missing, first verify whether the learner already included it. "
"If the learner stated a caveat, limitation, or nuance, quote or paraphrase that part and evaluate its quality rather than pretending it is absent. "
- "Do not invent omissions that are contradicted by the learner's actual text."
+ "Do not invent omissions that are contradicted by the learner's actual text. "
+ "Treat honest revision, explicit uncertainty, and careful separation of observation from interpretation as strengths in scientific reasoning."
)
diff --git a/tests/test_api_scaffold.py b/tests/test_api_scaffold.py
index d4c4848..3bcfed3 100644
--- a/tests/test_api_scaffold.py
+++ b/tests/test_api_scaffold.py
@@ -1,6 +1,11 @@
from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
def test_api_files_exist():
- assert Path("src/didactopus/api.py").exists()
- assert Path("src/didactopus/storage.py").exists()
- assert Path("data/packs/bayes-pack.json").exists()
+ assert (ROOT / "src/didactopus/api.py").exists()
+ assert (ROOT / "src/didactopus/storage.py").exists()
+ assert (ROOT / "src/didactopus/learner_workbench.py").exists()
+ assert (ROOT / "data/packs/bayes-pack.json").exists()
diff --git a/tests/test_frontend_files.py b/tests/test_frontend_files.py
index 9048037..6ae372e 100644
--- a/tests/test_frontend_files.py
+++ b/tests/test_frontend_files.py
@@ -1,5 +1,9 @@
from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
def test_frontend_scaffold_exists():
- assert Path("webui/src/App.jsx").exists()
- assert Path("webui/src/api.js").exists()
+ assert (ROOT / "webui/src/App.jsx").exists()
+ assert (ROOT / "webui/src/api.js").exists()
diff --git a/tests/test_learner_workbench.py b/tests/test_learner_workbench.py
new file mode 100755
index 0000000..59070f0
--- /dev/null
+++ b/tests/test_learner_workbench.py
@@ -0,0 +1,47 @@
+from didactopus import learner_workbench as learner_workbench_module
+
+
+class _FakeProvider:
+ def __init__(self, _config) -> None:
+ pass
+
+ def generate(
+ self,
+ prompt: str,
+ *,
+ role: str,
+ system_prompt: str | None = None,
+ temperature: float | None = None,
+ max_tokens: int | None = None,
+ ):
+ return type(
+ "Response",
+ (),
+ {
+ "text": f"{role}: {prompt.splitlines()[0]}",
+ "provider": "fake",
+ "model_name": "fake-model",
+ },
+ )()
+
+
+def test_build_pack_workbench_session_uses_evidence_trail_pack(monkeypatch) -> None:
+ monkeypatch.setattr(learner_workbench_module, "ModelProvider", _FakeProvider)
+
+ payload = learner_workbench_module.build_pack_workbench_session(
+ pack_id="evidence-trail",
+ concept_id="question-framing",
+ learner_goal="Understand how to compare evidence.",
+ question="Which question should guide the study step?",
+ observation="The pack starts with question framing.",
+ interpretation="Question framing organizes the next evidence move.",
+ uncertainty="I do not yet know which source will best test the interpretation.",
+ revision_trigger="A stronger source trail could change the current framing.",
+ )
+
+ assert payload["pack_id"] == "evidence-trail"
+ assert payload["concept_id"] == "question-framing"
+ assert payload["next_concept_id"] == "observation-vs-interpretation"
+ assert payload["mentor"]["provider"] == "fake"
+ assert payload["feedback"]["strengths"]
+ assert payload["feedback"]["gaps"] == []
diff --git a/tests/test_ui_files.py b/tests/test_ui_files.py
index 7a06f82..69280c5 100644
--- a/tests/test_ui_files.py
+++ b/tests/test_ui_files.py
@@ -1,6 +1,11 @@
from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
def test_ui_files_exist():
- assert Path("webui/src/App.jsx").exists()
- assert Path("webui/src/storage.js").exists()
- assert Path("webui/public/packs/bayes-pack.json").exists()
+ assert (ROOT / "webui/src/App.jsx").exists()
+ assert (ROOT / "webui/src/storage.js").exists()
+ assert (ROOT / "webui/public/packs/bayes-pack.json").exists()
+ assert (ROOT / "webui/public/packs/evidence-trail-pack.json").exists()
diff --git a/webui/public/packs/evidence-trail-pack.json b/webui/public/packs/evidence-trail-pack.json
new file mode 100755
index 0000000..ae02418
--- /dev/null
+++ b/webui/public/packs/evidence-trail-pack.json
@@ -0,0 +1,70 @@
+{
+ "id": "evidence-trail",
+ "title": "Evidence Trail",
+ "subtitle": "Draft evo-edu-aligned pack for guided independent study through question framing, source comparison, bibliography growth, and reflective revision.",
+ "level": "self-learner",
+ "concepts": [
+ {
+ "id": "question-framing",
+ "title": "Question Framing",
+ "prerequisites": [],
+ "masteryDimension": "mastery",
+ "exerciseReward": "Question Framing progress recorded"
+ },
+ {
+ "id": "observation-vs-interpretation",
+ "title": "Observation Versus Interpretation",
+ "prerequisites": [
+ "question-framing"
+ ],
+ "masteryDimension": "mastery",
+ "exerciseReward": "Observation Versus Interpretation progress recorded"
+ },
+ {
+ "id": "source-comparison",
+ "title": "Source Comparison",
+ "prerequisites": [
+ "observation-vs-interpretation"
+ ],
+ "masteryDimension": "mastery",
+ "exerciseReward": "Source Comparison progress recorded"
+ },
+ {
+ "id": "bibliography-growth",
+ "title": "Bibliography Growth",
+ "prerequisites": [
+ "source-comparison"
+ ],
+ "masteryDimension": "mastery",
+ "exerciseReward": "Bibliography Growth progress recorded"
+ },
+ {
+ "id": "revision-under-uncertainty",
+ "title": "Revision Under Uncertainty",
+ "prerequisites": [
+ "bibliography-growth"
+ ],
+ "masteryDimension": "mastery",
+ "exerciseReward": "Revision Under Uncertainty progress recorded"
+ }
+ ],
+ "onboarding": {
+ "headline": "Start with one question, one source trail, and one explicit revision.",
+ "body": "Begin with a focused question, compare model output and source material carefully, then record what changed in your understanding and why.",
+ "checklist": [
+ "State the question you are trying to answer.",
+ "Compare one model observation with one cited source or species account.",
+ "Write one interpretation and one uncertainty.",
+ "Record what evidence would make you revise your current view."
+ ]
+ },
+ "compliance": {
+ "sources": 6,
+ "attributionRequired": true,
+ "shareAlikeRequired": true,
+ "noncommercialOnly": true,
+ "flags": [
+ "derived-from-evo-edu-materials"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/webui/src/App.jsx b/webui/src/App.jsx
index 2ac4da7..53f18ab 100644
--- a/webui/src/App.jsx
+++ b/webui/src/App.jsx
@@ -1,10 +1,78 @@
-import React, { useEffect, useState } from "react";
-import { login, listCandidates, createCandidate, createReview, promoteCandidate, runSynthesis, listSynthesisCandidates, promoteSynthesis } from "./api";
+import React, { useEffect, useMemo, useState } from "react";
+import {
+ login,
+ listCandidates,
+ createCandidate,
+ createReview,
+ promoteCandidate,
+ runSynthesis,
+ listSynthesisCandidates,
+ promoteSynthesis,
+ createLearnerWorkbenchSession,
+} from "./api";
+import {
+ applyEvidence,
+ buildMasteryMap,
+ progressPercent,
+ recommendNext,
+ milestoneMessages,
+ claimReadiness,
+} from "./engine";
-function LoginView({ onAuth }) {
+function LauncherView({ onSelect }) {
+ return (
+
+
+
+
Didactopus
+
Choose a work mode.
+
+ The current prototype now has two distinct entry points: the existing review workbench
+ and a learner-workbench pilot using the evo-edu `Evidence Trail` pack.
+
+
+
+
+
+
+
+
+
+
+
Learner workbench pilot
+
+ Use a guided-study surface built around question framing, source comparison, and
+ revision under uncertainty.
+
+
+
Loads the `Evidence Trail` pack.
+
Shows concept path and onboarding.
+
Separates observation from interpretation.
+
Treats revision as normal progress.
+
+
+
+
Review workbench
+
+ Keep using the existing knowledge-candidate and synthesis workflow for pack-improvement
+ and review operations.
+
+ This pilot uses scientific virtues as operating rules: separate observation from interpretation,
+ preserve uncertainty, and treat revision as progress.
+
No immediate gap detected; extend with another source comparison.
}
+
+
+
+ Next revision target: {feedback.nextRevision}
+
+ >
+ ) : (
+
Submit a study record to get evaluator-style feedback grounded in the current concept and scientific-virtues framing.
+ )}
+
+
+
+
Backend Session Output
+
Grounded mentor/practice/evaluator loop
+ {sessionOutput ? (
+
+
+ Mentor
+
{sessionOutput.mentor?.text}
+
+
+ Practice
+
{sessionOutput.practice?.text}
+
+
+ Evaluator
+
{sessionOutput.evaluator?.text}
+
+
+ Next step
+
{sessionOutput.next_step?.text}
+
+
+ ) : (
+
Run an evaluation to request grounded mentor, practice, evaluator, and next-step text from the backend session endpoint.
+ )}
+
+
+
+
Next Study Action
+
{nextConcept ? nextConcept.title : "Complete the current concept carefully"}
+
+ {nextConcept
+ ? `Move forward when you can justify your interpretation and name what evidence would change it. The next concept extends this work into ${nextConcept.title.toLowerCase()}.`
+ : "You are at the end of the current concept path. Review your uncertainty and revision trigger before treating the topic as settled."}
+