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
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)}"