Skip to content

Contracts: Evaluator Interfaces

Feature: 012-deepeval-metrics Date: 2025-01-30

Overview

This document defines the Python interfaces (protocols/abstract classes) for DeepEval evaluators. These contracts ensure consistency across all evaluator implementations and enable proper integration with the existing HoloDeck evaluation framework.


1. DeepEvalBaseEvaluator Interface

Base class for all DeepEval-based evaluators. Extends BaseEvaluator.

from abc import abstractmethod
from typing import Any

from holodeck.lib.evaluators.base import BaseEvaluator, RetryConfig
from holodeck.lib.evaluators.deepeval.config import DeepEvalModelConfig


class DeepEvalBaseEvaluator(BaseEvaluator):
    """Base class for DeepEval metric evaluators.

    All DeepEval evaluators inherit from this class to get:
    - Model configuration and initialization
    - Retry logic with exponential backoff
    - Timeout handling
    - Standard logging

    Subclasses must implement:
    - _create_metric(): Returns the DeepEval metric instance
    - _extract_result(): Extracts and normalizes the metric result
    """

    def __init__(
        self,
        model_config: DeepEvalModelConfig | None = None,
        threshold: float = 0.5,
        timeout: float | None = 60.0,
        retry_config: RetryConfig | None = None,
    ) -> None:
        """Initialize DeepEval evaluator.

        Args:
            model_config: LLM judge configuration. Defaults to Ollama gpt-oss:20b.
            threshold: Pass/fail score threshold (0.0-1.0). Default: 0.5.
            timeout: Evaluation timeout in seconds. Default: 60.0.
            retry_config: Retry configuration for transient failures.
        """
        ...

    @abstractmethod
    def _create_metric(self) -> Any:
        """Create and return the DeepEval metric instance.

        Returns:
            DeepEval metric object (e.g., GEval, AnswerRelevancyMetric)
        """
        ...

    @abstractmethod
    def _extract_result(self, metric: Any) -> dict[str, Any]:
        """Extract evaluation result from DeepEval metric.

        Args:
            metric: DeepEval metric after measure() has been called

        Returns:
            Dictionary with at minimum:
                - score: float (0.0-1.0)
                - passed: bool
                - reasoning: str
        """
        ...

    def _build_test_case(self, **kwargs: Any) -> Any:
        """Build DeepEval LLMTestCase from evaluation inputs.

        Args:
            **kwargs: Evaluation parameters:
                - input/query: str (user query)
                - actual_output/response: str (agent response)
                - expected_output/ground_truth: str | None
                - context: str | None
                - retrieval_context: list[str] | None

        Returns:
            LLMTestCase instance

        Raises:
            ValueError: If required parameters are missing
        """
        ...

    async def _evaluate_impl(self, **kwargs: Any) -> dict[str, Any]:
        """Implement evaluation using DeepEval metric.

        This method is called by BaseEvaluator.evaluate() with retry/timeout.

        Args:
            **kwargs: Evaluation parameters (see _build_test_case)

        Returns:
            Evaluation result dictionary

        Raises:
            DeepEvalError: If DeepEval metric evaluation fails
            ValueError: If required parameters are missing
        """
        ...

2. GEvalEvaluator Interface

Custom criteria evaluator using G-Eval algorithm.

