Spaces:
Sleeping
Sleeping
| """FastAPI server for the multi-agent content generation system. | |
| All agents run on port 7860 with different API endpoints: | |
| - /api/agents/showrunner - Generate episode directive | |
| - /api/agents/story-editor - Generate episode outline | |
| - /api/agents/cultural-consultant - Review for cultural accuracy | |
| - /api/agents/lead-writer - Write episode script | |
| - /api/agents/dialogue-specialist - Polish dialogue | |
| - /api/agents/comedy-writer - Add humor and punch-ups | |
| - /api/agents/proofreader - Final quality control | |
| - /api/pipeline - Execute full pipeline | |
| """ | |
| import logging | |
| from fastapi import FastAPI, HTTPException | |
| from pydantic import BaseModel | |
| from typing import Optional | |
| from orchestrator import PipelineOrchestrator | |
| from config import settings | |
| from instructor import Instructor | |
| from fastapi.responses import HTMLResponse | |
| import requests | |
| # Configure logging | |
| logging.basicConfig( | |
| level=settings.log_level, | |
| format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", | |
| ) | |
| logger = logging.getLogger(__name__) | |
| # Initialize FastAPI app | |
| app = FastAPI( | |
| title="Multi-Agent Content Generation System", | |
| description="API-first system for collaborative content generation using poolside/laguna-m.1:free. All agents on port 7860.", | |
| version="1.0.0", | |
| ) | |
| # Initialize orchestrator and instructor | |
| orchestrator = PipelineOrchestrator() | |
| instructor = Instructor() | |
| latest_briefs = {} # Store by session or just global for now | |
| # Request/Response Models | |
| class PipelineRequest(BaseModel): | |
| """Request model for pipeline execution.""" | |
| user_brief: str | |
| season_arc_document: str | |
| character_bible: str | |
| world_building_document: str | |
| character_voice_guide: str | |
| style_guide: str | |
| continuity_log: str | |
| hook_brief: Optional[str] = None | |
| class PipelineResponse(BaseModel): | |
| """Response model for pipeline execution.""" | |
| run_id: str | |
| status: str | |
| final_output: dict | |
| hf_output_url: str | |
| hf_metadata_url: str | |
| class HealthResponse(BaseModel): | |
| """Health check response.""" | |
| status: str | |
| run_id: str | |
| port: int | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # HEALTH & STATUS ENDPOINTS | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def health_check(): | |
| """Health check endpoint.""" | |
| return { | |
| "status": "healthy", | |
| "run_id": orchestrator.run_id, | |
| "port": settings.port, | |
| } | |
| async def root(): | |
| """Root endpoint redirects to Instructor UI.""" | |
| return await get_instructor_ui() | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # PIPELINE ENDPOINTS (Port 7860) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def execute_pipeline(request: PipelineRequest): | |
| """Execute the full content generation pipeline. | |
| Endpoint: POST /api/pipeline/execute | |
| Port: 7860 | |
| Args: | |
| request: Pipeline execution request with all required inputs | |
| Returns: | |
| Pipeline execution result with final output and URLs | |
| """ | |
| try: | |
| logger.info(f"[PIPELINE] Received execution request: {request.user_brief[:50]}...") | |
| result = orchestrator.execute_pipeline( | |
| user_brief=request.user_brief, | |
| season_arc_document=request.season_arc_document, | |
| character_bible=request.character_bible, | |
| world_building_document=request.world_building_document, | |
| character_voice_guide=request.character_voice_guide, | |
| style_guide=request.style_guide, | |
| continuity_log=request.continuity_log, | |
| hook_brief=request.hook_brief, | |
| ) | |
| return { | |
| "run_id": result["run_id"], | |
| "status": result["status"], | |
| "final_output": result["final_output"], | |
| "hf_output_url": result["hf_output_url"], | |
| "hf_metadata_url": result["hf_metadata_url"], | |
| } | |
| except Exception as e: | |
| logger.error(f"[PIPELINE] Execution error: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def get_pipeline_status(run_id: str): | |
| """Get the status of a pipeline run. | |
| Endpoint: GET /api/pipeline/status/{run_id} | |
| Port: 7860 | |
| Args: | |
| run_id: The run ID to check | |
| Returns: | |
| Pipeline status and state | |
| """ | |
| try: | |
| if run_id == orchestrator.run_id: | |
| return { | |
| "run_id": run_id, | |
| "status": orchestrator.pipeline_state.get("status", "unknown"), | |
| "pipeline_state": orchestrator.pipeline_state, | |
| } | |
| else: | |
| raise HTTPException(status_code=404, detail="Run ID not found") | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # INSTRUCTOR ENDPOINTS (Port 7860) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def get_instructor_ui(): | |
| """Instructor UI for brief approval.""" | |
| html_content = """ | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Instructor Module - Brief Approval</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <style> | |
| .loading { display: none; } | |
| .loading.active { display: flex; } | |
| </style> | |
| </head> | |
| <body class="bg-gray-100 min-h-screen p-8"> | |
| <div class="max-w-4xl mx-auto bg-white rounded-xl shadow-md overflow-hidden p-6"> | |
| <h1 class="text-3xl font-bold text-gray-800 mb-6">Instructor Module</h1> | |
| <div id="setup-section" class="mb-8"> | |
| <h2 class="text-xl font-semibold mb-4">Generate New Brief</h2> | |
| <div class="flex flex-col gap-4"> | |
| <p class="text-sm text-gray-600">Enter trending topics or news items to fuse into a storytelling brief.</p> | |
| <div class="flex gap-4"> | |
| <input type="text" id="topics-input" placeholder="e.g. AI agents, TikTok trends, Summer Olympics" | |
| class="flex-1 p-2 border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none"> | |
| <button onclick="generateBrief()" id="gen-btn" | |
| class="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 transition"> | |
| Generate | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="loading-spinner" class="loading flex-col items-center justify-center py-12"> | |
| <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div> | |
| <span class="ml-4 mt-4 text-gray-600">Instructor is fusing trends into a story...</span> | |
| </div> | |
| <div id="brief-section" class="hidden"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h2 class="text-xl font-semibold text-blue-600">Proposed Storytelling Brief</h2> | |
| <span class="bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-0.5 rounded">LLM Generated</span> | |
| </div> | |
| <div id="brief-content" class="space-y-4 bg-gray-50 p-6 rounded-lg border border-gray-200 mb-6 max-h-[500px] overflow-y-auto"> | |
| <!-- Brief fields will be injected here --> | |
| </div> | |
| <div class="flex gap-4"> | |
| <button onclick="approveBrief(true)" id="approve-btn" | |
| class="bg-green-600 text-white px-8 py-3 rounded-lg font-bold hover:bg-green-700 transition flex-1 shadow-lg"> | |
| Approve & Execute Pipeline | |
| </button> | |
| <button onclick="approveBrief(false)" | |
| class="bg-gray-400 text-white px-8 py-3 rounded-lg font-bold hover:bg-gray-500 transition"> | |
| Discard | |
| </button> | |
| </div> | |
| </div> | |
| <div id="status-section" class="mt-8 p-4 rounded-lg hidden"> | |
| <p id="status-message" class="font-medium"></p> | |
| <div id="status-details" class="mt-2 text-sm"></div> | |
| </div> | |
| </div> | |
| <script> | |
| let currentBrief = null; | |
| async function generateBrief() { | |
| const topicsInput = document.getElementById('topics-input').value; | |
| const topics = topicsInput.split(',').map(t => t.trim()).filter(t => t); | |
| if (topics.length === 0) { | |
| alert('Please enter at least one topic'); | |
| return; | |
| } | |
| showLoading(true); | |
| document.getElementById('brief-section').classList.add('hidden'); | |
| document.getElementById('status-section').classList.add('hidden'); | |
| try { | |
| const response = await fetch('/api/instructor/generate', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ topics }) | |
| }); | |
| const data = await response.json(); | |
| if (data.status === 'success') { | |
| currentBrief = data.brief; | |
| displayBrief(currentBrief); | |
| } else { | |
| throw new Error(data.detail || 'Generation failed'); | |
| } | |
| } catch (error) { | |
| showStatus('Error generating brief: ' + error.message, 'bg-red-100 text-red-700'); | |
| } finally { | |
| showLoading(false); | |
| } | |
| } | |
| function displayBrief(brief) { | |
| const content = document.getElementById('brief-content'); | |
| content.innerHTML = ''; | |
| const fieldOrder = [ | |
| 'user_brief', 'hook_brief', 'style_guide', | |
| 'character_bible', 'world_building_document', | |
| 'season_arc_document', 'character_voice_guide', 'continuity_log' | |
| ]; | |
| fieldOrder.forEach(key => { | |
| if (brief[key]) { | |
| const div = document.createElement('div'); | |
| div.className = 'mb-6 border-b border-gray-200 pb-4 last:border-0'; | |
| div.innerHTML = ` | |
| <h3 class="text-xs font-bold text-blue-500 uppercase tracking-widest mb-1">${key.replace(/_/g, ' ')}</h3> | |
| <p class="text-gray-800 leading-relaxed">${brief[key]}</p> | |
| `; | |
| content.appendChild(div); | |
| } | |
| }); | |
| document.getElementById('brief-section').classList.remove('hidden'); | |
| content.scrollTop = 0; | |
| } | |
| async function approveBrief(approved) { | |
| if (!approved) { | |
| document.getElementById('brief-section').classList.add('hidden'); | |
| return; | |
| } | |
| const btn = document.getElementById('approve-btn'); | |
| btn.disabled = true; | |
| btn.innerHTML = ` | |
| <svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white inline" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> | |
| <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> | |
| <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> | |
| </svg> | |
| Executing Pipeline... | |
| `; | |
| try { | |
| const response = await fetch('/api/instructor/approve', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ approved: true }) | |
| }); | |
| const data = await response.json(); | |
| if (response.ok) { | |
| showStatus('Success! Pipeline execution started.', 'bg-green-100 text-green-700'); | |
| const details = document.getElementById('status-details'); | |
| details.innerHTML = ` | |
| <p><strong>Run ID:</strong> ${data.run_id}</p> | |
| <p class="mt-2"><a href="${data.hf_output_url}" target="_blank" class="text-blue-600 underline">View Output on Hugging Face</a></p> | |
| `; | |
| document.getElementById('brief-section').classList.add('hidden'); | |
| } else { | |
| showStatus('Error: ' + (data.detail || 'Failed to start pipeline'), 'bg-red-100 text-red-700'); | |
| } | |
| } catch (error) { | |
| showStatus('Connection error: ' + error.message, 'bg-red-100 text-red-700'); | |
| } finally { | |
| btn.disabled = false; | |
| btn.innerText = 'Approve & Execute Pipeline'; | |
| } | |
| } | |
| function showLoading(show) { | |
| document.getElementById('loading-spinner').classList.toggle('active', show); | |
| document.getElementById('gen-btn').disabled = show; | |
| } | |
| function showStatus(msg, classes) { | |
| const section = document.getElementById('status-section'); | |
| section.className = 'mt-8 p-4 rounded-lg ' + classes; | |
| document.getElementById('status-message').innerText = msg; | |
| section.classList.remove('hidden'); | |
| } | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| return HTMLResponse(content=html_content) | |
| async def api_generate_brief(request: dict): | |
| """API endpoint to generate a brief from trending topics.""" | |
| try: | |
| topics = request.get("topics", []) | |
| if not topics: | |
| raise HTTPException(status_code=400, detail="No topics provided") | |
| logger.info(f"[INSTRUCTOR] Generating brief for topics: {topics}") | |
| brief = instructor.generate_brief(topics) | |
| # Store the brief for approval (using a simple global for this demo) | |
| global latest_briefs | |
| latest_briefs["current"] = brief | |
| return {"status": "success", "brief": brief} | |
| except Exception as e: | |
| logger.error(f"[INSTRUCTOR] Error: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def api_approve_brief(request: dict): | |
| """API endpoint to approve and send the brief to the orchestrator.""" | |
| if not request.get("approved"): | |
| return {"status": "discarded"} | |
| global latest_briefs | |
| brief = latest_briefs.get("current") | |
| if not brief: | |
| raise HTTPException(status_code=400, detail="No brief available to approve") | |
| try: | |
| logger.info("[INSTRUCTOR] Brief approved, executing pipeline") | |
| # We can call the orchestrator directly since we're in the same process | |
| result = orchestrator.execute_pipeline( | |
| user_brief=brief["user_brief"], | |
| season_arc_document=brief["season_arc_document"], | |
| character_bible=brief["character_bible"], | |
| world_building_document=brief["world_building_document"], | |
| character_voice_guide=brief["character_voice_guide"], | |
| style_guide=brief["style_guide"], | |
| continuity_log=brief["continuity_log"], | |
| hook_brief=brief.get("hook_brief"), | |
| ) | |
| return { | |
| "status": "success", | |
| "run_id": result["run_id"], | |
| "hf_output_url": result["hf_output_url"] | |
| } | |
| except Exception as e: | |
| logger.error(f"[INSTRUCTOR] Pipeline error: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # AGENT ENDPOINTS (Port 7860) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def showrunner_endpoint(request: dict): | |
| """Showrunner Agent: Generate episode directive. | |
| Endpoint: POST /api/agents/showrunner | |
| Port: 7860 | |
| Input: | |
| - user_brief: Initial user brief | |
| - season_arc_document: Season context | |
| - character_bible: Character definitions | |
| Output: | |
| - episode_directive: Generated directive | |
| - story_premise: Story premise | |
| - tone_brief: Tone brief | |
| - character_focus_notes: Character notes | |
| """ | |
| try: | |
| logger.info("[SHOWRUNNER] Processing directive generation") | |
| result = orchestrator.showrunner.generate_directive(request) | |
| return result | |
| except Exception as e: | |
| logger.error(f"[SHOWRUNNER] Error: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def story_editor_endpoint(request: dict): | |
| """Story Editor Agent: Generate episode outline. | |
| Endpoint: POST /api/agents/story-editor | |
| Port: 7860 | |
| Input: | |
| - episode_directive: From Showrunner | |
| - series_continuity_log: Continuity reference | |
| - character_list: Valid character names | |
| Output: | |
| - episode_outline: Generated outline | |
| - act_structure: Act breakdown | |
| - story_notes_for_writers: Story notes | |
| """ | |
| try: | |
| logger.info("[STORY-EDITOR] Processing outline generation") | |
| result = orchestrator.story_editor.generate_outline(request) | |
| return result | |
| except Exception as e: | |
| logger.error(f"[STORY-EDITOR] Error: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def cultural_consultant_endpoint(request: dict): | |
| """Cultural Consultant Agent: Review for cultural accuracy. | |
| Endpoint: POST /api/agents/cultural-consultant | |
| Port: 7860 | |
| Input: | |
| - episode_outline: From Story Editor | |
| - world_building_document: World context | |
| - character_list: Valid character names | |
| Output: | |
| - cultural_accuracy_notes: Accuracy notes | |
| - flagged_inaccuracies: Issues found | |
| - approved_touchpoints: Approved elements | |
| - reference_suggestions: Suggestions | |
| """ | |
| try: | |
| logger.info("[CULTURAL-CONSULTANT] Processing cultural review") | |
| result = orchestrator.cultural_consultant.review_outline(request) | |
| return result | |
| except Exception as e: | |
| logger.error(f"[CULTURAL-CONSULTANT] Error: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def lead_writer_endpoint(request: dict): | |
| """Lead Writer Agent: Write episode script. | |
| Endpoint: POST /api/agents/lead-writer | |
| Port: 7860 | |
| Input: | |
| - approved_outline: From Story Editor | |
| - cultural_consultant_notes: From Cultural Consultant | |
| - character_voice_guide: Character voice definitions | |
| - character_list: Valid character names | |
| - story_premise: Story premise from Showrunner | |
| Output: | |
| - full_episode_first_draft: Complete script | |
| - scene_descriptions: Scene descriptions | |
| - dialogue: Dialogue content | |
| - stage_directions: Stage directions | |
| """ | |
| try: | |
| logger.info("[LEAD-WRITER] Processing script writing") | |
| result = orchestrator.lead_writer.write_script(request) | |
| return result | |
| except Exception as e: | |
| logger.error(f"[LEAD-WRITER] Error: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def dialogue_specialist_endpoint(request: dict): | |
| """Dialogue Specialist Agent: Polish dialogue. | |
| Endpoint: POST /api/agents/dialogue-specialist | |
| Port: 7860 | |
| Input: | |
| - first_draft_script: From Lead Writer | |
| - character_voice_guide: Character voice definitions | |
| - character_list: Valid character names | |
| - dialect_slang_reference: Reference material | |
| Output: | |
| - dialogue_polished_script: Polished script | |
| - voice_consistency_notes: Voice notes | |
| """ | |
| try: | |
| logger.info("[DIALOGUE-SPECIALIST] Processing dialogue polish") | |
| result = orchestrator.dialogue_specialist.polish_dialogue(request) | |
| return result | |
| except Exception as e: | |
| logger.error(f"[DIALOGUE-SPECIALIST] Error: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def comedy_writer_endpoint(request: dict): | |
| """Comedy Writer Agent: Add humor and punch-ups. | |
| Endpoint: POST /api/agents/comedy-writer | |
| Port: 7860 | |
| Input: | |
| - dialogue_polished_script: From Dialogue Specialist | |
| - hook_brief_from_showrunner: Hook context | |
| - character_list: Valid character names | |
| - tone_brief: Tone reference | |
| Output: | |
| - comedy_sharpened_script: Comedy-enhanced script | |
| - punch_up_notes: Notes on changes | |
| - hook_rewrite_for_opening: Opening hook | |
| """ | |
| try: | |
| logger.info("[COMEDY-WRITER] Processing humor addition") | |
| result = orchestrator.comedy_writer.add_humor(request) | |
| return result | |
| except Exception as e: | |
| logger.error(f"[COMEDY-WRITER] Error: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def proofreader_endpoint(request: dict): | |
| """Proofreader Agent: Final quality control. | |
| Endpoint: POST /api/agents/proofreader | |
| Port: 7860 | |
| Input: | |
| - comedy_sharpened_script: From Comedy Writer | |
| - style_guide: Style reference | |
| - continuity_log: Continuity tracking | |
| - character_list: Valid character names | |
| Output: | |
| - final_locked_script: Final script | |
| - qc_report: Quality control report | |
| - continuity_log_update: Updated continuity log | |
| """ | |
| try: | |
| logger.info("[PROOFREADER] Processing final QC") | |
| result = orchestrator.proofreader.final_qc(request) | |
| return result | |
| except Exception as e: | |
| logger.error(f"[PROOFREADER] Error: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # SERVER STARTUP | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if __name__ == "__main__": | |
| import uvicorn | |
| logger.info(f"Starting Multi-Agent System on port {settings.port}") | |
| logger.info("All agents available at http://localhost:7860/api/agents/*") | |
| logger.info("Full pipeline available at http://localhost:7860/api/pipeline/execute") | |
| logger.info("API documentation at http://localhost:7860/docs") | |
| uvicorn.run( | |
| app, | |
| host=settings.host, | |
| port=settings.port, | |
| log_level=settings.log_level.lower(), | |
| ) | |