Coverage for /home/admin/Documents/AI/applications/lexigram-dev/lexigram/experimental/ai/lexigram-ai-rag/src/lexigram/ai/rag/evaluation/answer.py: 85%
55 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 07:19 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 07:19 +0800
1"""Answer quality evaluation metrics."""
3from __future__ import annotations
5from typing import TYPE_CHECKING, Any
7from lexigram.ai.rag.evaluation.base import EvaluatorBase
8from lexigram.ai.rag.evaluation.types import EvaluationResult, MetricType
9from lexigram.contracts import ChatMessage
10from lexigram.di.decorators import inject
11from lexigram.logging import (
12 get_logger,
13)
15if TYPE_CHECKING:
16 from lexigram.contracts.ai import LLMClientProtocol
18logger = get_logger(__name__)
21@inject
22class AnswerRelevanceEvaluator(EvaluatorBase):
23 """Evaluates answer relevance using LLM-as-judge.
25 Measures how well the answer addresses the query.
26 """
28 def __init__(self, llm_client: LLMClientProtocol):
29 """Initialize answer relevance evaluator.
31 Args:
32 llm_client: LLM client for evaluation.
33 """
34 super().__init__("answer_relevance")
35 self.llm_client = llm_client
37 async def evaluate(
38 self,
39 query: str,
40 retrieved_docs: list[Any],
41 generated_answer: str,
42 reference_answer: str | None = None,
43 **kwargs,
44 ) -> EvaluationResult:
45 """Evaluate answer relevance.
47 Args:
48 query: The query.
49 retrieved_docs: Retrieved documents (not used).
50 generated_answer: Generated answer to evaluate.
51 reference_answer: Not used.
52 **kwargs: Additional parameters.
54 Returns:
55 Relevance score.
56 """
57 prompt = f"""Evaluate how relevant the following answer is to the query.
58Score from 0.0 (completely irrelevant) to 1.0 (perfectly relevant).
60Query: {query}
62Answer: {generated_answer}
64Provide only a number between 0.0 and 1.0 as your response."""
66 try:
67 result = await self.llm_client.complete(
68 messages=[ChatMessage(role="user", content=prompt)]
69 )
70 if result.is_err():
71 raise result.unwrap_err()
72 response = result.unwrap()
73 # Extract score from response
74 score_text = response.content.strip()
75 score = float(score_text)
76 score = max(0.0, min(1.0, score)) # Clamp to [0, 1]
78 return EvaluationResult(
79 metric_type=MetricType.ANSWER_RELEVANCE,
80 score=score,
81 details={"llm_response": score_text},
82 )
83 except Exception as e:
84 logger.exception("Error computing evaluation metric")
85 return EvaluationResult(
86 metric_type=MetricType.ANSWER_RELEVANCE,
87 score=0.0,
88 details={"error": str(e)},
89 )
92@inject
93class AnswerFaithfulnessEvaluator(EvaluatorBase):
94 """Evaluates answer faithfulness using LLM-as-judge.
96 Measures whether the answer is grounded in the retrieved context
97 (i.e., not hallucinated).
98 """
100 def __init__(self, llm_client: LLMClientProtocol):
101 """Initialize answer faithfulness evaluator.
103 Args:
104 llm_client: LLM client for evaluation.
105 """
106 super().__init__("answer_faithfulness")
107 self.llm_client = llm_client
109 async def evaluate(
110 self,
111 query: str,
112 retrieved_docs: list[Any],
113 generated_answer: str,
114 reference_answer: str | None = None,
115 **kwargs,
116 ) -> EvaluationResult:
117 """Evaluate answer faithfulness.
119 Args:
120 query: The query.
121 retrieved_docs: Retrieved documents to check against.
122 generated_answer: Generated answer to evaluate.
123 reference_answer: Not used.
124 **kwargs: Additional parameters.
126 Returns:
127 Faithfulness score.
128 """
129 # Build context from retrieved docs
130 context_parts = []
131 for i, doc in enumerate(retrieved_docs):
132 if isinstance(doc, dict):
133 content = doc.get("content", str(doc))
134 elif hasattr(doc, "content"):
135 content = doc.content
136 else:
137 content = str(doc)
138 context_parts.append(f"[{i + 1}] {content}")
140 context_str = "\n".join(context_parts)
142 prompt = f"""Evaluate whether the answer is faithful to the provided context.
143Score from 0.0 (completely unfaithful/hallucinated) to 1.0 (perfectly faithful).
145Context:
146{context_str}
148Answer: {generated_answer}
150The answer should only contain information that can be verified from the context.
151Provide only a number between 0.0 and 1.0 as your response."""
153 try:
154 result = await self.llm_client.complete(
155 messages=[ChatMessage(role="user", content=prompt)]
156 )
157 if result.is_err():
158 raise result.unwrap_err()
159 response = result.unwrap()
160 score_text = response.content.strip()
161 score = float(score_text)
162 score = max(0.0, min(1.0, score))
164 return EvaluationResult(
165 metric_type=MetricType.ANSWER_FAITHFULNESS,
166 score=score,
167 details={
168 "llm_response": score_text,
169 "context_chunks": len(retrieved_docs),
170 },
171 )
172 except Exception as e:
173 logger.exception("Error computing evaluation metric")
174 return EvaluationResult(
175 metric_type=MetricType.ANSWER_FAITHFULNESS,
176 score=0.0,
177 details={"error": str(e)},
178 )