Skip to content

feat: Third-party eval metrics adapter (DeepEval + Autoevals) with strands-evals mappers#568

Open
stone-coding wants to merge 20 commits into
aws:mainfrom
stone-coding:feature/third-party-eval-adapter
Open

feat: Third-party eval metrics adapter (DeepEval + Autoevals) with strands-evals mappers#568
stone-coding wants to merge 20 commits into
aws:mainfrom
stone-coding:feature/third-party-eval-adapter

Conversation

@stone-coding

@stone-coding stone-coding commented Jul 6, 2026

Copy link
Copy Markdown

Summary

Add DeepEvalAdapter and AutoevalsAdapter that integrate third-party evaluation metrics with
AgentCore's code-based evaluator framework.

  • Span mapping via strands-evals: Uses detect_otel_mapper() from strands-agents-evals for
    auto-detection of Strands, OpenInference LangChain, and OpenTelemetry LangChain span formats.
    Bridge function converts SessionSpanMapResult for adapter consumption.
  • custom_mapper parameter lets customers bypass built-in mappers for unsupported frameworks:
    • DeepEval: custom_mapper: Callable[[EvaluatorInput], LLMTestCase]
    • Autoevals: custom_mapper: Callable[[EvaluatorInput], Dict[str, Any]]
  • tools_called + expected_tools extraction from execute_tool spans — enables
    ToolCorrectnessMetric and ArgumentCorrectnessMetric
  • assertions from reference_inputs mapped to LLMTestCase.context → should now say assertions from reference_inputs mapped to LLMTestCase.context (None when no assertions)
  • reference_inputs full pipeline: expectedResponse → expected_output,
    expectedTrajectory → expected_tools, assertions → context
  • Never raises unhandled exceptions — all error paths return valid EvaluatorOutput with
    structured errorCode/errorMessage
  • Pinned dependency: strands-agents-evals>=1.0.0,<2.0.0

Test plan

  • 38 unit tests passing (pytest tests/.../third_party/ -v)
  • 21 E2E tests passing across:
    • DeepEval: 12 metrics (RAG, Agentic, Custom/GEval, Contextual, Non-LLM, Safety + OpenInference +
      OpenTelemetry)
    • Autoevals: 6 metrics (LLM-Judge, LLM-Scorer, Deterministic + OpenInference + OpenTelemetry)
    • Custom mapper: 3 tests (Google ADK, OpenAI Agents, Autoevals path)
  • E2E validated against live AgentCore service (evaluate() API → Lambda → metric → score)

Introduces a new integrations/deepeval/ module that adapts AgentCore
Lambda evaluation events into DeepEval LLMTestCase objects, runs any
BaseMetric, and returns structured score/label/explanation responses.
- Rename span_parsers → span_mappers, simplify to Strands-only
- Rename field_mapper → customer_mapper across all adapters
- customer_mapper now returns native types directly:
  - DeepEval: EvaluatorInput → LLMTestCase
  - Autoevals: EvaluatorInput → Dict[str, Any] (eval kwargs)
