From c437ae0173d94ee54ab6ebc152bd6a6f4a6aef26 Mon Sep 17 00:00:00 2001 From: Simon Diesenreiter Date: Sat, 11 Apr 2026 22:19:42 +0200 Subject: [PATCH] fix: increase LLM timeouts, refs NOISSUE --- ai_software_factory/agents/llm_service.py | 6 +- ai_software_factory/config.py | 6 ++ ai_software_factory/main.py | 74 ++++++++++++++++++++++- 3 files changed, 82 insertions(+), 4 deletions(-) diff --git a/ai_software_factory/agents/llm_service.py b/ai_software_factory/agents/llm_service.py index 9112205..08143ef 100644 --- a/ai_software_factory/agents/llm_service.py +++ b/ai_software_factory/agents/llm_service.py @@ -185,6 +185,7 @@ class LLMServiceClient: def __init__(self, ollama_url: str | None = None, model: str | None = None): self.ollama_url = (ollama_url or settings.ollama_url).rstrip('/') self.model = model or settings.OLLAMA_MODEL + self.request_timeout_seconds = settings.llm_request_timeout_seconds self.toolbox = LLMToolbox() self.live_tool_executor = LLMLiveToolExecutor() @@ -290,13 +291,16 @@ class LLMServiceClient: try: import aiohttp - async with aiohttp.ClientSession() as session: + async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=self.request_timeout_seconds)) as session: async with session.post(f'{self.ollama_url}/api/chat', json=request_payload) as resp: payload = await resp.json() if 200 <= resp.status < 300: return (payload.get('message') or {}).get('content', ''), payload, None return None, payload, str(payload.get('error') or payload) except Exception as exc: + if exc.__class__.__name__ == 'TimeoutError': + message = f'LLM request timed out after {self.request_timeout_seconds} seconds' + return None, {'error': message}, message return None, {'error': str(exc)}, str(exc) @staticmethod diff --git a/ai_software_factory/config.py b/ai_software_factory/config.py index 5a30914..866ddb6 100644 --- a/ai_software_factory/config.py +++ b/ai_software_factory/config.py @@ -222,6 +222,7 @@ class Settings(BaseSettings): # Ollama settings computed from environment OLLAMA_URL: str = "http://ollama:11434" OLLAMA_MODEL: str = "llama3" + LLM_REQUEST_TIMEOUT_SECONDS: int = 240 LLM_GUARDRAIL_PROMPT: str = ( "You are operating inside AI Software Factory. Follow the requested schema exactly, " "treat provided tool outputs as authoritative, and do not invent repositories, issues, pull requests, or delivery facts." @@ -613,6 +614,11 @@ class Settings(BaseSettings): """Get the maximum number of queued prompts to process in one batch.""" return max(int(_resolve_runtime_setting_value('PROMPT_QUEUE_MAX_BATCH_SIZE', self.PROMPT_QUEUE_MAX_BATCH_SIZE)), 1) + @property + def llm_request_timeout_seconds(self) -> int: + """Get the outbound provider timeout for one LLM request.""" + return max(int(_resolve_runtime_setting_value('LLM_REQUEST_TIMEOUT_SECONDS', self.LLM_REQUEST_TIMEOUT_SECONDS)), 1) + @property def projects_root(self) -> Path: """Get the root directory for generated project artifacts.""" diff --git a/ai_software_factory/main.py b/ai_software_factory/main.py index 3c342cb..25d246a 100644 --- a/ai_software_factory/main.py +++ b/ai_software_factory/main.py @@ -36,6 +36,7 @@ try: from .agents.orchestrator import AgentOrchestrator from .agents.n8n_setup import N8NSetupAgent from .agents.prompt_workflow import PromptWorkflowManager + from .agents.telegram import TelegramHandler from .agents.ui_manager import UIManager from .models import ProjectHistory, ProjectLog, SystemLog except ImportError: @@ -49,6 +50,7 @@ except ImportError: from agents.orchestrator import AgentOrchestrator from agents.n8n_setup import N8NSetupAgent from agents.prompt_workflow import PromptWorkflowManager + from agents.telegram import TelegramHandler from agents.ui_manager import UIManager from models import ProjectHistory, ProjectLog, SystemLog @@ -256,6 +258,63 @@ def _ensure_summary_mentions_pull_request(summary_message: str, pull_request: di return f"{summary_message}{separator} Review PR: {pr_url}" +def _should_queue_telegram_request(request: FreeformSoftwareRequest) -> bool: + """Return whether a Telegram request should be accepted for background processing.""" + return ( + request.source == 'telegram' + and bool(request.chat_id) + and bool(database_module.settings.telegram_bot_token) + and not request.process_now + ) + + +def _schedule_prompt_queue_processing() -> None: + """Kick off background queue processing without blocking the current HTTP request.""" + if database_module.settings.prompt_queue_enabled and not database_module.settings.prompt_queue_auto_process: + return + limit = database_module.settings.prompt_queue_max_batch_size if database_module.settings.prompt_queue_enabled else 1 + force = database_module.settings.prompt_queue_force_process if database_module.settings.prompt_queue_enabled else True + task = asyncio.create_task(_process_prompt_queue_batch(limit=limit, force=force)) + + def _log_task_result(completed_task: asyncio.Task) -> None: + try: + completed_task.result() + except Exception as exc: + db = database_module.get_db_sync() + try: + DatabaseManager(db).log_system_event('prompt-queue', 'ERROR', f'Background queue processing failed: {exc}') + finally: + db.close() + + task.add_done_callback(_log_task_result) + + +async def _notify_telegram_queue_result(request: FreeformSoftwareRequest, *, response: dict | None = None, error_message: str | None = None) -> None: + """Send the final queued result back to Telegram when chat metadata is available.""" + if request.source != 'telegram' or not request.chat_id or not database_module.settings.telegram_bot_token: + return + if response is not None: + message = ( + response.get('summary_message') + or (response.get('data') or {}).get('summary_message') + or response.get('message') + or 'Software generation completed.' + ) + else: + message = f"Software generation failed: {error_message or 'Unknown error'}" + result = await TelegramHandler(webhook_url=database_module.settings.backend_public_url).send_message( + bot_token=database_module.settings.telegram_bot_token, + chat_id=request.chat_id, + text=message, + ) + if result.get('status') == 'error': + db = database_module.get_db_sync() + try: + DatabaseManager(db).log_system_event('telegram', 'ERROR', f"Unable to send queued Telegram update: {result.get('message')}") + finally: + db.close() + + def _serialize_system_log(log: SystemLog) -> dict: """Serialize a system log row.""" return { @@ -732,6 +791,7 @@ async def _process_prompt_queue_batch(limit: int = 1, force: bool = False) -> di process_now=True, ) response = await _run_freeform_generation(request, work_db, queue_item_id=claimed['id']) + await _notify_telegram_queue_result(request, response=response) processed.append( { 'queue_item_id': claimed['id'], @@ -741,6 +801,7 @@ async def _process_prompt_queue_batch(limit: int = 1, force: bool = False) -> di ) except Exception as exc: DatabaseManager(work_db).fail_queued_prompt(claimed['id'], str(exc)) + await _notify_telegram_queue_result(request, error_message=str(exc)) processed.append({'queue_item_id': claimed['id'], 'status': 'failed', 'error': str(exc)}) finally: work_db.close() @@ -960,7 +1021,7 @@ async def generate_software_from_text(request: FreeformSoftwareRequest, db: DbSe }, } - if request.source == 'telegram' and database_module.settings.prompt_queue_enabled and not request.process_now: + if _should_queue_telegram_request(request): manager = DatabaseManager(db) queue_item = manager.enqueue_prompt( prompt_text=request.prompt_text, @@ -969,12 +1030,19 @@ async def generate_software_from_text(request: FreeformSoftwareRequest, db: DbSe chat_type=request.chat_type, source_context={'chat_id': request.chat_id, 'chat_type': request.chat_type}, ) + queue_gate = await _get_queue_gate_status(force=False) + if not database_module.settings.prompt_queue_enabled or database_module.settings.prompt_queue_auto_process: + _schedule_prompt_queue_processing() return { 'status': 'queued', - 'message': 'Prompt queued for energy-aware processing.', + 'message': ( + 'Prompt accepted for background processing.' + if not database_module.settings.prompt_queue_enabled else + 'Prompt queued for background processing.' + ), 'queue_item': queue_item, 'queue_summary': manager.get_prompt_queue_summary(), - 'queue_gate': await _get_queue_gate_status(force=False), + 'queue_gate': queue_gate, 'source': { 'type': request.source, 'chat_id': request.chat_id,