class GEvalEvaluator(DeepEvalBaseEvaluator):
    """G-Eval custom criteria evaluator.

    Evaluates LLM outputs against user-defined criteria using
    chain-of-thought prompting and token probability scoring.

    Example:
        >>> evaluator = GEvalEvaluator(
        ...     name="Professionalism",
        ...     criteria="Evaluate if the response uses professional language",
        ...     evaluation_params=["actual_output"],
        ...     threshold=0.7
        ... )
        >>> result = await evaluator.evaluate(
        ...     input="Help me write an email",
        ...     actual_output="Yo, here's ur email bro..."
        ... )
        >>> print(result["score"])  # Low score due to informal language
    """

    def __init__(
        self,
        name: str,
        criteria: str,
        evaluation_params: list[str] | None = None,
        evaluation_steps: list[str] | None = None,
        model_config: DeepEvalModelConfig | None = None,
        threshold: float = 0.5,
        strict_mode: bool = False,
        timeout: float | None = 60.0,
        retry_config: RetryConfig | None = None,
    ) -> None:
        """Initialize G-Eval evaluator.

        Args:
            name: Metric identifier (e.g., "Correctness", "Helpfulness")
            criteria: Natural language evaluation criteria
            evaluation_params: Test case fields to include.
                Valid: ["input", "actual_output", "expected_output",
                        "context", "retrieval_context"]
                Default: ["actual_output"]
            evaluation_steps: Explicit evaluation steps (auto-generated if None)
            model_config: LLM judge configuration
            threshold: Pass/fail score threshold
            strict_mode: If True, score is 1.0 or 0.0 only
            timeout: Evaluation timeout in seconds
            retry_config: Retry configuration
        """
        ...

3. RAG Metric Evaluator Interfaces

3.1 AnswerRelevancyEvaluator

class AnswerRelevancyEvaluator(DeepEvalBaseEvaluator):
    """Evaluates relevance of response to user query.

    Required inputs:
        - input: User query
        - actual_output: Agent response

    Example:
        >>> evaluator = AnswerRelevancyEvaluator(threshold=0.7)
        >>> result = await evaluator.evaluate(
        ...     input="What is the return policy?",
        ...     actual_output="We offer 30-day returns with full refund."
        ... )
        >>> print(result["score"])  # High relevance score
    """

    def __init__(
        self,
        model_config: DeepEvalModelConfig | None = None,
        threshold: float = 0.5,
        include_reason: bool = True,
        timeout: float | None = 60.0,
        retry_config: RetryConfig | None = None,
    ) -> None:
        ...

3.2 FaithfulnessEvaluator

class FaithfulnessEvaluator(DeepEvalBaseEvaluator):
    """Detects hallucinations by comparing response to retrieval context.

    Required inputs:
        - input: User query
        - actual_output: Agent response
        - retrieval_context: List of retrieved text chunks

    Example:
        >>> evaluator = FaithfulnessEvaluator(threshold=0.8)
        >>> result = await evaluator.evaluate(
        ...     input="What are the store hours?",
        ...     actual_output="Store is open 24/7.",
        ...     retrieval_context=["Store hours: Mon-Fri 9am-5pm"]
        ... )
        >>> print(result["score"])  # Low score (hallucination detected)
    """

    def __init__(
        self,
        model_config: DeepEvalModelConfig | None = None,
        threshold: float = 0.5,
        include_reason: bool = True,
        timeout: float | None = 60.0,
        retry_config: RetryConfig | None = None,
    ) -> None:
        ...

3.3 ContextualRelevancyEvaluator

class ContextualRelevancyEvaluator(DeepEvalBaseEvaluator):
    """Measures relevance of retrieved context to user query.

    Required inputs:
        - input: User query
        - actual_output: Agent response
        - retrieval_context: List of retrieved text chunks

    Returns proportion of chunks that are relevant to the query.

    Example:
        >>> evaluator = ContextualRelevancyEvaluator(threshold=0.6)
        >>> result = await evaluator.evaluate(
        ...     input="What is the pricing?",
        ...     actual_output="Basic plan is $10/month.",
        ...     retrieval_context=[
        ...         "Pricing: Basic $10, Pro $25",  # Relevant
        ...         "Company founded in 2020",       # Irrelevant
        ...     ]
        ... )
        >>> print(result["score"])  # 0.5 (1 of 2 chunks relevant)
    """

    def __init__(
        self,
        model_config: DeepEvalModelConfig | None = None,
        threshold: float = 0.5,
        include_reason: bool = True,
        timeout: float | None = 60.0,
        retry_config: RetryConfig | None = None,
    ) -> None:
        ...

3.4 ContextualPrecisionEvaluator

