File size: 9,981 Bytes
73e99c6 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 |
#!/usr/bin/env python3
"""
LLM Completion Workflow for The Shadow of Lillya
Supports multiple LLMs for generating novel completions
"""
import os
import json
from pathlib import Path
from datetime import datetime
from typing import List, Dict, Optional
import argparse
class LLMCompletion:
"""Base class for LLM completion"""
def __init__(self, model_name: str):
self.model_name = model_name
self.completions_dir = Path("completion_attempts")
self.completions_dir.mkdir(exist_ok=True)
def load_manuscripts(self) -> Dict[str, str]:
"""Load all manuscript files"""
manuscripts = {}
manuscripts_dir = Path("manuscripts")
# Load Circus of the Queens
circus_dir = manuscripts_dir / "Circus_of_the_Queens"
for md_file in circus_dir.glob("*.md"):
with open(md_file, 'r', encoding='utf-8') as f:
manuscripts['circus_of_the_queens'] = f.read()
# Load edited version of Shadow of Lillya
edited_dir = manuscripts_dir / "Shadow_of_Lillya" / "edited_version"
for md_file in edited_dir.glob("*.md"):
with open(md_file, 'r', encoding='utf-8') as f:
manuscripts['shadow_edited'] = f.read()
# Load unedited material
unedited_dir = manuscripts_dir / "Shadow_of_Lillya" / "unedited_material"
unedited_texts = []
for md_file in unedited_dir.glob("*.md"):
with open(md_file, 'r', encoding='utf-8') as f:
unedited_texts.append(f.read())
manuscripts['shadow_unedited'] = '\n\n---\n\n'.join(unedited_texts)
# Load notes and outlines
shadow_dir = manuscripts_dir / "Shadow_of_Lillya"
notes = []
for md_file in shadow_dir.glob("*.md"):
if 'notes' in md_file.name.lower() or 'outline' in md_file.name.lower():
with open(md_file, 'r', encoding='utf-8') as f:
notes.append(f.read())
manuscripts['notes'] = '\n\n---\n\n'.join(notes)
return manuscripts
def create_prompt(self, manuscripts: Dict[str, str], continuation_point: Optional[str] = None) -> str:
"""Create a prompt for LLM completion"""
prompt = f"""You are completing the novel "The Shadow of Lillya" by Audrey Berger Welz. This is a sequel/prequel to her novel "Circus of the Queens."
CONTEXT - CIRCUS OF THE QUEENS:
{manuscripts.get('circus_of_the_queens', '')[:5000]}...
CURRENT MANUSCRIPT - THE SHADOW OF LILLYA (Edited Version):
{manuscripts.get('shadow_edited', '')}
ADDITIONAL MATERIAL - UNEDITED VERSIONS:
{manuscripts.get('shadow_unedited', '')[:3000]}...
NOTES AND OUTLINES:
{manuscripts.get('notes', '')}
INSTRUCTIONS:
1. Continue the story from where Audrey left off, maintaining her unique voice and writing style
2. Stay true to the characters and world established in "Circus of the Queens"
3. Preserve the thematic elements and narrative style of Audrey's work
4. Ensure character consistency and plot coherence
5. Write in a style that matches Audrey's voice as closely as possible
CONTINUATION POINT:
{continuation_point if continuation_point else 'Continue from the end of the edited manuscript.'}
Please continue the novel, writing approximately 1000-2000 words that seamlessly continue from where Audrey's manuscript ends."""
return prompt
def generate_completion(self, prompt: str, max_tokens: int = 2000) -> str:
"""Generate completion using the LLM (to be implemented by subclasses)"""
raise NotImplementedError("Subclasses must implement generate_completion")
def save_completion(self, completion: str, metadata: Dict) -> Path:
"""Save completion to file"""
model_dir = self.completions_dir / self.model_name.lower().replace(' ', '_')
model_dir.mkdir(exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"completion_{timestamp}.md"
filepath = model_dir / filename
# Create markdown file with metadata
content = f"""# Completion Attempt - {self.model_name}
**Generated:** {metadata.get('timestamp', datetime.now().isoformat())}
**Model:** {self.model_name}
**Version:** {metadata.get('version', '1.0')}
**Continuation Point:** {metadata.get('continuation_point', 'End of edited manuscript')}
---
## Generated Continuation
{completion}
---
## Metadata
```json
{json.dumps(metadata, indent=2)}
```
"""
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
# Also save metadata separately
metadata_file = model_dir / f"metadata_{timestamp}.json"
with open(metadata_file, 'w', encoding='utf-8') as f:
json.dump(metadata, f, indent=2)
return filepath
class OpenAICompletion(LLMCompletion):
"""OpenAI GPT completion"""
def __init__(self, model_name: str = "gpt-4", api_key: Optional[str] = None):
super().__init__(f"OpenAI-{model_name}")
self.model_name_full = model_name
self.api_key = api_key or os.getenv('OPENAI_API_KEY')
if not self.api_key:
raise ValueError("OpenAI API key required. Set OPENAI_API_KEY environment variable.")
def generate_completion(self, prompt: str, max_tokens: int = 2000) -> str:
"""Generate completion using OpenAI API"""
try:
from openai import OpenAI
client = OpenAI(api_key=self.api_key)
response = client.chat.completions.create(
model=self.model_name_full,
messages=[
{"role": "system", "content": "You are a literary assistant helping to complete a novel in the author's voice and style."},
{"role": "user", "content": prompt}
],
max_tokens=max_tokens,
temperature=0.7
)
return response.choices[0].message.content
except ImportError:
raise ImportError("OpenAI library not installed. Run: pip install openai")
except Exception as e:
raise Exception(f"OpenAI API error: {e}")
class AnthropicCompletion(LLMCompletion):
"""Anthropic Claude completion"""
def __init__(self, model_name: str = "claude-3-opus-20240229", api_key: Optional[str] = None):
super().__init__(f"Anthropic-{model_name}")
self.model_name_full = model_name
self.api_key = api_key or os.getenv('ANTHROPIC_API_KEY')
if not self.api_key:
raise ValueError("Anthropic API key required. Set ANTHROPIC_API_KEY environment variable.")
def generate_completion(self, prompt: str, max_tokens: int = 2000) -> str:
"""Generate completion using Anthropic API"""
try:
from anthropic import Anthropic
client = Anthropic(api_key=self.api_key)
response = client.messages.create(
model=self.model_name_full,
max_tokens=max_tokens,
messages=[
{"role": "user", "content": prompt}
]
)
return response.content[0].text
except ImportError:
raise ImportError("Anthropic library not installed. Run: pip install anthropic")
except Exception as e:
raise Exception(f"Anthropic API error: {e}")
def main():
parser = argparse.ArgumentParser(description='Generate LLM completions for The Shadow of Lillya')
parser.add_argument('--model', choices=['openai', 'anthropic'], default='openai',
help='LLM provider to use')
parser.add_argument('--model-name', type=str,
help='Specific model name (e.g., gpt-4, claude-3-opus-20240229)')
parser.add_argument('--api-key', type=str,
help='API key (or set environment variable)')
parser.add_argument('--max-tokens', type=int, default=2000,
help='Maximum tokens to generate')
parser.add_argument('--continuation-point', type=str,
help='Specific point in text to continue from')
args = parser.parse_args()
# Load manuscripts
print("π Loading manuscripts...")
base_completion = LLMCompletion("base")
manuscripts = base_completion.load_manuscripts()
print(f" β Loaded {len(manuscripts)} manuscript sections")
# Create prompt
print("π Creating prompt...")
prompt = base_completion.create_prompt(manuscripts, args.continuation_point)
print(f" β Prompt created ({len(prompt)} characters)")
# Initialize LLM
print(f"π€ Initializing {args.model}...")
if args.model == 'openai':
model_name = args.model_name or "gpt-4"
llm = OpenAICompletion(model_name, args.api_key)
elif args.model == 'anthropic':
model_name = args.model_name or "claude-3-opus-20240229"
llm = AnthropicCompletion(model_name, args.api_key)
# Generate completion
print("β¨ Generating completion...")
try:
completion = llm.generate_completion(prompt, args.max_tokens)
print(f" β Generated {len(completion)} characters")
# Save completion
metadata = {
'model': llm.model_name,
'model_full': model_name,
'timestamp': datetime.now().isoformat(),
'continuation_point': args.continuation_point or 'End of edited manuscript',
'max_tokens': args.max_tokens,
'completion_length': len(completion)
}
filepath = llm.save_completion(completion, metadata)
print(f" β Saved to: {filepath}")
except Exception as e:
print(f" β Error: {e}")
return 1
return 0
if __name__ == '__main__':
exit(main())
|