Skip to main content
Everything the pipecat eval CLI does is available as a library under pipecat.evals. Use it to run evals from your own test runner (pytest, a CI script, a custom dashboard), to build scenarios in code instead of YAML, or to customize pieces like the judge LLM or the persona LLM. A session is one conversation with a bot, driven to a result. EvalSession.from_scenario() builds the session for a loaded scenario of either kind: an EvalScriptSession for a scripted scenario, which returns an EvalScriptResult, or an EvalSimulationSession for a simulation, which returns an EvalSimulationResult. EvalSession is the base class of both, and EvalSessionParams is how a run behaves, whichever kind it is.
pipecat.evals itself exports nothing. Import each name from the submodule that defines it, as the examples below do.

Running a scripted scenario

EvalScriptScenario.load() parses a scenario file, and EvalSession.from_scenario() builds a ready-to-run session, constructing the judge, user speech, and transcriber the scenario calls for:
The agent must already be running with its eval transport (python bot.py -t eval), just as with pipecat eval run. from_scenario() is typed by the scenario it is given: a type checker sees a session built from an EvalScriptScenario as an EvalScriptSession, with no narrowing needed. EvalScriptSession.from_scenario() from pipecat.evals.script_session takes the same arguments.

The result

run() returns an EvalScriptResult: Each EvalScriptTurnResult carries its turn_index, a status of passed, failed, or not_run, the failures it produced, and its duration_ms. EvalScriptResult.failures is these turns’ failures flattened, plus any that belong to no turn, such as a failed connect. not_run is deliberately distinct from a pass, so a turn the scenario never reached doesn’t inflate a rate:
failures carry a kind alongside the reason (timeout, judge_no, text_mismatch, missing_function_call, and so on), which is the stable key to group by when scoring a sweep rather than reading one run. This maps cleanly onto a pytest test:

Running a simulation

A simulation loads and runs the same way. For a simulation, EvalSession.from_scenario() builds an EvalSimulationSession, constructing the persona LLM from the file’s simulator: block and the judge from its judge: block, plus the user TTS and the transcriber in audio mode:
A session runs the simulation once. The runs: field is honored by the suite, which spawns a fresh agent for each run. EvalSimulationSession.from_scenario() from pipecat.evals.simulation_session is the same call on the kind’s own class.

The result

run() returns an EvalSimulationResult: Each EvalSimulationMetricScore carries the metric’s name, its score (the share of turns the judge said yes to for a judged metric, 1.0 or 0.0 for a measured one, None when there was nothing to judge or measure), whether it passed, its reason, its min_score, its measured value (the seconds, words, turns, or number of calls), and a failure_kind (judge_no, judge_no_verdict, out_of_range, or function_calls; None when it passed). A judged metric also carries one EvalSimulationTurnVerdict per agent turn with the 1-based turn, whether it passed, the judge’s verdict (yes, no, or none when the judge gave none, which counts as a no), and its reason:

Loading either kind

load_scenario_file() reads a file as whichever kind it is, and EvalSession.from_scenario() builds the matching session, so a list mixing both kinds runs through one call each:
A file with both turns: and persona:, or neither, raises a ValueError naming the problem. EvalKind, from pipecat.evals.scenario, names the two kinds, script and simulation, as a suite run or a results record reports them.

Run parameters

How a run behaves, whichever kind it is, is one EvalSessionParams from pipecat.evals.session, passed as params= to from_scenario() or to a session’s constructor. It is plain configuration, so one instance serves many runs:
Passing these as individual keyword arguments to from_scenario() (connect_timeout_s=, stop_bot=, and so on) is deprecated since 1.9.0 and will be removed in 2.0.0. They still work, override the params field of the same name, and emit a DeprecationWarning.

Building scenarios in code

Scenarios are plain dataclasses, so you can construct them programmatically, generating turns from a dataset, parameterizing a template, or skipping YAML entirely:
The modality-agnostic response event is resolved while parsing YAML. When constructing scenarios in code, use llm_response for text mode directly (or response only when you also configure audio judging).
A simulation is built the same way. Each metric is an EvalSimulationMetric with either a criterion or a measure, and simulator= takes a plain mapping with the same shape as the YAML block when the persona shouldn’t run on the default local model:

Customizing the judge and the persona

from_scenario() builds the judge from the scenario’s judge: block, but you can inject your own. EvalJudge works with any Pipecat LLM service backed by an OpenAI-compatible API, and judge= applies to either kind:
For a simulation, persona_llm= is the LLM that plays the caller. It runs inside the harness’s own pipeline, so it can be any Pipecat LLMService that supports function calling, which the persona needs to hang up with its end_call tool. Passing it for a scripted scenario raises a ValueError:
Passing judge=None explicitly to the EvalSimulationSession constructor runs the conversation without a judge: measured metrics are still computed, and the run reports no verdict on the goal.

Custom audio services

from_scenario() also takes user_tts= for the user’s synthesized voice and bot_stt= for transcribing the agent’s spoken audio, for either kind. bot_stt is any Pipecat STTService. user_tts is a CachingTTSService, which wraps a TTSService and caches its audio on disk so repeated turns don’t re-synthesize; give it a cache_key that identifies the voice configuration:
The wrapped services can be local models or HTTP-based; WebSocket-streaming services are rejected, since they need a running pipeline to manage their connection lifecycle. For the YAML-only route, the factory: escape hatch in the Scenario Configuration page reaches the same services without code.

Observing progress

Every session emits an on_progress event as the conversation advances. A scripted session reports an EvalScriptTurnProgress as each turn and expectation resolves; a simulation session reports an EvalSimulationProgress for each line as it is spoken, with a status of bot or user, the text, and the persona’s turn count so far, then one with a status of ended whose text says how the conversation ended:
For a scripted session the record has turn_index, status, event_name, and detail:
The on_progress callback parameter is deprecated since 1.9.0 and will be removed in 2.0.0. Use the on_progress event handler instead. It only ever applied to scripted scenarios, so passing it for a simulation raises a ValueError.

Orchestrating suites

EvalManifest and EvalSuite are the library behind pipecat eval suite: the suite spawns each agent with its eval transport on its own port, runs its scenarios of either kind, each in its own process, and executes several runs concurrently:
Each run is mutated in place as it executes (status, result, error, duration_ms), so a live display can render directly from suite.runs. A run’s kind is script or simulation, and its result is the matching result type. A simulation appears once per attempt: attempts is the manifest’s repeat, or the simulation’s own runs, and sweep says which. A sweep is a measurement, where a failure is data; a simulation’s own runs are a requirement, where every attempt must pass.
The on_update callback parameter to suite.run() is deprecated since 1.9.0 and will be removed in 2.0.0. Use the on_update event handler instead. So are its use_cache and default_timeout_ms keyword arguments: pass params=EvalSessionParams(...).
EvalManifest.load() accepts keyword overrides for every manifest value (concurrency, base_port, spawn, scenarios_dir, repeat, and so on), mirroring the CLI flags.

Migrating from 1.8

Simulations arrived in 1.9.0 with a rename of the scripted API, so the two kinds sit side by side. EvalSession stays. It is now the base class of both kinds and the home of the from_scenario() that builds either, and it moved to pipecat.evals.session. Constructing EvalSession(...) directly is no longer supported; use the kind’s own class. The old names keep working until 2.0.0 and emit a DeprecationWarning: EvalSpeech and EvalTranscriber are gone: pass user_tts= and bot_stt= as shown above. SEND_CHUNK_MS was removed with no replacement, since the harness now paces the user’s audio through its own output transport.