2 Commits

Author SHA1 Message Date
2eba98dff4 release: version 0.9.15 🚀
All checks were successful
Upload Python Package / Create Release (push) Successful in 15s
Upload Python Package / deploy (push) Successful in 33s
2026-04-11 22:20:14 +02:00
c437ae0173 fix: increase LLM timeouts, refs NOISSUE 2026-04-11 22:19:42 +02:00
5 changed files with 94 additions and 5 deletions

View File

@@ -5,11 +5,22 @@ Changelog
(unreleased) (unreleased)
------------ ------------
Fix
~~~
- Increase LLM timeouts, refs NOISSUE. [Simon Diesenreiter]
0.9.14 (2026-04-11)
-------------------
Fix Fix
~~~ ~~~
- Add Ollama connection health details in UI, refs NOISSUE. [Simon - Add Ollama connection health details in UI, refs NOISSUE. [Simon
Diesenreiter] Diesenreiter]
Other
~~~~~
0.9.13 (2026-04-11) 0.9.13 (2026-04-11)
------------------- -------------------

View File

@@ -1 +1 @@
0.9.14 0.9.15

View File

@@ -185,6 +185,7 @@ class LLMServiceClient:
def __init__(self, ollama_url: str | None = None, model: str | None = None): def __init__(self, ollama_url: str | None = None, model: str | None = None):
self.ollama_url = (ollama_url or settings.ollama_url).rstrip('/') self.ollama_url = (ollama_url or settings.ollama_url).rstrip('/')
self.model = model or settings.OLLAMA_MODEL self.model = model or settings.OLLAMA_MODEL
self.request_timeout_seconds = settings.llm_request_timeout_seconds
self.toolbox = LLMToolbox() self.toolbox = LLMToolbox()
self.live_tool_executor = LLMLiveToolExecutor() self.live_tool_executor = LLMLiveToolExecutor()
@@ -290,13 +291,16 @@ class LLMServiceClient:
try: try:
import aiohttp 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: async with session.post(f'{self.ollama_url}/api/chat', json=request_payload) as resp:
payload = await resp.json() payload = await resp.json()
if 200 <= resp.status < 300: if 200 <= resp.status < 300:
return (payload.get('message') or {}).get('content', ''), payload, None return (payload.get('message') or {}).get('content', ''), payload, None
return None, payload, str(payload.get('error') or payload) return None, payload, str(payload.get('error') or payload)
except Exception as exc: 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) return None, {'error': str(exc)}, str(exc)
@staticmethod @staticmethod

View File

@@ -222,6 +222,7 @@ class Settings(BaseSettings):
# Ollama settings computed from environment # Ollama settings computed from environment
OLLAMA_URL: str = "http://ollama:11434" OLLAMA_URL: str = "http://ollama:11434"
OLLAMA_MODEL: str = "llama3" OLLAMA_MODEL: str = "llama3"
LLM_REQUEST_TIMEOUT_SECONDS: int = 240
LLM_GUARDRAIL_PROMPT: str = ( LLM_GUARDRAIL_PROMPT: str = (
"You are operating inside AI Software Factory. Follow the requested schema exactly, " "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." "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.""" """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) 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 @property
def projects_root(self) -> Path: def projects_root(self) -> Path:
"""Get the root directory for generated project artifacts.""" """Get the root directory for generated project artifacts."""

View File

@@ -36,6 +36,7 @@ try:
from .agents.orchestrator import AgentOrchestrator from .agents.orchestrator import AgentOrchestrator
from .agents.n8n_setup import N8NSetupAgent from .agents.n8n_setup import N8NSetupAgent
from .agents.prompt_workflow import PromptWorkflowManager from .agents.prompt_workflow import PromptWorkflowManager
from .agents.telegram import TelegramHandler
from .agents.ui_manager import UIManager from .agents.ui_manager import UIManager
from .models import ProjectHistory, ProjectLog, SystemLog from .models import ProjectHistory, ProjectLog, SystemLog
except ImportError: except ImportError:
@@ -49,6 +50,7 @@ except ImportError:
from agents.orchestrator import AgentOrchestrator from agents.orchestrator import AgentOrchestrator
from agents.n8n_setup import N8NSetupAgent from agents.n8n_setup import N8NSetupAgent
from agents.prompt_workflow import PromptWorkflowManager from agents.prompt_workflow import PromptWorkflowManager
from agents.telegram import TelegramHandler
from agents.ui_manager import UIManager from agents.ui_manager import UIManager
from models import ProjectHistory, ProjectLog, SystemLog 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}" 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: def _serialize_system_log(log: SystemLog) -> dict:
"""Serialize a system log row.""" """Serialize a system log row."""
return { return {
@@ -732,6 +791,7 @@ async def _process_prompt_queue_batch(limit: int = 1, force: bool = False) -> di
process_now=True, process_now=True,
) )
response = await _run_freeform_generation(request, work_db, queue_item_id=claimed['id']) response = await _run_freeform_generation(request, work_db, queue_item_id=claimed['id'])
await _notify_telegram_queue_result(request, response=response)
processed.append( processed.append(
{ {
'queue_item_id': claimed['id'], '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: except Exception as exc:
DatabaseManager(work_db).fail_queued_prompt(claimed['id'], str(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)}) processed.append({'queue_item_id': claimed['id'], 'status': 'failed', 'error': str(exc)})
finally: finally:
work_db.close() 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) manager = DatabaseManager(db)
queue_item = manager.enqueue_prompt( queue_item = manager.enqueue_prompt(
prompt_text=request.prompt_text, prompt_text=request.prompt_text,
@@ -969,12 +1030,19 @@ async def generate_software_from_text(request: FreeformSoftwareRequest, db: DbSe
chat_type=request.chat_type, chat_type=request.chat_type,
source_context={'chat_id': request.chat_id, '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 { return {
'status': 'queued', '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_item': queue_item,
'queue_summary': manager.get_prompt_queue_summary(), 'queue_summary': manager.get_prompt_queue_summary(),
'queue_gate': await _get_queue_gate_status(force=False), 'queue_gate': queue_gate,
'source': { 'source': {
'type': request.source, 'type': request.source,
'chat_id': request.chat_id, 'chat_id': request.chat_id,