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

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: ALL multi-turn metrics (GoalAccuracy, RoleAdherence, ToolUse, ConversationCompleteness, KnowledgeRetention, TopicAdherence, TurnContextualPrecision, TurnContextualRecall, TurnFaithfulness) failed when invoked through the adapter at SESSION level. This forced users to bypass the adapter entirely and write custom conversational-template 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.

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

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.

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