|
18 | 18 |
|
19 | 19 | from datetime import datetime |
20 | 20 |
|
| 21 | +# SPM (Strategy-Procedural Memory, paper Sec. 2.4.1) — planner-side strategic priors. |
| 22 | +# The same experience KB used by the executor also guides the planner toward globally |
| 23 | +# coherent reasoning graphs. Imported lazily/safely so the planner still runs when the |
| 24 | +# agent_kb service/package is unavailable. |
| 25 | +try: |
| 26 | + from agent_kb.example import call_hybrid_search |
| 27 | +except Exception: # pragma: no cover - agent_kb is optional |
| 28 | + call_hybrid_search = None |
| 29 | + |
| 30 | + |
| 31 | +def _is_truthy(val) -> bool: |
| 32 | + """True for boolean True or the strings 'true'/'True' (env-var friendly).""" |
| 33 | + return val is True or (isinstance(val, str) and val.strip().lower() == "true") |
| 34 | + |
| 35 | + |
| 36 | +def _need_memory_enabled(config) -> bool: |
| 37 | + """Resolve the SPM on/off switch: env `need_memory` takes precedence, then config.""" |
| 38 | + cfg = config or {} |
| 39 | + return _is_truthy(os.getenv("need_memory", cfg.get("need_memory", False))) |
| 40 | + |
21 | 41 |
|
22 | 42 | class GlobalPlannerAgent(BaseAgent): |
23 | 43 | """ |
@@ -47,10 +67,60 @@ def __init__(self, model: str = "deepseek-r1", tools: List[Dict[str, Any]] = Non |
47 | 67 | self.config, |
48 | 68 | default_name="GLOBAL_PLANNER_PROMPT" |
49 | 69 | ) |
50 | | - |
| 70 | + |
| 71 | + # SPM planner-side: strategic priors block, appended to the base prompt only when |
| 72 | + # SPM is active. Kept separate so the default planner prompt is unchanged when off. |
| 73 | + planner_memory_block_config = { |
| 74 | + "prompt_path": self.config.get("planner_memory_block_path"), |
| 75 | + "prompt_name": self.config.get("planner_memory_block_name") |
| 76 | + } |
| 77 | + self.planner_memory_block = load_prompt( |
| 78 | + planner_memory_block_config, |
| 79 | + default_name="GLOBAL_PLANNER_MEMORY_BLOCK" |
| 80 | + ) |
| 81 | + self.prompt_template_with_memory = self.prompt_template + "\n" + self.planner_memory_block |
| 82 | + self.need_memory = _need_memory_enabled(self.config) |
| 83 | + self.memory_prompt = None # external override hook (set_memory_prompt) |
| 84 | + self.base_retrieve_score_threshold = self.config.get('base_retrieve_score_threshold', 0.3) |
| 85 | + self.append_retrieve_score_threshold = self.config.get('append_retrieve_score_threshold', 0.65) |
| 86 | + |
51 | 87 | self.max_iter = self.config.get('max_iter', 5) |
52 | 88 | self.max_retries = self.config.get('max_retries', 3) |
53 | 89 | self.max_nodes = self.config.get('max_nodes', 8) |
| 90 | + |
| 91 | + def set_memory_prompt(self, memory_prompt: str): |
| 92 | + """外部注入 SPM 记忆提示(覆盖内部检索)。paper Sec. 2.4.1 的 planner-side 钩子。""" |
| 93 | + self.memory_prompt = memory_prompt |
| 94 | + |
| 95 | + def _retrieve_planner_memory(self, question: str): |
| 96 | + """ |
| 97 | + 从 agent_kb 检索与当前问题相似的历史 planning 经验(strategic priors), |
| 98 | + 组装成注入 planner prompt 的 memory_prompt。检索不可用或失败时返回 None(不影响规划)。 |
| 99 | + """ |
| 100 | + if not (self.need_memory and call_hybrid_search is not None): |
| 101 | + if self.need_memory and call_hybrid_search is None: |
| 102 | + self.logger.warning("need_memory 已开启,但 agent_kb 客户端不可用,planner 不使用记忆") |
| 103 | + return None |
| 104 | + try: |
| 105 | + base_similar = call_hybrid_search('base', question, top_k=2, weights={"text": 0.3, "semantic": 0.7}) |
| 106 | + append_similar = call_hybrid_search('append', question, top_k=2, weights={"text": 0.3, "semantic": 0.7}) |
| 107 | + refs = [] |
| 108 | + for exp in (base_similar or []): |
| 109 | + if exp.get('total_score', 0) > self.base_retrieve_score_threshold: |
| 110 | + refs.append((exp.get('query'), exp.get('agent_experience', '') or '')) |
| 111 | + for exp in (append_similar or []): |
| 112 | + if exp.get('total_score', 0) > self.append_retrieve_score_threshold: |
| 113 | + refs.append((exp.get('query'), exp.get('agent_experience', '') or '')) |
| 114 | + if not refs: |
| 115 | + return None |
| 116 | + parts = [] |
| 117 | + for q, planning_exp in refs: |
| 118 | + parts.append(f"[Similar past task]\n{q}\n[Planning experience]\n{planning_exp}") |
| 119 | + self.logger.info(f"SPM planner 检索到 {len(refs)} 条 planning 经验") |
| 120 | + return "\n\n".join(parts) |
| 121 | + except Exception as e: |
| 122 | + self.logger.warning(f"SPM planner 检索失败,本次规划不使用记忆: {e}") |
| 123 | + return None |
54 | 124 |
|
55 | 125 | def build_graph_from_plan(self, plan_result: Dict[str, Any]) -> DirectedGraph: |
56 | 126 | """ |
@@ -123,34 +193,47 @@ def mark_node_executed(self, node_id: str) -> bool: |
123 | 193 |
|
124 | 194 | return self.graph.set_node_status(node_id, NodeExecutionStatus.EXECUTED) |
125 | 195 |
|
126 | | - def execute_one_step(self, input_data: str, question: str, current_iter = 1, max_iter = 5, additional_info = None, tools = None) -> Dict[str, Any]: |
| 196 | + def execute_one_step(self, input_data: str, question: str, current_iter = 1, max_iter = 5, additional_info = None, tools = None, memory_prompt = None) -> Dict[str, Any]: |
127 | 197 | """ |
128 | 198 | 执行任务规划,将自然语言任务拆解为子任务并构建依赖关系图 |
129 | | - |
| 199 | +
|
130 | 200 | Args: |
131 | 201 | input_data: 中间图,包含nodes和edges |
132 | 202 | question: 问题描述 |
133 | 203 | current_iter: 当前迭代次数 |
134 | 204 | max_iter: 最大迭代次数 |
135 | | - |
| 205 | + memory_prompt: SPM 检索到的 planning 先验(可选);提供时使用带记忆的 prompt |
| 206 | +
|
136 | 207 | Returns: |
137 | 208 | 包含 nodes 和 edges 的字典,表示任务分解结果 |
138 | 209 | """ |
139 | 210 |
|
140 | | - formatted_prompt = self.prompt_template.format( |
141 | | - graph=input_data, |
142 | | - question=question, |
143 | | - max_iter=str(max_iter), |
144 | | - current_iter=str(current_iter), |
145 | | - additional_info=additional_info, |
146 | | - tools=json.dumps(tools), |
147 | | - max_nodes=str(self.max_nodes) |
148 | | - ) |
149 | | - |
| 211 | + if memory_prompt: |
| 212 | + formatted_prompt = self.prompt_template_with_memory.format( |
| 213 | + graph=input_data, |
| 214 | + question=question, |
| 215 | + max_iter=str(max_iter), |
| 216 | + current_iter=str(current_iter), |
| 217 | + additional_info=additional_info, |
| 218 | + tools=json.dumps(tools), |
| 219 | + max_nodes=str(self.max_nodes), |
| 220 | + memory_prompt=memory_prompt |
| 221 | + ) |
| 222 | + else: |
| 223 | + formatted_prompt = self.prompt_template.format( |
| 224 | + graph=input_data, |
| 225 | + question=question, |
| 226 | + max_iter=str(max_iter), |
| 227 | + current_iter=str(current_iter), |
| 228 | + additional_info=additional_info, |
| 229 | + tools=json.dumps(tools), |
| 230 | + max_nodes=str(self.max_nodes) |
| 231 | + ) |
| 232 | + |
150 | 233 | # 调用模型生成响应 |
151 | 234 | response = self._generate_plan(formatted_prompt) |
152 | | - |
153 | | - return response |
| 235 | + |
| 236 | + return response |
154 | 237 |
|
155 | 238 | def execute(self, input_data: str, file_path = None, additional_info = None) -> Dict[str, Any]: |
156 | 239 | """ |
@@ -182,17 +265,21 @@ def execute(self, input_data: str, file_path = None, additional_info = None) -> |
182 | 265 | tool_info = self.tool_manager.get_simple_tool_info(tool) |
183 | 266 | tool_info_list.append(tool_info) |
184 | 267 |
|
| 268 | + # SPM planner-side: 取一次 planning 先验(外部注入优先,否则从 agent_kb 检索), |
| 269 | + # 整个规划循环复用,避免每轮重复检索。 |
| 270 | + memory_prompt = self.memory_prompt or self._retrieve_planner_memory(input_data) |
| 271 | + |
185 | 272 | while True: # 检查生成的图中是否存在环,如果存在环,则重新构建图 |
186 | 273 | idx = 1 |
187 | 274 | while idx <= max_iter: |
188 | 275 | # 添加重试机制 |
189 | 276 | max_retries = self.max_retries |
190 | 277 | retry_count = 0 |
191 | 278 | response = None |
192 | | - |
| 279 | + |
193 | 280 | while retry_count < max_retries: |
194 | 281 | try: |
195 | | - response = self.execute_one_step(json.dumps(graph), input_data, current_iter=idx, max_iter=max_iter, additional_info=additional_info, tools=tool_info_list) |
| 282 | + response = self.execute_one_step(json.dumps(graph), input_data, current_iter=idx, max_iter=max_iter, additional_info=additional_info, tools=tool_info_list, memory_prompt=memory_prompt) |
196 | 283 | if response is not None: |
197 | 284 | break # 成功获得响应,跳出重试循环 |
198 | 285 | except Exception as e: |
|
0 commit comments