Spaces:
Sleeping
Sleeping
Deployment Script commited on
Commit Β·
2dfc473
1
Parent(s): 6225fce
Deploy multi-agent system
Browse files- .dockerignore +31 -0
- .gitignore +64 -0
- Dockerfile +33 -0
- README_SPACE.md +164 -0
- agents/__init__.py +18 -0
- agents/comedy_writer.py +45 -0
- agents/cultural_consultant.py +45 -0
- agents/dialogue_specialist.py +46 -0
- agents/lead_writer.py +46 -0
- agents/proofreader.py +46 -0
- agents/showrunner.py +46 -0
- agents/story_editor.py +45 -0
- app.py +28 -0
- base_agent.py +137 -0
- config.py +82 -0
- env_loader.py +113 -0
- hf_uploader.py +110 -0
- main.py +255 -0
- orchestrator.py +207 -0
- requirements.txt +10 -0
- setup_space.py +143 -0
.dockerignore
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.git
|
| 2 |
+
.gitignore
|
| 3 |
+
.env
|
| 4 |
+
.env.local
|
| 5 |
+
__pycache__
|
| 6 |
+
*.pyc
|
| 7 |
+
*.pyo
|
| 8 |
+
*.pyd
|
| 9 |
+
.Python
|
| 10 |
+
env/
|
| 11 |
+
venv/
|
| 12 |
+
.venv
|
| 13 |
+
.DS_Store
|
| 14 |
+
*.log
|
| 15 |
+
logs/
|
| 16 |
+
data/
|
| 17 |
+
output/
|
| 18 |
+
.pytest_cache
|
| 19 |
+
.coverage
|
| 20 |
+
htmlcov/
|
| 21 |
+
dist/
|
| 22 |
+
build/
|
| 23 |
+
*.egg-info/
|
| 24 |
+
.idea/
|
| 25 |
+
.vscode/
|
| 26 |
+
*.swp
|
| 27 |
+
*.swo
|
| 28 |
+
*~
|
| 29 |
+
.DS_Store
|
| 30 |
+
node_modules/
|
| 31 |
+
npm-debug.log
|
.gitignore
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Environment
|
| 2 |
+
.env
|
| 3 |
+
.env.local
|
| 4 |
+
.env.*.local
|
| 5 |
+
|
| 6 |
+
# Python
|
| 7 |
+
__pycache__/
|
| 8 |
+
*.py[cod]
|
| 9 |
+
*$py.class
|
| 10 |
+
*.so
|
| 11 |
+
.Python
|
| 12 |
+
env/
|
| 13 |
+
venv/
|
| 14 |
+
ENV/
|
| 15 |
+
build/
|
| 16 |
+
develop-eggs/
|
| 17 |
+
dist/
|
| 18 |
+
downloads/
|
| 19 |
+
eggs/
|
| 20 |
+
.eggs/
|
| 21 |
+
lib/
|
| 22 |
+
lib64/
|
| 23 |
+
parts/
|
| 24 |
+
sdist/
|
| 25 |
+
var/
|
| 26 |
+
wheels/
|
| 27 |
+
pip-wheel-metadata/
|
| 28 |
+
share/python-wheels/
|
| 29 |
+
*.egg-info/
|
| 30 |
+
.installed.cfg
|
| 31 |
+
*.egg
|
| 32 |
+
MANIFEST
|
| 33 |
+
|
| 34 |
+
# Testing
|
| 35 |
+
.pytest_cache/
|
| 36 |
+
.coverage
|
| 37 |
+
htmlcov/
|
| 38 |
+
|
| 39 |
+
# IDE
|
| 40 |
+
.vscode/
|
| 41 |
+
.idea/
|
| 42 |
+
*.swp
|
| 43 |
+
*.swo
|
| 44 |
+
*~
|
| 45 |
+
.DS_Store
|
| 46 |
+
|
| 47 |
+
# Logs
|
| 48 |
+
logs/
|
| 49 |
+
*.log
|
| 50 |
+
server.log
|
| 51 |
+
|
| 52 |
+
# Data and output
|
| 53 |
+
data/
|
| 54 |
+
output/
|
| 55 |
+
*.json
|
| 56 |
+
|
| 57 |
+
# OS
|
| 58 |
+
.DS_Store
|
| 59 |
+
Thumbs.db
|
| 60 |
+
|
| 61 |
+
# Dependencies
|
| 62 |
+
node_modules/
|
| 63 |
+
npm-debug.log
|
| 64 |
+
yarn-error.log
|
Dockerfile
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
# Set working directory
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
|
| 6 |
+
# Install system dependencies
|
| 7 |
+
RUN apt-get update && apt-get install -y \
|
| 8 |
+
git \
|
| 9 |
+
curl \
|
| 10 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 11 |
+
|
| 12 |
+
# Copy requirements
|
| 13 |
+
COPY requirements.txt .
|
| 14 |
+
|
| 15 |
+
# Install Python dependencies
|
| 16 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 17 |
+
|
| 18 |
+
# Copy application code
|
| 19 |
+
COPY . .
|
| 20 |
+
|
| 21 |
+
# Create necessary directories
|
| 22 |
+
RUN mkdir -p data output logs
|
| 23 |
+
|
| 24 |
+
# Expose port
|
| 25 |
+
EXPOSE 7860
|
| 26 |
+
|
| 27 |
+
# Health check
|
| 28 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
| 29 |
+
CMD curl -f http://localhost:7860/health || exit 1
|
| 30 |
+
|
| 31 |
+
# Run setup and start the application
|
| 32 |
+
RUN chmod +x /app/setup_space.py
|
| 33 |
+
CMD python setup_space.py && python app.py
|
README_SPACE.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Multi-Agent Content Generation System - Hugging Face Space
|
| 2 |
+
|
| 3 |
+
This is a Hugging Face Space deployment of the multi-agent content generation system. The system orchestrates seven specialized AI agents to collaboratively produce high-quality written content.
|
| 4 |
+
|
| 5 |
+
## Features
|
| 6 |
+
|
| 7 |
+
- **7 Specialized Agents** - Showrunner, Story Editor, Cultural Consultant, Lead Writer, Dialogue Specialist, Comedy Writer, and Proofreader
|
| 8 |
+
- **API-First Architecture** - RESTful endpoints for all operations
|
| 9 |
+
- **OpenRouter Integration** - Powered by `poolside/laguna-m.1:free` model
|
| 10 |
+
- **Hugging Face Integration** - Automatic upload of outputs to dataset
|
| 11 |
+
- **Persistent Storage** - All agent outputs saved as JSON
|
| 12 |
+
|
| 13 |
+
## Setup
|
| 14 |
+
|
| 15 |
+
### 1. Create the Space
|
| 16 |
+
|
| 17 |
+
Go to [Hugging Face Spaces](https://huggingface.co/spaces) and create a new Space:
|
| 18 |
+
- **Name:** `multi-agent-system` (or your preferred name)
|
| 19 |
+
- **License:** MIT (or your choice)
|
| 20 |
+
- **Space SDK:** Docker
|
| 21 |
+
- **Visibility:** Private (recommended for security)
|
| 22 |
+
|
| 23 |
+
### 2. Add Secrets
|
| 24 |
+
|
| 25 |
+
In your Space settings, add the following secrets:
|
| 26 |
+
|
| 27 |
+
#### `OPENROUTER_API_KEY`
|
| 28 |
+
Your OpenRouter API key for accessing the `poolside/laguna-m.1:free` model.
|
| 29 |
+
- Get it from: https://openrouter.ai/keys
|
| 30 |
+
|
| 31 |
+
#### `HUGGINGFACE_TOKEN`
|
| 32 |
+
Your Hugging Face user access token with write permissions.
|
| 33 |
+
- Get it from: https://huggingface.co/settings/tokens
|
| 34 |
+
- Must have write access to the `factorstudios/Pipeline` dataset
|
| 35 |
+
|
| 36 |
+
### 3. Configure Environment Variables
|
| 37 |
+
|
| 38 |
+
Optional environment variables (set in Space settings if needed):
|
| 39 |
+
|
| 40 |
+
- `HUGGINGFACE_DATASET` - Target dataset (default: `factorstudios/Pipeline`)
|
| 41 |
+
- `OPENROUTER_BASE_URL` - OpenRouter API endpoint (default: `https://openrouter.ai/api/v1`)
|
| 42 |
+
- `MODEL_NAME` - Model to use (default: `poolside/laguna-m.1:free`)
|
| 43 |
+
- `LOG_LEVEL` - Logging level (default: `INFO`)
|
| 44 |
+
|
| 45 |
+
## Usage
|
| 46 |
+
|
| 47 |
+
### Health Check
|
| 48 |
+
|
| 49 |
+
```bash
|
| 50 |
+
curl https://your-space-url/health
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
### Execute Full Pipeline
|
| 54 |
+
|
| 55 |
+
```bash
|
| 56 |
+
curl -X POST https://your-space-url/api/v1/pipeline/execute \
|
| 57 |
+
-H "Content-Type: application/json" \
|
| 58 |
+
-d '{
|
| 59 |
+
"user_brief": "A comedic episode about...",
|
| 60 |
+
"season_arc_document": "Season context...",
|
| 61 |
+
"character_bible": "Character definitions...",
|
| 62 |
+
"world_building_document": "World context...",
|
| 63 |
+
"character_voice_guide": "Voice definitions...",
|
| 64 |
+
"style_guide": "Style reference...",
|
| 65 |
+
"continuity_log": "Continuity tracking...",
|
| 66 |
+
"hook_brief": "Optional hook..."
|
| 67 |
+
}'
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
### API Documentation
|
| 71 |
+
|
| 72 |
+
Visit `https://your-space-url/docs` for interactive API documentation.
|
| 73 |
+
|
| 74 |
+
## API Endpoints
|
| 75 |
+
|
| 76 |
+
### Pipeline Management
|
| 77 |
+
- `GET /health` - Health check
|
| 78 |
+
- `POST /api/v1/pipeline/execute` - Execute full pipeline
|
| 79 |
+
- `GET /api/v1/pipeline/status/{run_id}` - Get pipeline status
|
| 80 |
+
|
| 81 |
+
### Individual Agents
|
| 82 |
+
- `POST /api/v1/showrunner/generate_directive` - Generate directive
|
| 83 |
+
- `POST /api/v1/story_editor/generate_outline` - Generate outline
|
| 84 |
+
- `POST /api/v1/cultural_consultant/review_outline` - Review outline
|
| 85 |
+
- `POST /api/v1/lead_writer/write_script` - Write script
|
| 86 |
+
- `POST /api/v1/dialogue_specialist/polish_dialogue` - Polish dialogue
|
| 87 |
+
- `POST /api/v1/comedy_writer/add_humor` - Add humor
|
| 88 |
+
- `POST /api/v1/proofreader/final_qc` - Final QC
|
| 89 |
+
|
| 90 |
+
## Architecture
|
| 91 |
+
|
| 92 |
+
```
|
| 93 |
+
FastAPI Server (Port 7860)
|
| 94 |
+
β
|
| 95 |
+
Pipeline Orchestrator
|
| 96 |
+
β
|
| 97 |
+
7 Agent Modules
|
| 98 |
+
β
|
| 99 |
+
OpenRouter API (poolside/laguna-m.1:free)
|
| 100 |
+
β
|
| 101 |
+
Hugging Face Dataset Upload
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
## Security
|
| 105 |
+
|
| 106 |
+
- **Secrets Management** - API keys stored securely in HF Secrets
|
| 107 |
+
- **No Hardcoded Credentials** - Environment variables loaded at runtime
|
| 108 |
+
- **HTTPS Only** - All communication encrypted
|
| 109 |
+
- **Private Space** - Recommended for production use
|
| 110 |
+
|
| 111 |
+
## Performance
|
| 112 |
+
|
| 113 |
+
Typical pipeline execution time: **10-15 minutes**
|
| 114 |
+
|
| 115 |
+
Each agent processes sequentially through the OpenRouter API:
|
| 116 |
+
1. Showrunner: ~40 seconds
|
| 117 |
+
2. Story Editor: ~150 seconds
|
| 118 |
+
3. Cultural Consultant: ~130 seconds
|
| 119 |
+
4. Lead Writer: ~140 seconds
|
| 120 |
+
5. Dialogue Specialist: ~50 seconds
|
| 121 |
+
6. Comedy Writer: ~120 seconds
|
| 122 |
+
7. Proofreader: ~40 seconds
|
| 123 |
+
|
| 124 |
+
## Storage
|
| 125 |
+
|
| 126 |
+
- **Local Storage** - Agent outputs saved in Space filesystem
|
| 127 |
+
- **Hugging Face Dataset** - Final outputs uploaded to `factorstudios/Pipeline`
|
| 128 |
+
- **Metadata** - Pipeline execution metadata also uploaded
|
| 129 |
+
|
| 130 |
+
## Troubleshooting
|
| 131 |
+
|
| 132 |
+
### 401 Unauthorized
|
| 133 |
+
- Check `OPENROUTER_API_KEY` is set correctly in Secrets
|
| 134 |
+
- Verify key has access to `poolside/laguna-m.1:free` model
|
| 135 |
+
|
| 136 |
+
### 403 Forbidden (Hugging Face)
|
| 137 |
+
- Check `HUGGINGFACE_TOKEN` is set correctly in Secrets
|
| 138 |
+
- Verify token has write access to dataset
|
| 139 |
+
- Confirm dataset exists: `factorstudios/Pipeline`
|
| 140 |
+
|
| 141 |
+
### Timeout Errors
|
| 142 |
+
- Pipeline execution can take 10-15 minutes
|
| 143 |
+
- Increase request timeout to 600+ seconds
|
| 144 |
+
- Check Space logs for details
|
| 145 |
+
|
| 146 |
+
### Out of Memory
|
| 147 |
+
- Spaces have limited memory
|
| 148 |
+
- Consider splitting pipeline into separate requests
|
| 149 |
+
- Use individual agent endpoints instead of full pipeline
|
| 150 |
+
|
| 151 |
+
## Logs
|
| 152 |
+
|
| 153 |
+
View Space logs in the Hugging Face Space interface for debugging.
|
| 154 |
+
|
| 155 |
+
## Support
|
| 156 |
+
|
| 157 |
+
For issues or questions:
|
| 158 |
+
1. Check the logs in your Space
|
| 159 |
+
2. Review API documentation at `/docs`
|
| 160 |
+
3. Verify all Secrets are configured correctly
|
| 161 |
+
|
| 162 |
+
## License
|
| 163 |
+
|
| 164 |
+
This project is part of Factor Studios' multi-agent content generation initiative.
|
agents/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Agent modules for the multi-agent system."""
|
| 2 |
+
from agents.showrunner import ShowrunnerAgent
|
| 3 |
+
from agents.story_editor import StoryEditorAgent
|
| 4 |
+
from agents.cultural_consultant import CulturalConsultantAgent
|
| 5 |
+
from agents.lead_writer import LeadWriterAgent
|
| 6 |
+
from agents.dialogue_specialist import DialogueSpecialistAgent
|
| 7 |
+
from agents.comedy_writer import ComedyWriterAgent
|
| 8 |
+
from agents.proofreader import ProofreaderAgent
|
| 9 |
+
|
| 10 |
+
__all__ = [
|
| 11 |
+
"ShowrunnerAgent",
|
| 12 |
+
"StoryEditorAgent",
|
| 13 |
+
"CulturalConsultantAgent",
|
| 14 |
+
"LeadWriterAgent",
|
| 15 |
+
"DialogueSpecialistAgent",
|
| 16 |
+
"ComedyWriterAgent",
|
| 17 |
+
"ProofreaderAgent",
|
| 18 |
+
]
|
agents/comedy_writer.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Comedy Writer agent - Humor enhancement specialist."""
|
| 2 |
+
from typing import Any, Dict
|
| 3 |
+
from base_agent import BaseAgent
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class ComedyWriterAgent(BaseAgent):
|
| 7 |
+
"""Comedy Writer agent that enhances script with humor."""
|
| 8 |
+
|
| 9 |
+
def __init__(self):
|
| 10 |
+
"""Initialize the Comedy Writer agent."""
|
| 11 |
+
system_prompt = (
|
| 12 |
+
"You are the Comedy Writer. Punch up the script β sharpen punchlines, tighten "
|
| 13 |
+
"comic timing, make the first 3 seconds irresistible. Don't break the story. "
|
| 14 |
+
"Return the improved full script with a note on what you changed as JSON with keys: "
|
| 15 |
+
"comedy_sharpened_script, punch_up_notes, hook_rewrite_for_opening."
|
| 16 |
+
)
|
| 17 |
+
super().__init__(
|
| 18 |
+
agent_id="comedy-writer",
|
| 19 |
+
agent_name="Comedy Writer",
|
| 20 |
+
system_prompt=system_prompt,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
def add_humor(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
| 24 |
+
"""Add humor and punch-ups to the script.
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
inputs: Dictionary with keys:
|
| 28 |
+
- dialogue_polished_script: From Dialogue Specialist
|
| 29 |
+
- hook_brief_from_showrunner: Hook context
|
| 30 |
+
|
| 31 |
+
Returns:
|
| 32 |
+
Dictionary with comedy-enhanced script and notes
|
| 33 |
+
"""
|
| 34 |
+
outputs = self.process(inputs)
|
| 35 |
+
|
| 36 |
+
# Save state
|
| 37 |
+
self.save_state(
|
| 38 |
+
{
|
| 39 |
+
"inputs": inputs,
|
| 40 |
+
"outputs": outputs,
|
| 41 |
+
},
|
| 42 |
+
filename="comedy_punch_up.json",
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
return outputs
|
agents/cultural_consultant.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Cultural Consultant agent - Authenticity specialist."""
|
| 2 |
+
from typing import Any, Dict
|
| 3 |
+
from base_agent import BaseAgent
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class CulturalConsultantAgent(BaseAgent):
|
| 7 |
+
"""Cultural Consultant agent that ensures cultural accuracy."""
|
| 8 |
+
|
| 9 |
+
def __init__(self):
|
| 10 |
+
"""Initialize the Cultural Consultant agent."""
|
| 11 |
+
system_prompt = (
|
| 12 |
+
"You are the Cultural Consultant. Review the episode outline for cultural accuracy, "
|
| 13 |
+
"stereotypes, or misrepresentations. Return a brief report as a JSON object with keys: "
|
| 14 |
+
"cultural_accuracy_notes, reference_suggestions, flagged_inaccuracies, "
|
| 15 |
+
"approved_cultural_touchpoints. Be direct."
|
| 16 |
+
)
|
| 17 |
+
super().__init__(
|
| 18 |
+
agent_id="cultural-consultant",
|
| 19 |
+
agent_name="Cultural Consultant",
|
| 20 |
+
system_prompt=system_prompt,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
def review_outline(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
| 24 |
+
"""Review outline for cultural accuracy.
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
inputs: Dictionary with keys:
|
| 28 |
+
- episode_outline: From Story Editor
|
| 29 |
+
- world_building_document: World context
|
| 30 |
+
|
| 31 |
+
Returns:
|
| 32 |
+
Dictionary with cultural review and recommendations
|
| 33 |
+
"""
|
| 34 |
+
outputs = self.process(inputs)
|
| 35 |
+
|
| 36 |
+
# Save state
|
| 37 |
+
self.save_state(
|
| 38 |
+
{
|
| 39 |
+
"inputs": inputs,
|
| 40 |
+
"outputs": outputs,
|
| 41 |
+
},
|
| 42 |
+
filename="cultural_review.json",
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
return outputs
|
agents/dialogue_specialist.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Dialogue Specialist agent - Voice polish expert."""
|
| 2 |
+
from typing import Any, Dict
|
| 3 |
+
from base_agent import BaseAgent
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class DialogueSpecialistAgent(BaseAgent):
|
| 7 |
+
"""Dialogue Specialist agent that refines dialogue for authenticity."""
|
| 8 |
+
|
| 9 |
+
def __init__(self):
|
| 10 |
+
"""Initialize the Dialogue Specialist agent."""
|
| 11 |
+
system_prompt = (
|
| 12 |
+
"You are the Dialogue Specialist. Polish the script's dialogue only β don't touch "
|
| 13 |
+
"action lines or structure. Make each character's speech feel authentic, natural, and "
|
| 14 |
+
"distinct. Return the full script with improved dialogue as JSON with keys: "
|
| 15 |
+
"dialogue_polished_script, voice_consistency_notes."
|
| 16 |
+
)
|
| 17 |
+
super().__init__(
|
| 18 |
+
agent_id="dialogue-specialist",
|
| 19 |
+
agent_name="Dialogue Specialist",
|
| 20 |
+
system_prompt=system_prompt,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
def polish_dialogue(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
| 24 |
+
"""Polish dialogue in the script.
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
inputs: Dictionary with keys:
|
| 28 |
+
- first_draft_script: From Lead Writer
|
| 29 |
+
- character_voice_guide: Character voice definitions
|
| 30 |
+
- dialect_slang_reference: Reference material
|
| 31 |
+
|
| 32 |
+
Returns:
|
| 33 |
+
Dictionary with polished script and voice notes
|
| 34 |
+
"""
|
| 35 |
+
outputs = self.process(inputs)
|
| 36 |
+
|
| 37 |
+
# Save state
|
| 38 |
+
self.save_state(
|
| 39 |
+
{
|
| 40 |
+
"inputs": inputs,
|
| 41 |
+
"outputs": outputs,
|
| 42 |
+
},
|
| 43 |
+
filename="polished_dialogue.json",
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
return outputs
|
agents/lead_writer.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Lead Writer agent - Script creation specialist."""
|
| 2 |
+
from typing import Any, Dict
|
| 3 |
+
from base_agent import BaseAgent
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class LeadWriterAgent(BaseAgent):
|
| 7 |
+
"""Lead Writer agent that crafts the first draft script."""
|
| 8 |
+
|
| 9 |
+
def __init__(self):
|
| 10 |
+
"""Initialize the Lead Writer agent."""
|
| 11 |
+
system_prompt = (
|
| 12 |
+
"You are the Lead Writer. Using the outline and cultural notes, write a complete "
|
| 13 |
+
"episode script. Each character must speak in their own voice. Include scene headings, "
|
| 14 |
+
"action lines, and dialogue. Format it like a proper script. Output as JSON with keys: "
|
| 15 |
+
"full_episode_first_draft, scene_descriptions, dialogue, stage_directions."
|
| 16 |
+
)
|
| 17 |
+
super().__init__(
|
| 18 |
+
agent_id="lead-writer",
|
| 19 |
+
agent_name="Lead Writer",
|
| 20 |
+
system_prompt=system_prompt,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
def write_script(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
| 24 |
+
"""Write episode script from outline and cultural notes.
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
inputs: Dictionary with keys:
|
| 28 |
+
- approved_outline: From Story Editor
|
| 29 |
+
- cultural_consultant_notes: From Cultural Consultant
|
| 30 |
+
- character_voice_guide: Character definitions
|
| 31 |
+
|
| 32 |
+
Returns:
|
| 33 |
+
Dictionary with generated script and related outputs
|
| 34 |
+
"""
|
| 35 |
+
outputs = self.process(inputs)
|
| 36 |
+
|
| 37 |
+
# Save state
|
| 38 |
+
self.save_state(
|
| 39 |
+
{
|
| 40 |
+
"inputs": inputs,
|
| 41 |
+
"outputs": outputs,
|
| 42 |
+
},
|
| 43 |
+
filename="first_draft.json",
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
return outputs
|
agents/proofreader.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Proofreader agent - Quality control specialist."""
|
| 2 |
+
from typing import Any, Dict
|
| 3 |
+
from base_agent import BaseAgent
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class ProofreaderAgent(BaseAgent):
|
| 7 |
+
"""Proofreader agent that performs final quality control."""
|
| 8 |
+
|
| 9 |
+
def __init__(self):
|
| 10 |
+
"""Initialize the Proofreader agent."""
|
| 11 |
+
system_prompt = (
|
| 12 |
+
"You are the Script Proofreader. Check for grammar, formatting, continuity errors, "
|
| 13 |
+
"character name consistency, and style guide violations. Return the corrected final "
|
| 14 |
+
"script and a brief QC report as JSON with keys: final_locked_script, qc_report, "
|
| 15 |
+
"continuity_log_update."
|
| 16 |
+
)
|
| 17 |
+
super().__init__(
|
| 18 |
+
agent_id="proofreader",
|
| 19 |
+
agent_name="Proofreader",
|
| 20 |
+
system_prompt=system_prompt,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
def final_qc(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
| 24 |
+
"""Perform final quality control on the script.
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
inputs: Dictionary with keys:
|
| 28 |
+
- comedy_sharpened_script: From Comedy Writer
|
| 29 |
+
- style_guide: Style reference
|
| 30 |
+
- continuity_log: Continuity tracking
|
| 31 |
+
|
| 32 |
+
Returns:
|
| 33 |
+
Dictionary with final script and QC report
|
| 34 |
+
"""
|
| 35 |
+
outputs = self.process(inputs)
|
| 36 |
+
|
| 37 |
+
# Save state
|
| 38 |
+
self.save_state(
|
| 39 |
+
{
|
| 40 |
+
"inputs": inputs,
|
| 41 |
+
"outputs": outputs,
|
| 42 |
+
},
|
| 43 |
+
filename="final_qc.json",
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
return outputs
|
agents/showrunner.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Showrunner agent - Orchestrator for content generation."""
|
| 2 |
+
from typing import Any, Dict
|
| 3 |
+
from base_agent import BaseAgent
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class ShowrunnerAgent(BaseAgent):
|
| 7 |
+
"""Orchestrator agent that translates briefs into actionable directives."""
|
| 8 |
+
|
| 9 |
+
def __init__(self):
|
| 10 |
+
"""Initialize the Showrunner agent."""
|
| 11 |
+
system_prompt = (
|
| 12 |
+
"You are the Showrunner. Given a brief, produce a tight episode directive: "
|
| 13 |
+
"premise, tone, which characters are featured, and the emotional core. "
|
| 14 |
+
"Keep it under 200 words. Output only the directive as a JSON object with keys: "
|
| 15 |
+
"episode_directive, story_premise, tone_brief, character_focus_notes."
|
| 16 |
+
)
|
| 17 |
+
super().__init__(
|
| 18 |
+
agent_id="showrunner",
|
| 19 |
+
agent_name="Showrunner",
|
| 20 |
+
system_prompt=system_prompt,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
def generate_directive(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
| 24 |
+
"""Generate episode directive from user brief.
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
inputs: Dictionary with keys:
|
| 28 |
+
- user_brief: The initial brief
|
| 29 |
+
- season_arc_document: Season context
|
| 30 |
+
- character_bible: Character definitions
|
| 31 |
+
|
| 32 |
+
Returns:
|
| 33 |
+
Dictionary with generated directive and related outputs
|
| 34 |
+
"""
|
| 35 |
+
outputs = self.process(inputs)
|
| 36 |
+
|
| 37 |
+
# Save state
|
| 38 |
+
self.save_state(
|
| 39 |
+
{
|
| 40 |
+
"inputs": inputs,
|
| 41 |
+
"outputs": outputs,
|
| 42 |
+
},
|
| 43 |
+
filename="directive.json",
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
return outputs
|
agents/story_editor.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Story Editor agent - Structure specialist for content."""
|
| 2 |
+
from typing import Any, Dict
|
| 3 |
+
from base_agent import BaseAgent
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class StoryEditorAgent(BaseAgent):
|
| 7 |
+
"""Story Editor agent that develops structural outlines."""
|
| 8 |
+
|
| 9 |
+
def __init__(self):
|
| 10 |
+
"""Initialize the Story Editor agent."""
|
| 11 |
+
system_prompt = (
|
| 12 |
+
"You are the Story Editor. Given a showrunner directive, produce a structured "
|
| 13 |
+
"episode outline with clear act breaks, a hook, rising tension, and a satisfying end. "
|
| 14 |
+
"Flag any continuity risks. Output the outline as a JSON object with keys: "
|
| 15 |
+
"episode_outline, act_structure, story_notes_for_writers."
|
| 16 |
+
)
|
| 17 |
+
super().__init__(
|
| 18 |
+
agent_id="story-editor",
|
| 19 |
+
agent_name="Story Editor",
|
| 20 |
+
system_prompt=system_prompt,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
def generate_outline(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
| 24 |
+
"""Generate episode outline from showrunner directive.
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
inputs: Dictionary with keys:
|
| 28 |
+
- episode_directive: From Showrunner
|
| 29 |
+
- series_continuity_log: Continuity reference
|
| 30 |
+
|
| 31 |
+
Returns:
|
| 32 |
+
Dictionary with generated outline and related outputs
|
| 33 |
+
"""
|
| 34 |
+
outputs = self.process(inputs)
|
| 35 |
+
|
| 36 |
+
# Save state
|
| 37 |
+
self.save_state(
|
| 38 |
+
{
|
| 39 |
+
"inputs": inputs,
|
| 40 |
+
"outputs": outputs,
|
| 41 |
+
},
|
| 42 |
+
filename="outline.json",
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
return outputs
|
app.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Hugging Face Spaces wrapper for the multi-agent system."""
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
# Set environment variables from HF Secrets
|
| 7 |
+
os.environ["OPENROUTER_API_KEY"] = os.getenv("OPENROUTER_API_KEY", "")
|
| 8 |
+
os.environ["HUGGINGFACE_TOKEN"] = os.getenv("HUGGINGFACE_TOKEN", "")
|
| 9 |
+
os.environ["HUGGINGFACE_DATASET"] = os.getenv("HUGGINGFACE_DATASET", "factorstudios/Pipeline")
|
| 10 |
+
os.environ["OPENROUTER_BASE_URL"] = os.getenv("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1")
|
| 11 |
+
os.environ["MODEL_NAME"] = os.getenv("MODEL_NAME", "poolside/laguna-m.1:free")
|
| 12 |
+
|
| 13 |
+
# Import and run the FastAPI app
|
| 14 |
+
from main import app
|
| 15 |
+
|
| 16 |
+
if __name__ == "__main__":
|
| 17 |
+
import uvicorn
|
| 18 |
+
|
| 19 |
+
# Get port from environment or use default
|
| 20 |
+
port = int(os.getenv("PORT", 7860))
|
| 21 |
+
|
| 22 |
+
# Run the server
|
| 23 |
+
uvicorn.run(
|
| 24 |
+
app,
|
| 25 |
+
host="0.0.0.0",
|
| 26 |
+
port=port,
|
| 27 |
+
log_level="info",
|
| 28 |
+
)
|
base_agent.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Base agent module for all specialized agents."""
|
| 2 |
+
import json
|
| 3 |
+
import logging
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any, Dict, Optional
|
| 7 |
+
from openai import OpenAI
|
| 8 |
+
from config import settings
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class BaseAgent:
|
| 15 |
+
"""Base class for all agents in the system."""
|
| 16 |
+
|
| 17 |
+
def __init__(self, agent_id: str, agent_name: str, system_prompt: str):
|
| 18 |
+
"""Initialize the base agent.
|
| 19 |
+
|
| 20 |
+
Args:
|
| 21 |
+
agent_id: Unique identifier for the agent
|
| 22 |
+
agent_name: Human-readable name of the agent
|
| 23 |
+
system_prompt: System prompt for the agent's behavior
|
| 24 |
+
"""
|
| 25 |
+
self.agent_id = agent_id
|
| 26 |
+
self.agent_name = agent_name
|
| 27 |
+
self.system_prompt = system_prompt
|
| 28 |
+
|
| 29 |
+
# Initialize OpenRouter client
|
| 30 |
+
self.client = OpenAI(
|
| 31 |
+
api_key=settings.openrouter_api_key,
|
| 32 |
+
base_url=settings.openrouter_base_url,
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
# Data storage
|
| 36 |
+
self.data_dir = Path(settings.data_dir) / agent_id
|
| 37 |
+
self.data_dir.mkdir(parents=True, exist_ok=True)
|
| 38 |
+
|
| 39 |
+
logger.info(f"Initialized agent: {agent_name} ({agent_id})")
|
| 40 |
+
|
| 41 |
+
def process(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
| 42 |
+
"""Process inputs and generate outputs using the LLM.
|
| 43 |
+
|
| 44 |
+
Args:
|
| 45 |
+
inputs: Dictionary containing input data for the agent
|
| 46 |
+
|
| 47 |
+
Returns:
|
| 48 |
+
Dictionary containing the agent's outputs
|
| 49 |
+
"""
|
| 50 |
+
# Prepare the prompt
|
| 51 |
+
input_text = json.dumps(inputs, indent=2)
|
| 52 |
+
|
| 53 |
+
logger.info(f"{self.agent_name}: Processing inputs")
|
| 54 |
+
|
| 55 |
+
try:
|
| 56 |
+
# Call OpenRouter API via OpenAI client
|
| 57 |
+
response = self.client.chat.completions.create(
|
| 58 |
+
model=settings.model_name,
|
| 59 |
+
messages=[
|
| 60 |
+
{
|
| 61 |
+
"role": "system",
|
| 62 |
+
"content": self.system_prompt,
|
| 63 |
+
},
|
| 64 |
+
{
|
| 65 |
+
"role": "user",
|
| 66 |
+
"content": f"Process the following inputs:\n\n{input_text}",
|
| 67 |
+
},
|
| 68 |
+
],
|
| 69 |
+
temperature=0.7,
|
| 70 |
+
max_tokens=4096,
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
output_text = response.choices[0].message.content
|
| 74 |
+
|
| 75 |
+
# Parse output (assuming JSON format)
|
| 76 |
+
try:
|
| 77 |
+
output_data = json.loads(output_text)
|
| 78 |
+
except json.JSONDecodeError:
|
| 79 |
+
# If not JSON, wrap in a generic output
|
| 80 |
+
output_data = {"output": output_text}
|
| 81 |
+
|
| 82 |
+
logger.info(f"{self.agent_name}: Processing completed")
|
| 83 |
+
|
| 84 |
+
return output_data
|
| 85 |
+
|
| 86 |
+
except Exception as e:
|
| 87 |
+
logger.error(f"{self.agent_name}: Error during processing - {str(e)}")
|
| 88 |
+
raise
|
| 89 |
+
|
| 90 |
+
def save_state(self, data: Dict[str, Any], filename: Optional[str] = None) -> str:
|
| 91 |
+
"""Save agent state and data to persistent JSON storage.
|
| 92 |
+
|
| 93 |
+
Args:
|
| 94 |
+
data: Data to save
|
| 95 |
+
filename: Optional custom filename (defaults to timestamp)
|
| 96 |
+
|
| 97 |
+
Returns:
|
| 98 |
+
Path to saved file
|
| 99 |
+
"""
|
| 100 |
+
if filename is None:
|
| 101 |
+
timestamp = datetime.now().isoformat().replace(":", "-")
|
| 102 |
+
filename = f"{self.agent_id}_{timestamp}.json"
|
| 103 |
+
|
| 104 |
+
filepath = self.data_dir / filename
|
| 105 |
+
|
| 106 |
+
# Add metadata
|
| 107 |
+
state_data = {
|
| 108 |
+
"agent_id": self.agent_id,
|
| 109 |
+
"agent_name": self.agent_name,
|
| 110 |
+
"timestamp": datetime.now().isoformat(),
|
| 111 |
+
"data": data,
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
with open(filepath, "w") as f:
|
| 115 |
+
json.dump(state_data, f, indent=2)
|
| 116 |
+
|
| 117 |
+
logger.info(f"{self.agent_name}: State saved to {filepath}")
|
| 118 |
+
|
| 119 |
+
return str(filepath)
|
| 120 |
+
|
| 121 |
+
def load_state(self, filename: str) -> Dict[str, Any]:
|
| 122 |
+
"""Load agent state from persistent storage.
|
| 123 |
+
|
| 124 |
+
Args:
|
| 125 |
+
filename: Filename to load
|
| 126 |
+
|
| 127 |
+
Returns:
|
| 128 |
+
Loaded data
|
| 129 |
+
"""
|
| 130 |
+
filepath = self.data_dir / filename
|
| 131 |
+
|
| 132 |
+
with open(filepath, "r") as f:
|
| 133 |
+
state_data = json.load(f)
|
| 134 |
+
|
| 135 |
+
logger.info(f"{self.agent_name}: State loaded from {filepath}")
|
| 136 |
+
|
| 137 |
+
return state_data.get("data", {})
|
config.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Configuration module for the multi-agent system."""
|
| 2 |
+
import os
|
| 3 |
+
import logging
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from pydantic_settings import BaseSettings
|
| 6 |
+
from env_loader import EnvironmentLoader, validate_on_startup
|
| 7 |
+
|
| 8 |
+
logger = logging.getLogger(__name__)
|
| 9 |
+
|
| 10 |
+
# Validate secrets on startup
|
| 11 |
+
validate_on_startup()
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class Settings(BaseSettings):
|
| 15 |
+
"""Application settings loaded from environment variables."""
|
| 16 |
+
|
| 17 |
+
# OpenRouter Configuration
|
| 18 |
+
openrouter_api_key: str = ""
|
| 19 |
+
openrouter_base_url: str = "https://openrouter.ai/api/v1"
|
| 20 |
+
model_name: str = "poolside/laguna-m.1:free"
|
| 21 |
+
|
| 22 |
+
# Hugging Face Configuration
|
| 23 |
+
huggingface_token: str = ""
|
| 24 |
+
huggingface_dataset: str = "factorstudios/Pipeline"
|
| 25 |
+
huggingface_api_url: str = "https://huggingface.co/api"
|
| 26 |
+
|
| 27 |
+
# System Configuration
|
| 28 |
+
data_dir: str = "./data"
|
| 29 |
+
output_dir: str = "./output"
|
| 30 |
+
log_dir: str = "./logs"
|
| 31 |
+
port: int = 8000
|
| 32 |
+
host: str = "0.0.0.0"
|
| 33 |
+
|
| 34 |
+
# Agent Ports
|
| 35 |
+
showrunner_port: int = 8001
|
| 36 |
+
story_editor_port: int = 8002
|
| 37 |
+
cultural_consultant_port: int = 8003
|
| 38 |
+
lead_writer_port: int = 8004
|
| 39 |
+
dialogue_specialist_port: int = 8005
|
| 40 |
+
comedy_writer_port: int = 8006
|
| 41 |
+
proofreader_port: int = 8007
|
| 42 |
+
|
| 43 |
+
# Logging
|
| 44 |
+
log_level: str = "INFO"
|
| 45 |
+
|
| 46 |
+
class Config:
|
| 47 |
+
env_file = ".env"
|
| 48 |
+
case_sensitive = False
|
| 49 |
+
extra = "allow" # Allow extra fields
|
| 50 |
+
|
| 51 |
+
def __init__(self, **data):
|
| 52 |
+
# Load from environment with fallbacks
|
| 53 |
+
if not data.get("openrouter_api_key"):
|
| 54 |
+
data["openrouter_api_key"] = os.getenv("OPENROUTER_API_KEY", "")
|
| 55 |
+
if not data.get("huggingface_token"):
|
| 56 |
+
data["huggingface_token"] = os.getenv("HUGGINGFACE_TOKEN", "")
|
| 57 |
+
if not data.get("huggingface_dataset"):
|
| 58 |
+
data["huggingface_dataset"] = os.getenv(
|
| 59 |
+
"HUGGINGFACE_DATASET", "factorstudios/Pipeline"
|
| 60 |
+
)
|
| 61 |
+
if not data.get("openrouter_base_url"):
|
| 62 |
+
data["openrouter_base_url"] = os.getenv(
|
| 63 |
+
"OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1"
|
| 64 |
+
)
|
| 65 |
+
if not data.get("model_name"):
|
| 66 |
+
data["model_name"] = os.getenv(
|
| 67 |
+
"MODEL_NAME", "poolside/laguna-m.1:free"
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
super().__init__(**data)
|
| 71 |
+
# Create necessary directories
|
| 72 |
+
Path(self.data_dir).mkdir(parents=True, exist_ok=True)
|
| 73 |
+
Path(self.output_dir).mkdir(parents=True, exist_ok=True)
|
| 74 |
+
Path(self.log_dir).mkdir(parents=True, exist_ok=True)
|
| 75 |
+
|
| 76 |
+
logger.info("Configuration loaded successfully")
|
| 77 |
+
logger.info(f"Model: {self.model_name}")
|
| 78 |
+
logger.info(f"Dataset: {self.huggingface_dataset}")
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
# Load settings
|
| 82 |
+
settings = Settings()
|
env_loader.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Environment variable loader with HF Secrets support."""
|
| 2 |
+
import os
|
| 3 |
+
import logging
|
| 4 |
+
from typing import Optional
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
logger = logging.getLogger(__name__)
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class EnvironmentLoader:
|
| 11 |
+
"""Loads environment variables with support for HF Secrets."""
|
| 12 |
+
|
| 13 |
+
@staticmethod
|
| 14 |
+
def get_secret(key: str, default: Optional[str] = None) -> str:
|
| 15 |
+
"""Get environment variable or secret.
|
| 16 |
+
|
| 17 |
+
In Hugging Face Spaces, secrets are injected as environment variables.
|
| 18 |
+
This function provides a consistent interface for accessing them.
|
| 19 |
+
|
| 20 |
+
Args:
|
| 21 |
+
key: Environment variable name
|
| 22 |
+
default: Default value if not found
|
| 23 |
+
|
| 24 |
+
Returns:
|
| 25 |
+
Environment variable value or default
|
| 26 |
+
|
| 27 |
+
Raises:
|
| 28 |
+
ValueError: If required variable is missing and no default provided
|
| 29 |
+
"""
|
| 30 |
+
value = os.getenv(key, default)
|
| 31 |
+
|
| 32 |
+
if value is None:
|
| 33 |
+
raise ValueError(
|
| 34 |
+
f"Required environment variable '{key}' is not set. "
|
| 35 |
+
f"Please add it to HF Secrets in your Space settings."
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
if not value:
|
| 39 |
+
logger.warning(f"Environment variable '{key}' is empty")
|
| 40 |
+
|
| 41 |
+
return value
|
| 42 |
+
|
| 43 |
+
@staticmethod
|
| 44 |
+
def validate_secrets() -> bool:
|
| 45 |
+
"""Validate that all required secrets are configured.
|
| 46 |
+
|
| 47 |
+
Returns:
|
| 48 |
+
True if all required secrets are present, False otherwise
|
| 49 |
+
"""
|
| 50 |
+
required_secrets = [
|
| 51 |
+
"OPENROUTER_API_KEY",
|
| 52 |
+
"HUGGINGFACE_TOKEN",
|
| 53 |
+
]
|
| 54 |
+
|
| 55 |
+
optional_secrets = [
|
| 56 |
+
"HUGGINGFACE_DATASET",
|
| 57 |
+
"OPENROUTER_BASE_URL",
|
| 58 |
+
"MODEL_NAME",
|
| 59 |
+
]
|
| 60 |
+
|
| 61 |
+
all_valid = True
|
| 62 |
+
|
| 63 |
+
# Check required secrets
|
| 64 |
+
for secret in required_secrets:
|
| 65 |
+
value = os.getenv(secret)
|
| 66 |
+
if not value:
|
| 67 |
+
logger.error(f"Missing required secret: {secret}")
|
| 68 |
+
all_valid = False
|
| 69 |
+
else:
|
| 70 |
+
# Log masked value for debugging
|
| 71 |
+
masked = value[:10] + "..." if len(value) > 10 else "***"
|
| 72 |
+
logger.info(f"β {secret} configured ({masked})")
|
| 73 |
+
|
| 74 |
+
# Check optional secrets
|
| 75 |
+
for secret in optional_secrets:
|
| 76 |
+
value = os.getenv(secret)
|
| 77 |
+
if value:
|
| 78 |
+
masked = value[:10] + "..." if len(value) > 10 else "***"
|
| 79 |
+
logger.info(f"β {secret} configured ({masked})")
|
| 80 |
+
else:
|
| 81 |
+
logger.info(f"βΉ {secret} not configured (using default)")
|
| 82 |
+
|
| 83 |
+
return all_valid
|
| 84 |
+
|
| 85 |
+
@staticmethod
|
| 86 |
+
def get_api_config() -> dict:
|
| 87 |
+
"""Get API configuration from environment.
|
| 88 |
+
|
| 89 |
+
Returns:
|
| 90 |
+
Dictionary with API configuration
|
| 91 |
+
"""
|
| 92 |
+
return {
|
| 93 |
+
"openrouter_api_key": EnvironmentLoader.get_secret("OPENROUTER_API_KEY"),
|
| 94 |
+
"openrouter_base_url": os.getenv(
|
| 95 |
+
"OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1"
|
| 96 |
+
),
|
| 97 |
+
"model_name": os.getenv("MODEL_NAME", "poolside/laguna-m.1:free"),
|
| 98 |
+
"huggingface_token": EnvironmentLoader.get_secret("HUGGINGFACE_TOKEN"),
|
| 99 |
+
"huggingface_dataset": os.getenv(
|
| 100 |
+
"HUGGINGFACE_DATASET", "factorstudios/Pipeline"
|
| 101 |
+
),
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
# Validate secrets on import
|
| 106 |
+
def validate_on_startup():
|
| 107 |
+
"""Validate secrets when module is imported."""
|
| 108 |
+
if not EnvironmentLoader.validate_secrets():
|
| 109 |
+
logger.warning(
|
| 110 |
+
"Some secrets are not configured. "
|
| 111 |
+
"The application may not work correctly. "
|
| 112 |
+
"Please add missing secrets to HF Secrets in your Space settings."
|
| 113 |
+
)
|
hf_uploader.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Hugging Face dataset uploader for final outputs."""
|
| 2 |
+
import json
|
| 3 |
+
import logging
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
from huggingface_hub import HfApi, CommitOperationAdd
|
| 7 |
+
from config import settings
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class HFUploader:
|
| 14 |
+
"""Handles uploading final outputs to Hugging Face dataset."""
|
| 15 |
+
|
| 16 |
+
def __init__(self):
|
| 17 |
+
"""Initialize the Hugging Face uploader."""
|
| 18 |
+
self.api = HfApi()
|
| 19 |
+
self.token = settings.huggingface_token
|
| 20 |
+
self.dataset_id = settings.huggingface_dataset
|
| 21 |
+
logger.info(f"Initialized HF uploader for dataset: {self.dataset_id}")
|
| 22 |
+
|
| 23 |
+
def upload_final_output(self, final_data: dict, run_id: str) -> str:
|
| 24 |
+
"""Upload final output to Hugging Face dataset.
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
final_data: The final processed data from the pipeline
|
| 28 |
+
run_id: Unique identifier for this pipeline run
|
| 29 |
+
|
| 30 |
+
Returns:
|
| 31 |
+
URL of the uploaded file
|
| 32 |
+
"""
|
| 33 |
+
try:
|
| 34 |
+
# Prepare the data
|
| 35 |
+
upload_data = {
|
| 36 |
+
"run_id": run_id,
|
| 37 |
+
"timestamp": datetime.now().isoformat(),
|
| 38 |
+
"final_output": final_data,
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
# Create filename
|
| 42 |
+
filename = f"output_{run_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
| 43 |
+
|
| 44 |
+
# Convert to JSON string
|
| 45 |
+
json_content = json.dumps(upload_data, indent=2)
|
| 46 |
+
|
| 47 |
+
# Create commit operation
|
| 48 |
+
commit_operation = CommitOperationAdd(
|
| 49 |
+
path_in_repo=f"outputs/{filename}",
|
| 50 |
+
path_or_fileobj=json_content.encode("utf-8"),
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
# Upload to dataset
|
| 54 |
+
commit_info = self.api.create_commit(
|
| 55 |
+
repo_id=self.dataset_id,
|
| 56 |
+
repo_type="dataset",
|
| 57 |
+
operations=[commit_operation],
|
| 58 |
+
commit_message=f"Pipeline output: {run_id}",
|
| 59 |
+
token=self.token,
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
file_url = f"https://huggingface.co/datasets/{self.dataset_id}/blob/main/outputs/{filename}"
|
| 63 |
+
logger.info(f"Successfully uploaded to HF: {file_url}")
|
| 64 |
+
|
| 65 |
+
return file_url
|
| 66 |
+
|
| 67 |
+
except Exception as e:
|
| 68 |
+
logger.error(f"Error uploading to Hugging Face: {str(e)}")
|
| 69 |
+
raise
|
| 70 |
+
|
| 71 |
+
def upload_pipeline_metadata(self, metadata: dict) -> str:
|
| 72 |
+
"""Upload pipeline metadata to Hugging Face dataset.
|
| 73 |
+
|
| 74 |
+
Args:
|
| 75 |
+
metadata: Pipeline metadata including all agent outputs
|
| 76 |
+
|
| 77 |
+
Returns:
|
| 78 |
+
URL of the uploaded metadata file
|
| 79 |
+
"""
|
| 80 |
+
try:
|
| 81 |
+
# Create filename
|
| 82 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 83 |
+
filename = f"metadata_{timestamp}.json"
|
| 84 |
+
|
| 85 |
+
# Convert to JSON string
|
| 86 |
+
json_content = json.dumps(metadata, indent=2)
|
| 87 |
+
|
| 88 |
+
# Create commit operation
|
| 89 |
+
commit_operation = CommitOperationAdd(
|
| 90 |
+
path_in_repo=f"metadata/{filename}",
|
| 91 |
+
path_or_fileobj=json_content.encode("utf-8"),
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
# Upload to dataset
|
| 95 |
+
commit_info = self.api.create_commit(
|
| 96 |
+
repo_id=self.dataset_id,
|
| 97 |
+
repo_type="dataset",
|
| 98 |
+
operations=[commit_operation],
|
| 99 |
+
commit_message=f"Pipeline metadata: {timestamp}",
|
| 100 |
+
token=self.token,
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
file_url = f"https://huggingface.co/datasets/{self.dataset_id}/blob/main/metadata/{filename}"
|
| 104 |
+
logger.info(f"Successfully uploaded metadata to HF: {file_url}")
|
| 105 |
+
|
| 106 |
+
return file_url
|
| 107 |
+
|
| 108 |
+
except Exception as e:
|
| 109 |
+
logger.error(f"Error uploading metadata to Hugging Face: {str(e)}")
|
| 110 |
+
raise
|
main.py
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""FastAPI server for the multi-agent content generation system."""
|
| 2 |
+
import logging
|
| 3 |
+
from fastapi import FastAPI, HTTPException
|
| 4 |
+
from pydantic import BaseModel
|
| 5 |
+
from typing import Optional
|
| 6 |
+
from orchestrator import PipelineOrchestrator
|
| 7 |
+
from config import settings
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
# Configure logging
|
| 11 |
+
logging.basicConfig(
|
| 12 |
+
level=settings.log_level,
|
| 13 |
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
| 14 |
+
)
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
# Initialize FastAPI app
|
| 18 |
+
app = FastAPI(
|
| 19 |
+
title="Multi-Agent Content Generation System",
|
| 20 |
+
description="API-first system for collaborative content generation using poolside/laguna-m.1:free",
|
| 21 |
+
version="1.0.0",
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
# Initialize orchestrator
|
| 25 |
+
orchestrator = PipelineOrchestrator()
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
# Request/Response Models
|
| 29 |
+
class PipelineRequest(BaseModel):
|
| 30 |
+
"""Request model for pipeline execution."""
|
| 31 |
+
user_brief: str
|
| 32 |
+
season_arc_document: str
|
| 33 |
+
character_bible: str
|
| 34 |
+
world_building_document: str
|
| 35 |
+
character_voice_guide: str
|
| 36 |
+
style_guide: str
|
| 37 |
+
continuity_log: str
|
| 38 |
+
hook_brief: Optional[str] = None
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class PipelineResponse(BaseModel):
|
| 42 |
+
"""Response model for pipeline execution."""
|
| 43 |
+
run_id: str
|
| 44 |
+
status: str
|
| 45 |
+
final_output: dict
|
| 46 |
+
hf_output_url: str
|
| 47 |
+
hf_metadata_url: str
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class HealthResponse(BaseModel):
|
| 51 |
+
"""Health check response."""
|
| 52 |
+
status: str
|
| 53 |
+
run_id: str
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# Endpoints
|
| 57 |
+
@app.get("/health", response_model=HealthResponse)
|
| 58 |
+
async def health_check():
|
| 59 |
+
"""Health check endpoint."""
|
| 60 |
+
return {
|
| 61 |
+
"status": "healthy",
|
| 62 |
+
"run_id": orchestrator.run_id,
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
@app.post("/api/v1/pipeline/execute", response_model=PipelineResponse)
|
| 67 |
+
async def execute_pipeline(request: PipelineRequest):
|
| 68 |
+
"""Execute the full content generation pipeline.
|
| 69 |
+
|
| 70 |
+
Args:
|
| 71 |
+
request: Pipeline execution request with all required inputs
|
| 72 |
+
|
| 73 |
+
Returns:
|
| 74 |
+
Pipeline execution result with final output and URLs
|
| 75 |
+
"""
|
| 76 |
+
try:
|
| 77 |
+
logger.info(f"Received pipeline execution request: {request.user_brief[:50]}...")
|
| 78 |
+
|
| 79 |
+
result = orchestrator.execute_pipeline(
|
| 80 |
+
user_brief=request.user_brief,
|
| 81 |
+
season_arc_document=request.season_arc_document,
|
| 82 |
+
character_bible=request.character_bible,
|
| 83 |
+
world_building_document=request.world_building_document,
|
| 84 |
+
character_voice_guide=request.character_voice_guide,
|
| 85 |
+
style_guide=request.style_guide,
|
| 86 |
+
continuity_log=request.continuity_log,
|
| 87 |
+
hook_brief=request.hook_brief,
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
return {
|
| 91 |
+
"run_id": result["run_id"],
|
| 92 |
+
"status": result["status"],
|
| 93 |
+
"final_output": result["final_output"],
|
| 94 |
+
"hf_output_url": result["hf_output_url"],
|
| 95 |
+
"hf_metadata_url": result["hf_metadata_url"],
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
except Exception as e:
|
| 99 |
+
logger.error(f"Pipeline execution error: {str(e)}")
|
| 100 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
@app.post("/api/v1/showrunner/generate_directive")
|
| 104 |
+
async def generate_directive(request: dict):
|
| 105 |
+
"""Generate episode directive from user brief.
|
| 106 |
+
|
| 107 |
+
Args:
|
| 108 |
+
request: Dictionary with user_brief, season_arc_document, character_bible
|
| 109 |
+
|
| 110 |
+
Returns:
|
| 111 |
+
Generated directive
|
| 112 |
+
"""
|
| 113 |
+
try:
|
| 114 |
+
result = orchestrator.showrunner.generate_directive(request)
|
| 115 |
+
return result
|
| 116 |
+
except Exception as e:
|
| 117 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
@app.post("/api/v1/story_editor/generate_outline")
|
| 121 |
+
async def generate_outline(request: dict):
|
| 122 |
+
"""Generate episode outline from directive.
|
| 123 |
+
|
| 124 |
+
Args:
|
| 125 |
+
request: Dictionary with episode_directive, series_continuity_log
|
| 126 |
+
|
| 127 |
+
Returns:
|
| 128 |
+
Generated outline
|
| 129 |
+
"""
|
| 130 |
+
try:
|
| 131 |
+
result = orchestrator.story_editor.generate_outline(request)
|
| 132 |
+
return result
|
| 133 |
+
except Exception as e:
|
| 134 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
@app.post("/api/v1/cultural_consultant/review_outline")
|
| 138 |
+
async def review_outline(request: dict):
|
| 139 |
+
"""Review outline for cultural accuracy.
|
| 140 |
+
|
| 141 |
+
Args:
|
| 142 |
+
request: Dictionary with episode_outline, world_building_document
|
| 143 |
+
|
| 144 |
+
Returns:
|
| 145 |
+
Cultural review and recommendations
|
| 146 |
+
"""
|
| 147 |
+
try:
|
| 148 |
+
result = orchestrator.cultural_consultant.review_outline(request)
|
| 149 |
+
return result
|
| 150 |
+
except Exception as e:
|
| 151 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
@app.post("/api/v1/lead_writer/write_script")
|
| 155 |
+
async def write_script(request: dict):
|
| 156 |
+
"""Write episode script from outline and cultural notes.
|
| 157 |
+
|
| 158 |
+
Args:
|
| 159 |
+
request: Dictionary with approved_outline, cultural_consultant_notes, character_voice_guide
|
| 160 |
+
|
| 161 |
+
Returns:
|
| 162 |
+
Generated script
|
| 163 |
+
"""
|
| 164 |
+
try:
|
| 165 |
+
result = orchestrator.lead_writer.write_script(request)
|
| 166 |
+
return result
|
| 167 |
+
except Exception as e:
|
| 168 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
@app.post("/api/v1/dialogue_specialist/polish_dialogue")
|
| 172 |
+
async def polish_dialogue(request: dict):
|
| 173 |
+
"""Polish dialogue in the script.
|
| 174 |
+
|
| 175 |
+
Args:
|
| 176 |
+
request: Dictionary with first_draft_script, character_voice_guide, dialect_slang_reference
|
| 177 |
+
|
| 178 |
+
Returns:
|
| 179 |
+
Polished script
|
| 180 |
+
"""
|
| 181 |
+
try:
|
| 182 |
+
result = orchestrator.dialogue_specialist.polish_dialogue(request)
|
| 183 |
+
return result
|
| 184 |
+
except Exception as e:
|
| 185 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
@app.post("/api/v1/comedy_writer/add_humor")
|
| 189 |
+
async def add_humor(request: dict):
|
| 190 |
+
"""Add humor and punch-ups to the script.
|
| 191 |
+
|
| 192 |
+
Args:
|
| 193 |
+
request: Dictionary with dialogue_polished_script, hook_brief_from_showrunner
|
| 194 |
+
|
| 195 |
+
Returns:
|
| 196 |
+
Comedy-enhanced script
|
| 197 |
+
"""
|
| 198 |
+
try:
|
| 199 |
+
result = orchestrator.comedy_writer.add_humor(request)
|
| 200 |
+
return result
|
| 201 |
+
except Exception as e:
|
| 202 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
@app.post("/api/v1/proofreader/final_qc")
|
| 206 |
+
async def final_qc(request: dict):
|
| 207 |
+
"""Perform final quality control on the script.
|
| 208 |
+
|
| 209 |
+
Args:
|
| 210 |
+
request: Dictionary with comedy_sharpened_script, style_guide, continuity_log
|
| 211 |
+
|
| 212 |
+
Returns:
|
| 213 |
+
Final QC report and locked script
|
| 214 |
+
"""
|
| 215 |
+
try:
|
| 216 |
+
result = orchestrator.proofreader.final_qc(request)
|
| 217 |
+
return result
|
| 218 |
+
except Exception as e:
|
| 219 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
@app.get("/api/v1/pipeline/status/{run_id}")
|
| 223 |
+
async def get_pipeline_status(run_id: str):
|
| 224 |
+
"""Get the status of a pipeline run.
|
| 225 |
+
|
| 226 |
+
Args:
|
| 227 |
+
run_id: The run ID to check
|
| 228 |
+
|
| 229 |
+
Returns:
|
| 230 |
+
Pipeline status and state
|
| 231 |
+
"""
|
| 232 |
+
try:
|
| 233 |
+
# In a real system, this would query a database
|
| 234 |
+
# For now, we return the current orchestrator state if it matches
|
| 235 |
+
if run_id == orchestrator.run_id:
|
| 236 |
+
return {
|
| 237 |
+
"run_id": run_id,
|
| 238 |
+
"status": orchestrator.pipeline_state.get("status", "unknown"),
|
| 239 |
+
"pipeline_state": orchestrator.pipeline_state,
|
| 240 |
+
}
|
| 241 |
+
else:
|
| 242 |
+
raise HTTPException(status_code=404, detail="Run ID not found")
|
| 243 |
+
except Exception as e:
|
| 244 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
if __name__ == "__main__":
|
| 248 |
+
import uvicorn
|
| 249 |
+
|
| 250 |
+
uvicorn.run(
|
| 251 |
+
app,
|
| 252 |
+
host=settings.host,
|
| 253 |
+
port=settings.port,
|
| 254 |
+
log_level=settings.log_level.lower(),
|
| 255 |
+
)
|
orchestrator.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pipeline orchestrator that manages agent flow and data passing."""
|
| 2 |
+
import json
|
| 3 |
+
import logging
|
| 4 |
+
import uuid
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Any, Dict, Optional
|
| 8 |
+
from agents import (
|
| 9 |
+
ShowrunnerAgent,
|
| 10 |
+
StoryEditorAgent,
|
| 11 |
+
CulturalConsultantAgent,
|
| 12 |
+
LeadWriterAgent,
|
| 13 |
+
DialogueSpecialistAgent,
|
| 14 |
+
ComedyWriterAgent,
|
| 15 |
+
ProofreaderAgent,
|
| 16 |
+
)
|
| 17 |
+
from hf_uploader import HFUploader
|
| 18 |
+
from config import settings
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
logger = logging.getLogger(__name__)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class PipelineOrchestrator:
|
| 25 |
+
"""Orchestrates the multi-agent content generation pipeline."""
|
| 26 |
+
|
| 27 |
+
def __init__(self):
|
| 28 |
+
"""Initialize the orchestrator with all agents."""
|
| 29 |
+
self.showrunner = ShowrunnerAgent()
|
| 30 |
+
self.story_editor = StoryEditorAgent()
|
| 31 |
+
self.cultural_consultant = CulturalConsultantAgent()
|
| 32 |
+
self.lead_writer = LeadWriterAgent()
|
| 33 |
+
self.dialogue_specialist = DialogueSpecialistAgent()
|
| 34 |
+
self.comedy_writer = ComedyWriterAgent()
|
| 35 |
+
self.proofreader = ProofreaderAgent()
|
| 36 |
+
self.hf_uploader = HFUploader()
|
| 37 |
+
|
| 38 |
+
# Pipeline state
|
| 39 |
+
self.run_id = str(uuid.uuid4())
|
| 40 |
+
self.pipeline_state = {
|
| 41 |
+
"run_id": self.run_id,
|
| 42 |
+
"start_time": datetime.now().isoformat(),
|
| 43 |
+
"stages": {},
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
logger.info(f"Initialized pipeline orchestrator with run_id: {self.run_id}")
|
| 47 |
+
|
| 48 |
+
def execute_pipeline(
|
| 49 |
+
self,
|
| 50 |
+
user_brief: str,
|
| 51 |
+
season_arc_document: str,
|
| 52 |
+
character_bible: str,
|
| 53 |
+
world_building_document: str,
|
| 54 |
+
character_voice_guide: str,
|
| 55 |
+
style_guide: str,
|
| 56 |
+
continuity_log: str,
|
| 57 |
+
hook_brief: Optional[str] = None,
|
| 58 |
+
) -> Dict[str, Any]:
|
| 59 |
+
"""Execute the full content generation pipeline.
|
| 60 |
+
|
| 61 |
+
Args:
|
| 62 |
+
user_brief: Initial user brief
|
| 63 |
+
season_arc_document: Season context
|
| 64 |
+
character_bible: Character definitions
|
| 65 |
+
world_building_document: World context
|
| 66 |
+
character_voice_guide: Character voice definitions
|
| 67 |
+
style_guide: Style reference
|
| 68 |
+
continuity_log: Continuity tracking
|
| 69 |
+
hook_brief: Optional hook brief for comedy writer
|
| 70 |
+
|
| 71 |
+
Returns:
|
| 72 |
+
Dictionary with final output and metadata
|
| 73 |
+
"""
|
| 74 |
+
try:
|
| 75 |
+
logger.info("Starting pipeline execution")
|
| 76 |
+
|
| 77 |
+
# Stage 1: Showrunner
|
| 78 |
+
logger.info("Stage 1: Showrunner - Generating directive")
|
| 79 |
+
showrunner_inputs = {
|
| 80 |
+
"user_brief": user_brief,
|
| 81 |
+
"season_arc_document": season_arc_document,
|
| 82 |
+
"character_bible": character_bible,
|
| 83 |
+
}
|
| 84 |
+
showrunner_output = self.showrunner.generate_directive(showrunner_inputs)
|
| 85 |
+
self.pipeline_state["stages"]["showrunner"] = showrunner_output
|
| 86 |
+
logger.info("Stage 1 completed")
|
| 87 |
+
|
| 88 |
+
# Stage 2: Story Editor
|
| 89 |
+
logger.info("Stage 2: Story Editor - Generating outline")
|
| 90 |
+
story_editor_inputs = {
|
| 91 |
+
"episode_directive": showrunner_output.get(
|
| 92 |
+
"episode_directive", ""
|
| 93 |
+
),
|
| 94 |
+
"series_continuity_log": continuity_log,
|
| 95 |
+
}
|
| 96 |
+
story_editor_output = self.story_editor.generate_outline(
|
| 97 |
+
story_editor_inputs
|
| 98 |
+
)
|
| 99 |
+
self.pipeline_state["stages"]["story_editor"] = story_editor_output
|
| 100 |
+
logger.info("Stage 2 completed")
|
| 101 |
+
|
| 102 |
+
# Stage 3: Cultural Consultant (parallel with Lead Writer)
|
| 103 |
+
logger.info("Stage 3: Cultural Consultant - Reviewing outline")
|
| 104 |
+
cultural_inputs = {
|
| 105 |
+
"episode_outline": story_editor_output.get("episode_outline", ""),
|
| 106 |
+
"world_building_document": world_building_document,
|
| 107 |
+
}
|
| 108 |
+
cultural_output = self.cultural_consultant.review_outline(cultural_inputs)
|
| 109 |
+
self.pipeline_state["stages"]["cultural_consultant"] = cultural_output
|
| 110 |
+
logger.info("Stage 3 completed")
|
| 111 |
+
|
| 112 |
+
# Stage 4: Lead Writer
|
| 113 |
+
logger.info("Stage 4: Lead Writer - Writing script")
|
| 114 |
+
lead_writer_inputs = {
|
| 115 |
+
"approved_outline": story_editor_output.get("episode_outline", ""),
|
| 116 |
+
"cultural_consultant_notes": cultural_output.get(
|
| 117 |
+
"cultural_accuracy_notes", ""
|
| 118 |
+
),
|
| 119 |
+
"character_voice_guide": character_voice_guide,
|
| 120 |
+
}
|
| 121 |
+
lead_writer_output = self.lead_writer.write_script(lead_writer_inputs)
|
| 122 |
+
self.pipeline_state["stages"]["lead_writer"] = lead_writer_output
|
| 123 |
+
logger.info("Stage 4 completed")
|
| 124 |
+
|
| 125 |
+
# Stage 5: Dialogue Specialist
|
| 126 |
+
logger.info("Stage 5: Dialogue Specialist - Polishing dialogue")
|
| 127 |
+
dialogue_inputs = {
|
| 128 |
+
"first_draft_script": lead_writer_output.get(
|
| 129 |
+
"full_episode_first_draft", ""
|
| 130 |
+
),
|
| 131 |
+
"character_voice_guide": character_voice_guide,
|
| 132 |
+
"dialect_slang_reference": "",
|
| 133 |
+
}
|
| 134 |
+
dialogue_output = self.dialogue_specialist.polish_dialogue(dialogue_inputs)
|
| 135 |
+
self.pipeline_state["stages"]["dialogue_specialist"] = dialogue_output
|
| 136 |
+
logger.info("Stage 5 completed")
|
| 137 |
+
|
| 138 |
+
# Stage 6: Comedy Writer
|
| 139 |
+
logger.info("Stage 6: Comedy Writer - Adding humor")
|
| 140 |
+
comedy_inputs = {
|
| 141 |
+
"dialogue_polished_script": dialogue_output.get(
|
| 142 |
+
"dialogue_polished_script", ""
|
| 143 |
+
),
|
| 144 |
+
"hook_brief_from_showrunner": hook_brief or user_brief,
|
| 145 |
+
}
|
| 146 |
+
comedy_output = self.comedy_writer.add_humor(comedy_inputs)
|
| 147 |
+
self.pipeline_state["stages"]["comedy_writer"] = comedy_output
|
| 148 |
+
logger.info("Stage 6 completed")
|
| 149 |
+
|
| 150 |
+
# Stage 7: Proofreader (Final QC)
|
| 151 |
+
logger.info("Stage 7: Proofreader - Final quality control")
|
| 152 |
+
proofreader_inputs = {
|
| 153 |
+
"comedy_sharpened_script": comedy_output.get(
|
| 154 |
+
"comedy_sharpened_script", ""
|
| 155 |
+
),
|
| 156 |
+
"style_guide": style_guide,
|
| 157 |
+
"continuity_log": continuity_log,
|
| 158 |
+
}
|
| 159 |
+
proofreader_output = self.proofreader.final_qc(proofreader_inputs)
|
| 160 |
+
self.pipeline_state["stages"]["proofreader"] = proofreader_output
|
| 161 |
+
logger.info("Stage 7 completed")
|
| 162 |
+
|
| 163 |
+
# Mark completion
|
| 164 |
+
self.pipeline_state["end_time"] = datetime.now().isoformat()
|
| 165 |
+
self.pipeline_state["status"] = "completed"
|
| 166 |
+
|
| 167 |
+
# Save local state
|
| 168 |
+
self._save_pipeline_state()
|
| 169 |
+
|
| 170 |
+
# Upload to Hugging Face
|
| 171 |
+
logger.info("Uploading final output to Hugging Face")
|
| 172 |
+
hf_url = self.hf_uploader.upload_final_output(
|
| 173 |
+
proofreader_output, self.run_id
|
| 174 |
+
)
|
| 175 |
+
hf_metadata_url = self.hf_uploader.upload_pipeline_metadata(
|
| 176 |
+
self.pipeline_state
|
| 177 |
+
)
|
| 178 |
+
|
| 179 |
+
final_result = {
|
| 180 |
+
"run_id": self.run_id,
|
| 181 |
+
"status": "success",
|
| 182 |
+
"final_output": proofreader_output,
|
| 183 |
+
"hf_output_url": hf_url,
|
| 184 |
+
"hf_metadata_url": hf_metadata_url,
|
| 185 |
+
"pipeline_state": self.pipeline_state,
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
logger.info("Pipeline execution completed successfully")
|
| 189 |
+
return final_result
|
| 190 |
+
|
| 191 |
+
except Exception as e:
|
| 192 |
+
logger.error(f"Pipeline execution failed: {str(e)}")
|
| 193 |
+
self.pipeline_state["status"] = "failed"
|
| 194 |
+
self.pipeline_state["error"] = str(e)
|
| 195 |
+
self._save_pipeline_state()
|
| 196 |
+
raise
|
| 197 |
+
|
| 198 |
+
def _save_pipeline_state(self) -> None:
|
| 199 |
+
"""Save the pipeline state to local storage."""
|
| 200 |
+
output_dir = Path(settings.output_dir)
|
| 201 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 202 |
+
|
| 203 |
+
state_file = output_dir / f"pipeline_{self.run_id}.json"
|
| 204 |
+
with open(state_file, "w") as f:
|
| 205 |
+
json.dump(self.pipeline_state, f, indent=2)
|
| 206 |
+
|
| 207 |
+
logger.info(f"Pipeline state saved to {state_file}")
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.104.1
|
| 2 |
+
uvicorn==0.24.0
|
| 3 |
+
python-dotenv==1.0.0
|
| 4 |
+
requests==2.31.0
|
| 5 |
+
pydantic==2.5.0
|
| 6 |
+
pydantic-settings==2.1.0
|
| 7 |
+
openai==1.3.0
|
| 8 |
+
huggingface-hub==0.19.4
|
| 9 |
+
aiofiles==23.2.1
|
| 10 |
+
httpx==0.25.1
|
setup_space.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Setup script for Hugging Face Spaces deployment."""
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
import logging
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
logging.basicConfig(
|
| 9 |
+
level=logging.INFO,
|
| 10 |
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
| 11 |
+
)
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def check_environment():
|
| 16 |
+
"""Check if running in Hugging Face Space environment."""
|
| 17 |
+
is_hf_space = os.getenv("SPACE_ID") is not None
|
| 18 |
+
return is_hf_space
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def validate_secrets():
|
| 22 |
+
"""Validate that required secrets are configured."""
|
| 23 |
+
required_secrets = [
|
| 24 |
+
"OPENROUTER_API_KEY",
|
| 25 |
+
"HUGGINGFACE_TOKEN",
|
| 26 |
+
]
|
| 27 |
+
|
| 28 |
+
optional_secrets = [
|
| 29 |
+
"HUGGINGFACE_DATASET",
|
| 30 |
+
"OPENROUTER_BASE_URL",
|
| 31 |
+
"MODEL_NAME",
|
| 32 |
+
]
|
| 33 |
+
|
| 34 |
+
logger.info("=" * 60)
|
| 35 |
+
logger.info("Validating Hugging Face Secrets")
|
| 36 |
+
logger.info("=" * 60)
|
| 37 |
+
|
| 38 |
+
missing_secrets = []
|
| 39 |
+
|
| 40 |
+
# Check required secrets
|
| 41 |
+
for secret in required_secrets:
|
| 42 |
+
value = os.getenv(secret)
|
| 43 |
+
if not value:
|
| 44 |
+
logger.error(f"β MISSING: {secret}")
|
| 45 |
+
missing_secrets.append(secret)
|
| 46 |
+
else:
|
| 47 |
+
masked = value[:10] + "..." if len(value) > 10 else "***"
|
| 48 |
+
logger.info(f"β CONFIGURED: {secret} ({masked})")
|
| 49 |
+
|
| 50 |
+
# Check optional secrets
|
| 51 |
+
for secret in optional_secrets:
|
| 52 |
+
value = os.getenv(secret)
|
| 53 |
+
if value:
|
| 54 |
+
masked = value[:10] + "..." if len(value) > 10 else "***"
|
| 55 |
+
logger.info(f"β CONFIGURED: {secret} ({masked})")
|
| 56 |
+
else:
|
| 57 |
+
logger.info(f"βΉ NOT SET: {secret} (using default)")
|
| 58 |
+
|
| 59 |
+
logger.info("=" * 60)
|
| 60 |
+
|
| 61 |
+
if missing_secrets:
|
| 62 |
+
logger.error(f"\nβ Missing required secrets: {', '.join(missing_secrets)}")
|
| 63 |
+
logger.error("\nPlease add these secrets to your Space:")
|
| 64 |
+
logger.error("1. Go to your Space settings")
|
| 65 |
+
logger.error("2. Click 'Repository secrets'")
|
| 66 |
+
logger.error("3. Add each missing secret")
|
| 67 |
+
return False
|
| 68 |
+
|
| 69 |
+
logger.info("\nβ
All required secrets are configured!")
|
| 70 |
+
return True
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def create_directories():
|
| 74 |
+
"""Create necessary directories."""
|
| 75 |
+
directories = [
|
| 76 |
+
"./data",
|
| 77 |
+
"./output",
|
| 78 |
+
"./logs",
|
| 79 |
+
]
|
| 80 |
+
|
| 81 |
+
logger.info("\nCreating directories...")
|
| 82 |
+
for directory in directories:
|
| 83 |
+
Path(directory).mkdir(parents=True, exist_ok=True)
|
| 84 |
+
logger.info(f"β {directory}")
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def print_deployment_info():
|
| 88 |
+
"""Print deployment information."""
|
| 89 |
+
logger.info("\n" + "=" * 60)
|
| 90 |
+
logger.info("Deployment Information")
|
| 91 |
+
logger.info("=" * 60)
|
| 92 |
+
|
| 93 |
+
is_hf_space = check_environment()
|
| 94 |
+
|
| 95 |
+
if is_hf_space:
|
| 96 |
+
space_id = os.getenv("SPACE_ID", "unknown")
|
| 97 |
+
logger.info(f"β Running in Hugging Face Space: {space_id}")
|
| 98 |
+
logger.info(f"β Space URL: https://huggingface.co/spaces/{space_id}")
|
| 99 |
+
else:
|
| 100 |
+
logger.info("βΉ Not running in Hugging Face Space")
|
| 101 |
+
logger.info("βΉ Running locally or in different environment")
|
| 102 |
+
|
| 103 |
+
logger.info(f"β Model: {os.getenv('MODEL_NAME', 'poolside/laguna-m.1:free')}")
|
| 104 |
+
logger.info(f"β Dataset: {os.getenv('HUGGINGFACE_DATASET', 'factorstudios/Pipeline')}")
|
| 105 |
+
logger.info(f"β Port: {os.getenv('PORT', '7860')}")
|
| 106 |
+
|
| 107 |
+
logger.info("\n" + "=" * 60)
|
| 108 |
+
logger.info("API Endpoints")
|
| 109 |
+
logger.info("=" * 60)
|
| 110 |
+
logger.info("Health: GET /health")
|
| 111 |
+
logger.info("Docs: GET /docs")
|
| 112 |
+
logger.info("Pipeline: POST /api/v1/pipeline/execute")
|
| 113 |
+
logger.info("=" * 60)
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def main():
|
| 117 |
+
"""Run setup checks."""
|
| 118 |
+
logger.info("Starting Hugging Face Space setup...\n")
|
| 119 |
+
|
| 120 |
+
# Check environment
|
| 121 |
+
is_hf_space = check_environment()
|
| 122 |
+
|
| 123 |
+
# Validate secrets
|
| 124 |
+
secrets_valid = validate_secrets()
|
| 125 |
+
|
| 126 |
+
# Create directories
|
| 127 |
+
create_directories()
|
| 128 |
+
|
| 129 |
+
# Print info
|
| 130 |
+
print_deployment_info()
|
| 131 |
+
|
| 132 |
+
if not secrets_valid:
|
| 133 |
+
logger.error("\nβ Setup validation failed!")
|
| 134 |
+
sys.exit(1)
|
| 135 |
+
|
| 136 |
+
logger.info("\nβ
Setup validation passed!")
|
| 137 |
+
logger.info("The application is ready to start.\n")
|
| 138 |
+
|
| 139 |
+
return 0
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
if __name__ == "__main__":
|
| 143 |
+
sys.exit(main())
|