Deployment Script commited on
Commit
2dfc473
Β·
1 Parent(s): 6225fce

Deploy multi-agent system

Browse files
.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())