Skip to content

Commit 2119c68

Browse files
committed
feat: migrate context recall, answer relevancy, and context entity recall metrics to modular BasePrompt architecture (vibrantlabsai#2435)
1 parent 52fe43a commit 2119c68

10 files changed

Lines changed: 416 additions & 99 deletions

File tree

src/ragas/metrics/collections/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@
22

33
from ragas.metrics.collections._answer_accuracy import AnswerAccuracy
44
from ragas.metrics.collections._answer_correctness import AnswerCorrectness
5-
from ragas.metrics.collections._answer_relevancy import AnswerRelevancy
65
from ragas.metrics.collections._bleu_score import BleuScore
7-
from ragas.metrics.collections._context_entity_recall import ContextEntityRecall
8-
from ragas.metrics.collections._context_recall import ContextRecall
96
from ragas.metrics.collections._context_relevance import ContextRelevance
107
from ragas.metrics.collections._factual_correctness import FactualCorrectness
118
from ragas.metrics.collections._faithfulness import Faithfulness
@@ -20,13 +17,16 @@
2017
StringPresence,
2118
)
2219
from ragas.metrics.collections._summary_score import SummaryScore
20+
from ragas.metrics.collections.answer_relevancy import AnswerRelevancy
2321
from ragas.metrics.collections.base import BaseMetric
22+
from ragas.metrics.collections.context_entity_recall import ContextEntityRecall
2423
from ragas.metrics.collections.context_precision import (
2524
ContextPrecision,
2625
ContextPrecisionWithoutReference,
2726
ContextPrecisionWithReference,
2827
ContextUtilization,
2928
)
29+
from ragas.metrics.collections.context_recall import ContextRecall
3030

3131
__all__ = [
3232
"BaseMetric", # Base class
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""Answer Relevancy metrics v2 - Modern implementation."""
2+
3+
from .metric import AnswerRelevancy
4+
5+
__all__ = [
6+
"AnswerRelevancy",
7+
]
Lines changed: 51 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,45 @@
1-
"""Answer Relevancy metric v2 - Class-based implementation with modern components."""
1+
"""Answer Relevancy metrics v2 - Modern implementation with structured prompts."""
22

33
import typing as t
44

55
import numpy as np
6-
from pydantic import BaseModel
76

87
from ragas.metrics.collections.base import BaseMetric
98
from 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

1216
if 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-
2421
class 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))
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""Answer Relevancy prompt classes and models."""
2+
3+
from pydantic import BaseModel, Field
4+
5+
from ragas.prompt.metrics.base_prompt import BasePrompt
6+
7+
8+
class AnswerRelevanceInput(BaseModel):
9+
"""Input model for answer relevance evaluation."""
10+
11+
response: str = Field(
12+
..., description="The response/answer to generate questions from"
13+
)
14+
15+
16+
class AnswerRelevanceOutput(BaseModel):
17+
"""Structured output for answer relevance question generation."""
18+
19+
question: str = Field(
20+
..., description="Question that can be answered from the response"
21+
)
22+
noncommittal: int = Field(
23+
...,
24+
description="1 if the response is evasive/vague, 0 if it is substantive",
25+
)
26+
27+
28+
class AnswerRelevancePrompt(BasePrompt[AnswerRelevanceInput, AnswerRelevanceOutput]):
29+
"""Answer relevance evaluation prompt with structured input/output."""
30+
31+
input_model = AnswerRelevanceInput
32+
output_model = AnswerRelevanceOutput
33+
34+
instruction = """Generate a question for the given answer and identify if the answer is noncommittal.
35+
Give noncommittal as 1 if the answer is noncommittal (evasive, vague, or ambiguous) and 0 if the answer is substantive.
36+
Examples of noncommittal answers: "I don't know", "I'm not sure", "It depends"."""
37+
38+
examples = [
39+
(
40+
AnswerRelevanceInput(response="Albert Einstein was born in Germany."),
41+
AnswerRelevanceOutput(
42+
question="Where was Albert Einstein born?",
43+
noncommittal=0,
44+
),
45+
),
46+
(
47+
AnswerRelevanceInput(
48+
response="The capital of France is Paris, a city known for its architecture and culture."
49+
),
50+
AnswerRelevanceOutput(
51+
question="What is the capital of France?",
52+
noncommittal=0,
53+
),
54+
),
55+
(
56+
AnswerRelevanceInput(
57+
response="I don't know about the groundbreaking feature of the smartphone invented in 2023 as I am unaware of information beyond 2022."
58+
),
59+
AnswerRelevanceOutput(
60+
question="What was the groundbreaking feature of the smartphone invented in 2023?",
61+
noncommittal=1,
62+
),
63+
),
64+
]
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""Context Entity Recall metrics v2 - Modern implementation."""
2+
3+
from .metric import ContextEntityRecall
4+
5+
__all__ = [
6+
"ContextEntityRecall",
7+
]

0 commit comments

Comments
 (0)