quantalogic 0.35.0__py3-none-any.whl → 0.50.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 (107) hide show
  1. quantalogic/__init__.py +0 -4
  2. quantalogic/agent.py +603 -363
  3. quantalogic/agent_config.py +233 -46
  4. quantalogic/agent_factory.py +34 -22
  5. quantalogic/coding_agent.py +16 -14
  6. quantalogic/config.py +2 -1
  7. quantalogic/console_print_events.py +4 -8
  8. quantalogic/console_print_token.py +2 -2
  9. quantalogic/docs_cli.py +15 -10
  10. quantalogic/event_emitter.py +258 -83
  11. quantalogic/flow/__init__.py +23 -0
  12. quantalogic/flow/flow.py +595 -0
  13. quantalogic/flow/flow_extractor.py +672 -0
  14. quantalogic/flow/flow_generator.py +89 -0
  15. quantalogic/flow/flow_manager.py +407 -0
  16. quantalogic/flow/flow_manager_schema.py +169 -0
  17. quantalogic/flow/flow_yaml.md +419 -0
  18. quantalogic/generative_model.py +109 -77
  19. quantalogic/get_model_info.py +5 -5
  20. quantalogic/interactive_text_editor.py +100 -73
  21. quantalogic/main.py +17 -21
  22. quantalogic/model_info_list.py +3 -3
  23. quantalogic/model_info_litellm.py +14 -14
  24. quantalogic/prompts.py +2 -1
  25. quantalogic/{llm.py → quantlitellm.py} +29 -39
  26. quantalogic/search_agent.py +4 -4
  27. quantalogic/server/models.py +4 -1
  28. quantalogic/task_file_reader.py +5 -5
  29. quantalogic/task_runner.py +20 -20
  30. quantalogic/tool_manager.py +10 -21
  31. quantalogic/tools/__init__.py +98 -68
  32. quantalogic/tools/composio/composio.py +416 -0
  33. quantalogic/tools/{generate_database_report_tool.py → database/generate_database_report_tool.py} +4 -9
  34. quantalogic/tools/database/sql_query_tool_advanced.py +261 -0
  35. quantalogic/tools/document_tools/markdown_to_docx_tool.py +620 -0
  36. quantalogic/tools/document_tools/markdown_to_epub_tool.py +438 -0
  37. quantalogic/tools/document_tools/markdown_to_html_tool.py +362 -0
  38. quantalogic/tools/document_tools/markdown_to_ipynb_tool.py +319 -0
  39. quantalogic/tools/document_tools/markdown_to_latex_tool.py +420 -0
  40. quantalogic/tools/document_tools/markdown_to_pdf_tool.py +623 -0
  41. quantalogic/tools/document_tools/markdown_to_pptx_tool.py +319 -0
  42. quantalogic/tools/duckduckgo_search_tool.py +2 -4
  43. quantalogic/tools/finance/alpha_vantage_tool.py +440 -0
  44. quantalogic/tools/finance/ccxt_tool.py +373 -0
  45. quantalogic/tools/finance/finance_llm_tool.py +387 -0
  46. quantalogic/tools/finance/google_finance.py +192 -0
  47. quantalogic/tools/finance/market_intelligence_tool.py +520 -0
  48. quantalogic/tools/finance/technical_analysis_tool.py +491 -0
  49. quantalogic/tools/finance/tradingview_tool.py +336 -0
  50. quantalogic/tools/finance/yahoo_finance.py +236 -0
  51. quantalogic/tools/git/bitbucket_clone_repo_tool.py +181 -0
  52. quantalogic/tools/git/bitbucket_operations_tool.py +326 -0
  53. quantalogic/tools/git/clone_repo_tool.py +189 -0
  54. quantalogic/tools/git/git_operations_tool.py +532 -0
  55. quantalogic/tools/google_packages/google_news_tool.py +480 -0
  56. quantalogic/tools/grep_app_tool.py +123 -186
  57. quantalogic/tools/{dalle_e.py → image_generation/dalle_e.py} +37 -27
  58. quantalogic/tools/jinja_tool.py +6 -10
  59. quantalogic/tools/language_handlers/__init__.py +22 -9
  60. quantalogic/tools/list_directory_tool.py +131 -42
  61. quantalogic/tools/llm_tool.py +45 -15
  62. quantalogic/tools/llm_vision_tool.py +59 -7
  63. quantalogic/tools/markitdown_tool.py +17 -5
  64. quantalogic/tools/nasa_packages/models.py +47 -0
  65. quantalogic/tools/nasa_packages/nasa_apod_tool.py +232 -0
  66. quantalogic/tools/nasa_packages/nasa_neows_tool.py +147 -0
  67. quantalogic/tools/nasa_packages/services.py +82 -0
  68. quantalogic/tools/presentation_tools/presentation_llm_tool.py +396 -0
  69. quantalogic/tools/product_hunt/product_hunt_tool.py +258 -0
  70. quantalogic/tools/product_hunt/services.py +63 -0
  71. quantalogic/tools/rag_tool/__init__.py +48 -0
  72. quantalogic/tools/rag_tool/document_metadata.py +15 -0
  73. quantalogic/tools/rag_tool/query_response.py +20 -0
  74. quantalogic/tools/rag_tool/rag_tool.py +566 -0
  75. quantalogic/tools/rag_tool/rag_tool_beta.py +264 -0
  76. quantalogic/tools/read_html_tool.py +24 -38
  77. quantalogic/tools/replace_in_file_tool.py +10 -10
  78. quantalogic/tools/safe_python_interpreter_tool.py +10 -24
  79. quantalogic/tools/search_definition_names.py +2 -2
  80. quantalogic/tools/sequence_tool.py +14 -23
  81. quantalogic/tools/sql_query_tool.py +17 -19
  82. quantalogic/tools/tool.py +39 -15
  83. quantalogic/tools/unified_diff_tool.py +1 -1
  84. quantalogic/tools/utilities/csv_processor_tool.py +234 -0
  85. quantalogic/tools/utilities/download_file_tool.py +179 -0
  86. quantalogic/tools/utilities/mermaid_validator_tool.py +661 -0
  87. quantalogic/tools/utils/__init__.py +1 -4
  88. quantalogic/tools/utils/create_sample_database.py +24 -38
  89. quantalogic/tools/utils/generate_database_report.py +74 -82
  90. quantalogic/tools/wikipedia_search_tool.py +17 -21
  91. quantalogic/utils/ask_user_validation.py +1 -1
  92. quantalogic/utils/async_utils.py +35 -0
  93. quantalogic/utils/check_version.py +3 -5
  94. quantalogic/utils/get_all_models.py +2 -1
  95. quantalogic/utils/git_ls.py +21 -7
  96. quantalogic/utils/lm_studio_model_info.py +9 -7
  97. quantalogic/utils/python_interpreter.py +113 -43
  98. quantalogic/utils/xml_utility.py +178 -0
  99. quantalogic/version_check.py +1 -1
  100. quantalogic/welcome_message.py +7 -7
  101. quantalogic/xml_parser.py +0 -1
  102. {quantalogic-0.35.0.dist-info → quantalogic-0.50.0.dist-info}/METADATA +40 -1
  103. quantalogic-0.50.0.dist-info/RECORD +148 -0
  104. quantalogic-0.35.0.dist-info/RECORD +0 -102
  105. {quantalogic-0.35.0.dist-info → quantalogic-0.50.0.dist-info}/LICENSE +0 -0
  106. {quantalogic-0.35.0.dist-info → quantalogic-0.50.0.dist-info}/WHEEL +0 -0
  107. {quantalogic-0.35.0.dist-info → quantalogic-0.50.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,623 @@
1
+ """Tool for converting markdown content to well-structured PDF documents.
2
+
3
+ Why this tool:
4
+ - Provides a standardized way to convert markdown to professional PDF documents
5
+ - Maintains consistent styling and formatting across documents
6
+ - Handles complex elements like diagrams, code blocks, and tables
7
+ - Supports customization through style configurations and templates
8
+ """
9
+
10
+ import json
11
+ import os
12
+ from pathlib import Path
13
+ from typing import ClassVar, Dict, List, Optional, Union
14
+
15
+ import markdown
16
+ import mermaid
17
+ from bs4 import BeautifulSoup, Tag
18
+ from loguru import logger
19
+ from pygments import highlight
20
+ from pygments.formatters import HtmlFormatter
21
+ from pygments.lexers import TextLexer, get_lexer_by_name
22
+ from weasyprint import CSS, HTML
23
+ from weasyprint.text.fonts import FontConfiguration
24
+
25
+ from quantalogic.tools.tool import Tool, ToolArgument
26
+
27
+
28
+ class MarkdownToPdfTool(Tool):
29
+ """Converts markdown to professional PDF documents with advanced formatting."""
30
+
31
+ model_config = {
32
+ "arbitrary_types_allowed": True
33
+ }
34
+
35
+ name: str = "markdown_to_pdf_tool"
36
+ description: str = (
37
+ "Converts markdown to PDF with support for images, Mermaid diagrams, "
38
+ "code blocks, tables, and advanced formatting."
39
+ )
40
+ need_validation: bool = False
41
+
42
+ arguments: List[ToolArgument] = [
43
+ ToolArgument(
44
+ name="markdown_content",
45
+ arg_type="string",
46
+ description="Markdown content with support for Mermaid, images, code blocks, and tables",
47
+ required=True,
48
+ example="# Title\n\nContent with **bold** text\n\n```mermaid\ngraph TD\nA-->B\n```",
49
+ ),
50
+ ToolArgument(
51
+ name="output_path",
52
+ arg_type="string",
53
+ description="Path for saving the PDF file",
54
+ required=True,
55
+ example="/path/to/output.pdf",
56
+ ),
57
+ ToolArgument(
58
+ name="style_config",
59
+ arg_type="string",
60
+ description="JSON string with style settings (fonts, colors, sizes)",
61
+ required=False,
62
+ example='{"font_family": "Calibri", "font_size": "11pt"}',
63
+ ),
64
+ ToolArgument(
65
+ name="css_template",
66
+ arg_type="string",
67
+ description="Optional CSS template for custom styling",
68
+ required=False,
69
+ example="path/to/custom.css",
70
+ ),
71
+ ]
72
+
73
+ DEFAULT_STYLES: ClassVar[Dict[str, Union[str, int, Dict[str, str]]]] = {
74
+ "font_family": "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif",
75
+ "heading_font": "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif",
76
+ "code_font": "'Source Code Pro', 'Consolas', monospace",
77
+ "font_size": "12pt",
78
+ "line_height": "1.6",
79
+ "text_color": "#333333",
80
+ "heading_color": "#2c3e50",
81
+ "link_color": "#0366d6",
82
+ "code_bg": "#f6f8fa",
83
+ "code_color": "#24292e",
84
+ "code_border": "#eaecef",
85
+ "page_size": "A4",
86
+ "margins": {
87
+ "top": "2.5cm",
88
+ "right": "2.5cm",
89
+ "bottom": "2.5cm",
90
+ "left": "2.5cm"
91
+ }
92
+ }
93
+
94
+ DEFAULT_CSS: ClassVar[str] = """
95
+ @import url('https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap');
96
+
97
+ @page {
98
+ size: %(page_size)s;
99
+ margin: %(margins_top)s %(margins_right)s %(margins_bottom)s %(margins_left)s;
100
+ }
101
+
102
+ body {
103
+ font-family: %(font_family)s;
104
+ font-size: %(font_size)s;
105
+ line-height: %(line_height)s;
106
+ color: %(text_color)s;
107
+ }
108
+
109
+ h1, h2, h3, h4, h5, h6 {
110
+ font-family: %(heading_font)s;
111
+ color: %(heading_color)s;
112
+ margin-top: 1.5em;
113
+ margin-bottom: 0.5em;
114
+ }
115
+
116
+ a {
117
+ color: %(link_color)s;
118
+ text-decoration: none;
119
+ }
120
+
121
+ pre {
122
+ background-color: %(code_bg)s;
123
+ border: 1px solid %(code_border)s;
124
+ border-radius: 6px;
125
+ padding: 16px;
126
+ overflow-x: auto;
127
+ font-size: 85%%;
128
+ line-height: 1.45;
129
+ margin: 1em 0;
130
+ }
131
+
132
+ pre code {
133
+ font-family: %(code_font)s;
134
+ color: %(code_color)s;
135
+ background: none;
136
+ padding: 0;
137
+ font-size: inherit;
138
+ white-space: pre;
139
+ word-break: normal;
140
+ word-wrap: normal;
141
+ }
142
+
143
+ code {
144
+ font-family: %(code_font)s;
145
+ background-color: %(code_bg)s;
146
+ border-radius: 3px;
147
+ font-size: 85%%;
148
+ margin: 0;
149
+ padding: 0.2em 0.4em;
150
+ }
151
+
152
+ .mermaid-container {
153
+ margin: 2em 0;
154
+ padding: 1em;
155
+ border: 1px solid %(code_border)s;
156
+ border-radius: 8px;
157
+ background-color: white;
158
+ }
159
+
160
+ .mermaid-code {
161
+ margin-bottom: 1.5em;
162
+ }
163
+
164
+ .mermaid-code-header {
165
+ font-weight: bold;
166
+ color: #666;
167
+ margin-bottom: 0.5em;
168
+ font-size: 90%%;
169
+ border-bottom: 1px solid #eee;
170
+ padding-bottom: 0.3em;
171
+ }
172
+
173
+ .mermaid-code-content {
174
+ background-color: %(code_bg)s;
175
+ padding: 1em;
176
+ border-radius: 6px;
177
+ border: 1px solid %(code_border)s;
178
+ font-family: %(code_font)s;
179
+ font-size: 85%%;
180
+ line-height: 1.45;
181
+ }
182
+
183
+ .mermaid-visualization {
184
+ margin-top: 1.5em;
185
+ }
186
+
187
+ .mermaid-visualization-header {
188
+ font-weight: bold;
189
+ color: #666;
190
+ margin-bottom: 0.5em;
191
+ font-size: 90%%;
192
+ border-bottom: 1px solid #eee;
193
+ padding-bottom: 0.3em;
194
+ }
195
+
196
+ .mermaid-diagram {
197
+ text-align: center;
198
+ padding: 1em;
199
+ background-color: white;
200
+ }
201
+
202
+ .mermaid-diagram img {
203
+ max-width: 100%%;
204
+ height: auto;
205
+ margin: 0 auto;
206
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
207
+ border-radius: 4px;
208
+ }
209
+
210
+ .diagram-caption {
211
+ text-align: center;
212
+ color: #666;
213
+ font-style: italic;
214
+ font-size: 90%%;
215
+ margin-top: 0.5em;
216
+ }
217
+
218
+ /* Syntax Highlighting Styles */
219
+ .highlight .hll { background-color: #ffc; }
220
+ .highlight .c { color: #998; font-style: italic; }
221
+ .highlight .err { color: #a61717; background-color: #e3d2d2; }
222
+ .highlight .k { color: #000; font-weight: bold; }
223
+ .highlight .o { color: #000; font-weight: bold; }
224
+ .highlight .cm { color: #998; font-style: italic; }
225
+ .highlight .cp { color: #999; font-weight: bold; font-style: italic; }
226
+ .highlight .c1 { color: #998; font-style: italic; }
227
+ .highlight .cs { color: #999; font-weight: bold; font-style: italic; }
228
+ .highlight .gd { color: #000; background-color: #fdd; }
229
+ .highlight .ge { color: #000; font-style: italic; }
230
+ .highlight .gr { color: #a00; }
231
+ .highlight .gh { color: #999; }
232
+ .highlight .gi { color: #000; background-color: #dfd; }
233
+ .highlight .go { color: #888; }
234
+ .highlight .gp { color: #555; }
235
+ .highlight .gs { font-weight: bold; }
236
+ .highlight .gu { color: #aaa; }
237
+ .highlight .gt { color: #a00; }
238
+ .highlight .kc { color: #000; font-weight: bold; }
239
+ .highlight .kd { color: #000; font-weight: bold; }
240
+ .highlight .kn { color: #000; font-weight: bold; }
241
+ .highlight .kp { color: #000; font-weight: bold; }
242
+ .highlight .kr { color: #000; font-weight: bold; }
243
+ .highlight .kt { color: #458; font-weight: bold; }
244
+ .highlight .m { color: #099; }
245
+ .highlight .s { color: #d01040; }
246
+ .highlight .na { color: #008080; }
247
+ .highlight .nb { color: #0086B3; }
248
+ .highlight .nc { color: #458; font-weight: bold; }
249
+ .highlight .no { color: #008080; }
250
+ .highlight .nd { color: #3c5d5d; font-weight: bold; }
251
+ .highlight .ni { color: #800080; }
252
+ .highlight .ne { color: #900; font-weight: bold; }
253
+ .highlight .nf { color: #900; font-weight: bold; }
254
+ .highlight .nl { color: #900; font-weight: bold; }
255
+ .highlight .nn { color: #555; }
256
+ .highlight .nt { color: #000080; }
257
+ .highlight .nv { color: #008080; }
258
+ .highlight .ow { color: #000; font-weight: bold; }
259
+ .highlight .w { color: #bbb; }
260
+ .highlight .mf { color: #099; }
261
+ .highlight .mh { color: #099; }
262
+ .highlight .mi { color: #099; }
263
+ .highlight .mo { color: #099; }
264
+ .highlight .sb { color: #d01040; }
265
+ .highlight .sc { color: #d01040; }
266
+ .highlight .sd { color: #d01040; }
267
+ .highlight .s2 { color: #d01040; }
268
+ .highlight .se { color: #d01040; }
269
+ .highlight .sh { color: #d01040; }
270
+ .highlight .si { color: #d01040; }
271
+ .highlight .sx { color: #d01040; }
272
+ .highlight .sr { color: #009926; }
273
+ .highlight .s1 { color: #d01040; }
274
+ .highlight .ss { color: #990073; }
275
+ .highlight .bp { color: #999; }
276
+ .highlight .vc { color: #008080; }
277
+ .highlight .vg { color: #008080; }
278
+ .highlight .vi { color: #008080; }
279
+ .highlight .il { color: #099; }
280
+
281
+ img {
282
+ max-width: 100%%;
283
+ height: auto;
284
+ display: block;
285
+ margin: 1em auto;
286
+ }
287
+
288
+ blockquote {
289
+ margin: 1em 0;
290
+ padding-left: 1em;
291
+ border-left: 4px solid #ddd;
292
+ color: #666666;
293
+ }
294
+
295
+ table {
296
+ width: 100%%;
297
+ border-collapse: collapse;
298
+ margin: 1em 0;
299
+ }
300
+
301
+ th, td {
302
+ border: 1px solid #ddd;
303
+ padding: 8px;
304
+ text-align: left;
305
+ }
306
+
307
+ th {
308
+ background-color: #f6f8fa;
309
+ }
310
+
311
+ @media print {
312
+ body {
313
+ background-color: white;
314
+ }
315
+
316
+ pre, code {
317
+ white-space: pre-wrap;
318
+ word-wrap: break-word;
319
+ }
320
+ }
321
+ """
322
+
323
+ def _normalize_path(self, path: str) -> Path:
324
+ """Convert path string to normalized Path object.
325
+
326
+ Args:
327
+ path: Input path string
328
+
329
+ Returns:
330
+ Normalized Path object
331
+ """
332
+ if path.startswith("~"):
333
+ path = os.path.expanduser(path)
334
+ return Path(path).resolve()
335
+
336
+ def _parse_style_config(self, style_config: Optional[str]) -> Dict[str, str]:
337
+ """Parse and validate style configuration.
338
+
339
+ Args:
340
+ style_config: JSON style configuration string
341
+
342
+ Returns:
343
+ Merged style configuration dictionary
344
+ """
345
+ try:
346
+ if not style_config:
347
+ return self.DEFAULT_STYLES.copy()
348
+
349
+ custom_styles = json.loads(style_config)
350
+ styles = self.DEFAULT_STYLES.copy()
351
+ styles.update(custom_styles)
352
+ return styles
353
+ except json.JSONDecodeError as e:
354
+ logger.error(f"Invalid style configuration JSON: {e}")
355
+ return self.DEFAULT_STYLES.copy()
356
+
357
+ def _generate_css(self, styles: Dict[str, str], css_template: Optional[str] = None) -> CSS:
358
+ """Generate CSS for PDF styling.
359
+
360
+ Args:
361
+ styles: Style configuration dictionary
362
+ css_template: Optional path to CSS template file
363
+
364
+ Returns:
365
+ WeasyPrint CSS object
366
+ """
367
+ # Unpack margins for CSS template
368
+ if isinstance(styles.get('margins'), dict):
369
+ margins = styles['margins']
370
+ styles.update({
371
+ 'margins_top': margins.get('top', '2.5cm'),
372
+ 'margins_right': margins.get('right', '2.5cm'),
373
+ 'margins_bottom': margins.get('bottom', '2.5cm'),
374
+ 'margins_left': margins.get('left', '2.5cm')
375
+ })
376
+
377
+ base_css = self.DEFAULT_CSS % styles
378
+
379
+ font_config = FontConfiguration()
380
+ css_list = [CSS(string=base_css, font_config=font_config)]
381
+
382
+ if css_template:
383
+ try:
384
+ template_path = self._normalize_path(css_template)
385
+ if template_path.exists():
386
+ css_list.append(CSS(filename=str(template_path), font_config=font_config))
387
+ except Exception as e:
388
+ logger.error(f"Error loading CSS template: {e}")
389
+
390
+ return css_list
391
+
392
+ def _create_mermaid_code_section(self, soup: BeautifulSoup, code: str) -> Tag:
393
+ """Create the code section of a Mermaid diagram.
394
+
395
+ Args:
396
+ soup: BeautifulSoup instance for HTML manipulation
397
+ code: The Mermaid diagram code
398
+
399
+ Returns:
400
+ BeautifulSoup Tag containing the code section
401
+ """
402
+ code_section = soup.new_tag('div')
403
+ code_section['class'] = 'mermaid-code'
404
+
405
+ # Add header
406
+ header = soup.new_tag('div')
407
+ header['class'] = 'mermaid-code-header'
408
+ header.string = 'Mermaid Diagram Code'
409
+ code_section.append(header)
410
+
411
+ # Add code content
412
+ content = soup.new_tag('div')
413
+ content['class'] = 'mermaid-code-content'
414
+ code_tag = soup.new_tag('code')
415
+ code_tag['class'] = 'language-mermaid'
416
+ code_tag.string = code
417
+ content.append(code_tag)
418
+ code_section.append(content)
419
+
420
+ return code_section
421
+
422
+ def _create_mermaid_visualization(self, soup: BeautifulSoup, code: str) -> Tag:
423
+ """Create the visualization section of a Mermaid diagram.
424
+
425
+ Args:
426
+ soup: BeautifulSoup instance for HTML manipulation
427
+ code: The Mermaid diagram code to render
428
+
429
+ Returns:
430
+ BeautifulSoup Tag containing the visualization section
431
+ """
432
+ viz_section = soup.new_tag('div')
433
+ viz_section['class'] = 'mermaid-visualization'
434
+
435
+ # Add header
436
+ header = soup.new_tag('div')
437
+ header['class'] = 'mermaid-visualization-header'
438
+ header.string = 'Diagram Visualization'
439
+ viz_section.append(header)
440
+
441
+ # Create diagram container
442
+ diagram = soup.new_tag('div')
443
+ diagram['class'] = 'mermaid-diagram'
444
+
445
+ try:
446
+ # Generate diagram
447
+ svg_data = mermaid.generate_diagram(code)
448
+
449
+ # Create and add image
450
+ img = soup.new_tag('img')
451
+ img['src'] = f"data:image/svg+xml;base64,{svg_data}"
452
+ img['alt'] = 'Mermaid Diagram'
453
+ diagram.append(img)
454
+
455
+ # Add caption
456
+ caption = soup.new_tag('div')
457
+ caption['class'] = 'diagram-caption'
458
+ caption.string = 'Generated diagram visualization'
459
+ diagram.append(caption)
460
+
461
+ except Exception as e:
462
+ logger.error(f"Failed to generate Mermaid diagram: {e}")
463
+ error = soup.new_tag('div')
464
+ error['style'] = 'color: red; padding: 1em;'
465
+ error.string = f'Error generating diagram: {str(e)}'
466
+ diagram.append(error)
467
+
468
+ viz_section.append(diagram)
469
+ return viz_section
470
+
471
+ def _process_mermaid_diagrams(self, html_content: str) -> str:
472
+ """Convert Mermaid diagram code blocks to images in HTML while preserving the original code.
473
+
474
+ Args:
475
+ html_content: HTML content with Mermaid code blocks
476
+
477
+ Returns:
478
+ HTML content with Mermaid diagrams converted to images while keeping the code
479
+ """
480
+ soup = BeautifulSoup(html_content, 'html.parser')
481
+ mermaid_blocks = soup.find_all('code', class_='language-mermaid')
482
+
483
+ for block in mermaid_blocks:
484
+ try:
485
+ # Create main container
486
+ container = soup.new_tag('div')
487
+ container['class'] = 'mermaid-container'
488
+
489
+ # Add code section
490
+ container.append(self._create_mermaid_code_section(soup, block.text))
491
+
492
+ # Add visualization section
493
+ container.append(self._create_mermaid_visualization(soup, block.text))
494
+
495
+ # Replace original block
496
+ parent = block.parent
497
+ if parent.name == 'pre':
498
+ parent.replace_with(container)
499
+ else:
500
+ block.replace_with(container)
501
+
502
+ except Exception as e:
503
+ logger.error(f"Error processing Mermaid diagram block: {e}")
504
+ continue
505
+
506
+ return str(soup)
507
+
508
+ def _process_code_blocks(self, html_content: str) -> str:
509
+ """Process and syntax highlight code blocks.
510
+
511
+ Args:
512
+ html_content: HTML content with code blocks
513
+
514
+ Returns:
515
+ HTML content with syntax highlighted code blocks
516
+ """
517
+ soup = BeautifulSoup(html_content, 'html.parser')
518
+ code_blocks = soup.find_all('code', class_=lambda x: x and x.startswith('language-'))
519
+
520
+ for block in code_blocks:
521
+ try:
522
+ # Get the language from the class
523
+ lang = block['class'][0].replace('language-', '')
524
+ if lang == 'mermaid':
525
+ continue # Skip Mermaid blocks as they're handled separately
526
+
527
+ # Get the appropriate lexer
528
+ try:
529
+ lexer = get_lexer_by_name(lang)
530
+ except ValueError:
531
+ lexer = TextLexer()
532
+
533
+ # Highlight the code
534
+ formatter = HtmlFormatter(style='github', cssclass='highlight')
535
+ highlighted = highlight(block.text, lexer, formatter)
536
+
537
+ # Create a new tag with the highlighted code
538
+ new_tag = soup.new_tag('div')
539
+ new_tag['class'] = 'highlight-wrapper'
540
+ new_tag.append(BeautifulSoup(highlighted, 'html.parser'))
541
+
542
+ # Replace the original code block
543
+ if block.parent.name == 'pre':
544
+ block.parent.replace_with(new_tag)
545
+ else:
546
+ block.replace_with(new_tag)
547
+
548
+ except Exception as e:
549
+ logger.error(f"Error processing code block: {e}")
550
+ continue
551
+
552
+ return str(soup)
553
+
554
+ def execute(self, **kwargs) -> str:
555
+ """Execute the markdown to PDF conversion.
556
+
557
+ Args:
558
+ **kwargs: Tool arguments including markdown_content, output_path,
559
+ style_config, and css_template
560
+
561
+ Returns:
562
+ Success message with output path
563
+ """
564
+ try:
565
+ markdown_content = kwargs['markdown_content']
566
+ output_path = self._normalize_path(kwargs['output_path'])
567
+ style_config = kwargs.get('style_config')
568
+ css_template = kwargs.get('css_template')
569
+
570
+ # Create output directory if it doesn't exist
571
+ output_path.parent.mkdir(parents=True, exist_ok=True)
572
+
573
+ # Convert markdown to HTML with extensions
574
+ html_content = markdown.markdown(
575
+ markdown_content,
576
+ extensions=[
577
+ 'extra',
578
+ 'codehilite',
579
+ 'tables',
580
+ 'fenced_code',
581
+ 'sane_lists',
582
+ 'nl2br',
583
+ 'attr_list'
584
+ ]
585
+ )
586
+
587
+ # Process code blocks with syntax highlighting
588
+ html_content = self._process_code_blocks(html_content)
589
+
590
+ # Process Mermaid diagrams
591
+ html_content = self._process_mermaid_diagrams(html_content)
592
+
593
+ # Generate CSS
594
+ styles = self._parse_style_config(style_config)
595
+ css_list = self._generate_css(styles, css_template)
596
+
597
+ # Convert to PDF
598
+ html = HTML(string=html_content)
599
+ html.write_pdf(
600
+ str(output_path),
601
+ stylesheets=css_list,
602
+ optimize_size=('fonts', 'images')
603
+ )
604
+
605
+ return f"Successfully created PDF at: {output_path}"
606
+
607
+ except Exception as e:
608
+ error_msg = f"Error converting markdown to PDF: {str(e)}"
609
+ logger.error(error_msg)
610
+ raise RuntimeError(error_msg)
611
+
612
+
613
+ if __name__ == "__main__":
614
+ # Example usage with error handling
615
+ try:
616
+ tool = MarkdownToPdfTool()
617
+ result = tool.execute(
618
+ markdown_content="# Test Document\n\nThis is a test.",
619
+ output_path="test_output.pdf"
620
+ )
621
+ print(result)
622
+ except Exception as e:
623
+ print(f"Error: {e}")