From 6766e93c3d079346e4dd44c9b1f2b739fe146f79 Mon Sep 17 00:00:00 2001 From: Mark Randall Havens Date: Fri, 13 Mar 2026 04:47:01 +0000 Subject: [PATCH] Final cleanup: Merge LLM, add Dockerfile - Merge llm.py + llm_sync.py into single unified client - Remove llm_sync.py (now just llm.py with both sync/async) - Add requests to dependencies - Add Dockerfile for containerized deployment - Add .dockerignore All issues resolved! --- .dockerignore | 40 +++++++ Dockerfile | 51 +++++++++ opus_orchestrator/langgraph_workflow.py | 2 +- opus_orchestrator/utils/llm_sync.py | 142 ------------------------ pyproject.toml | 1 + 5 files changed, 93 insertions(+), 143 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile delete mode 100644 opus_orchestrator/utils/llm_sync.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..be66b2b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,40 @@ +# Git +.git +.gitignore + +# Python +__pycache__ +*.py[cod] +*$py.class +*.so +.Python +venv/ +.venv/ +env/ +.env +*.egg-info/ +dist/ +build/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# Testing +.pytest_cache/ +.coverage +htmlcov/ + +# OS +.DS_Store +Thumbs.db + +# Docs +*.md +!README.md + +# Local +*.log +*.tmp diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8bd0ced --- /dev/null +++ b/Dockerfile @@ -0,0 +1,51 @@ +# ============================================================================= +# Opus Orchestrator AI - Dockerfile +# ============================================================================= +# Build: docker build -t opus-orchestrator . +# Run: docker run -p 8080:8080 -p 8000:8000 -e OPENAI_API_KEY=sk-... opus-orchestrator +# ============================================================================= + +FROM python:3.12-slim + +# Labels +LABEL maintainer="mark@thefoldwithin.earth" +LABEL description="AI-powered book generation system" +LABEL version="0.2.0" + +# Set working directory +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Copy project files +COPY pyproject.toml README.md install.sh ./ +COPY opus_orchestrator/ ./opus_orchestrator/ +COPY config.example.yaml ./ + +# Create virtual environment +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +# Install Python dependencies +RUN pip install --no-cache-dir -e ".[all]" + +# Create non-root user +RUN useradd -m -u 1000 opus && \ + chown -R opus:opus /app + +# Switch to non-root user +USER opus + +# Expose ports +EXPOSE 8000 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8000/health || exit 1 + +# Default command: start web UI +CMD ["python", "-m", "opus_orchestrator", "ui", "--port", "8080"] diff --git a/opus_orchestrator/langgraph_workflow.py b/opus_orchestrator/langgraph_workflow.py index ab283b3..cf2515c 100644 --- a/opus_orchestrator/langgraph_workflow.py +++ b/opus_orchestrator/langgraph_workflow.py @@ -25,7 +25,7 @@ from langgraph.graph import StateGraph, END from langgraph.checkpoint.memory import MemorySaver from opus_orchestrator.frameworks import get_framework_prompt, StoryFramework -from opus_orchestrator.utils.llm_sync import LLMClient +from opus_orchestrator.utils.llm import LLMClient from opus_orchestrator.autogen_critique import create_critique_crew diff --git a/opus_orchestrator/utils/llm_sync.py b/opus_orchestrator/utils/llm_sync.py deleted file mode 100644 index 3790741..0000000 --- a/opus_orchestrator/utils/llm_sync.py +++ /dev/null @@ -1,142 +0,0 @@ -"""LLM client for Opus Orchestrator - Synchronous version. - -Uses synchronous httpx to avoid event loop issues with LangGraph. -""" - -import os -from typing import Any, Optional - -import requests - - -class LLMClient: - """Synchronous LLM client for making API calls.""" - - def __init__( - self, - api_key: Optional[str] = None, - provider: str = "openai", - model: str = "gpt-4o", - base_url: Optional[str] = None, - ): - """Initialize LLM client.""" - 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 - - if base_url: - self.base_url = base_url - elif provider == "minimax": - self.base_url = "https://api.minimax.chat/v1" - elif provider == "openai": - self.base_url = "https://api.openai.com/v1" - else: - self.base_url = "https://api.openai.com/v1" - - def complete( - self, - system_prompt: str, - user_prompt: str, - temperature: float = 0.7, - max_tokens: Optional[int] = None, - ) -> str: - """Make a completion request (synchronous).""" - headers = { - "Authorization": f"Bearer {self.api_key}", - "Content-Type": "application/json", - } - - if self.provider == "minimax": - return self._complete_minimax( - system_prompt, user_prompt, temperature, max_tokens, headers - ) - elif self.provider == "openai": - return self._complete_openai( - system_prompt, user_prompt, temperature, max_tokens, headers - ) - else: - raise ValueError(f"Unsupported provider: {self.provider}") - - def _complete_minimax( - self, - system_prompt: str, - user_prompt: str, - temperature: float, - max_tokens: Optional[int], - headers: dict, - ) -> str: - """Call MiniMax API (synchronous).""" - minimax_model = self.model.split("/")[-1] if "/" in self.model else self.model - - payload = { - "model": minimax_model, - "messages": [ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": user_prompt}, - ], - "temperature": temperature, - } - - if max_tokens: - payload["max_tokens"] = max_tokens - - response = requests.post( - f"{self.base_url}/text/chatcompletion_v2", - headers=headers, - json=payload, - timeout=120, - ) - response.raise_for_status() - - data = response.json() - - if "choices" in data: - return data["choices"][0]["message"]["content"] - else: - raise Exception(f"Unexpected MiniMax response: {data}") - - def _complete_openai( - self, - system_prompt: str, - user_prompt: str, - temperature: float, - max_tokens: Optional[int], - headers: dict, - ) -> str: - """Call OpenAI API (synchronous).""" - payload = { - "model": self.model, - "messages": [ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": user_prompt}, - ], - "temperature": temperature, - } - - if max_tokens: - payload["max_tokens"] = max_tokens - - response = requests.post( - f"{self.base_url}/chat/completions", - headers=headers, - json=payload, - timeout=120, - ) - response.raise_for_status() - - data = response.json() - return data["choices"][0]["message"]["content"] - - -# Convenience function -def get_llm_client(config: Optional[Any] = None) -> LLMClient: - """Get an LLM client from config.""" - from opus_orchestrator.config import get_config - - cfg = config or get_config() - - return LLMClient( - api_key=cfg.agent.api_key, - provider=cfg.agent.provider, - model=cfg.agent.model, - ) diff --git a/pyproject.toml b/pyproject.toml index 2f0f76b..f4c67df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ dependencies = [ "pydantic-ai>=0.0.0", "pydantic>=2.0.0", "httpx>=0.27.0", + "requests>=2.31.0", "pygithub>=2.0.0", "pyyaml>=6.0", "tiktoken>=0.7.0",