diff --git a/api/services/agent_runner.py b/api/services/agent_runner.py index a8bda9a..55d8dc5 100644 --- a/api/services/agent_runner.py +++ b/api/services/agent_runner.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio import json import logging from typing import Any, Callable @@ -84,6 +85,7 @@ class AgentRunner: model: str = "sonnet", max_tokens: int = 4096, max_iterations: int = 10, + total_timeout: float = 570.0, ): self.client = AsyncOpenAI( base_url=f"{ai_gateway_url.rstrip('/')}/v1", @@ -92,6 +94,7 @@ class AgentRunner: self.model = model self.max_tokens = max_tokens self.max_iterations = max_iterations + self.total_timeout = total_timeout async def run( self, @@ -108,7 +111,45 @@ class AgentRunner: kb_topic_name: str | None = None, on_event: Callable[[dict], None] | None = None, ) -> str: - """Run a conversation with tool-calling loop. Returns the final text response. + """Run a conversation with tool-calling loop, bounded by total_timeout seconds.""" + try: + return await asyncio.wait_for( + self._run_impl( + system_prompt=system_prompt, + messages=messages, + context=context, + espocrm_url=espocrm_url, + espocrm_api_key=espocrm_api_key, + depth=depth, + blocked_tools=blocked_tools, + allowed_toolsets=allowed_toolsets, + kb_sources_used=kb_sources_used, + kb_topic_id=kb_topic_id, + kb_topic_name=kb_topic_name, + on_event=on_event, + ), + timeout=self.total_timeout, + ) + except asyncio.TimeoutError: + logger.warning("[agent] total timeout (%.0fs) reached", self.total_timeout) + return "⏱️ המשימה לקחה יותר זמן מהמותר. נסי לפצל אותה לחלקים קטנים — למשל: קודם בקשי לקרוא מסמך ספציפי, ואז לכתוב את הסיכום." + + async def _run_impl( + self, + system_prompt: str, + messages: list[dict[str, str]], + context: dict, + espocrm_url: str, + espocrm_api_key: str, + depth: int = 0, + blocked_tools: set[str] | None = None, + allowed_toolsets: list[str] | None = None, + kb_sources_used: list[dict] | None = None, + kb_topic_id: int | None = None, + kb_topic_name: str | None = None, + on_event: Callable[[dict], None] | None = None, + ) -> str: + """Internal tool-calling loop. Returns the final text response. Args: depth: Current delegation depth. 0 = main conversation, 1 = child agent.