class ContextualPrecisionEvaluator(DeepEvalBaseEvaluator):
    """Evaluates ranking quality of retrieved chunks.

    Measures whether relevant chunks appear before irrelevant ones.

    Required inputs:
        - input: User query
        - actual_output: Agent response
        - expected_output: Ground truth answer
        - retrieval_context: List of retrieved text chunks (order matters)

    Example:
        >>> evaluator = ContextualPrecisionEvaluator(threshold=0.7)
        >>> result = await evaluator.evaluate(
        ...     input="What is X?",
        ...     actual_output="X is...",
        ...     expected_output="X is the correct definition.",
        ...     retrieval_context=[
        ...         "Irrelevant info",      # Bad: irrelevant first
        ...         "X is the definition",  # Good: relevant
        ...     ]
        ... )
        >>> print(result["score"])  # Lower due to poor ranking
    """

    def __init__(
        self,
        model_config: DeepEvalModelConfig | None = None,
        threshold: float = 0.5,
        include_reason: bool = True,
        timeout: float | None = 60.0,
        retry_config: RetryConfig | None = None,
    ) -> None:
        ...

3.5 ContextualRecallEvaluator

class ContextualRecallEvaluator(DeepEvalBaseEvaluator):
    """Measures retrieval completeness against expected output.

    Evaluates whether retrieval context contains all facts needed
    to produce the expected output.

    Required inputs:
        - input: User query
        - actual_output: Agent response
        - expected_output: Ground truth answer
        - retrieval_context: List of retrieved text chunks

    Example:
        >>> evaluator = ContextualRecallEvaluator(threshold=0.8)
        >>> result = await evaluator.evaluate(
        ...     input="List all features",
        ...     actual_output="Features are A and B",
        ...     expected_output="Features are A, B, and C",
        ...     retrieval_context=["Feature A: ...", "Feature B: ..."]
        ... )
        >>> print(result["score"])  # ~0.67 (missing Feature C)
    """

    def __init__(
        self,
        model_config: DeepEvalModelConfig | None = None,
        threshold: float = 0.5,
        include_reason: bool = True,
        timeout: float | None = 60.0,
        retry_config: RetryConfig | None = None,
    ) -> None:
        ...

4. Model Configuration Interface

from enum import Enum
from typing import Any

from pydantic import BaseModel, Field, model_validator


class ProviderEnum(str, Enum):
    """Supported LLM providers for DeepEval."""
    OPENAI = "openai"
    AZURE_OPENAI = "azure_openai"
    ANTHROPIC = "anthropic"
    OLLAMA = "ollama"


class DeepEvalModelConfig(BaseModel):
    """Configuration for DeepEval LLM judge model.

    Supports multiple providers with appropriate validation for each.

    Example (OpenAI):
        >>> config = DeepEvalModelConfig(
        ...     provider=ProviderEnum.OPENAI,
        ...     model_name="gpt-4o",
        ...     api_key="sk-..."
        ... )

    Example (Ollama - default):
        >>> config = DeepEvalModelConfig(
        ...     provider=ProviderEnum.OLLAMA,
        ...     model_name="gpt-oss:20b"
        ... )
    """

    provider: ProviderEnum = Field(
        default=ProviderEnum.OLLAMA,
        description="LLM provider"
    )
    model_name: str = Field(
        default="gpt-oss:20b",
        description="Model name or identifier"
    )
    api_key: str | None = Field(
        default=None,
        description="API key (required for cloud providers)"
    )
    endpoint: str | None = Field(
        default=None,
        description="API endpoint (required for Azure, optional for Ollama)"
    )
    api_version: str | None = Field(
        default="2024-02-15-preview",
        description="API version (Azure OpenAI only)"
    )
    deployment_name: str | None = Field(
        default=None,
        description="Deployment name (Azure OpenAI only)"
    )
    temperature: float = Field(
        default=0.0,
        ge=0.0,
        le=2.0,
        description="Model temperature (0.0 for deterministic)"
    )

    @model_validator(mode="after")
    def validate_provider_requirements(self) -> "DeepEvalModelConfig":
        """Validate required fields based on provider."""
        ...

    def to_deepeval_model(self) -> Any:
        """Convert to DeepEval native model instance.

        Uses DeepEval's built-in model classes - no custom implementation required:
        - GPTModel for OpenAI
        - AzureOpenAIModel for Azure OpenAI
        - AnthropicModel for Anthropic
        - OllamaModel for Ollama

        Returns:
            Native DeepEval model instance (GPTModel, AzureOpenAIModel,
            AnthropicModel, or OllamaModel)

        Example:
            >>> config = DeepEvalModelConfig(provider=ProviderEnum.OLLAMA, model_name="gpt-oss:20b")
            >>> model = config.to_deepeval_model()
            >>> # Returns: OllamaModel(model="gpt-oss:20b", base_url="http://localhost:11434", temperature=0)
        """
        from deepeval.models import GPTModel, AzureOpenAIModel, AnthropicModel, OllamaModel

        if self.provider == ProviderEnum.OPENAI:
            return GPTModel(model=self.model_name, temperature=self.temperature)
        elif self.provider == ProviderEnum.AZURE_OPENAI:
            return AzureOpenAIModel(
                model_name=self.model_name,
                deployment_name=self.deployment_name,
                azure_endpoint=self.endpoint,
                azure_openai_api_key=self.api_key,
                openai_api_version=self.api_version,
                temperature=self.temperature
            )
        elif self.provider == ProviderEnum.ANTHROPIC:
            return AnthropicModel(model=self.model_name, temperature=self.temperature)
        elif self.provider == ProviderEnum.OLLAMA:
            return OllamaModel(
                model=self.model_name,
                base_url=self.endpoint or "http://localhost:11434",
                temperature=self.temperature
            )
        ...

