Skip to main content

Google Colab

colab.research.google.com
Most evals score a single turn: take one user message and one model reply, and judge whether the reply is good. That’s the right unit for a one-shot task. But a tutor - like a support agent, a coach, or any assistant you talk to over time - isn’t one turn. It’s a session: many turns that are supposed to add up to something. The properties you actually care about in a session are emergent - they exist only across the whole conversation, and you can’t see them one turn at a time:
  • Coherence - does the tutor stay consistent across turns and build on what was already said? A single turn can’t be “incoherent with turn 3” - incoherence is a relationship between turns.
  • Goal completion - did the user actually get what they came for? A goal is reached (or abandoned) over the arc of the session, often many turns after it was stated.
  • Behavior / affect - is the user engaged, or quietly getting more frustrated with every reply? Frustration builds; it’s a trend across turns, not a property of one.
The trap: every individual turn can look fine while the session as a whole fails. The tutor can give a locally-correct answer on each turn and still never converge on the student’s actual question. This cookbook traces a multi-turn AI tutor, aggregates each session into one transcript, and runs session-scoped judges over the whole conversation. This cookbook shows examples of:
  • Tracing a multi-turn tutor as sessions with a shared session_id (driven by a simulated student, so it runs end-to-end with no manual typing)
  • Aggregating a session’s spans into one clean, ordered transcript
  • Running four session-level judges - coherence, goal completion, frustration, and correctness
  • Seeing a controlled example where every turn looks fine but the session fails
  • Logging the results back to Phoenix as session annotations

Notebook Walkthrough

We will go through key code snippets on this page. To follow the full tutorial, check out the full notebook. After configuring tracing with phoenix.otel.register(...), build a tutor that teaches Socratically. To keep the notebook runnable top-to-bottom, a second LLM call plays the student: only the tutor calls are wrapped in using_attributes(session_id=...) so each session’s spans share a session.id, while the student calls run under suppress_tracing() to stay out of the project. Run a couple of sessions with different student personas, then pull the spans with px_client.spans.get_spans_dataframe(...) into primary_df. See the notebook for the full tutor loop.

Aggregate spans into a session transcript

Session-level evaluation runs on the whole conversation, so we group spans by attributes.session.id and rebuild a clean, role-tagged transcript. Each tutor turn is one LLM span carrying structured llm.input_messages / llm.output_messages - we read those (not the raw request JSON) and walk the turns in order, so the judges receive exactly the user: / assistant: format their prompts describe.

Define the four session evaluators

Each evaluator is an LLM-as-a-judge that reads the entire transcript (the messages column) and scores one session-only property. They run together in a single pass, wrapped in suppress_tracing() so the judges’ own LLM calls don’t get traced into the project.
Each prompt is written to judge only its own concern and to read the whole session. For example, the coherence judge:
See the notebook for the goal-completion, frustration, and correctness prompts - each follows the same shape.

Seeing what session-level evals catch

On a healthy session the four judges agree it’s fine. The point of session-level evaluation is the sessions where every turn looks locally correct, but the conversation fails as a whole - exactly what a turn-by-turn check waves through. Running the judges on four hand-written transcripts, each built to exhibit one dominant session-level failure: A turn-by-turn correctness check would pass every individual turn in all four transcripts - yet three of them fail at the session level. The dimensions aren’t fully independent: a blatant self-contradiction also reads as less sound and more confusing, so the incoherent case trips correctness, frustration, and goal completion too. That cascade is itself worth seeing. (The judge is probabilistic, so non-dominant labels can shift run-to-run; the bolded dimension is the one each case is built to expose.)

Log results back to Phoenix

Finally, log the four evaluations as session annotations - scores attached to the whole session, not to any single span. async_evaluate_dataframe returns one result per evaluator in the *_score columns; expand them into Phoenix’s long-form annotation schema (one row per session per evaluator), keyed by the session_id you traced under, and log with sessions.log_session_annotations_dataframe(...).
The four evaluations now appear on each session in the Sessions tab of your project.
Session-level evaluations in Phoenix

Takeaway

We evaluated whole sessions, not turns - scoring four properties that only exist across a full conversation: coherence, goal completion, frustration, and correctness. The controlled cases show why this matters: every transcript looked fine turn by turn, yet three failed at the session level, each on a different dimension. The pattern generalizes: for any session-only property you care about, aggregate the session’s spans into one transcript, write a judge that reads the whole thing, and log it back as a session annotation - right next to your turn-level and trace-level evals.