Multi-Turn Conversation Metrics
UAEF ships a Multi-Turn metric dimension for evaluating multi-turn agent
conversations. The dimension contains 15 metrics — 4 originals plus 11
ported from Amazon's AMACE Conversation Evaluator — and is integrated into
the standard evaluation flow described in
Multi-Turn Evaluation Workflow.
This document is the catalog: what each metric measures, how it scores, whether it needs ground truth, and where it fits in the per-turn vs. full-trace dispatch.
Where these metrics run
The evaluator routes metrics in two phases (see
src/uaef/evaluation/base_evaluator.py:_run_multi_turn):
- Per-turn phase — judges each (user, assistant) exchange in isolation.
The loop averages the per-turn scores and preserves each turn's reasoning
in
metric_score.metadata["per_turn"]. - Full-trace phase — judges once over the full conversation transcript.
A metric is per-turn iff its get_name() is not in the
FULL_TRACE_ONLY set in base_evaluator.py. Today every Multi-Turn metric
is full-trace. For per-turn relevance scoring across a multi-turn dialog,
include answer_relevance (Response Quality dimension) in your metric list
— the per-turn dispatch loop runs it on each turn and averages.
The metrics
Originals
| Metric | Phase | Description | Score | Ground truth | LLM judge |
|---|---|---|---|---|---|
context_retention |
full-trace | Whether the agent maintains and uses information from earlier turns. Returns null when the conversation has no shared context to retain. |
0–1 | no | yes (Bedrock) |
coherence |
full-trace | Logical flow, relevance, consistency, and topic-tracking across the conversation. | 0–1 | no | yes (Bedrock) |
conversation_completeness |
full-trace | Whether the user's goals were achieved by end of conversation. Uses GroundTruth.expected_output as success criteria when present; otherwise the judge extracts intentions from the conversation. |
0–1 | optional | yes (Bedrock) |
turn_efficiency |
full-trace | Deterministic. Score is completeness × min(1, expected_turns / actual_turns). Reads completeness_score from trace.metadata if present. |
0–1 | no | no |
AMACE-derived (ported in 2026)
All of the following inherit the existing UAEF Bedrock-judge pattern and
return a MetricScore with metric_name, score in [0, 1], reasoning,
and a metadata dict. For the 1–5 scale metrics, the raw integer is
preserved in metadata["raw_score"]. For boolean metrics, the original
0/1 is stored in metadata["raw"].
| Metric | Phase | Description | Score | Ground truth | LLM judge |
|---|---|---|---|---|---|
role_adherence |
full-trace | Ratio of assistant turns that stay in the assigned role/persona. | 0–1 | required (expected_arguments["chatbot_role"]) |
yes (Bedrock) |
holistic_llm_judge |
full-trace | Composite multi-factor judge weighing completeness, relevancy, knowledge retention, sentiment, tone, satisfaction, and (heavily) containment. | 0–1 | no | yes (Bedrock) |
user_satisfaction |
full-trace | End-of-conversation user satisfaction. Raw 1–5 normalized to 0–1. | 0–1 (raw 1–5 in metadata["raw_score"]) |
no | yes (Bedrock) |
sentiment |
full-trace | User's sentiment across the conversation. The judge weighs later interactions more heavily. Raw 1–5 normalized to 0–1. | 0–1 (raw 1–5 in metadata["raw_score"]) |
no | yes (Bedrock) |
agent_tone |
full-trace | Agent's tone professionalism. Raw 1–5 normalized to 0–1. | 0–1 (raw 1–5 in metadata["raw_score"]) |
no | yes (Bedrock) |
naturalness |
full-trace | Naturalness of the agent's responses across the conversation. Raw 1–5 (1 = robotic / unnatural, 5 = natural / human-like) normalized to 0–1. | 0–1 (raw 1–5 in metadata["raw_score"]) |
no | yes (Bedrock) |
per_turn_sentiment |
per-turn (mean) | User sentiment scored independently per user→assistant exchange. Mean across turns. Per-turn scores in metadata["per_turn_scores"]. |
0–1 (per-turn array in metadata) | no | yes (Bedrock) |
per_turn_agent_tone |
per-turn (mean) | Agent tone professionalism scored independently per user→assistant exchange. Mean across turns. | 0–1 (per-turn array in metadata) | no | yes (Bedrock) |
per_turn_naturalness |
per-turn (mean) | Naturalness of the agent's response scored independently per user→assistant exchange. Mean across turns. | 0–1 (per-turn array in metadata) | no | yes (Bedrock) |
instruction_compliance |
full-trace | Whether the agent followed the original system-prompt instructions across the conversation. | 0–1 | required (expected_arguments["system_instructions"], falls back to expected_output) |
yes (Bedrock) |
optimum_turns |
full-trace | LLM-estimated optimum turn count divided by actual turns, capped at 1.0. Raw integer optimum stored in metadata["optimum_turns"]. |
0–1 (raw counts in metadata) |
no | yes (Bedrock) |
Note:
containmentandresolutionwere originally part of this set but were carved out as opt-in use-case-specific metrics (contact-center focus). They are not in the default registry. See Use-Case-Specific Metrics for activation and usage.
For a list of which specific turns drifted out of role, opt into
deepeval_role_adherence instead — it wraps DeepEval's
RoleAdherenceMetric, which returns per-turn out-of-character verdicts in
metadata["out_of_character_turns"].
Ground-truth shape
Two AMACE metrics need extra context that you supply via
GroundTruth.expected_arguments:
from uaef.models.ground_truth import GroundTruth
gt = GroundTruth(
expected_output="Resolve the customer's billing question.", # used by conversation_completeness
expected_arguments={
"chatbot_role": (
"You are a customer support specialist for ACME Corp's "
"billing team. You can answer billing questions and create "
"tickets, but you cannot make refunds."
),
"system_instructions": (
"Always greet the customer by name. Never quote prices "
"without confirming the latest pricing. Escalate to a human "
"agent if the customer asks for a refund."
),
},
)
When these arguments are missing, role_adherence
(no fallback — supply this key explicitly) and
instruction_compliance short-circuit to score=None with
metadata["warning"] = "not_applicable" rather than calling the judge.
Picking a metric set
Each new metric belongs to the Multi-Turn dimension already configured
in SingleAgentEvaluator / MultiAgentEvaluator (default weight 0.10,
threshold 0.6). You don't need to register or weight new dimensions to use
them. To run a curated subset, pass a metric_set to evaluate() /
batch_evaluate():
from uaef.api import evaluate
result = evaluate(
trace=trace,
ground_truth=gt,
metric_set={
"Multi-Turn": [
"context_retention",
"coherence",
"role_adherence",
],
# Add per-turn relevance via the Response Quality dimension:
"Response Quality": ["answer_relevance"],
},
)
If you omit metric_set entirely, the evaluator runs every metric in the
registry that matches the configured dimensions — including all Multi-Turn
metrics. Each LLM-judge metric is one Bedrock call per evaluation, so
curate aggressively in tight loops.
How AMACE prompts were adapted
- Verbatim where possible — the sentiment, agent-tone, uncanny-valley,
containment, resolution, and optimum-turns prompts are direct ports of
AMACE's
Libraries/Evaluation/src/metrics/*.pyprompts, lightly edited for the UAEF transcript format (USER:/ASSISTANT:rather than AMACE's[Customer]/[Bot]). - Newly authored —
role_adherencemaps to a DeepEval library metric in AMACE; UAEF reimplements it as a native Bedrock judge following the same scoring formula stated in the AMACE README ("turns adhered / total turns"). - Composite —
holistic_llm_judgeports AMACE'sLLMJudgeScorerubric (completeness, relevancy, retention, sentiment, tone, satisfaction, containment, with extra weight on containment) into a single 0–1 judge.
eval() calls in the source repo's resolution.py / optimum_turns.py
were not preserved — the ports use UAEF's
uaef.metrics.utils.extract_json_from_llm_response for safe parsing.
Cost and behavior notes
- Each LLM-judge metric is one Bedrock call per evaluation. Multi-Turn
metrics are full-trace, so each runs once per conversation regardless of
turn count. Per-turn metrics (e.g.,
answer_relevancein the Response Quality dimension) run once per turn and are averaged. - Sentiment / tone / naturalness ship in two flavors. The full-trace
versions (
sentiment,agent_tone,naturalness) evaluate the conversational arc holistically — one judge call, with the prompt weighing later turns more heavily. The per-turn variants (per_turn_sentiment,per_turn_agent_tone,per_turn_naturalness) score each user→assistant exchange independently and aggregate by mean, exposing per-turn scores inmetadata["per_turn_scores"]so consumers can pinpoint the specific turn that went bad. Per-turn variants make N judge calls per conversation but parallelize them viaasyncio.gather, so wall-clock stays roughly flat as conversations grow. turn_efficiencyreadstrace.metadata["completeness_score"]thatconversation_completenesswrites during the same evaluation. The evaluator's dependency-aware scheduler runsconversation_completenessfirst; if you skip it,turn_efficiencyfalls back to assuming completeness 1.0.- All metrics handle Bedrock failures uniformly — they return
MetricScore(score=None, ..., metadata={"warning": "api_error", ...})rather than raising, so a single judge outage doesn't fail an entire evaluation.
Code locations
| Component | File |
|---|---|
| Metric implementations (all 15) | src/uaef/metrics/multi_turn.py |
| Shared Bedrock helper | src/uaef/metrics/multi_turn.py:_invoke_bedrock_judge |
| AMACE-derived base | src/uaef/metrics/multi_turn.py:_AMACEFullTraceMetric |
| 1–5 normalizer | src/uaef/metrics/multi_turn.py:_normalize_1_to_5 |
| Registry entries | src/uaef/metrics/registry.py:_register_builtin_metrics |
| Phase routing | src/uaef/evaluation/base_evaluator.py:_run_multi_turn (FULL_TRACE_ONLY) |
| Public exports | src/uaef/metrics/__init__.py |