Skip to content

feat: Add generic BaseAdapter framework for third-party evaluator integration (DeepEval + Autoevals)#528

Open
stone-coding wants to merge 13 commits into
aws:mainfrom
stone-coding:deepeval-handler
Open

feat: Add generic BaseAdapter framework for third-party evaluator integration (DeepEval + Autoevals)#528
stone-coding wants to merge 13 commits into
aws:mainfrom
stone-coding:deepeval-handler

Conversation

@stone-coding

@stone-coding stone-coding commented Jun 16, 2026

Copy link
Copy Markdown

Major changes in this PR:

  1. Adds a generic third-party evaluator adapter framework
  • Introduces BaseAdapter under evaluation/custom_code_based_evaluators/third_party/.
  • Standardizes the flow: extract fields from AgentCore spans → validate fields → execute third-party metric/scorer → return EvaluatorOutput.
  • Supports field_mapper as an escape hatch for custom or unsupported span formats.
  1. Adds DeepEval support
  • Adds DeepEvalAdapter for running DeepEval BaseMetric implementations such as AnswerRelevancy, Faithfulness, Bias, Toxicity, and GEval.
  • Converts extracted fields into DeepEval LLMTestCase.
  • Handles MissingTestCaseParamsError and returns actionable MISSING_REQUIRED_FIELD errors.
  1. Adds Autoevals support
  • Adds AutoevalsAdapter for Autoevals scorers such as Factuality and ClosedQA.
  • Maps AgentCore fields into Autoevals eval(input, output, expected) format.
  • Supports configurable pass/fail threshold.
  1. Adds span parsing support
  • Adds parser layer for extracting input, actual_output, retrieval_context, context, and expected_output from AgentCore ADOT spans and evaluationReferenceInputs.
  • Supports Strands, LangChain OTel, and OpenInference parser entry points, with shared Phase 1 extraction logic.
  • Returns clear FIELD_EXTRACTION_ERROR when supported fields cannot be extracted.
  1. Updates evaluator input model
  • Adds reference_inputs to EvaluatorInput so expected_output can flow from evaluation reference inputs into third-party evaluators.
  1. Adds tests
  • Adds unit tests covering DeepEval, Autoevals, span parsing, field mapping, missing required fields, and error handling.
  • All 42 tests pass.

@stone-coding stone-coding requested a review from a team June 16, 2026 22:46
@stone-coding stone-coding changed the title feat: Add DeepEvalHandler for third-party evaluator integration feat: Add generic BaseAdapter framework for third-party evaluator integration (DeepEval + Autoevals) Jun 25, 2026
@@ -0,0 +1,5 @@
"""DeepEval integration for AgentCore Evaluation."""

from bedrock_agentcore.evaluation.integrations.deepeval.handler import DeepEvalHandler

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.

Since this is using eval's custom code evaluator please but this under custom_code_based_evaluators.

import threading
from typing import Any, Callable, Dict, Optional

from deepeval.metrics import BaseMetric

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.

Please add an integ test for this. Look into tests_integ for examples.

import threading
from typing import Any, Callable, Dict, Optional

from deepeval.metrics import BaseMetric

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.

Also, let's add this in our pyproject as an optional dependency, so customer's know which deepeval version we support.



@dataclass
class ParsedEvaluationEvent:

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.

Please use EvaluatorInput from our code_based_evaluator. No need to duplicate lambda logic.

Error: {"errorCode": str, "errorMessage": str}
"""
try:
if isinstance(event, EvaluatorInput):

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.

ParsedEvaluationEvent and EvaluatorInput look like they're doing the same job — both just turn the raw lambda event into a structured input. call even copies one into the other field-for-field. Is there a reason we need a second type instead of reusing EvaluatorInput?

Proposal: make it a requirement that customers place these adapters within the @code_based_evaluators decorator. That way the adapter stops owning input/output validation and the decorator does it instead. Keeps the adapter focused on just running the eval.

}


def _get_required_params(metric: BaseMetric) -> List[str]:

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.

metric.measure() already calls check_llm_test_case_params with the metric's own _required_params and raises MissingTestCaseParamsError.

So we can drop the registry: build the LLMTestCase with whatever fields we have, call measure(), and catch that error.

By doing this, we let customers use GEval too — its required fields aren't fixed on the class, they're whatever the customer passes to evaluation_params at construction, so a static registry can never cover it. Letting the metric validate itself handles that case for free.

) -> Dict[str, Any]:
"""Extract evaluation fields from AgentCore session spans.

Parses _eval_log_records from span attributes, filters by target_trace_id,

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 you tell me what otel agent semantic you are following here? Because I haven't seen any agent SDK emit this _eval_log_records?

self.validate_fields(fields)
return fields

def validate_fields(self, fields: Dict[str, Any]) -> None:

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 you add @AbstractMethod here please? The no-op default means a subclass that forgets to override it silently skips validation, and bad fields fail deeper in execute instead. Both adapters override it anyway, so abstract just makes each one declare its required fields on purpose.


thread = threading.Thread(target=target, daemon=True)
thread.start()
thread.join(timeout=self.timeout)

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.

When the thread "times out" here, it doesn't actually end join just returns back to the caller while the worker keeps running. So if Lambda reuses the same container, we can have a background thread from a previous invocation still executing during the next one. I've heard this is a real failure case, so let's drop the thread machinery and let the AWS Lambda timeout handle it for us instead.


def __init__(
self,
field_mapper: Optional[Callable[[Dict[str, Any]], Dict[str, Any]]] = None,

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 we make extract_fields_from_spans the default value of field_mapper in the constructor? Then we have one extraction path instead of the if-field_mapper-else branch.

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.

3 participants