fix-cron-scheduled-tasks #1

Merged
tanyar09 merged 11 commits from fix-cron-scheduled-tasks into feature/cleanup-providers-llama-only 2026-03-04 12:04:57 -05:00
3 changed files with 87 additions and 17 deletions
Showing only changes of commit 096d76430b - Show all commits

View File

@ -186,14 +186,26 @@ class AgentLoop:
while iteration < self.max_iterations: while iteration < self.max_iterations:
iteration += 1 iteration += 1
logger.debug(f"Agent loop iteration {iteration}/{self.max_iterations}, calling LLM provider...")
response = await self.provider.chat( try:
messages=messages, response = await asyncio.wait_for(
tools=self.tools.get_definitions(), self.provider.chat(
model=self.model, messages=messages,
temperature=self.temperature, tools=self.tools.get_definitions(),
max_tokens=self.max_tokens, 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 response.has_tool_calls:
if on_progress: if on_progress:
@ -324,8 +336,21 @@ class AgentLoop:
return OutboundMessage(channel=msg.channel, chat_id=msg.chat_id, return OutboundMessage(channel=msg.channel, chat_id=msg.chat_id,
content="🐈 nanobot commands:\n/new — Start a new conversation\n/help — Show available commands") content="🐈 nanobot commands:\n/new — Start a new conversation\n/help — Show available commands")
if len(session.messages) > self.memory_window: # Skip memory consolidation for CLI mode to avoid blocking/hanging
asyncio.create_task(self._consolidate_memory(session)) # 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) self._set_tool_context(msg.channel, msg.chat_id)
initial_messages = self.context.build_messages( initial_messages = self.context.build_messages(
@ -460,12 +485,16 @@ class AgentLoop:
Respond with ONLY valid JSON, no markdown fences.""" Respond with ONLY valid JSON, no markdown fences."""
try: try:
response = await self.provider.chat( # Add timeout to memory consolidation LLM call
messages=[ response = await asyncio.wait_for(
{"role": "system", "content": "You are a memory consolidation agent. Respond only with valid JSON."}, self.provider.chat(
{"role": "user", "content": prompt}, messages=[
], {"role": "system", "content": "You are a memory consolidation agent. Respond only with valid JSON."},
model=self.model, {"role": "user", "content": prompt},
],
model=self.model,
),
timeout=120.0 # 2 minute timeout for consolidation LLM call
) )
text = (response.content or "").strip() text = (response.content or "").strip()
if not text: if not text:
@ -479,8 +508,14 @@ Respond with ONLY valid JSON, no markdown fences."""
return return
if entry := result.get("history_entry"): 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) memory.append_history(entry)
if update := result.get("memory_update"): 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: if update != current_memory:
memory.write_long_term(update) memory.write_long_term(update)

View File

@ -52,6 +52,36 @@ class Tool(ABC):
""" """
pass 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]: def validate_params(self, params: dict[str, Any]) -> list[str]:
"""Validate tool parameters against JSON schema. Returns error list (empty if valid).""" """Validate tool parameters against JSON schema. Returns error list (empty if valid)."""
schema = self.parameters or {} schema = self.parameters or {}
@ -61,6 +91,9 @@ class Tool(ABC):
def _validate(self, val: Any, schema: dict[str, Any], path: str) -> list[str]: def _validate(self, val: Any, schema: dict[str, Any], path: str) -> list[str]:
t, label = schema.get("type"), path or "parameter" 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]): if t in self._TYPE_MAP and not isinstance(val, self._TYPE_MAP[t]):
return [f"{label} should be {t}"] return [f"{label} should be {t}"]

View File

@ -54,10 +54,12 @@ class ToolRegistry:
return f"Error: Tool '{name}' not found" return f"Error: Tool '{name}' not found"
try: 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: if errors:
return f"Error: Invalid parameters for tool '{name}': " + "; ".join(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: except Exception as e:
return f"Error executing {name}: {str(e)}" return f"Error executing {name}: {str(e)}"