-
Notifications
You must be signed in to change notification settings - Fork 49
/
Copy pathbook_generator.py
377 lines (302 loc) · 15.5 KB
/
book_generator.py
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
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
"""Main class for generating books using AutoGen with improved iteration control"""
import autogen
from typing import Dict, List, Optional
import os
import time
import re
class BookGenerator:
def __init__(self, agents: Dict[str, autogen.ConversableAgent], agent_config: Dict, outline: List[Dict]):
"""Initialize with outline to maintain chapter count context"""
self.agents = agents
self.agent_config = agent_config
self.output_dir = "book_output"
self.chapters_memory = [] # Store chapter summaries
self.max_iterations = 3 # Limit editor-writer iterations
self.outline = outline # Store the outline
os.makedirs(self.output_dir, exist_ok=True)
def _clean_chapter_content(self, content: str) -> str:
"""Clean up chapter content by removing artifacts and chapter numbers"""
# Remove chapter number references
content = re.sub(r'\*?\s*\(Chapter \d+.*?\)', '', content)
content = re.sub(r'\*?\s*Chapter \d+.*?\n', '', content, count=1)
# Clean up any remaining markdown artifacts
content = content.replace('*', '')
content = content.strip()
return content
def initiate_group_chat(self) -> autogen.GroupChat:
"""Create a new group chat for the agents with improved speaking order"""
outline_context = "\n".join([
f"\nChapter {ch['chapter_number']}: {ch['title']}\n{ch['prompt']}"
for ch in sorted(self.outline, key=lambda x: x['chapter_number'])
])
messages = [{
"role": "system",
"content": f"Complete Book Outline:\n{outline_context}"
}]
writer_final = autogen.AssistantAgent(
name="writer_final",
system_message=self.agents["writer"].system_message,
llm_config=self.agent_config
)
return autogen.GroupChat(
agents=[
self.agents["user_proxy"],
self.agents["memory_keeper"],
self.agents["writer"],
self.agents["editor"],
writer_final
],
messages=messages,
max_round=5,
speaker_selection_method="round_robin"
)
def _get_sender(self, msg: Dict) -> str:
"""Helper to get sender from message regardless of format"""
return msg.get("sender") or msg.get("name", "")
def _verify_chapter_complete(self, messages: List[Dict]) -> bool:
"""Verify chapter completion by analyzing entire conversation context"""
print("******************** VERIFYING CHAPTER COMPLETION ****************")
current_chapter = None
chapter_content = None
sequence_complete = {
'memory_update': False,
'plan': False,
'setting': False,
'scene': False,
'feedback': False,
'scene_final': False,
'confirmation': False
}
# Analyze full conversation
for msg in messages:
content = msg.get("content", "")
# Track chapter number
if not current_chapter:
num_match = re.search(r"Chapter (\d+):", content)
if num_match:
current_chapter = int(num_match.group(1))
# Track completion sequence
if "MEMORY UPDATE:" in content: sequence_complete['memory_update'] = True
if "PLAN:" in content: sequence_complete['plan'] = True
if "SETTING:" in content: sequence_complete['setting'] = True
if "SCENE:" in content: sequence_complete['scene'] = True
if "FEEDBACK:" in content: sequence_complete['feedback'] = True
if "SCENE FINAL:" in content:
sequence_complete['scene_final'] = True
chapter_content = content.split("SCENE FINAL:")[1].strip()
if "**Confirmation:**" in content and "successfully" in content:
sequence_complete['confirmation'] = True
#print all sequence_complete flags
print("******************** SEQUENCE COMPLETE **************", sequence_complete)
print("******************** CURRENT_CHAPTER ****************", current_chapter)
print("******************** CHAPTER_CONTENT ****************", chapter_content)
# Verify all steps completed and content exists
if all(sequence_complete.values()) and current_chapter and chapter_content:
self._save_chapter(current_chapter, chapter_content)
return True
return False
def _prepare_chapter_context(self, chapter_number: int, prompt: str) -> str:
"""Prepare context for chapter generation"""
if chapter_number == 1:
return f"Initial Chapter\nRequirements:\n{prompt}"
context_parts = [
"Previous Chapter Summaries:",
*[f"Chapter {i+1}: {summary}" for i, summary in enumerate(self.chapters_memory)],
"\nCurrent Chapter Requirements:",
prompt
]
return "\n".join(context_parts)
def generate_chapter(self, chapter_number: int, prompt: str) -> None:
"""Generate a single chapter with completion verification"""
print(f"\nGenerating Chapter {chapter_number}...")
try:
# Create group chat with reduced rounds
groupchat = self.initiate_group_chat()
manager = autogen.GroupChatManager(
groupchat=groupchat,
llm_config=self.agent_config
)
# Prepare context
context = self._prepare_chapter_context(chapter_number, prompt)
chapter_prompt = f"""
IMPORTANT: Wait for confirmation before proceeding.
IMPORTANT: This is Chapter {chapter_number}. Do not proceed to next chapter until explicitly instructed.
DO NOT END THE STORY HERE unless this is actually the final chapter ({self.outline[-1]['chapter_number']}).
Current Task: Generate Chapter {chapter_number} content only.
Chapter Outline:
Title: {self.outline[chapter_number - 1]['title']}
Chapter Requirements:
{prompt}
Previous Context for Reference:
{context}
Follow this exact sequence for Chapter {chapter_number} only:
1. Memory Keeper: Context (MEMORY UPDATE)
2. Writer: Draft (CHAPTER)
3. Editor: Review (FEEDBACK)
4. Writer Final: Revision (CHAPTER FINAL)
Wait for each step to complete before proceeding."""
# Start generation
self.agents["user_proxy"].initiate_chat(
manager,
message=chapter_prompt
)
if not self._verify_chapter_complete(groupchat.messages):
raise ValueError(f"Chapter {chapter_number} generation incomplete")
self._process_chapter_results(chapter_number, groupchat.messages)
chapter_file = os.path.join(self.output_dir, f"chapter_{chapter_number:02d}.txt")
if not os.path.exists(chapter_file):
raise FileNotFoundError(f"Chapter {chapter_number} file not created")
completion_msg = f"Chapter {chapter_number} is complete. Proceed with next chapter."
self.agents["user_proxy"].send(completion_msg, manager)
except Exception as e:
print(f"Error in chapter {chapter_number}: {str(e)}")
self._handle_chapter_generation_failure(chapter_number, prompt)
def _extract_final_scene(self, messages: List[Dict]) -> Optional[str]:
"""Extract chapter content with improved content detection"""
for msg in reversed(messages):
content = msg.get("content", "")
sender = self._get_sender(msg)
if sender in ["writer", "writer_final"]:
# Handle complete scene content
if "SCENE FINAL:" in content:
scene_text = content.split("SCENE FINAL:")[1].strip()
if scene_text:
return scene_text
# Fallback to scene content
if "SCENE:" in content:
scene_text = content.split("SCENE:")[1].strip()
if scene_text:
return scene_text
# Handle raw content
if len(content.strip()) > 100: # Minimum content threshold
return content.strip()
return None
def _handle_chapter_generation_failure(self, chapter_number: int, prompt: str) -> None:
"""Handle failed chapter generation with simplified retry"""
print(f"Attempting simplified retry for Chapter {chapter_number}...")
try:
# Create a new group chat with just essential agents
retry_groupchat = autogen.GroupChat(
agents=[
self.agents["user_proxy"],
self.agents["story_planner"],
self.agents["writer"]
],
messages=[],
max_round=3
)
manager = autogen.GroupChatManager(
groupchat=retry_groupchat,
llm_config=self.agent_config
)
retry_prompt = f"""Emergency chapter generation for Chapter {chapter_number}.
{prompt}
Please generate this chapter in two steps:
1. Story Planner: Create a basic outline (tag: PLAN)
2. Writer: Write the complete chapter (tag: SCENE FINAL)
Keep it simple and direct."""
self.agents["user_proxy"].initiate_chat(
manager,
message=retry_prompt
)
# Save the retry results
self._process_chapter_results(chapter_number, retry_groupchat.messages)
except Exception as e:
print(f"Error in retry attempt for Chapter {chapter_number}: {str(e)}")
print("Unable to generate chapter content after retry")
def _process_chapter_results(self, chapter_number: int, messages: List[Dict]) -> None:
"""Process and save chapter results, updating memory"""
try:
# Extract the Memory Keeper's final summary
memory_updates = []
for msg in reversed(messages):
sender = self._get_sender(msg)
content = msg.get("content", "")
if sender == "memory_keeper" and "MEMORY UPDATE:" in content:
update_start = content.find("MEMORY UPDATE:") + 14
memory_updates.append(content[update_start:].strip())
break
# Add to memory even if no explicit update (use basic content summary)
if memory_updates:
self.chapters_memory.append(memory_updates[0])
else:
# Create basic memory from chapter content
chapter_content = self._extract_final_scene(messages)
if chapter_content:
basic_summary = f"Chapter {chapter_number} Summary: {chapter_content[:200]}..."
self.chapters_memory.append(basic_summary)
# Extract and save the chapter content
self._save_chapter(chapter_number, messages)
except Exception as e:
print(f"Error processing chapter results: {str(e)}")
raise
def _save_chapter(self, chapter_number: int, messages: List[Dict]) -> None:
print(f"\nSaving Chapter {chapter_number}")
try:
chapter_content = self._extract_final_scene(messages)
if not chapter_content:
raise ValueError(f"No content found for Chapter {chapter_number}")
chapter_content = self._clean_chapter_content(chapter_content)
filename = os.path.join(self.output_dir, f"chapter_{chapter_number:02d}.txt")
# Create backup if file exists
if os.path.exists(filename):
backup_filename = f"{filename}.backup"
import shutil
shutil.copy2(filename, backup_filename)
with open(filename, "w", encoding='utf-8') as f:
f.write(f"Chapter {chapter_number}\n\n{chapter_content}")
# Verify file
with open(filename, "r", encoding='utf-8') as f:
saved_content = f.read()
if len(saved_content.strip()) == 0:
raise IOError(f"File {filename} is empty")
print(f"✓ Saved to: {filename}")
except Exception as e:
print(f"Error saving chapter: {str(e)}")
raise
def generate_book(self, outline: List[Dict]) -> None:
"""Generate the book with strict chapter sequencing"""
print("\nStarting Book Generation...")
print(f"Total chapters: {len(outline)}")
# Sort outline by chapter number
sorted_outline = sorted(outline, key=lambda x: x["chapter_number"])
for chapter in sorted_outline:
chapter_number = chapter["chapter_number"]
# Verify previous chapter exists and is valid
if chapter_number > 1:
prev_file = os.path.join(self.output_dir, f"chapter_{chapter_number-1:02d}.txt")
if not os.path.exists(prev_file):
print(f"Previous chapter {chapter_number-1} not found. Stopping.")
break
# Verify previous chapter content
with open(prev_file, 'r', encoding='utf-8') as f:
content = f.read()
if not self._verify_chapter_content(content, chapter_number-1):
print(f"Previous chapter {chapter_number-1} content invalid. Stopping.")
break
# Generate current chapter
print(f"\n{'='*20} Chapter {chapter_number} {'='*20}")
self.generate_chapter(chapter_number, chapter["prompt"])
# Verify current chapter
chapter_file = os.path.join(self.output_dir, f"chapter_{chapter_number:02d}.txt")
if not os.path.exists(chapter_file):
print(f"Failed to generate chapter {chapter_number}")
break
with open(chapter_file, 'r', encoding='utf-8') as f:
content = f.read()
if not self._verify_chapter_content(content, chapter_number):
print(f"Chapter {chapter_number} content invalid")
break
print(f"✓ Chapter {chapter_number} complete")
time.sleep(5)
def _verify_chapter_content(self, content: str, chapter_number: int) -> bool:
"""Verify chapter content is valid"""
if not content:
return False
# Check for chapter header
if f"Chapter {chapter_number}" not in content:
return False
# Ensure content isn't just metadata
lines = content.split('\n')
content_lines = [line for line in lines if line.strip() and 'MEMORY UPDATE:' not in line]
return len(content_lines) >= 3 # At least chapter header + 2 content lines