Google Colab
colab.research.google.com
- 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.
- 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 withphoenix.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 byattributes.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 (themessages 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.
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:| case | coherence | goal completion | frustration | correctness |
|---|---|---|---|---|
| clean | coherent | completed | not_frustrated | correct |
| incoherent (tutor contradicts an earlier turn) | incoherent | not_completed | frustrated | incorrect |
| goal not met (drifts onto tangents, never answers) | coherent | not_completed | not_frustrated | correct |
| frustrated (correct answers, impatient student) | coherent | completed | frustrated | correct |
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(...).


