Improve agent reliability and error handling
- Add timeout protection (120s) for LLM provider calls - Skip memory consolidation for CLI mode to avoid blocking - Add timeout protection for memory consolidation (120s) - Improve error handling with better logging - Add parameter type coercion before validation - Allow None values for optional parameters in validation - Fix type coercion for memory updates (handle dict responses)
This commit is contained in:
parent
d9919828c5
commit
096d76430b
@ -186,14 +186,26 @@ class AgentLoop:
|
||||
|
||||
while iteration < self.max_iterations:
|
||||
iteration += 1
|
||||
logger.debug(f"Agent loop iteration {iteration}/{self.max_iterations}, calling LLM provider...")
|
||||
|
||||
response = await self.provider.chat(
|
||||
try:
|
||||
response = await asyncio.wait_for(
|
||||
self.provider.chat(
|
||||
messages=messages,
|
||||
tools=self.tools.get_definitions(),
|
||||
model=self.model,
|
||||
temperature=self.temperature,
|
||||
max_tokens=self.max_tokens,
|
||||
),
|
||||
timeout=120.0 # 2 minute timeout per LLM call
|
||||
)
|
||||
logger.debug(f"LLM provider returned response, has_tool_calls={response.has_tool_calls}")
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"LLM provider call timed out after 120 seconds")
|
||||
return "Error: Request timed out. The LLM provider may be slow or unresponsive.", tools_used
|
||||
except Exception as e:
|
||||
logger.error(f"LLM provider error: {e}")
|
||||
return f"Error calling LLM: {str(e)}", tools_used
|
||||
|
||||
if response.has_tool_calls:
|
||||
if on_progress:
|
||||
@ -324,8 +336,21 @@ class AgentLoop:
|
||||
return OutboundMessage(channel=msg.channel, chat_id=msg.chat_id,
|
||||
content="🐈 nanobot commands:\n/new — Start a new conversation\n/help — Show available commands")
|
||||
|
||||
if len(session.messages) > self.memory_window:
|
||||
asyncio.create_task(self._consolidate_memory(session))
|
||||
# Skip memory consolidation for CLI mode to avoid blocking/hanging
|
||||
# Memory consolidation can be slow and CLI users want fast responses
|
||||
if len(session.messages) > self.memory_window and msg.channel != "cli":
|
||||
# Start memory consolidation in background with timeout protection
|
||||
async def _consolidate_with_timeout():
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
self._consolidate_memory(session),
|
||||
timeout=120.0 # 2 minute timeout for memory consolidation
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning(f"Memory consolidation timed out for session {session.key}")
|
||||
except Exception as e:
|
||||
logger.error(f"Memory consolidation error: {e}")
|
||||
asyncio.create_task(_consolidate_with_timeout())
|
||||
|
||||
self._set_tool_context(msg.channel, msg.chat_id)
|
||||
initial_messages = self.context.build_messages(
|
||||
@ -460,12 +485,16 @@ class AgentLoop:
|
||||
Respond with ONLY valid JSON, no markdown fences."""
|
||||
|
||||
try:
|
||||
response = await self.provider.chat(
|
||||
# Add timeout to memory consolidation LLM call
|
||||
response = await asyncio.wait_for(
|
||||
self.provider.chat(
|
||||
messages=[
|
||||
{"role": "system", "content": "You are a memory consolidation agent. Respond only with valid JSON."},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
model=self.model,
|
||||
),
|
||||
timeout=120.0 # 2 minute timeout for consolidation LLM call
|
||||
)
|
||||
text = (response.content or "").strip()
|
||||
if not text:
|
||||
@ -479,8 +508,14 @@ Respond with ONLY valid JSON, no markdown fences."""
|
||||
return
|
||||
|
||||
if entry := result.get("history_entry"):
|
||||
# Convert to string if LLM returned a non-string (e.g., dict)
|
||||
if not isinstance(entry, str):
|
||||
entry = str(entry)
|
||||
memory.append_history(entry)
|
||||
if update := result.get("memory_update"):
|
||||
# Convert to string if LLM returned a non-string (e.g., dict)
|
||||
if not isinstance(update, str):
|
||||
update = str(update)
|
||||
if update != current_memory:
|
||||
memory.write_long_term(update)
|
||||
|
||||
|
||||
@ -52,6 +52,36 @@ class Tool(ABC):
|
||||
"""
|
||||
pass
|
||||
|
||||
def coerce_params(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Coerce parameter types based on schema before validation."""
|
||||
schema = self.parameters or {}
|
||||
if schema.get("type", "object") != "object":
|
||||
return params
|
||||
|
||||
coerced = params.copy()
|
||||
props = schema.get("properties", {})
|
||||
|
||||
for key, value in list(coerced.items()): # Use list() to avoid modification during iteration
|
||||
if key in props:
|
||||
prop_schema = props[key]
|
||||
param_type = prop_schema.get("type")
|
||||
|
||||
# Coerce types if value is not already the correct type
|
||||
if param_type == "integer" and isinstance(value, str):
|
||||
try:
|
||||
coerced[key] = int(value)
|
||||
except (ValueError, TypeError):
|
||||
pass # Let validation catch the error
|
||||
elif param_type == "number" and isinstance(value, str):
|
||||
try:
|
||||
coerced[key] = float(value)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
elif param_type == "boolean" and isinstance(value, str):
|
||||
coerced[key] = value.lower() in ("true", "1", "yes", "on")
|
||||
|
||||
return coerced
|
||||
|
||||
def validate_params(self, params: dict[str, Any]) -> list[str]:
|
||||
"""Validate tool parameters against JSON schema. Returns error list (empty if valid)."""
|
||||
schema = self.parameters or {}
|
||||
@ -61,6 +91,9 @@ class Tool(ABC):
|
||||
|
||||
def _validate(self, val: Any, schema: dict[str, Any], path: str) -> list[str]:
|
||||
t, label = schema.get("type"), path or "parameter"
|
||||
# Allow None/null for optional parameters (not in required list)
|
||||
if val is None:
|
||||
return []
|
||||
if t in self._TYPE_MAP and not isinstance(val, self._TYPE_MAP[t]):
|
||||
return [f"{label} should be {t}"]
|
||||
|
||||
|
||||
@ -54,10 +54,12 @@ class ToolRegistry:
|
||||
return f"Error: Tool '{name}' not found"
|
||||
|
||||
try:
|
||||
errors = tool.validate_params(params)
|
||||
# Coerce parameter types before validation
|
||||
coerced_params = tool.coerce_params(params)
|
||||
errors = tool.validate_params(coerced_params)
|
||||
if errors:
|
||||
return f"Error: Invalid parameters for tool '{name}': " + "; ".join(errors)
|
||||
return await tool.execute(**params)
|
||||
return await tool.execute(**coerced_params)
|
||||
except Exception as e:
|
||||
return f"Error executing {name}: {str(e)}"
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user