1- """Answer Relevancy metric v2 - Class-based implementation with modern components ."""
1+ """Answer Relevancy metrics v2 - Modern implementation with structured prompts ."""
22
33import typing as t
44
55import numpy as np
6- from pydantic import BaseModel
76
87from ragas .metrics .collections .base import BaseMetric
98from ragas .metrics .result import MetricResult
10- from ragas .prompt .metrics .answer_relevance import answer_relevancy_prompt
9+
10+ from .util import (
11+ AnswerRelevanceInput ,
12+ AnswerRelevanceOutput ,
13+ AnswerRelevancePrompt ,
14+ )
1115
1216if t .TYPE_CHECKING :
1317 from ragas .embeddings .base import BaseRagasEmbedding
1418 from ragas .llms .base import InstructorBaseRagasLLM
1519
1620
17- class AnswerRelevanceOutput (BaseModel ):
18- """Structured output for answer relevance question generation."""
19-
20- question : str
21- noncommittal : int
22-
23-
2421class AnswerRelevancy (BaseMetric ):
2522 """
26- Evaluate answer relevancy by generating questions from the response and comparing to original question.
23+ Modern v2 implementation of answer relevancy evaluation.
24+
25+ Evaluates answer relevancy by generating multiple questions from the response
26+ and comparing them to the original question using cosine similarity.
27+ The metric detects evasive/noncommittal answers.
2728
28- This implementation uses modern instructor LLMs with structured output and modern embeddings.
29+ This implementation uses modern instructor LLMs with structured output
30+ and modern embeddings for semantic comparison.
2931 Only supports modern components - legacy wrappers are rejected with clear error messages.
3032
3133 Usage:
32- >>> from openai import AsyncOpenAI
33- >>> from ragas.llms import llm_factory
34+ >>> import openai
35+ >>> from ragas.llms.base import llm_factory
3436 >>> from ragas.embeddings.base import embedding_factory
3537 >>> from ragas.metrics.collections import AnswerRelevancy
3638 >>>
3739 >>> # Setup dependencies
38- >>> client = AsyncOpenAI()
40+ >>> client = openai. AsyncOpenAI()
3941 >>> llm = llm_factory("gpt-4o-mini", client=client)
40- >>> embeddings = embedding_factory("openai", model="text-embedding-ada-002", client=client, interface="modern" )
42+ >>> embeddings = embedding_factory("openai", model="text-embedding-ada-002", client=client)
4143 >>>
4244 >>> # Create metric instance
4345 >>> metric = AnswerRelevancy(llm=llm, embeddings=embeddings, strictness=3)
@@ -47,20 +49,14 @@ class AnswerRelevancy(BaseMetric):
4749 ... user_input="What is the capital of France?",
4850 ... response="Paris is the capital of France."
4951 ... )
50- >>> print(f"Score: {result.value}")
51- >>>
52- >>> # Batch evaluation
53- >>> results = await metric.abatch_score([
54- ... {"user_input": "Q1", "response": "A1"},
55- ... {"user_input": "Q2", "response": "A2"},
56- ... ])
52+ >>> print(f"Answer Relevancy: {result.value}")
5753
5854 Attributes:
5955 llm: Modern instructor-based LLM for question generation
60- embeddings: Modern embeddings model with embed_text() and embed_texts() methods
56+ embeddings: Modern embeddings model for semantic comparison
6157 name: The metric name
62- strictness: Number of questions to generate per answer (3-5 recommended )
63- allowed_values: Score range (0.0 to 1.0)
58+ strictness: Number of questions to generate (default: 3 )
59+ allowed_values: Score range (0.0 to 1.0, higher is better )
6460 """
6561
6662 # Type hints for linter (attributes are set in __init__)
@@ -75,13 +71,23 @@ def __init__(
7571 strictness : int = 3 ,
7672 ** kwargs ,
7773 ):
78- """Initialize AnswerRelevancy metric with required components."""
74+ """
75+ Initialize AnswerRelevancy metric with required components.
76+
77+ Args:
78+ llm: Modern instructor-based LLM for question generation
79+ embeddings: Modern embeddings model for semantic comparison
80+ name: The metric name (default: "answer_relevancy")
81+ strictness: Number of questions to generate (default: 3)
82+ **kwargs: Additional arguments passed to BaseMetric
83+ """
7984 # Set attributes explicitly before calling super()
8085 self .llm = llm
8186 self .embeddings = embeddings
8287 self .strictness = strictness
88+ self .prompt = AnswerRelevancePrompt () # Initialize prompt class once
8389
84- # Call super() for validation (without passing llm/embeddings in kwargs)
90+ # Call super() for validation
8591 super ().__init__ (name = name , ** kwargs )
8692
8793 async def ascore (self , user_input : str , response : str ) -> MetricResult :
@@ -95,15 +101,23 @@ async def ascore(self, user_input: str, response: str) -> MetricResult:
95101 response: The response to evaluate
96102
97103 Returns:
98- MetricResult with relevancy score (0.0-1.0)
104+ MetricResult with relevancy score (0.0-1.0, higher is better )
99105 """
100- prompt = answer_relevancy_prompt (response )
106+ # Input validation
107+ if not user_input :
108+ raise ValueError ("user_input cannot be empty" )
109+ if not response :
110+ raise ValueError ("response cannot be empty" )
101111
112+ # Generate multiple questions from response
102113 generated_questions = []
103114 noncommittal_flags = []
104115
105116 for _ in range (self .strictness ):
106- result = await self .llm .agenerate (prompt , AnswerRelevanceOutput )
117+ # Create input data and generate prompt
118+ input_data = AnswerRelevanceInput (response = response )
119+ prompt_string = self .prompt .to_string (input_data )
120+ result = await self .llm .agenerate (prompt_string , AnswerRelevanceOutput )
107121
108122 if result .question :
109123 generated_questions .append (result .question )
@@ -112,13 +126,18 @@ async def ascore(self, user_input: str, response: str) -> MetricResult:
112126 if not generated_questions :
113127 return MetricResult (value = 0.0 )
114128
129+ # Check if all responses are noncommittal
115130 all_noncommittal = np .all (noncommittal_flags )
116131
132+ # Embed the original question
117133 question_vec = np .asarray (self .embeddings .embed_text (user_input )).reshape (1 , - 1 )
134+
135+ # Embed the generated questions
118136 gen_question_vec = np .asarray (
119137 self .embeddings .embed_texts (generated_questions )
120138 ).reshape (len (generated_questions ), - 1 )
121139
140+ # Calculate cosine similarity
122141 norm = np .linalg .norm (gen_question_vec , axis = 1 ) * np .linalg .norm (
123142 question_vec , axis = 1
124143 )
@@ -129,6 +148,7 @@ async def ascore(self, user_input: str, response: str) -> MetricResult:
129148 / norm
130149 )
131150
151+ # Score is average cosine similarity, reduced to 0 if response is noncommittal
132152 score = cosine_sim .mean () * int (not all_noncommittal )
133153
134154 return MetricResult (value = float (score ))
0 commit comments