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:
+321
-60
@@ -2,6 +2,12 @@
|
|||||||
"""Opus Orchestrator CLI.
|
"""Opus Orchestrator CLI.
|
||||||
|
|
||||||
Standalone CLI for running Opus book generation without OpenClaw.
|
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
|
import argparse
|
||||||
@@ -24,28 +30,36 @@ def setup_cli() -> argparse.ArgumentParser:
|
|||||||
"""Set up the CLI argument parser."""
|
"""Set up the CLI argument parser."""
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
prog="opus",
|
prog="opus",
|
||||||
description="Opus Orchestrator AI - Full-flow AI book generation",
|
description="""Opus Orchestrator AI - Full-flow AI book generation
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
||||||
epilog="""
|
A comprehensive book generation system using LangGraph, CrewAI, AutoGen,
|
||||||
|
and PydanticAI for professional manuscript production.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
# Generate a short story
|
opus generate --concept "A robot dreams of love" --framework snowflake
|
||||||
opus generate --concept "A robot dreams of love" --framework snowflake --words 1000
|
opus serve --port 8080
|
||||||
|
opus docs
|
||||||
# Generate from GitHub repo
|
opus ingest --repo mrhavens/my-book
|
||||||
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
|
|
||||||
""",
|
""",
|
||||||
|
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")
|
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(
|
gen_parser.add_argument(
|
||||||
"--concept", "-c",
|
"--concept", "-c",
|
||||||
help="Seed concept or story idea",
|
help="Seed concept or story idea",
|
||||||
@@ -77,48 +91,158 @@ Examples:
|
|||||||
"--words", "-w",
|
"--words", "-w",
|
||||||
type=int,
|
type=int,
|
||||||
default=5000,
|
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(
|
gen_parser.add_argument(
|
||||||
"--tone",
|
"--tone",
|
||||||
default="literary",
|
default="literary",
|
||||||
help="Writing tone",
|
help="Writing tone (default: literary)",
|
||||||
)
|
)
|
||||||
gen_parser.add_argument(
|
gen_parser.add_argument(
|
||||||
"--output", "-o",
|
"--output", "-o",
|
||||||
help="Output file path",
|
help="Output file path",
|
||||||
)
|
)
|
||||||
gen_parser.add_argument(
|
|
||||||
"--chapters", "-n",
|
|
||||||
type=int,
|
|
||||||
default=3,
|
|
||||||
help="Number of chapters",
|
|
||||||
)
|
|
||||||
gen_parser.add_argument(
|
gen_parser.add_argument(
|
||||||
"--use-crewai",
|
"--use-crewai",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Use CrewAI crews instead of direct agent calls",
|
help="Use CrewAI crews instead of LangGraph",
|
||||||
)
|
)
|
||||||
gen_parser.add_argument(
|
gen_parser.add_argument(
|
||||||
"--use-autogen",
|
"--no-autogen",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
default=True,
|
help="Disable AutoGen critique",
|
||||||
help="Use AutoGen for critique (default: True)",
|
)
|
||||||
|
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(
|
subparsers.add_parser(
|
||||||
"frameworks",
|
"frameworks",
|
||||||
help="List available story 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(
|
config_parser.add_argument(
|
||||||
"--show-keys",
|
"--show-keys",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Show API keys (masked)",
|
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
|
return parser
|
||||||
|
|
||||||
@@ -136,7 +260,7 @@ async def run_generate(args: argparse.Namespace) -> int:
|
|||||||
seed_concept = args.concept
|
seed_concept = args.concept
|
||||||
|
|
||||||
if args.repo:
|
if args.repo:
|
||||||
# Ingest from GitHub
|
# Ingest from GitHub - use FULL content
|
||||||
print(f"📥 Ingesting from GitHub: {args.repo}")
|
print(f"📥 Ingesting from GitHub: {args.repo}")
|
||||||
|
|
||||||
orch = OpusOrchestrator(
|
orch = OpusOrchestrator(
|
||||||
@@ -147,22 +271,30 @@ async def run_generate(args: argparse.Namespace) -> int:
|
|||||||
)
|
)
|
||||||
|
|
||||||
content = orch.ingest_from_github(args.repo)
|
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:
|
if not seed_concept:
|
||||||
print("Error: Please provide --concept or --repo")
|
print("Error: Please provide --concept or --repo")
|
||||||
return 1
|
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" Framework: {args.framework}")
|
||||||
print(f" Genre: {args.genre}")
|
print(f" Genre: {args.genre}")
|
||||||
print(f" Type: {args.book_type}")
|
print(f" Type: {args.book_type}")
|
||||||
|
print(f" Tone: {args.tone}")
|
||||||
print(f" CrewAI: {args.use_crewai}")
|
print(f" CrewAI: {args.use_crewai}")
|
||||||
print(f" AutoGen: {args.use_autogen}")
|
print(f" AutoGen: {not args.no_autogen}")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
use_autogen = not args.no_autogen
|
||||||
|
|
||||||
if args.use_crewai:
|
if args.use_crewai:
|
||||||
# Use CrewAI crews
|
# Use CrewAI crews
|
||||||
print("🛠️ Using CrewAI crews...\n")
|
print("🛠️ Using CrewAI crews...\n")
|
||||||
@@ -175,13 +307,12 @@ async def run_generate(args: argparse.Namespace) -> int:
|
|||||||
)
|
)
|
||||||
|
|
||||||
story = crew.write_full_story(
|
story = crew.write_full_story(
|
||||||
story_outline=seed_concept,
|
story_outline=seed_concept[:10000], # Limit for crew context
|
||||||
character_sheets="",
|
character_sheets="",
|
||||||
style_guide=f"Tone: {args.tone}",
|
style_guide=f"Tone: {args.tone}",
|
||||||
num_chapters=args.chapters,
|
num_chapters=args.chapters,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Combine chapters
|
|
||||||
manuscript = "\n\n---\n\n".join(story)
|
manuscript = "\n\n---\n\n".join(story)
|
||||||
else:
|
else:
|
||||||
crew = create_nonfiction_crew(
|
crew = create_nonfiction_crew(
|
||||||
@@ -191,7 +322,7 @@ async def run_generate(args: argparse.Namespace) -> int:
|
|||||||
)
|
)
|
||||||
|
|
||||||
manuscript = crew.write_section(
|
manuscript = crew.write_section(
|
||||||
section_outline=seed_concept,
|
section_outline=seed_concept[:10000],
|
||||||
style_guide=f"Tone: {args.tone}",
|
style_guide=f"Tone: {args.tone}",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -201,7 +332,7 @@ async def run_generate(args: argparse.Namespace) -> int:
|
|||||||
framework=args.framework,
|
framework=args.framework,
|
||||||
genre=args.genre,
|
genre=args.genre,
|
||||||
target_word_count=args.words,
|
target_word_count=args.words,
|
||||||
use_autogen=args.use_autogen,
|
use_autogen=use_autogen,
|
||||||
)
|
)
|
||||||
|
|
||||||
manuscript = result.get("manuscript", str(result))
|
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"# Opus Generated Manuscript\n\n")
|
||||||
f.write(f"Framework: {args.framework}\n")
|
f.write(f"Framework: {args.framework}\n")
|
||||||
f.write(f"Genre: {args.genre}\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(f"---\n\n")
|
||||||
f.write(manuscript)
|
f.write(manuscript)
|
||||||
|
|
||||||
@@ -232,20 +365,81 @@ async def run_generate(args: argparse.Namespace) -> int:
|
|||||||
return 0
|
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:
|
def run_frameworks(args: argparse.Namespace) -> int:
|
||||||
"""List available frameworks."""
|
"""List available frameworks."""
|
||||||
from opus_orchestrator.frameworks import 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():
|
for framework, info in FRAMEWORKS.items():
|
||||||
name = info.get("name", framework.value if hasattr(framework, "value") else str(framework))
|
name = info.get("name", framework.value if hasattr(framework, "value") else str(framework))
|
||||||
desc = info.get("description", "")
|
desc = info.get("description", "")
|
||||||
print(f" {name}")
|
stages = info.get("stages", [])
|
||||||
if desc:
|
beats = info.get("beats", [])
|
||||||
print(f" {desc}")
|
|
||||||
print()
|
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
|
return 0
|
||||||
|
|
||||||
|
|
||||||
@@ -255,27 +449,95 @@ def run_config(args: argparse.Namespace) -> int:
|
|||||||
|
|
||||||
config = get_config()
|
config = get_config()
|
||||||
|
|
||||||
print("\n⚙️ Opus Configuration:\n")
|
print("\n⚙️ OPUS CONFIGURATION\n")
|
||||||
print(f" Provider: {config.agent.provider}")
|
print("=" * 40)
|
||||||
print(f" Model: {config.agent.model}")
|
|
||||||
print(f" Temperature: {config.agent.temperature}")
|
print(f"\n🔹 Agent")
|
||||||
print(f" Max Tokens: {config.agent.max_tokens}")
|
print(f" Provider: {config.agent.provider}")
|
||||||
print(f" GitHub Token: {'✓ Set' if config.github_token else '✗ Not Set'}")
|
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:
|
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
|
return 0
|
||||||
|
|
||||||
|
|
||||||
async def main_async(args: argparse.Namespace) -> int:
|
async def main_async(args: argparse.Namespace) -> int:
|
||||||
"""Async main function."""
|
"""Async main function."""
|
||||||
if args.command == "generate":
|
commands = {
|
||||||
return await run_generate(args)
|
"generate": run_generate,
|
||||||
elif args.command == "frameworks":
|
"serve": run_serve,
|
||||||
return run_frameworks(args)
|
"ingest": run_ingest,
|
||||||
elif args.command == "config":
|
"frameworks": run_frameworks,
|
||||||
return run_config(args)
|
"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:
|
else:
|
||||||
# No command given, show help
|
# No command given, show help
|
||||||
args.parser.print_help()
|
args.parser.print_help()
|
||||||
@@ -291,7 +553,6 @@ def main():
|
|||||||
parser.print_help()
|
parser.print_help()
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
# Run async main
|
|
||||||
return asyncio.run(main_async(args))
|
return asyncio.run(main_async(args))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -1,3 +1,12 @@
|
|||||||
"""Utility functions for Opus Orchestrator."""
|
"""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",
|
||||||
|
]
|
||||||
|
|||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user