Skip to content

fix: DeepEvalAdapter to extract multi-turn conversations from service-normalized SESSION format - #622

Open
ybdarrenwang wants to merge 1 commit into
aws:mainfrom
ybdarrenwang:fix/deepeval-adapter
Open

fix: DeepEvalAdapter to extract multi-turn conversations from service-normalized SESSION format#622
ybdarrenwang wants to merge 1 commit into
aws:mainfrom
ybdarrenwang:fix/deepeval-adapter

Conversation

@ybdarrenwang

@ybdarrenwang ybdarrenwang commented Aug 5, 2026

Copy link
Copy Markdown

Background: PR #568

PR #568 (feat: third-party eval metrics adapter (DeepEval + Autoevals) with strands-evals mappers) introduced the DeepEvalAdapter — a generic wrapper that takes any DeepEval metric and runs it inside an AgentCore Lambda evaluator. The adapter uses strands-evals mappers to auto-detect span formats, extract fields, and construct the appropriate DeepEval test case (LLMTestCase for single-turn, ConversationalTestCase for multi-turn).

The Bug

The adapter's multi-turn extraction path relies on _session_to_span_map_result() which builds the turns list from multiple AgentInvocationSpan objects — one per trace/invocation in the session. This works when the strands-evals mapper successfully parses spans into the Session → Trace → AgentInvocationSpan hierarchy (i.e., when each conversation turn arrives as a separate span in the CloudWatch split format).

However, the AgentCore evaluation service normalizes spans before invoking Lambda at SESSION level. It collapses all ADOT span documents sharing the same session.id into a single span with multiple span_events entries — each span_event.body contains one conversation turn's input.messages and output.messages.

In this service-normalized format:

  1. The strands-evals mapper either fails to detect the format (wrong/missing scope) or maps it into a Session with only one AgentInvocationSpan (because there's only one physical span)
  2. _session_to_span_map_result() produces turns with only 2 entries (1 user + 1 assistant from the single AgentInvocationSpan)
  3. The guard turns if len(turns) > 2 else None filters this to None
  4. _build_conversational_test_case() sees result.turns is empty and raises: "Multi-turn metric requires multiple conversation turns but only a single turn was found"

The fallback _extract_from_service_format() only handled single-turn extraction from gen_ai semantic convention events — it had no logic for parsing span_events[*].body.

Impact: Every multi-turn metric failed at SESSION level because no turns were extracted at all, forcing users to bypass the adapter and write custom conversational handlers.

The Fix

The fix extends _extract_from_service_format() in registry.py to handle the service-normalized SESSION format:

  1. New helper _extract_message_text() — parses the nested service message structure ({content: {content: [{text: ...}]}} and the {content: {message: [{text: ...}]}} variant) into plain text.

  2. Multi-turn extraction in _extract_from_service_format() — added a new code path that runs first:

    • Scans each span for span_events with ≥ 1 entry
    • Iterates each span_event.body, extracts user input and assistant output from body.input.messages / body.output.messages
    • Builds the turns list (with turns=None when only 1 turn pair exists, matching existing semantics)
    • Returns a SpanMapResult with populated turns field
  3. Test expectation update — Two existing error-handling tests (test_15_unrecognized_scope_deepeval, test_02_unrecognized_scope in autoevals) changed from FIELD_EXTRACTION_ERROR to MISSING_REQUIRED_FIELD because the mapper now succeeds in parsing the span (no extraction error) but finds empty input/output fields (triggering the more specific "missing required field" error).

The fix is backward-compatible: the single-turn gen_ai events path remains unchanged as a fallback, and the multi-turn path only activates when span_events entries are present.

Scope

This PR restores turn extraction (role + content) from the service-normalized
SESSION format. That is sufficient for the multi-turn metrics that only need the
conversation turns:

  • GoalAccuracy, RoleAdherence, ConversationCompleteness, KnowledgeRetention,
    TopicAdherence, TurnRelevancy, ConversationalGEval

Out of Scope / Known Limitation (follow-up)

Metrics that need per-turn side data are NOT fixed by this PR, because the
service-normalized span_events[*].body format has no slot for that data — only
input.messages and output.messages:

  • retrieval_context → TurnContextualPrecision, TurnContextualRecall, TurnFaithfulness
  • tools_called → ToolUse

_build_conversational_test_case() constructs Turn(role, content) only, so even
if the body carried these fields, they would be dropped. Fixing this requires a
format convention for where per-turn retrieval_context / tools_called live in
span_events[*].body (e.g. body.retrieval_context, body.tools), then threading
them through SpanMapResult.turns[i] into the Turn(...) constructor. Tracked as
a follow-up.

Testing

Added 9 new test cases:

test_span_mappers.pyTestServiceNormalizedMultiTurn (6 tests):

  • Extracts all turns from multi-event span_events
  • Input/output reflect the last turn
  • Single span_event returns None turns (not multi-turn)
  • Handles plain string content variant
  • Handles nested {content: {content: [{text: ...}]}} variant
  • Realistic 5-turn session evaluation

test_adapter.pyTestDeepEvalAdapterServiceNormalizedMultiTurn (3 tests):

  • Conversational metric receives all turns with correct count
  • Turn content is correctly extracted from nested message format
  • End-to-end 5-turn session evaluation passes through adapter

Note: RAG-context / tool-use multi-turn metrics are intentionally not covered
(see Out of Scope) — they require a format change, tracked separately.

Issue #, if available:

Description of changes:

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@ybdarrenwang
ybdarrenwang requested a review from a team August 5, 2026 17:16
@github-actions github-actions Bot added the size/m PR size: M label Aug 5, 2026
@ybdarrenwang ybdarrenwang changed the title Fix DeepEvalAdapter to extract multi-turn conversations from service-normalized SESSION format fix: DeepEvalAdapter to extract multi-turn conversations from service-normalized SESSION format Aug 5, 2026
@github-actions github-actions Bot added size/m PR size: M and removed size/m PR size: M labels Aug 5, 2026

@stone-coding stone-coding left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed PR. The fix correctly handles the SESSION-level service-normalized format where multiple turns are collapsed into span_events[*].body. My original _extract_from_service_format() only covered single-turn gen_ai events.

@ybdarrenwang
ybdarrenwang force-pushed the fix/deepeval-adapter branch from 699048e to 338de68 Compare August 6, 2026 19:50
@github-actions github-actions Bot added size/m PR size: M and removed size/m PR size: M labels Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m PR size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants