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.
- agents/content_editor/__init__.py +1 -0
- agents/content_editor/agent.py +279 -0
- agents/content_editor/content_reviewer.py +327 -0
- agents/content_editor/versioned_agent.py +455 -0
- agents/latex_specialist/__init__.py +1 -0
- agents/latex_specialist/agent.py +531 -0
- agents/latex_specialist/latex_analyzer.py +510 -0
- agents/latex_specialist/latex_optimizer.py +1192 -0
- agents/qa_orchestrator/__init__.py +1 -0
- agents/qa_orchestrator/agent.py +603 -0
- agents/qa_orchestrator/langgraph_workflow.py +733 -0
- agents/qa_orchestrator/pipeline_types.py +72 -0
- agents/qa_orchestrator/quality_gates.py +495 -0
- agents/qa_orchestrator/workflow_coordinator.py +139 -0
- agents/research_agent/__init__.py +1 -0
- agents/research_agent/agent.py +258 -0
- agents/research_agent/llm_report_generator.py +1023 -0
- agents/research_agent/report_generator.py +536 -0
- agents/visual_qa/__init__.py +1 -0
- agents/visual_qa/agent.py +410 -0
- deepagents_printshop-0.1.0.dist-info/METADATA +744 -0
- deepagents_printshop-0.1.0.dist-info/RECORD +37 -0
- deepagents_printshop-0.1.0.dist-info/WHEEL +4 -0
- deepagents_printshop-0.1.0.dist-info/entry_points.txt +2 -0
- deepagents_printshop-0.1.0.dist-info/licenses/LICENSE +86 -0
- tools/__init__.py +1 -0
- tools/change_tracker.py +419 -0
- tools/content_type_loader.py +171 -0
- tools/graph_generator.py +281 -0
- tools/latex_generator.py +374 -0
- tools/llm_latex_generator.py +678 -0
- tools/magazine_layout.py +462 -0
- tools/pattern_injector.py +250 -0
- tools/pattern_learner.py +477 -0
- tools/pdf_compiler.py +386 -0
- tools/version_manager.py +346 -0
- 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
|