Skip to content

Track and Evaluate Multi-Turn Conversations with TruLens

A response can look good by itself while the conversation around it fails. TruLens 2.12 adds first-class conversation tracking and evaluation so you can measure context retention, consistency, and progress across turns without losing per-turn metrics and traces.


A Conversation Is More Than a Collection of Good Turns

Answer relevance asks whether one response addresses one prompt. That remains useful, but averaging relevance across a thread cannot tell you whether the assistant:

  • Contradicted an earlier answer
  • Remembered a constraint from several turns ago
  • Repeated itself without moving the task forward
  • Recovered after a correction
  • Kept a coherent plan across tool calls and follow-up questions

Those are properties of the ordered interaction. Evaluating them requires the complete transcript, not a set of independent rows.

TruLens 2.12 supports both scopes:

Scope Evaluator input Result
Turn level One record's input and output One score per turn
Conversation level All ordered {"input", "output"} records One score for the conversation

Group Turns with conversation_id

Your application owns the conversation identifier. Pass it into the TruLens recording context around the turns that belong together:

conversation_id = "conv-climate-001"

with tru_chatbot(conversation_id=conversation_id) as recording:
    for user_input in turns:
        chatbot.invoke(
            {"input": user_input},
            config={"configurable": {"session_id": conversation_id}},
        )

TruLens propagates the value through OpenTelemetry Baggage. Every child span recorded inside the context inherits ai.observability.conversation_id, including spans from instrumented third-party libraries.

The application framework's session identifier and the TruLens conversation identifier have different jobs. In the example above, LangChain uses session_id to select message history. TruLens uses conversation_id to group records for evaluation, retrieval, and display. Reusing the same value keeps application memory and observability aligned.

You can also reuse an ID across recording contexts when each turn is recorded separately:

conversation_id = "support-case-42"

with tru_app(conversation_id=conversation_id):
    app.respond("I cannot sign in.")

with tru_app(conversation_id=conversation_id):
    app.respond("I already reset my password.")

Run Turn and Conversation Metrics Together

Define turn-level metrics with the existing record selectors:

from trulens.core import Metric
from trulens.core import Selector
from trulens.providers.openai import OpenAI

provider = OpenAI(model_engine="gpt-4o-mini")

answer_relevance = Metric(
    implementation=provider.relevance_with_cot_reasons,
    name="Answer Relevance",
    selectors={
        "prompt": Selector.select_record_input(),
        "response": Selector.select_record_output(),
    },
)

Use .on_conversation() when the evaluator needs the complete ordered transcript:

conversation_coherence = Metric(
    implementation=provider.coherence_across_turns,
    name="Coherence Across Turns",
).on_conversation()

Attach both metrics to the same app:

from trulens.apps.langchain import TruChain

tru_chatbot = TruChain(
    chatbot,
    app_name="Support Assistant",
    app_version="v1",
    feedbacks=[answer_relevance, conversation_coherence],
)

When the recording context exits successfully, turn-level metrics are queued once per record. Conversation-level metrics are queued once over the exact ordered records created in that context.

Use the returned recording when your next step needs to wait for the results:

feedback_results = recording.retrieve_feedback_results()

For a custom evaluator, TruLens also exposes selectors for the full transcript or only one side of it:

Selector.select_conversation()
Selector.select_conversation_input()
Selector.select_conversation_output()

Conversation selectors cannot be mixed with record or span selectors in one Metric. Keep metrics at one scope and attach separate metrics when you need both views.

Retrieve Conversations as First-Class Objects

TruLens reconstructs conversations from their ordered records. You can list conversations for an app or retrieve the records from one thread:

conversations = session.get_conversations(app_id=tru_chatbot.app_id)

records = session.get_records_by_conversation(
    conversation_id="conv-climate-001",
    app_id=tru_chatbot.app_id,
)

Conversation-level scores belong to the latest record in the evaluated batch. Earlier rows intentionally remain empty for that metric. Do not average a conversation metric; it is already one evaluation over the ordered transcript. You can still group and average turn-level metrics when that is useful for reporting.

Inspect Threads Without Losing Turn-Level Detail

The Records page now groups records that share a conversation_id into one thread. The conversation view shows:

  • The first and last turns, with long middle sections collapsed
  • One result for each conversation-level metric
  • Separate per-turn metric results
  • Total latency, tokens, and cost across the recorded turns
  • A link from every turn to its ordinary record trace

Standalone records still use the existing record detail view. Adding conversation tracking does not remove the per-record traces you use to investigate retrievals, generations, tool calls, or latency.

Try the Complete Workflow

The framework-neutral quickstart records a 12-turn conversation and a single-turn conversation, waits for their evaluations, and opens the thread-aware dashboard: