Skip to content

Commit 4dcb30d

Browse files
authored
feat: add agent response graders (#174)
* feat: add agent response graders * feat: add agent response graders
1 parent e0772cd commit 4dcb30d

5 files changed

Lines changed: 1440 additions & 0 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# -*- coding: utf-8 -*-
2+
"""Response graders for evaluating agent response quality."""
3+
4+
from openjudge.graders.agent.response.response_completeness import (
5+
ResponseCompletenessGrader,
6+
)
7+
from openjudge.graders.agent.response.response_helpfulness import (
8+
ResponseHelpfulnessGrader,
9+
)
10+
11+
__all__ = [
12+
"ResponseCompletenessGrader",
13+
"ResponseHelpfulnessGrader",
14+
]
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Response Completeness Grader
4+
5+
Evaluates whether the agent's final response completely addresses all aspects
6+
of the user's query, covering all sub-questions and constraints.
7+
"""
8+
9+
import textwrap
10+
from typing import Any, Dict, List, Optional, Union
11+
12+
from loguru import logger
13+
14+
from openjudge.evaluation_strategy import BaseEvaluationStrategy
15+
from openjudge.graders.base_grader import GraderMode, GraderScore
16+
from openjudge.graders.llm_grader import LLMGrader
17+
from openjudge.models.base_chat_model import BaseChatModel
18+
from openjudge.models.schema.oai.message import ChatMessage
19+
from openjudge.models.schema.prompt_template import LanguageEnum, PromptTemplate
20+
21+
# pylint: disable=line-too-long
22+
23+
# English Prompt
24+
RESPONSE_COMPLETENESS_PROMPT_EN = textwrap.dedent(
25+
"""You are an expert in evaluating AI agent responses. Your task is to evaluate whether the agent's response completely addresses all aspects of the user's query. A complete response covers all sub-questions, satisfies all constraints, and does not leave any part of the query unanswered.
26+
27+
<Rubrics>
28+
1. The response addresses the main intent of the user's query
29+
2. The response covers all sub-questions or sub-topics mentioned in the query
30+
3. The response satisfies all explicit constraints or conditions stated in the query
31+
4. The response does not omit any critical information requested by the user
32+
5. The response provides sufficient detail for each aspect of the query (not just brief mentions)
33+
</Rubrics>
34+
35+
<Steps>
36+
1. Parse the query: Identify all sub-questions, constraints, and information requests
37+
2. Map response coverage: Check which aspects are addressed and which are missing
38+
3. Assess depth: For each addressed aspect, evaluate if the detail level is sufficient
39+
4. Check constraints: Verify that all explicit constraints are satisfied
40+
5. Identify gaps: List any aspects of the query that are unanswered or insufficiently addressed
41+
</Steps>
42+
43+
<Scale>
44+
- **Score 5**: Complete — All aspects of the query are fully addressed with sufficient detail
45+
- **Score 4**: Mostly complete — All major aspects addressed, but minor gaps in detail or coverage
46+
- **Score 3**: Partially complete — Some aspects addressed, but notable gaps exist
47+
- **Score 2**: Incomplete — Only a few aspects addressed, major parts of the query are unanswered
48+
- **Score 1**: Severely incomplete — The response fails to address the query meaningfully
49+
</Scale>
50+
51+
<User Query>
52+
{query}
53+
</User Query>
54+
55+
<Agent Response>
56+
{response}
57+
</Agent Response>
58+
59+
<Output Schema>
60+
Provide your evaluation in the following structured JSON format:
61+
{{
62+
"reason": "<detailed explanation of response completeness, listing covered and missing aspects>",
63+
"score": <integer between 1 and 5>
64+
}}
65+
</Output Schema>
66+
67+
JSON:
68+
"""
69+
).strip()
70+
71+
# Chinese Prompt
72+
RESPONSE_COMPLETENESS_PROMPT_ZH = textwrap.dedent(
73+
"""你是一名评估AI智能体回复的专家。你的任务是评估智能体的回复是否完整地解决了用户查询的所有方面。完整的回复应该涵盖所有子问题、满足所有约束条件,不遗漏查询的任何部分。
74+
75+
<评分标准>
76+
1. 回复针对了用户查询的主要意图
77+
2. 回复涵盖了查询中提到的所有子问题或子主题
78+
3. 回复满足了查询中陈述的所有明确约束或条件
79+
4. 回复没有遗漏用户请求的任何关键信息
80+
5. 回复为查询的每个方面提供了充分的细节(不仅仅是简要提及)
81+
</评分标准>
82+
83+
<评估步骤>
84+
1. 解析查询:识别所有子问题、约束和信息请求
85+
2. 映射回复覆盖:检查哪些方面被处理,哪些被遗漏
86+
3. 评估深度:对于每个已处理的方面,评估细节水平是否足够
87+
4. 检查约束:验证是否满足了所有明确约束
88+
5. 识别缺口:列出查询中未回答或回答不充分的方面
89+
</评估步骤>
90+
91+
<评分量表>
92+
- **分数 5**:完整 — 查询的所有方面都得到了充分详细的处理
93+
- **分数 4**:基本完整 — 所有主要方面都已处理,但在细节或覆盖范围上有小缺口
94+
- **分数 3**:部分完整 — 处理了一些方面,但存在明显的缺口
95+
- **分数 2**:不完整 — 仅处理了少数方面,查询的大部分内容未回答
96+
- **分数 1**:严重不完整 — 回复未能有意义地解决查询
97+
</评分量表>
98+
99+
<用户查询>
100+
{query}
101+
</用户查询>
102+
103+
<智能体回复>
104+
{response}
105+
</智能体回复>
106+
107+
<输出格式>
108+
请按以下结构化 JSON 格式提供你的评估:
109+
{{
110+
"reason": "<关于回复完整性的详细解释,列出已覆盖和缺失的方面>",
111+
"score": <1 到 5 之间的整数>
112+
}}
113+
</输出格式>
114+
115+
JSON:
116+
"""
117+
).strip()
118+
119+
# Build default template from prompts
120+
DEFAULT_RESPONSE_COMPLETENESS_TEMPLATE = PromptTemplate(
121+
messages={
122+
LanguageEnum.EN: [
123+
ChatMessage(
124+
role="system",
125+
content=LLMGrader.SYSTEM_PROMPT_EN,
126+
),
127+
ChatMessage(
128+
role="user",
129+
content=RESPONSE_COMPLETENESS_PROMPT_EN,
130+
),
131+
],
132+
LanguageEnum.ZH: [
133+
ChatMessage(
134+
role="system",
135+
content=LLMGrader.SYSTEM_PROMPT_ZH,
136+
),
137+
ChatMessage(
138+
role="user",
139+
content=RESPONSE_COMPLETENESS_PROMPT_ZH,
140+
),
141+
],
142+
},
143+
)
144+
145+
146+
class ResponseCompletenessGrader(LLMGrader):
147+
"""
148+
Response Completeness Grader
149+
150+
Evaluates whether the agent's final response completely addresses all aspects
151+
of the user's query.
152+
153+
Attributes:
154+
name: Grader name
155+
model: BaseChatModel instance for evaluation
156+
template: Evaluation template
157+
language: Language for evaluation prompts (default: LanguageEnum.EN)
158+
159+
Example:
160+
>>> import asyncio
161+
>>> from openjudge.models.openai_chat_model import OpenAIChatModel
162+
>>> from openjudge.models.schema.prompt_template import LanguageEnum
163+
>>>
164+
>>> api = OpenAIChatModel(
165+
... api_key="your-key",
166+
... model="qwen3-max",
167+
... generate_kwargs={"temperature": 0.1}
168+
... )
169+
>>> grader = ResponseCompletenessGrader(
170+
... model=api,
171+
... language=LanguageEnum.EN
172+
... )
173+
>>> result = asyncio.run(grader.aevaluate(
174+
... query="What's the weather in NYC and should I bring an umbrella?",
175+
... response="It's sunny in NYC, 72°F. No umbrella needed."
176+
... ))
177+
>>> print(f"Score: {result.score}")
178+
"""
179+
180+
DEFAULT_TEMPLATE = DEFAULT_RESPONSE_COMPLETENESS_TEMPLATE
181+
182+
def __init__(
183+
self,
184+
model: BaseChatModel | dict,
185+
template: Optional[PromptTemplate] = None,
186+
language: LanguageEnum = LanguageEnum.EN,
187+
strategy: BaseEvaluationStrategy | None = None,
188+
):
189+
"""
190+
Initialize ResponseCompletenessGrader.
191+
192+
Args:
193+
model: BaseChatModel instance or dict config for OpenAIChatModel
194+
template: PromptTemplate for evaluation prompts
195+
language: Language for prompts (default: LanguageEnum.EN)
196+
strategy: The evaluation strategy to use. Defaults to DirectStrategy.
197+
"""
198+
super().__init__(
199+
name="response_completeness",
200+
mode=GraderMode.POINTWISE,
201+
description="Evaluate response completeness in addressing the query",
202+
model=model,
203+
template=template or self.DEFAULT_TEMPLATE,
204+
language=language,
205+
strategy=strategy,
206+
)
207+
208+
async def _aevaluate(
209+
self,
210+
query: Union[str, List[Dict[str, Any]]],
211+
response: str,
212+
context: Optional[str] = None,
213+
**kwargs: Any,
214+
) -> GraderScore:
215+
"""
216+
Evaluate response completeness.
217+
218+
Args:
219+
query: User query or chat history
220+
response: Agent's final response to evaluate
221+
context: Optional task context
222+
223+
Returns:
224+
GraderScore: Score between 1 and 5
225+
"""
226+
# Format query as string
227+
if isinstance(query, list):
228+
query_str = "\n".join(
229+
[f"{msg.get('role', 'user')}: {msg.get('content', '')}" for msg in query],
230+
)
231+
else:
232+
query_str = str(query)
233+
234+
context_str = context if context else ""
235+
236+
try:
237+
result = await super()._aevaluate(
238+
query=query_str,
239+
response=response,
240+
context=context_str,
241+
)
242+
score = result.score
243+
reason = result.reason
244+
245+
except Exception as e:
246+
logger.error(f"Error evaluating response completeness: {e}")
247+
score = 0.0
248+
reason = f"Evaluation error: {str(e)}"
249+
250+
metadata = {
251+
"raw_score": score,
252+
"evaluation_type": "response_completeness",
253+
}
254+
255+
return GraderScore(
256+
name=self.name,
257+
score=score,
258+
reason=reason,
259+
metadata=metadata,
260+
)
261+
262+
263+
__all__ = [
264+
"ResponseCompletenessGrader",
265+
"DEFAULT_RESPONSE_COMPLETENESS_TEMPLATE",
266+
]

0 commit comments

Comments
 (0)