tiny-agent-os 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tiny_agent_os-0.0.1.dist-info/METADATA +377 -0
- tiny_agent_os-0.0.1.dist-info/RECORD +64 -0
- tiny_agent_os-0.0.1.dist-info/WHEEL +5 -0
- tiny_agent_os-0.0.1.dist-info/entry_points.txt +2 -0
- tiny_agent_os-0.0.1.dist-info/licenses/LICENSE +53 -0
- tiny_agent_os-0.0.1.dist-info/top_level.txt +1 -0
- tinyagent/__init__.py +75 -0
- tinyagent/_version.py +21 -0
- tinyagent/agent.py +957 -0
- tinyagent/chat/__init__.py +12 -0
- tinyagent/chat/chat_mode.py +291 -0
- tinyagent/cli/__init__.py +16 -0
- tinyagent/cli/colors.py +104 -0
- tinyagent/cli/main.py +664 -0
- tinyagent/cli/spinner.py +94 -0
- tinyagent/cli.py +47 -0
- tinyagent/config/__init__.py +14 -0
- tinyagent/config/config.py +258 -0
- tinyagent/decorators.py +187 -0
- tinyagent/exceptions.py +85 -0
- tinyagent/factory/__init__.py +18 -0
- tinyagent/factory/agent_factory.py +439 -0
- tinyagent/factory/dynamic_agent_factory.py +561 -0
- tinyagent/factory/orchestrator.py +1514 -0
- tinyagent/factory/tiny_chain.py +552 -0
- tinyagent/logging.py +97 -0
- tinyagent/mcp/__init__.py +14 -0
- tinyagent/mcp/manager.py +321 -0
- tinyagent/prompts/README.md +133 -0
- tinyagent/prompts/default.md +14 -0
- tinyagent/prompts/prompt_manager.py +206 -0
- tinyagent/prompts/system/agent.md +50 -0
- tinyagent/prompts/system/retry.md +55 -0
- tinyagent/prompts/system/strict_json.md +54 -0
- tinyagent/prompts/system.md +10 -0
- tinyagent/prompts/tools/calculator.md +13 -0
- tinyagent/prompts/tools/weather.md +7 -0
- tinyagent/prompts/workflows/riv_reflect.md +62 -0
- tinyagent/prompts/workflows/riv_verify.md +47 -0
- tinyagent/prompts/workflows/triage.md +129 -0
- tinyagent/tool.py +185 -0
- tinyagent/tools/README.md +391 -0
- tinyagent/tools/__init__.py +39 -0
- tinyagent/tools/aider.py +122 -0
- tinyagent/tools/anon_coder.py +296 -0
- tinyagent/tools/boilerplate_tool.py +147 -0
- tinyagent/tools/brave_search.py +104 -0
- tinyagent/tools/codeagent_tool.py +217 -0
- tinyagent/tools/content_processor.py +285 -0
- tinyagent/tools/custom_text_browser.py +965 -0
- tinyagent/tools/duckduckgo_search.py +153 -0
- tinyagent/tools/external.py +303 -0
- tinyagent/tools/file_manipulator.py +274 -0
- tinyagent/tools/final_extractor_tool.py +249 -0
- tinyagent/tools/llm_serializer.py +124 -0
- tinyagent/tools/markdown_gen.py +300 -0
- tinyagent/tools/ripgrep.py +136 -0
- tinyagent/utils/__init__.py +13 -0
- tinyagent/utils/json_parser.py +231 -0
- tinyagent/utils/logging_utils.py +78 -0
- tinyagent/utils/openrouter_request.py +123 -0
- tinyagent/utils/serialization.py +185 -0
- tinyagent/utils/structured_outputs.py +131 -0
- tinyagent/utils/type_converter.py +134 -0
|
@@ -0,0 +1,1514 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Orchestrator module for coordinating multiple agents.
|
|
3
|
+
|
|
4
|
+
This module provides an orchestrator that manages and coordinates multiple agents
|
|
5
|
+
to accomplish complex tasks, handling task delegation, agent coordination, and
|
|
6
|
+
result integration.
|
|
7
|
+
"""
|
|
8
|
+
#this file is over 1000 lines, it is only this long for debugging purposes
|
|
9
|
+
import json
|
|
10
|
+
import pprint
|
|
11
|
+
import time
|
|
12
|
+
import re
|
|
13
|
+
import os
|
|
14
|
+
import threading
|
|
15
|
+
from typing import Dict, List, Any, Optional, Union, TypeVar, Type, cast
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
|
|
18
|
+
from ..logging import get_logger
|
|
19
|
+
from tinyagent.prompts.prompt_manager import PromptManager
|
|
20
|
+
from ..agent import Agent, get_llm
|
|
21
|
+
from ..config import load_config, get_config_value
|
|
22
|
+
from ..exceptions import OrchestratorError, AgentNotFoundError
|
|
23
|
+
from .dynamic_agent_factory import DynamicAgentFactory
|
|
24
|
+
|
|
25
|
+
# Set up logger with more detailed formatting
|
|
26
|
+
logger = get_logger(__name__)
|
|
27
|
+
|
|
28
|
+
from tinyagent.utils.logging_utils import (
|
|
29
|
+
log_task_event,
|
|
30
|
+
log_decision,
|
|
31
|
+
log_section_header,
|
|
32
|
+
log_step,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class TaskStatus:
|
|
38
|
+
"""
|
|
39
|
+
Status information for a task being orchestrated.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
task_id: Unique identifier for the task
|
|
43
|
+
description: Text description of the task
|
|
44
|
+
status: Current status (pending, in_progress, completed, failed, needs_permission)
|
|
45
|
+
assigned_agent: ID of the agent assigned to the task (optional)
|
|
46
|
+
created_at: Timestamp when the task was created
|
|
47
|
+
started_at: Timestamp when the task was started (optional)
|
|
48
|
+
completed_at: Timestamp when the task was completed (optional)
|
|
49
|
+
result: The result of the task (optional)
|
|
50
|
+
error: Error message if the task failed (optional)
|
|
51
|
+
"""
|
|
52
|
+
task_id: str
|
|
53
|
+
description: str
|
|
54
|
+
status: str = "pending" # pending, in_progress, completed, failed, needs_permission
|
|
55
|
+
assigned_agent: Optional[str] = None
|
|
56
|
+
created_at: float = field(default_factory=time.time)
|
|
57
|
+
started_at: Optional[float] = None
|
|
58
|
+
completed_at: Optional[float] = None
|
|
59
|
+
result: Any = None
|
|
60
|
+
error: Optional[str] = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# Type variable for the singleton pattern
|
|
64
|
+
T = TypeVar('T')
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Orchestrator:
|
|
68
|
+
"""
|
|
69
|
+
Orchestrates multiple agents to accomplish complex tasks.
|
|
70
|
+
|
|
71
|
+
Manages task delegation, agent coordination, and result integration across
|
|
72
|
+
multiple specialized agents.
|
|
73
|
+
|
|
74
|
+
Attributes:
|
|
75
|
+
factory: DynamicAgentFactory instance for creating agents
|
|
76
|
+
tasks: Dictionary of tasks being managed
|
|
77
|
+
agents: Dictionary of registered agents
|
|
78
|
+
_lock: Threading lock for concurrency control
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
_instance = None # Singleton instance
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def get_instance(cls: Type[T], config: Optional[Dict[str, Any]] = None) -> T:
|
|
85
|
+
"""
|
|
86
|
+
Get or create the singleton orchestrator instance.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
config: Optional configuration dictionary
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
The singleton Orchestrator instance
|
|
93
|
+
"""
|
|
94
|
+
if cls._instance is None:
|
|
95
|
+
cls._instance = cls(config)
|
|
96
|
+
return cls._instance
|
|
97
|
+
|
|
98
|
+
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
|
99
|
+
"""
|
|
100
|
+
Initialize the orchestrator with configuration.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
config: Optional configuration dictionary
|
|
104
|
+
"""
|
|
105
|
+
self.factory = DynamicAgentFactory.get_instance(config)
|
|
106
|
+
self.tasks: Dict[str, TaskStatus] = {}
|
|
107
|
+
self.agents: Dict[str, Agent] = {}
|
|
108
|
+
self.next_task_id = 1
|
|
109
|
+
self.next_agent_id = 1
|
|
110
|
+
self.lock = threading.Lock()
|
|
111
|
+
self.prompt_manager = PromptManager()
|
|
112
|
+
|
|
113
|
+
# Load configuration
|
|
114
|
+
if config is None:
|
|
115
|
+
self.config = load_config()
|
|
116
|
+
else:
|
|
117
|
+
self.config = config
|
|
118
|
+
|
|
119
|
+
# Initialize the triage agent
|
|
120
|
+
self._create_triage_agent()
|
|
121
|
+
logger.info("Orchestrator initialized")
|
|
122
|
+
|
|
123
|
+
def _create_triage_agent(self) -> None:
|
|
124
|
+
"""
|
|
125
|
+
Create and register the triage agent.
|
|
126
|
+
|
|
127
|
+
The triage agent is responsible for analyzing user queries and delegating
|
|
128
|
+
to specialized agents.
|
|
129
|
+
"""
|
|
130
|
+
# Get max_retries from config if available, otherwise use default (3)
|
|
131
|
+
max_retries = get_config_value(self.config, 'retries.max_attempts', 3)
|
|
132
|
+
|
|
133
|
+
# Get preferred model from config if available
|
|
134
|
+
model = get_config_value(self.config, 'model.triage', None)
|
|
135
|
+
|
|
136
|
+
# Make sure all tools are registered with the factory first
|
|
137
|
+
try:
|
|
138
|
+
from ..tools import (
|
|
139
|
+
anon_coder_tool,
|
|
140
|
+
llm_serializer_tool,
|
|
141
|
+
brave_web_search_tool,
|
|
142
|
+
ripgrep_tool,
|
|
143
|
+
aider_tool,
|
|
144
|
+
load_external_tools
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Register all tools explicitly
|
|
148
|
+
self.factory.register_tool(anon_coder_tool)
|
|
149
|
+
self.factory.register_tool(llm_serializer_tool)
|
|
150
|
+
self.factory.register_tool(brave_web_search_tool)
|
|
151
|
+
self.factory.register_tool(ripgrep_tool)
|
|
152
|
+
self.factory.register_tool(aider_tool)
|
|
153
|
+
|
|
154
|
+
# Add external tools
|
|
155
|
+
external_tools = load_external_tools()
|
|
156
|
+
for tool in external_tools:
|
|
157
|
+
self.factory.register_tool(tool)
|
|
158
|
+
|
|
159
|
+
logger.info(f"Registered {len(self.factory.list_tools())} tools with factory")
|
|
160
|
+
except Exception as e:
|
|
161
|
+
logger.error(f"Error registering tools: {str(e)}")
|
|
162
|
+
|
|
163
|
+
# Create a specialized agent with all tools for triage using the factory
|
|
164
|
+
triage_agent = self.factory.create_agent(
|
|
165
|
+
tools=list(self.factory.list_tools().values()),
|
|
166
|
+
model=model
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Manually set max_retries
|
|
170
|
+
triage_agent.max_retries = max_retries
|
|
171
|
+
|
|
172
|
+
# Register the triage agent
|
|
173
|
+
triage_agent.name = "triage_agent"
|
|
174
|
+
triage_agent.description = "Analyzes queries and delegates to specialized agents"
|
|
175
|
+
self.agents["triage"] = triage_agent
|
|
176
|
+
|
|
177
|
+
logger.info("Triage agent created")
|
|
178
|
+
|
|
179
|
+
def _generate_task_id(self) -> str:
|
|
180
|
+
"""
|
|
181
|
+
Generate a unique task ID.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
A unique task ID string
|
|
185
|
+
"""
|
|
186
|
+
with self.lock:
|
|
187
|
+
task_id = f"task_{self.next_task_id}"
|
|
188
|
+
self.next_task_id += 1
|
|
189
|
+
return task_id
|
|
190
|
+
|
|
191
|
+
def _generate_agent_id(self) -> str:
|
|
192
|
+
"""
|
|
193
|
+
Generate a unique agent ID.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
A unique agent ID string
|
|
197
|
+
"""
|
|
198
|
+
with self.lock:
|
|
199
|
+
agent_id = f"agent_{self.next_agent_id}"
|
|
200
|
+
self.next_agent_id += 1
|
|
201
|
+
return agent_id
|
|
202
|
+
|
|
203
|
+
def submit_task(self, description: str, need_permission: bool = True, execution_mode: str = "standard", max_iterations: int = 5) -> str:
|
|
204
|
+
"""
|
|
205
|
+
Submit a new task to the orchestrator.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
description: Natural language description of the task
|
|
209
|
+
need_permission: Whether to ask for permission to create new tools
|
|
210
|
+
execution_mode: Execution mode to use ("standard", "riv")
|
|
211
|
+
max_iterations: Maximum number of iterations for iterative execution modes
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Task ID for tracking
|
|
215
|
+
"""
|
|
216
|
+
task_id = self._generate_task_id()
|
|
217
|
+
self.tasks[task_id] = TaskStatus(
|
|
218
|
+
task_id=task_id,
|
|
219
|
+
description=description
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
log_section_header("New Task Submission")
|
|
223
|
+
log_step(
|
|
224
|
+
step_number=1,
|
|
225
|
+
title="Task Initialization",
|
|
226
|
+
details={
|
|
227
|
+
"task_id": task_id,
|
|
228
|
+
"description": description[:100] + "..." if len(description) > 100 else description,
|
|
229
|
+
"need_permission": need_permission,
|
|
230
|
+
"execution_mode": execution_mode,
|
|
231
|
+
"max_iterations": max_iterations if execution_mode != "standard" else None,
|
|
232
|
+
"timestamp": time.time()
|
|
233
|
+
},
|
|
234
|
+
reasoning="Initializing new task with provided description"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# Process the task based on the execution approach
|
|
238
|
+
if execution_mode == "riv":
|
|
239
|
+
# Use RIV (Reflect-Improve-Verify) execution approach
|
|
240
|
+
try:
|
|
241
|
+
threading.Thread(
|
|
242
|
+
target=self.execute_riv_task,
|
|
243
|
+
args=(task_id, max_iterations)
|
|
244
|
+
).start()
|
|
245
|
+
|
|
246
|
+
log_step(
|
|
247
|
+
step_number=2,
|
|
248
|
+
title="Execution Approach",
|
|
249
|
+
details={
|
|
250
|
+
"approach": "RIV (Reflect-Improve-Verify)",
|
|
251
|
+
"max_iterations": max_iterations
|
|
252
|
+
},
|
|
253
|
+
reasoning="Starting RIV execution in background thread"
|
|
254
|
+
)
|
|
255
|
+
except Exception as e:
|
|
256
|
+
logger.error(f"Error starting RIV execution: {str(e)}")
|
|
257
|
+
# Fall back to standard execution
|
|
258
|
+
self._process_task(task_id, need_permission)
|
|
259
|
+
else:
|
|
260
|
+
# Use standard one-pass execution approach
|
|
261
|
+
self._process_task(task_id, need_permission)
|
|
262
|
+
|
|
263
|
+
return task_id
|
|
264
|
+
|
|
265
|
+
def _process_task(self, task_id: str, need_permission: bool) -> None:
|
|
266
|
+
"""
|
|
267
|
+
Process a task by triaging and delegating to appropriate agents.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
task_id: ID of the task to process
|
|
271
|
+
need_permission: Whether to require permission for new tools
|
|
272
|
+
"""
|
|
273
|
+
task = self.tasks[task_id]
|
|
274
|
+
task.status = "in_progress"
|
|
275
|
+
task.started_at = time.time()
|
|
276
|
+
|
|
277
|
+
log_section_header("Task Processing")
|
|
278
|
+
log_step(
|
|
279
|
+
step_number=2,
|
|
280
|
+
title="Task Status Update",
|
|
281
|
+
details={
|
|
282
|
+
"task_id": task_id,
|
|
283
|
+
"status": task.status,
|
|
284
|
+
"started_at": task.started_at
|
|
285
|
+
},
|
|
286
|
+
reasoning="Starting task processing with initial status set to in_progress"
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
# First, use the triage agent to analyze the task
|
|
291
|
+
try:
|
|
292
|
+
log_section_header("Task Analysis")
|
|
293
|
+
log_step(
|
|
294
|
+
step_number=3,
|
|
295
|
+
title="Triage Process",
|
|
296
|
+
details={
|
|
297
|
+
"task_id": task_id,
|
|
298
|
+
"timestamp": time.time()
|
|
299
|
+
},
|
|
300
|
+
reasoning="Starting task analysis to determine best handling approach"
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
triage_result = self._triage_task(task)
|
|
304
|
+
|
|
305
|
+
log_step(
|
|
306
|
+
step_number=4,
|
|
307
|
+
title="Triage Results",
|
|
308
|
+
details={
|
|
309
|
+
"assessment": triage_result.get("assessment", "unknown"),
|
|
310
|
+
"requires_new_agent": triage_result.get("requires_new_agent", False),
|
|
311
|
+
"duration": time.time() - task.started_at
|
|
312
|
+
},
|
|
313
|
+
reasoning=f"Task analysis completed with assessment: {triage_result.get('assessment', 'unknown')}"
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
except Exception as triage_error:
|
|
317
|
+
log_section_header("Triage Error")
|
|
318
|
+
log_step(
|
|
319
|
+
step_number=5,
|
|
320
|
+
title="Error Handling",
|
|
321
|
+
details={
|
|
322
|
+
"error": str(triage_error),
|
|
323
|
+
"error_type": type(triage_error).__name__,
|
|
324
|
+
"duration": time.time() - task.started_at
|
|
325
|
+
},
|
|
326
|
+
reasoning=f"Triage process failed due to: {str(triage_error)}"
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# Capture detailed error information about retry attempts
|
|
330
|
+
if hasattr(triage_error, 'history') and triage_error.history:
|
|
331
|
+
attempts_info = []
|
|
332
|
+
for i, entry in enumerate(triage_error.history, 1):
|
|
333
|
+
if isinstance(entry, dict):
|
|
334
|
+
attempts_info.append(f"Attempt {i}: {entry.get('error', 'Unknown error')}")
|
|
335
|
+
|
|
336
|
+
if attempts_info:
|
|
337
|
+
task.error = "\n".join(attempts_info)
|
|
338
|
+
else:
|
|
339
|
+
task.error = f"Triage failed after multiple attempts: {str(triage_error)}"
|
|
340
|
+
else:
|
|
341
|
+
task.error = f"Triage error: {str(triage_error)}"
|
|
342
|
+
|
|
343
|
+
# Use a default triage result
|
|
344
|
+
triage_result = {
|
|
345
|
+
"assessment": "direct",
|
|
346
|
+
"requires_new_agent": False,
|
|
347
|
+
"reasoning": f"Triage failed: {str(triage_error)}, falling back to direct handling"
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
log_step(
|
|
351
|
+
step_number=6,
|
|
352
|
+
title="STEP 1: Initial Triage Analysis",
|
|
353
|
+
details={
|
|
354
|
+
"assessment": triage_result.get("assessment"),
|
|
355
|
+
"requires_new_agent": triage_result.get("requires_new_agent", False),
|
|
356
|
+
"has_tool": "tool" in triage_result,
|
|
357
|
+
"has_arguments": "arguments" in triage_result,
|
|
358
|
+
"has_tool_sequence": "tool_sequence" in triage_result
|
|
359
|
+
},
|
|
360
|
+
reasoning="Analyzing initial triage results"
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# Check if triage_result contains a multi-step tool sequence
|
|
364
|
+
if "tool_sequence" in triage_result:
|
|
365
|
+
steps = triage_result["tool_sequence"]
|
|
366
|
+
# Make sure it's a list of steps
|
|
367
|
+
if not isinstance(steps, list):
|
|
368
|
+
raise ValueError("tool_sequence must be a list of tool-call steps.")
|
|
369
|
+
|
|
370
|
+
log_section_header("Multi-Step Tool Sequence Execution")
|
|
371
|
+
|
|
372
|
+
aggregated_results = []
|
|
373
|
+
for i, step_info in enumerate(steps, start=1):
|
|
374
|
+
tool_name = step_info.get("tool")
|
|
375
|
+
arguments = step_info.get("arguments", {})
|
|
376
|
+
|
|
377
|
+
if not tool_name:
|
|
378
|
+
raise ValueError(f"Missing 'tool' field in step {i} of tool_sequence.")
|
|
379
|
+
|
|
380
|
+
log_step(
|
|
381
|
+
step_number=i + 6,
|
|
382
|
+
title=f"Tool Sequence Step {i}",
|
|
383
|
+
details={
|
|
384
|
+
"tool_name": tool_name,
|
|
385
|
+
"arguments": arguments
|
|
386
|
+
},
|
|
387
|
+
reasoning=f"Executing step {i} in the multi-tool plan."
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
try:
|
|
391
|
+
# Execute the tool via the factory
|
|
392
|
+
result = self.factory.execute_tool(tool_name, **arguments)
|
|
393
|
+
aggregated_results.append({
|
|
394
|
+
"step": i,
|
|
395
|
+
"tool": tool_name,
|
|
396
|
+
"arguments": arguments,
|
|
397
|
+
"result": result
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
log_step(
|
|
401
|
+
step_number=i + 6,
|
|
402
|
+
title=f"Tool Execution Result for Step {i}",
|
|
403
|
+
details={
|
|
404
|
+
"tool": tool_name,
|
|
405
|
+
"result": result
|
|
406
|
+
},
|
|
407
|
+
reasoning=f"Step {i} completed successfully"
|
|
408
|
+
)
|
|
409
|
+
except Exception as e:
|
|
410
|
+
error_msg = f"Error executing tool {tool_name} in sequence step {i}: {str(e)}"
|
|
411
|
+
log_step(
|
|
412
|
+
step_number=i + 6,
|
|
413
|
+
title=f"Tool Execution Error for Step {i}",
|
|
414
|
+
details={
|
|
415
|
+
"tool": tool_name,
|
|
416
|
+
"error": str(e),
|
|
417
|
+
"error_type": type(e).__name__
|
|
418
|
+
},
|
|
419
|
+
reasoning=f"Step {i} failed: {str(e)}"
|
|
420
|
+
)
|
|
421
|
+
# Add the error to the results and continue with the next step
|
|
422
|
+
aggregated_results.append({
|
|
423
|
+
"step": i,
|
|
424
|
+
"tool": tool_name,
|
|
425
|
+
"arguments": arguments,
|
|
426
|
+
"error": str(e)
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
task.result = {
|
|
430
|
+
"type": "tool_sequence",
|
|
431
|
+
"steps": aggregated_results,
|
|
432
|
+
"reasoning": triage_result.get("reasoning", "Multi-step tool execution")
|
|
433
|
+
}
|
|
434
|
+
task.status = "completed"
|
|
435
|
+
task.completed_at = time.time()
|
|
436
|
+
|
|
437
|
+
log_section_header("Tool Sequence Completion")
|
|
438
|
+
log_step(
|
|
439
|
+
step_number=7 + len(steps),
|
|
440
|
+
title="Sequence Completion",
|
|
441
|
+
details={
|
|
442
|
+
"total_steps": len(steps),
|
|
443
|
+
"successful_steps": sum(1 for r in aggregated_results if "error" not in r),
|
|
444
|
+
"failed_steps": sum(1 for r in aggregated_results if "error" in r),
|
|
445
|
+
"duration": time.time() - task.started_at
|
|
446
|
+
},
|
|
447
|
+
reasoning="Tool sequence execution completed"
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
return
|
|
451
|
+
|
|
452
|
+
# Check if triage_result is actually a direct tool call
|
|
453
|
+
if "tool" in triage_result and "arguments" in triage_result:
|
|
454
|
+
log_section_header("Direct Tool Execution")
|
|
455
|
+
log_step(
|
|
456
|
+
step_number=8,
|
|
457
|
+
title="Tool Selection",
|
|
458
|
+
details={
|
|
459
|
+
"tool": triage_result["tool"],
|
|
460
|
+
"arguments": triage_result["arguments"]
|
|
461
|
+
},
|
|
462
|
+
reasoning=f"Task can be handled directly with tool: {triage_result['tool']}"
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
try:
|
|
466
|
+
result = self.factory.execute_tool(triage_result["tool"], **triage_result["arguments"])
|
|
467
|
+
task.result = result
|
|
468
|
+
task.status = "completed"
|
|
469
|
+
|
|
470
|
+
log_step(
|
|
471
|
+
step_number=9,
|
|
472
|
+
title="Tool Execution Results",
|
|
473
|
+
details={
|
|
474
|
+
"tool": triage_result["tool"],
|
|
475
|
+
"status": task.status,
|
|
476
|
+
"result": result,
|
|
477
|
+
"duration": time.time() - task.started_at
|
|
478
|
+
},
|
|
479
|
+
reasoning=f"Tool execution completed successfully with result: {result}"
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
task.completed_at = time.time()
|
|
483
|
+
return
|
|
484
|
+
except Exception as e:
|
|
485
|
+
log_section_header("Tool Execution Error")
|
|
486
|
+
log_step(
|
|
487
|
+
step_number=10,
|
|
488
|
+
title="Error Handling",
|
|
489
|
+
details={
|
|
490
|
+
"tool": triage_result["tool"],
|
|
491
|
+
"error": str(e),
|
|
492
|
+
"error_type": type(e).__name__
|
|
493
|
+
},
|
|
494
|
+
reasoning=f"Tool execution failed due to: {str(e)}"
|
|
495
|
+
)
|
|
496
|
+
logger.error(f"Error executing tool {triage_result['tool']} directly: {str(e)}")
|
|
497
|
+
|
|
498
|
+
if triage_result.get("requires_new_agent", False):
|
|
499
|
+
log_section_header("New Agent Creation")
|
|
500
|
+
log_step(
|
|
501
|
+
step_number=11,
|
|
502
|
+
title="Agent Creation Decision",
|
|
503
|
+
details={"need_permission": need_permission},
|
|
504
|
+
reasoning="Task complexity requires creation of a new specialized agent"
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
if need_permission:
|
|
508
|
+
agent_result = self.factory.create_agent_from_requirement(
|
|
509
|
+
requirement=task.description,
|
|
510
|
+
ask_permission=True
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
if agent_result.get("requires_permission", False):
|
|
514
|
+
log_step(
|
|
515
|
+
step_number=12,
|
|
516
|
+
title="Permission Required",
|
|
517
|
+
details={
|
|
518
|
+
"new_tools_needed": agent_result.get("analysis", {}).get("new_tools_needed", []),
|
|
519
|
+
"message": "This task requires creating new tools. Please run again with permission."
|
|
520
|
+
},
|
|
521
|
+
reasoning="Task requires new tools that need user permission to create"
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
task.result = {
|
|
525
|
+
"requires_permission": True,
|
|
526
|
+
"new_tools_needed": agent_result.get("analysis", {}).get("new_tools_needed", []),
|
|
527
|
+
"message": "This task requires creating new tools. Please run again with permission."
|
|
528
|
+
}
|
|
529
|
+
task.status = "needs_permission"
|
|
530
|
+
return
|
|
531
|
+
|
|
532
|
+
log_step(
|
|
533
|
+
step_number=13,
|
|
534
|
+
title="Creating New Agent",
|
|
535
|
+
details={"requirement": task.description},
|
|
536
|
+
reasoning="Creating new specialized agent to handle task requirements"
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
agent_result = self.factory.create_agent_from_requirement(
|
|
540
|
+
requirement=task.description,
|
|
541
|
+
ask_permission=False
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
if agent_result["success"]:
|
|
545
|
+
agent_id = self._generate_agent_id()
|
|
546
|
+
self.agents[agent_id] = agent_result["agent"]
|
|
547
|
+
|
|
548
|
+
log_step(
|
|
549
|
+
step_number=14,
|
|
550
|
+
title="New Agent Created",
|
|
551
|
+
details={
|
|
552
|
+
"agent_id": agent_id,
|
|
553
|
+
"agent_type": type(agent_result["agent"]).__name__
|
|
554
|
+
},
|
|
555
|
+
reasoning=f"Successfully created new agent of type: {type(agent_result['agent']).__name__}"
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
result = self._execute_with_agent(agent_id, task)
|
|
559
|
+
task.result = result
|
|
560
|
+
task.status = "completed"
|
|
561
|
+
|
|
562
|
+
log_step(
|
|
563
|
+
step_number=15,
|
|
564
|
+
title="New Agent Task Completion",
|
|
565
|
+
details={
|
|
566
|
+
"agent_id": agent_id,
|
|
567
|
+
"status": task.status,
|
|
568
|
+
"duration": time.time() - task.started_at
|
|
569
|
+
},
|
|
570
|
+
reasoning=f"New agent successfully completed task with status: {task.status}"
|
|
571
|
+
)
|
|
572
|
+
else:
|
|
573
|
+
task.error = f"Failed to create specialized agent: {agent_result.get('error', 'Unknown error')}"
|
|
574
|
+
task.status = "failed"
|
|
575
|
+
|
|
576
|
+
log_section_header("Agent Creation Error")
|
|
577
|
+
log_step(
|
|
578
|
+
step_number=16,
|
|
579
|
+
title="Error Handling",
|
|
580
|
+
details={
|
|
581
|
+
"error": task.error,
|
|
582
|
+
"duration": time.time() - task.started_at
|
|
583
|
+
},
|
|
584
|
+
reasoning=f"Failed to create new agent due to: {task.error}"
|
|
585
|
+
)
|
|
586
|
+
else:
|
|
587
|
+
agent_id = triage_result.get("agent_id", "triage")
|
|
588
|
+
|
|
589
|
+
log_section_header("Existing Agent Usage")
|
|
590
|
+
log_step(
|
|
591
|
+
step_number=17,
|
|
592
|
+
title="Agent Selection",
|
|
593
|
+
details={"agent_id": agent_id},
|
|
594
|
+
reasoning=f"Using existing agent {agent_id} as determined by triage analysis"
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
result = self._execute_with_agent(agent_id, task)
|
|
598
|
+
task.result = result
|
|
599
|
+
task.status = "completed"
|
|
600
|
+
|
|
601
|
+
log_step(
|
|
602
|
+
step_number=18,
|
|
603
|
+
title="Task Completion",
|
|
604
|
+
details={
|
|
605
|
+
"agent_id": agent_id,
|
|
606
|
+
"status": task.status,
|
|
607
|
+
"duration": time.time() - task.started_at
|
|
608
|
+
},
|
|
609
|
+
reasoning=f"Existing agent {agent_id} completed task with status: {task.status}"
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
except Exception as e:
|
|
613
|
+
task.error = str(e)
|
|
614
|
+
task.status = "failed"
|
|
615
|
+
|
|
616
|
+
log_section_header("Task Failure")
|
|
617
|
+
log_step(
|
|
618
|
+
step_number=19,
|
|
619
|
+
title="Error Handling",
|
|
620
|
+
details={
|
|
621
|
+
"error": str(e),
|
|
622
|
+
"error_type": type(e).__name__,
|
|
623
|
+
"duration": time.time() - task.started_at
|
|
624
|
+
},
|
|
625
|
+
reasoning=f"Task failed due to unexpected error: {str(e)}"
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
finally:
|
|
629
|
+
task.completed_at = time.time()
|
|
630
|
+
|
|
631
|
+
log_section_header("Task Summary")
|
|
632
|
+
log_step(
|
|
633
|
+
step_number=20,
|
|
634
|
+
title="Final Status",
|
|
635
|
+
details={
|
|
636
|
+
"final_status": task.status,
|
|
637
|
+
"total_duration": time.time() - task.started_at,
|
|
638
|
+
"has_error": bool(task.error)
|
|
639
|
+
},
|
|
640
|
+
reasoning=f"Task processing completed with final status: {task.status}"
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
def _triage_task(self, task: TaskStatus) -> Dict[str, Any]:
|
|
644
|
+
"""
|
|
645
|
+
Use the triage agent to analyze a task and determine how to handle it.
|
|
646
|
+
|
|
647
|
+
Args:
|
|
648
|
+
task: Task to analyze
|
|
649
|
+
|
|
650
|
+
Returns:
|
|
651
|
+
Dict with triage results
|
|
652
|
+
|
|
653
|
+
Raises:
|
|
654
|
+
OrchestratorError: If triage fails and no fallback can be used
|
|
655
|
+
"""
|
|
656
|
+
# First, check if we can handle with existing tools
|
|
657
|
+
existing_tools_check = self.factory.can_handle_with_existing_tools(task.description)
|
|
658
|
+
|
|
659
|
+
if existing_tools_check.get("success", False):
|
|
660
|
+
analysis = existing_tools_check["analysis"]
|
|
661
|
+
if analysis.get("can_handle", False):
|
|
662
|
+
# We can handle with existing tools
|
|
663
|
+
logger.info(f"Task '{task.task_id}' can be handled with existing tools")
|
|
664
|
+
return {
|
|
665
|
+
"assessment": "direct",
|
|
666
|
+
"requires_new_agent": False,
|
|
667
|
+
"required_tools": analysis.get("required_tools", []),
|
|
668
|
+
"reasoning": analysis.get("reasoning", "Can be handled with existing tools")
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
# If we get here, either the check failed or we need more analysis
|
|
672
|
+
triage_agent = self.agents["triage"]
|
|
673
|
+
|
|
674
|
+
# Get list of dynamic agents
|
|
675
|
+
dynamic_agents = self.factory.list_dynamic_agents()
|
|
676
|
+
dynamic_agent_ids = list(dynamic_agents.keys())
|
|
677
|
+
|
|
678
|
+
# Create a prompt for the triage agent, including information from our tools check
|
|
679
|
+
reasoning_from_check = ""
|
|
680
|
+
if existing_tools_check.get("success", False):
|
|
681
|
+
analysis = existing_tools_check["analysis"]
|
|
682
|
+
missing = analysis.get("missing_capabilities", [])
|
|
683
|
+
if missing:
|
|
684
|
+
reasoning_from_check = f"Missing capabilities: {', '.join(missing)}"
|
|
685
|
+
|
|
686
|
+
# Add special handling for capability queries like "what can you do"
|
|
687
|
+
if "what can you do" in task.description.lower() or "capabilities" in task.description.lower() or "help me" in task.description.lower():
|
|
688
|
+
return {
|
|
689
|
+
"assessment": "direct",
|
|
690
|
+
"requires_new_agent": False,
|
|
691
|
+
"reasoning": "Capability query detected. Responding with system capabilities information."
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
try:
|
|
695
|
+
triage_template = self.prompt_manager.load_template("workflows/triage.md")
|
|
696
|
+
triage_prompt = self.prompt_manager.process_template(
|
|
697
|
+
triage_template,
|
|
698
|
+
{
|
|
699
|
+
"query": task.description,
|
|
700
|
+
"tools": ', '.join(self.factory.list_tools().keys()),
|
|
701
|
+
"agents": ', '.join(list(self.agents.keys()) + dynamic_agent_ids),
|
|
702
|
+
"reasoning": reasoning_from_check,
|
|
703
|
+
},
|
|
704
|
+
)
|
|
705
|
+
logger.debug("Using PromptManager to generate triage prompt")
|
|
706
|
+
except Exception as e:
|
|
707
|
+
logger.error(f"Failed to load or process triage prompt template: {str(e)}")
|
|
708
|
+
raise RuntimeError(
|
|
709
|
+
"Critical: triage prompt template missing or invalid. "
|
|
710
|
+
"Please ensure 'prompts/workflows/triage.md' exists and is correct."
|
|
711
|
+
) from e
|
|
712
|
+
|
|
713
|
+
# Get triage analysis - let the Agent.run method handle retries internally
|
|
714
|
+
try:
|
|
715
|
+
# Wrap the entire execution in an exception handler to catch tool execution errors
|
|
716
|
+
try:
|
|
717
|
+
result = triage_agent.run(triage_prompt)
|
|
718
|
+
except Exception as tool_error:
|
|
719
|
+
# If a tool execution fails, still return a valid assessment
|
|
720
|
+
logger.error(f"Tool execution failed during triage: {str(tool_error)}")
|
|
721
|
+
return {
|
|
722
|
+
"assessment": "direct",
|
|
723
|
+
"requires_new_agent": False,
|
|
724
|
+
"reasoning": f"Tool execution error: {str(tool_error)}"
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
# The Agent.run method should have already tried to parse and retry
|
|
728
|
+
# up to max_retries times, and returned a fallback if all failed.
|
|
729
|
+
# Here we just need to do a final check and parsing.
|
|
730
|
+
|
|
731
|
+
# First check if we already got a dictionary (already parsed)
|
|
732
|
+
if isinstance(result, dict):
|
|
733
|
+
# Special case: If we got a chat tool with empty arguments, convert to assessment format
|
|
734
|
+
if "tool" in result and result.get("tool") == "chat":
|
|
735
|
+
logger.info(f"Triage returned chat tool format for task {task.task_id}, converting to assessment")
|
|
736
|
+
# If arguments.message exists, use that for reasoning
|
|
737
|
+
reasoning = "Chat tool returned by triage agent, converting to direct assessment"
|
|
738
|
+
if "arguments" in result and isinstance(result["arguments"], dict):
|
|
739
|
+
if "message" in result["arguments"] and result["arguments"]["message"]:
|
|
740
|
+
reasoning = result["arguments"]["message"]
|
|
741
|
+
|
|
742
|
+
return {
|
|
743
|
+
"assessment": "direct",
|
|
744
|
+
"requires_new_agent": False,
|
|
745
|
+
"reasoning": reasoning
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
# Otherwise try to parse the JSON result
|
|
749
|
+
if isinstance(result, str):
|
|
750
|
+
# Strategy 1: Try to find JSON object using regex
|
|
751
|
+
json_match = re.search(r'({[\s\S]*})', result)
|
|
752
|
+
if json_match:
|
|
753
|
+
try:
|
|
754
|
+
parsed_result = json.loads(json_match.group(1))
|
|
755
|
+
print("\n[Parsed JSON from regex extraction]:")
|
|
756
|
+
pprint.pprint(parsed_result)
|
|
757
|
+
return parsed_result
|
|
758
|
+
except json.JSONDecodeError:
|
|
759
|
+
pass
|
|
760
|
+
|
|
761
|
+
# Strategy 2: Try parsing entire content as JSON
|
|
762
|
+
try:
|
|
763
|
+
parsed_result = json.loads(result)
|
|
764
|
+
print("\n[Parsed JSON from direct parse]:")
|
|
765
|
+
pprint.pprint(parsed_result)
|
|
766
|
+
return parsed_result
|
|
767
|
+
except json.JSONDecodeError:
|
|
768
|
+
# Check if this looks like a chat response rather than a JSON response
|
|
769
|
+
if isinstance(result, str) and len(result.strip()) > 0 and "{" not in result and "}" not in result:
|
|
770
|
+
logger.warning(f"Triage returned chat response instead of JSON format: {result[:100]}...")
|
|
771
|
+
# Extract any assessment-like keywords to make a best guess
|
|
772
|
+
assessment = "direct" # Default
|
|
773
|
+
if re.search(r'\b(new agent|specialized|create agent)\b', result, re.IGNORECASE):
|
|
774
|
+
assessment = "create_new"
|
|
775
|
+
|
|
776
|
+
default_result = {
|
|
777
|
+
"assessment": assessment,
|
|
778
|
+
"requires_new_agent": assessment == "create_new",
|
|
779
|
+
"reasoning": f"Inferred from chat response: {result[:100]}...",
|
|
780
|
+
"original_response": result[:500] # Store original for debugging
|
|
781
|
+
}
|
|
782
|
+
else:
|
|
783
|
+
# Standard JSON parsing failure
|
|
784
|
+
logger.error(f"Failed to parse triage result for task {task.task_id}: {result[:100]}...")
|
|
785
|
+
default_result = {
|
|
786
|
+
"assessment": "direct",
|
|
787
|
+
"requires_new_agent": False,
|
|
788
|
+
"reasoning": "Could not parse triage result, falling back to direct handling"
|
|
789
|
+
}
|
|
790
|
+
return default_result
|
|
791
|
+
else:
|
|
792
|
+
# If not a string, it's probably already a structured result
|
|
793
|
+
return result
|
|
794
|
+
|
|
795
|
+
except Exception as e:
|
|
796
|
+
# Fallback on error
|
|
797
|
+
logger.error(f"Error in triage for task {task.task_id}: {str(e)}")
|
|
798
|
+
fallback_result = {
|
|
799
|
+
"assessment": "direct",
|
|
800
|
+
"requires_new_agent": False,
|
|
801
|
+
"reasoning": f"Triage error: {str(e)}, falling back to direct handling"
|
|
802
|
+
}
|
|
803
|
+
return fallback_result
|
|
804
|
+
|
|
805
|
+
def _execute_with_agent(self, agent_id: str, task: TaskStatus) -> Any:
|
|
806
|
+
"""
|
|
807
|
+
Execute a task using the specified agent.
|
|
808
|
+
|
|
809
|
+
Args:
|
|
810
|
+
agent_id: ID of the agent to use
|
|
811
|
+
task: Task to execute
|
|
812
|
+
|
|
813
|
+
Returns:
|
|
814
|
+
Result from the agent
|
|
815
|
+
|
|
816
|
+
Raises:
|
|
817
|
+
AgentNotFoundError: If the agent is not found
|
|
818
|
+
"""
|
|
819
|
+
# First check if it's a dynamic agent
|
|
820
|
+
agent = self.factory.get_dynamic_agent(agent_id)
|
|
821
|
+
|
|
822
|
+
# If not found in dynamic agents, check regular agents
|
|
823
|
+
if agent is None:
|
|
824
|
+
agent = self.agents.get(agent_id)
|
|
825
|
+
|
|
826
|
+
if agent is None:
|
|
827
|
+
error_msg = f"Agent with ID '{agent_id}' not found"
|
|
828
|
+
logger.error(error_msg)
|
|
829
|
+
raise AgentNotFoundError(error_msg)
|
|
830
|
+
|
|
831
|
+
task.assigned_agent = agent_id
|
|
832
|
+
logger.info(f"Executing task {task.task_id} with agent {agent_id}")
|
|
833
|
+
|
|
834
|
+
# Execute task
|
|
835
|
+
result = agent.run(task.description)
|
|
836
|
+
return result
|
|
837
|
+
|
|
838
|
+
def get_task_status(self, task_id: str) -> Optional[TaskStatus]:
|
|
839
|
+
"""
|
|
840
|
+
Get the current status of a task.
|
|
841
|
+
|
|
842
|
+
Args:
|
|
843
|
+
task_id: ID of the task to check
|
|
844
|
+
|
|
845
|
+
Returns:
|
|
846
|
+
TaskStatus or None if not found
|
|
847
|
+
"""
|
|
848
|
+
return self.tasks.get(task_id)
|
|
849
|
+
|
|
850
|
+
def grant_permission(self, task_id: str) -> None:
|
|
851
|
+
"""
|
|
852
|
+
Grant permission for a task that needs it.
|
|
853
|
+
|
|
854
|
+
Args:
|
|
855
|
+
task_id: ID of the task that needs permission
|
|
856
|
+
|
|
857
|
+
Raises:
|
|
858
|
+
ValueError: If the task is not found or does not need permission
|
|
859
|
+
"""
|
|
860
|
+
task = self.tasks.get(task_id)
|
|
861
|
+
if not task:
|
|
862
|
+
error_msg = f"Task {task_id} not found"
|
|
863
|
+
logger.error(error_msg)
|
|
864
|
+
raise ValueError(error_msg)
|
|
865
|
+
|
|
866
|
+
if task.status != "needs_permission":
|
|
867
|
+
error_msg = f"Task {task_id} does not need permission (status: {task.status})"
|
|
868
|
+
logger.error(error_msg)
|
|
869
|
+
raise ValueError(error_msg)
|
|
870
|
+
|
|
871
|
+
# Reprocess the task with permission granted
|
|
872
|
+
task.status = "pending"
|
|
873
|
+
logger.info(f"Permission granted for task {task_id}")
|
|
874
|
+
self._process_task(task_id, need_permission=False)
|
|
875
|
+
|
|
876
|
+
def list_agents(self) -> Dict[str, Dict[str, Any]]:
|
|
877
|
+
"""
|
|
878
|
+
List all registered agents.
|
|
879
|
+
|
|
880
|
+
Returns:
|
|
881
|
+
Dictionary of agent IDs to metadata
|
|
882
|
+
"""
|
|
883
|
+
# Combine built-in agents and dynamic agents
|
|
884
|
+
all_agents = {}
|
|
885
|
+
|
|
886
|
+
# Add built-in agents
|
|
887
|
+
for agent_id, agent in self.agents.items():
|
|
888
|
+
all_agents[agent_id] = {
|
|
889
|
+
"name": getattr(agent, "name", agent_id),
|
|
890
|
+
"description": getattr(agent, "description", "No description"),
|
|
891
|
+
"dynamic": False
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
# Add dynamic agents
|
|
895
|
+
dynamic_agents = self.factory.list_dynamic_agents()
|
|
896
|
+
for agent_id, agent_info in dynamic_agents.items():
|
|
897
|
+
all_agents[agent_id] = {
|
|
898
|
+
**agent_info,
|
|
899
|
+
"dynamic": True
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
return all_agents
|
|
903
|
+
|
|
904
|
+
def list_tools(self) -> Dict[str, Any]:
|
|
905
|
+
"""
|
|
906
|
+
Return a dictionary of all registered tools from the factory.
|
|
907
|
+
|
|
908
|
+
Returns:
|
|
909
|
+
Dictionary of tool names to tool definitions/objects
|
|
910
|
+
"""
|
|
911
|
+
return self.factory.list_tools()
|
|
912
|
+
|
|
913
|
+
def execute_riv_task(self, task_id: str, max_iterations: int = 5) -> Dict[str, Any]:
|
|
914
|
+
"""
|
|
915
|
+
Execute a task using the RIV (Reflect, Improve, Verify) pattern.
|
|
916
|
+
|
|
917
|
+
This adaptive approach uses an iterative loop that:
|
|
918
|
+
1. Reflects on the current state
|
|
919
|
+
2. Improves with targeted actions
|
|
920
|
+
3. Verifies if the task is complete
|
|
921
|
+
|
|
922
|
+
Args:
|
|
923
|
+
task_id: ID of the task to execute
|
|
924
|
+
max_iterations: Maximum number of iterations to perform
|
|
925
|
+
|
|
926
|
+
Returns:
|
|
927
|
+
A dictionary containing the final result and execution history
|
|
928
|
+
"""
|
|
929
|
+
task = self.tasks.get(task_id)
|
|
930
|
+
if not task:
|
|
931
|
+
raise ValueError(f"Task {task_id} not found")
|
|
932
|
+
|
|
933
|
+
task.status = "in_progress"
|
|
934
|
+
task.started_at = time.time()
|
|
935
|
+
|
|
936
|
+
# Initialize execution memory
|
|
937
|
+
memory = {
|
|
938
|
+
"iterations": [],
|
|
939
|
+
"current_result": None,
|
|
940
|
+
"original_task": task.description
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
iteration = 0
|
|
944
|
+
is_complete = False
|
|
945
|
+
|
|
946
|
+
log_section_header("Starting RIV Task Execution")
|
|
947
|
+
log_step(
|
|
948
|
+
step_number=1,
|
|
949
|
+
title="Task Initialization",
|
|
950
|
+
details={
|
|
951
|
+
"task_id": task_id,
|
|
952
|
+
"description": task.description[:100] + "..." if len(task.description) > 100 else task.description,
|
|
953
|
+
"max_iterations": max_iterations
|
|
954
|
+
},
|
|
955
|
+
reasoning="Beginning RIV execution loop (Reflect-Improve-Verify)"
|
|
956
|
+
)
|
|
957
|
+
|
|
958
|
+
# Main RIV execution loop
|
|
959
|
+
while iteration < max_iterations and not is_complete:
|
|
960
|
+
iteration += 1
|
|
961
|
+
iteration_memory = {"iteration": iteration, "timestamp": time.time()}
|
|
962
|
+
|
|
963
|
+
log_section_header(f"RIV Iteration {iteration}")
|
|
964
|
+
|
|
965
|
+
try:
|
|
966
|
+
# ------- REFLECT phase -------
|
|
967
|
+
# Analyze current state and identify what's needed
|
|
968
|
+
log_step(
|
|
969
|
+
step_number=iteration*3-2,
|
|
970
|
+
title=f"REFLECT (Iteration {iteration})",
|
|
971
|
+
details={"current_state": "analyzing"},
|
|
972
|
+
reasoning="Analyzing current state and planning next steps"
|
|
973
|
+
)
|
|
974
|
+
|
|
975
|
+
reflection_prompt = self._build_reflection_prompt(task, memory)
|
|
976
|
+
triage_agent = self.agents["triage"]
|
|
977
|
+
reflection_result = triage_agent.run(reflection_prompt)
|
|
978
|
+
|
|
979
|
+
# Parse the reflection result
|
|
980
|
+
if isinstance(reflection_result, dict):
|
|
981
|
+
reflection = reflection_result
|
|
982
|
+
else:
|
|
983
|
+
# Try to parse as JSON
|
|
984
|
+
try:
|
|
985
|
+
json_match = re.search(r'({[\s\S]*})', reflection_result)
|
|
986
|
+
if json_match:
|
|
987
|
+
reflection = json.loads(json_match.group(1))
|
|
988
|
+
else:
|
|
989
|
+
reflection = json.loads(reflection_result)
|
|
990
|
+
except (json.JSONDecodeError, TypeError):
|
|
991
|
+
logger.error(f"Failed to parse reflection result: {reflection_result[:200]}...")
|
|
992
|
+
reflection = {
|
|
993
|
+
"status": "needs_improvement",
|
|
994
|
+
"action_plan": {"type": "retry", "reason": "Failed to parse previous result"}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
is_complete = reflection.get("status") == "complete"
|
|
998
|
+
iteration_memory["reflection"] = reflection
|
|
999
|
+
|
|
1000
|
+
log_step(
|
|
1001
|
+
step_number=iteration*3-1,
|
|
1002
|
+
title=f"Reflection Analysis (Iteration {iteration})",
|
|
1003
|
+
details={
|
|
1004
|
+
"status": reflection.get("status", "unknown"),
|
|
1005
|
+
"reasoning": reflection.get("reasoning", "No reasoning provided"),
|
|
1006
|
+
"is_complete": is_complete
|
|
1007
|
+
},
|
|
1008
|
+
reasoning="Reflecting on current progress and determining next actions"
|
|
1009
|
+
)
|
|
1010
|
+
|
|
1011
|
+
# If reflection says we're done, break the loop
|
|
1012
|
+
if is_complete:
|
|
1013
|
+
task.status = "completed"
|
|
1014
|
+
task.result = memory["current_result"]
|
|
1015
|
+
iteration_memory["status"] = "complete"
|
|
1016
|
+
memory["iterations"].append(iteration_memory)
|
|
1017
|
+
break
|
|
1018
|
+
|
|
1019
|
+
# ------- IMPROVE phase -------
|
|
1020
|
+
# Execute the planned action to improve the result
|
|
1021
|
+
action_plan = reflection.get("action_plan", {})
|
|
1022
|
+
action_type = action_plan.get("type", "unknown")
|
|
1023
|
+
|
|
1024
|
+
log_step(
|
|
1025
|
+
step_number=iteration*3,
|
|
1026
|
+
title=f"IMPROVE (Iteration {iteration})",
|
|
1027
|
+
details={
|
|
1028
|
+
"action_type": action_type,
|
|
1029
|
+
"details": action_plan
|
|
1030
|
+
},
|
|
1031
|
+
reasoning=f"Executing improvement action: {action_type}"
|
|
1032
|
+
)
|
|
1033
|
+
|
|
1034
|
+
action_result = None
|
|
1035
|
+
action_error = None
|
|
1036
|
+
|
|
1037
|
+
try:
|
|
1038
|
+
if action_type == "use_tool":
|
|
1039
|
+
# Execute a single tool
|
|
1040
|
+
tool_name = action_plan.get("tool")
|
|
1041
|
+
arguments = action_plan.get("arguments", {})
|
|
1042
|
+
|
|
1043
|
+
if not tool_name:
|
|
1044
|
+
raise ValueError("Missing tool name in action plan")
|
|
1045
|
+
|
|
1046
|
+
action_result = self.factory.execute_tool(tool_name, **arguments)
|
|
1047
|
+
|
|
1048
|
+
elif action_type == "use_tool_sequence":
|
|
1049
|
+
# Execute a sequence of tools
|
|
1050
|
+
steps = action_plan.get("tool_sequence", [])
|
|
1051
|
+
if not steps or not isinstance(steps, list):
|
|
1052
|
+
raise ValueError("Invalid or missing tool sequence")
|
|
1053
|
+
|
|
1054
|
+
sequence_results = []
|
|
1055
|
+
for i, step_info in enumerate(steps, start=1):
|
|
1056
|
+
tool_name = step_info.get("tool")
|
|
1057
|
+
arguments = step_info.get("arguments", {})
|
|
1058
|
+
|
|
1059
|
+
if not tool_name:
|
|
1060
|
+
raise ValueError(f"Missing tool name in sequence step {i}")
|
|
1061
|
+
|
|
1062
|
+
try:
|
|
1063
|
+
result = self.factory.execute_tool(tool_name, **arguments)
|
|
1064
|
+
sequence_results.append({
|
|
1065
|
+
"step": i,
|
|
1066
|
+
"tool": tool_name,
|
|
1067
|
+
"arguments": arguments,
|
|
1068
|
+
"result": result
|
|
1069
|
+
})
|
|
1070
|
+
except Exception as e:
|
|
1071
|
+
sequence_results.append({
|
|
1072
|
+
"step": i,
|
|
1073
|
+
"tool": tool_name,
|
|
1074
|
+
"arguments": arguments,
|
|
1075
|
+
"error": str(e)
|
|
1076
|
+
})
|
|
1077
|
+
|
|
1078
|
+
action_result = sequence_results
|
|
1079
|
+
|
|
1080
|
+
elif action_type == "use_agent":
|
|
1081
|
+
# Use a specific agent
|
|
1082
|
+
agent_id = action_plan.get("agent_id")
|
|
1083
|
+
if not agent_id:
|
|
1084
|
+
raise ValueError("Missing agent_id in action plan")
|
|
1085
|
+
|
|
1086
|
+
prompt = action_plan.get("prompt", task.description)
|
|
1087
|
+
action_result = self._execute_with_agent(agent_id, prompt)
|
|
1088
|
+
|
|
1089
|
+
elif action_type == "create_agent":
|
|
1090
|
+
# Create a new specialized agent
|
|
1091
|
+
requirement = action_plan.get("requirement", task.description)
|
|
1092
|
+
agent_result = self.factory.create_agent_from_requirement(
|
|
1093
|
+
requirement=requirement,
|
|
1094
|
+
ask_permission=False
|
|
1095
|
+
)
|
|
1096
|
+
|
|
1097
|
+
if agent_result["success"]:
|
|
1098
|
+
agent_id = self._generate_agent_id()
|
|
1099
|
+
self.agents[agent_id] = agent_result["agent"]
|
|
1100
|
+
prompt = action_plan.get("prompt", task.description)
|
|
1101
|
+
action_result = self._execute_with_agent(agent_id, prompt)
|
|
1102
|
+
else:
|
|
1103
|
+
raise ValueError(f"Failed to create agent: {agent_result.get('error')}")
|
|
1104
|
+
|
|
1105
|
+
elif action_type == "refine_result":
|
|
1106
|
+
# Refine the current result
|
|
1107
|
+
refinement_prompt = action_plan.get("prompt")
|
|
1108
|
+
if not refinement_prompt:
|
|
1109
|
+
raise ValueError("Missing refinement prompt in action plan")
|
|
1110
|
+
|
|
1111
|
+
action_result = triage_agent.run(refinement_prompt)
|
|
1112
|
+
|
|
1113
|
+
elif action_type == "retry":
|
|
1114
|
+
# Simply retry with the triage agent
|
|
1115
|
+
action_result = triage_agent.run(task.description)
|
|
1116
|
+
|
|
1117
|
+
else:
|
|
1118
|
+
raise ValueError(f"Unknown action type: {action_type}")
|
|
1119
|
+
|
|
1120
|
+
except Exception as e:
|
|
1121
|
+
action_error = str(e)
|
|
1122
|
+
logger.error(f"Error in IMPROVE phase (iteration {iteration}): {action_error}")
|
|
1123
|
+
|
|
1124
|
+
# Store the action result
|
|
1125
|
+
iteration_memory["action"] = {
|
|
1126
|
+
"type": action_type,
|
|
1127
|
+
"details": action_plan,
|
|
1128
|
+
"result": action_result,
|
|
1129
|
+
"error": action_error
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
# Update the current result
|
|
1133
|
+
if action_error is None:
|
|
1134
|
+
memory["current_result"] = action_result
|
|
1135
|
+
|
|
1136
|
+
# ------- VERIFY phase -------
|
|
1137
|
+
# Check if the result is satisfactory
|
|
1138
|
+
log_step(
|
|
1139
|
+
step_number=iteration*3+1,
|
|
1140
|
+
title=f"VERIFY (Iteration {iteration})",
|
|
1141
|
+
details={
|
|
1142
|
+
"has_result": action_result is not None,
|
|
1143
|
+
"has_error": action_error is not None
|
|
1144
|
+
},
|
|
1145
|
+
reasoning="Verifying result of improvement action"
|
|
1146
|
+
)
|
|
1147
|
+
|
|
1148
|
+
verify_prompt = self._build_verification_prompt(task, memory, action_result, action_error)
|
|
1149
|
+
verification_result = triage_agent.run(verify_prompt)
|
|
1150
|
+
|
|
1151
|
+
# Parse the verification result
|
|
1152
|
+
if isinstance(verification_result, dict):
|
|
1153
|
+
verification = verification_result
|
|
1154
|
+
else:
|
|
1155
|
+
try:
|
|
1156
|
+
json_match = re.search(r'({[\s\S]*})', verification_result)
|
|
1157
|
+
if json_match:
|
|
1158
|
+
verification = json.loads(json_match.group(1))
|
|
1159
|
+
else:
|
|
1160
|
+
verification = json.loads(verification_result)
|
|
1161
|
+
except (json.JSONDecodeError, TypeError):
|
|
1162
|
+
logger.error(f"Failed to parse verification result: {verification_result[:200]}...")
|
|
1163
|
+
verification = {
|
|
1164
|
+
"is_complete": False,
|
|
1165
|
+
"quality": "unknown",
|
|
1166
|
+
"reasoning": "Failed to parse verification result"
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
is_complete = verification.get("is_complete", False)
|
|
1170
|
+
iteration_memory["verification"] = verification
|
|
1171
|
+
|
|
1172
|
+
log_step(
|
|
1173
|
+
step_number=iteration*3+2,
|
|
1174
|
+
title=f"Verification Results (Iteration {iteration})",
|
|
1175
|
+
details={
|
|
1176
|
+
"is_complete": is_complete,
|
|
1177
|
+
"quality": verification.get("quality", "unknown"),
|
|
1178
|
+
"reasoning": verification.get("reasoning", "No reasoning provided")
|
|
1179
|
+
},
|
|
1180
|
+
reasoning="Determining if task is complete based on verification"
|
|
1181
|
+
)
|
|
1182
|
+
|
|
1183
|
+
# Add the iteration to memory
|
|
1184
|
+
memory["iterations"].append(iteration_memory)
|
|
1185
|
+
|
|
1186
|
+
# If verification says we're done, break the loop
|
|
1187
|
+
if is_complete:
|
|
1188
|
+
task.status = "completed"
|
|
1189
|
+
task.result = memory["current_result"]
|
|
1190
|
+
break
|
|
1191
|
+
|
|
1192
|
+
except Exception as e:
|
|
1193
|
+
# Handle any uncaught exceptions in the iteration
|
|
1194
|
+
logger.error(f"Error in RIV cycle {iteration}: {str(e)}")
|
|
1195
|
+
iteration_memory["error"] = str(e)
|
|
1196
|
+
memory["iterations"].append(iteration_memory)
|
|
1197
|
+
|
|
1198
|
+
if iteration == max_iterations:
|
|
1199
|
+
task.error = f"Failed after {max_iterations} iterations: {str(e)}"
|
|
1200
|
+
task.status = "failed"
|
|
1201
|
+
|
|
1202
|
+
# If we've hit max iterations without completing
|
|
1203
|
+
if iteration >= max_iterations and not is_complete:
|
|
1204
|
+
task.status = "failed" if task.status == "in_progress" else task.status
|
|
1205
|
+
task.error = f"Reached maximum iterations ({max_iterations}) without completing task"
|
|
1206
|
+
|
|
1207
|
+
log_section_header("Max Iterations Reached")
|
|
1208
|
+
log_step(
|
|
1209
|
+
step_number=iteration*3+3,
|
|
1210
|
+
title="Iteration Limit Reached",
|
|
1211
|
+
details={
|
|
1212
|
+
"max_iterations": max_iterations,
|
|
1213
|
+
"final_status": task.status
|
|
1214
|
+
},
|
|
1215
|
+
reasoning="Task execution stopped due to reaching maximum allowed iterations"
|
|
1216
|
+
)
|
|
1217
|
+
|
|
1218
|
+
# Update task with final results
|
|
1219
|
+
task.completed_at = time.time()
|
|
1220
|
+
final_result = {
|
|
1221
|
+
"execution_memory": memory,
|
|
1222
|
+
"iterations_completed": iteration,
|
|
1223
|
+
"task_completed": is_complete,
|
|
1224
|
+
"final_result": memory["current_result"]
|
|
1225
|
+
}
|
|
1226
|
+
task.result = final_result
|
|
1227
|
+
|
|
1228
|
+
log_section_header("RIV Execution Complete")
|
|
1229
|
+
log_step(
|
|
1230
|
+
step_number=iteration*3+4,
|
|
1231
|
+
title="Final Status",
|
|
1232
|
+
details={
|
|
1233
|
+
"task_id": task_id,
|
|
1234
|
+
"status": task.status,
|
|
1235
|
+
"iterations": iteration,
|
|
1236
|
+
"duration": task.completed_at - task.started_at
|
|
1237
|
+
},
|
|
1238
|
+
reasoning="RIV process completed with final status"
|
|
1239
|
+
)
|
|
1240
|
+
|
|
1241
|
+
return final_result
|
|
1242
|
+
|
|
1243
|
+
def _build_reflection_prompt(self, task: TaskStatus, memory: Dict[str, Any]) -> str:
|
|
1244
|
+
"""
|
|
1245
|
+
Build a prompt for the REFLECT phase to analyze current state and plan next steps.
|
|
1246
|
+
|
|
1247
|
+
Args:
|
|
1248
|
+
task: The task being executed
|
|
1249
|
+
memory: The execution memory so far
|
|
1250
|
+
|
|
1251
|
+
Returns:
|
|
1252
|
+
A prompt string for the LLM to analyze
|
|
1253
|
+
"""
|
|
1254
|
+
available_tools = ', '.join(self.factory.list_tools().keys())
|
|
1255
|
+
available_agents = ', '.join(self.agents.keys())
|
|
1256
|
+
|
|
1257
|
+
# Create a concise summary of previous iterations
|
|
1258
|
+
iterations_summary = ""
|
|
1259
|
+
if memory["iterations"]:
|
|
1260
|
+
for i, iteration in enumerate(memory["iterations"], start=1):
|
|
1261
|
+
iterations_summary += f"\nITERATION {i} SUMMARY:\n"
|
|
1262
|
+
|
|
1263
|
+
# Add reflection
|
|
1264
|
+
reflection = iteration.get("reflection", {})
|
|
1265
|
+
iterations_summary += f"- Status: {reflection.get('status', 'unknown')}\n"
|
|
1266
|
+
iterations_summary += f"- Reasoning: {reflection.get('reasoning', 'N/A')[:200]}...\n"
|
|
1267
|
+
|
|
1268
|
+
# Add action
|
|
1269
|
+
action = iteration.get("action", {})
|
|
1270
|
+
action_type = action.get("type", "unknown")
|
|
1271
|
+
iterations_summary += f"- Action: {action_type}\n"
|
|
1272
|
+
|
|
1273
|
+
if action_type == "use_tool":
|
|
1274
|
+
tool_name = action.get("details", {}).get("tool", "unknown")
|
|
1275
|
+
iterations_summary += f" - Tool used: {tool_name}\n"
|
|
1276
|
+
|
|
1277
|
+
# Add results summary
|
|
1278
|
+
result = action.get("result")
|
|
1279
|
+
if result is not None:
|
|
1280
|
+
if isinstance(result, str) and len(result) > 100:
|
|
1281
|
+
iterations_summary += f" - Result: {result[:100]}...(truncated)\n"
|
|
1282
|
+
elif isinstance(result, list) and len(result) > 0:
|
|
1283
|
+
iterations_summary += f" - Result: List with {len(result)} items\n"
|
|
1284
|
+
else:
|
|
1285
|
+
iterations_summary += f" - Result: Result obtained\n"
|
|
1286
|
+
|
|
1287
|
+
# Add verification
|
|
1288
|
+
verification = iteration.get("verification", {})
|
|
1289
|
+
iterations_summary += f"- Verification: {verification.get('quality', 'unknown')}\n"
|
|
1290
|
+
iterations_summary += f"- Complete: {verification.get('is_complete', False)}\n"
|
|
1291
|
+
if "missing_outputs" in verification:
|
|
1292
|
+
missing = verification.get("missing_outputs", [])
|
|
1293
|
+
if missing:
|
|
1294
|
+
iterations_summary += f"- Missing outputs: {', '.join(missing)}\n"
|
|
1295
|
+
|
|
1296
|
+
# Check for existing files in the current directory that might be related to the task
|
|
1297
|
+
cwd = os.getcwd()
|
|
1298
|
+
existing_files = []
|
|
1299
|
+
try:
|
|
1300
|
+
import glob
|
|
1301
|
+
# Look for files that might be related to the task
|
|
1302
|
+
for file_path in glob.glob(os.path.join(cwd, "*.md")) + glob.glob(os.path.join(cwd, "*.txt")):
|
|
1303
|
+
if os.path.isfile(file_path):
|
|
1304
|
+
rel_path = os.path.relpath(file_path, cwd)
|
|
1305
|
+
existing_files.append(rel_path)
|
|
1306
|
+
except Exception as e:
|
|
1307
|
+
logger.error(f"Error checking for existing files: {str(e)}")
|
|
1308
|
+
|
|
1309
|
+
files_info = ""
|
|
1310
|
+
if existing_files:
|
|
1311
|
+
files_info = "Files already in workspace:\n"
|
|
1312
|
+
for file_path in existing_files:
|
|
1313
|
+
files_info += f"- {file_path}\n"
|
|
1314
|
+
|
|
1315
|
+
current_result = memory["current_result"]
|
|
1316
|
+
result_summary = ""
|
|
1317
|
+
if current_result is not None:
|
|
1318
|
+
# Format and truncate the result for readability
|
|
1319
|
+
if isinstance(current_result, str):
|
|
1320
|
+
result_summary = current_result[:500] + "..." if len(current_result) > 500 else current_result
|
|
1321
|
+
else:
|
|
1322
|
+
try:
|
|
1323
|
+
result_summary = json.dumps(current_result, indent=2, default=str)
|
|
1324
|
+
if len(result_summary) > 500:
|
|
1325
|
+
result_summary = result_summary[:500] + "...(truncated)"
|
|
1326
|
+
except:
|
|
1327
|
+
result_summary = str(current_result)[:500] + "..." if len(str(current_result)) > 500 else str(current_result)
|
|
1328
|
+
|
|
1329
|
+
# Add tool details to make better recommendations
|
|
1330
|
+
tool_details = ""
|
|
1331
|
+
# Get information about all available tools
|
|
1332
|
+
useful_tools = list(self.factory.list_tools().keys())
|
|
1333
|
+
for tool_name in useful_tools:
|
|
1334
|
+
try:
|
|
1335
|
+
tool_info = self._get_tool_info(tool_name)
|
|
1336
|
+
if tool_info:
|
|
1337
|
+
tool_details += f"\n{tool_name}:\n"
|
|
1338
|
+
tool_details += f" Description: {tool_info.get('description', 'No description')}\n"
|
|
1339
|
+
params = tool_info.get("parameters", {})
|
|
1340
|
+
if params:
|
|
1341
|
+
tool_details += f" Parameters: {json.dumps(params, indent=2)[:200]}...\n"
|
|
1342
|
+
except:
|
|
1343
|
+
pass # Skip if tool not found
|
|
1344
|
+
|
|
1345
|
+
# First iteration vs subsequent iterations
|
|
1346
|
+
if not memory["iterations"]:
|
|
1347
|
+
# First iteration - no history yet
|
|
1348
|
+
try:
|
|
1349
|
+
template = self.prompt_manager.load_template("workflows/riv_reflect.md")
|
|
1350
|
+
prompt = self.prompt_manager.process_template(
|
|
1351
|
+
template,
|
|
1352
|
+
{
|
|
1353
|
+
"task_description": task.description,
|
|
1354
|
+
"available_tools": available_tools,
|
|
1355
|
+
"available_agents": available_agents,
|
|
1356
|
+
"iterations_summary": iterations_summary,
|
|
1357
|
+
"result_summary": result_summary,
|
|
1358
|
+
"files_info": files_info,
|
|
1359
|
+
"tool_details": tool_details,
|
|
1360
|
+
},
|
|
1361
|
+
)
|
|
1362
|
+
return prompt
|
|
1363
|
+
except Exception as e:
|
|
1364
|
+
logger.error(f"Failed to load or process RIV reflect prompt template: {str(e)}")
|
|
1365
|
+
raise RuntimeError(
|
|
1366
|
+
"Critical: RIV reflect prompt template missing or invalid. "
|
|
1367
|
+
"Please ensure 'prompts/workflows/riv_reflect.md' exists and is correct."
|
|
1368
|
+
) from e
|
|
1369
|
+
|
|
1370
|
+
def _get_tool_info(self, tool_name: str) -> Optional[Dict[str, Any]]:
|
|
1371
|
+
"""
|
|
1372
|
+
Get information about a specific tool.
|
|
1373
|
+
|
|
1374
|
+
Args:
|
|
1375
|
+
tool_name: Name of the tool to get information about
|
|
1376
|
+
|
|
1377
|
+
Returns:
|
|
1378
|
+
Dictionary with tool information or None if the tool is not found
|
|
1379
|
+
"""
|
|
1380
|
+
tools = self.factory.list_tools()
|
|
1381
|
+
tool = tools.get(tool_name)
|
|
1382
|
+
if not tool:
|
|
1383
|
+
return None
|
|
1384
|
+
|
|
1385
|
+
return {
|
|
1386
|
+
"name": tool_name,
|
|
1387
|
+
"description": getattr(tool, "description", "No description available"),
|
|
1388
|
+
"parameters": getattr(tool, "parameters", {})
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
def _build_verification_prompt(self, task: TaskStatus, memory: Dict[str, Any],
|
|
1392
|
+
action_result: Any, action_error: Optional[str]) -> str:
|
|
1393
|
+
"""
|
|
1394
|
+
Build a prompt for the VERIFY phase to check if the task is complete.
|
|
1395
|
+
|
|
1396
|
+
Args:
|
|
1397
|
+
task: The task being executed
|
|
1398
|
+
memory: The execution memory so far
|
|
1399
|
+
action_result: The result of the most recent action
|
|
1400
|
+
action_error: Any error that occurred during the action
|
|
1401
|
+
|
|
1402
|
+
Returns:
|
|
1403
|
+
A prompt string for the LLM to verify
|
|
1404
|
+
"""
|
|
1405
|
+
# Format action result for readability
|
|
1406
|
+
if action_result is not None:
|
|
1407
|
+
if isinstance(action_result, str):
|
|
1408
|
+
result_text = action_result[:1000] + "..." if len(action_result) > 1000 else action_result
|
|
1409
|
+
else:
|
|
1410
|
+
try:
|
|
1411
|
+
result_text = json.dumps(action_result, indent=2, default=str)
|
|
1412
|
+
if len(result_text) > 1000:
|
|
1413
|
+
result_text = result_text[:1000] + "...(truncated)"
|
|
1414
|
+
except:
|
|
1415
|
+
result_text = str(action_result)[:1000] + "..." if len(str(action_result)) > 1000 else str(action_result)
|
|
1416
|
+
else:
|
|
1417
|
+
result_text = "No result produced."
|
|
1418
|
+
|
|
1419
|
+
# Add error information if present
|
|
1420
|
+
error_text = f"ERROR: {action_error}" if action_error else "No errors."
|
|
1421
|
+
|
|
1422
|
+
# Create a summary of all previous iterations and results
|
|
1423
|
+
iteration_count = len(memory.get("iterations", []))
|
|
1424
|
+
history_summary = ""
|
|
1425
|
+
if iteration_count > 0:
|
|
1426
|
+
history_summary = f"This is iteration {iteration_count}. Previous steps include:\n"
|
|
1427
|
+
for i, iter_data in enumerate(memory.get("iterations", []), 1):
|
|
1428
|
+
action = iter_data.get("action", {})
|
|
1429
|
+
action_type = action.get("type", "unknown")
|
|
1430
|
+
|
|
1431
|
+
# Add specific details based on action type
|
|
1432
|
+
if action_type == "use_tool":
|
|
1433
|
+
tool_name = action.get("details", {}).get("tool", "unknown")
|
|
1434
|
+
history_summary += f"- Iteration {i}: Used tool '{tool_name}'\n"
|
|
1435
|
+
else:
|
|
1436
|
+
history_summary += f"- Iteration {i}: Performed {action_type}\n"
|
|
1437
|
+
|
|
1438
|
+
# Check for all files created or modified in the current workspace
|
|
1439
|
+
cwd = os.getcwd()
|
|
1440
|
+
files_created = []
|
|
1441
|
+
try:
|
|
1442
|
+
import glob
|
|
1443
|
+
for md_file in glob.glob(os.path.join(cwd, "*.md")):
|
|
1444
|
+
if os.path.isfile(md_file):
|
|
1445
|
+
rel_path = os.path.relpath(md_file, cwd)
|
|
1446
|
+
files_created.append(rel_path)
|
|
1447
|
+
except Exception as e:
|
|
1448
|
+
logger.error(f"Error checking for created files: {str(e)}")
|
|
1449
|
+
|
|
1450
|
+
files_info = ""
|
|
1451
|
+
if files_created:
|
|
1452
|
+
files_info = "Files found in workspace:\n"
|
|
1453
|
+
for file_path in files_created:
|
|
1454
|
+
files_info += f"- {file_path}\n"
|
|
1455
|
+
try:
|
|
1456
|
+
full_path = os.path.join(cwd, file_path)
|
|
1457
|
+
if os.path.getsize(full_path) < 5000:
|
|
1458
|
+
with open(full_path, 'r', encoding='utf-8') as f:
|
|
1459
|
+
content = f.read()
|
|
1460
|
+
files_info += f"Content of {file_path}:\n```\n{content}\n```\n"
|
|
1461
|
+
except Exception as e:
|
|
1462
|
+
files_info += f"(Error reading file: {str(e)})\n"
|
|
1463
|
+
else:
|
|
1464
|
+
files_info = "No files have been created or modified in the workspace."
|
|
1465
|
+
|
|
1466
|
+
# Format action result
|
|
1467
|
+
if action_result is not None:
|
|
1468
|
+
try:
|
|
1469
|
+
if isinstance(action_result, str):
|
|
1470
|
+
result_text = action_result[:1000] + "..." if len(action_result) > 1000 else action_result
|
|
1471
|
+
else:
|
|
1472
|
+
result_text = json.dumps(action_result, indent=2, default=str)
|
|
1473
|
+
if len(result_text) > 1000:
|
|
1474
|
+
result_text = result_text[:1000] + "...(truncated)"
|
|
1475
|
+
except:
|
|
1476
|
+
result_text = str(action_result)[:1000] + "..." if len(str(action_result)) > 1000 else str(action_result)
|
|
1477
|
+
else:
|
|
1478
|
+
result_text = "No result produced."
|
|
1479
|
+
|
|
1480
|
+
error_text = f"ERROR: {action_error}" if action_error else "No errors."
|
|
1481
|
+
|
|
1482
|
+
# Build history summary
|
|
1483
|
+
iteration_count = len(memory.get("iterations", []))
|
|
1484
|
+
history_summary = ""
|
|
1485
|
+
if iteration_count > 0:
|
|
1486
|
+
history_summary = f"This is iteration {iteration_count}. Previous steps include:\n"
|
|
1487
|
+
for i, iter_data in enumerate(memory.get("iterations", []), 1):
|
|
1488
|
+
action = iter_data.get("action", {})
|
|
1489
|
+
action_type = action.get("type", "unknown")
|
|
1490
|
+
if action_type == "use_tool":
|
|
1491
|
+
tool_name = action.get("details", {}).get("tool", "unknown")
|
|
1492
|
+
history_summary += f"- Iteration {i}: Used tool '{tool_name}'\n"
|
|
1493
|
+
else:
|
|
1494
|
+
history_summary += f"- Iteration {i}: Performed {action_type}\n"
|
|
1495
|
+
|
|
1496
|
+
try:
|
|
1497
|
+
template = self.prompt_manager.load_template("workflows/riv_verify.md")
|
|
1498
|
+
prompt = self.prompt_manager.process_template(
|
|
1499
|
+
template,
|
|
1500
|
+
{
|
|
1501
|
+
"task_description": task.description,
|
|
1502
|
+
"history_summary": history_summary,
|
|
1503
|
+
"result_text": result_text,
|
|
1504
|
+
"files_info": files_info,
|
|
1505
|
+
"error_text": error_text,
|
|
1506
|
+
},
|
|
1507
|
+
)
|
|
1508
|
+
return prompt
|
|
1509
|
+
except Exception as e:
|
|
1510
|
+
logger.error(f"Failed to load or process RIV verify prompt template: {str(e)}")
|
|
1511
|
+
raise RuntimeError(
|
|
1512
|
+
"Critical: RIV verify prompt template missing or invalid. "
|
|
1513
|
+
"Please ensure 'prompts/workflows/riv_verify.md' exists and is correct."
|
|
1514
|
+
) from e
|