Files
opus-orchestrator-ai/opus_orchestrator/orchestrator.py
T
mrhavens 265b8934a0 Add multiple story frameworks
- Snowflake Method (already had)
- Three-Act Structure
- Save the Cat (Blake Snyder)
- Hero's Journey (Joseph Campbell)
- Story Circle (Dan Harmon)
- The 7-Point Plot (The Pantone)
- Fichtean Curve

Pass framework='hero-journey' or 'save-the-cat' etc. to use.
2026-03-12 19:52:47 +00:00

694 lines
24 KiB
Python

"""Main Opus Orchestrator - Snowflake Method Implementation with Multiple Frameworks.
Full pipeline supporting multiple story frameworks:
- Snowflake Method (fractal expansion)
- Three-Act Structure
- Save the Cat (Blake Snyder)
- Hero's Journey (Joseph Campbell)
- Story Circle (Dan Harmon)
- The 7-Point Plot (The Pantone)
- Fichtean Curve
"""
import asyncio
import os
from pathlib import Path
from typing import Any, Optional
from dotenv import load_dotenv
load_dotenv("/home/solaria/.openclaw/workspace/opus-orchestrator-ai/.env")
from opus_orchestrator.agents.fiction import (
ArchitectAgent,
CharacterLeadAgent,
EditorAgent,
VoiceAgent,
WorldsmithAgent,
)
from opus_orchestrator.agents.nonfiction import (
AnalystAgent,
FactCheckerAgent,
NonfictionEditorAgent,
NonfictionWriterAgent,
ResearcherAgent,
)
from opus_orchestrator.config import OpusConfig, get_config
from opus_orchestrator.frameworks import (
StoryFramework,
FRAMEWORKS,
get_framework_for_genre,
get_framework_prompt,
)
from opus_orchestrator.schemas import (
BookBlueprint,
BookIntent,
BookType,
Chapter,
ChapterBlueprint,
ChapterCritique,
ChapterDraft,
Manuscript,
RawContent,
)
from opus_orchestrator.state import OpusState
class OpusOrchestrator:
"""Main orchestrator implementing multiple story frameworks."""
def __init__(
self,
repo_url: str | None = None,
book_type: str = "fiction",
genre: Optional[str] = None,
target_audience: str = "general readers",
intended_outcome: str = "complete novel",
tone: Optional[str] = None,
target_word_count: int = 80000,
framework: str = "snowflake",
config: Optional[OpusConfig] = None,
):
"""Initialize the Opus Orchestrator with selectable framework.
Args:
repo_url: GitHub URL for content
book_type: "fiction" or "nonfiction"
genre: Genre (for framework suggestions)
target_audience: Who is this for
intended_outcome: What to produce
tone: Desired tone
target_word_count: Target length
framework: Story framework to use (snowflake, three-act, save-the-cat,
hero-journey, story-circle, seven-point, fichtean)
config: Optional config override
"""
self.config = config or get_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")
self.book_type = BookType(book_type.lower())
self.repo_url = repo_url
# Handle framework
if isinstance(framework, str):
try:
self.framework = StoryFramework(framework.lower())
except ValueError:
# Default to snowflake if invalid
self.framework = StoryFramework.SNOWFLAKE
else:
self.framework = framework
# Get framework info
self.framework_info = FRAMEWORKS.get(self.framework, FRAMEWORKS[StoryFramework.SNOWFLAKE])
self.intent = BookIntent(
book_type=self.book_type,
genre=genre,
target_audience=target_audience,
intended_outcome=intended_outcome,
tone=tone,
target_word_count=target_word_count,
)
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:
"""Initialize agents based on book type."""
if self.book_type == BookType.FICTION:
self.agents = {
"architect": ArchitectAgent(self.config.agent),
"worldsmith": WorldsmithAgent(self.config.agent),
"character_lead": CharacterLeadAgent(self.config.agent),
"voice": VoiceAgent(self.config.agent),
"editor": EditorAgent(self.config.agent),
}
else:
self.agents = {
"researcher": ResearcherAgent(self.config.agent),
"analyst": AnalystAgent(self.config.agent),
"writer": NonfictionWriterAgent(self.config.agent),
"fact_checker": FactCheckerAgent(self.config.agent),
"editor": NonfictionEditorAgent(self.config.agent),
}
async def ingest(self, content: Optional[RawContent] = None) -> OpusState:
"""Ingest raw content from repository."""
if self.repo_url and not content:
content = RawContent(
content_type="repository",
text="[Content would be extracted from GitHub repository]",
metadata={"repo_url": self.repo_url},
)
self.state = OpusState(
repo_url=self.repo_url or "",
intent=self.intent,
raw_content=content,
current_stage="ingestion",
)
return self.state
# =========================================================================
# SNOWFLAKE METHOD STAGES
# =========================================================================
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 outline (framework-dependent).
Expand the one sentence to a paragraph with setup, 3 acts, and resolution.
Uses the selected story framework.
"""
print(f"❄️ SNOWFLAKE STAGE 2: One paragraph outline ({self.framework_info['name']})...")
# Get framework-specific prompt
framework_system_prompt = get_framework_prompt(self.framework)
user_prompt = f"""Expand this one-sentence summary into a full one-paragraph story outline.
Use the {self.framework_info['name']} framework: {self.framework_info.get('description', '')}
## 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=framework_system_prompt,
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
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.
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,
)
self.four_page_outline = response.strip()
print(f" → Created four-page outline ({len(self.four_page_outline)} chars)")
return self.four_page_outline
async def snowflake_stage_5(self) -> str:
"""Stage 5: Detailed character charts.
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 (framework-dependent).
Create a list of all scenes using the selected framework.
"""
print(f"❄️ SNOWFLAKE STAGE 6: Scene list ({self.framework_info['name']})...")
# Get framework-specific prompt
framework_system_prompt = get_framework_prompt(self.framework)
words_per_scene = 1500 # Average scene length
num_scenes = max(10, self.intent.target_word_count // words_per_scene)
# Get framework beats if available
framework_beats = ""
if "beats" in self.framework_info:
framework_beats = f"\n\n## Framework Beats:\n"
for beat_name, beat_desc in self.framework_info["beats"]:
framework_beats += f"- {beat_name}: {beat_desc}\n"
user_prompt = f"""Create a complete SCENE LIST for this story using the {self.framework_info['name']}.
Target: approximately {num_scenes} scenes for a {self.intent.target_word_count:,} word novel.
{framework_beats}
## 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=framework_system_prompt,
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 the style guide for prose."""
print("🎨 Creating style guide...")
voice = self.agents["voice"]
response = await voice.execute(
{
"genre": self.intent.genre or "general",
"tone": self.intent.tone or "neutral",
"target_audience": self.intent.target_audience,
},
{},
)
if response.success:
self.style_guide = response.output if isinstance(response.output, str) else str(response.output)
else:
self.style_guide = "Professional fiction prose style."
print(" ✅ Style guide created")
return self.style_guide
async def write_chapter(self, chapter_num: int, total_chapters: int) -> ChapterDraft:
"""Write a single chapter."""
print(f"✍️ Writing chapter {chapter_num}/{total_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_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,
{},
)
if not response.success:
raise Exception(f"Chapter writing failed: {response.error}")
output = response.output if isinstance(response.output, dict) else {"content": str(response.output)}
draft = ChapterDraft(
chapter_number=chapter_num,
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.5 + (0.4 * chapter_num / total_chapters)
self.state.progress = progress
print(f" ✅ Chapter {chapter_num}: {draft.word_count} words")
return draft
async def critique_chapter(self, chapter_num: int) -> ChapterCritique:
"""Critique a chapter."""
draft = self.state.drafts.get(chapter_num)
if not draft:
raise ValueError(f"No draft for chapter {chapter_num}")
editor = self.agents["editor"]
response = await editor.review_chapter(
draft.model_dump(),
{"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 ChapterCritique(
chapter_number=chapter_num,
overall_score=0.7,
criteria_scores=[],
consensus_strengths=["Good effort"],
consensus_weaknesses=[],
revision_priority="minor_revisions",
)
output = response.output if isinstance(response.output, dict) else {"critique": str(response.output)}
critique = ChapterCritique(
chapter_number=chapter_num,
overall_score=output.get("score", 0.7),
criteria_scores=[],
consensus_strengths=[],
consensus_weaknesses=[],
revision_priority="minor_revisions",
)
if chapter_num not in self.state.critiques:
self.state.critiques[chapter_num] = []
self.state.critiques[chapter_num].append(critique)
return critique
async def iterate_chapter(self, chapter_num: int, max_iterations: int = 2) -> Chapter:
"""Iterate on a chapter."""
for iteration in range(1, max_iterations + 1):
critique = await self.critique_chapter(chapter_num)
if critique.overall_score >= self.config.iteration.approval_threshold:
print(f" ✅ Chapter {chapter_num} approved! (score: {critique.overall_score:.2f})")
break
else:
print(f" 🔄 Iteration {iteration}: score {critique.overall_score:.2f}")
draft = self.state.drafts.get(chapter_num)
return Chapter(
chapter_number=chapter_num,
title=draft.title,
content=draft.content,
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=[
ChapterBlueprint(
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."""
num_chapters = len(self.state.blueprint.chapters)
chapters = []
for i in range(1, num_chapters + 1):
await self.write_chapter(i, num_chapters)
chapter = await self.iterate_chapter(i)
chapters.append(chapter)
manuscript = Manuscript(
title=self.state.blueprint.title,
book_type=self.book_type,
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
return manuscript
# =========================================================================
# MAIN RUN METHOD - FULL SNOWFLAKE
# =========================================================================
async def run(self) -> Manuscript:
"""Run the full pipeline with selected framework."""
framework_name = self.framework_info.get("name", "Unknown")
print(f"\n{'='*60}")
print(f"❄️ OPUS ORCHESTRATOR - {framework_name.upper()}")
print(f"{'='*60}")
print(f"Framework: {self.framework_info.get('description', '')}\n")
await self.ingest()
# Pre-writing stages
await self.snowflake_stage_1() # One sentence
await self.snowflake_stage_2() # One paragraph/outline with framework
await self.snowflake_stage_3() # Character sheets
await self.snowflake_stage_4() # Expanded 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()
# Write and critique chapters
manuscript = await self.compile_manuscript()
print(f"\n{'='*60}")
print("✅ COMPLETE!")
print(f"{'='*60}")
print(f"📖 Title: {manuscript.title}")
print(f"📄 Words: {manuscript.total_word_count:,}")
print(f"📑 Chapters: {len(manuscript.chapters)}")
print(f"🎯 Framework: {framework_name}")
return manuscript
def save_manuscript(self, output_path: Optional[Path] = None) -> Path:
"""Save manuscript and pre-writing to files."""
if not self.state.manuscript:
raise ValueError("No manuscript to save. Run first.")
output_dir = output_path or Path("./output")
output_dir.mkdir(parents=True, exist_ok=True)
# 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())
# 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