From fe1e001878071870436bc080c70b689f2680812a Mon Sep 17 00:00:00 2001 From: Mark Randall Havens Date: Thu, 12 Mar 2026 19:36:25 +0000 Subject: [PATCH] Implement full Snowflake Method pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Stage 1: One sentence summary - Stage 2: One paragraph outline - Stage 3: Character sheets - Stage 4: Four-page outline - Stage 5: Detailed character charts - Stage 6: Scene list - Stage 7: Scene descriptions - Then: Style guide → Write chapters → Critique → Compile Full pre-writing workflow now wired up. --- opus_orchestrator/config.py | 28 +- opus_orchestrator/orchestrator.py | 521 +++++++++++++++++++++++------- opus_orchestrator/utils/llm.py | 47 +-- 3 files changed, 443 insertions(+), 153 deletions(-) diff --git a/opus_orchestrator/config.py b/opus_orchestrator/config.py index a46979a..0eeac87 100644 --- a/opus_orchestrator/config.py +++ b/opus_orchestrator/config.py @@ -73,18 +73,28 @@ def load_config_from_env() -> OpusConfig: Reads: - MINIMAX_API_KEY or OPENAI_API_KEY for LLM - GITHUB_TOKEN for GitHub operations - """ - # Load API keys - api_key = _load_env("MINIMAX_API_KEY") or _load_env("OPENAI_API_KEY") - github_token = _load_env("GITHUB_TOKEN") - # Determine provider - if _load_env("MINIMAX_API_KEY"): - provider = "minimax" - default_model = "MiniMax/MiniMax-M2.1" - else: + Prefers OPENAI_API_KEY if available (more reliable). + """ + # Load API keys - prefer OpenAI as MiniMax key may be invalid + openai_key = _load_env("OPENAI_API_KEY") + minimax_key = _load_env("MINIMAX_API_KEY") + + # Use OpenAI by default if available, otherwise try MiniMax + if openai_key: provider = "openai" default_model = "gpt-4o" + api_key = openai_key + elif minimax_key: + provider = "minimax" + default_model = "MiniMax/MiniMax-M2.1" + api_key = minimax_key + else: + provider = "openai" # default + default_model = "gpt-4o" + api_key = None + + github_token = _load_env("GITHUB_TOKEN") agent_config = AgentConfig( model=default_model, diff --git a/opus_orchestrator/orchestrator.py b/opus_orchestrator/orchestrator.py index 1905963..653f763 100644 --- a/opus_orchestrator/orchestrator.py +++ b/opus_orchestrator/orchestrator.py @@ -1,6 +1,15 @@ -"""Main Opus Orchestrator class.""" +"""Main Opus Orchestrator - Snowflake Method Implementation. -from __future__ import annotations +Full pipeline following the Snowflake Method for book writing: +1. One sentence (concept) +2. One paragraph (outline) +3. Character sheets +4. Four-page outline +5. Detailed character charts +6. Scene list +7. Scene descriptions +8. First draft +""" import asyncio import os @@ -9,7 +18,6 @@ from typing import Any, Optional from dotenv import load_dotenv -# Load local environment load_dotenv("/home/solaria/.openclaw/workspace/opus-orchestrator-ai/.env") from opus_orchestrator.agents.fiction import ( @@ -32,6 +40,7 @@ from opus_orchestrator.schemas import ( BookIntent, BookType, Chapter, + ChapterBlueprint, ChapterCritique, ChapterDraft, Manuscript, @@ -41,7 +50,7 @@ from opus_orchestrator.state import OpusState class OpusOrchestrator: - """Main orchestrator for AI book generation.""" + """Main orchestrator implementing the Snowflake Method.""" def __init__( self, @@ -49,15 +58,14 @@ class OpusOrchestrator: book_type: str = "fiction", genre: Optional[str] = None, target_audience: str = "general readers", - intended_outcome: str = "complete manuscript", + intended_outcome: str = "complete novel", tone: Optional[str] = None, target_word_count: int = 80000, config: Optional[OpusConfig] = None, ): - """Initialize the Opus Orchestrator.""" + """Initialize the Opus Orchestrator with Snowflake Method.""" self.config = config or get_config() - # Set API key from environment if not in config if not self.config.agent.api_key: self.config.agent.api_key = os.environ.get("MINIMAX_API_KEY") or os.environ.get("OPENAI_API_KEY") @@ -75,6 +83,15 @@ class OpusOrchestrator: self._init_agents() self.state: Optional[OpusState] = None + + # Snowflake method outputs + self.one_sentence: str = "" + self.one_paragraph: str = "" + self.character_sheets: str = "" + self.four_page_outline: str = "" + self.character_charts: str = "" + self.scene_list: str = "" + self.scene_descriptions: str = "" self.style_guide: str = "" def _init_agents(self) -> None: @@ -114,65 +131,275 @@ class OpusOrchestrator: return self.state - async def generate_blueprint(self) -> BookBlueprint: - """Generate the book blueprint using the Architect agent.""" - print(f"🧠 Generating blueprint with {self.config.agent.provider}/{self.config.agent.model}...") + # ========================================================================= + # SNOWFLAKE METHOD STAGES + # ========================================================================= - # Call Architect - architect = self.agents["architect"] - response = await architect.execute( - { - "raw_content": self.state.raw_content.text if self.state.raw_content else "", - "intent": self.intent.model_dump(), - }, + async def snowflake_stage_1(self) -> str: + """Stage 1: One sentence summary. + + Take your one-paragraph story summary and cut it down to one sentence. + """ + print("ā„ļø SNOWFLAKE STAGE 1: One sentence summary...") + + raw_content = self.state.raw_content.text if self.state.raw_content else "" + + user_prompt = f"""Create a ONE SENTENCE summary of this story concept. + +The sentence should contain: +- Protagonist's name (or descriptor) +- Their goal +- The conflict/obstacle +- The stakes + +Example: "In a world where magic is forbidden, a young mage must master forbidden arts to save her dying brother, even if it means sparking a war with the ruling theocracy." + +## Your seed content: +{raw_content} + +## Task: +Write ONE compelling sentence that captures the entire story. +""" + response = await self.agents["architect"].call_llm( + system_prompt="You are an expert story architect. Create concise, compelling summaries.", + user_prompt=user_prompt, + ) + + self.one_sentence = response.strip() + print(f" → {self.one_sentence}") + + return self.one_sentence + + async def snowflake_stage_2(self) -> str: + """Stage 2: One paragraph summary. + + Expand the one sentence to a paragraph with setup, 3 acts, and resolution. + """ + print("ā„ļø SNOWFLAKE STAGE 2: One paragraph outline...") + + user_prompt = f"""Expand this one-sentence summary into a full one-paragraph story outline. + +Include: +- Opening image (the "before" state) +- Setup (normal world, who the protagonist is) +- Catalyst (what changes everything) +- Rising action (attempts to solve the problem) +- Midpoint (major twist or revelation) +- Complications (things get worse) +- Crisis (lowest point) +- Resolution (how it ends) + +## One sentence: +{self.one_sentence} + +## Task: +Write one detailed paragraph (4-8 sentences) that tells the complete story arc. +""" + response = await self.agents["architect"].call_llm( + system_prompt="You are an expert story architect. Create detailed, act-structured outlines.", + user_prompt=user_prompt, + ) + + self.one_paragraph = response.strip() + print(f" → {self.one_paragraph[:200]}...") + + return self.one_paragraph + + async def snowflake_stage_3(self) -> str: + """Stage 3: Character sheets (one page per major character). + + Create character sheets for all major characters. + """ + print("ā„ļø SNOWFLAKE STAGE 3: Character sheets...") + + user_prompt = f"""Create character sheets for all major characters in this story. + +For each character, provide: +- Name +- Role (protagonist, antagonist, love interest, mentor, etc.) +- Age and physical description +- Background/history (2-3 sentences) +- Want (external goal) +- Need (internal growth) +- Fear +- Secret +- Character arc (how do they change?) + +## Story outline: +{self.one_paragraph} + +## Task: +Write comprehensive character sheets for all major characters. +""" + response = await self.agents["character_lead"].execute( + {"characters": [], "raw_content": self.one_paragraph}, {}, ) + + self.character_sheets = response.output if isinstance(response.output, str) else str(response.output) + print(f" → Created character sheets ({len(self.character_sheets)} chars)") + + return self.character_sheets - if not response.success: - raise Exception(f"Blueprint generation failed: {response.error}") + async def snowflake_stage_4(self) -> str: + """Stage 4: Four-page outline. + + Expand each sentence of the one-paragraph outline into a full page. + """ + print("ā„ļø SNOWFLAKE STAGE 4: Four-page outline...") + + user_prompt = f"""Expand this one-paragraph outline into a detailed four-page outline. - # Parse response into blueprint - # For now, create a basic blueprint from the response - blueprint = BookBlueprint( - title=self.intent.working_title or "Untitled", - genre=self.intent.genre or "general", - target_audience=self.intent.target_audience, - target_word_count=self.intent.target_word_count, - structure="three-act", - themes=[], - tone=self.intent.tone or "neutral", - chapters=[], +For each major section (setup, 3 acts, resolution), provide: +- Multiple scenes +- Character motivations +- Plot developments +- World details +- Dialogue hooks + +This should be approximately 4 pages worth of outline material. + +## Current outline: +{self.one_paragraph} + +## Characters: +{self.character_sheets[:1000]}... + +## Task: +Write a comprehensive four-page outline covering the entire story. +""" + response = await self.agents["architect"].call_llm( + system_prompt="You are an expert story architect. Create detailed, scene-by-scene outlines.", + user_prompt=user_prompt, ) - - # Try to extract chapters from response if it's detailed - response_text = response.output if isinstance(response.output, str) else str(response.output) - # Basic chapter structure (in real impl, would parse LLM output) - words_per_chapter = 3000 - num_chapters = max(3, self.intent.target_word_count // words_per_chapter) + self.four_page_outline = response.strip() + print(f" → Created four-page outline ({len(self.four_page_outline)} chars)") - for i in range(1, num_chapters + 1): - blueprint.chapters.append( - BookBlueprint.model_construct( - chapter_number=i, - title=f"Chapter {i}", - summary=f"Chapter {i} of the story", - word_count_target=words_per_chapter, - ) - ) + return self.four_page_outline - self.state.blueprint = blueprint - self.state.current_stage = "blueprint" - self.state.progress = 0.2 - - print(f"āœ… Blueprint generated: {num_chapters} chapters planned") + async def snowflake_stage_5(self) -> str: + """Stage 5: Detailed character charts. - return blueprint + Expand character sheets into full character charts with dialogue samples. + """ + print("ā„ļø SNOWFLAKE STAGE 5: Detailed character charts...") + + user_prompt = f"""Create detailed character charts for all major characters. + +For each character include: +- Full backstory +- Psychological profile +- Speech patterns (with sample dialogue) +- Character quirks +- Relationships with other characters +- How they appear to others vs. who they really are +- Key scenes they're in + +## Characters (basic): +{self.character_sheets} + +## Story outline: +{self.one_paragraph} + +## Task: +Write comprehensive, detailed character charts. +""" + response = await self.agents["character_lead"].execute( + {"characters": [], "raw_content": self.four_page_outline}, + {}, + ) + + self.character_charts = response.output if isinstance(response.output, str) else str(response.output) + print(f" → Created detailed character charts") + + return self.character_charts + + async def snowflake_stage_6(self) -> str: + """Stage 6: Scene list. + + Create a list of all scenes needed (like index cards). + """ + print("ā„ļø SNOWFLAKE STAGE 6: Scene list...") + + words_per_scene = 1500 # Average scene length + num_scenes = max(10, self.intent.target_word_count // words_per_scene) + + user_prompt = f"""Create a complete SCENE LIST for this story. + +For each scene, provide: +- Scene number +- POV character +- Setting/location +- What happens (one line) +- Purpose (advances plot? reveals character? builds world?) +- Chapter placement + +Target: approximately {num_scenes} scenes for a {self.intent.target_word_count:,} word novel. + +## Four-page outline: +{self.four_page_outline} + +## Characters: +{self.character_charts[:1000]}... + +## Task: +Create a comprehensive scene list with all scenes needed. +""" + response = await self.agents["architect"].call_llm( + system_prompt="You are an expert story architect. Create detailed scene lists.", + user_prompt=user_prompt, + ) + + self.scene_list = response.strip() + + # Parse scene count + scene_count = self.scene_list.count("Scene ") + self.scene_list.count("Chapter") + print(f" → Scene list created ({scene_count}+ scenes)") + + return self.scene_list + + async def snowflake_stage_7(self) -> str: + """Stage 7: Scene descriptions. + + Expand each scene into a full description (like index card back). + """ + print("ā„ļø SNOWFLAKE STAGE 7: Scene descriptions...") + + user_prompt = f"""Expand the scene list into detailed scene descriptions. + +For each scene, provide: +- Opening beat +- Key dialogue points +- Conflict moment +- Turning point +- Closing beat + +This is like writing the back of each index card - you know what happens but not the full prose. + +## Scene list: +{self.scene_list} + +## Characters: +{self.character_charts[:500]}... + +## Task: +Write detailed descriptions for key scenes (at least 20 most important scenes). +""" + response = await self.agents["architect"].call_llm( + system_prompt="You are an expert story architect. Create vivid scene descriptions.", + user_prompt=user_prompt, + ) + + self.scene_descriptions = response.strip() + print(f" → Scene descriptions created") + + return self.scene_descriptions async def create_style_guide(self) -> str: - """Create style guide using Voice agent.""" + """Create the style guide for prose.""" print("šŸŽØ Creating style guide...") - + voice = self.agents["voice"] response = await voice.execute( { @@ -188,23 +415,39 @@ class OpusOrchestrator: else: self.style_guide = "Professional fiction prose style." - print("āœ… Style guide created") + print(" āœ… Style guide created") return self.style_guide - async def write_chapter(self, chapter_num: int) -> ChapterDraft: - """Write a single chapter using Voice agent.""" - blueprint = self.state.blueprint - if not blueprint or chapter_num > len(blueprint.chapters): - raise ValueError(f"No blueprint or chapter {chapter_num} not found") - - chapter_spec = blueprint.chapters[chapter_num - 1] + async def write_chapter(self, chapter_num: int, total_chapters: int) -> ChapterDraft: + """Write a single chapter.""" + print(f"āœļø Writing chapter {chapter_num}/{total_chapters}...") - print(f"āœļø Writing chapter {chapter_num}/{len(blueprint.chapters)}...") + # Build chapter spec from our pre-writing + chapter_context = f""" +## Story context (from Snowflake pre-writing): +ONE SENTENCE: {self.one_sentence} +ONE PARAGRAPH: {self.one_paragraph} +SCENE LIST: {self.scene_list[:1000]}... +STYLE GUIDE: {self.style_guide[:500]}... + +## Task: +Write Chapter {chapter_num} following the scene list and style guide. +Make it vivid, engaging, and true to the characters. +""" + voice = self.agents["voice"] + target_words = self.intent.target_word_count // total_chapters + response = await voice.write_chapter( - chapter_spec.model_dump(), - self.style_guide, + { + "chapter_number": chapter_num, + "title": f"Chapter {chapter_num}", + "summary": f"Chapter {chapter_num} based on scene list", + "word_count_target": target_words, + "key_events": [], + }, + chapter_context, {}, ) @@ -215,41 +458,33 @@ class OpusOrchestrator: draft = ChapterDraft( chapter_number=chapter_num, - title=chapter_spec.title, + title=f"Chapter {chapter_num}", content=output.get("content", ""), word_count=output.get("word_count", len(output.get("content", "").split())), ) self.state.drafts[chapter_num] = draft - - progress = 0.2 + (0.6 * chapter_num / len(blueprint.chapters)) + progress = 0.5 + (0.4 * chapter_num / total_chapters) self.state.progress = progress - print(f"āœ… Chapter {chapter_num} written: {draft.word_count} words") + print(f" āœ… Chapter {chapter_num}: {draft.word_count} words") return draft async def critique_chapter(self, chapter_num: int) -> ChapterCritique: - """Critique a chapter using Editor agent.""" + """Critique a chapter.""" draft = self.state.drafts.get(chapter_num) if not draft: raise ValueError(f"No draft for chapter {chapter_num}") - print(f"šŸ” Critiquing chapter {chapter_num}...") - editor = self.agents["editor"] response = await editor.review_chapter( draft.model_dump(), - { - "title": self.state.blueprint.title if self.state.blueprint else "Untitled", - "genre": self.intent.genre or "general", - "total_chapters": len(self.state.blueprint.chapters) if self.state.blueprint else 0, - }, + {"title": self.one_sentence, "genre": self.intent.genre or "general", "total_chapters": len(self.state.blueprint.chapters) if self.state.blueprint else 0}, {}, ) if not response.success: - # Return a default critique if it fails return ChapterCritique( chapter_number=chapter_num, overall_score=0.7, @@ -274,29 +509,19 @@ class OpusOrchestrator: self.state.critiques[chapter_num] = [] self.state.critiques[chapter_num].append(critique) - print(f"āœ… Chapter {chapter_num} critiqued: score {critique.overall_score:.2f}") - return critique async def iterate_chapter(self, chapter_num: int, max_iterations: int = 2) -> Chapter: - """Iterate on a chapter until approved or max iterations reached.""" - draft = self.state.drafts.get(chapter_num) - + """Iterate on a chapter.""" for iteration in range(1, max_iterations + 1): - print(f"šŸ”„ Iteration {iteration}/{max_iterations} for chapter {chapter_num}") - - # Critique critique = await self.critique_chapter(chapter_num) - # Check if approved if critique.overall_score >= self.config.iteration.approval_threshold: - print(f"āœ… Chapter {chapter_num} approved!") + print(f" āœ… Chapter {chapter_num} approved! (score: {critique.overall_score:.2f})") break - - # If not approved and have more iterations, could revise here - # For now, we'll proceed with what we have + else: + print(f" šŸ”„ Iteration {iteration}: score {critique.overall_score:.2f}") - # Get final draft draft = self.state.drafts.get(chapter_num) return Chapter( @@ -306,21 +531,43 @@ class OpusOrchestrator: word_count=draft.word_count, ) + async def generate_blueprint(self) -> BookBlueprint: + """Generate the book blueprint.""" + words_per_chapter = 3000 + num_chapters = max(3, self.intent.target_word_count // words_per_chapter) + + blueprint = BookBlueprint( + title=self.intent.working_title or "Untitled", + genre=self.intent.genre or "general", + target_audience=self.intent.target_audience, + target_word_count=self.intent.target_word_count, + structure="three-act", + themes=[], + tone=self.intent.tone or "neutral", + chapters=[ + BookBlueprint.model_construct( + chapter_number=i, + title=f"Chapter {i}", + summary=f"Chapter {i}", + word_count_target=words_per_chapter, + ) + for i in range(1, num_chapters + 1) + ], + ) + + self.state.blueprint = blueprint + self.state.current_stage = "blueprint" + self.state.progress = 0.1 + + return blueprint + async def compile_manuscript(self) -> Manuscript: """Compile all chapters into final manuscript.""" - if not self.state.blueprint: - raise ValueError("No blueprint. Run generate_blueprint first.") - num_chapters = len(self.state.blueprint.chapters) - print(f"\nšŸ“š Compiling manuscript: {num_chapters} chapters\n") - - chapters = [] + chapters = [] for i in range(1, num_chapters + 1): - # Write chapter - await self.write_chapter(i) - - # Iterate/critique + await self.write_chapter(i, num_chapters) chapter = await self.iterate_chapter(i) chapters.append(chapter) @@ -330,49 +577,81 @@ class OpusOrchestrator: genre=self.intent.genre or "general", chapters=chapters, total_word_count=sum(c.word_count for c in chapters), + frontmatter={ + "one_sentence": self.one_sentence, + "one_paragraph": self.one_paragraph, + "include_toc": True, + }, ) self.state.manuscript = manuscript self.state.current_stage = "complete" self.state.progress = 1.0 - print(f"\nāœ… Manuscript complete: {manuscript.total_word_count} words") - return manuscript - async def run(self) -> Manuscript: - """Run the full orchestrator pipeline.""" - print(f"\n{'='*50}") - print("šŸŽÆ OPUS ORCHESTRATOR - Starting") - print(f"{'='*50}\n") + # ========================================================================= + # MAIN RUN METHOD - FULL SNOWFLAKE + # ========================================================================= + + async def run(self) -> Manuscript: + """Run the full Snowflake Method pipeline.""" + print(f"\n{'='*60}") + print("ā„ļø OPUS ORCHESTRATOR - SNOWFLAKE METHOD") + print(f"{'='*60}\n") - # Ingest await self.ingest() + # Pre-writing stages (Snowflake 1-7) + await self.snowflake_stage_1() # One sentence + await self.snowflake_stage_2() # One paragraph + await self.snowflake_stage_3() # Character sheets + await self.snowflake_stage_4() # Four-page outline + await self.snowflake_stage_5() # Detailed character charts + await self.snowflake_stage_6() # Scene list + await self.snowflake_stage_7() # Scene descriptions + + # Style and writing + await self.create_style_guide() + # Generate blueprint await self.generate_blueprint() - # Create style guide - await self.create_style_guide() - - # Write and iterate chapters + # Write and critique chapters manuscript = await self.compile_manuscript() - print(f"\n{'='*50}") - print("šŸŽ‰ OPUS ORCHESTRATOR - Complete!") - print(f"{'='*50}\n") + print(f"\n{'='*60}") + print("āœ… SNOWFLAKE COMPLETE!") + print(f"{'='*60}") + print(f"šŸ“– Title: {manuscript.title}") + print(f"šŸ“„ Words: {manuscript.total_word_count:,}") + print(f"šŸ“‘ Chapters: {len(manuscript.chapters)}") return manuscript def save_manuscript(self, output_path: Optional[Path] = None) -> Path: - """Save manuscript to file.""" + """Save manuscript and pre-writing to files.""" if not self.state.manuscript: raise ValueError("No manuscript to save. Run first.") - output_path = output_path or Path("./output") / f"{self.state.manuscript.title.lower().replace(' ', '_')}.md" - output_path.parent.mkdir(parents=True, exist_ok=True) + output_dir = output_path or Path("./output") + output_dir.mkdir(parents=True, exist_ok=True) - with open(output_path, "w") as f: + # Save manuscript + manuscript_path = output_dir / f"{self.state.manuscript.title.lower().replace(' ', '_')}.md" + with open(manuscript_path, "w") as f: f.write(self.state.manuscript.to_markdown()) - return output_path + # Save pre-writing + prewriting_path = output_dir / f"{self.state.manuscript.title.lower().replace(' ', '_')}_prewriting.md" + with open(prewriting_path, "w") as f: + f.write(f"# Pre-Writing: {self.state.manuscript.title}\n\n") + f.write(f"## Stage 1: One Sentence\n{self.one_sentence}\n\n") + f.write(f"## Stage 2: One Paragraph\n{self.one_paragraph}\n\n") + f.write(f"## Stage 3: Character Sheets\n{self.character_sheets}\n\n") + f.write(f"## Stage 4: Four-Page Outline\n{self.four_page_outline}\n\n") + f.write(f"## Stage 5: Character Charts\n{self.character_charts}\n\n") + f.write(f"## Stage 6: Scene List\n{self.scene_list}\n\n") + f.write(f"## Stage 7: Scene Descriptions\n{self.scene_descriptions}\n\n") + + return manuscript_path diff --git a/opus_orchestrator/utils/llm.py b/opus_orchestrator/utils/llm.py index 7288617..9ff10d4 100644 --- a/opus_orchestrator/utils/llm.py +++ b/opus_orchestrator/utils/llm.py @@ -19,18 +19,16 @@ class LLMClient: model: str = "MiniMax/MiniMax-M2.1", base_url: Optional[str] = None, ): - """Initialize LLM client. - - Args: - api_key: API key for the provider - provider: Provider name (minimax, openai, anthropic) - model: Model identifier - base_url: Optional custom base URL - """ + """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 + # 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 if base_url: self.base_url = base_url @@ -50,17 +48,7 @@ class LLMClient: temperature: float = 0.7, max_tokens: Optional[int] = None, ) -> str: - """Make a completion request. - - Args: - system_prompt: System prompt - user_prompt: User prompt - temperature: Sampling temperature - max_tokens: Maximum tokens to generate - - Returns: - Generated text - """ + """Make a completion request.""" headers = { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", @@ -86,9 +74,9 @@ class LLMClient: headers: dict, ) -> str: """Call MiniMax API.""" - # MiniMax uses chat/completions format + # MiniMax chat completion format payload = { - "model": self.model, + "model": self.minimax_model, "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, @@ -104,10 +92,23 @@ class LLMClient: headers=headers, json=payload, ) - response.raise_for_status() + + # Debug output + 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() - return data["choices"][0]["message"]["content"] + + # Handle different response formats + if "choices" in data: + return data["choices"][0]["message"]["content"] + elif "choices" in data.get("data", {}): + return data["data"]["choices"][0]["message"]["content"] + else: + # Try to find content in response + raise Exception(f"Unexpected MiniMax response: {data}") async def _complete_openai( self,