- Refactor BaseAdapter: remove intermediate execute(), add _run() pattern
- 83 unit tests passing
Comment on lines +27 to +31
customer_mapper=lambda ev: {
"input": ev.session_spans[0]["attributes"]["question"],
"output": ev.session_spans[0]["attributes"]["answer"],
"expected": "the expected answer",
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Do we need to accept aws lambda?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Replaced lambda examples with named functions in docstrings

def __init__(
self,
scorer: Any,
customer_mapper: Optional[Callable[[EvaluatorInput], Dict[str, Any]]] = None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

  1. We have not updated this type Dict[str, Any]?
  2. Regarding naming, "Custom mapper" would be a better choice for describing an arbitrary mapper provided by a customer?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

1.The type Dict[str, Any] is correct — Autoevals has no typed
input class like DeepEval's LLMTestCase. Scorers take plain
kwargs (input, output, expected) and the dict gets unpacked
as metric.eval(**kwargs). Different scorers need different
keys, so a generic dict is the right contract.

self,
scorer: Any,
customer_mapper: Optional[Callable[[EvaluatorInput], Dict[str, Any]]] = None,
threshold: float = 0.5,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

label: Optional[str] = None label in EvaluatorOutput is optional.
We don't need to have a default value to generate a label when a metric doesn't have a label and the user doesn't provide any threshold. Can we set default as None?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Changed threshold default to None. When no threshold is set,
no Pass/Fail judgment is made. Note: EvaluatorOutput's
validator currently requires a label for success responses —
I'm returning the score as the label string when threshold is
None. Let me know if you'd rather relax the validator to
allow label=None.

mapping when provided. Expected keys: input, output, expected (optional).
threshold: Score threshold for Pass/Fail determination. Defaults to 0.5.
"""
self.scorer = scorer

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

this naming is not aligned with DeepEval adaptor. Looks like you haven't updated AutoevalsAdapter.
If you haven't completed end to end tests for AutoevalsAdapter, you should not include it in your PR.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Working on E2E test plan now. Considering how to strcuture parameterized Lambda handlers and how to obtains span from strands, openinference, opentelemetry, and customer mappers.

customer_mapper=lambda ev: LLMTestCase(
input=ev.session_spans[0]["attributes"]["user_query"],
actual_output=ev.session_spans[0]["attributes"]["response"],
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

using lambda? same as above

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

fixed

def __init__(
self,
metric: BaseMetric,
customer_mapper: Optional[Callable[[EvaluatorInput], LLMTestCase]] = None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

naming? same as above

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

fixed

@jariy17 jariy17 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.

Lefts some comments but its a good addition to our SDK.


from bedrock_agentcore.evaluation.custom_code_based_evaluators.third_party.autoevals.adapter import AutoevalsAdapter

__all__ = ["AutoevalsAdapter"]

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.

nit: It should be AutoEvals

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed — renamed to AutoEvalsAdapter throughout.

threshold: Optional score threshold for Pass/Fail label. If None, label
is omitted from the output.
"""
self.metric = metric

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.

Can this support custom evaluators?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yes, metric accepts any object with an .eval(output, expected, **kwargs) method. Customers can pass their own scorer class alongside the built-in ones.

scope_name = get_scope_name(span)
if scope_name == SCOPE_AMAZON_OTEL_LANGCHAIN:
return LangChainOtelSessionMapper()
if scope_name == "opentelemetry.instrumentation.langchain":

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.

nit: you can combine this and the above if statements together

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed, combined into if scope_name in (SCOPE_AMAZON_OTEL_LANGCHAIN, "opentelemetry.instrumentation.langchain").

metric = BiasMetric(threshold=0.5)
adapter = DeepEvalAdapter(
metric=metric,
field_mapper=lambda ev: {

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.

does this exist? This should be custom_mapper.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed, renamed to custom_mapper and updated return type to LLMTestCase.

from bedrock_agentcore.evaluation.custom_code_based_evaluators.third_party.autoevals import AutoevalsAdapter

scorer = Factuality()
adapter = AutoevalsAdapter(scorer=scorer)

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.

I think this scorer is suppose to be metric.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed, rename to metric

try:
self.metric.measure(test_case)
except Exception as e:
if type(e).__name__ == "MissingTestCaseParamsError":

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.

type(e).__name__ == "MissingTestCaseParamsError" - we already hard-import deepeval at the top of this file, so just import the exception class and use isinstance. String-matching on class name is fragile.

Also, pyproject.toml pins deepeval>=2.0.0 with no upper bound. If DeepEval renames or moves this class in a future release, this check silently returns False, falls through to the base except Exception, and every MISSING_REQUIRED_FIELD becomes METRIC_ERROR.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed, imported MissingTestCaseParamsError from deepeval.errors and using except MissingTestCaseParamsError directly. No more string matching.

try:
self.metric.measure(test_case)
except Exception as e:
if type(e).__name__ == "MissingTestCaseParamsError":

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.

type(e).__name__ == "MissingTestCaseParamsError" - we already hard-import deepeval at the top of this file, so just import the exception class and use isinstance. String-matching on class name is fragile.

Also, pyproject.toml pins deepeval>=2.0.0 with no upper bound. If DeepEval renames or moves this class in a future release, this check silently returns False, falls through to the base except Exception, and every MISSING_REQUIRED_FIELD becomes METRIC_ERROR.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed, imported MissingTestCaseParamsError from deepeval.errors and using except MissingTestCaseParamsError directly. No more string matching.

Comment thread pyproject.toml
"requests>=2.31.0",
]
deepeval = [
"deepeval>=2.0.0",

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.

registry.py imports strands_evals but neither the deepeval nor autoevals extras include strands-agents-evals. A customer following the advertised install path (pip install bedrock-agentcore[deepeval]) gets an ImportError at cold start. Add strands-agents-evals>=1.0.0,<2.0.0 to both extras.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed, added strands-agents-evals>=1.0.0,<2.0.0 to both deepeval and autoevals extras.

expected_tools: Optional[List[Dict[str, Any]]] = None
assertions: Optional[List[str]] = None

def to_dict(self) -> Dict[str, Any]:

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.

is this being used? Please remove if it's not

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

removed.

return result


def _try_parse_json(val: str) -> Any:

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.

is this dead code? PLease remove

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

removed.

- Rename AutoevalsAdapter → AutoEvalsAdapter (class + all imports)
- Fix integ tests: scorer→metric, field_mapper→custom_mapper, correct return types
- Combine LangChain scope if-statements into single check
- context=None when no assertions (don't use tool outputs as ground truth)
- Import MissingTestCaseParamsError properly, use isinstance() not string matching
- Add strands-agents-evals>=1.0.0,<2.0.0 to deepeval and autoevals extras
- Create FieldExtractionError(ValueError) for scoped exception handling
- Remove dead code: to_dict(), _try_parse_json
- Document trace-level only limitation in _session_to_span_map_result docstring
brandonaxu pushed a commit to brandonaxu/bedrock-agentcore-sdk-python that referenced this pull request Jul 20, 2026
…SAdapter to use the new BaseAdapter._run() pattern with SpanMapResult, custom_mapper (replacing field_mapper), and CloudWatch split format spans. Update tests to use the new span format and API.
- Add `turns` field to SpanMapResult for multi-turn session data
- Build turns from all AgentInvocationSpans across traces in registry
- Branch on BaseConversationalMetric in DeepEvalAdapter to build
  ConversationalTestCase with Turn objects
- Return FieldExtractionError when single-turn spans used with
  conversational metrics
- Remove SCOPE_AMAZON_OTEL_LANGCHAIN workaround (no longer needed
  after Irene sync — not a supported path)
- Set context=None instead of duplicating retrieval_context (per TJ)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants