amd-gaia 0.15.1__py3-none-any.whl → 0.15.3__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.
- {amd_gaia-0.15.1.dist-info → amd_gaia-0.15.3.dist-info}/METADATA +2 -2
- {amd_gaia-0.15.1.dist-info → amd_gaia-0.15.3.dist-info}/RECORD +38 -32
- {amd_gaia-0.15.1.dist-info → amd_gaia-0.15.3.dist-info}/WHEEL +1 -1
- gaia/agents/base/agent.py +317 -113
- gaia/agents/base/api_agent.py +0 -1
- gaia/agents/base/console.py +334 -9
- gaia/agents/base/tools.py +7 -2
- gaia/agents/blender/__init__.py +7 -0
- gaia/agents/blender/agent.py +7 -10
- gaia/agents/blender/core/view.py +2 -2
- gaia/agents/chat/agent.py +22 -48
- gaia/agents/chat/app.py +7 -0
- gaia/agents/chat/tools/rag_tools.py +23 -8
- gaia/agents/chat/tools/shell_tools.py +1 -0
- gaia/agents/code/prompts/code_patterns.py +2 -4
- gaia/agents/docker/agent.py +1 -0
- gaia/agents/emr/agent.py +3 -5
- gaia/agents/emr/cli.py +1 -1
- gaia/agents/emr/dashboard/server.py +2 -4
- gaia/agents/tools/__init__.py +11 -0
- gaia/agents/tools/file_tools.py +715 -0
- gaia/apps/llm/app.py +14 -3
- gaia/chat/app.py +2 -4
- gaia/cli.py +751 -333
- gaia/installer/__init__.py +23 -0
- gaia/installer/init_command.py +1605 -0
- gaia/installer/lemonade_installer.py +678 -0
- gaia/llm/__init__.py +2 -1
- gaia/llm/lemonade_client.py +427 -99
- gaia/llm/lemonade_manager.py +55 -11
- gaia/llm/providers/lemonade.py +21 -14
- gaia/rag/sdk.py +1 -1
- gaia/security.py +24 -4
- gaia/talk/app.py +2 -4
- gaia/version.py +2 -2
- {amd_gaia-0.15.1.dist-info → amd_gaia-0.15.3.dist-info}/entry_points.txt +0 -0
- {amd_gaia-0.15.1.dist-info → amd_gaia-0.15.3.dist-info}/licenses/LICENSE.md +0 -0
- {amd_gaia-0.15.1.dist-info → amd_gaia-0.15.3.dist-info}/top_level.txt +0 -0
gaia/agents/base/agent.py
CHANGED
|
@@ -61,10 +61,6 @@ class Agent(abc.ABC):
|
|
|
61
61
|
STATE_ERROR_RECOVERY = "ERROR_RECOVERY"
|
|
62
62
|
STATE_COMPLETION = "COMPLETION"
|
|
63
63
|
|
|
64
|
-
# Define tools that can execute directly without requiring a plan
|
|
65
|
-
# Subclasses can override this to specify domain-specific simple tools
|
|
66
|
-
SIMPLE_TOOLS = []
|
|
67
|
-
|
|
68
64
|
def __init__(
|
|
69
65
|
self,
|
|
70
66
|
use_claude: bool = False,
|
|
@@ -72,7 +68,7 @@ class Agent(abc.ABC):
|
|
|
72
68
|
claude_model: str = "claude-sonnet-4-20250514",
|
|
73
69
|
base_url: Optional[str] = None,
|
|
74
70
|
model_id: str = None,
|
|
75
|
-
max_steps: int =
|
|
71
|
+
max_steps: int = 20,
|
|
76
72
|
debug_prompts: bool = False,
|
|
77
73
|
show_prompts: bool = False,
|
|
78
74
|
output_dir: str = None,
|
|
@@ -82,6 +78,7 @@ class Agent(abc.ABC):
|
|
|
82
78
|
debug: bool = False,
|
|
83
79
|
output_handler=None,
|
|
84
80
|
max_plan_iterations: int = 3,
|
|
81
|
+
max_consecutive_repeats: int = 4,
|
|
85
82
|
min_context_size: int = 32768,
|
|
86
83
|
skip_lemonade: bool = False,
|
|
87
84
|
):
|
|
@@ -104,6 +101,7 @@ class Agent(abc.ABC):
|
|
|
104
101
|
debug: If True, enables debug output for troubleshooting (default: False)
|
|
105
102
|
output_handler: Custom OutputHandler for displaying agent output (default: None, creates console based on silent_mode)
|
|
106
103
|
max_plan_iterations: Maximum number of plan-execute-replan cycles (default: 3, 0 = unlimited)
|
|
104
|
+
max_consecutive_repeats: Maximum consecutive identical tool calls before stopping (default: 4)
|
|
107
105
|
min_context_size: Minimum context size required for this agent (default: 32768).
|
|
108
106
|
skip_lemonade: If True, skip Lemonade server initialization (default: False).
|
|
109
107
|
Use this when connecting to a different OpenAI-compatible backend.
|
|
@@ -124,6 +122,7 @@ class Agent(abc.ABC):
|
|
|
124
122
|
self.debug = debug
|
|
125
123
|
self.last_result = None # Store the most recent result
|
|
126
124
|
self.max_plan_iterations = max_plan_iterations
|
|
125
|
+
self.max_consecutive_repeats = max_consecutive_repeats
|
|
127
126
|
self._current_query: Optional[str] = (
|
|
128
127
|
None # Store current query for error context
|
|
129
128
|
)
|
|
@@ -158,17 +157,17 @@ class Agent(abc.ABC):
|
|
|
158
157
|
self.console = self._create_console()
|
|
159
158
|
|
|
160
159
|
# Initialize LLM client for local model
|
|
161
|
-
|
|
160
|
+
# Note: System prompt will be composed after _register_tools()
|
|
161
|
+
# This allows mixins to be initialized first (in subclass __init__)
|
|
162
162
|
|
|
163
163
|
# Register tools for this agent
|
|
164
164
|
self._register_tools()
|
|
165
165
|
|
|
166
|
-
#
|
|
167
|
-
|
|
168
|
-
self.system_prompt += f"\n\n==== AVAILABLE TOOLS ====\n{tools_description}\n"
|
|
166
|
+
# Note: system_prompt is now a lazy @property that composes on first access
|
|
167
|
+
# Tool descriptions and response format are added in _compose_system_prompt()
|
|
169
168
|
|
|
170
|
-
#
|
|
171
|
-
self.
|
|
169
|
+
# Store response format template for use in composition
|
|
170
|
+
self._response_format_template = """
|
|
172
171
|
==== RESPONSE FORMAT ====
|
|
173
172
|
You must respond ONLY in valid JSON. No text before { or after }.
|
|
174
173
|
|
|
@@ -224,13 +223,127 @@ You must respond ONLY in valid JSON. No text before { or after }.
|
|
|
224
223
|
if self.show_prompts:
|
|
225
224
|
self.console.print_prompt(self.system_prompt, "Initial System Prompt")
|
|
226
225
|
|
|
227
|
-
|
|
226
|
+
def _get_mixin_prompts(self) -> list[str]:
|
|
227
|
+
"""
|
|
228
|
+
Auto-collect system prompt fragments from inherited mixins.
|
|
229
|
+
|
|
230
|
+
Checks for mixin methods following the pattern: get_*_system_prompt()
|
|
231
|
+
Override this method to modify, reorder, or filter mixin prompts.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
List of prompt fragments from mixins (empty list if no mixins provide prompts)
|
|
235
|
+
|
|
236
|
+
Example:
|
|
237
|
+
def _get_mixin_prompts(self) -> list[str]:
|
|
238
|
+
prompts = super()._get_mixin_prompts()
|
|
239
|
+
# Modify SD prompt
|
|
240
|
+
if prompts:
|
|
241
|
+
prompts[0] = prompts[0].replace("whimsical", "serious")
|
|
242
|
+
return prompts
|
|
243
|
+
"""
|
|
244
|
+
prompts = []
|
|
245
|
+
|
|
246
|
+
# Check for SD mixin prompts
|
|
247
|
+
if hasattr(self, "get_sd_system_prompt"):
|
|
248
|
+
fragment = self.get_sd_system_prompt()
|
|
249
|
+
if fragment:
|
|
250
|
+
prompts.append(fragment)
|
|
251
|
+
|
|
252
|
+
# Check for VLM mixin prompts
|
|
253
|
+
if hasattr(self, "get_vlm_system_prompt"):
|
|
254
|
+
fragment = self.get_vlm_system_prompt()
|
|
255
|
+
if fragment:
|
|
256
|
+
prompts.append(fragment)
|
|
257
|
+
|
|
258
|
+
# Add more mixin checks here as new prompt-providing mixins are created
|
|
259
|
+
|
|
260
|
+
return prompts
|
|
261
|
+
|
|
262
|
+
def _compose_system_prompt(self) -> str:
|
|
263
|
+
"""
|
|
264
|
+
Compose final system prompt from mixin fragments + agent custom + tools + format.
|
|
265
|
+
|
|
266
|
+
Override this method for complete control over prompt composition order.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Composed system prompt string
|
|
270
|
+
|
|
271
|
+
Example:
|
|
272
|
+
def _compose_system_prompt(self) -> str:
|
|
273
|
+
# Custom composition order
|
|
274
|
+
parts = [
|
|
275
|
+
"Base instructions first",
|
|
276
|
+
*self._get_mixin_prompts(),
|
|
277
|
+
self._get_system_prompt(),
|
|
278
|
+
]
|
|
279
|
+
return "\n\n".join(p for p in parts if p)
|
|
280
|
+
"""
|
|
281
|
+
parts = []
|
|
282
|
+
|
|
283
|
+
# Add mixin prompts first
|
|
284
|
+
parts.extend(self._get_mixin_prompts())
|
|
285
|
+
|
|
286
|
+
# Add agent-specific prompt
|
|
287
|
+
custom = self._get_system_prompt()
|
|
288
|
+
if custom:
|
|
289
|
+
parts.append(custom)
|
|
290
|
+
|
|
291
|
+
# Add tool descriptions (if tools registered)
|
|
292
|
+
if hasattr(self, "_format_tools_for_prompt"):
|
|
293
|
+
tools_description = self._format_tools_for_prompt()
|
|
294
|
+
if tools_description:
|
|
295
|
+
parts.append(f"==== AVAILABLE TOOLS ====\n{tools_description}")
|
|
296
|
+
|
|
297
|
+
# Add response format (if template set)
|
|
298
|
+
if hasattr(self, "_response_format_template"):
|
|
299
|
+
parts.append(self._response_format_template)
|
|
300
|
+
|
|
301
|
+
return "\n\n".join(p for p in parts if p)
|
|
302
|
+
|
|
303
|
+
@property
|
|
304
|
+
def system_prompt(self) -> str:
|
|
305
|
+
"""
|
|
306
|
+
Lazy-loaded system prompt composed from mixins + agent custom.
|
|
307
|
+
|
|
308
|
+
Computed on first access to allow mixins to initialize in subclass __init__.
|
|
309
|
+
|
|
310
|
+
To see the prompt for debugging:
|
|
311
|
+
print(agent.system_prompt)
|
|
312
|
+
"""
|
|
313
|
+
if not hasattr(self, "_system_prompt_cache"):
|
|
314
|
+
self._system_prompt_cache = self._compose_system_prompt()
|
|
315
|
+
return self._system_prompt_cache
|
|
316
|
+
|
|
317
|
+
@system_prompt.setter
|
|
318
|
+
def system_prompt(self, value: str):
|
|
319
|
+
"""Allow setting system prompt (used when appending tool descriptions)."""
|
|
320
|
+
self._system_prompt_cache = value
|
|
321
|
+
|
|
228
322
|
def _get_system_prompt(self) -> str:
|
|
229
323
|
"""
|
|
230
|
-
|
|
231
|
-
|
|
324
|
+
Return agent-specific system prompt additions.
|
|
325
|
+
|
|
326
|
+
Default implementation returns empty string (use only mixin prompts).
|
|
327
|
+
Override this method to add custom instructions.
|
|
328
|
+
|
|
329
|
+
When using mixins that provide prompts (e.g., SDToolsMixin):
|
|
330
|
+
- Return "" to use only mixin prompts (default behavior)
|
|
331
|
+
- Return custom instructions to append to mixin prompts
|
|
332
|
+
- Override _compose_system_prompt() for full control over composition
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
Agent-specific system prompt (empty string by default)
|
|
336
|
+
|
|
337
|
+
Example:
|
|
338
|
+
# Use only mixin prompts (default)
|
|
339
|
+
def _get_system_prompt(self) -> str:
|
|
340
|
+
return ""
|
|
341
|
+
|
|
342
|
+
# Add custom instructions
|
|
343
|
+
def _get_system_prompt(self) -> str:
|
|
344
|
+
return "Always save metadata to logs"
|
|
232
345
|
"""
|
|
233
|
-
|
|
346
|
+
return "" # Default: use only mixin prompts
|
|
234
347
|
|
|
235
348
|
def _create_console(self):
|
|
236
349
|
"""
|
|
@@ -280,7 +393,7 @@ You must respond ONLY in valid JSON. No text before { or after }.
|
|
|
280
393
|
self.console.print_header(f"🛠️ Registered Tools for {self.__class__.__name__}")
|
|
281
394
|
self.console.print_separator()
|
|
282
395
|
|
|
283
|
-
for name, tool_info in
|
|
396
|
+
for name, tool_info in self.get_tools_info().items():
|
|
284
397
|
# Format parameters
|
|
285
398
|
params = []
|
|
286
399
|
for param_name, param_info in tool_info["parameters"].items():
|
|
@@ -313,6 +426,14 @@ You must respond ONLY in valid JSON. No text before { or after }.
|
|
|
313
426
|
|
|
314
427
|
return None
|
|
315
428
|
|
|
429
|
+
def get_tools_info(self) -> Dict[str, Any]:
|
|
430
|
+
"""Get information about all registered tools."""
|
|
431
|
+
return _TOOL_REGISTRY
|
|
432
|
+
|
|
433
|
+
def get_tools(self) -> List[Dict[str, Any]]:
|
|
434
|
+
"""Get a list of registered tools for the agent."""
|
|
435
|
+
return list(_TOOL_REGISTRY.values())
|
|
436
|
+
|
|
316
437
|
def _extract_json_from_response(self, response: str) -> Optional[Dict[str, Any]]:
|
|
317
438
|
"""
|
|
318
439
|
Apply multiple extraction strategies to find valid JSON in the response.
|
|
@@ -743,6 +864,106 @@ You must respond ONLY in valid JSON. No text before { or after }.
|
|
|
743
864
|
# Valid conversational response - wrap it in expected format
|
|
744
865
|
return {"thought": "", "goal": "", "answer": response.strip()}
|
|
745
866
|
|
|
867
|
+
def _resolve_plan_parameters(
|
|
868
|
+
self, tool_args: Any, step_results: List[Dict[str, Any]], _depth: int = 0
|
|
869
|
+
) -> Any:
|
|
870
|
+
"""
|
|
871
|
+
Recursively resolve placeholder references in tool arguments from previous step results.
|
|
872
|
+
|
|
873
|
+
Supports dynamic parameter substitution in multi-step plans:
|
|
874
|
+
- $PREV.field - Get field from previous step result
|
|
875
|
+
- $STEP_0.field - Get field from specific step result (0-indexed)
|
|
876
|
+
|
|
877
|
+
Args:
|
|
878
|
+
tool_args: Tool arguments that may contain placeholders
|
|
879
|
+
step_results: List of results from previously executed steps
|
|
880
|
+
_depth: Internal recursion depth counter (max 50 levels)
|
|
881
|
+
|
|
882
|
+
Returns:
|
|
883
|
+
Tool arguments with placeholders resolved to actual values
|
|
884
|
+
|
|
885
|
+
Examples:
|
|
886
|
+
>>> step_results = [{"image_path": "/path/to/img.png", "status": "success"}]
|
|
887
|
+
>>> tool_args = {"image_path": "$PREV.image_path", "style": "dramatic"}
|
|
888
|
+
>>> resolved = agent._resolve_plan_parameters(tool_args, step_results)
|
|
889
|
+
>>> resolved
|
|
890
|
+
{"image_path": "/path/to/img.png", "style": "dramatic"}
|
|
891
|
+
|
|
892
|
+
Backward Compatibility:
|
|
893
|
+
- If no placeholders exist, returns original tool_args unchanged
|
|
894
|
+
- If placeholder references invalid step/field, returns placeholder string unchanged
|
|
895
|
+
|
|
896
|
+
Limitations:
|
|
897
|
+
- Field names cannot contain dots (e.g., $PREV.user.name not supported - use $PREV.user_name)
|
|
898
|
+
- Maximum nesting depth of 50 levels to prevent stack overflow
|
|
899
|
+
- No type checking - resolved values are used as-is (tools should validate inputs)
|
|
900
|
+
"""
|
|
901
|
+
# Prevent stack overflow from deeply nested structures
|
|
902
|
+
MAX_DEPTH = 50
|
|
903
|
+
if _depth > MAX_DEPTH:
|
|
904
|
+
logger.warning(
|
|
905
|
+
f"Maximum recursion depth ({MAX_DEPTH}) exceeded in parameter resolution, returning unchanged"
|
|
906
|
+
)
|
|
907
|
+
return tool_args
|
|
908
|
+
|
|
909
|
+
# Handle dict: recursively resolve each value
|
|
910
|
+
if isinstance(tool_args, dict):
|
|
911
|
+
return {
|
|
912
|
+
k: self._resolve_plan_parameters(v, step_results, _depth + 1)
|
|
913
|
+
for k, v in tool_args.items()
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
# Handle list: recursively resolve each item
|
|
917
|
+
elif isinstance(tool_args, list):
|
|
918
|
+
return [
|
|
919
|
+
self._resolve_plan_parameters(item, step_results, _depth + 1)
|
|
920
|
+
for item in tool_args
|
|
921
|
+
]
|
|
922
|
+
|
|
923
|
+
# Handle string: check for placeholder patterns
|
|
924
|
+
elif isinstance(tool_args, str):
|
|
925
|
+
# Handle $PREV.field - get field from previous step
|
|
926
|
+
if tool_args.startswith("$PREV.") and step_results:
|
|
927
|
+
field = tool_args[6:] # Strip "$PREV."
|
|
928
|
+
prev_result = step_results[-1]
|
|
929
|
+
if isinstance(prev_result, dict) and field in prev_result:
|
|
930
|
+
resolved = prev_result[field]
|
|
931
|
+
logger.debug(
|
|
932
|
+
f"Resolved {tool_args} -> {resolved} from previous step result"
|
|
933
|
+
)
|
|
934
|
+
return resolved
|
|
935
|
+
else:
|
|
936
|
+
logger.warning(
|
|
937
|
+
f"Could not resolve {tool_args}: field '{field}' not found in previous result"
|
|
938
|
+
)
|
|
939
|
+
return tool_args # Return unchanged if field not found
|
|
940
|
+
|
|
941
|
+
# Handle $STEP_N.field - get field from specific step
|
|
942
|
+
match = re.match(r"\$STEP_(\d+)\.(.+)", tool_args)
|
|
943
|
+
if match and step_results:
|
|
944
|
+
step_idx = int(match.group(1))
|
|
945
|
+
field = match.group(2)
|
|
946
|
+
if 0 <= step_idx < len(step_results):
|
|
947
|
+
step_result = step_results[step_idx]
|
|
948
|
+
if isinstance(step_result, dict) and field in step_result:
|
|
949
|
+
resolved = step_result[field]
|
|
950
|
+
logger.debug(
|
|
951
|
+
f"Resolved {tool_args} -> {resolved} from step {step_idx} result"
|
|
952
|
+
)
|
|
953
|
+
return resolved
|
|
954
|
+
else:
|
|
955
|
+
logger.warning(
|
|
956
|
+
f"Could not resolve {tool_args}: field '{field}' not found in step {step_idx} result"
|
|
957
|
+
)
|
|
958
|
+
else:
|
|
959
|
+
logger.warning(
|
|
960
|
+
f"Could not resolve {tool_args}: step {step_idx} out of range (0-{len(step_results)-1})"
|
|
961
|
+
)
|
|
962
|
+
return tool_args # Return unchanged if reference invalid
|
|
963
|
+
|
|
964
|
+
# For all other types (int, float, bool, None), return unchanged
|
|
965
|
+
return tool_args
|
|
966
|
+
|
|
746
967
|
def _execute_tool(self, tool_name: str, tool_args: Dict[str, Any]) -> Any:
|
|
747
968
|
"""
|
|
748
969
|
Execute a tool by name with the provided arguments.
|
|
@@ -1133,9 +1354,10 @@ You must respond ONLY in valid JSON. No text before { or after }.
|
|
|
1133
1354
|
steps_taken = 0
|
|
1134
1355
|
final_answer = None
|
|
1135
1356
|
error_count = 0
|
|
1136
|
-
|
|
1357
|
+
tool_call_history = [] # Track recent tool calls to detect loops (last 5 calls)
|
|
1137
1358
|
last_error = None # Track the last error to handle it properly
|
|
1138
|
-
previous_outputs = [] # Track previous tool outputs
|
|
1359
|
+
previous_outputs = [] # Track previous tool outputs (truncated for context)
|
|
1360
|
+
step_results = [] # Track full tool results for parameter substitution
|
|
1139
1361
|
|
|
1140
1362
|
# Reset state management
|
|
1141
1363
|
self.execution_state = self.STATE_PLANNING
|
|
@@ -1152,7 +1374,7 @@ You must respond ONLY in valid JSON. No text before { or after }.
|
|
|
1152
1374
|
steps_limit = max_steps if max_steps is not None else self.max_steps
|
|
1153
1375
|
|
|
1154
1376
|
# Print initial message with max steps info
|
|
1155
|
-
self.console.print_processing_start(user_input, steps_limit)
|
|
1377
|
+
self.console.print_processing_start(user_input, steps_limit, self.model_id)
|
|
1156
1378
|
logger.debug(f"Using max_steps: {steps_limit}")
|
|
1157
1379
|
|
|
1158
1380
|
prompt = f"User request: {user_input}\n\n"
|
|
@@ -1177,43 +1399,6 @@ You must respond ONLY in valid JSON. No text before { or after }.
|
|
|
1177
1399
|
steps_taken += 1
|
|
1178
1400
|
logger.debug(f"Step {steps_taken}/{steps_limit}")
|
|
1179
1401
|
|
|
1180
|
-
# Check if we're at the limit and ask user if they want to continue
|
|
1181
|
-
if steps_taken == steps_limit and final_answer is None:
|
|
1182
|
-
# Show what was accomplished
|
|
1183
|
-
max_steps_msg = self._generate_max_steps_message(
|
|
1184
|
-
conversation, steps_taken, steps_limit
|
|
1185
|
-
)
|
|
1186
|
-
self.console.print_warning(max_steps_msg)
|
|
1187
|
-
|
|
1188
|
-
# Ask user if they want to continue (skip in silent mode OR if stdin is not available)
|
|
1189
|
-
# IMPORTANT: Never call input() in API/CI contexts to avoid blocking threads
|
|
1190
|
-
import sys
|
|
1191
|
-
|
|
1192
|
-
has_stdin = sys.stdin and sys.stdin.isatty()
|
|
1193
|
-
if has_stdin and not (
|
|
1194
|
-
hasattr(self, "silent_mode") and self.silent_mode
|
|
1195
|
-
):
|
|
1196
|
-
try:
|
|
1197
|
-
response = (
|
|
1198
|
-
input("\nContinue with 50 more steps? (y/n): ")
|
|
1199
|
-
.strip()
|
|
1200
|
-
.lower()
|
|
1201
|
-
)
|
|
1202
|
-
if response in ["y", "yes"]:
|
|
1203
|
-
steps_limit += 50
|
|
1204
|
-
self.console.print_info(
|
|
1205
|
-
f"✓ Continuing with {steps_limit} total steps...\n"
|
|
1206
|
-
)
|
|
1207
|
-
else:
|
|
1208
|
-
self.console.print_info("Stopping at user request.")
|
|
1209
|
-
break
|
|
1210
|
-
except (EOFError, KeyboardInterrupt):
|
|
1211
|
-
self.console.print_info("\nStopping at user request.")
|
|
1212
|
-
break
|
|
1213
|
-
else:
|
|
1214
|
-
# Silent mode - just stop
|
|
1215
|
-
break
|
|
1216
|
-
|
|
1217
1402
|
# Display current step
|
|
1218
1403
|
self.console.print_step_header(steps_taken, steps_limit)
|
|
1219
1404
|
|
|
@@ -1247,6 +1432,9 @@ You must respond ONLY in valid JSON. No text before { or after }.
|
|
|
1247
1432
|
tool_name = next_step["tool"]
|
|
1248
1433
|
tool_args = next_step["tool_args"]
|
|
1249
1434
|
|
|
1435
|
+
# Resolve dynamic parameters from previous step results
|
|
1436
|
+
tool_args = self._resolve_plan_parameters(tool_args, step_results)
|
|
1437
|
+
|
|
1250
1438
|
# Create a parsed response structure as if it came from the LLM
|
|
1251
1439
|
parsed = {
|
|
1252
1440
|
"thought": f"Executing step {self.current_step + 1} of the plan",
|
|
@@ -1298,6 +1486,9 @@ You must respond ONLY in valid JSON. No text before { or after }.
|
|
|
1298
1486
|
}
|
|
1299
1487
|
)
|
|
1300
1488
|
|
|
1489
|
+
# Store full result for parameter substitution in subsequent plan steps
|
|
1490
|
+
step_results.append(tool_result)
|
|
1491
|
+
|
|
1301
1492
|
# Share tool output with subsequent LLM calls
|
|
1302
1493
|
messages.append(
|
|
1303
1494
|
self._create_tool_message(tool_name, truncated_result)
|
|
@@ -1459,9 +1650,14 @@ You must respond ONLY in valid JSON. No text before { or after }.
|
|
|
1459
1650
|
)
|
|
1460
1651
|
|
|
1461
1652
|
# Create a specific error recovery prompt
|
|
1653
|
+
last_tool = (
|
|
1654
|
+
tool_call_history[-1][0]
|
|
1655
|
+
if tool_call_history
|
|
1656
|
+
else "unknown tool"
|
|
1657
|
+
)
|
|
1462
1658
|
prompt = (
|
|
1463
1659
|
"TOOL EXECUTION FAILED!\n\n"
|
|
1464
|
-
f"You were trying to execute: {
|
|
1660
|
+
f"You were trying to execute: {last_tool}\n"
|
|
1465
1661
|
f"Error: {last_error}\n\n"
|
|
1466
1662
|
f"Original task: {user_input}\n\n"
|
|
1467
1663
|
f"Current plan step {self.current_step + 1}/{self.total_plan_steps} failed.\n"
|
|
@@ -1483,6 +1679,7 @@ You must respond ONLY in valid JSON. No text before { or after }.
|
|
|
1483
1679
|
self.current_plan = None
|
|
1484
1680
|
self.current_step = 0
|
|
1485
1681
|
self.total_plan_steps = 0
|
|
1682
|
+
step_results.clear() # Clear stale results from failed plan
|
|
1486
1683
|
|
|
1487
1684
|
elif self.execution_state == self.STATE_COMPLETION:
|
|
1488
1685
|
self.console.print_state_info("COMPLETION: Finalizing response")
|
|
@@ -1668,9 +1865,6 @@ You must respond ONLY in valid JSON. No text before { or after }.
|
|
|
1668
1865
|
# Add assistant response to messages for chat history
|
|
1669
1866
|
messages.append({"role": "assistant", "content": response})
|
|
1670
1867
|
|
|
1671
|
-
# Validate the response has a plan if required
|
|
1672
|
-
self._validate_plan_required(parsed, steps_taken)
|
|
1673
|
-
|
|
1674
1868
|
# If the LLM needs to create a plan first, re-prompt it specifically for that
|
|
1675
1869
|
if "needs_plan" in parsed and parsed["needs_plan"]:
|
|
1676
1870
|
# Prepare a special prompt that specifically requests a plan
|
|
@@ -1825,8 +2019,15 @@ You must respond ONLY in valid JSON. No text before { or after }.
|
|
|
1825
2019
|
for i, step in enumerate(parsed["plan"]):
|
|
1826
2020
|
if not isinstance(step, dict):
|
|
1827
2021
|
invalid_steps.append((i, type(step).__name__, step))
|
|
1828
|
-
elif "tool" not in step
|
|
1829
|
-
invalid_steps.append((i, "missing
|
|
2022
|
+
elif "tool" not in step:
|
|
2023
|
+
invalid_steps.append((i, "missing tool field", step))
|
|
2024
|
+
elif "tool_args" not in step:
|
|
2025
|
+
# Auto-add empty tool_args for convenience
|
|
2026
|
+
# LLMs sometimes omit this for tools with all optional parameters
|
|
2027
|
+
step["tool_args"] = {}
|
|
2028
|
+
logger.debug(
|
|
2029
|
+
f"Auto-added empty tool_args for step {i+1}: {step['tool']}"
|
|
2030
|
+
)
|
|
1830
2031
|
|
|
1831
2032
|
if invalid_steps:
|
|
1832
2033
|
logger.error(f"Invalid plan steps found: {invalid_steps}")
|
|
@@ -1901,12 +2102,27 @@ You must respond ONLY in valid JSON. No text before { or after }.
|
|
|
1901
2102
|
# Start progress indicator for tool execution
|
|
1902
2103
|
self.console.start_progress(f"Executing {tool_name}")
|
|
1903
2104
|
|
|
1904
|
-
# Check for repeated tool calls
|
|
1905
|
-
|
|
2105
|
+
# Check for repeated tool calls (allow up to 3 identical calls)
|
|
2106
|
+
current_call = (tool_name, str(tool_args))
|
|
2107
|
+
tool_call_history.append(current_call)
|
|
2108
|
+
|
|
2109
|
+
# Keep only last 5 calls for loop detection
|
|
2110
|
+
if len(tool_call_history) > 5:
|
|
2111
|
+
tool_call_history.pop(0)
|
|
2112
|
+
|
|
2113
|
+
# Count consecutive identical calls
|
|
2114
|
+
consecutive_count = 0
|
|
2115
|
+
for call in reversed(tool_call_history):
|
|
2116
|
+
if call == current_call:
|
|
2117
|
+
consecutive_count += 1
|
|
2118
|
+
else:
|
|
2119
|
+
break
|
|
2120
|
+
|
|
2121
|
+
# Stop after max_consecutive_repeats identical calls
|
|
2122
|
+
if consecutive_count >= self.max_consecutive_repeats:
|
|
1906
2123
|
# Stop progress indicator
|
|
1907
2124
|
self.console.stop_progress()
|
|
1908
2125
|
|
|
1909
|
-
logger.warning(f"Detected repeated tool call: {tool_name}")
|
|
1910
2126
|
# Force a final answer if the same tool is called repeatedly
|
|
1911
2127
|
final_answer = (
|
|
1912
2128
|
f"Task completed with {tool_name}. No further action needed."
|
|
@@ -1942,9 +2158,6 @@ You must respond ONLY in valid JSON. No text before { or after }.
|
|
|
1942
2158
|
# Share tool output with subsequent LLM calls
|
|
1943
2159
|
messages.append(self._create_tool_message(tool_name, truncated_result))
|
|
1944
2160
|
|
|
1945
|
-
# Update last tool call
|
|
1946
|
-
last_tool_call = (tool_name, str(tool_args))
|
|
1947
|
-
|
|
1948
2161
|
# For single-step plans, we still need to let the LLM process the result
|
|
1949
2162
|
# This is especially important for RAG queries where the LLM needs to
|
|
1950
2163
|
# synthesize the retrieved information into a coherent answer
|
|
@@ -2015,8 +2228,42 @@ You must respond ONLY in valid JSON. No text before { or after }.
|
|
|
2015
2228
|
self.console.print_final_answer(final_answer, streaming=self.streaming)
|
|
2016
2229
|
break
|
|
2017
2230
|
|
|
2018
|
-
#
|
|
2019
|
-
|
|
2231
|
+
# Check if we're at the limit and ask user if they want to continue
|
|
2232
|
+
if steps_taken == steps_limit and final_answer is None:
|
|
2233
|
+
# Show what was accomplished
|
|
2234
|
+
max_steps_msg = self._generate_max_steps_message(
|
|
2235
|
+
conversation, steps_taken, steps_limit
|
|
2236
|
+
)
|
|
2237
|
+
self.console.print_warning(max_steps_msg)
|
|
2238
|
+
|
|
2239
|
+
# Ask user if they want to continue (skip in silent mode OR if stdin is not available)
|
|
2240
|
+
# IMPORTANT: Never call input() in API/CI contexts to avoid blocking threads
|
|
2241
|
+
import sys
|
|
2242
|
+
|
|
2243
|
+
has_stdin = sys.stdin and sys.stdin.isatty()
|
|
2244
|
+
if has_stdin and not (
|
|
2245
|
+
hasattr(self, "silent_mode") and self.silent_mode
|
|
2246
|
+
):
|
|
2247
|
+
try:
|
|
2248
|
+
response = (
|
|
2249
|
+
input("\nContinue with 50 more steps? (y/n): ")
|
|
2250
|
+
.strip()
|
|
2251
|
+
.lower()
|
|
2252
|
+
)
|
|
2253
|
+
if response in ["y", "yes"]:
|
|
2254
|
+
steps_limit += 50
|
|
2255
|
+
self.console.print_info(
|
|
2256
|
+
f"✓ Continuing with {steps_limit} total steps...\n"
|
|
2257
|
+
)
|
|
2258
|
+
else:
|
|
2259
|
+
self.console.print_info("Stopping at user request.")
|
|
2260
|
+
break
|
|
2261
|
+
except (EOFError, KeyboardInterrupt):
|
|
2262
|
+
self.console.print_info("\nStopping at user request.")
|
|
2263
|
+
break
|
|
2264
|
+
else:
|
|
2265
|
+
# Silent mode - just stop
|
|
2266
|
+
break
|
|
2020
2267
|
|
|
2021
2268
|
# Print completion message
|
|
2022
2269
|
self.console.print_completion(steps_taken, steps_limit)
|
|
@@ -2132,46 +2379,3 @@ You must respond ONLY in valid JSON. No text before { or after }.
|
|
|
2132
2379
|
List of error messages
|
|
2133
2380
|
"""
|
|
2134
2381
|
return self.error_history
|
|
2135
|
-
|
|
2136
|
-
def _validate_plan_required(self, parsed: Dict[str, Any], step: int) -> None:
|
|
2137
|
-
"""
|
|
2138
|
-
Validate that the response includes a plan when required by the agent.
|
|
2139
|
-
|
|
2140
|
-
Args:
|
|
2141
|
-
parsed: The parsed response from the LLM
|
|
2142
|
-
step: The current step number
|
|
2143
|
-
"""
|
|
2144
|
-
# Skip validation if we're not in planning mode or if we're already executing a plan
|
|
2145
|
-
if self.execution_state != self.STATE_PLANNING or self.current_plan is not None:
|
|
2146
|
-
return
|
|
2147
|
-
|
|
2148
|
-
# Allow simple single-tool operations without requiring a plan
|
|
2149
|
-
if "tool" in parsed and step == 1:
|
|
2150
|
-
tool_name = parsed.get("tool", "")
|
|
2151
|
-
# List of tools that can execute directly without a plan
|
|
2152
|
-
simple_tools = self.SIMPLE_TOOLS
|
|
2153
|
-
if tool_name in simple_tools:
|
|
2154
|
-
logger.debug(f"Allowing direct execution of simple tool: {tool_name}")
|
|
2155
|
-
return
|
|
2156
|
-
|
|
2157
|
-
# Check if plan is missing on the first step
|
|
2158
|
-
# BUT: Allow direct answers without plans (for simple conversational queries)
|
|
2159
|
-
if "plan" not in parsed and "answer" not in parsed and step == 1:
|
|
2160
|
-
warning_msg = f"No plan found in step {step} response. The agent should create a plan for all tasks."
|
|
2161
|
-
logger.warning(warning_msg)
|
|
2162
|
-
self.console.print_warning(warning_msg)
|
|
2163
|
-
|
|
2164
|
-
# For the first step, we'll add a flag to indicate we need to re-prompt for a plan
|
|
2165
|
-
parsed["needs_plan"] = True
|
|
2166
|
-
|
|
2167
|
-
# If there's a tool in the response, store it but don't execute it yet
|
|
2168
|
-
if "tool" in parsed:
|
|
2169
|
-
parsed["deferred_tool"] = parsed["tool"]
|
|
2170
|
-
parsed["deferred_tool_args"] = parsed.get("tool_args", {})
|
|
2171
|
-
# Remove the tool so it won't be executed
|
|
2172
|
-
del parsed["tool"]
|
|
2173
|
-
if "tool_args" in parsed:
|
|
2174
|
-
del parsed["tool_args"]
|
|
2175
|
-
|
|
2176
|
-
# Set state to indicate we need planning
|
|
2177
|
-
self.execution_state = self.STATE_PLANNING
|