Didactopus/src/didactopus/agentic_loop.py

162 lines
5.8 KiB
Python

from dataclasses import dataclass, field
from .concept_graph import ConceptGraph
from .evaluator_pipeline import (
LearnerAttempt,
RubricEvaluator,
CodeTestEvaluator,
SymbolicRuleEvaluator,
CritiqueEvaluator,
PortfolioEvaluator,
run_pipeline,
aggregate,
)
from .planner import PlannerWeights, rank_next_concepts
@dataclass
class ConceptEvidenceSummary:
concept_key: str
weak_dimensions: list[str] = field(default_factory=list)
mastered: bool = False
aggregated: dict = field(default_factory=dict)
evaluators: list[str] = field(default_factory=list)
@dataclass
class EvidenceState:
summary_by_concept: dict[str, ConceptEvidenceSummary] = field(default_factory=dict)
resurfaced_concepts: set[str] = field(default_factory=set)
@dataclass
class AgenticStudentState:
learner_id: str = "demo-agent"
display_name: str = "Demo Agentic Student"
mastered_concepts: set[str] = field(default_factory=set)
evidence_state: EvidenceState = field(default_factory=EvidenceState)
attempt_history: list[dict] = field(default_factory=list)
artifacts: list[dict] = field(default_factory=list)
def synthetic_attempt_for_concept(concept: str) -> LearnerAttempt:
if "descriptive-statistics" in concept:
return LearnerAttempt(
concept=concept,
artifact_type="explanation",
content="Mean and variance summarize a dataset because they describe center and spread.",
metadata={"deliverable_count": 1, "artifact_name": "descriptive_statistics_note.md"},
)
if "probability-basics" in concept:
return LearnerAttempt(
concept=concept,
artifact_type="explanation",
content="Conditional probability changes because context changes the sample space.",
metadata={"deliverable_count": 1, "artifact_name": "probability_basics_note.md"},
)
if "prior" in concept:
return LearnerAttempt(
concept=concept,
artifact_type="explanation",
content="A prior is an assumption before evidence, but one limitation is bias.",
metadata={"deliverable_count": 1, "artifact_name": "prior_reflection.md"},
)
if "posterior" in concept:
return LearnerAttempt(
concept=concept,
artifact_type="symbolic",
content="Therefore posterior = updated belief after evidence, but one assumption may be model fit.",
metadata={"deliverable_count": 1, "artifact_name": "posterior_symbolic_note.md"},
)
return LearnerAttempt(
concept=concept,
artifact_type="critique",
content="A weakness is hidden assumptions; a limitation is poor fit; uncertainty remains.",
metadata={"deliverable_count": 2, "artifact_name": "critique_report.md"},
)
def evaluator_set_for_attempt(attempt: LearnerAttempt):
evaluators = [RubricEvaluator(), CritiqueEvaluator()]
if attempt.artifact_type == "code":
evaluators.append(CodeTestEvaluator())
if attempt.artifact_type == "symbolic":
evaluators.append(SymbolicRuleEvaluator())
if attempt.artifact_type in {"project", "portfolio", "critique"}:
evaluators.append(PortfolioEvaluator())
return evaluators
def integrate_attempt(state: AgenticStudentState, attempt: LearnerAttempt) -> None:
results = run_pipeline(attempt, evaluator_set_for_attempt(attempt))
aggregated = aggregate(results)
weak = [dim for dim, score in aggregated.items() if score < 0.75]
mastered = len(aggregated) > 0 and all(score >= 0.75 for score in aggregated.values())
summary = ConceptEvidenceSummary(
concept_key=attempt.concept,
weak_dimensions=weak,
mastered=mastered,
aggregated=aggregated,
evaluators=[r.evaluator_name for r in results],
)
state.evidence_state.summary_by_concept[attempt.concept] = summary
if mastered:
state.mastered_concepts.add(attempt.concept)
state.evidence_state.resurfaced_concepts.discard(attempt.concept)
else:
if attempt.concept in state.mastered_concepts:
state.mastered_concepts.remove(attempt.concept)
state.evidence_state.resurfaced_concepts.add(attempt.concept)
state.attempt_history.append({
"concept": attempt.concept,
"artifact_type": attempt.artifact_type,
"aggregated": aggregated,
"weak_dimensions": weak,
"mastered": mastered,
"evaluators": [r.evaluator_name for r in results],
})
state.artifacts.append({
"concept": attempt.concept,
"artifact_type": attempt.artifact_type,
"artifact_name": attempt.metadata.get("artifact_name", f"{attempt.concept}.txt"),
})
def run_demo_agentic_loop(concepts: list[str]) -> AgenticStudentState:
state = AgenticStudentState()
for concept in concepts:
attempt = synthetic_attempt_for_concept(concept)
integrate_attempt(state, attempt)
return state
def run_agentic_learning_loop(
graph: ConceptGraph,
project_catalog: list[dict],
target_concepts: list[str],
weights: PlannerWeights,
max_steps: int = 4,
) -> AgenticStudentState:
state = AgenticStudentState()
for _ in range(max_steps):
ranked = rank_next_concepts(
graph=graph,
mastered=state.mastered_concepts,
targets=target_concepts,
weak_dimensions_by_concept={},
fragile_concepts=state.evidence_state.resurfaced_concepts,
project_catalog=project_catalog,
weights=weights,
)
if not ranked:
break
concept = ranked[0]["concept"]
integrate_attempt(state, synthetic_attempt_for_concept(concept))
if set(target_concepts).issubset(state.mastered_concepts):
break
return state