From 919f6b9e42ec9ce7907e5b83d02e2406150c20ed Mon Sep 17 00:00:00 2001 From: Mark Randall Havens Date: Sat, 14 Mar 2026 09:24:45 +0000 Subject: [PATCH 1/2] Remove unused load_dotenv import (#36) --- opus_orchestrator/orchestrator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/opus_orchestrator/orchestrator.py b/opus_orchestrator/orchestrator.py index f0c4135..91aa02d 100644 --- a/opus_orchestrator/orchestrator.py +++ b/opus_orchestrator/orchestrator.py @@ -8,7 +8,6 @@ import os from pathlib import Path from typing import Any, Optional -from dotenv import load_dotenv from opus_orchestrator.agents.fiction import ( From 51fe18323f8fba188f9631b5a8ce0e915b975534 Mon Sep 17 00:00:00 2001 From: Solaria Date: Sat, 14 Mar 2026 09:30:01 +0000 Subject: [PATCH 2/2] Fix CRITICAL issues: timeout config, request validation, streaming (#37, #38, #39) --- opus_orchestrator/server.py | 48 +++++++++++++++++++++++++++------- opus_orchestrator/utils/llm.py | 8 ++++-- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/opus_orchestrator/server.py b/opus_orchestrator/server.py index fa82ce1..ad3db3b 100644 --- a/opus_orchestrator/server.py +++ b/opus_orchestrator/server.py @@ -58,8 +58,8 @@ class GenerateRequest(BaseModel): framework: str = Field("snowflake", description="Story framework") genre: str = Field("fiction", description="Genre") book_type: str = Field("fiction", description="Book type (fiction/nonfiction)") - target_word_count: int = Field(5000, description="Target word count") - chapters: int = Field(3, description="Number of chapters") + target_word_count: int = Field(5000, ge=1, le=500000, description="Target word count") + chapters: int = Field(3, ge=1, le=100, description="Number of chapters") tone: str = Field("literary", description="Writing tone") use_crewai: bool = Field(False, description="Use CrewAI instead of LangGraph") use_autogen: bool = Field(True, description="Use AutoGen critique") @@ -312,17 +312,45 @@ async def generate_stream(request: GenerateRequest): yield "data: " + json.dumps({"status": "ingested", "message": f"Ingested {len(seed_concept)} characters"}) + "\n\n" if not seed_concept: - raise HTTPException(status_code=400, detail="Must provide concept or repo") + yield "data: " + json.dumps({"status": "error", "message": "Must provide concept or repo"}) + "\n\n" + return - # For now, just stream a completion message - # Full streaming requires modifying the LangGraph workflow + # Call run_opus and return real result yield "data: " + json.dumps({"status": "generating", "progress": 0.1, "message": "Starting generation..."}) + "\n\n" - # TODO: Implement actual streaming from LangGraph workflow - # This requires modifying run_opus to yield progress events - yield "data: " + json.dumps({"status": "generating", "progress": 0.5, "message": "Generating manuscript..."}) + "\n\n" - - yield "data: " + json.dumps({"status": "complete", "progress": 1.0, "message": "Generation complete"}) + "\n\n" + try: + result = await run_opus( + seed_concept=seed_concept, + framework=request.framework, + genre=request.genre, + target_word_count=request.target_word_count, + ) + + yield "data: " + json.dumps({"status": "generating", "progress": 0.5, "message": "Processing result..."}) + "\n\n" + + # Extract manuscript from result + if isinstance(result, dict): + manuscript = result.get("manuscript", "") + if not manuscript: + chapters = result.get("chapters", []) + if chapters: + manuscript = "\n\n---\n\n".join(str(c) for c in chapters) + else: + manuscript = str(result) + else: + manuscript = str(result) + + word_count = len(manuscript.split()) + + yield "data: " + json.dumps({ + "status": "complete", + "progress": 1.0, + "message": f"Generation complete ({word_count} words)", + "manuscript": manuscript[:1000] + "..." if len(manuscript) > 1000 else manuscript + }) + "\n\n" + + except Exception as e: + yield "data: " + json.dumps({"status": "error", "message": str(e)}) + "\n\n" except Exception as e: yield "data: " + json.dumps({"status": "error", "message": str(e)}) + "\n\n" diff --git a/opus_orchestrator/utils/llm.py b/opus_orchestrator/utils/llm.py index 1bb5804..5c28f5c 100644 --- a/opus_orchestrator/utils/llm.py +++ b/opus_orchestrator/utils/llm.py @@ -27,6 +27,7 @@ class LLMClient: model: str = "MiniMax/MiniMax-M2.1", base_url: Optional[str] = None, max_retries: int = 3, + timeout: float = 120.0, ): """Initialize LLM client. @@ -36,10 +37,12 @@ class LLMClient: model: Model name base_url: Optional custom base URL max_retries: Maximum retry attempts (default 3) + timeout: HTTP timeout in seconds (default 120.0) """ self.api_key = api_key or os.environ.get("MINIMAX_API_KEY") or os.environ.get("OPENAI_API_KEY") self.provider = provider self.model = model + self.timeout = timeout # Normalize model name for MiniMax if provider == "minimax": @@ -56,8 +59,8 @@ class LLMClient: else: self.base_url = "https://api.openai.com/v1" - # Async client - self._async_client = httpx.AsyncClient(timeout=120.0) + # Async client with configurable timeout + self._async_client = httpx.AsyncClient(timeout=self.timeout) # Initialize retry handler retry_config = RetryConfig( @@ -307,4 +310,5 @@ def get_llm_client(config: Optional[Any] = None) -> LLMClient: api_key=cfg.agent.api_key, provider=cfg.agent.provider, model=cfg.agent.model, + timeout=getattr(cfg.agent, 'timeout', 120.0), )