feat: Third-party eval metrics adapter (DeepEval + Autoevals) with strands-evals mappers#568
feat: Third-party eval metrics adapter (DeepEval + Autoevals) with strands-evals mappers#568stone-coding wants to merge 20 commits into
Conversation
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.
…leTurnParams deprecation
…d EvaluatorInput support
… layer, simplify per TJ/Irene feedback
…ion, add validate_fields to DeepEvalAdapter
…xpected_response_text property
- 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
| customer_mapper=lambda ev: { | ||
| "input": ev.session_spans[0]["attributes"]["question"], | ||
| "output": ev.session_spans[0]["attributes"]["answer"], | ||
| "expected": "the expected answer", | ||
| }, |
There was a problem hiding this comment.
Replaced lambda examples with named functions in docstrings
| def __init__( | ||
| self, | ||
| scorer: Any, | ||
| customer_mapper: Optional[Callable[[EvaluatorInput], Dict[str, Any]]] = None, |
There was a problem hiding this comment.
- We have not updated this type Dict[str, Any]?
- Regarding naming, "Custom mapper" would be a better choice for describing an arbitrary mapper provided by a customer?
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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"], | ||
| ), |
| def __init__( | ||
| self, | ||
| metric: BaseMetric, | ||
| customer_mapper: Optional[Callable[[EvaluatorInput], LLMTestCase]] = None, |
jariy17
left a comment
There was a problem hiding this comment.
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"] |
There was a problem hiding this comment.
nit: It should be AutoEvals
There was a problem hiding this comment.
Fixed — renamed to AutoEvalsAdapter throughout.
| threshold: Optional score threshold for Pass/Fail label. If None, label | ||
| is omitted from the output. | ||
| """ | ||
| self.metric = metric |
There was a problem hiding this comment.
Can this support custom evaluators?
There was a problem hiding this comment.
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": |
There was a problem hiding this comment.
nit: you can combine this and the above if statements together
There was a problem hiding this comment.
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: { |
There was a problem hiding this comment.
does this exist? This should be custom_mapper.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
I think this scorer is suppose to be metric.
| try: | ||
| self.metric.measure(test_case) | ||
| except Exception as e: | ||
| if type(e).__name__ == "MissingTestCaseParamsError": |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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": |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Fixed, imported MissingTestCaseParamsError from deepeval.errors and using except MissingTestCaseParamsError directly. No more string matching.
| "requests>=2.31.0", | ||
| ] | ||
| deepeval = [ | ||
| "deepeval>=2.0.0", |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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]: |
There was a problem hiding this comment.
is this being used? Please remove if it's not
| return result | ||
|
|
||
|
|
||
| def _try_parse_json(val: str) -> Any: |
There was a problem hiding this comment.
is this dead code? PLease remove
- 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
…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)
Summary
Add DeepEvalAdapter and AutoevalsAdapter that integrate third-party evaluation metrics with
AgentCore's code-based evaluator framework.
detect_otel_mapper()fromstrands-agents-evalsforauto-detection of Strands, OpenInference LangChain, and OpenTelemetry LangChain span formats.
Bridge function converts
Session→SpanMapResultfor adapter consumption.custom_mapper: Callable[[EvaluatorInput], LLMTestCase]custom_mapper: Callable[[EvaluatorInput], Dict[str, Any]]ToolCorrectnessMetric and ArgumentCorrectnessMetric
expectedTrajectory → expected_tools, assertions → context
structured errorCode/errorMessage
strands-agents-evals>=1.0.0,<2.0.0Test plan
pytest tests/.../third_party/ -v)OpenTelemetry)