forked from HKUDS/OpenSpace
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathresults.py
More file actions
285 lines (237 loc) · 10 KB
/
Copy pathresults.py
File metadata and controls
285 lines (237 loc) · 10 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
"""Results, telemetry, and iteration helpers for GroundingAgent.
Pure functions and agent-delegating helpers for building iteration
feedback, final summaries, task-completion checks, and recording.
Extracted from grounding_agent.py (Epic 5.10).
"""
from __future__ import annotations
import copy
import json
from typing import Any, Dict, List, Optional
from openspace.prompts import GroundingAgentPrompts
from openspace.utils.logging import Logger
logger = Logger.get_logger("openspace.agents.grounding_agent")
# ── Pure functions (no agent parameter) ────────────────────────────
def build_iteration_feedback(
iteration: int,
llm_summary: Optional[str] = None,
add_guidance: bool = True,
) -> Optional[Dict[str, str]]:
"""Build feedback message to add to next iteration.
Returns ``None`` when *llm_summary* is falsy.
"""
if not llm_summary:
return None
feedback_content = GroundingAgentPrompts.iteration_feedback(
iteration=iteration, llm_summary=llm_summary, add_guidance=add_guidance
)
return {"role": "system", "content": feedback_content}
def remove_previous_guidance(messages: List[Dict[str, Any]]) -> None:
"""Remove guidance section from previous iteration feedback messages.
Mutates *messages* in-place.
"""
for msg in messages:
if msg.get("role") == "system":
content = msg.get("content", "")
# Check if this is an iteration feedback message with guidance
if "## Iteration" in content and "Summary" in content and "---" in content:
# Remove everything from "---" onwards (the guidance part)
summary_only = content.split("---")[0].strip()
msg["content"] = summary_only
def format_tool_executions(all_tool_results: List[Dict]) -> List[Dict]:
"""Format raw tool-result dicts into a serialisable execution list."""
executions = []
for tr in all_tool_results:
tool_result_obj = tr.get("result")
tool_call = tr.get("tool_call")
status = "unknown"
if hasattr(tool_result_obj, "status"):
status_obj = tool_result_obj.status
status = getattr(status_obj, "value", status_obj)
# Extract tool_name and arguments from tool_call object (litellm format)
tool_name = "unknown"
arguments: dict = {}
if tool_call is not None:
if hasattr(tool_call, "function"):
# tool_call is an object with .function attribute
tool_name = getattr(tool_call.function, "name", "unknown")
args_raw = getattr(tool_call.function, "arguments", "{}")
if isinstance(args_raw, str):
try:
arguments = json.loads(args_raw) if args_raw.strip() else {}
except json.JSONDecodeError:
arguments = {}
else:
arguments = args_raw if isinstance(args_raw, dict) else {}
elif isinstance(tool_call, dict):
# Fallback: tool_call is a dict
func = tool_call.get("function", {})
tool_name = func.get("name", "unknown")
args_raw = func.get("arguments", "{}")
if isinstance(args_raw, str):
try:
arguments = json.loads(args_raw) if args_raw.strip() else {}
except json.JSONDecodeError:
arguments = {}
else:
arguments = args_raw if isinstance(args_raw, dict) else {}
executions.append(
{
"tool_name": tool_name,
"arguments": arguments,
"backend": tr.get("backend"),
"server_name": tr.get("server_name"),
"status": status,
"content": tool_result_obj.content if hasattr(tool_result_obj, "content") else None,
"error": tool_result_obj.error if hasattr(tool_result_obj, "error") else None,
"execution_time": tool_result_obj.execution_time
if hasattr(tool_result_obj, "execution_time")
else None,
"metadata": tool_result_obj.metadata if hasattr(tool_result_obj, "metadata") else {},
}
)
return executions
def check_task_completion(messages: List[Dict]) -> bool:
"""Return True if the last assistant message contains the COMPLETE token."""
for msg in reversed(messages):
if msg.get("role") == "assistant":
content = msg.get("content", "")
return GroundingAgentPrompts.TASK_COMPLETE in content
return False
def extract_last_assistant_message(messages: List[Dict]) -> str:
"""Return content of the last assistant message, or ``""``."""
for msg in reversed(messages):
if msg.get("role") == "assistant":
return msg.get("content", "")
return ""
# ── Agent-dependent functions ──────────────────────────────────────
async def generate_final_summary(
agent,
instruction: str,
messages: List[Dict],
iterations: int,
) -> tuple[str, bool, List[Dict]]:
"""Generate final summary across all iterations for reporting to upper layer.
Returns:
tuple[str, bool, List[Dict]]: (summary_text, success_flag, context_used)
"""
final_summary_prompt = {
"role": "user",
"content": GroundingAgentPrompts.final_summary(instruction=instruction, iterations=iterations),
}
clean_messages = []
for msg in messages:
# Skip tool result messages
if msg.get("role") == "tool":
continue
# Copy message and remove tool_calls if present
clean_msg = msg.copy()
if "tool_calls" in clean_msg:
del clean_msg["tool_calls"]
clean_messages.append(clean_msg)
clean_messages.append(final_summary_prompt)
# Save context for return
context_for_return = copy.deepcopy(clean_messages)
try:
# Call LLMClient to generate final summary (without tools)
summary_response = await agent._llm_client.complete(
messages=clean_messages, tools=None, execute_tools=False
)
final_summary = summary_response.get("message", {}).get("content", "")
if final_summary:
logger.info(f"Generated final summary: {final_summary[:200]}...")
return final_summary, True, context_for_return
else:
logger.warning("LLM returned empty final summary")
return (
f"Task completed after {iterations} iteration(s). Check execution history for details.",
True,
context_for_return,
)
except Exception as e:
logger.error(f"Error generating final summary: {e}")
return (
f"Task completed after {iterations} iteration(s), but failed to generate summary.",
False,
context_for_return,
)
async def build_final_result(
agent,
instruction: str,
messages: List[Dict],
all_tool_results: List[Dict],
iterations: int,
max_iterations: int,
iteration_contexts: List[Dict] = None,
retrieved_tools_list: List[Dict] = None,
search_debug_info: Dict[str, Any] = None,
) -> Dict[str, Any]:
"""Build the final execution result dict.
Routes ``_check_task_completion``, ``_format_tool_executions``, and
``_extract_last_assistant_message`` through *agent* to preserve MRO.
"""
is_complete = agent._check_task_completion(messages)
tool_executions = agent._format_tool_executions(all_tool_results)
result = {
"instruction": instruction,
"step": agent.step,
"iterations": iterations,
"tool_executions": tool_executions,
"messages": messages,
"iteration_contexts": iteration_contexts or [],
"retrieved_tools_list": retrieved_tools_list or [],
"search_debug_info": search_debug_info,
"active_skills": list(agent._active_skill_ids),
"keep_session": True,
}
if is_complete:
logger.info("Task completed with <COMPLETE> marker")
last_response = agent._extract_last_assistant_message(messages)
result["response"] = last_response.replace(GroundingAgentPrompts.TASK_COMPLETE, "").strip()
result["status"] = "success"
else:
result["response"] = agent._extract_last_assistant_message(messages)
result["status"] = "incomplete"
result["warning"] = (
f"Task reached max iterations ({max_iterations}) without completion. "
f"This may indicate the task needs more steps or clarification."
)
return result
async def record_agent_execution(
agent,
result: Dict[str, Any],
instruction: str,
) -> None:
"""Record agent execution to recording manager.
No-op when ``agent._recording_manager`` is falsy.
"""
if not agent._recording_manager:
return
# Extract tool execution summary
tool_summary = []
if result.get("tool_executions"):
for exec_info in result["tool_executions"]:
tool_summary.append(
{
"tool": exec_info.get("tool_name", "unknown"),
"backend": exec_info.get("backend", "unknown"),
"status": exec_info.get("status", "unknown"),
}
)
await agent._recording_manager.record_agent_action(
agent_name=agent.name,
action_type="execute",
input_data={"instruction": instruction},
reasoning={
"response": result.get("response", ""),
"tools_selected": tool_summary,
},
output_data={
"status": result.get("status", "unknown"),
"iterations": result.get("iterations", 0),
"num_tool_executions": len(result.get("tool_executions", [])),
},
metadata={
"step": agent.step,
"instruction": instruction,
},
)