Skip to content

feat(deepeval): support custom metric instances in evaluator - #3841

Open
ClaireXi99 wants to merge 2 commits into
deepset-ai:mainfrom
ClaireXi99:feat/deepeval-custom-metrics
Open

feat(deepeval): support custom metric instances in evaluator#3841
ClaireXi99 wants to merge 2 commits into
deepset-ai:mainfrom
ClaireXi99:feat/deepeval-custom-metrics

Conversation

@ClaireXi99

Copy link
Copy Markdown

Fixes #3838

What

DeepEvalEvaluator now accepts an already initialized DeepEval metric, so any subclass
of deepeval.metrics.BaseMetric can be used for evaluation:

class ResponseLengthMetric(BaseMetric):
    _required_params = [SingleTurnParams.INPUT, SingleTurnParams.ACTUAL_OUTPUT]

    def measure(self, test_case: LLMTestCase) -> float:
        words = len((test_case.actual_output or "").split())
        self.score = min(1.0, words / self.max_words)
        self.reason = f"The response contains {words} words"
        return self.score


evaluator = DeepEvalEvaluator(metric=ResponseLengthMetric(max_words=20))

The inputs the component exposes are derived from the test case parameters the metric
declares, and results come back in the same name/score/explanation shape as the
built-ins.

Backwards compatibility

Purely additive. metric widens from str | DeepEvalMetric to
str | DeepEvalMetric | BaseMetric; the built-in branch, its init validation, its input
validation and its serde are untouched. Existing tests still pass unchanged.

Two design points worth your call

1. Parameter discovery reads _required_params. DeepEval exposes no public way to
ask a metric which test case params it needs, so this reads the private attribute. Two
details:

  • Any collection of SingleTurnParams is accepted, not just list — DeepEval does not
    promise a concrete container type, and silently discarding a tuple/set declaration
    would map the component's inputs to the RAG defaults without telling the user.
  • BaseMetric only annotates _required_params without assigning it, so on a metric
    that never declares it getattr returns the typing alias
    typing.List[SingleTurnParams] rather than None. That case — and anything else that
    cannot be read as params — falls back to the built-in RAG inputs.

If you would rather not depend on a private attribute, the alternative is an explicit
constructor argument for the expected inputs. Happy to switch.

2. A metric instance cannot be serialized. to_dict raises for that case, since an
instance carries runtime state that cannot be reliably reconstructed. The class docstring
points at the built-in metrics when a pipeline has to survive a to_dict/from_dict
round trip. Tell me if you would prefer a warning plus a degraded dict instead of a hard
error.

Also note this supports a single metric instance, matching the current DeepEval
evaluator shape. RagasEvaluator takes a list of metrics — say the word and I will align
this one to that API.

Tests

integrations/deepeval/tests/test_evaluator.py gains coverage for input derivation
(declared as list, declared as tuple, undeclared, unreadable, unsupported params), the
measure() call path, output shape, LLMTestCase conversion, and passing a built-in
metric as an instance. Locally: 32 passed, 5 skipped (the skips need OPENAI_API_KEY).
ruff check and ruff format --check are clean.

Two notes so they don't surprise you in review:

  • I did not touch CHANGELOG.md — it looks like HaystackBot generates it via
    cliff.toml during the release workflow. Let me know if contributors are expected to
    add an entry.
  • mypy src reports 8 errors in this integration, but they are identical before and
    after this change (only one line number shifts, because the docstring grew). They come
    from deepeval.metrics attribute resolution against the installed deepeval version, so
    I left them alone rather than widening the diff.

`DeepEvalEvaluator` now accepts an already initialized DeepEval metric, so any
subclass of `deepeval.metrics.BaseMetric` can be used for evaluation. The inputs
the component exposes are derived from the test case parameters the metric
declares, and built-in metrics selected by name or `DeepEvalMetric` keep working
exactly as before.

A metric instance cannot be serialized, so `to_dict` raises for that case and the
docstring points at the built-in metrics when a pipeline must survive a
`to_dict`/`from_dict` round trip.

Parameter discovery reads any collection of `SingleTurnParams`, not just `list`:
DeepEval does not promise a concrete container. When the declaration cannot be
read as params -- including the common case where a metric never declares it and
the annotation leaks through as `typing.List[SingleTurnParams]` -- the built-in
RAG inputs are assumed.
@ClaireXi99
ClaireXi99 requested a review from a team as a code owner August 24, 2026 09:50
@ClaireXi99
ClaireXi99 requested review from sjrl and removed request for a team August 24, 2026 09:50
@github-actions

Copy link
Copy Markdown
Contributor

Heads-up for maintainers

This PR is from a fork and touches integrations whose integration tests require API keys.
Those tests are skipped in CI because fork PRs don't have access to repo secrets for security reasons.

Affected integrations:

  • deepeval

Please run the integration tests locally (hatch run test:integration inside each folder) before approving.

@CLAassistant

CLAassistant commented Aug 24, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@github-actions github-actions Bot added the type:documentation Improvements or additions to documentation label Aug 24, 2026
@ClaireXi99
ClaireXi99 force-pushed the feat/deepeval-custom-metrics branch from 4387011 to 5d785cb Compare August 24, 2026 10:15
CI runs the unit tests against the lowest allowed direct dependencies, where
deepeval is 2.9.0. Two things broke there:

- `SingleTurnParams` does not exist before deepeval 4, where the enum was named
  `LLMTestCaseParams`. Import the new name and fall back to the old one, and have
  the tests take the enum from the module under test so both always agree.
- `MetricData` requires `threshold` and `success` before deepeval 4 and makes them
  optional after, so the local measurement double passes them explicitly.

Verified against deepeval 2.9.0 and 4.1.8: 33 passed, 5 skipped on both.
@ClaireXi99
ClaireXi99 force-pushed the feat/deepeval-custom-metrics branch from 5d785cb to fbb39f8 Compare August 24, 2026 10:26
@github-actions

Copy link
Copy Markdown
Contributor

Coverage report (deepeval)

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  integrations/deepeval/src/haystack_integrations/components/evaluators/deepeval
  evaluator.py
  metrics.py
Project Total  

This report was generated by python-coverage-comment-action

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integration:deepeval type:documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add ability to use custom metrics in the DeepEval Integration

2 participants