162 lines
5.8 KiB
Python
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
|