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
tools/pdf_compiler.py
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"""PDF compiler for LaTeX documents with intelligent error correction."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional, Tuple, List, Dict
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PDFCompiler:
|
|
11
|
+
"""Compile LaTeX documents to PDF using pdflatex."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, output_dir: Optional[str] = None):
|
|
14
|
+
"""
|
|
15
|
+
Initialize the PDF compiler.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
output_dir: Directory for output files. If None, uses the same dir as input.
|
|
19
|
+
"""
|
|
20
|
+
self.output_dir = output_dir
|
|
21
|
+
self.error_patterns = self._init_error_patterns()
|
|
22
|
+
|
|
23
|
+
def _init_error_patterns(self) -> Dict[str, Dict[str, str]]:
|
|
24
|
+
"""Initialize common LaTeX error patterns and their fixes."""
|
|
25
|
+
return {
|
|
26
|
+
'misplaced_noalign': {
|
|
27
|
+
'pattern': r'Misplaced \\noalign',
|
|
28
|
+
'description': 'Table rule commands like \\midrule used outside table context',
|
|
29
|
+
'fix': 'move_table_rules_inside_tabular'
|
|
30
|
+
},
|
|
31
|
+
'undefined_control_sequence': {
|
|
32
|
+
'pattern': r'Undefined control sequence.*\\(\w+)',
|
|
33
|
+
'description': 'Unknown command used',
|
|
34
|
+
'fix': 'add_missing_package_or_fix_command'
|
|
35
|
+
},
|
|
36
|
+
'missing_end_group': {
|
|
37
|
+
'pattern': r'Missing } inserted',
|
|
38
|
+
'description': 'Unmatched braces',
|
|
39
|
+
'fix': 'balance_braces'
|
|
40
|
+
},
|
|
41
|
+
'runaway_argument': {
|
|
42
|
+
'pattern': r'Runaway argument',
|
|
43
|
+
'description': 'Missing closing brace in command argument',
|
|
44
|
+
'fix': 'fix_runaway_argument'
|
|
45
|
+
},
|
|
46
|
+
'extra_alignment_tab': {
|
|
47
|
+
'pattern': r'Extra alignment tab has been changed to \\cr',
|
|
48
|
+
'description': 'Too many & in table row',
|
|
49
|
+
'fix': 'fix_table_alignment'
|
|
50
|
+
},
|
|
51
|
+
'missing_number': {
|
|
52
|
+
'pattern': r'Missing number, treated as zero',
|
|
53
|
+
'description': 'Invalid numeric value in length or counter',
|
|
54
|
+
'fix': 'fix_numeric_values'
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
def compile(self, tex_file: str, runs: int = 2, max_fix_attempts: int = 3) -> Tuple[bool, str]:
|
|
59
|
+
"""
|
|
60
|
+
Compile a LaTeX file to PDF with automatic error correction.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
tex_file: Path to the .tex file
|
|
64
|
+
runs: Number of compilation runs (default 2 for proper references)
|
|
65
|
+
max_fix_attempts: Maximum number of error correction attempts
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Tuple of (success: bool, message: str)
|
|
69
|
+
"""
|
|
70
|
+
tex_path = Path(tex_file)
|
|
71
|
+
|
|
72
|
+
if not tex_path.exists():
|
|
73
|
+
return False, f"LaTeX file not found: {tex_file}"
|
|
74
|
+
|
|
75
|
+
# Determine output directory
|
|
76
|
+
if self.output_dir:
|
|
77
|
+
output_path = Path(self.output_dir)
|
|
78
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
else:
|
|
80
|
+
output_path = tex_path.parent
|
|
81
|
+
|
|
82
|
+
# Try compilation with error correction
|
|
83
|
+
for attempt in range(max_fix_attempts + 1):
|
|
84
|
+
try:
|
|
85
|
+
success, message = self._attempt_compilation(tex_path, output_path, runs)
|
|
86
|
+
|
|
87
|
+
if success:
|
|
88
|
+
return True, message
|
|
89
|
+
|
|
90
|
+
# If this was the last attempt, return failure
|
|
91
|
+
if attempt == max_fix_attempts:
|
|
92
|
+
return False, f"Compilation failed after {max_fix_attempts} fix attempts:\n{message}"
|
|
93
|
+
|
|
94
|
+
# Try to fix the error and continue
|
|
95
|
+
print(f"Compilation attempt {attempt + 1} failed. Attempting to fix errors...")
|
|
96
|
+
fixed = self._auto_fix_latex_errors(tex_path, message)
|
|
97
|
+
|
|
98
|
+
if not fixed:
|
|
99
|
+
return False, f"Could not automatically fix LaTeX errors:\n{message}"
|
|
100
|
+
|
|
101
|
+
except subprocess.TimeoutExpired:
|
|
102
|
+
return False, "Compilation timed out (60s limit exceeded)"
|
|
103
|
+
except FileNotFoundError:
|
|
104
|
+
return False, "pdflatex not found. Please install TeX Live or MiKTeX."
|
|
105
|
+
except Exception as e:
|
|
106
|
+
return False, f"Compilation error: {str(e)}"
|
|
107
|
+
|
|
108
|
+
return False, "Maximum fix attempts exceeded"
|
|
109
|
+
|
|
110
|
+
def _attempt_compilation(self, tex_path: Path, output_path: Path, runs: int) -> Tuple[bool, str]:
|
|
111
|
+
"""Attempt to compile the LaTeX document."""
|
|
112
|
+
for run in range(runs):
|
|
113
|
+
result = subprocess.run(
|
|
114
|
+
[
|
|
115
|
+
'pdflatex',
|
|
116
|
+
'-interaction=nonstopmode',
|
|
117
|
+
'-output-directory', str(output_path),
|
|
118
|
+
str(tex_path)
|
|
119
|
+
],
|
|
120
|
+
capture_output=True,
|
|
121
|
+
text=True,
|
|
122
|
+
timeout=60
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if result.returncode != 0:
|
|
126
|
+
return False, f"Compilation failed on run {run + 1}:\n{result.stdout}\n{result.stderr}"
|
|
127
|
+
|
|
128
|
+
pdf_file = output_path / f"{tex_path.stem}.pdf"
|
|
129
|
+
|
|
130
|
+
if pdf_file.exists():
|
|
131
|
+
# Clean up auxiliary files
|
|
132
|
+
self._cleanup_aux_files(output_path, tex_path.stem)
|
|
133
|
+
return True, f"PDF successfully created: {pdf_file}"
|
|
134
|
+
else:
|
|
135
|
+
return False, "PDF file was not created"
|
|
136
|
+
|
|
137
|
+
def _cleanup_aux_files(self, output_dir: Path, basename: str, keep_log: bool = False):
|
|
138
|
+
"""Clean up auxiliary LaTeX files."""
|
|
139
|
+
extensions = ['.aux', '.toc', '.out', '.nav', '.snm', '.vrb']
|
|
140
|
+
if not keep_log:
|
|
141
|
+
extensions.append('.log')
|
|
142
|
+
|
|
143
|
+
for ext in extensions:
|
|
144
|
+
aux_file = output_dir / f"{basename}{ext}"
|
|
145
|
+
if aux_file.exists():
|
|
146
|
+
try:
|
|
147
|
+
aux_file.unlink()
|
|
148
|
+
except Exception:
|
|
149
|
+
pass # Ignore cleanup errors
|
|
150
|
+
|
|
151
|
+
def _auto_fix_latex_errors(self, tex_path: Path, error_message: str) -> bool:
|
|
152
|
+
"""Automatically fix common LaTeX errors."""
|
|
153
|
+
try:
|
|
154
|
+
# Read the current file content
|
|
155
|
+
with open(tex_path, 'r', encoding='utf-8') as f:
|
|
156
|
+
content = f.read()
|
|
157
|
+
|
|
158
|
+
original_content = content
|
|
159
|
+
|
|
160
|
+
# Check each error pattern and apply fixes
|
|
161
|
+
for error_type, error_info in self.error_patterns.items():
|
|
162
|
+
if re.search(error_info['pattern'], error_message, re.IGNORECASE):
|
|
163
|
+
print(f"Detected error type: {error_type} - {error_info['description']}")
|
|
164
|
+
content = self._apply_fix(content, error_type, error_info['fix'])
|
|
165
|
+
|
|
166
|
+
# If content was modified, write it back
|
|
167
|
+
if content != original_content:
|
|
168
|
+
with open(tex_path, 'w', encoding='utf-8') as f:
|
|
169
|
+
f.write(content)
|
|
170
|
+
print(f"Applied fixes to {tex_path}")
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
except Exception as e:
|
|
176
|
+
print(f"Error during auto-fix: {str(e)}")
|
|
177
|
+
return False
|
|
178
|
+
|
|
179
|
+
def _apply_fix(self, content: str, error_type: str, fix_method: str) -> str:
|
|
180
|
+
"""Apply specific fixes based on error type."""
|
|
181
|
+
if fix_method == 'move_table_rules_inside_tabular':
|
|
182
|
+
return self._fix_misplaced_table_rules(content)
|
|
183
|
+
elif fix_method == 'balance_braces':
|
|
184
|
+
return self._fix_unbalanced_braces(content)
|
|
185
|
+
elif fix_method == 'fix_table_alignment':
|
|
186
|
+
return self._fix_table_alignment(content)
|
|
187
|
+
elif fix_method == 'add_missing_package_or_fix_command':
|
|
188
|
+
return self._fix_undefined_commands(content)
|
|
189
|
+
# Add more fix methods as needed
|
|
190
|
+
return content
|
|
191
|
+
|
|
192
|
+
def _fix_misplaced_table_rules(self, content: str) -> str:
|
|
193
|
+
"""Fix misplaced table rules like \midrule."""
|
|
194
|
+
# Look for table environments and ensure rules are properly placed
|
|
195
|
+
pattern = r'(\\begin\{tabular\}[^}]*\}[^\\]*)(\\toprule.*?)(\\end\{tabular\})'
|
|
196
|
+
|
|
197
|
+
def fix_table(match):
|
|
198
|
+
table_start = match.group(1)
|
|
199
|
+
table_content = match.group(2)
|
|
200
|
+
table_end = match.group(3)
|
|
201
|
+
|
|
202
|
+
# Ensure proper structure: \toprule -> headers -> \midrule -> data -> \bottomrule
|
|
203
|
+
lines = table_content.split('\n')
|
|
204
|
+
fixed_lines = []
|
|
205
|
+
|
|
206
|
+
for line in lines:
|
|
207
|
+
stripped = line.strip()
|
|
208
|
+
# Fix percentage signs that need escaping
|
|
209
|
+
if '(%)' in stripped and not '(\\%)' in stripped:
|
|
210
|
+
line = line.replace('(%)', '(\\%)')
|
|
211
|
+
fixed_lines.append(line)
|
|
212
|
+
|
|
213
|
+
return table_start + '\n'.join(fixed_lines) + table_end
|
|
214
|
+
|
|
215
|
+
return re.sub(pattern, fix_table, content, flags=re.DOTALL)
|
|
216
|
+
|
|
217
|
+
def _fix_unbalanced_braces(self, content: str) -> str:
|
|
218
|
+
"""Attempt to fix unbalanced braces."""
|
|
219
|
+
# Simple brace balancing - count and add missing closing braces at end of problematic lines
|
|
220
|
+
lines = content.split('\n')
|
|
221
|
+
fixed_lines = []
|
|
222
|
+
|
|
223
|
+
for line in lines:
|
|
224
|
+
open_braces = line.count('{')
|
|
225
|
+
close_braces = line.count('}')
|
|
226
|
+
|
|
227
|
+
if open_braces > close_braces:
|
|
228
|
+
# Add missing closing braces
|
|
229
|
+
missing = open_braces - close_braces
|
|
230
|
+
line += '}' * missing
|
|
231
|
+
|
|
232
|
+
fixed_lines.append(line)
|
|
233
|
+
|
|
234
|
+
return '\n'.join(fixed_lines)
|
|
235
|
+
|
|
236
|
+
def _fix_table_alignment(self, content: str) -> str:
|
|
237
|
+
"""Fix table alignment issues (too many &)."""
|
|
238
|
+
# Find tabular environments and fix column alignment
|
|
239
|
+
pattern = r'(\\begin\{tabular\}\{)([^}]+)(\}.*?\\end\{tabular\})'
|
|
240
|
+
|
|
241
|
+
def fix_alignment(match):
|
|
242
|
+
start = match.group(1)
|
|
243
|
+
alignment = match.group(2)
|
|
244
|
+
table_body = match.group(3)
|
|
245
|
+
|
|
246
|
+
# Count actual columns used in table rows
|
|
247
|
+
rows = [line.strip() for line in table_body.split('\n')
|
|
248
|
+
if '&' in line and not line.strip().startswith('%')]
|
|
249
|
+
|
|
250
|
+
if rows:
|
|
251
|
+
max_cols = max(line.count('&') + 1 for line in rows)
|
|
252
|
+
# Ensure alignment spec matches actual columns
|
|
253
|
+
if len(alignment) < max_cols:
|
|
254
|
+
alignment = alignment * (max_cols // len(alignment) + 1)
|
|
255
|
+
alignment = alignment[:max_cols]
|
|
256
|
+
|
|
257
|
+
return start + alignment + table_body
|
|
258
|
+
|
|
259
|
+
return re.sub(pattern, fix_alignment, content, flags=re.DOTALL)
|
|
260
|
+
|
|
261
|
+
def _fix_undefined_commands(self, content: str) -> str:
|
|
262
|
+
"""Fix undefined commands by adding common packages."""
|
|
263
|
+
# Add common packages that might be missing
|
|
264
|
+
packages_to_add = []
|
|
265
|
+
|
|
266
|
+
if '\\cite' in content and '\\usepackage{cite}' not in content:
|
|
267
|
+
packages_to_add.append('\\usepackage{cite}')
|
|
268
|
+
|
|
269
|
+
if '\\url' in content and '\\usepackage{url}' not in content:
|
|
270
|
+
packages_to_add.append('\\usepackage{url}')
|
|
271
|
+
|
|
272
|
+
if packages_to_add:
|
|
273
|
+
# Find where to insert packages (after \documentclass)
|
|
274
|
+
doc_class_match = re.search(r'(\\documentclass.*?\n)', content)
|
|
275
|
+
if doc_class_match:
|
|
276
|
+
insertion_point = doc_class_match.end()
|
|
277
|
+
package_block = '\n'.join(packages_to_add) + '\n'
|
|
278
|
+
content = content[:insertion_point] + package_block + content[insertion_point:]
|
|
279
|
+
|
|
280
|
+
return content
|
|
281
|
+
|
|
282
|
+
def compile_with_bibliography(self, tex_file: str, bib_style: str = "plain") -> Tuple[bool, str]:
|
|
283
|
+
"""
|
|
284
|
+
Compile a LaTeX document with bibliography.
|
|
285
|
+
|
|
286
|
+
Runs: pdflatex -> bibtex -> pdflatex -> pdflatex
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
tex_file: Path to the .tex file
|
|
290
|
+
bib_style: Bibliography style (plain, alpha, abbrv, etc.)
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Tuple of (success: bool, message: str)
|
|
294
|
+
"""
|
|
295
|
+
tex_path = Path(tex_file)
|
|
296
|
+
|
|
297
|
+
if not tex_path.exists():
|
|
298
|
+
return False, f"LaTeX file not found: {tex_file}"
|
|
299
|
+
|
|
300
|
+
# Determine output directory
|
|
301
|
+
if self.output_dir:
|
|
302
|
+
output_path = Path(self.output_dir)
|
|
303
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
304
|
+
else:
|
|
305
|
+
output_path = tex_path.parent
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
# First pdflatex run
|
|
309
|
+
result = subprocess.run(
|
|
310
|
+
['pdflatex', '-interaction=nonstopmode',
|
|
311
|
+
'-output-directory', str(output_path), str(tex_path)],
|
|
312
|
+
capture_output=True, text=True, timeout=60
|
|
313
|
+
)
|
|
314
|
+
if result.returncode != 0:
|
|
315
|
+
return False, f"First pdflatex run failed:\n{result.stderr}"
|
|
316
|
+
|
|
317
|
+
# Run bibtex
|
|
318
|
+
aux_file = output_path / f"{tex_path.stem}.aux"
|
|
319
|
+
if aux_file.exists():
|
|
320
|
+
result = subprocess.run(
|
|
321
|
+
['bibtex', str(aux_file)],
|
|
322
|
+
capture_output=True, text=True, timeout=30,
|
|
323
|
+
cwd=str(output_path)
|
|
324
|
+
)
|
|
325
|
+
# Bibtex may return non-zero even on success, check output
|
|
326
|
+
|
|
327
|
+
# Second pdflatex run
|
|
328
|
+
result = subprocess.run(
|
|
329
|
+
['pdflatex', '-interaction=nonstopmode',
|
|
330
|
+
'-output-directory', str(output_path), str(tex_path)],
|
|
331
|
+
capture_output=True, text=True, timeout=60
|
|
332
|
+
)
|
|
333
|
+
if result.returncode != 0:
|
|
334
|
+
return False, f"Second pdflatex run failed:\n{result.stderr}"
|
|
335
|
+
|
|
336
|
+
# Third pdflatex run
|
|
337
|
+
result = subprocess.run(
|
|
338
|
+
['pdflatex', '-interaction=nonstopmode',
|
|
339
|
+
'-output-directory', str(output_path), str(tex_path)],
|
|
340
|
+
capture_output=True, text=True, timeout=60
|
|
341
|
+
)
|
|
342
|
+
if result.returncode != 0:
|
|
343
|
+
return False, f"Third pdflatex run failed:\n{result.stderr}"
|
|
344
|
+
|
|
345
|
+
pdf_file = output_path / f"{tex_path.stem}.pdf"
|
|
346
|
+
|
|
347
|
+
if pdf_file.exists():
|
|
348
|
+
self._cleanup_aux_files(output_path, tex_path.stem)
|
|
349
|
+
# Also clean .bbl and .blg
|
|
350
|
+
for ext in ['.bbl', '.blg']:
|
|
351
|
+
f = output_path / f"{tex_path.stem}{ext}"
|
|
352
|
+
if f.exists():
|
|
353
|
+
try:
|
|
354
|
+
f.unlink()
|
|
355
|
+
except Exception:
|
|
356
|
+
pass
|
|
357
|
+
|
|
358
|
+
return True, f"PDF with bibliography successfully created: {pdf_file}"
|
|
359
|
+
else:
|
|
360
|
+
return False, "PDF file was not created"
|
|
361
|
+
|
|
362
|
+
except subprocess.TimeoutExpired:
|
|
363
|
+
return False, "Compilation timed out"
|
|
364
|
+
except FileNotFoundError as e:
|
|
365
|
+
return False, f"Required tool not found: {str(e)}"
|
|
366
|
+
except Exception as e:
|
|
367
|
+
return False, f"Compilation error: {str(e)}"
|
|
368
|
+
|
|
369
|
+
def validate_latex_installation(self) -> Tuple[bool, str]:
|
|
370
|
+
"""Check if LaTeX is properly installed."""
|
|
371
|
+
try:
|
|
372
|
+
result = subprocess.run(
|
|
373
|
+
['pdflatex', '--version'],
|
|
374
|
+
capture_output=True,
|
|
375
|
+
text=True,
|
|
376
|
+
timeout=5
|
|
377
|
+
)
|
|
378
|
+
if result.returncode == 0:
|
|
379
|
+
version_line = result.stdout.split('\n')[0]
|
|
380
|
+
return True, f"LaTeX installed: {version_line}"
|
|
381
|
+
else:
|
|
382
|
+
return False, "pdflatex found but returned an error"
|
|
383
|
+
except FileNotFoundError:
|
|
384
|
+
return False, "pdflatex not found. Please install TeX Live or MiKTeX."
|
|
385
|
+
except Exception as e:
|
|
386
|
+
return False, f"Error checking LaTeX installation: {str(e)}"
|