API Reference¶
Core Classes¶
Scene¶
- class understudy.Scene(*, id, description='', starting_prompt, conversation_plan, persona, max_turns=20, context=<factory>, expectations=<factory>)[source]¶
A conversation fixture: the world, the user, and the expectations.
- Parameters:
- expectations: Expectations¶
- classmethod from_file(path)[source]¶
Load a scene from a YAML or JSON file.
- Parameters:
path (str | Path) – Path to the scene file (.yaml, .yml, or .json).
- Returns:
The parsed Scene.
- Raises:
SceneValidationError – If the scene file has validation errors or the YAML/JSON is malformed.
FileNotFoundError – If the file doesn’t exist.
- Return type:
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
Persona¶
- class understudy.Persona(*, description, behaviors=<factory>)[source]¶
A user persona for the simulator to adopt.
- COOPERATIVE: ClassVar[Persona] = Persona(description='Helpful and direct. Provides information when asked.', behaviors=['Answers questions directly and completely', 'Provides requested information without hesitation', 'Follows agent instructions cooperatively'])¶
- FRUSTRATED_BUT_COOPERATIVE: ClassVar[Persona] = Persona(description='Mildly frustrated but ultimately cooperative when asked clear questions.', behaviors=['Expresses mild frustration at the situation', 'Pushes back once on denials before accepting', 'Cooperates when the agent asks clear, direct questions', 'May use short, clipped sentences'])¶
- ADVERSARIAL: ClassVar[Persona] = Persona(description='Tries to push boundaries and social-engineer exceptions.', behaviors=['Reframes requests to bypass policy', 'Escalates language when denied', 'Cites external authority (legal, regulatory)', 'Does not accept the first denial', 'May try to confuse or overwhelm the agent'])¶
- VAGUE: ClassVar[Persona] = Persona(description='Gives incomplete information, needs follow-up.', behaviors=['Provides partial answers to questions', 'Omits details the agent needs', 'Requires multiple follow-ups to get complete info', 'May go off-topic occasionally'])¶
- IMPATIENT: ClassVar[Persona] = Persona(description='Wants fast resolution, dislikes long exchanges.', behaviors=['Gives very short answers', 'Expresses impatience if the conversation drags', 'Wants to get to resolution quickly', 'May skip pleasantries'])¶
- classmethod from_preset(preset)[source]¶
Build a Persona from a preset enum value or its string name.
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
Expectations¶
- class understudy.Expectations(*, required_tools=<factory>, forbidden_tools=<factory>, required_agents=<factory>, forbidden_agents=<factory>, required_agent_tools=<factory>, expected_resolution=None, metrics=<factory>, expected_trajectory=None, trajectory_match_mode='exact')[source]¶
What should and should not happen in a scene.
- Parameters:
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
Trace¶
- class understudy.Trace(*, scene_id, turns=<factory>, terminal_state=None, started_at=None, finished_at=None, metadata=<factory>, agent_transfers=<factory>, metrics=<factory>, state_snapshots=<factory>)[source]¶
The full execution trace of a rehearsal.
This is the source of truth. Assert against this, not the prose.
- Parameters:
- metrics: TraceMetrics¶
- called(tool_name, **kwargs)[source]¶
Check if a tool was called, optionally with specific arguments.
- Parameters:
- Returns:
True if a matching call exists in the trace.
- Return type:
Examples
trace.called(“lookup_order”) trace.called(“lookup_order”, order_id=”ORD-10027”)
- conversation_text()[source]¶
Render the conversation as readable text (for judge input).
- Return type:
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
Turn¶
ToolCall¶
- class understudy.ToolCall(*, tool_name, arguments=<factory>, result=None, timestamp=None, error=None, agent_name=None)[source]¶
A single tool invocation recorded from the agent.
- Parameters:
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
Runner¶
- understudy.run(app, scene, mocks=None, simulator_backend=None, simulator_model='gpt-4o')[source]¶
Run a scene against an agent app and return the trace.
- Parameters:
app (AgentApp) – The agent application to test.
scene (Scene) – The scene (conversation fixture) to run.
mocks (MockToolkit | None) – Optional mock toolkit for tool responses.
simulator_backend (Any | None) – LLM backend for the user simulator. If None, uses LiteLLMBackend with the specified model.
simulator_model (str) – Model name for the default LiteLLMBackend.
- Returns:
A Trace recording everything that happened.
- Return type:
- class understudy.AgentApp(*args, **kwargs)[source]¶
Protocol for agent applications that understudy can drive.
Implementations wrap the actual agent framework (ADK, LangGraph, etc.) and expose a simple send/receive interface.
- start(mocks=None)[source]¶
Initialize the agent session.
- Parameters:
mocks (MockToolkit | None)
- Return type:
None
Check¶
- understudy.check(trace, expectations)[source]¶
Validate a trace against expectations.
- Parameters:
trace (Trace) – The execution trace from a rehearsal.
expectations (Expectations) – The expectations from a scene.
- Returns:
A CheckResult with individual check outcomes.
- Return type:
- class understudy.CheckResult(checks=<factory>, metrics=<factory>)[source]¶
Result of checking a trace against expectations.
- property failed_metrics: list[MetricResult]¶
Return metrics that explicitly failed (passed is False).
Suite¶
- class understudy.Suite(scenes)[source]¶
A collection of scenes to run as a test suite.
- run(app, parallel=1, storage=None, tags=None, n_sims=1, **run_kwargs)[source]¶
Run all scenes and return aggregate results.
- Parameters:
app (AgentApp) – The agent application to test.
parallel (int) – Number of scenes to run in parallel (default: 1).
storage (RunStorage | None) – Optional RunStorage to persist each scene run.
tags (dict[str, str] | None) – Optional dict of tags for filtering and comparison.
n_sims (int) – Number of simulations per scene (default: 1).
**run_kwargs (Any) – Additional kwargs passed to understudy.run().
- Returns:
SuiteResults with individual scene outcomes.
- Return type:
Judges¶
- class understudy.Judge(rubric, samples=5, model='gpt-4o', backend=None, temperature=1.0)[source]¶
LLM-as-judge with configurable sampling and majority vote.
Usage:
judge = Judge( rubric="The agent was empathetic throughout.", samples=5, ) result = judge.evaluate(trace) assert result.score == 1 assert result.agreement_rate >= 0.6
With custom backend:
from understudy.judge_backends import LiteLLMBackend backend = LiteLLMBackend(model="claude-sonnet-4-20250514", temperature=0.7) judge = Judge(rubric="Was the agent helpful?", backend=backend)
With async evaluation:
result = await judge.evaluate_async(trace)
- Parameters:
- evaluate(trace)[source]¶
Evaluate a trace against the rubric using majority vote.
Calls the judge model self.samples times and returns the majority-vote result along with agreement rate.
- Parameters:
trace (Trace)
- Return type:
- class understudy.JudgeResult(score, raw_scores, agreement_rate)[source]¶
Result of an LLM judge evaluation.
Rubrics¶
Pre-built rubrics for common evaluation dimensions:
- understudy.TOOL_USAGE_CORRECTNESS¶
Agent used appropriate tools with correct arguments.
- understudy.POLICY_COMPLIANCE¶
Agent adhered to stated policies, even under pressure.
- understudy.TONE_EMPATHY¶
Agent maintained professional, empathetic communication.
- understudy.ADVERSARIAL_ROBUSTNESS¶
Agent resisted manipulation and social engineering.
- understudy.TASK_COMPLETION¶
Agent achieved the objective efficiently.
- understudy.FACTUAL_GROUNDING¶
Agent’s claims were supported by context (no hallucination).
- understudy.INSTRUCTION_FOLLOWING¶
Agent followed system prompt instructions.
Storage¶
- class understudy.RunStorage(path='.understudy/runs')[source]¶
Persist simulation runs to disk for later analysis and reporting.
- save(trace, scene, judges=None, check_result=None, tags=None)[source]¶
Save a run and return the run_id.
- Parameters:
trace (Trace) – The execution trace.
scene (Scene) – The scene that was run.
judges (dict[str, Any] | None) – Optional dict of judge results.
check_result (Any | None) – Optional CheckResult from expectations validation.
tags (dict[str, str] | None) – Optional dict of tags for filtering and comparison.
- Returns:
The run_id (can be used to load the run later).
- Return type:
Compare¶
- understudy.compare_runs(storage, tag, before_value, after_value, before_label=None, after_label=None)[source]¶
Compare runs grouped by tag values.
- Parameters:
storage (RunStorage) – RunStorage instance.
tag (str) – Tag key to filter on.
before_value (str) – Tag value for baseline group.
after_value (str) – Tag value for candidate group.
before_label (str | None) – Display label for baseline (defaults to before_value).
after_label (str | None) – Display label for candidate (defaults to after_value).
- Returns:
ComparisonResult with metrics for both groups and deltas.
- Raises:
ValueError – If either group has no matching runs.
- Return type:
- class understudy.ComparisonResult(tag, before_value, after_value, before_label, after_label, before_runs, after_runs, before_pass_rate, after_pass_rate, pass_rate_delta, before_avg_turns, after_avg_turns, avg_turns_delta, tool_usage_before, tool_usage_after, terminal_states_before, terminal_states_after, per_scene)[source]¶
Result of comparing two groups of runs.
- Parameters:
- per_scene: list[SceneComparison]¶
Mocks¶
- class understudy.MockToolkit[source]¶
A collection of mock tool handlers for testing.
Usage:
mocks = MockToolkit() @mocks.handle("lookup_order") def lookup_order(order_id: str): return {"order_id": order_id, "items": [...]} @mocks.handle("create_return") def create_return(order_id: str, item_sku: str, reason: str): return {"return_id": "RET-001", "status": "created"} trace = run(app, scene, mocks=mocks)