|
| 1 | +# -*- coding: utf-8 -*- |
| 2 | +""" |
| 3 | +Plan Decomposition Grader |
| 4 | +
|
| 5 | +Evaluates whether the agent correctly decomposes a complex task into appropriate |
| 6 | +sub-tasks in its plan — identifying all necessary sub-goals, ordering them correctly, |
| 7 | +and recognizing dependencies. |
| 8 | +""" |
| 9 | + |
| 10 | +import textwrap |
| 11 | +from typing import Any, Dict, List, Optional |
| 12 | + |
| 13 | +from loguru import logger |
| 14 | + |
| 15 | +from openjudge.evaluation_strategy import BaseEvaluationStrategy |
| 16 | +from openjudge.graders.agent.utils import format_history |
| 17 | +from openjudge.graders.base_grader import GraderMode, GraderScore |
| 18 | +from openjudge.graders.llm_grader import LLMGrader |
| 19 | +from openjudge.models.base_chat_model import BaseChatModel |
| 20 | +from openjudge.models.schema.oai.message import ChatMessage |
| 21 | +from openjudge.models.schema.prompt_template import LanguageEnum, PromptTemplate |
| 22 | + |
| 23 | +# pylint: disable=line-too-long |
| 24 | + |
| 25 | +# English Prompt |
| 26 | +PLAN_DECOMPOSITION_PROMPT_EN = textwrap.dedent( |
| 27 | + """You are an expert in analyzing agent plan decomposition. Your task is to evaluate whether the agent correctly decomposes a complex task into appropriate sub-tasks in its plan. Good plan decomposition identifies all necessary sub-goals, orders them correctly based on dependencies, and creates a plan that is both complete and efficient. |
| 28 | +
|
| 29 | +<Rubrics> |
| 30 | +1. The agent identifies all necessary sub-goals required to complete the task (no critical sub-goals are missed) |
| 31 | +2. The sub-goals are ordered correctly respecting dependencies (prerequisites are completed before dependent steps) |
| 32 | +3. The decomposition is at an appropriate granularity (not too coarse, not overly detailed) |
| 33 | +4. The agent recognizes and handles parallel vs. sequential sub-tasks appropriately |
| 34 | +5. The decomposition does not include unnecessary or irrelevant sub-tasks |
| 35 | +6. The agent correctly identifies constraints and requirements from the task description that affect decomposition |
| 36 | +</Rubrics> |
| 37 | +
|
| 38 | +<Steps> |
| 39 | +1. Parse the task: Identify the overall goal, constraints, and implicit requirements |
| 40 | +2. Evaluate sub-goal completeness: Are all necessary steps identified? |
| 41 | +3. Check ordering: Are dependencies respected in the sub-goal ordering? |
| 42 | +4. Assess granularity: Is the decomposition at an appropriate level of detail? |
| 43 | +5. Check for extraneous sub-tasks: Are there unnecessary steps? |
| 44 | +6. Evaluate constraint awareness: Does the decomposition account for task constraints? |
| 45 | +</Steps> |
| 46 | +
|
| 47 | +<Scale> |
| 48 | +- **Score 5**: Excellent decomposition — All sub-goals identified, correctly ordered, appropriate granularity, no unnecessary steps, constraints fully accounted for |
| 49 | +- **Score 4**: Good decomposition — All major sub-goals identified with minor ordering or granularity issues |
| 50 | +- **Score 3**: Adequate decomposition — Most sub-goals identified, but some missing or ordering issues that could affect task completion |
| 51 | +- **Score 2**: Poor decomposition — Significant sub-goals missing or incorrect ordering that would lead to task failure |
| 52 | +- **Score 1**: Failed decomposition — The agent fails to decompose the task meaningfully or misses most critical sub-goals |
| 53 | +</Scale> |
| 54 | +
|
| 55 | +<Context (Optional)> |
| 56 | +{context} |
| 57 | +</Context> |
| 58 | +
|
| 59 | +<History (Optional)> |
| 60 | +{history} |
| 61 | +</History> |
| 62 | +
|
| 63 | +<Task> |
| 64 | +{query} |
| 65 | +</Task> |
| 66 | +
|
| 67 | +<Agent's Decomposition/Plan> |
| 68 | +{plan} |
| 69 | +</Agent's Decomposition/Plan> |
| 70 | +
|
| 71 | +<Output Schema> |
| 72 | +Provide your evaluation in the following structured JSON format: |
| 73 | +{{ |
| 74 | + "reason": "<detailed explanation of plan decomposition quality, including identified strengths and weaknesses>", |
| 75 | + "score": <integer between 1 and 5> |
| 76 | +}} |
| 77 | +</Output Schema> |
| 78 | +
|
| 79 | +JSON: |
| 80 | +""" |
| 81 | +).strip() |
| 82 | + |
| 83 | +# Chinese Prompt |
| 84 | +PLAN_DECOMPOSITION_PROMPT_ZH = textwrap.dedent( |
| 85 | + """你是一名分析智能体计划分解的专家。你的任务是评估智能体是否在计划中正确地将复杂任务分解为适当的子任务。良好的计划分解能够识别所有必要的子目标、根据依赖关系正确排序,并创建完整且高效的计划。 |
| 86 | +
|
| 87 | +<评分标准> |
| 88 | +1. 智能体识别了完成任务所需的所有必要子目标(没有遗漏关键子目标) |
| 89 | +2. 子目标按照依赖关系正确排序(先决条件在依赖步骤之前完成) |
| 90 | +3. 分解处于适当的粒度(不太粗略,也不太过于详细) |
| 91 | +4. 智能体适当识别和处理并行与顺序子任务 |
| 92 | +5. 分解不包括不必要或不相关的子任务 |
| 93 | +6. 智能体正确识别任务描述中影响分解的约束和需求 |
| 94 | +</评分标准> |
| 95 | +
|
| 96 | +<评估步骤> |
| 97 | +1. 解析任务:识别总体目标、约束和隐含需求 |
| 98 | +2. 评估子目标完整性:是否识别了所有必要步骤? |
| 99 | +3. 检查排序:子目标排序是否尊重了依赖关系? |
| 100 | +4. 评估粒度:分解是否处于适当的详细程度? |
| 101 | +5. 检查多余的子任务:是否存在不必要的步骤? |
| 102 | +6. 评估约束意识:分解是否考虑了任务约束? |
| 103 | +</评估步骤> |
| 104 | +
|
| 105 | +<评分量表> |
| 106 | +- **分数 5**:优秀的分解 — 所有子目标都已识别、正确排序、粒度适当、没有不必要的步骤、完全考虑了约束 |
| 107 | +- **分数 4**:良好的分解 — 所有主要子目标已识别,有轻微的排序或粒度问题 |
| 108 | +- **分数 3**:足够的分解 — 大多数子目标已识别,但有些缺失或排序问题可能影响任务完成 |
| 109 | +- **分数 2**:较差的分解 — 缺少重要子目标或排序不正确,可能导致任务失败 |
| 110 | +- **分数 1**:失败的分解 — 智能体未能有意义地分解任务或遗漏了大多数关键子目标 |
| 111 | +</评分量表> |
| 112 | +
|
| 113 | +<上下文(可选)> |
| 114 | +{context} |
| 115 | +</上下文> |
| 116 | +
|
| 117 | +<历史记录(可选)> |
| 118 | +{history} |
| 119 | +</历史记录> |
| 120 | +
|
| 121 | +<任务> |
| 122 | +{query} |
| 123 | +</任务> |
| 124 | +
|
| 125 | +<智能体的分解/计划> |
| 126 | +{plan} |
| 127 | +</智能体的分解/计划> |
| 128 | +
|
| 129 | +<输出格式> |
| 130 | +请按以下结构化 JSON 格式提供你的评估: |
| 131 | +{{ |
| 132 | + "reason": "<关于计划分解质量的详细解释,包括识别的优点和缺点>", |
| 133 | + "score": <1 到 5 之间的整数> |
| 134 | +}} |
| 135 | +</输出格式> |
| 136 | +
|
| 137 | +JSON: |
| 138 | +""" |
| 139 | +).strip() |
| 140 | + |
| 141 | +# Build default template from prompts |
| 142 | +DEFAULT_PLAN_DECOMPOSITION_TEMPLATE = PromptTemplate( |
| 143 | + messages={ |
| 144 | + LanguageEnum.EN: [ |
| 145 | + ChatMessage( |
| 146 | + role="system", |
| 147 | + content=LLMGrader.SYSTEM_PROMPT_EN, |
| 148 | + ), |
| 149 | + ChatMessage( |
| 150 | + role="user", |
| 151 | + content=PLAN_DECOMPOSITION_PROMPT_EN, |
| 152 | + ), |
| 153 | + ], |
| 154 | + LanguageEnum.ZH: [ |
| 155 | + ChatMessage( |
| 156 | + role="system", |
| 157 | + content=LLMGrader.SYSTEM_PROMPT_ZH, |
| 158 | + ), |
| 159 | + ChatMessage( |
| 160 | + role="user", |
| 161 | + content=PLAN_DECOMPOSITION_PROMPT_ZH, |
| 162 | + ), |
| 163 | + ], |
| 164 | + }, |
| 165 | +) |
| 166 | + |
| 167 | + |
| 168 | +class PlanDecompositionGrader(LLMGrader): |
| 169 | + """ |
| 170 | + Plan Decomposition Grader |
| 171 | +
|
| 172 | + Evaluates whether the agent correctly decomposes a complex task into appropriate |
| 173 | + sub-tasks in its plan. |
| 174 | +
|
| 175 | + Required modules: query, plan |
| 176 | +
|
| 177 | + Attributes: |
| 178 | + name: Grader name |
| 179 | + model: BaseChatModel instance for evaluation |
| 180 | + template: Evaluation template |
| 181 | + language: Language for evaluation prompts (default: LanguageEnum.EN) |
| 182 | +
|
| 183 | + Example: |
| 184 | + >>> import asyncio |
| 185 | + >>> from openjudge.models.openai_chat_model import OpenAIChatModel |
| 186 | + >>> from openjudge.models.schema.prompt_template import LanguageEnum |
| 187 | + >>> |
| 188 | + >>> api = OpenAIChatModel( |
| 189 | + ... api_key="your-key", |
| 190 | + ... model="qwen3-max", |
| 191 | + ... generate_kwargs={"temperature": 0.1} |
| 192 | + ... ) |
| 193 | + >>> grader = PlanDecompositionGrader( |
| 194 | + ... model=api, |
| 195 | + ... language=LanguageEnum.EN |
| 196 | + ... ) |
| 197 | + >>> result = asyncio.run(grader.aevaluate( |
| 198 | + ... query="Book a flight from NYC to London, find a hotel near the airport, and arrange airport transfer", |
| 199 | + ... plan="1. Search flights NYC→London 2. Book flight 3. Search hotels near London airport 4. Book hotel 5. Search airport transfer options 6. Book transfer" |
| 200 | + ... )) |
| 201 | + >>> print(f"Score: {result.score}") |
| 202 | + """ |
| 203 | + |
| 204 | + DEFAULT_TEMPLATE = DEFAULT_PLAN_DECOMPOSITION_TEMPLATE |
| 205 | + |
| 206 | + def __init__( |
| 207 | + self, |
| 208 | + model: BaseChatModel | dict, |
| 209 | + template: Optional[PromptTemplate] = None, |
| 210 | + language: LanguageEnum = LanguageEnum.EN, |
| 211 | + strategy: BaseEvaluationStrategy | None = None, |
| 212 | + ): |
| 213 | + """ |
| 214 | + Initialize PlanDecompositionGrader. |
| 215 | +
|
| 216 | + Args: |
| 217 | + model: BaseChatModel instance or dict config for OpenAIChatModel |
| 218 | + template: PromptTemplate for evaluation prompts |
| 219 | + language: Language for prompts (default: LanguageEnum.EN) |
| 220 | + strategy: The evaluation strategy to use. Defaults to DirectStrategy. |
| 221 | + """ |
| 222 | + super().__init__( |
| 223 | + name="plan_decomposition", |
| 224 | + mode=GraderMode.POINTWISE, |
| 225 | + description="Evaluate plan decomposition quality", |
| 226 | + model=model, |
| 227 | + template=template or self.DEFAULT_TEMPLATE, |
| 228 | + language=language, |
| 229 | + strategy=strategy, |
| 230 | + ) |
| 231 | + |
| 232 | + async def _aevaluate( |
| 233 | + self, |
| 234 | + query: str, |
| 235 | + plan: str, |
| 236 | + history: Optional[List[Dict[str, Any]]] = None, |
| 237 | + context: Optional[str] = None, |
| 238 | + **kwargs: Any, |
| 239 | + ) -> GraderScore: |
| 240 | + """ |
| 241 | + Evaluate plan decomposition quality. |
| 242 | +
|
| 243 | + Args: |
| 244 | + query: The user's task or query |
| 245 | + plan: The agent's plan/decomposition of the task |
| 246 | + history: Optional list of previous step dictionaries for context |
| 247 | + context: Optional task context |
| 248 | +
|
| 249 | + Returns: |
| 250 | + GraderScore: Score between 1 and 5 |
| 251 | + """ |
| 252 | + context_str = context if context else "" |
| 253 | + history_str = format_history(history, include_tags=False) |
| 254 | + |
| 255 | + try: |
| 256 | + result = await super()._aevaluate( |
| 257 | + query=query, |
| 258 | + plan=plan, |
| 259 | + history=history_str, |
| 260 | + context=context_str, |
| 261 | + ) |
| 262 | + score = result.score |
| 263 | + reason = result.reason |
| 264 | + |
| 265 | + except Exception as e: |
| 266 | + logger.error(f"Error evaluating plan decomposition: {e}") |
| 267 | + score = 0.0 |
| 268 | + reason = f"Evaluation error: {str(e)}" |
| 269 | + |
| 270 | + metadata = { |
| 271 | + "raw_score": score, |
| 272 | + "evaluation_type": "plan_decomposition", |
| 273 | + } |
| 274 | + |
| 275 | + return GraderScore( |
| 276 | + name=self.name, |
| 277 | + score=score, |
| 278 | + reason=reason, |
| 279 | + metadata=metadata, |
| 280 | + ) |
| 281 | + |
| 282 | + |
| 283 | +__all__ = [ |
| 284 | + "PlanDecompositionGrader", |
| 285 | + "DEFAULT_PLAN_DECOMPOSITION_TEMPLATE", |
| 286 | +] |
0 commit comments