Add comprehensive CLI, OpenAPI server, and documentation

CLI Commands:
- generate: Full manuscript generation with full GitHub content
- serve: Start FastAPI server with OpenAPI docs
- ingest: Standalone GitHub ingestion
- frameworks: List all story frameworks
- config: Show configuration
- docs: Show comprehensive docs (terminal/markdown/html)
- api: Export OpenAPI spec

Server:
- FastAPI with /docs, /redoc interactive docs
- /generate, /ingest, /frameworks, /health endpoints
- OpenAPI 3.0 specification

Documentation:
- Terminal, markdown, and HTML formats
- Full API reference
- Framework documentation
- Environment variables guide
- Project structure

Fix: Use full GitHub content as seed (not just 5000 chars)
This commit is contained in:
2026-03-13 03:10:01 +00:00
parent 6d23707ae4
commit 64bea5cae6
4 changed files with 938 additions and 61 deletions
+321 -60
View File
@@ -2,6 +2,12 @@
"""Opus Orchestrator CLI.
Standalone CLI for running Opus book generation without OpenClaw.
Usage:
opus --help
opus generate --concept "Your story idea"
opus serve --port 8000 # Start API server
opus docs # Show documentation
"""
import argparse
@@ -24,28 +30,36 @@ def setup_cli() -> argparse.ArgumentParser:
"""Set up the CLI argument parser."""
parser = argparse.ArgumentParser(
prog="opus",
description="Opus Orchestrator AI - Full-flow AI book generation",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
description="""Opus Orchestrator AI - Full-flow AI book generation
A comprehensive book generation system using LangGraph, CrewAI, AutoGen,
and PydanticAI for professional manuscript production.
Examples:
# Generate a short story
opus generate --concept "A robot dreams of love" --framework snowflake --words 1000
# Generate from GitHub repo
opus generate --repo mrhavens/my-book-ideas --framework hero-journey
# Run with specific genre
opus generate --concept "Space opera adventure" --genre sci-fi --words 50000
# List available frameworks
opus frameworks
opus generate --concept "A robot dreams of love" --framework snowflake
opus serve --port 8080
opus docs
opus ingest --repo mrhavens/my-book
""",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--version", "-v",
action="version",
version="Opus Orchestrator AI v0.2.0",
)
subparsers = parser.add_subparsers(dest="command", help="Command to run")
# Generate command
gen_parser = subparsers.add_parser("generate", help="Generate a book/manuscript")
# -------------------------------------------------------------------------
# GENERATE COMMAND
# -------------------------------------------------------------------------
gen_parser = subparsers.add_parser(
"generate",
help="Generate a book/manuscript",
description="Generate a complete manuscript from a concept or GitHub repo",
)
gen_parser.add_argument(
"--concept", "-c",
help="Seed concept or story idea",
@@ -77,48 +91,158 @@ Examples:
"--words", "-w",
type=int,
default=5000,
help="Target word count",
help="Target word count (default: 5000)",
)
gen_parser.add_argument(
"--chapters", "-n",
type=int,
default=3,
help="Number of chapters (default: 3)",
)
gen_parser.add_argument(
"--tone",
default="literary",
help="Writing tone",
help="Writing tone (default: literary)",
)
gen_parser.add_argument(
"--output", "-o",
help="Output file path",
)
gen_parser.add_argument(
"--chapters", "-n",
type=int,
default=3,
help="Number of chapters",
)
gen_parser.add_argument(
"--use-crewai",
action="store_true",
help="Use CrewAI crews instead of direct agent calls",
help="Use CrewAI crews instead of LangGraph",
)
gen_parser.add_argument(
"--use-autogen",
"--no-autogen",
action="store_true",
default=True,
help="Use AutoGen for critique (default: True)",
help="Disable AutoGen critique",
)
gen_parser.add_argument(
"--verbose", "-V",
action="store_true",
help="Enable verbose output",
)
# Frameworks command
# -------------------------------------------------------------------------
# SERVE COMMAND (OpenAPI Server)
# -------------------------------------------------------------------------
serve_parser = subparsers.add_parser(
"serve",
help="Start OpenAPI REST server",
description="Start a REST API server with OpenAPI documentation",
)
serve_parser.add_argument(
"--host",
default="0.0.0.0",
help="Host to bind to (default: 0.0.0.0)",
)
serve_parser.add_argument(
"--port", "-p",
type=int,
default=8000,
help="Port to bind to (default: 8000)",
)
serve_parser.add_argument(
"--reload",
action="store_true",
help="Enable auto-reload on code changes",
)
# -------------------------------------------------------------------------
# INGEST COMMAND
# -------------------------------------------------------------------------
ingest_parser = subparsers.add_parser(
"ingest",
help="Ingest content from GitHub",
description="Fetch and analyze content from a GitHub repository",
)
ingest_parser.add_argument(
"--repo", "-r",
required=True,
help="GitHub repo (owner/repo format)",
)
ingest_parser.add_argument(
"--output", "-o",
help="Output file for ingested content",
)
ingest_parser.add_argument(
"--include-readme",
action="store_true",
default=True,
help="Include README files (default: True)",
)
ingest_parser.add_argument(
"--preview",
action="store_true",
help="Show preview of ingested content",
)
# -------------------------------------------------------------------------
# FRAMEWORKS COMMAND
# -------------------------------------------------------------------------
subparsers.add_parser(
"frameworks",
help="List available story frameworks",
description="Show all available story frameworks with descriptions",
)
# Config command
config_parser = subparsers.add_parser("config", help="Show configuration")
# -------------------------------------------------------------------------
# CONFIG COMMAND
# -------------------------------------------------------------------------
config_parser = subparsers.add_parser(
"config",
help="Show configuration",
description="Display current configuration settings",
)
config_parser.add_argument(
"--show-keys",
action="store_true",
help="Show API keys (masked)",
)
config_parser.add_argument(
"--env",
action="store_true",
help="Show environment variables needed",
)
# -------------------------------------------------------------------------
# DOCS COMMAND
# -------------------------------------------------------------------------
docs_parser = subparsers.add_parser(
"docs",
help="Show documentation",
description="Display comprehensive documentation",
)
docs_parser.add_argument(
"--format", "-f",
choices=["terminal", "markdown", "html"],
default="terminal",
help="Output format (default: terminal)",
)
docs_parser.add_argument(
"--output", "-o",
help="Output file path",
)
# -------------------------------------------------------------------------
# API COMMAND
# -------------------------------------------------------------------------
api_parser = subparsers.add_parser(
"api",
help="Show OpenAPI specification",
description="Display or export OpenAPI schema",
)
api_parser.add_argument(
"--format", "-f",
choices=["json", "yaml"],
default="yaml",
help="Output format (default: yaml)",
)
api_parser.add_argument(
"--output", "-o",
help="Output file path",
)
return parser
@@ -136,7 +260,7 @@ async def run_generate(args: argparse.Namespace) -> int:
seed_concept = args.concept
if args.repo:
# Ingest from GitHub
# Ingest from GitHub - use FULL content
print(f"📥 Ingesting from GitHub: {args.repo}")
orch = OpusOrchestrator(
@@ -147,22 +271,30 @@ async def run_generate(args: argparse.Namespace) -> int:
)
content = orch.ingest_from_github(args.repo)
seed_concept = content.text[:5000] # Use first 5000 chars as seed
print(f" Loaded {len(content.text):,} characters\n")
# Use full content as seed
full_text = content.text
print(f" ✅ Loaded {len(full_text):,} characters from {content.metadata['file_count']} files")
print(f" 📄 Files: {', '.join(content.metadata['files'])}\n")
seed_concept = full_text
if not seed_concept:
print("Error: Please provide --concept or --repo")
return 1
print(f"🎯 Generating {args.words:,} words")
# Show generation parameters
print(f"🎯 Generating {args.words:,} words ({args.chapters} chapters)")
print(f" Framework: {args.framework}")
print(f" Genre: {args.genre}")
print(f" Type: {args.book_type}")
print(f" Tone: {args.tone}")
print(f" CrewAI: {args.use_crewai}")
print(f" AutoGen: {args.use_autogen}")
print(f" AutoGen: {not args.no_autogen}")
print()
use_autogen = not args.no_autogen
if args.use_crewai:
# Use CrewAI crews
print("🛠️ Using CrewAI crews...\n")
@@ -175,13 +307,12 @@ async def run_generate(args: argparse.Namespace) -> int:
)
story = crew.write_full_story(
story_outline=seed_concept,
story_outline=seed_concept[:10000], # Limit for crew context
character_sheets="",
style_guide=f"Tone: {args.tone}",
num_chapters=args.chapters,
)
# Combine chapters
manuscript = "\n\n---\n\n".join(story)
else:
crew = create_nonfiction_crew(
@@ -191,7 +322,7 @@ async def run_generate(args: argparse.Namespace) -> int:
)
manuscript = crew.write_section(
section_outline=seed_concept,
section_outline=seed_concept[:10000],
style_guide=f"Tone: {args.tone}",
)
else:
@@ -201,7 +332,7 @@ async def run_generate(args: argparse.Namespace) -> int:
framework=args.framework,
genre=args.genre,
target_word_count=args.words,
use_autogen=args.use_autogen,
use_autogen=use_autogen,
)
manuscript = result.get("manuscript", str(result))
@@ -217,7 +348,9 @@ async def run_generate(args: argparse.Namespace) -> int:
f.write(f"# Opus Generated Manuscript\n\n")
f.write(f"Framework: {args.framework}\n")
f.write(f"Genre: {args.genre}\n")
f.write(f"Type: {args.book_type}\n\n")
f.write(f"Type: {args.book_type}\n")
f.write(f"Chapters: {args.chapters}\n")
f.write(f"Target Words: {args.words:,}\n\n")
f.write(f"---\n\n")
f.write(manuscript)
@@ -232,20 +365,81 @@ async def run_generate(args: argparse.Namespace) -> int:
return 0
async def run_serve(args: argparse.Namespace) -> int:
"""Start the OpenAPI server."""
print(f"\n🚀 Starting Opus API Server...")
print(f" Host: {args.host}")
print(f" Port: {args.port}")
print(f" Docs: http://{args.host}:{args.port}/docs\n")
try:
from opus_orchestrator.server import run_server
await run_server(host=args.host, port=args.port, reload=args.reload)
except ImportError:
print("Error: Run `pip install fastapi uvicorn` to enable API server")
return 1
return 0
def run_ingest(args: argparse.Namespace) -> int:
"""Ingest content from GitHub."""
from opus_orchestrator import OpusOrchestrator
print(f"\n📥 Ingesting from GitHub: {args.repo}\n")
orch = OpusOrchestrator(book_type="fiction")
content = orch.ingest_from_github(args.repo, include_readme=args.include_readme)
print(f"✅ Loaded {len(content.text):,} characters")
print(f" Files: {content.metadata['file_count']}")
print(f" File list: {', '.join(content.metadata['files'])}\n")
if args.preview:
print("📄 PREVIEW (first 2000 chars):")
print("-" * 40)
print(content.text[:2000])
print("-" * 40)
if args.output:
with open(args.output, "w") as f:
f.write(content.text)
print(f"\n💾 Saved to: {args.output}")
return 0
def run_frameworks(args: argparse.Namespace) -> int:
"""List available frameworks."""
from opus_orchestrator.frameworks import FRAMEWORKS
print("\n📚 Available Story Frameworks:\n")
print("\n📚 AVAILABLE STORY FRAMEWORKS\n")
print("=" * 50)
for framework, info in FRAMEWORKS.items():
name = info.get("name", framework.value if hasattr(framework, "value") else str(framework))
desc = info.get("description", "")
print(f" {name}")
if desc:
print(f" {desc}")
print()
stages = info.get("stages", [])
beats = info.get("beats", [])
print(f"\n{name}")
print(f" {desc}")
if stages:
print(f" Stages: {len(stages)}")
for i, stage in enumerate(stages[:3], 1):
print(f" {i}. {stage}")
if len(stages) > 3:
print(f" ... and {len(stages) - 3} more")
if beats:
print(f" Beats: {len(beats)}")
for beat in beats[:3]:
print(f"{beat}")
if len(beats) > 3:
print(f" ... and {len(beats) - 3} more")
print("\n" + "=" * 50)
return 0
@@ -255,27 +449,95 @@ def run_config(args: argparse.Namespace) -> int:
config = get_config()
print("\n⚙️ Opus Configuration:\n")
print(f" Provider: {config.agent.provider}")
print(f" Model: {config.agent.model}")
print(f" Temperature: {config.agent.temperature}")
print(f" Max Tokens: {config.agent.max_tokens}")
print(f" GitHub Token: {'✓ Set' if config.github_token else '✗ Not Set'}")
print("\n⚙️ OPUS CONFIGURATION\n")
print("=" * 40)
print(f"\n🔹 Agent")
print(f" Provider: {config.agent.provider}")
print(f" Model: {config.agent.model}")
print(f" Temperature: {config.agent.temperature}")
print(f" Max Tokens: {config.agent.max_tokens or 'None'}")
print(f"\n🔹 Iteration")
print(f" Min Critic Rounds: {config.iteration.min_critic_rounds}")
print(f" Max Critic Rounds: {config.iteration.max_critic_rounds}")
print(f" Approval Threshold: {config.iteration.approval_threshold}")
print(f"\n🔹 Output")
print(f" Format: {config.output.format}")
print(f" Include TOC: {config.output.include_toc}")
print(f" Output Dir: {config.output.output_dir}")
print(f"\n🔹 Integrations")
print(f" GitHub Token: {'✓ Set' if config.github_token else '✗ Not Set'}")
print(f" API Key: {'✓ Set' if config.agent.api_key else '✗ Not Set'}")
if args.show_keys:
print(f" API Key: {'✓ Set' if config.agent.api_key else '✗ Not Set'}")
print(f"\n🔹 API Keys (unmasked)")
print(f" OPENAI_API_KEY: {os.environ.get('OPENAI_API_KEY', 'Not Set')[:20]}...")
print(f" MINIMAX_API_KEY: {os.environ.get('MINIMAX_API_KEY', 'Not Set')[:20]}...")
print(f" GITHUB_TOKEN: {os.environ.get('GITHUB_TOKEN', 'Not Set')[:20]}...")
if args.env:
print(f"\n📋 ENVIRONMENT VARIABLES NEEDED:")
print("-" * 40)
print("OPENAI_API_KEY=sk-... # Required for LLM")
print("GITHUB_TOKEN=ghp_... # For private repos")
print("MINIMAX_API_KEY=sk-... # Optional alternative")
print()
return 0
def run_docs(args: argparse.Namespace) -> int:
"""Show documentation."""
from opus_orchestrator.utils.docs import generate_docs
docs = generate_docs(format=args.format)
if args.output:
with open(args.output, "w") as f:
f.write(docs)
print(f"📄 Documentation saved to: {args.output}")
else:
print(docs)
return 0
def run_api(args: argparse.Namespace) -> int:
"""Show OpenAPI spec."""
from opus_orchestrator.server import get_openapi_spec
spec = get_openapi_spec(format=args.format)
if args.output:
with open(args.output, "w") as f:
f.write(spec)
print(f"📄 OpenAPI spec saved to: {args.output}")
else:
print(spec)
return 0
async def main_async(args: argparse.Namespace) -> int:
"""Async main function."""
if args.command == "generate":
return await run_generate(args)
elif args.command == "frameworks":
return run_frameworks(args)
elif args.command == "config":
return run_config(args)
commands = {
"generate": run_generate,
"serve": run_serve,
"ingest": run_ingest,
"frameworks": run_frameworks,
"config": run_config,
"docs": run_docs,
"api": run_api,
}
if args.command in commands:
if args.command in ["generate", "serve"]:
return await commands[args.command](args)
else:
return commands[args.command](args)
else:
# No command given, show help
args.parser.print_help()
@@ -291,7 +553,6 @@ def main():
parser.print_help()
return 0
# Run async main
return asyncio.run(main_async(args))
+290
View File
@@ -0,0 +1,290 @@
"""OpenAPI Server for Opus Orchestrator.
FastAPI-based REST API with OpenAPI documentation.
"""
import os
from typing import Any, Optional
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, BackgroundTasks
from fastapi.responses import JSONResponse, RedirectResponse
from pydantic import BaseModel, Field
from dotenv import load_dotenv
load_dotenv("/home/solaria/.openclaw/workspace/opus-orchestrator-ai/.env")
from opus_orchestrator.config import get_config
from opus_orchestrator import run_opus, OpusOrchestrator
from opus_orchestrator.frameworks import FRAMEWORKS
# =============================================================================
# REQUEST/RESPONSE MODELS
# =============================================================================
class GenerateRequest(BaseModel):
"""Request to generate a manuscript."""
concept: Optional[str] = Field(None, description="Seed concept or story idea")
repo: Optional[str] = Field(None, description="GitHub repo to ingest")
framework: str = Field("snowflake", description="Story framework")
genre: str = Field("fiction", description="Genre")
book_type: str = Field("fiction", description="Book type (fiction/nonfiction)")
target_word_count: int = Field(5000, description="Target word count")
chapters: int = Field(3, description="Number of chapters")
tone: str = Field("literary", description="Writing tone")
use_crewai: bool = Field(False, description="Use CrewAI instead of LangGraph")
use_autogen: bool = Field(True, description="Use AutoGen critique")
class GenerateResponse(BaseModel):
"""Response from manuscript generation."""
manuscript: str = Field(..., description="Generated manuscript text")
word_count: int = Field(..., description="Word count")
chapters: int = Field(..., description="Number of chapters")
framework: str = Field(..., description="Framework used")
genre: str = Field(..., description="Genre")
status: str = Field("success", description="Generation status")
class IngestRequest(BaseModel):
"""Request to ingest from GitHub."""
repo: str = Field(..., description="GitHub repo (owner/repo)")
include_readme: bool = Field(True, description="Include README files")
class IngestResponse(BaseModel):
"""Response from GitHub ingestion."""
content: str = Field(..., description="Ingested content")
file_count: int = Field(..., description="Number of files")
total_chars: int = Field(..., description="Total characters")
files: list[str] = Field(..., description="File names")
class FrameworkInfo(BaseModel):
"""Information about a story framework."""
name: str
description: str
stages: list[str] = []
beats: list[str] = []
class HealthResponse(BaseModel):
"""Health check response."""
status: str
version: str
config: dict
# =============================================================================
# APP LIFECYCLE
# =============================================================================
@asynccontextmanager
async def lifespan(app: FastAPI):
"""App lifespan handler."""
# Startup
config = get_config()
print(f"🚀 Opus API starting...")
print(f" Provider: {config.agent.provider}")
print(f" Model: {config.agent.model}")
yield
# Shutdown
print("\n👋 Opus API shutting down...")
# =============================================================================
# CREATE APP
# =============================================================================
def create_app() -> FastAPI:
"""Create and configure the FastAPI application."""
app = FastAPI(
title="Opus Orchestrator API",
description="""Full-flow AI book generation API using LangGraph, CrewAI, AutoGen, and PydanticAI.
## Features
- **Multiple Frameworks**: Snowflake Method, Hero's Journey, Save the Cat, Three-Act, Story Circle, 7-Point, Fichtean
- **CrewAI Integration**: Agent crews for writing, editing, proofreading
- **AutoGen Critique**: Multi-agent debate for editorial feedback
- **PydanticAI Validation**: Structured output validation
- **GitHub Ingestion**: Pull content from repositories
## Quick Start
1. Generate a manuscript:
```bash
curl -X POST "http://localhost:8000/generate" \\
-H "Content-Type: application/json" \\
-d '{"concept": "A robot dreams of love", "target_word_count": 1000}'
```
2. Ingest from GitHub:
```bash
curl -X POST "http://localhost:8000/ingest" \\
-H "Content-Type: application/json" \\
-d '{"repo": "owner/my-book-notes"}'
```
""",
version="0.2.0",
lifespan=lifespan,
docs_url="/docs",
redoc_url="/redoc",
openapi_url="/openapi.json",
)
return app
app = create_app()
# =============================================================================
# ROUTES
# =============================================================================
@app.get("/", tags=["root"])
async def root():
"""Redirect to documentation."""
return RedirectResponse(url="/docs")
@app.get("/health", response_model=HealthResponse, tags=["health"])
async def health():
"""Health check endpoint."""
config = get_config()
return HealthResponse(
status="healthy",
version="0.2.0",
config={
"provider": config.agent.provider,
"model": config.agent.model,
"github_token_set": bool(config.github_token),
},
)
@app.get("/frameworks", response_model=dict[str, FrameworkInfo], tags=["frameworks"])
async def list_frameworks():
"""List all available story frameworks."""
result = {}
for framework, info in FRAMEWORKS.items():
name = info.get("name", framework.value if hasattr(framework, "value") else str(framework))
result[name.lower().replace(" ", "_")] = FrameworkInfo(
name=name,
description=info.get("description", ""),
stages=info.get("stages", []),
beats=[b[0] if isinstance(b, tuple) else b for b in info.get("beats", [])],
)
return result
@app.post("/generate", response_model=GenerateResponse, tags=["generate"])
async def generate(request: GenerateRequest, background_tasks: BackgroundTasks):
"""Generate a manuscript from concept or GitHub repo."""
try:
# Prepare seed concept
seed_concept = request.concept
if request.repo:
# Ingest from GitHub
orch = OpusOrchestrator(
book_type=request.book_type,
genre=request.genre,
target_word_count=request.target_word_count,
framework=request.framework,
)
content = orch.ingest_from_github(request.repo)
seed_concept = content.text
if not seed_concept:
raise HTTPException(status_code=400, detail="Must provide concept or repo")
# Generate
result = await run_opus(
seed_concept=seed_concept,
framework=request.framework,
genre=request.genre,
target_word_count=request.target_word_count,
use_autogen=request.use_autogen,
)
manuscript = result.get("manuscript", str(result))
word_count = len(manuscript.split())
return GenerateResponse(
manuscript=manuscript,
word_count=word_count,
chapters=request.chapters,
framework=request.framework,
genre=request.genre,
status="success",
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/ingest", response_model=IngestResponse, tags=["ingest"])
async def ingest(request: IngestRequest):
"""Ingest content from a GitHub repository."""
try:
orch = OpusOrchestrator(book_type="fiction")
content = orch.ingest_from_github(
request.repo,
include_readme=request.include_readme
)
return IngestResponse(
content=content.text,
file_count=content.metadata["file_count"],
total_chars=len(content.text),
files=content.metadata["files"],
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# =============================================================================
# SERVER RUNNER
# =============================================================================
async def run_server(host: str = "0.0.0.0", port: int = 8000, reload: bool = False):
"""Run the API server."""
import uvicorn
uvicorn.run(
"opus_orchestrator.server:app",
host=host,
port=port,
reload=reload,
log_level="info",
)
def get_openapi_spec(format: str = "yaml") -> str:
"""Get OpenAPI specification."""
import json
spec = app.openapi()
if format == "json":
return json.dumps(spec, indent=2)
else:
# Convert to YAML-like format
import yaml
return yaml.dump(spec, default_flow_style=False)
# =============================================================================
# MAIN
# =============================================================================
if __name__ == "__main__":
import sys
import uvicorn
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8000
uvicorn.run(app, host="0.0.0.0", port=port)
+10 -1
View File
@@ -1,3 +1,12 @@
"""Utility functions for Opus Orchestrator."""
__all__ = []
from opus_orchestrator.utils.docs import generate_docs
from opus_orchestrator.utils.github_ingest import GitHubIngestor, create_github_ingestor
from opus_orchestrator.utils.llm import get_llm_client
__all__ = [
"generate_docs",
"GitHubIngestor",
"create_github_ingestor",
"get_llm_client",
]
+317
View File
@@ -0,0 +1,317 @@
"""Documentation generator for Opus Orchestrator."""
from opus_orchestrator.frameworks import FRAMEWORKS
def generate_docs(format: str = "terminal") -> str:
"""Generate comprehensive documentation.
Args:
format: Output format (terminal, markdown, html)
Returns:
Formatted documentation string
"""
if format == "markdown":
return generate_markdown()
elif format == "html":
return generate_html()
else:
return generate_terminal()
def generate_terminal() -> str:
"""Generate terminal-formatted documentation."""
return f"""
OPUS ORCHESTRATOR AI
Full-Flow AI Book Generation System
VERSION: 0.2.0
📖 OVERVIEW
Opus Orchestrator is a comprehensive AI book generation system that
transforms raw content into publication-ready manuscripts.
TECHNOLOGY STACK:
LangGraph - Workflow orchestration & state management
CrewAI - Role-based agent crews
AutoGen - Multi-agent critique & debate
PydanticAI - Structured output validation
🚀 QUICK START
# Install
pip install opus-orchestrator-ai
# Set environment variables
export OPENAI_API_KEY="sk-..."
export GITHUB_TOKEN="ghp_..."
# Generate a manuscript
opus generate --concept "A robot dreams of love" --words 5000
# Or from GitHub repo
opus generate --repo mrhavens/my-book-ideas --framework hero-journey
# Start API server
opus serve --port 8000
📋 COMMANDS
opus generate [OPTIONS]
Generate a manuscript
--concept, -c Seed concept or story idea
--repo, -r GitHub repo to ingest
--framework, -f Framework (snowflake, hero-journey, etc.)
--genre, -g Genre (fiction, sci-fi, fantasy, etc.)
--type, -t Book type (fiction, nonfiction)
--words, -w Target word count (default: 5000)
--chapters, -n Number of chapters (default: 3)
--tone Writing tone (default: literary)
--use-crewai Use CrewAI instead of LangGraph
--no-autogen Disable AutoGen critique
opus serve [OPTIONS]
Start OpenAPI REST server
--host Host to bind (default: 0.0.0.0)
--port, -p Port to bind (default: 8000)
--reload Enable auto-reload
opus ingest --repo OWNER/REPO
Ingest content from GitHub
opus frameworks
List available story frameworks
opus config [--env]
Show configuration
opus docs
Show this documentation
opus api [--format json|yaml]
Show OpenAPI specification
📚 STORY FRAMEWORKS
{_format_frameworks()}
🌐 API REFERENCE
Base URL: http://localhost:8000
Endpoints:
GET / Redirect to /docs
GET /health Health check
GET /frameworks List frameworks
POST /generate Generate manuscript
POST /ingest Ingest from GitHub
Interactive Docs: http://localhost:8000/docs
🔧 ENVIRONMENT VARIABLES
OPENAI_API_KEY Required for LLM calls (or MINIMAX_API_KEY)
GITHUB_TOKEN For accessing private repositories
ANTHROPIC_API_KEY Optional - alternative LLM provider
📁 PROJECT STRUCTURE
opus_orchestrator/
__init__.py # Main exports
cli.py # CLI entry point
server.py # FastAPI server
orchestrator.py # Main orchestrator
langgraph_workflow.py # LangGraph pipeline
autogen_critique.py # AutoGen critique
pydanticai_agent.py # PydanticAI agents
config.py # Configuration
frameworks.py # Story frameworks
agents/ # Agent implementations
fiction/ # Fiction agents
nonfiction/ # Nonfiction agents
crews/ # CrewAI crews
fiction_crew.py
nonfiction_crew.py
schemas/ # Pydantic schemas
utils/ # Utilities
github_ingest.py
llm.py
docs.py
💡 EXAMPLES
# Generate a sci-fi novel
opus generate \\
--concept "In 2150, humanity's last robot dreams of love" \\
--framework hero-journey \\
--genre science-fiction \\
--words 80000
# Generate from your notes
opus generate --repo mrhavens/my-novel-ideas \\
--framework snowflake \\
--chapters 12
# Use CrewAI for faster generation
opus generate --concept "Your idea" --use-crewai
# API usage
curl -X POST "http://localhost:8000/generate" \\
-H "Content-Type: application/json" \\
-d '{{"concept": "A love story", "target_word_count": 1000}}'
📄 LICENSE
MIT License
Built with the WE Architecture witness and co-creation in code.
"""
def generate_markdown() -> str:
"""Generate Markdown documentation."""
return f"""# Opus Orchestrator AI
> Full-flow AI book generation using LangGraph, CrewAI, AutoGen, and PydanticAI
## Overview
Opus Orchestrator transforms raw content into publication-ready manuscripts using a multi-agent system.
## Installation
```bash
pip install opus-orchestrator-ai
```
## Quick Start
```python
from opus_orchestrator import run_opus
result = await run_opus(
seed_concept="A robot dreams of love",
framework="snowflake",
genre="science-fiction",
target_word_count=5000,
)
```
## CLI Usage
```bash
# Generate manuscript
opus generate --concept "Your story" --words 5000
# From GitHub
opus generate --repo owner/repo --framework hero-journey
# Start API
opus serve --port 8000
```
## Story Frameworks
{_format_frameworks_markdown()}
## API
See http://localhost:8000/docs for interactive API documentation.
## Configuration
Set these environment variables:
- `OPENAI_API_KEY` - Required for LLM
- `GITHUB_TOKEN` - For private repos
- `MINIMAX_API_KEY` - Alternative LLM
## License
MIT
"""
def generate_html() -> str:
"""Generate HTML documentation."""
terminal = generate_terminal()
# Simple HTML wrapper
return f"""<!DOCTYPE html>
<html>
<head>
<title>Opus Orchestrator AI</title>
<style>
body {{ font-family: monospace; background: #1e1e1e; color: #d4d4d4; padding: 20px; }}
pre {{ white-space: pre-wrap; }}
</style>
</head>
<body>
<pre>{terminal}</pre>
</body>
</html>"""
def _format_frameworks() -> str:
"""Format frameworks for terminal output."""
lines = []
for framework, info in FRAMEWORKS.items():
name = info.get("name", str(framework))
desc = info.get("description", "")
stages = info.get("stages", [])
beats = info.get("beats", [])
lines.append(f"\n {name}")
lines.append(f" {desc}")
if stages:
lines.append(f" Stages: {len(stages)}")
for i, stage in enumerate(stages[:3], 1):
lines.append(f" {i}. {stage}")
if len(stages) > 3:
lines.append(f" ... and {len(stages) - 3} more")
if beats:
lines.append(f" Beats: {len(beats)}")
for beat in beats[:3]:
beat_name = beat[0] if isinstance(beat, tuple) else beat
lines.append(f"{beat_name}")
if len(beats) > 3:
lines.append(f" ... and {len(beats) - 3} more")
return "\n".join(lines)
def _format_frameworks_markdown() -> str:
"""Format frameworks for markdown."""
lines = []
for framework, info in FRAMEWORKS.items():
name = info.get("name", str(framework))
desc = info.get("description", "")
lines.append(f"### {name}\n{desc}\n")
return "\n".join(lines)