5. Error Types

from holodeck.lib.evaluators.base import EvaluationError


class ProviderNotSupportedError(EvaluationError):
    """Raised when evaluator is used with incompatible provider.

    Attributes:
        evaluator_type: Name of the evaluator class
        configured_provider: Provider that was configured
        supported_providers: List of supported providers
    """

    def __init__(
        self,
        message: str,
        evaluator_type: str,
        configured_provider: str,
        supported_providers: list[str],
    ) -> None:
        ...


class DeepEvalError(EvaluationError):
    """Wraps errors from DeepEval library.

    Attributes:
        metric_name: Name of the DeepEval metric that failed
        original_error: Original exception from DeepEval
        test_case_summary: Truncated input/output for debugging
    """

    def __init__(
        self,
        message: str,
        metric_name: str,
        original_error: Exception | None = None,
        test_case_summary: dict[str, str] | None = None,
    ) -> None:
        ...

6. Return Type Contract

All evaluators return a consistent structure:

from typing import TypedDict, Any


class EvaluationResult(TypedDict):
    """Standard return type for all evaluators."""

    score: float           # 0.0 - 1.0 normalized score
    passed: bool           # score >= threshold
    reasoning: str         # LLM-generated explanation
    metric_name: str       # e.g., "GEval", "AnswerRelevancy"
    threshold: float       # Configured threshold
    raw_score: float | None  # Original score (if different from normalized)
    evaluation_steps: list[str] | None  # Steps used (G-Eval only)
    metadata: dict[str, Any]  # Metric-specific additional data

7. Integration Points

7.1 Test Runner Integration

DeepEval evaluators integrate with existing test runner via BaseEvaluator.evaluate():

# In test_runner/executor.py
async def run_evaluation(
    evaluator: BaseEvaluator,
    test_case: TestCaseConfig,
) -> EvaluationResult:
    """Run evaluation using any BaseEvaluator subclass."""
    return await evaluator.evaluate(
        input=test_case.input,
        actual_output=agent_response,
        expected_output=test_case.ground_truth,
        retrieval_context=test_case.retrieval_context,
    )

7.2 YAML Configuration

Evaluators are configured via agent YAML:

evaluations:
  model:
    provider: ollama
    name: gpt-oss:20b
  metrics:
    - type: geval
      name: Helpfulness
      criteria: "Evaluate if the response is helpful and actionable"
      threshold: 0.7
    - type: answer_relevancy
      threshold: 0.6
    - type: faithfulness
      threshold: 0.8