-
Notifications
You must be signed in to change notification settings - Fork 129
feat: Third-party eval metrics adapter (DeepEval + Autoevals) with strands-evals mappers #568
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
stone-coding
wants to merge
17
commits into
aws:main
Choose a base branch
from
stone-coding:feature/third-party-eval-adapter
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,856
−8
Open
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
77d16a9
Add DeepEvalHandler integration with unit tests
haomiao037 402ea78
Fix span extraction to use real AgentCore _eval_log_records structure
haomiao037 e9ef47d
Set context field from tool messages for HallucinationMetric support
haomiao037 f97827e
Use metric.success for label instead of manual threshold comparison
haomiao037 9d256ae
Add model override and timeout enforcement to DeepEvalHandler
haomiao037 c142d50
Add model override, timeout enforcement, use metric.success, fix Sing…
haomiao037 3ccc98c
Fix _get_required_params to handle GEval unmappable typing params
haomiao037 a884f91
Add .deepeval/ to gitignore
haomiao037 6ac198c
Move model override to init to avoid per-call mutation
haomiao037 8d415e5
Refactor to BaseAdapter framework with DeepEval/Autoevals adapters an…
haomiao037 8627ab0
Major refactor: move to custom_code_based_evaluators, add span parser…
haomiao037 9a9b6a7
Fix review items: add reference_inputs to model, tighten error detect…
haomiao037 0499d4b
Adapt to upstream ReferenceInput model: remove duplicate field, use e…
haomiao037 9f407ea
feat: Add third-party eval metrics adapter (DeepEval + Autoevals)
haomiao037 abf1f9a
feat: Add tools_called/expected_tools, LangChain mappers, E2E tests (…
haomiao037 e7af34f
refactor: Replace custom span mappers with strands-evals dependency
haomiao037 2ca8a5a
chore: Remove e2e_tests from public repo (internal testing only)
haomiao037 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -229,3 +229,5 @@ local_settings.py | |
| Dockerfile | ||
| CLAUDE.md | ||
| .omc/ | ||
| .deepeval/ | ||
| e2e_tests/ | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
5 changes: 5 additions & 0 deletions
5
src/bedrock_agentcore/evaluation/custom_code_based_evaluators/third_party/__init__.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| """Third-party evaluation adapters for AgentCore code-based evaluators.""" | ||
|
|
||
| from bedrock_agentcore.evaluation.custom_code_based_evaluators.third_party.base import BaseAdapter | ||
|
|
||
| __all__ = ["BaseAdapter"] |
5 changes: 5 additions & 0 deletions
5
...drock_agentcore/evaluation/custom_code_based_evaluators/third_party/autoevals/__init__.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| """Autoevals adapter for AgentCore code-based evaluators.""" | ||
|
|
||
| from bedrock_agentcore.evaluation.custom_code_based_evaluators.third_party.autoevals.adapter import AutoevalsAdapter | ||
|
|
||
| __all__ = ["AutoevalsAdapter"] |
98 changes: 98 additions & 0 deletions
98
...edrock_agentcore/evaluation/custom_code_based_evaluators/third_party/autoevals/adapter.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| """Autoevals adapter for AgentCore code-based evaluators.""" | ||
|
|
||
| import logging | ||
| from typing import Any, Callable, Dict, Optional | ||
|
|
||
| from bedrock_agentcore.evaluation.custom_code_based_evaluators.models import EvaluatorInput, EvaluatorOutput | ||
| from bedrock_agentcore.evaluation.custom_code_based_evaluators.third_party.base import BaseAdapter | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class AutoevalsAdapter(BaseAdapter): | ||
| """Adapter that runs an Autoevals scorer against AgentCore evaluation events. | ||
|
|
||
| Example (default span mapping):: | ||
|
|
||
| from autoevals import Factuality | ||
| from bedrock_agentcore.evaluation.custom_code_based_evaluators.third_party.autoevals import AutoevalsAdapter | ||
|
|
||
| scorer = Factuality() | ||
| adapter = AutoevalsAdapter(metric=scorer) | ||
|
|
||
| Example (custom mapper returning eval kwargs):: | ||
|
|
||
| from typing import Dict, Any | ||
|
|
||
| def my_mapper(ev: EvaluatorInput) -> Dict[str, Any]: | ||
| return { | ||
| "input": ev.session_spans[0]["attributes"]["question"], | ||
| "output": ev.session_spans[0]["attributes"]["answer"], | ||
| "expected": "the expected answer", | ||
| } | ||
|
|
||
| adapter = AutoevalsAdapter( | ||
| metric=Factuality(), | ||
| custom_mapper=my_mapper, | ||
| ) | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| metric: Any, | ||
| custom_mapper: Optional[Callable[[EvaluatorInput], Dict[str, Any]]] = None, | ||
| threshold: Optional[float] = None, | ||
| ): | ||
| """Initialize the adapter. | ||
|
|
||
| Args: | ||
| metric: An Autoevals scorer instance (e.g. Factuality(), ClosedQA()). | ||
| custom_mapper: Optional callable that receives the EvaluatorInput and | ||
| returns a dict of kwargs for metric.eval(). Bypasses default span | ||
| mapping when provided. Expected keys: input, output, expected (optional). | ||
| threshold: Optional score threshold for Pass/Fail label. If None, label | ||
| is omitted from the output. | ||
| """ | ||
| self.metric = metric | ||
| self.custom_mapper = custom_mapper | ||
| self.threshold = threshold | ||
|
|
||
| def _run(self, evaluator_input: EvaluatorInput) -> EvaluatorOutput: | ||
| """Run the Autoevals scorer pipeline.""" | ||
| if self.custom_mapper is not None: | ||
| kwargs = self.custom_mapper(evaluator_input) | ||
| else: | ||
| result = self._default_extract(evaluator_input) | ||
| if not result.input or not result.actual_output: | ||
| missing = [] | ||
| if not result.input: | ||
| missing.append("input") | ||
| if not result.actual_output: | ||
| missing.append("actual_output") | ||
| metric_name = type(self.metric).__name__ | ||
| return EvaluatorOutput( | ||
| label="Error", | ||
| errorCode="MISSING_REQUIRED_FIELD", | ||
| errorMessage=f"Field(s) {missing} required by {metric_name} but not found in evaluation event. " | ||
| f"Provide a custom_mapper or ensure spans contain the necessary data.", | ||
| ) | ||
| kwargs: Dict[str, Any] = { | ||
| "input": result.input, | ||
| "output": result.actual_output, | ||
| } | ||
| if result.expected_output: | ||
| kwargs["expected"] = result.expected_output | ||
| elif result.assertions: | ||
| kwargs["expected"] = "\n".join(result.assertions) | ||
| if result.retrieval_context: | ||
| kwargs["context"] = "\n".join(result.retrieval_context) | ||
|
|
||
| eval_result = self.metric.eval(**kwargs) | ||
|
|
||
| score = eval_result.score | ||
| label = None | ||
| if self.threshold is not None: | ||
| label = "Pass" if score is not None and score >= self.threshold else "Fail" | ||
| explanation = getattr(eval_result, "metadata", {}).get("rationale", "") if hasattr(eval_result, "metadata") else "" | ||
|
|
||
| return EvaluatorOutput(value=score, label=label, explanation=explanation) |
84 changes: 84 additions & 0 deletions
84
src/bedrock_agentcore/evaluation/custom_code_based_evaluators/third_party/base.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| """Base adapter for third-party evaluation framework integrations.""" | ||
|
|
||
| import abc | ||
| import logging | ||
| from typing import Any, Dict, List, Optional | ||
|
|
||
| from bedrock_agentcore.evaluation.custom_code_based_evaluators.models import EvaluatorInput, EvaluatorOutput | ||
| from bedrock_agentcore.evaluation.custom_code_based_evaluators.third_party.span_mappers import ( | ||
| SpanMapResult, | ||
| map_spans, | ||
| ) | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class BaseAdapter(abc.ABC): | ||
| """Base adapter for third-party evaluation framework integrations. | ||
|
|
||
| Accepts an EvaluatorInput (from the code_based_evaluators flow), | ||
| extracts fields from spans using the built-in mapper layer, runs the | ||
| evaluation via execute(), and returns an EvaluatorOutput. | ||
|
|
||
| Never raises unhandled exceptions — always returns a valid EvaluatorOutput. | ||
| """ | ||
|
|
||
| def __call__(self, evaluator_input: EvaluatorInput, context: Any = None) -> EvaluatorOutput: | ||
| """Handle an evaluation invocation. | ||
|
|
||
| Args: | ||
| evaluator_input: Parsed EvaluatorInput from the code-based evaluator flow. | ||
| context: Lambda context object (unused). | ||
|
|
||
| Returns: | ||
| EvaluatorOutput with score, label, and explanation or error fields. | ||
| """ | ||
| try: | ||
| return self._run(evaluator_input) | ||
| except ValueError as e: | ||
| logger.error("Field extraction failed: %s", e) | ||
| return EvaluatorOutput( | ||
| label="Error", | ||
| errorCode="FIELD_EXTRACTION_ERROR", | ||
| errorMessage=str(e), | ||
| ) | ||
| except Exception as e: | ||
| logger.error("Execution failed: %s", e, exc_info=True) | ||
| return EvaluatorOutput( | ||
| label="Error", | ||
| errorCode="METRIC_ERROR", | ||
| errorMessage=f"{type(self).__name__} failed: {e}", | ||
| ) | ||
|
|
||
| @abc.abstractmethod | ||
| def _run(self, evaluator_input: EvaluatorInput) -> EvaluatorOutput: | ||
| """Run the full evaluation pipeline. Subclasses implement this.""" | ||
|
|
||
| def _default_extract(self, evaluator_input: EvaluatorInput) -> SpanMapResult: | ||
| """Extract fields using the built-in span mapper layer.""" | ||
| spans = self._filter_spans_by_target(evaluator_input) | ||
| return map_spans(spans, evaluator_input.reference_inputs) | ||
|
|
||
| def _filter_spans_by_target(self, evaluator_input: EvaluatorInput) -> List[Dict]: | ||
| """Filter session spans based on evaluationLevel and evaluationTarget. | ||
|
|
||
| The service passes ALL session spans in every Lambda invocation without | ||
| pre-filtering. It fans out one Lambda call per evaluation target (one per | ||
| trace at TRACE level, one per span at TOOL_CALL level) and provides the | ||
| target ID so the Lambda can scope its evaluation. We filter here because | ||
| the service-side _invoke_single_eval_target passes session_spans directly | ||
| into the payload without filtering. | ||
|
|
||
| Levels: | ||
| - SESSION: all spans (no filtering) — one call for the entire session | ||
| - TRACE: only spans matching target_trace_id (service sends exactly one) | ||
| - TOOL_CALL: only the span matching target_span_id (service sends exactly one) | ||
| """ | ||
| spans = evaluator_input.session_spans | ||
|
|
||
| if evaluator_input.evaluation_level == "TRACE" and evaluator_input.target_trace_id: | ||
| spans = [s for s in spans if s.get("traceId") == evaluator_input.target_trace_id] | ||
| elif evaluator_input.evaluation_level == "TOOL_CALL" and evaluator_input.target_span_id: | ||
| spans = [s for s in spans if s.get("spanId") == evaluator_input.target_span_id] | ||
|
|
||
| return spans |
5 changes: 5 additions & 0 deletions
5
...edrock_agentcore/evaluation/custom_code_based_evaluators/third_party/deepeval/__init__.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| """DeepEval adapter for AgentCore code-based evaluators.""" | ||
|
|
||
| from bedrock_agentcore.evaluation.custom_code_based_evaluators.third_party.deepeval.adapter import DeepEvalAdapter | ||
|
|
||
| __all__ = ["DeepEvalAdapter"] |
121 changes: 121 additions & 0 deletions
121
...bedrock_agentcore/evaluation/custom_code_based_evaluators/third_party/deepeval/adapter.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| """DeepEval adapter for AgentCore code-based evaluators.""" | ||
|
|
||
| import logging | ||
| from typing import Any, Callable, Dict, List, Optional | ||
|
|
||
| from deepeval.metrics import BaseMetric | ||
| from deepeval.test_case import LLMTestCase, ToolCall | ||
|
|
||
| from bedrock_agentcore.evaluation.custom_code_based_evaluators.models import EvaluatorInput, EvaluatorOutput | ||
| from bedrock_agentcore.evaluation.custom_code_based_evaluators.third_party.base import BaseAdapter | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class DeepEvalAdapter(BaseAdapter): | ||
| """Adapter that runs a DeepEval metric against AgentCore evaluation events. | ||
|
|
||
| Example (default span mapping):: | ||
|
|
||
| from deepeval.metrics import AnswerRelevancyMetric | ||
| from bedrock_agentcore.evaluation.custom_code_based_evaluators.third_party.deepeval import DeepEvalAdapter | ||
|
|
||
| metric = AnswerRelevancyMetric(threshold=0.7) | ||
| adapter = DeepEvalAdapter(metric=metric) | ||
|
|
||
| Example (custom mapper returning LLMTestCase):: | ||
|
|
||
| from deepeval.test_case import LLMTestCase | ||
|
|
||
| def my_mapper(ev: EvaluatorInput) -> LLMTestCase: | ||
| return LLMTestCase( | ||
| input=ev.session_spans[0]["attributes"]["user_query"], | ||
| actual_output=ev.session_spans[0]["attributes"]["response"], | ||
| ) | ||
|
|
||
| adapter = DeepEvalAdapter( | ||
| metric=AnswerRelevancyMetric(threshold=0.7), | ||
| custom_mapper=my_mapper, | ||
| ) | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| metric: BaseMetric, | ||
| custom_mapper: Optional[Callable[[EvaluatorInput], LLMTestCase]] = None, | ||
| ): | ||
| """Initialize the adapter. | ||
|
|
||
| Args: | ||
| metric: A DeepEval BaseMetric instance (e.g. AnswerRelevancyMetric). | ||
| custom_mapper: Optional callable that receives the EvaluatorInput and | ||
| returns a LLMTestCase. Bypasses default span mapping when provided. | ||
| """ | ||
| self.metric = metric | ||
| self.custom_mapper = custom_mapper | ||
|
|
||
| def _run(self, evaluator_input: EvaluatorInput) -> EvaluatorOutput: | ||
| """Run the DeepEval metric pipeline.""" | ||
| if self.custom_mapper is not None: | ||
| test_case = self.custom_mapper(evaluator_input) | ||
| else: | ||
| result = self._default_extract(evaluator_input) | ||
| if not result.input or not result.actual_output: | ||
| missing = [] | ||
| if not result.input: | ||
| missing.append("input") | ||
| if not result.actual_output: | ||
| missing.append("actual_output") | ||
| metric_name = type(self.metric).__name__ | ||
| return EvaluatorOutput( | ||
| label="Error", | ||
| errorCode="MISSING_REQUIRED_FIELD", | ||
| errorMessage=f"Field(s) {missing} required by {metric_name} but not found in evaluation event. " | ||
| f"Provide a custom_mapper or ensure spans contain the necessary data.", | ||
| ) | ||
| # assertions from reference_inputs take precedence over span-extracted context | ||
| context = result.assertions if result.assertions else result.context | ||
| test_case = LLMTestCase( | ||
| input=result.input, | ||
| actual_output=result.actual_output, | ||
| expected_output=result.expected_output, | ||
| context=context, | ||
| retrieval_context=result.retrieval_context, | ||
| tools_called=self._build_tool_calls(result.tools_called) if result.tools_called else None, | ||
| expected_tools=self._build_tool_calls(result.expected_tools) if result.expected_tools else None, | ||
| ) | ||
|
|
||
| try: | ||
| self.metric.measure(test_case) | ||
| except Exception as e: | ||
| if type(e).__name__ == "MissingTestCaseParamsError": | ||
| return EvaluatorOutput( | ||
| label="Error", | ||
| errorCode="MISSING_REQUIRED_FIELD", | ||
| errorMessage=f"{type(self.metric).__name__} requires fields not extracted from spans: {e}. " | ||
| f"Provide a custom_mapper to supply custom fields from your trace data.", | ||
| ) | ||
| raise | ||
|
|
||
| score = self.metric.score | ||
| reason = getattr(self.metric, "reason", None) or "" | ||
| threshold = getattr(self.metric, "threshold", 0.5) | ||
| success = getattr(self.metric, "success", score is not None and score >= threshold) | ||
| label = "Pass" if success else "Fail" | ||
|
|
||
| return EvaluatorOutput(value=score, label=label, explanation=reason) | ||
|
|
||
| @staticmethod | ||
| def _build_tool_calls(tools_called: List[Dict[str, Any]]) -> List[ToolCall]: | ||
| """Convert extracted tool call dicts to DeepEval ToolCall objects.""" | ||
| result: List[ToolCall] = [] | ||
| for tc in tools_called: | ||
| name = tc.get("name", "") | ||
| if not name: | ||
| continue | ||
| result.append(ToolCall( | ||
| name=name, | ||
| input_parameters=tc.get("input_parameters"), | ||
| output=tc.get("output"), | ||
| )) | ||
| return result | ||
17 changes: 17 additions & 0 deletions
17
...ck_agentcore/evaluation/custom_code_based_evaluators/third_party/span_mappers/__init__.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| """Span mappers for extracting evaluation fields from Agent SDK trace formats. | ||
|
|
||
| Uses strands-evals mappers for span format auto-detection and extraction, | ||
| bridged to SpanMapResult for adapter consumption. | ||
| """ | ||
|
|
||
| from bedrock_agentcore.evaluation.custom_code_based_evaluators.third_party.span_mappers.common import ( | ||
| SpanMapResult, | ||
| ) | ||
| from bedrock_agentcore.evaluation.custom_code_based_evaluators.third_party.span_mappers.registry import ( | ||
| map_spans, | ||
| ) | ||
|
|
||
| __all__ = [ | ||
| "SpanMapResult", | ||
| "map_spans", | ||
| ] |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why don't we have tools_called and expected_tools?
Do we want to support ToolCorrectnessMetric, ArgumentCorrectnessMetric in DeepEval?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added both. tools_called is now extracted from execute_tool spans (name + input_parameters + output) and mapped to LLMTestCase.tools_called. expected_tools comes from reference_inputs[0].expectedTrajectory.toolNames → LLMTestCase.expected_tools. This enables ToolCorrectnessMetric and ArgumentCorrectnessMetric.