|
| 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