Multi-Turn Conversation Eval
Score multi-turn support conversations for quality, context retention, loop detection, and escalation with fi.evals, then run the 10-metric conversational agent group from the dashboard.
Score a multi-turn customer support conversation for overall quality, then diagnose it further for context loss, repetitive loops, and missed escalation using Future AGI’s built-in conversation metrics. Finish with a scorecard comparing a good conversation against a bad one.
| Time | Difficulty | Package |
|---|---|---|
| 15 min | Intermediate | ai-evaluation |
- Future AGI account → app.futureagi.com
- API keys:
FI_API_KEYandFI_SECRET_KEY(see Get your API keys) - Python 3.11
Install
pip install ai-evaluation
export FI_API_KEY="your-api-key"
export FI_SECRET_KEY="your-secret-key"
Tutorial
Set up the evaluator
The Evaluator client reads both keys and is reused for every metric below.
import os
import json
from fi.evals import Evaluator
evaluator = Evaluator(
fi_api_key=os.environ["FI_API_KEY"],
fi_secret_key=os.environ["FI_SECRET_KEY"],
)The constructor makes no network call, so a clean import and no traceback is the success signal here; a bad key surfaces on the first evaluate() call in Step 3.
Define two conversations
A well-handled support conversation and a poorly-handled one. Both start with the same customer issue: a duplicate charge.
good_conversation = [
{"role": "user", "content": "Hi, I was charged twice for order #7291. Can you help?"},
{"role": "assistant", "content": "I'm sorry about that! Let me look up order #7291 right away."},
{"role": "assistant", "content": "I can see the duplicate charge. I've initiated a refund of $49.99 to your original payment method. It should appear within 3-5 business days."},
{"role": "user", "content": "Great, thank you. Will I get a confirmation email?"},
{"role": "assistant", "content": "Yes, you'll receive a refund confirmation email within the next hour. Is there anything else I can help with?"},
{"role": "user", "content": "No, that's all. Thanks for the quick help!"},
{"role": "assistant", "content": "You're welcome! Don't hesitate to reach out if you need anything else. Have a great day!"},
]
bad_conversation = [
{"role": "user", "content": "Hi, I was charged twice for order #7291. Can you help?"},
{"role": "assistant", "content": "I'd be happy to help! Have you tried checking your account settings?"},
{"role": "user", "content": "This isn't about account settings. I was charged $49.99 twice for the same order."},
{"role": "assistant", "content": "I understand. Could you provide your order number?"},
{"role": "user", "content": "I already said it's order #7291."},
{"role": "assistant", "content": "Could you provide your order number so I can look into this?"},
{"role": "user", "content": "I just told you twice, #7291. Can I speak to a manager?"},
{"role": "assistant", "content": "I apologize for the inconvenience. Have you considered upgrading to our Premium support plan for faster resolution?"},
{"role": "user", "content": "No! I just want my refund. Please transfer me to someone who can help."},
{"role": "assistant", "content": "I understand your concern. Let me check your account settings to see if there's an issue."},
]The bad conversation has multiple failure modes. It ignores the actual question, forgets the order number and asks twice, loops back to “account settings”, tries to upsell, and doesn’t escalate when the customer asks for a manager.
Score overall conversation quality
customer_agent_conversation_quality rates the overall interaction on a 1-5 scale considering clarity, helpfulness, responsiveness, and tone.
Note
Choice-based metrics (quality, query handling, loop detection, escalation) return eval_result.output as a list (for example ['5']). Score-based metrics (coherence, resolution, context retention) return a plain number. In the snippets below, output[0] extracts the value when the output is a list.
for label, convo in [("Good", good_conversation), ("Bad", bad_conversation)]:
result = evaluator.evaluate(
eval_templates="customer_agent_conversation_quality",
inputs={"conversation": json.dumps(convo)},
model_name="turing_small",
)
eval_result = result.eval_results[0]
score = eval_result.output[0] if isinstance(eval_result.output, list) else eval_result.output
print(f"{label} conversation: {score}/5")
print(f" Reason: {eval_result.reason}\n")Tip
Every conversation metric on this page also accepts a valid audio URL in place of the JSON conversation string. Use model_name="turing_large" for audio inputs.
You should see output shaped like this (illustrative scores, your model call will vary):
Good conversation: 5/5
Reason: The agent promptly addressed the issue, provided a clear resolution...
Bad conversation: 1/5
Reason: The agent repeatedly ignored the customer's request, forgot context... Diagnose why the bad conversation failed
Run targeted metrics on the bad conversation to pinpoint specific failure modes.
Context retention: did the agent remember details from earlier in the conversation?
result = evaluator.evaluate(
eval_templates="customer_agent_context_retention",
inputs={"conversation": json.dumps(bad_conversation)},
model_name="turing_small",
)
eval_result = result.eval_results[0]
print(f"Context retention: {eval_result.output}")
print(f"Reason: {eval_result.reason}")Query handling: did the agent correctly interpret and answer the customer’s questions?
result = evaluator.evaluate(
eval_templates="customer_agent_query_handling",
inputs={"conversation": json.dumps(bad_conversation)},
model_name="turing_small",
)
eval_result = result.eval_results[0]
score = eval_result.output[0] if isinstance(eval_result.output, list) else eval_result.output
print(f"Query handling: {score}")
print(f"Reason: {eval_result.reason}")Loop detection: did the agent get stuck repeating the same prompts?
result = evaluator.evaluate(
eval_templates="customer_agent_loop_detection",
inputs={"conversation": json.dumps(bad_conversation)},
model_name="turing_small",
)
eval_result = result.eval_results[0]
score = eval_result.output[0] if isinstance(eval_result.output, list) else eval_result.output
print(f"Loop detection: {score}")
print(f"Reason: {eval_result.reason}")Human escalation: did the agent escalate when the customer asked for a manager?
result = evaluator.evaluate(
eval_templates="customer_agent_human_escalation",
inputs={"conversation": json.dumps(bad_conversation)},
model_name="turing_small",
)
eval_result = result.eval_results[0]
score = eval_result.output[0] if isinstance(eval_result.output, list) else eval_result.output
print(f"Human escalation: {score}")
print(f"Reason: {eval_result.reason}")You should see four verdicts, each naming a different failure (illustrative):
Context retention: 12
Reason: The agent asked for the order number twice despite the user providing it...
Query handling: never
Reason: The agent never directly addressed the duplicate charge issue...
Loop detection: frequently
Reason: The agent circled back to "account settings" twice and asked for the order number twice...
Human escalation: Failed
Reason: The user explicitly requested a manager but the agent deflected with an upsell...Each metric catches a different dimension of failure. Together they tell a clear story: the agent forgot context, ignored the question, looped, and refused to escalate.
Evaluate prompt conformance
customer_agent_prompt_conformance checks whether the agent followed its system prompt throughout the conversation. It’s the only conversation metric that takes an additional system_prompt input.
system_prompt = (
"You are a billing support agent for TechStore. "
"Your role is to help customers resolve payment and billing issues. "
"Always acknowledge the customer's issue first, then investigate. "
"Never upsell products during a support interaction. "
"If a customer asks to speak with a manager, escalate immediately."
)
for label, convo in [("Good", good_conversation), ("Bad", bad_conversation)]:
result = evaluator.evaluate(
eval_templates="customer_agent_prompt_conformance",
inputs={
"system_prompt": system_prompt,
"conversation": json.dumps(convo),
},
model_name="turing_small",
)
eval_result = result.eval_results[0]
score = eval_result.output[0] if isinstance(eval_result.output, list) else eval_result.output
print(f"{label} conversation - prompt conformance: {score}")
print(f" Reason: {eval_result.reason}\n")You should see the good conversation score high and the bad one score low (illustrative):
Good conversation - prompt conformance: 95
Reason: The agent acknowledged the issue, investigated, and resolved it...
Bad conversation - prompt conformance: 8
Reason: The agent violated multiple system prompt rules: upsold a product, failed to escalate... Run a full scorecard
Run 7 key metrics on both conversations in a single diagnostic sweep.
metrics = [
("conversation_coherence", "Coherence"),
("conversation_resolution", "Resolution"),
("customer_agent_conversation_quality", "Quality"),
("customer_agent_context_retention", "Context"),
("customer_agent_query_handling", "Queries"),
("customer_agent_loop_detection", "Loops"),
("customer_agent_human_escalation", "Escalation"),
]
print(f"{'Metric':<14} {'Good':>12} {'Bad':>12}")
print("-" * 42)
for metric_name, label in metrics:
good_result = evaluator.evaluate(
eval_templates=metric_name,
inputs={"conversation": json.dumps(good_conversation)},
model_name="turing_small",
)
bad_result = evaluator.evaluate(
eval_templates=metric_name,
inputs={"conversation": json.dumps(bad_conversation)},
model_name="turing_small",
)
good_raw = good_result.eval_results[0].output
bad_raw = bad_result.eval_results[0].output
good_val = good_raw[0] if isinstance(good_raw, list) else good_raw
bad_val = bad_raw[0] if isinstance(bad_raw, list) else bad_raw
print(f"{label:<14} {str(good_val):>12} {str(bad_val):>12}")You should see the good conversation pass every metric and the bad one fail across the board (illustrative scores):
Metric Good Bad
------------------------------------------
Coherence 1.0 0.4
Resolution 1.0 0.0
Quality 5/5 1/5
Context 95 12
Queries always never
Loops never frequently
Escalation Passed Failed Run the eval group from the dashboard
You can run all 10 conversational agent metrics at once from the dashboard using the Conversational agent evaluation eval group, no code required.
- Go to app.futureagi.com and open Dataset
- Open a dataset that has a
conversationcolumn (a JSON array ofrole/contentmessages) and asystem_promptcolumn with the agent’s system prompt - Click Evaluate then Add Evaluations
- Under Groups, select Conversational agent evaluation. This adds all 10 metrics in one click
- Map the
conversationcolumn to the conversation input, and thesystem_promptcolumn to the system prompt input. This is needed forcustomer_agent_prompt_conformance, which checks whether the agent followed its instructions - Click Add & Run
All metrics run in parallel. Scores appear as new columns alongside your data, one column per metric. Most metrics only need the conversation column; the system_prompt mapping is used by customer_agent_prompt_conformance and ignored by the rest.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
score = eval_result.output[0] if isinstance(...) raises IndexError | eval_templates is a choice-based metric but result.eval_results came back empty | Check result.eval_results is non-empty before indexing, and confirm the metric name is spelled exactly as in the eval reference |
KeyError: 'conversation' from evaluator.evaluate() | The inputs dict is missing the conversation key, or it wasn’t json.dumps-encoded | Pass inputs={"conversation": json.dumps(convo)}, not the raw Python list |
customer_agent_prompt_conformance call fails with a missing-input error | system_prompt wasn’t included in inputs | This is the only conversation metric that requires both system_prompt and conversation in inputs |
401 or KeyError from Evaluator(...) | FI_API_KEY or FI_SECRET_KEY isn’t exported in the shell running the script | Re-export both keys, then confirm with python -c "import os; print(os.environ['FI_API_KEY'][:4])" |
| Score-based and choice-based outputs printed inconsistently | Score metrics (coherence, resolution, context retention) return a plain value; choice metrics (quality, query handling, loop detection) return a list | Use the output[0] if isinstance(output, list) else output pattern from the steps above, not a fixed index |
Dashboard eval group finishes but customer_agent_prompt_conformance shows no score | The dataset’s system_prompt column wasn’t mapped in step 5 | Re-open Add Evaluations and map system_prompt explicitly; other metrics run fine without it |
| Full scorecard loop is slow | evaluator.evaluate() runs once per metric per conversation, synchronously, 14 calls total for 7 metrics | Pass is_async=True to evaluate(), or run the dashboard eval group instead for large datasets |
Next: Chat Simulation with Personas generates the multi-turn conversations these metrics score, and runs the same eval group automatically on every completed simulation.
Questions & Discussion