From 64bea5cae66a521ad495eaa8376c1d2928acea86 Mon Sep 17 00:00:00 2001 From: Mark Randall Havens Date: Fri, 13 Mar 2026 03:10:01 +0000 Subject: [PATCH] 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) --- opus_orchestrator/cli.py | 381 +++++++++++++++++++++++----- opus_orchestrator/server.py | 290 +++++++++++++++++++++ opus_orchestrator/utils/__init__.py | 11 +- opus_orchestrator/utils/docs.py | 317 +++++++++++++++++++++++ 4 files changed, 938 insertions(+), 61 deletions(-) create mode 100644 opus_orchestrator/server.py create mode 100644 opus_orchestrator/utils/docs.py diff --git a/opus_orchestrator/cli.py b/opus_orchestrator/cli.py index 746e840..f58b997 100644 --- a/opus_orchestrator/cli.py +++ b/opus_orchestrator/cli.py @@ -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)) diff --git a/opus_orchestrator/server.py b/opus_orchestrator/server.py new file mode 100644 index 0000000..e4bf70a --- /dev/null +++ b/opus_orchestrator/server.py @@ -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) diff --git a/opus_orchestrator/utils/__init__.py b/opus_orchestrator/utils/__init__.py index 965ca96..c526061 100644 --- a/opus_orchestrator/utils/__init__.py +++ b/opus_orchestrator/utils/__init__.py @@ -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", +] diff --git a/opus_orchestrator/utils/docs.py b/opus_orchestrator/utils/docs.py new file mode 100644 index 0000000..0f5459a --- /dev/null +++ b/opus_orchestrator/utils/docs.py @@ -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""" + + + Opus Orchestrator AI + + + +
{terminal}
+ +""" + + +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)