Skip to content

Commit b2d94ac

Browse files
authored
feat: add agent plan decomposition (#169)
1 parent 135facf commit b2d94ac

3 files changed

Lines changed: 593 additions & 0 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# -*- coding: utf-8 -*-
2+
"""Plan graders for evaluating agent planning and decomposition quality."""
3+
4+
from openjudge.graders.agent.plan.plan_decomposition import PlanDecompositionGrader
5+
from openjudge.graders.agent.plan.plan_feasibility import PlanFeasibilityGrader
6+
7+
__all__ = [
8+
"PlanDecompositionGrader",
9+
"PlanFeasibilityGrader",
10+
]
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
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

Comments
 (0)