500 lines
15 KiB
Markdown
500 lines
15 KiB
Markdown
# Story Generation Code Adaptation Guide
|
|
|
|
This guide shows how to adapt the existing story generation code to use the production-ready `main_text_generation` and subscription system.
|
|
|
|
## 1. Import Path Updates
|
|
|
|
### Before (Legacy)
|
|
```python
|
|
from ...gpt_providers.text_generation.main_text_generation import llm_text_gen
|
|
```
|
|
|
|
### After (Production)
|
|
```python
|
|
from services.llm_providers.main_text_generation import llm_text_gen
|
|
```
|
|
|
|
## 2. Adding User ID and Subscription Support
|
|
|
|
### Before
|
|
```python
|
|
def generate_with_retry(prompt, system_prompt=None):
|
|
try:
|
|
return llm_text_gen(prompt, system_prompt)
|
|
except Exception as e:
|
|
logger.error(f"Error generating content: {e}")
|
|
return ""
|
|
```
|
|
|
|
### After
|
|
```python
|
|
def generate_with_retry(prompt, system_prompt=None, user_id: str = None):
|
|
"""
|
|
Generate content with retry handling and subscription support.
|
|
|
|
Args:
|
|
prompt: The prompt to generate content from
|
|
system_prompt: Custom system prompt (optional)
|
|
user_id: Clerk user ID (required for subscription checking)
|
|
|
|
Returns:
|
|
Generated content string
|
|
|
|
Raises:
|
|
RuntimeError: If user_id is missing or subscription limits exceeded
|
|
HTTPException: If subscription limit exceeded (429 status)
|
|
"""
|
|
if not user_id:
|
|
raise RuntimeError("user_id is required for subscription checking")
|
|
|
|
try:
|
|
return llm_text_gen(
|
|
prompt=prompt,
|
|
system_prompt=system_prompt,
|
|
user_id=user_id
|
|
)
|
|
except HTTPException as e:
|
|
# Re-raise HTTPExceptions (e.g., 429 subscription limit)
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error generating content: {e}")
|
|
raise RuntimeError(f"Failed to generate content: {str(e)}") from e
|
|
```
|
|
|
|
## 3. Structured JSON Response for Outline
|
|
|
|
### Before
|
|
```python
|
|
outline = generate_with_retry(outline_prompt.format(premise=premise))
|
|
# Returns plain text, needs parsing
|
|
```
|
|
|
|
### After
|
|
```python
|
|
# Define JSON schema for structured outline
|
|
outline_schema = {
|
|
"type": "object",
|
|
"properties": {
|
|
"outline": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"scene_number": {"type": "integer"},
|
|
"title": {"type": "string"},
|
|
"description": {"type": "string"},
|
|
"key_events": {"type": "array", "items": {"type": "string"}}
|
|
},
|
|
"required": ["scene_number", "title", "description"]
|
|
}
|
|
}
|
|
},
|
|
"required": ["outline"]
|
|
}
|
|
|
|
# Generate structured outline
|
|
outline_response = llm_text_gen(
|
|
prompt=outline_prompt.format(premise=premise),
|
|
system_prompt=system_prompt,
|
|
json_struct=outline_schema,
|
|
user_id=user_id
|
|
)
|
|
|
|
# Parse JSON response
|
|
import json
|
|
outline_data = json.loads(outline_response)
|
|
outline = outline_data.get("outline", [])
|
|
```
|
|
|
|
## 4. Complete Service Example
|
|
|
|
### Story Service Structure
|
|
```python
|
|
# backend/services/story_writer/story_service.py
|
|
|
|
from typing import Dict, Any, Optional, List
|
|
from loguru import logger
|
|
from services.llm_providers.main_text_generation import llm_text_gen
|
|
import json
|
|
|
|
class StoryWriterService:
|
|
"""Service for generating stories using prompt chaining."""
|
|
|
|
def __init__(self):
|
|
self.guidelines = """\
|
|
Writing Guidelines:
|
|
|
|
Delve deeper. Lose yourself in the world you're building. Unleash vivid
|
|
descriptions to paint the scenes in your reader's mind.
|
|
Develop your characters — let their motivations, fears, and complexities unfold naturally.
|
|
Weave in the threads of your outline, but don't feel constrained by it.
|
|
Allow your story to surprise you as you write. Use rich imagery, sensory details, and
|
|
evocative language to bring the setting, characters, and events to life.
|
|
Introduce elements subtly that can blossom into complex subplots, relationships,
|
|
or worldbuilding details later in the story.
|
|
Keep things intriguing but not fully resolved.
|
|
Avoid boxing the story into a corner too early.
|
|
Plant the seeds of subplots or potential character arc shifts that can be expanded later.
|
|
|
|
Remember, your main goal is to write as much as you can. If you get through
|
|
the story too fast, that is bad. Expand, never summarize.
|
|
"""
|
|
|
|
def generate_premise(
|
|
self,
|
|
persona: str,
|
|
story_setting: str,
|
|
character_input: str,
|
|
plot_elements: str,
|
|
user_id: str
|
|
) -> str:
|
|
"""Generate story premise."""
|
|
prompt = f"""\
|
|
{persona}
|
|
|
|
Write a single sentence premise for a {story_setting} story featuring {character_input}.
|
|
The plot will revolve around: {plot_elements}
|
|
"""
|
|
|
|
try:
|
|
premise = llm_text_gen(
|
|
prompt=prompt,
|
|
user_id=user_id
|
|
)
|
|
return premise.strip()
|
|
except Exception as e:
|
|
logger.error(f"Error generating premise: {e}")
|
|
raise RuntimeError(f"Failed to generate premise: {str(e)}") from e
|
|
|
|
def generate_outline(
|
|
self,
|
|
premise: str,
|
|
persona: str,
|
|
story_setting: str,
|
|
character_input: str,
|
|
plot_elements: str,
|
|
user_id: str
|
|
) -> List[Dict[str, Any]]:
|
|
"""Generate structured story outline."""
|
|
prompt = f"""\
|
|
{persona}
|
|
|
|
You have a gripping premise in mind:
|
|
|
|
{premise}
|
|
|
|
Write an outline for the plot of your story set in {story_setting} featuring {character_input}.
|
|
The plot elements are: {plot_elements}
|
|
"""
|
|
|
|
# Define JSON schema for structured response
|
|
json_schema = {
|
|
"type": "object",
|
|
"properties": {
|
|
"outline": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"scene_number": {"type": "integer"},
|
|
"title": {"type": "string"},
|
|
"description": {"type": "string"},
|
|
"key_events": {
|
|
"type": "array",
|
|
"items": {"type": "string"}
|
|
}
|
|
},
|
|
"required": ["scene_number", "title", "description"]
|
|
}
|
|
}
|
|
},
|
|
"required": ["outline"]
|
|
}
|
|
|
|
try:
|
|
response = llm_text_gen(
|
|
prompt=prompt,
|
|
json_struct=json_schema,
|
|
user_id=user_id
|
|
)
|
|
|
|
# Parse JSON response
|
|
outline_data = json.loads(response)
|
|
return outline_data.get("outline", [])
|
|
except json.JSONDecodeError as e:
|
|
logger.error(f"Failed to parse outline JSON: {e}")
|
|
# Fallback to text parsing if JSON fails
|
|
return self._parse_text_outline(response)
|
|
except Exception as e:
|
|
logger.error(f"Error generating outline: {e}")
|
|
raise RuntimeError(f"Failed to generate outline: {str(e)}") from e
|
|
|
|
def generate_story_start(
|
|
self,
|
|
premise: str,
|
|
outline: str,
|
|
persona: str,
|
|
story_setting: str,
|
|
character_input: str,
|
|
plot_elements: str,
|
|
writing_style: str,
|
|
story_tone: str,
|
|
narrative_pov: str,
|
|
audience_age_group: str,
|
|
content_rating: str,
|
|
ending_preference: str,
|
|
user_id: str
|
|
) -> str:
|
|
"""Generate the starting section of the story."""
|
|
# Format outline as text if it's a list
|
|
if isinstance(outline, list):
|
|
outline_text = "\n".join([
|
|
f"{item.get('scene_number', i+1)}. {item.get('title', '')}: {item.get('description', '')}"
|
|
for i, item in enumerate(outline)
|
|
])
|
|
else:
|
|
outline_text = str(outline)
|
|
|
|
prompt = f"""\
|
|
{persona}
|
|
|
|
Write a story with the following details:
|
|
|
|
**The Story Setting is:**
|
|
{story_setting}
|
|
|
|
**The Characters of the story are:**
|
|
{character_input}
|
|
|
|
**Plot Elements of the story:**
|
|
{plot_elements}
|
|
|
|
**Story Writing Style:**
|
|
{writing_style}
|
|
|
|
**The story Tone is:**
|
|
{story_tone}
|
|
|
|
**Write story from the Point of View of:**
|
|
{narrative_pov}
|
|
|
|
**Target Audience of the story:**
|
|
{audience_age_group}, **Content Rating:** {content_rating}
|
|
|
|
**Story Ending:**
|
|
{ending_preference}
|
|
|
|
You have a gripping premise in mind:
|
|
|
|
{premise}
|
|
|
|
Your imagination has crafted a rich narrative outline:
|
|
|
|
{outline_text}
|
|
|
|
First, silently review the outline and the premise. Consider how to start the story.
|
|
|
|
Start to write the very beginning of the story. You are not expected to finish
|
|
the whole story now. Your writing should be detailed enough that you are only
|
|
scratching the surface of the first bullet of your outline. Try to write AT
|
|
MINIMUM 4000 WORDS.
|
|
|
|
{self.guidelines}
|
|
"""
|
|
|
|
try:
|
|
starting_draft = llm_text_gen(
|
|
prompt=prompt,
|
|
user_id=user_id
|
|
)
|
|
return starting_draft.strip()
|
|
except Exception as e:
|
|
logger.error(f"Error generating story start: {e}")
|
|
raise RuntimeError(f"Failed to generate story start: {str(e)}") from e
|
|
|
|
def continue_story(
|
|
self,
|
|
premise: str,
|
|
outline: str,
|
|
story_text: str,
|
|
persona: str,
|
|
story_setting: str,
|
|
character_input: str,
|
|
plot_elements: str,
|
|
writing_style: str,
|
|
story_tone: str,
|
|
narrative_pov: str,
|
|
audience_age_group: str,
|
|
content_rating: str,
|
|
ending_preference: str,
|
|
user_id: str
|
|
) -> str:
|
|
"""Continue writing the story."""
|
|
# Format outline as text if it's a list
|
|
if isinstance(outline, list):
|
|
outline_text = "\n".join([
|
|
f"{item.get('scene_number', i+1)}. {item.get('title', '')}: {item.get('description', '')}"
|
|
for i, item in enumerate(outline)
|
|
])
|
|
else:
|
|
outline_text = str(outline)
|
|
|
|
prompt = f"""\
|
|
{persona}
|
|
|
|
Write a story with the following details:
|
|
|
|
**The Story Setting is:**
|
|
{story_setting}
|
|
|
|
**The Characters of the story are:**
|
|
{character_input}
|
|
|
|
**Plot Elements of the story:**
|
|
{plot_elements}
|
|
|
|
**Story Writing Style:**
|
|
{writing_style}
|
|
|
|
**The story Tone is:**
|
|
{story_tone}
|
|
|
|
**Write story from the Point of View of:**
|
|
{narrative_pov}
|
|
|
|
**Target Audience of the story:**
|
|
{audience_age_group}, **Content Rating:** {content_rating}
|
|
|
|
**Story Ending:**
|
|
{ending_preference}
|
|
|
|
You have a gripping premise in mind:
|
|
|
|
{premise}
|
|
|
|
Your imagination has crafted a rich narrative outline:
|
|
|
|
{outline_text}
|
|
|
|
You've begun to immerse yourself in this world, and the words are flowing.
|
|
Here's what you've written so far:
|
|
|
|
{story_text}
|
|
|
|
=====
|
|
|
|
First, silently review the outline and story so far. Identify what the single
|
|
next part of your outline you should write.
|
|
|
|
Your task is to continue where you left off and write the next part of the story.
|
|
You are not expected to finish the whole story now. Your writing should be
|
|
detailed enough that you are only scratching the surface of the next part of
|
|
your outline. Try to write AT MINIMUM 2000 WORDS. However, only once the story
|
|
is COMPLETELY finished, write IAMDONE. Remember, do NOT write a whole chapter
|
|
right now.
|
|
|
|
{self.guidelines}
|
|
"""
|
|
|
|
try:
|
|
continuation = llm_text_gen(
|
|
prompt=prompt,
|
|
user_id=user_id
|
|
)
|
|
return continuation.strip()
|
|
except Exception as e:
|
|
logger.error(f"Error continuing story: {e}")
|
|
raise RuntimeError(f"Failed to continue story: {str(e)}") from e
|
|
|
|
def _parse_text_outline(self, text: str) -> List[Dict[str, Any]]:
|
|
"""Fallback method to parse text outline if JSON parsing fails."""
|
|
# Simple text parsing logic
|
|
lines = text.strip().split('\n')
|
|
outline = []
|
|
for i, line in enumerate(lines):
|
|
if line.strip():
|
|
outline.append({
|
|
"scene_number": i + 1,
|
|
"title": f"Scene {i + 1}",
|
|
"description": line.strip(),
|
|
"key_events": []
|
|
})
|
|
return outline
|
|
```
|
|
|
|
## 5. API Endpoint Example
|
|
|
|
```python
|
|
# backend/api/story_writer/router.py
|
|
|
|
from fastapi import APIRouter, HTTPException, Depends
|
|
from typing import Dict, Any
|
|
from middleware.auth_middleware import get_current_user
|
|
from services.story_writer.story_service import StoryWriterService
|
|
from models.story_models import StoryGenerationRequest
|
|
|
|
router = APIRouter(prefix="/api/story", tags=["Story Writer"])
|
|
service = StoryWriterService()
|
|
|
|
@router.post("/generate-premise")
|
|
async def generate_premise(
|
|
request: StoryGenerationRequest,
|
|
current_user: Dict[str, Any] = Depends(get_current_user)
|
|
) -> Dict[str, Any]:
|
|
"""Generate story premise."""
|
|
try:
|
|
if not current_user:
|
|
raise HTTPException(status_code=401, detail="Authentication required")
|
|
|
|
user_id = str(current_user.get('id', ''))
|
|
if not user_id:
|
|
raise HTTPException(status_code=401, detail="Invalid user ID")
|
|
|
|
premise = service.generate_premise(
|
|
persona=request.persona,
|
|
story_setting=request.story_setting,
|
|
character_input=request.character_input,
|
|
plot_elements=request.plot_elements,
|
|
user_id=user_id
|
|
)
|
|
|
|
return {"premise": premise, "success": True}
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Failed to generate premise: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
```
|
|
|
|
## 6. Key Differences Summary
|
|
|
|
| Aspect | Legacy Code | Production Code |
|
|
|--------|------------|-----------------|
|
|
| Import Path | `...gpt_providers.text_generation.main_text_generation` | `services.llm_providers.main_text_generation` |
|
|
| User ID | Not required | Required parameter |
|
|
| Subscription | No checks | Automatic via `main_text_generation` |
|
|
| Error Handling | Basic try/except | HTTPException handling for 429 errors |
|
|
| Structured Responses | Text parsing | JSON schema support |
|
|
| Async Support | Synchronous | Can use async/await |
|
|
| Logging | Basic | Comprehensive with loguru |
|
|
|
|
## 7. Testing Checklist
|
|
|
|
When adapting code, verify:
|
|
- [ ] All imports updated to production paths
|
|
- [ ] `user_id` parameter added to all LLM calls
|
|
- [ ] Subscription errors (429) are handled properly
|
|
- [ ] Error messages are user-friendly
|
|
- [ ] Logging is comprehensive
|
|
- [ ] Structured JSON responses work correctly
|
|
- [ ] Fallback logic for text parsing exists
|
|
- [ ] Long-running operations use task management
|
|
|
|
## 8. Common Pitfalls
|
|
|
|
1. **Missing user_id**: Always pass `user_id` parameter
|
|
2. **Ignoring HTTPException**: Re-raise HTTPExceptions (especially 429)
|
|
3. **No fallback parsing**: If JSON parsing fails, have text parsing fallback
|
|
4. **Synchronous blocking**: Use async endpoints for long-running operations
|
|
5. **No error context**: Include original exception in error messages
|