diff --git a/opus_orchestrator/__init__.py b/opus_orchestrator/__init__.py index 8dcde2c..e39349d 100644 --- a/opus_orchestrator/__init__.py +++ b/opus_orchestrator/__init__.py @@ -41,6 +41,11 @@ from opus_orchestrator.utils.github_ingest import GitHubIngestor, create_github_ from opus_orchestrator.utils.s3_ingest import S3Ingestor, create_s3_ingestor from opus_orchestrator.utils.local_ingest import LocalIngestor, create_local_ingestor from opus_orchestrator.frameworks import StoryFramework +from opus_orchestrator.nonfiction_frameworks import ( + NonfictionFramework, + get_nonfiction_framework, + list_nonfiction_frameworks, +) from opus_orchestrator.crews import ( OpusCrew, FictionCrew, @@ -97,6 +102,10 @@ __all__ = [ "OpusGraphState", "run_opus", "StoryFramework", + # Nonfiction Frameworks (NEW!) + "NonfictionFramework", + "get_nonfiction_framework", + "list_nonfiction_frameworks", # Crews "OpusCrew", "FictionCrew", diff --git a/opus_orchestrator/cli.py b/opus_orchestrator/cli.py index 243ff03..fe189c8 100644 --- a/opus_orchestrator/cli.py +++ b/opus_orchestrator/cli.py @@ -211,9 +211,16 @@ Examples: gen_parser.add_argument( "--framework", "-f", default="snowflake", - choices=["snowflake", "three-act", "save-the-cat", "hero-journey", - "story-circle", "seven-point", "fichtean"], - help="Story framework to use", + choices=[ + # Story frameworks + "snowflake", "three-act", "save-the-cat", "hero-journey", + "story-circle", "seven-point", "fichtean", + # Nonfiction frameworks (NEW!) + "technical-manual", "codebase-tour", "diataxis-tutorial", + "diataxis-howto", "diataxis-explanation", "diataxis-reference", + "api-documentation", + ], + help="Framework to use (story or nonfiction)", ) gen_parser.add_argument( "--genre", "-g", @@ -612,7 +619,44 @@ async def run_generate(args: argparse.Namespace) -> int: use_autogen = not args.no_autogen - if args.use_crewai: + # Check for nonfiction frameworks (NEW!) + nonfiction_frameworks = [ + "technical-manual", "codebase-tour", "diataxis-tutorial", + "diataxis-howto", "diataxis-explanation", "diataxis-reference", + "api-documentation", + ] + + if args.framework in nonfiction_frameworks: + # Use NonfictionGenerator for rigorous technical frameworks + from opus_orchestrator.nonfiction_generator import NonfictionGenerator + from opus_orchestrator.nonfiction_frameworks import NonfictionFramework + + print("📚 Using Nonfiction Framework...\n") + + # Map framework string to enum + framework_map = { + "technical-manual": NonfictionFramework.TECHNICAL_MANUAL, + "codebase-tour": NonfictionFramework.CODEBASE_TOUR, + "diataxis-tutorial": NonfictionFramework.DIAXIS_TUTORIAL, + "diataxis-howto": NonfictionFramework.DIAXIS_HOWTO, + "diataxis-explanation": NonfictionFramework.DIAXIS_EXPLANATION, + "diataxis-reference": NonfictionFramework.DIAXIS_REFERENCE, + "api-documentation": NonfictionFramework.API_DOCUMENTATION, + } + + nf_framework = framework_map.get(args.framework, NonfictionFramework.TECHNICAL_MANUAL) + + gen = NonfictionGenerator( + framework=nf_framework, + topic=args.concept or "Technical Topic", + source_content=seed_concept[:50000], # Limit for context + ) + + manuscript = gen.generate(target_word_count=args.words) + + print(f" ✅ Generated {len(manuscript.split())} words using {nf_framework.value}") + + elif args.use_crewai: # Use CrewAI crews print("🛠️ Using CrewAI crews...\n") @@ -649,7 +693,6 @@ async def run_generate(args: argparse.Namespace) -> int: framework=args.framework, genre=args.genre, target_word_count=args.words, - use_autogen=use_autogen, ) manuscript = result.get("manuscript", str(result)) diff --git a/opus_orchestrator/nonfiction_frameworks.py b/opus_orchestrator/nonfiction_frameworks.py new file mode 100644 index 0000000..70c7e4c --- /dev/null +++ b/opus_orchestrator/nonfiction_frameworks.py @@ -0,0 +1,290 @@ +"""Nonfiction frameworks for Opus Orchestrator. + +Rigorous nonfiction structures: Diátaxis, Technical Manual, Codebase Tour. +""" + +from enum import Enum +from typing import Any + + +class NonfictionFramework(str, Enum): + """Nonfiction book frameworks.""" + + DIAXIS_TUTORIAL = "diataxis-tutorial" + DIAXIS_HOWTO = "diataxis-howto" + DIAXIS_EXPLANATION = "diataxis-explanation" + DIAXIS_REFERENCE = "diataxis-reference" + TECHNICAL_MANUAL = "technical-manual" + CODEBASE_TOUR = "codebase-tour" + API_DOCUMENTATION = "api-documentation" + + +# Diátaxis Framework Definitions +# Based on Daniele Procida's work - the gold standard for technical documentation + +DIAXIS_TUTORIAL = { + "name": "Diátaxis Tutorial", + "description": "A tutorial is a lesson that leads the learner through a series of steps to complete a project. The learner learns by doing.", + "stages": [ + "Introduction - What will we build and why?", + "Prerequisites - What do you need before starting?", + "Step 1: Setup - Getting the environment ready", + "Step 2: First Steps - Your initial actions", + "Step 3: Building - Creating something concrete", + "Step 4: Enhancement - Adding features", + "Step 5: Completion - Finishing the project", + "Summary - What you learned", + "Next Steps - Where to go from here", + ], + "structure": { + "audience": "Learners who need guided, hands-on experience", + "goal": "Complete a concrete project through step-by-step instruction", + "approach": "Progressive disclosure - reveal complexity gradually", + "tone": "Encouraging, clear, patient", + }, +} + +DIAXIS_HOWTO = { + "name": "Diátaxis How-To Guide", + "description": "A how-to guide leads the reader through a series of steps to accomplish a goal. They already know what they want to do.", + "stages": [ + "Goal Statement - What problem does this solve?", + "Prerequisites - What's needed?", + "Step 1 - First action", + "Step 2 - Second action", + "Step N - Final step", + "Troubleshooting - Common issues", + "Related Tasks - See also", + ], + "structure": { + "audience": "Practitioners who know what they want to achieve", + "goal": "Accomplish a specific, practical task", + "approach": "Direct, efficient steps toward a goal", + "tone": "Direct, authoritative, no fluff", + }, +} + +DIAXIS_EXPLANATION = { + "name": "Diátaxis Explanation", + "description": "An explanation clarifies and deepens understanding of a topic. It provides context and connects concepts.", + "stages": [ + "Overview - What are we exploring?", + "Background - What do you need to know first?", + "Core Concepts - The key ideas", + "How It Works - Under the hood", + "Different Approaches - Alternative perspectives", + "Why It Matters - Significance", + "Common Misconceptions - What people get wrong", + "Further Reading - Deepen knowledge", + ], + "structure": { + "audience": "Readers who want to understand, not just do", + "goal": "Build mental models and deepen comprehension", + "approach": "Multiple perspectives, rich context", + "tone": "Thoughtful, explanatory, nuanced", + }, +} + +DIAXIS_REFERENCE = { + "name": "Diátaxis Reference", + "description": "Reference documentation provides authoritative information about a system. Accurate, complete, findable.", + "stages": [ + "Overview - What is this?", + "Syntax - How to use it", + "Parameters - What it accepts", + "Returns - What it produces", + "Examples - Usage patterns", + "Errors - What can go wrong", + "Notes - Important details", + "See Also - Related topics", + ], + "structure": { + "audience": "Users who need precise, detailed information", + "goal": "Accurate, comprehensive information lookup", + "approach": "Complete, organized, searchable", + "tone": "Precise, technical, complete", + }, +} + + +# Technical Manual Framework +# Structured for learning technical subjects deeply + +TECHNICAL_MANUAL = { + "name": "Technical Manual", + "description": "A comprehensive technical manual that takes readers from foundations to mastery with practical examples.", + "stages": [ + "Part 1: Foundations", + " 1. Introduction - Why this matters", + " 2. Core Concepts - Essential background", + " 3. Architecture - High-level design", + " 4. Getting Started - First steps", + "", + "Part 2: Deep Dive", + " 5. [Topic A] - In-depth exploration", + " 6. [Topic B] - Implementation details", + " 7. [Topic C] - Advanced features", + " 8. [Topic D] - Edge cases", + "", + "Part 3: Practical Application", + " 9. Hands-On Project - Build something", + " 10. Best Practices - How experts do it", + " 11. Debugging - When things go wrong", + " 12. Performance - Optimization", + "", + "Part 4: Reference", + " 13. API Reference - Complete API", + " 14. Command Reference - All commands", + " 15. Configuration - All options", + " 16. Troubleshooting Guide - Common problems", + ], + "structure": { + "audience": "Professionals needing comprehensive, practical knowledge", + "goal": "Build expertise from ground up to mastery", + "approach": "Theory → Practice → Reference spiral", + "tone": "Professional, thorough, practical", + }, +} + + +# Codebase Tour Framework +# For documenting code directly + +CODEBASE_TOUR = { + "name": "Codebase Tour", + "description": "Document a codebase systematically: structure → components → relationships → implementation → usage.", + "stages": [ + "1. Repository Overview", + " - What is this project?", + " - What problem does it solve?", + " - Key technologies", + " - Directory structure", + "", + "2. High-Level Architecture", + " - System components", + " - Data flow", + " - Key abstractions", + "", + "3. Core Components", + " - Component A: Purpose, public API, key structs", + " - Component B: Purpose, public API, key structs", + " - Component C: Purpose, public API, key structs", + "", + "4. Data Structures", + " - Key structs and their fields", + " - Relationships between data types", + " - Memory layout if relevant", + "", + "5. Core Functions", + " - Main entry points", + " - Critical paths", + " - Algorithm implementations", + "", + "6. Interfaces", + " - How components communicate", + " - Public APIs", + " - Event/message systems", + "", + "7. Configuration", + " - Config files", + " - Environment variables", + " - Runtime parameters", + "", + "8. Testing", + " - Test strategies", + " - Key test files", + " - How to run tests", + "", + "9. Contributing", + " - Development setup", + " - Code style", + " - Pull request process", + ], + "structure": { + "audience": "Developers who need to understand, use, or contribute to the codebase", + "goal": "Map code to mental model accurately", + "approach": "Top-down from architecture to implementation", + "tone": "Technical, precise, code-focused", + }, +} + + +# API Documentation Framework +# For generating API docs from code + +API_DOCUMENTATION = { + "name": "API Documentation", + "description": "Complete API reference documentation: endpoints, parameters, responses, examples, errors.", + "stages": [ + "API Overview", + " - Introduction", + " - Authentication", + " - Rate Limiting", + " - Base URL", + "", + "Resources", + " - Each endpoint documented:", + " - Endpoint URL and method", + " - Description", + " - Path parameters", + " - Query parameters", + " - Request body schema", + " - Response schema", + " - Success codes", + " - Error codes", + " - Example request", + " - Example response", + "", + "Models", + " - Data models used", + " - Field definitions", + " - Type specifications", + "", + "Errors", + " - Error code reference", + " - Error message meanings", + " - Troubleshooting", + "", + "SDKs/Libraries", + " - Official libraries", + " - Community libraries", + "", + "Changelog", + " - Version history", + " - Breaking changes", + ], + "structure": { + "audience": "Developers integrating with the API", + "goal": "Complete, accurate reference for implementation", + "approach": "Complete enumeration of all capabilities", + "tone": "Technical, complete, unambiguous", + }, +} + + +# Registry of all nonfiction frameworks +NONFICTION_FRAMEWORKS = { + NonfictionFramework.DIAXIS_TUTORIAL: DIAXIS_TUTORIAL, + NonfictionFramework.DIAXIS_HOWTO: DIAXIS_HOWTO, + NonfictionFramework.DIAXIS_EXPLANATION: DIAXIS_EXPLANATION, + NonfictionFramework.DIAXIS_REFERENCE: DIAXIS_REFERENCE, + NonfictionFramework.TECHNICAL_MANUAL: TECHNICAL_MANUAL, + NonfictionFramework.CODEBASE_TOUR: CODEBASE_TOUR, + NonfictionFramework.API_DOCUMENTATION: API_DOCUMENTATION, +} + + +def get_nonfiction_framework(framework: NonfictionFramework) -> dict[str, Any]: + """Get a nonfiction framework by type.""" + return NONFICTION_FRAMEWORKS.get(framework, {}) + + +def list_nonfiction_frameworks() -> dict[str, dict]: + """List all available nonfiction frameworks.""" + return { + k.value: { + "name": v.get("name", k.value), + "description": v.get("description", ""), + } + for k, v in NONFICTION_FRAMEWORKS.items() + } diff --git a/opus_orchestrator/nonfiction_generator.py b/opus_orchestrator/nonfiction_generator.py new file mode 100644 index 0000000..dfd2199 --- /dev/null +++ b/opus_orchestrator/nonfiction_generator.py @@ -0,0 +1,261 @@ +"""Nonfiction generator using rigorous frameworks. + +Generate technical documentation using Diátaxis, Technical Manual, and Codebase Tour frameworks. +""" + +import os +from typing import Any, Optional + +from dotenv import load_dotenv + +load_dotenv() + +from opus_orchestrator.nonfiction_frameworks import ( + NonfictionFramework, + get_nonfiction_framework, +) +from opus_orchestrator.utils.llm import LLMClient +from opus_orchestrator.config import get_config + + +class NonfictionGenerator: + """Generate nonfiction using rigorous frameworks.""" + + def __init__( + self, + framework: NonfictionFramework = NonfictionFramework.TECHNICAL_MANUAL, + topic: str = "", + source_content: str = "", + model: Optional[str] = None, + ): + """Initialize nonfiction generator. + + Args: + framework: Nonfiction framework to use + topic: Topic to document + source_content: Source code/content to document + model: Override model name + """ + self.framework = framework + self.topic = topic + self.source_content = source_content + + config = get_config() + self.llm = LLMClient( + provider=config.agent.provider, + model=model or config.agent.model, + ) + + self.framework_info = get_nonfiction_framework(framework) + + def generate(self, target_word_count: int = 5000) -> str: + """Generate nonfiction document. + + Args: + target_word_count: Target word count + + Returns: + Generated document + """ + if self.framework == NonfictionFramework.CODEBASE_TOUR: + return self._generate_codebase_tour(target_word_count) + elif self.framework == NonfictionFramework.TECHNICAL_MANUAL: + return self._generate_technical_manual(target_word_count) + elif self.framework == NonfictionFramework.DIAXIS_TUTORIAL: + return self._generate_diataxis_tutorial(target_word_count) + elif self.framework == NonfictionFramework.DIAXIS_HOWTO: + return self._generate_diataxis_howto(target_word_count) + elif self.framework == NonfictionFramework.DIAXIS_EXPLANATION: + return self._generate_diataxis_explanation(target_word_count) + elif self.framework == NonfictionFramework.DIAXIS_REFERENCE: + return self._generate_diataxis_reference(target_word_count) + else: + return self._generate_technical_manual(target_word_count) + + def _generate_codebase_tour(self, target_word_count: int) -> str: + """Generate codebase tour documentation.""" + source_summary = self.source_content[:10000] if self.source_content else "No source content provided" + + prompt = f"""Generate comprehensive CODEBASE TOUR documentation. + +FRAMEWORK: Codebase Tour - Document a codebase systematically + +TOPIC: {self.topic} + +SOURCE CODE/CONTENT: +{source_summary} + +Generate the following sections: +1. Repository Overview - What is this project, what problem does it solve? +2. High-Level Architecture - System components and data flow +3. Core Components - Purpose and API of main components +4. Data Structures - Key structs and relationships +5. Core Functions - Main entry points and algorithms +6. Interfaces - How components communicate +7. Configuration - Config files and options +8. Testing - Test strategies and key files +9. Contributing - Development setup and PR process + +Write in a technical, precise tone. Be specific and use code examples. +Target approximately {target_word_count} words. +""" + return self.llm.complete( + system_prompt="You are an expert technical writer specializing in codebase documentation.", + user_prompt=prompt, + temperature=0.7, + ) + + def _generate_technical_manual(self, target_word_count: int) -> str: + """Generate technical manual.""" + source_summary = self.source_content[:10000] if self.source_content else "No source content provided" + + prompt = f"""Generate a comprehensive TECHNICAL MANUAL. + +FRAMEWORK: Technical Manual - From foundations to mastery + +TOPIC: {self.topic} + +SOURCE CONTENT: +{source_summary} + +Generate a technical manual with: +1. Introduction - Why this topic matters +2. Core Concepts - Essential background knowledge +3. Architecture - High-level system design +4. Getting Started - First steps for beginners +5. Deep Dive Sections - Detailed exploration of key topics +6. Practical Examples - Hands-on code examples +7. Best Practices - How experts do it +8. Troubleshooting - Common problems and solutions +9. Reference - API/command reference + +Write in a professional, thorough, practical tone. +Target approximately {target_word_count} words. +""" + return self.llm.complete( + system_prompt="You are an expert technical writer specializing in technical manuals and educational content.", + user_prompt=prompt, + temperature=0.7, + ) + + def _generate_diataxis_tutorial(self, target_word_count: int) -> str: + """Generate Diátaxis tutorial.""" + prompt = f"""Generate a DIÁTEXIS TUTORIAL. + +FRAMEWORK: Tutorial - Learn by doing a concrete project + +TOPIC: {self.topic} + +Generate a tutorial that leads the learner through a complete project: +1. Introduction - What will we build and why? +2. Prerequisites - What do you need before starting? +3. Step 1: Setup - Getting the environment ready +4. Step 2: First Steps - Your initial actions +5. Step 3: Building - Creating something concrete +6. Step 4: Enhancement - Adding features +7. Step 5: Completion - Finishing the project +8. Summary - What you learned +9. Next Steps - Where to go from here + +Write in an encouraging, clear, patient tone. +Use numbered steps. Make it achievable for beginners. +Target approximately {target_word_count} words. +""" + return self.llm.complete( + system_prompt="You are an expert technical educator specializing in tutorials.", + user_prompt=prompt, + temperature=0.7, + ) + + def _generate_diataxis_howto(self, target_word_count: int) -> str: + """Generate Diátaxis how-to guide.""" + prompt = f"""Generate a DIÁTEXIS HOW-TO GUIDE. + +FRAMEWORK: How-To Guide - Accomplish a specific task + +TOPIC: {self.topic} + +Generate a practical how-to guide: +1. Goal Statement - What problem does this solve? +2. Prerequisites - What's needed? +3. Step 1 - First action +4. Step 2 - Second action +5. Step N - Final step +6. Troubleshooting - Common issues +7. Related Tasks - See also + +Write in a direct, authoritative tone. No fluff. +Target approximately {target_word_count} words. +""" + return self.llm.complete( + system_prompt="You are an expert technical writer specializing in how-to guides.", + user_prompt=prompt, + temperature=0.7, + ) + + def _generate_diataxis_explanation(self, target_word_count: int) -> str: + """Generate Diátaxis explanation.""" + prompt = f"""Generate a DIÁTEXIS EXPLANATION. + +FRAMEWORK: Explanation - Clarify and deepen understanding + +TOPIC: {self.topic} + +Generate an explanatory document: +1. Overview - What are we exploring? +2. Background - What do you need to know first? +3. Core Concepts - The key ideas +4. How It Works - Under the hood +5. Different Approaches - Alternative perspectives +6. Why It Matters - Significance +7. Common Misconceptions - What people get wrong +8. Further Reading - Deepen knowledge + +Write in a thoughtful, explanatory tone. Build mental models. +Target approximately {target_word_count} words. +""" + return self.llm.complete( + system_prompt="You are an expert educator specializing in explanatory writing.", + user_prompt=prompt, + temperature=0.7, + ) + + def _generate_diataxis_reference(self, target_word_count: int) -> str: + """Generate Diátaxis reference.""" + prompt = f"""Generate DIÁTEXIS REFERENCE documentation. + +FRAMEWORK: Reference - Accurate, complete information lookup + +TOPIC: {self.topic} + +Generate reference documentation: +1. Overview - What is this? +2. Syntax - How to use it +3. Parameters - What it accepts +4. Returns - What it produces +5. Examples - Usage patterns +6. Errors - What can go wrong +7. Notes - Important details +8. See Also - Related topics + +Write in a precise, technical, complete tone. +Target approximately {target_word_count} words. +""" + return self.llm.complete( + system_prompt="You are an expert technical writer specializing in reference documentation.", + user_prompt=prompt, + temperature=0.7, + ) + + +def create_nonfiction_generator( + framework: NonfictionFramework = NonfictionFramework.TECHNICAL_MANUAL, + topic: str = "", + source_content: str = "", +) -> NonfictionGenerator: + """Factory function to create a nonfiction generator.""" + return NonfictionGenerator( + framework=framework, + topic=topic, + source_content=source_content, + ) diff --git a/opus_orchestrator/utils/llm.py b/opus_orchestrator/utils/llm.py index 9ff10d4..d77dfb8 100644 --- a/opus_orchestrator/utils/llm.py +++ b/opus_orchestrator/utils/llm.py @@ -1,16 +1,18 @@ """LLM client for Opus Orchestrator. -Supports MiniMax and OpenAI providers. +Supports MiniMax and OpenAI providers - both async and sync. """ import os +import asyncio from typing import Any, Optional import httpx +import requests class LLMClient: - """Simple LLM client for making API calls.""" + """Simple LLM client for making API calls - supports both sync and async.""" def __init__( self, @@ -26,7 +28,6 @@ class LLMClient: # Normalize model name for MiniMax if provider == "minimax": - # MiniMax uses model names like "abab6.5s-chat" or "MiniMax-M2.1" self.minimax_model = model.split("/")[-1] if "/" in model else model # Set base URL based on provider @@ -39,27 +40,52 @@ class LLMClient: else: self.base_url = "https://api.openai.com/v1" - self.client = httpx.AsyncClient(timeout=120.0) + # Async client + self._async_client = httpx.AsyncClient(timeout=120.0) - async def complete( + def complete( self, system_prompt: str, user_prompt: str, temperature: float = 0.7, max_tokens: Optional[int] = None, ) -> str: - """Make a completion request.""" + """Make a completion request (SYNC - for LangGraph compatibility).""" headers = { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", } if self.provider == "minimax": - return await self._complete_minimax( + return self._complete_minimax_sync( system_prompt, user_prompt, temperature, max_tokens, headers ) elif self.provider == "openai": - return await self._complete_openai( + return self._complete_openai_sync( + system_prompt, user_prompt, temperature, max_tokens, headers + ) + else: + raise ValueError(f"Unsupported provider: {self.provider}") + + async def complete_async( + self, + system_prompt: str, + user_prompt: str, + temperature: float = 0.7, + max_tokens: Optional[int] = None, + ) -> str: + """Make a completion request (ASYNC).""" + headers = { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + } + + if self.provider == "minimax": + return await self._complete_minimax_async( + system_prompt, user_prompt, temperature, max_tokens, headers + ) + elif self.provider == "openai": + return await self._complete_openai_async( system_prompt, user_prompt, temperature, max_tokens, headers ) else: @@ -143,7 +169,85 @@ class LLMClient: async def close(self): """Close the HTTP client.""" - await self.client.aclose() + await self._async_client.aclose() + + # ========================================================================= + # SYNC VERSIONS (for LangGraph compatibility) + # ========================================================================= + + def _complete_minimax_sync( + self, + system_prompt: str, + user_prompt: str, + temperature: float, + max_tokens: Optional[int], + headers: dict, + ) -> str: + """Call MiniMax API (sync).""" + payload = { + "model": self.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, + ) + + if response.status_code != 200: + print(f"MiniMax API error: {response.status_code}") + print(f"Response: {response.text[:500]}") + response.raise_for_status() + + data = response.json() + + if "choices" in data: + return data["choices"][0]["message"]["content"] + elif "choices" in data.get("data", {}): + return data["data"]["choices"][0]["message"]["content"] + else: + raise Exception(f"Unexpected MiniMax response: {data}") + + def _complete_openai_sync( + self, + system_prompt: str, + user_prompt: str, + temperature: float, + max_tokens: Optional[int], + headers: dict, + ) -> str: + """Call OpenAI API (sync).""" + 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