2026-03-12 19:52:47 +00:00
"""Main Opus Orchestrator - Snowflake Method Implementation with Multiple Frameworks.
2026-03-13 00:40:47 +00:00
Full pipeline supporting multiple story frameworks and GitHub ingestion.
2026-03-12 19:36:25 +00:00
"""
2026-03-12 17:44:51 +00:00
import asyncio
2026-03-12 18:42:15 +00:00
import os
2026-03-12 17:44:51 +00:00
from pathlib import Path
from typing import Any , Optional
2026-03-12 18:42:15 +00:00
from dotenv import load_dotenv
2026-03-13 04:34:22 +00:00
load_dotenv ()
2026-03-12 18:42:15 +00:00
2026-03-12 17:44:51 +00:00
from opus_orchestrator.agents.fiction import (
ArchitectAgent ,
CharacterLeadAgent ,
EditorAgent ,
VoiceAgent ,
WorldsmithAgent ,
)
from opus_orchestrator.agents.nonfiction import (
AnalystAgent ,
FactCheckerAgent ,
NonfictionEditorAgent ,
NonfictionWriterAgent ,
ResearcherAgent ,
)
2026-03-12 18:42:15 +00:00
from opus_orchestrator.config import OpusConfig , get_config
2026-03-12 19:52:47 +00:00
from opus_orchestrator.frameworks import (
StoryFramework ,
FRAMEWORKS ,
get_framework_for_genre ,
get_framework_prompt ,
)
2026-03-12 17:44:51 +00:00
from opus_orchestrator.schemas import (
BookBlueprint ,
BookIntent ,
BookType ,
Chapter ,
2026-03-12 19:36:25 +00:00
ChapterBlueprint ,
2026-03-12 17:44:51 +00:00
ChapterCritique ,
ChapterDraft ,
Manuscript ,
RawContent ,
)
from opus_orchestrator.state import OpusState
2026-03-13 00:40:47 +00:00
from opus_orchestrator.utils.github_ingest import GitHubIngestor
2026-03-12 17:44:51 +00:00
2026-03-13 20:32:48 +00:00
# Nonfiction taxonomy - Purpose × Structure matrix
from opus_orchestrator.nonfiction import (
PurposeClassifier ,
ReaderPurpose ,
)
from opus_orchestrator.nonfiction_taxonomy import (
select_framework ,
get_frameworks_for_purpose ,
NONFICTION_FRAMEWORKS ,
PURPOSE_STRUCTURE_MATRIX ,
StructuralPattern ,
NonfictionCategory ,
)
2026-03-12 17:44:51 +00:00
class OpusOrchestrator :
2026-03-12 19:52:47 +00:00
"""Main orchestrator implementing multiple story frameworks."""
2026-03-12 17:44:51 +00:00
def __init__ (
self ,
repo_url : str | None = None ,
book_type : str = "fiction" ,
genre : Optional [ str ] = None ,
target_audience : str = "general readers" ,
2026-03-12 19:36:25 +00:00
intended_outcome : str = "complete novel" ,
2026-03-12 17:44:51 +00:00
tone : Optional [ str ] = None ,
target_word_count : int = 80000 ,
2026-03-12 19:52:47 +00:00
framework : str = "snowflake" ,
2026-03-12 17:44:51 +00:00
config : Optional [ OpusConfig ] = None ,
2026-03-13 20:32:48 +00:00
# Nonfiction-specific options
purpose : Optional [ str ] = None ,
category : Optional [ str ] = None ,
2026-03-12 17:44:51 +00:00
):
2026-03-12 19:52:47 +00:00
"""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
"""
2026-03-12 17:44:51 +00:00
self . config = config or get_config ()
2026-03-12 18:42:15 +00:00
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" )
2026-03-12 17:44:51 +00:00
self . book_type = BookType ( book_type . lower ())
self . repo_url = repo_url
2026-03-12 19:52:47 +00:00
# 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 ])
2026-03-12 17:44:51 +00:00
2026-03-13 20:32:48 +00:00
# ================================================================
# NONFICTION: Purpose Classification & Framework Selection
# ================================================================
self . purpose : Optional [ ReaderPurpose ] = None
self . nonfiction_framework : Optional [ dict ] = None
self . framework_stages : list [ str ] = []
if self . book_type == BookType . NONFICTION :
# Classify purpose if not explicitly provided
if purpose :
try :
self . purpose = ReaderPurpose ( purpose . lower ())
except ValueError :
# Default will be determined by classifier
self . purpose = None
# If purpose not yet determined, classify from intent
if not self . purpose :
self . _classify_purpose_from_intent (
concept = intended_outcome , # Using outcome as proxy for concept
target_audience = target_audience ,
)
# Select appropriate framework based on purpose
self . _select_nonfiction_framework ( category )
# ================================================================
2026-03-12 17:44:51 +00:00
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
2026-03-12 19:36:25 +00:00
# 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 = ""
2026-03-12 18:42:15 +00:00
self . style_guide : str = ""
2026-03-12 17:44:51 +00:00
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 ),
}
2026-03-13 20:32:48 +00:00
# =========================================================================
# NONFICTION: Purpose Classification & Framework Selection
# =========================================================================
def _classify_purpose_from_intent (
self ,
concept : str ,
target_audience : str ,
) -> None :
"""Classify purpose from book intent using keyword classifier.
Args:
concept: The book concept/title
target_audience: Target audience description
"""
classifier = PurposeClassifier ()
result = classifier . _keyword_classify (
concept = concept or "" ,
target_audience = target_audience ,
intended_outcome = self . intent . intended_outcome or "" ,
)
self . purpose = result . purpose
print ( f "[NONFICTION] Purpose classified: { self . purpose . value } (confidence: { result . confidence : .2f } )" )
print ( f "[NONFICTION] Reasoning: { result . reasoning } " )
def _select_nonfiction_framework ( self , category : Optional [ str ] = None ) -> None :
"""Select the best framework based on purpose and category.
Args:
category: Optional nonfiction category (business, self_help, etc.)
"""
if not self . purpose :
# Default to UNDERSTAND if not classified
self . purpose = ReaderPurpose . UNDERSTAND
# Map category string to NonfictionCategory enum
category_enum = None
if category :
try :
category_enum = NonfictionCategory ( category . lower ())
except ValueError :
pass
# Select framework using taxonomy
framework = select_framework (
purpose = self . purpose ,
category = category_enum ,
user_preferred_framework = None ,
)
self . nonfiction_framework = framework
self . framework_stages = framework . get ( "stages" , [])
print ( f "[NONFICTION] Framework selected: { framework . get ( 'name' , 'Unknown' ) } " )
print ( f "[NONFICTION] Structure: { framework . get ( 'structure' , 'N/A' ) } " )
print ( f "[NONFICTION] Stages ( { len ( self . framework_stages ) } ):" )
for i , stage in enumerate ( self . framework_stages [: 5 ], 1 ):
print ( f " { i } . { stage } " )
if len ( self . framework_stages ) > 5 :
print ( f " ... and { len ( self . framework_stages ) - 5 } more" )
def get_framework_context ( self ) -> dict [ str , Any ]:
"""Get the framework context for passing to agents.
Returns:
Dict with framework name, purpose, stages, and prompt template
"""
if self . book_type == BookType . NONFICTION and self . nonfiction_framework :
return {
"framework_name" : self . nonfiction_framework . get ( "name" , "" ),
"framework_purpose" : self . purpose . value if self . purpose else "" ,
"structure" : self . nonfiction_framework . get ( "structure" , "" ),
"stages" : self . framework_stages ,
"prompt_template" : self . nonfiction_framework . get ( "prompt_template" , "" ),
"tone_guidance" : self . nonfiction_framework . get ( "tone_guidance" , "" ),
}
else :
# Return fiction framework context
return {
"framework_name" : self . framework . value ,
"framework_purpose" : "entertainment" ,
"structure" : "narrative" ,
"stages" : [],
"prompt_template" : "" ,
"tone_guidance" : "" ,
}
def generate_stage_outline (
self ,
stage_name : str ,
stage_index : int ,
context : dict [ str , Any ],
) -> str :
"""Generate a chapter/section outline for a given stage.
Args:
stage_name: Name of the framework stage
stage_index: Index of the stage
context: Additional context (book concept, etc.)
Returns:
Generated outline for the stage
"""
if not self . nonfiction_framework :
return f "Chapter { stage_index + 1 } : { stage_name } "
# Build prompt for this specific stage
prompt = f """Generate a detailed outline for a book section.
Framework Stage: { stage_name }
Stage Number: { stage_index + 1 } of { len ( self . framework_stages ) }
Book Context:
- Concept: { context . get ( 'concept' , 'N/A' ) }
- Purpose: { self . purpose . value if self . purpose else 'N/A' }
- Target Audience: { self . intent . target_audience }
Framework: { self . nonfiction_framework . get ( 'name' , 'N/A' ) }
Structure: { self . nonfiction_framework . get ( 'structure' , 'N/A' ) }
Generate a detailed outline with:
1. Section title
2. Key points to cover (3-5)
3. Word count target
4. Tone guidance for this section
"""
return prompt
# =========================================================================
2026-03-12 17:44:51 +00:00
async def ingest ( self , content : Optional [ RawContent ] = None ) -> OpusState :
2026-03-12 18:42:15 +00:00
"""Ingest raw content from repository."""
2026-03-12 17:44:51 +00:00
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 },
)
2026-03-12 18:42:15 +00:00
self . state = OpusState (
2026-03-12 17:44:51 +00:00
repo_url = self . repo_url or "" ,
intent = self . intent ,
raw_content = content ,
2026-03-12 18:42:15 +00:00
current_stage = "ingestion" ,
2026-03-12 17:44:51 +00:00
)
return self . state
2026-03-13 02:42:00 +00:00
# =========================================================================
# GITHUB INGESTION
# =========================================================================
def ingest_from_github ( self , repo : str , include_readme : bool = True ) -> RawContent :
"""Ingest content from a GitHub repository.
Args:
repo: "owner/repo" format (e.g., "mrhavens/my-notes")
include_readme: Whether to include README files
Returns:
RawContent with the combined text from the repo
"""
from opus_orchestrator.utils.github_ingest import GitHubIngestor
print ( f "📥 Loading from GitHub: { repo } " )
github_token = self . config . github_token or os . environ . get ( "GITHUB_TOKEN" )
ingestor = GitHubIngestor ( token = github_token )
result = ingestor . ingest_repo ( repo , include_readme = include_readme )
print ( f " Found { result [ 'file_count' ] } files" )
print ( f " Total content: { result [ 'total_chars' ] : , } characters" )
return RawContent (
content_type = "github" ,
text = result [ "combined_text" ],
metadata = {
"repo" : repo ,
"files" : list ( result [ "files" ] . keys ()),
"file_count" : result [ "file_count" ],
},
)
2026-03-12 19:36:25 +00:00
# =========================================================================
# SNOWFLAKE METHOD STAGES
# =========================================================================
2026-03-12 18:42:15 +00:00
2026-03-12 19:36:25 +00:00
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 :
2026-03-12 19:52:47 +00:00
"""Stage 2: One paragraph outline (framework-dependent).
2026-03-12 19:36:25 +00:00
Expand the one sentence to a paragraph with setup, 3 acts, and resolution.
2026-03-12 19:52:47 +00:00
Uses the selected story framework.
2026-03-12 19:36:25 +00:00
"""
2026-03-12 19:52:47 +00:00
print ( f "❄️ SNOWFLAKE STAGE 2: One paragraph outline ( { self . framework_info [ 'name' ] } )..." )
# Get framework-specific prompt
framework_system_prompt = get_framework_prompt ( self . framework )
2026-03-12 19:36:25 +00:00
user_prompt = f """Expand this one-sentence summary into a full one-paragraph story outline.
2026-03-12 19:52:47 +00:00
Use the { self . framework_info [ 'name' ] } framework: { self . framework_info . get ( 'description' , '' ) }
2026-03-12 19:36:25 +00:00
## 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 (
2026-03-12 19:52:47 +00:00
system_prompt = framework_system_prompt ,
2026-03-12 19:36:25 +00:00
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 },
2026-03-12 18:42:15 +00:00
{},
)
2026-03-12 19:36:25 +00:00
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
2026-03-12 17:44:51 +00:00
2026-03-12 19:36:25 +00:00
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.
2026-03-12 18:42:15 +00:00
2026-03-12 19:36:25 +00:00
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 ,
2026-03-12 17:44:51 +00:00
)
2026-03-12 19:36:25 +00:00
self . four_page_outline = response . strip ()
print ( f " → Created four-page outline ( { len ( self . four_page_outline ) } chars)" )
return self . four_page_outline
2026-03-12 17:44:51 +00:00
2026-03-12 19:36:25 +00:00
async def snowflake_stage_5 ( self ) -> str :
"""Stage 5: Detailed character charts.
2026-03-12 18:42:15 +00:00
2026-03-12 19:36:25 +00:00
Expand character sheets into full character charts with dialogue samples.
"""
print ( "❄️ SNOWFLAKE STAGE 5: Detailed character charts..." )
2026-03-12 18:42:15 +00:00
2026-03-12 19:36:25 +00:00
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
2026-03-12 18:42:15 +00:00
2026-03-12 19:36:25 +00:00
async def snowflake_stage_6 ( self ) -> str :
2026-03-12 19:52:47 +00:00
"""Stage 6: Scene list (framework-dependent).
2026-03-12 19:36:25 +00:00
2026-03-12 19:52:47 +00:00
Create a list of all scenes using the selected framework.
2026-03-12 19:36:25 +00:00
"""
2026-03-12 19:52:47 +00:00
print ( f "❄️ SNOWFLAKE STAGE 6: Scene list ( { self . framework_info [ 'name' ] } )..." )
# Get framework-specific prompt
framework_system_prompt = get_framework_prompt ( self . framework )
2026-03-12 19:36:25 +00:00
words_per_scene = 1500 # Average scene length
num_scenes = max ( 10 , self . intent . target_word_count // words_per_scene )
2026-03-12 19:52:47 +00:00
# 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' ] } .
2026-03-12 19:36:25 +00:00
Target: approximately { num_scenes } scenes for a { self . intent . target_word_count : , } word novel.
2026-03-12 19:52:47 +00:00
{ framework_beats }
2026-03-12 19:36:25 +00:00
## 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 (
2026-03-12 19:52:47 +00:00
system_prompt = framework_system_prompt ,
2026-03-12 19:36:25 +00:00
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
2026-03-12 17:44:51 +00:00
2026-03-12 19:36:25 +00:00
async def snowflake_stage_7 ( self ) -> str :
"""Stage 7: Scene descriptions.
2026-03-12 18:42:15 +00:00
2026-03-12 19:36:25 +00:00
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
2026-03-12 17:44:51 +00:00
2026-03-12 18:42:15 +00:00
async def create_style_guide ( self ) -> str :
2026-03-12 19:36:25 +00:00
"""Create the style guide for prose."""
2026-03-12 18:42:15 +00:00
print ( "🎨 Creating style guide..." )
2026-03-12 19:36:25 +00:00
2026-03-12 18:42:15 +00:00
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."
2026-03-12 19:36:25 +00:00
print ( " ✅ Style guide created" )
2026-03-12 18:42:15 +00:00
return self . style_guide
2026-03-12 19:36:25 +00:00
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.
"""
2026-03-12 18:42:15 +00:00
voice = self . agents [ "voice" ]
2026-03-12 19:36:25 +00:00
target_words = self . intent . target_word_count // total_chapters
2026-03-12 18:42:15 +00:00
response = await voice . write_chapter (
2026-03-12 19:36:25 +00:00
{
"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 ,
2026-03-12 18:42:15 +00:00
{},
)
2026-03-12 17:44:51 +00:00
2026-03-12 18:42:15 +00:00
if not response . success :
raise Exception ( f "Chapter writing failed: { response . error } " )
2026-03-12 17:44:51 +00:00
2026-03-12 18:42:15 +00:00
output = response . output if isinstance ( response . output , dict ) else { "content" : str ( response . output )}
2026-03-12 17:44:51 +00:00
draft = ChapterDraft (
chapter_number = chapter_num ,
2026-03-12 19:36:25 +00:00
title = f "Chapter { chapter_num } " ,
2026-03-12 18:42:15 +00:00
content = output . get ( "content" , "" ),
word_count = output . get ( "word_count" , len ( output . get ( "content" , "" ) . split ())),
2026-03-12 17:44:51 +00:00
)
self . state . drafts [ chapter_num ] = draft
2026-03-12 19:36:25 +00:00
progress = 0.5 + ( 0.4 * chapter_num / total_chapters )
2026-03-12 18:42:15 +00:00
self . state . progress = progress
2026-03-12 19:36:25 +00:00
print ( f " ✅ Chapter { chapter_num } : { draft . word_count } words" )
2026-03-12 17:44:51 +00:00
return draft
async def critique_chapter ( self , chapter_num : int ) -> ChapterCritique :
2026-03-12 19:36:25 +00:00
"""Critique a chapter."""
2026-03-12 18:42:15 +00:00
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 (),
2026-03-12 19:36:25 +00:00
{ "title" : self . one_sentence , "genre" : self . intent . genre or "general" , "total_chapters" : len ( self . state . blueprint . chapters ) if self . state . blueprint else 0 },
2026-03-12 18:42:15 +00:00
{},
)
2026-03-12 17:44:51 +00:00
2026-03-12 18:42:15 +00:00
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" ,
)
2026-03-12 17:44:51 +00:00
2026-03-12 18:42:15 +00:00
output = response . output if isinstance ( response . output , dict ) else { "critique" : str ( response . output )}
2026-03-12 17:44:51 +00:00
critique = ChapterCritique (
chapter_number = chapter_num ,
2026-03-12 18:42:15 +00:00
overall_score = output . get ( "score" , 0.7 ),
2026-03-12 17:44:51 +00:00
criteria_scores = [],
2026-03-12 18:42:15 +00:00
consensus_strengths = [],
consensus_weaknesses = [],
2026-03-12 17:44:51 +00:00
revision_priority = "minor_revisions" ,
)
if chapter_num not in self . state . critiques :
self . state . critiques [ chapter_num ] = []
self . state . critiques [ chapter_num ] . append ( critique )
2026-03-12 18:42:15 +00:00
return critique
2026-03-12 17:44:51 +00:00
2026-03-12 18:42:15 +00:00
async def iterate_chapter ( self , chapter_num : int , max_iterations : int = 2 ) -> Chapter :
2026-03-12 19:36:25 +00:00
"""Iterate on a chapter."""
2026-03-12 18:42:15 +00:00
for iteration in range ( 1 , max_iterations + 1 ):
2026-03-12 17:44:51 +00:00
critique = await self . critique_chapter ( chapter_num )
2026-03-12 18:42:15 +00:00
2026-03-12 17:44:51 +00:00
if critique . overall_score >= self . config . iteration . approval_threshold :
2026-03-12 19:36:25 +00:00
print ( f " ✅ Chapter { chapter_num } approved! (score: { critique . overall_score : .2f } )" )
2026-03-12 17:44:51 +00:00
break
2026-03-12 19:36:25 +00:00
else :
print ( f " 🔄 Iteration { iteration } : score { critique . overall_score : .2f } " )
2026-03-12 18:42:15 +00:00
draft = self . state . drafts . get ( chapter_num )
2026-03-12 17:44:51 +00:00
return Chapter (
chapter_number = chapter_num ,
title = draft . title ,
content = draft . content ,
word_count = draft . word_count ,
)
2026-03-12 19:36:25 +00:00
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 = [
2026-03-12 19:52:47 +00:00
ChapterBlueprint (
2026-03-12 19:36:25 +00:00
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
2026-03-12 17:44:51 +00:00
async def compile_manuscript ( self ) -> Manuscript :
2026-03-12 18:42:15 +00:00
"""Compile all chapters into final manuscript."""
num_chapters = len ( self . state . blueprint . chapters )
2026-03-12 19:36:25 +00:00
chapters = []
2026-03-12 18:42:15 +00:00
for i in range ( 1 , num_chapters + 1 ):
2026-03-12 19:36:25 +00:00
await self . write_chapter ( i , num_chapters )
2026-03-12 18:42:15 +00:00
chapter = await self . iterate_chapter ( i )
chapters . append ( chapter )
2026-03-12 17:44:51 +00:00
manuscript = Manuscript (
2026-03-12 18:42:15 +00:00
title = self . state . blueprint . title ,
2026-03-12 17:44:51 +00:00
book_type = self . book_type ,
genre = self . intent . genre or "general" ,
chapters = chapters ,
total_word_count = sum ( c . word_count for c in chapters ),
2026-03-12 19:36:25 +00:00
frontmatter = {
"one_sentence" : self . one_sentence ,
"one_paragraph" : self . one_paragraph ,
"include_toc" : True ,
},
2026-03-12 17:44:51 +00:00
)
self . state . manuscript = manuscript
self . state . current_stage = "complete"
self . state . progress = 1.0
return manuscript
2026-03-12 19:36:25 +00:00
# =========================================================================
# MAIN RUN METHOD - FULL SNOWFLAKE
# =========================================================================
2026-03-12 17:44:51 +00:00
async def run ( self ) -> Manuscript :
2026-03-12 19:52:47 +00:00
"""Run the full pipeline with selected framework."""
framework_name = self . framework_info . get ( "name" , "Unknown" )
2026-03-12 19:36:25 +00:00
print ( f " \n { '=' * 60 } " )
2026-03-12 19:52:47 +00:00
print ( f "❄️ OPUS ORCHESTRATOR - { framework_name . upper () } " )
print ( f " { '=' * 60 } " )
print ( f "Framework: { self . framework_info . get ( 'description' , '' ) } \n " )
2026-03-12 17:44:51 +00:00
await self . ingest ()
2026-03-12 19:52:47 +00:00
# Pre-writing stages
2026-03-12 19:36:25 +00:00
await self . snowflake_stage_1 () # One sentence
2026-03-12 19:52:47 +00:00
await self . snowflake_stage_2 () # One paragraph/outline with framework
2026-03-12 19:36:25 +00:00
await self . snowflake_stage_3 () # Character sheets
2026-03-12 19:52:47 +00:00
await self . snowflake_stage_4 () # Expanded outline
2026-03-12 19:36:25 +00:00
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 ()
2026-03-12 17:44:51 +00:00
# Generate blueprint
await self . generate_blueprint ()
2026-03-12 19:36:25 +00:00
# Write and critique chapters
2026-03-12 18:42:15 +00:00
manuscript = await self . compile_manuscript ()
2026-03-12 17:44:51 +00:00
2026-03-12 19:36:25 +00:00
print ( f " \n { '=' * 60 } " )
2026-03-12 19:52:47 +00:00
print ( "✅ COMPLETE!" )
2026-03-12 19:36:25 +00:00
print ( f " { '=' * 60 } " )
print ( f "📖 Title: { manuscript . title } " )
print ( f "📄 Words: { manuscript . total_word_count : , } " )
print ( f "📑 Chapters: { len ( manuscript . chapters ) } " )
2026-03-12 19:52:47 +00:00
print ( f "🎯 Framework: { framework_name } " )
2026-03-12 17:44:51 +00:00
2026-03-12 18:42:15 +00:00
return manuscript
2026-03-12 17:44:51 +00:00
2026-03-12 18:42:15 +00:00
def save_manuscript ( self , output_path : Optional [ Path ] = None ) -> Path :
2026-03-12 19:36:25 +00:00
"""Save manuscript and pre-writing to files."""
2026-03-12 17:44:51 +00:00
if not self . state . manuscript :
raise ValueError ( "No manuscript to save. Run first." )
2026-03-12 19:36:25 +00:00
output_dir = output_path or Path ( "./output" )
output_dir . mkdir ( parents = True , exist_ok = True )
2026-03-12 17:44:51 +00:00
2026-03-12 19:36:25 +00:00
# Save manuscript
manuscript_path = output_dir / f " { self . state . manuscript . title . lower () . replace ( ' ' , '_' ) } .md"
with open ( manuscript_path , "w" ) as f :
2026-03-12 17:44:51 +00:00
f . write ( self . state . manuscript . to_markdown ())
2026-03-12 19:36:25 +00:00
# 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