deepagents-printshop 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. agents/content_editor/__init__.py +1 -0
  2. agents/content_editor/agent.py +279 -0
  3. agents/content_editor/content_reviewer.py +327 -0
  4. agents/content_editor/versioned_agent.py +455 -0
  5. agents/latex_specialist/__init__.py +1 -0
  6. agents/latex_specialist/agent.py +531 -0
  7. agents/latex_specialist/latex_analyzer.py +510 -0
  8. agents/latex_specialist/latex_optimizer.py +1192 -0
  9. agents/qa_orchestrator/__init__.py +1 -0
  10. agents/qa_orchestrator/agent.py +603 -0
  11. agents/qa_orchestrator/langgraph_workflow.py +733 -0
  12. agents/qa_orchestrator/pipeline_types.py +72 -0
  13. agents/qa_orchestrator/quality_gates.py +495 -0
  14. agents/qa_orchestrator/workflow_coordinator.py +139 -0
  15. agents/research_agent/__init__.py +1 -0
  16. agents/research_agent/agent.py +258 -0
  17. agents/research_agent/llm_report_generator.py +1023 -0
  18. agents/research_agent/report_generator.py +536 -0
  19. agents/visual_qa/__init__.py +1 -0
  20. agents/visual_qa/agent.py +410 -0
  21. deepagents_printshop-0.1.0.dist-info/METADATA +744 -0
  22. deepagents_printshop-0.1.0.dist-info/RECORD +37 -0
  23. deepagents_printshop-0.1.0.dist-info/WHEEL +4 -0
  24. deepagents_printshop-0.1.0.dist-info/entry_points.txt +2 -0
  25. deepagents_printshop-0.1.0.dist-info/licenses/LICENSE +86 -0
  26. tools/__init__.py +1 -0
  27. tools/change_tracker.py +419 -0
  28. tools/content_type_loader.py +171 -0
  29. tools/graph_generator.py +281 -0
  30. tools/latex_generator.py +374 -0
  31. tools/llm_latex_generator.py +678 -0
  32. tools/magazine_layout.py +462 -0
  33. tools/pattern_injector.py +250 -0
  34. tools/pattern_learner.py +477 -0
  35. tools/pdf_compiler.py +386 -0
  36. tools/version_manager.py +346 -0
  37. tools/visual_qa.py +799 -0
@@ -0,0 +1,678 @@
1
+ """LLM-Based LaTeX Generator that uses Claude for intelligent document generation."""
2
+
3
+ import os
4
+ import json
5
+ from typing import List, Dict, Optional, Tuple
6
+ from dataclasses import dataclass
7
+ import anthropic
8
+
9
+
10
+ @dataclass
11
+ class LaTeXGenerationRequest:
12
+ """Request for LaTeX document generation."""
13
+ title: str
14
+ author: str
15
+ content_sections: List[Dict] # List of {title, content, type}
16
+ tables: List[Dict] = None # List of {caption, data, format}
17
+ figures: List[Dict] = None # List of {path, caption, width}
18
+ requirements: List[str] = None # Special requirements
19
+
20
+ def __post_init__(self):
21
+ if self.tables is None:
22
+ self.tables = []
23
+ if self.figures is None:
24
+ self.figures = []
25
+ if self.requirements is None:
26
+ self.requirements = []
27
+
28
+
29
+ @dataclass
30
+ class LaTeXGenerationResult:
31
+ """Result of LaTeX generation."""
32
+ success: bool
33
+ latex_content: str
34
+ warnings: List[str]
35
+ improvements_made: List[str]
36
+ error_message: Optional[str] = None
37
+
38
+
39
+ class LLMLaTeXGenerator:
40
+ """
41
+ LLM-based LaTeX generator that uses Claude to intelligently create
42
+ LaTeX documents with proper error handling and edge case management.
43
+
44
+ This replaces the deterministic template-based approach with an
45
+ intelligent system that can reason about LaTeX structure and syntax.
46
+ """
47
+
48
+ def __init__(self, api_key: Optional[str] = None):
49
+ """Initialize with Anthropic API key."""
50
+ self.api_key = api_key or os.getenv('ANTHROPIC_API_KEY')
51
+ if not self.api_key:
52
+ raise ValueError("ANTHROPIC_API_KEY not found")
53
+ self.client = anthropic.Anthropic(api_key=self.api_key)
54
+
55
+ def generate_document(self, request: LaTeXGenerationRequest,
56
+ validate: bool = True) -> LaTeXGenerationResult:
57
+ """
58
+ Generate a complete LaTeX document using LLM reasoning.
59
+
60
+ Args:
61
+ request: LaTeX generation request with content and requirements
62
+ validate: Whether to validate and fix LaTeX syntax
63
+
64
+ Returns:
65
+ LaTeXGenerationResult with generated LaTeX and metadata
66
+ """
67
+ print("📝 Generating LaTeX document with LLM reasoning...")
68
+
69
+ # Step 1: Generate initial LaTeX
70
+ latex_content = self._generate_initial_latex(request)
71
+
72
+ if not latex_content:
73
+ return LaTeXGenerationResult(
74
+ success=False,
75
+ latex_content="",
76
+ warnings=[],
77
+ improvements_made=[],
78
+ error_message="Failed to generate initial LaTeX"
79
+ )
80
+
81
+ # Step 2: Validate and fix syntax if requested
82
+ warnings = []
83
+ improvements_made = []
84
+
85
+ if validate:
86
+ print("🔍 Validating and improving LaTeX syntax...")
87
+ latex_content, validation_warnings, fixes = self._validate_and_fix_latex(
88
+ latex_content, request
89
+ )
90
+ warnings.extend(validation_warnings)
91
+ improvements_made.extend(fixes)
92
+
93
+ return LaTeXGenerationResult(
94
+ success=True,
95
+ latex_content=latex_content,
96
+ warnings=warnings,
97
+ improvements_made=improvements_made
98
+ )
99
+
100
+ def _generate_initial_latex(self, request: LaTeXGenerationRequest) -> str:
101
+ """Generate initial LaTeX document using Claude."""
102
+ # Build the generation prompt
103
+ prompt = self._build_generation_prompt(request)
104
+
105
+ try:
106
+ response = self.client.messages.create(
107
+ model="claude-sonnet-4-20250514",
108
+ max_tokens=16000, # Increased for complex documents with many figures
109
+ temperature=0.2, # Lower temperature for more consistent LaTeX
110
+ messages=[{
111
+ "role": "user",
112
+ "content": prompt
113
+ }]
114
+ )
115
+
116
+ # Extract LaTeX from response
117
+ latex_content = self._extract_latex_from_response(response.content[0].text)
118
+ print(f"✅ Generated {len(latex_content)} characters of LaTeX")
119
+ return latex_content
120
+
121
+ except Exception as e:
122
+ print(f"❌ Error generating LaTeX: {e}")
123
+ return ""
124
+
125
+ def _build_generation_prompt(self, request: LaTeXGenerationRequest) -> str:
126
+ """Build the prompt for LaTeX generation."""
127
+ # Prepare content sections summary
128
+ sections_summary = "\n".join([
129
+ f"- {sec.get('title', 'Untitled')}: {len(sec.get('content', ''))} characters"
130
+ for sec in request.content_sections
131
+ ])
132
+
133
+ # Prepare tables summary
134
+ tables_summary = "\n".join([
135
+ f"- {table.get('caption', 'Untitled table')}"
136
+ for table in request.tables
137
+ ]) if request.tables else "No tables"
138
+
139
+ # Prepare figures summary
140
+ figures_summary = "\n".join([
141
+ f"- {fig.get('caption', 'Untitled figure')}: {fig.get('path', 'no path')}"
142
+ for fig in request.figures
143
+ ]) if request.figures else "No figures"
144
+
145
+ # Build requirements
146
+ requirements_text = "\n".join([
147
+ f"- {req}" for req in request.requirements
148
+ ]) if request.requirements else "Standard research document formatting"
149
+
150
+ prompt = f"""You are a LaTeX document generation expert. Generate a complete, professional LaTeX document based on the following specifications.
151
+
152
+ **CRITICAL REQUIREMENTS:**
153
+ 1. Generate COMPLETE, VALID LaTeX that compiles without errors
154
+ 2. Use ONLY packages that are commonly available in TeX Live
155
+ 3. Escape ALL special LaTeX characters properly (%, $, &, #, _, {{, }}, etc.)
156
+ 4. Include proper document structure: preamble, \\begin{{document}}, content, \\end{{document}}
157
+ 5. Use proper spacing and formatting for readability
158
+ 6. Include table of contents if document has multiple sections
159
+ 7. Add page numbers and basic header/footer
160
+
161
+ **Document Specifications:**
162
+ Title: {request.title}
163
+ Author: {request.author}
164
+
165
+ **Content Sections:**
166
+ {sections_summary}
167
+
168
+ **Tables:**
169
+ {tables_summary}
170
+
171
+ **Figures:**
172
+ {figures_summary}
173
+
174
+ **Special Requirements:**
175
+ {requirements_text}
176
+
177
+ **Content Details:**
178
+ """
179
+
180
+ # Add detailed content for each section
181
+ for i, section in enumerate(request.content_sections, 1):
182
+ prompt += f"\n\n--- Section {i}: {section.get('title', 'Untitled')} ---\n"
183
+ prompt += section.get('content', '')
184
+
185
+ # Add table data
186
+ if request.tables:
187
+ prompt += "\n\n**Table Data:**\n"
188
+ for table in request.tables:
189
+ prompt += f"\nTable: {table.get('caption', 'Untitled')}\n"
190
+ prompt += f"Data: {json.dumps(table.get('data', []))}\n"
191
+
192
+ # Add figure information with explicit LaTeX code
193
+ if request.figures:
194
+ prompt += "\n\n**FIGURES - MUST INCLUDE ALL:**\n"
195
+ prompt += "You MUST include \\includegraphics for each figure listed below.\n\n"
196
+ for fig in request.figures:
197
+ default_width = '0.8\\textwidth'
198
+ path = fig.get('path', 'unknown')
199
+ caption = fig.get('caption', 'Untitled')
200
+ width = fig.get('width', default_width)
201
+ placement = fig.get('placement', '')
202
+ prompt += f"Figure: {caption}\n"
203
+ prompt += f" Placement hint: {placement}\n" if placement else ""
204
+ prompt += f" USE THIS EXACT CODE:\n"
205
+ prompt += f" \\begin{{figure}}[H]\n"
206
+ prompt += f" \\centering\n"
207
+ prompt += f" \\includegraphics[width={width}]{{{path}}}\n"
208
+ prompt += f" \\caption{{{caption}}}\n"
209
+ prompt += f" \\end{{figure}}\n\n"
210
+
211
+ prompt += """
212
+
213
+ **Output Instructions:**
214
+ Generate a COMPLETE LaTeX document with the following structure:
215
+
216
+ 1. Preamble with necessary packages (use standard packages only)
217
+ 2. Document metadata (title, author, date)
218
+ 3. \\begin{document}
219
+ 4. Title page with \\maketitle
220
+ 5. Table of contents (if multiple sections)
221
+ 6. All content sections with proper formatting
222
+ 7. All tables with proper booktabs formatting
223
+ 8. **ALL FIGURES using \\includegraphics - DO NOT SKIP ANY**
224
+ 9. \\end{document}
225
+
226
+ **CRITICAL - FIGURES:**
227
+ - You MUST include ALL figures listed above using \\includegraphics
228
+ - Use the EXACT paths provided (e.g., ../sample_content/magazine/images/filename.png)
229
+ - Include \\usepackage{graphicx} in the preamble
230
+ - Use [H] placement specifier (requires \\usepackage{float})
231
+
232
+ **IMPORTANT:**
233
+ - Escape special characters: % → \\%, $ → \\$, & → \\&, # → \\#, _ → \\_, { → \\{, } → \\}
234
+ - Use \\section{}, \\subsection{}, etc. for structure
235
+ - Use [H] placement for tables/figures to avoid floating issues
236
+ - Include \\usepackage{hyperref} for clickable links
237
+ - Include \\usepackage{graphicx} for images
238
+ - Include \\usepackage{float} for [H] placement
239
+
240
+ **ATTRIBUTION REQUIREMENT:**
241
+ - Include "Generated by DeepAgents PrintShop" attribution at the end of the document
242
+ - For magazines: Add it on the back cover or last page footer
243
+ - For reports: Add it as a small footer note on the last page
244
+ - Example: \\textit{{Generated by DeepAgents PrintShop}} or in a footnote
245
+
246
+ Return ONLY the complete LaTeX code, no explanations or markdown code blocks.
247
+ """
248
+
249
+ return prompt
250
+
251
+ def _extract_latex_from_response(self, response_text: str) -> str:
252
+ """Extract LaTeX code from Claude's response."""
253
+ # Remove markdown code blocks if present
254
+ if "```latex" in response_text:
255
+ start = response_text.find("```latex") + 8
256
+ end = response_text.find("```", start)
257
+ return response_text[start:end].strip()
258
+ elif "```" in response_text:
259
+ start = response_text.find("```") + 3
260
+ end = response_text.find("```", start)
261
+ return response_text[start:end].strip()
262
+ else:
263
+ return response_text.strip()
264
+
265
+ def _validate_and_fix_latex(self, latex_content: str,
266
+ request: LaTeXGenerationRequest) -> Tuple[str, List[str], List[str]]:
267
+ """
268
+ Validate LaTeX syntax and fix common issues using LLM reasoning.
269
+
270
+ Returns:
271
+ Tuple of (fixed_latex, warnings, improvements_made)
272
+ """
273
+ validation_prompt = f"""You are a LaTeX syntax validator and fixer. Analyze this LaTeX document and fix any issues.
274
+
275
+ **LaTeX Document to Validate:**
276
+ ```latex
277
+ {latex_content}
278
+ ```
279
+
280
+ **Validation Checklist:**
281
+ 1. Proper document structure (\\documentclass, \\begin{{document}}, \\end{{document}})
282
+ 2. All special characters properly escaped
283
+ 3. All environments properly closed
284
+ 4. Package usage is correct and packages exist
285
+ 5. No syntax errors
286
+ 6. Proper use of math mode
287
+ 7. Figure and table references are valid
288
+ 8. No orphaned braces or brackets
289
+
290
+ **Your Task:**
291
+ 1. Identify any syntax errors or issues
292
+ 2. Fix all issues while preserving the document's intent
293
+ 3. List what improvements you made
294
+
295
+ **Output Format:**
296
+ First, list any issues you found as JSON:
297
+ {{"issues": ["issue1", "issue2"]}}
298
+
299
+ Then provide the CORRECTED LaTeX code (complete document).
300
+ """
301
+
302
+ try:
303
+ response = self.client.messages.create(
304
+ model="claude-sonnet-4-20250514",
305
+ max_tokens=8000,
306
+ temperature=0.1, # Very low temperature for precise fixes
307
+ messages=[{
308
+ "role": "user",
309
+ "content": validation_prompt
310
+ }]
311
+ )
312
+
313
+ response_text = response.content[0].text
314
+
315
+ # Extract issues
316
+ warnings = []
317
+ if '"issues":' in response_text:
318
+ try:
319
+ start = response_text.find('{')
320
+ end = response_text.find('}', start) + 1
321
+ issues_json = json.loads(response_text[start:end])
322
+ warnings = issues_json.get('issues', [])
323
+ except:
324
+ warnings = ["Unable to parse validation issues"]
325
+
326
+ # Extract fixed LaTeX
327
+ fixed_latex = self._extract_latex_from_response(response_text)
328
+
329
+ # If extraction failed, return original
330
+ if not fixed_latex or len(fixed_latex) < len(latex_content) * 0.5:
331
+ print("⚠️ Validation fix failed, using original LaTeX")
332
+ return latex_content, warnings, []
333
+
334
+ improvements = [f"Fixed {len(warnings)} LaTeX issues"] if warnings else []
335
+ print(f"✅ Validated and fixed {len(warnings)} issues")
336
+
337
+ return fixed_latex, warnings, improvements
338
+
339
+ except Exception as e:
340
+ print(f"⚠️ Validation error: {e}, using original LaTeX")
341
+ return latex_content, [f"Validation failed: {str(e)}"], []
342
+
343
+ def apply_visual_qa_fixes(self, latex_content: str,
344
+ issues: List[str]) -> Tuple[str, bool, List[str]]:
345
+ """
346
+ Apply fixes to LaTeX based on Visual QA feedback using targeted patches.
347
+
348
+ Instead of regenerating the entire document (which causes truncation),
349
+ this method generates specific patches to apply to the preamble.
350
+
351
+ Args:
352
+ latex_content: Current LaTeX document
353
+ issues: List of issues found by Visual QA
354
+
355
+ Returns:
356
+ Tuple of (fixed_latex, success, fixes_applied)
357
+ """
358
+ print(f"🔧 Applying {len(issues)} Visual QA fixes to LaTeX...")
359
+
360
+ # Build the fix prompt - ask for PATCHES not full document
361
+ issues_text = "\n".join([f"- {issue}" for issue in issues])
362
+
363
+ # Extract just the preamble for context (much smaller)
364
+ begin_doc_pos = latex_content.find('\\begin{document}')
365
+ if begin_doc_pos == -1:
366
+ print("❌ Could not find \\begin{document}")
367
+ return latex_content, False, []
368
+
369
+ preamble = latex_content[:begin_doc_pos]
370
+
371
+ fix_prompt = f"""You are a LaTeX document improvement specialist. Generate ONLY the LaTeX commands needed to fix these visual issues.
372
+
373
+ **Current Preamble (for context):**
374
+ ```latex
375
+ {preamble[:3000]}...
376
+ ```
377
+
378
+ **Issues to Fix:**
379
+ {issues_text}
380
+
381
+ **Your Task:**
382
+ Generate a small block of LaTeX commands that should be INSERTED just before \\begin{{document}} to fix these issues.
383
+
384
+ **Available Fixes (use these patterns):**
385
+ - Table spacing: \\renewcommand{{\\arraystretch}}{{1.2}}
386
+ - Table column padding: \\setlength{{\\tabcolsep}}{{6pt}}
387
+ - Line spacing: \\linespread{{1.1}}
388
+ - Paragraph spacing: \\setlength{{\\parskip}}{{0.5em plus 0.1em minus 0.05em}}
389
+ - Header height: \\setlength{{\\headheight}}{{14.5pt}}
390
+ - Top margin adjustment: \\addtolength{{\\topmargin}}{{-2.5pt}}
391
+
392
+ **Rules:**
393
+ - Output ONLY the LaTeX commands to add (no explanations)
394
+ - Do NOT include \\documentclass, \\begin{{document}}, etc.
395
+ - Do NOT use microtype, setspace, or longtabu packages
396
+ - Keep it minimal - only what's needed for the issues
397
+ - If no fix is needed, output: % No fixes required
398
+
399
+ **Output Format:**
400
+ ```latex
401
+ % Visual QA Fixes
402
+ <your commands here>
403
+ ```
404
+ """
405
+
406
+ try:
407
+ response = self.client.messages.create(
408
+ model="claude-sonnet-4-20250514",
409
+ max_tokens=1000, # Small output - just patches
410
+ temperature=0.1,
411
+ messages=[{
412
+ "role": "user",
413
+ "content": fix_prompt
414
+ }]
415
+ )
416
+
417
+ response_text = response.content[0].text
418
+
419
+ # Extract LaTeX commands from response
420
+ if '```latex' in response_text:
421
+ start = response_text.find('```latex') + 8
422
+ end = response_text.find('```', start)
423
+ patch_content = response_text[start:end].strip()
424
+ elif '```' in response_text:
425
+ start = response_text.find('```') + 3
426
+ end = response_text.find('```', start)
427
+ patch_content = response_text[start:end].strip()
428
+ else:
429
+ patch_content = response_text.strip()
430
+
431
+ # Skip if no fixes needed
432
+ if not patch_content or 'No fixes required' in patch_content:
433
+ print("ℹ️ No Visual QA fixes needed")
434
+ return latex_content, True, ["No fixes required"]
435
+
436
+ # Validate patch doesn't contain dangerous commands
437
+ dangerous_patterns = ['\\documentclass', '\\begin{document}', '\\end{document}']
438
+ for pattern in dangerous_patterns:
439
+ if pattern in patch_content:
440
+ print(f"❌ Patch contains invalid command: {pattern}")
441
+ return latex_content, False, []
442
+
443
+ # Insert patch before \begin{document}
444
+ fixed_latex = (
445
+ latex_content[:begin_doc_pos] +
446
+ f"\n% Visual QA Fixes\n{patch_content}\n\n" +
447
+ latex_content[begin_doc_pos:]
448
+ )
449
+
450
+ # Validate the result
451
+ if fixed_latex.count('\\begin{') != fixed_latex.count('\\end{'):
452
+ print("❌ Fix created unmatched environments")
453
+ return latex_content, False, []
454
+
455
+ fixes_applied = [f"Applied patch: {patch_content[:100]}..."]
456
+ print(f"✅ Successfully applied Visual QA patch")
457
+
458
+ return fixed_latex, True, fixes_applied
459
+
460
+ except Exception as e:
461
+ print(f"❌ Error applying Visual QA fixes: {e}")
462
+ return latex_content, False, []
463
+
464
+ def complete_truncated_document(self, latex_content: str, max_attempts: int = 3) -> Tuple[str, bool]:
465
+ """
466
+ Complete a truncated LaTeX document by generating only the missing ending.
467
+
468
+ Instead of regenerating the entire document, this method:
469
+ 1. Sends only the last portion of the document for context
470
+ 2. Asks the LLM to complete from where it was cut off
471
+ 3. Appends the completion to the original
472
+
473
+ Args:
474
+ latex_content: Truncated LaTeX document (missing \\end{document})
475
+ max_attempts: Maximum completion attempts
476
+
477
+ Returns:
478
+ Tuple of (completed_latex, success)
479
+ """
480
+ print(f"🔧 Completing truncated document...")
481
+
482
+ # Take the last ~4000 characters for context
483
+ context_size = 4000
484
+ context_text = latex_content[-context_size:] if len(latex_content) > context_size else latex_content
485
+
486
+ # Find a good cut point - end of a complete line or environment
487
+ cut_point = len(latex_content)
488
+ for marker in ['\n\\end{', '\n\\section', '\n\\subsection', '\n\n']:
489
+ last_pos = latex_content.rfind(marker)
490
+ if last_pos > len(latex_content) - 2000 and last_pos > 0:
491
+ # Found a good cut point near the end
492
+ cut_point = last_pos + len(marker.split('\n')[0]) + 1 if '\n' in marker else last_pos
493
+ break
494
+
495
+ # If we found incomplete environments, cut before them
496
+ last_begin = latex_content.rfind('\\begin{')
497
+ if last_begin > cut_point - 500:
498
+ # There's an unclosed environment - cut before it
499
+ cut_point = last_begin
500
+
501
+ # Get the truncated portion we're keeping
502
+ keep_text = latex_content[:cut_point]
503
+ context_for_llm = keep_text[-context_size:] if len(keep_text) > context_size else keep_text
504
+
505
+ for attempt in range(1, max_attempts + 1):
506
+ print(f" Completion attempt {attempt}/{max_attempts}...")
507
+
508
+ completion_prompt = f"""You are completing a LaTeX document that was truncated mid-generation.
509
+
510
+ **END OF THE TRUNCATED DOCUMENT (last part before cut-off):**
511
+ ```latex
512
+ {context_for_llm}
513
+ ```
514
+
515
+ **Your Task:**
516
+ Generate ONLY the remaining content needed to properly end this document.
517
+
518
+ **Requirements:**
519
+ 1. Close any open environments (multicols, figure, tikzpicture, itemize, etc.)
520
+ 2. Add any remaining content sections if appropriate
521
+ 3. End with \\end{{document}}
522
+ 4. Make sure all braces and environments are balanced
523
+ 5. Keep it concise - just complete what's missing
524
+
525
+ **CRITICAL:**
526
+ - Do NOT repeat content that's already in the document
527
+ - Start your output from exactly where the document was cut off
528
+ - Include ONLY the completion, not the full document
529
+ - The output should seamlessly continue from the last line shown above
530
+
531
+ Return ONLY the LaTeX completion code, no explanations."""
532
+
533
+ try:
534
+ response = self.client.messages.create(
535
+ model="claude-sonnet-4-20250514",
536
+ max_tokens=4000, # Enough for completion, not full document
537
+ temperature=0.1,
538
+ messages=[{
539
+ "role": "user",
540
+ "content": completion_prompt
541
+ }]
542
+ )
543
+
544
+ completion = response.content[0].text.strip()
545
+
546
+ # Clean up the completion - remove markdown code blocks if present
547
+ if '```latex' in completion:
548
+ completion = completion.split('```latex', 1)[1]
549
+ if '```' in completion:
550
+ completion = completion.split('```')[0]
551
+ elif '```' in completion:
552
+ completion = completion.split('```')[1] if completion.startswith('```') else completion
553
+ if '```' in completion:
554
+ completion = completion.split('```')[0]
555
+
556
+ completion = completion.strip()
557
+
558
+ # Validate completion has \end{document}
559
+ if '\\end{document}' not in completion:
560
+ print(f" ❌ Attempt {attempt}: Completion missing \\end{{document}}")
561
+ continue
562
+
563
+ # Combine original (truncated to cut point) with completion
564
+ completed_latex = keep_text + '\n' + completion
565
+
566
+ # Verify the result has proper structure
567
+ if '\\begin{document}' in completed_latex and '\\end{document}' in completed_latex:
568
+ print(f" ✅ Document completed successfully ({len(completion)} chars added)")
569
+ return completed_latex, True
570
+
571
+ except Exception as e:
572
+ print(f" ❌ Attempt {attempt} failed: {e}")
573
+ continue
574
+
575
+ print(f"❌ Document completion failed after {max_attempts} attempts")
576
+ return latex_content, False
577
+
578
+ def self_correct_compilation_errors(self, latex_content: str,
579
+ compilation_error: str,
580
+ max_attempts: int = 3) -> Tuple[str, bool, List[str]]:
581
+ """
582
+ Self-correct LaTeX based on compilation errors using LLM reasoning.
583
+
584
+ This implements a feedback loop where the LLM:
585
+ 1. Receives the LaTeX that failed to compile
586
+ 2. Analyzes the compilation error
587
+ 3. Generates a corrected version
588
+ 4. Returns for re-compilation
589
+
590
+ Args:
591
+ latex_content: LaTeX that failed to compile
592
+ compilation_error: Error message from pdflatex
593
+ max_attempts: Maximum self-correction attempts
594
+
595
+ Returns:
596
+ Tuple of (corrected_latex, success, corrections_made)
597
+ """
598
+ print(f"🤖 LLM Self-Correction: Analyzing compilation error...")
599
+
600
+ corrections_made = []
601
+ current_latex = latex_content
602
+
603
+ for attempt in range(1, max_attempts + 1):
604
+ print(f" Attempt {attempt}/{max_attempts}: Analyzing error...")
605
+
606
+ correction_prompt = f"""You are a LaTeX debugging expert. A LaTeX document failed to compile and you need to fix it.
607
+
608
+ **LaTeX Document (FAILED TO COMPILE):**
609
+ ```latex
610
+ {current_latex}
611
+ ```
612
+
613
+ **Compilation Error:**
614
+ ```
615
+ {compilation_error}
616
+ ```
617
+
618
+ **Your Task:**
619
+ 1. **Analyze the error carefully** - understand what went wrong
620
+ 2. **Identify the root cause** - is it a package issue, syntax error, or incompatibility?
621
+ 3. **Generate a corrected version** that will compile successfully
622
+ 4. **Use ONLY reliable, standard LaTeX techniques**
623
+
624
+ **Common Error Fixes:**
625
+ - "auto expansion is only possible with scalable fonts" → REMOVE microtype package or disable expansion
626
+ - "File X.sty not found" → REMOVE that package and use alternative approach
627
+ - "Missing \\begin{{document}}" → Fix document structure
628
+ - "Too many }}" or "Missing }}" → Fix brace matching
629
+ - Package conflicts → Remove conflicting packages
630
+
631
+ **Critical Rules:**
632
+ - If a package causes errors, REMOVE it entirely and use manual commands instead
633
+ - If microtype fails, remove it and use \\linespread{{}} for spacing
634
+ - If setspace fails, use \\setlength{{\\baselineskip}}{{}} instead
635
+ - Preserve ALL document content
636
+ - Focus on making it COMPILE, not perfection
637
+ - Use simple, proven LaTeX commands
638
+
639
+ **IMPORTANT: The corrected LaTeX MUST compile without errors.**
640
+
641
+ Return ONLY the COMPLETE CORRECTED LaTeX document, no explanations.
642
+ """
643
+
644
+ try:
645
+ response = self.client.messages.create(
646
+ model="claude-sonnet-4-20250514",
647
+ max_tokens=8000,
648
+ temperature=0.1,
649
+ messages=[{
650
+ "role": "user",
651
+ "content": correction_prompt
652
+ }]
653
+ )
654
+
655
+ corrected_latex = self._extract_latex_from_response(response.content[0].text)
656
+
657
+ # Validate correction
658
+ if not corrected_latex or len(corrected_latex) < len(latex_content) * 0.5:
659
+ print(f" ❌ Attempt {attempt} generated invalid LaTeX")
660
+ continue
661
+
662
+ # Check basic structure
663
+ if '\\begin{document}' not in corrected_latex or '\\end{document}' not in corrected_latex:
664
+ print(f" ❌ Attempt {attempt} missing document structure")
665
+ continue
666
+
667
+ corrections_made.append(f"Attempt {attempt}: Fixed compilation error")
668
+ print(f" ✅ Attempt {attempt}: Generated corrected LaTeX")
669
+
670
+ return corrected_latex, True, corrections_made
671
+
672
+ except Exception as e:
673
+ print(f" ❌ Attempt {attempt} failed: {e}")
674
+ continue
675
+
676
+ # All attempts failed
677
+ print(f"❌ Self-correction failed after {max_attempts} attempts")
678
+ return latex_content, False, corrections_made