quantalogic 0.35.0__py3-none-any.whl → 0.40.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.40.0.dist-info}/METADATA +41 -1
  103. quantalogic-0.40.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.40.0.dist-info}/LICENSE +0 -0
  106. {quantalogic-0.35.0.dist-info → quantalogic-0.40.0.dist-info}/WHEEL +0 -0
  107. {quantalogic-0.35.0.dist-info → quantalogic-0.40.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,661 @@
1
+ """Tool for validating Mermaid diagram syntax."""
2
+
3
+ import re
4
+ from typing import Dict, List, Optional
5
+
6
+ from loguru import logger
7
+
8
+ from quantalogic.tools.tool import Tool, ToolArgument
9
+
10
+
11
+ class MermaidValidatorTool(Tool):
12
+ """Tool for validating Mermaid diagram syntax."""
13
+
14
+ name: str = "mermaid_validator_tool"
15
+ description: str = "Validates Mermaid diagram syntax and returns any errors found."
16
+ need_validation: bool = False
17
+ arguments: list = [
18
+ ToolArgument(
19
+ name="mermaid_code",
20
+ arg_type="string",
21
+ description="The Mermaid diagram code to validate.",
22
+ required=True,
23
+ example="flowchart TD\n A[Start] --> B[End]",
24
+ )
25
+ ]
26
+
27
+ def _validate_flowchart(self, code: str) -> List[str]:
28
+ """Validate flowchart diagram syntax."""
29
+ errors = []
30
+ lines = code.strip().split('\n')
31
+
32
+ # Check if starts with flowchart
33
+ if not lines[0].strip().startswith('flowchart'):
34
+ errors.append("Flowchart must start with 'flowchart' keyword")
35
+
36
+ # Check node definitions and connections
37
+ node_ids = set()
38
+ for line in lines[1:]:
39
+ line = line.strip()
40
+ if not line:
41
+ continue
42
+
43
+ # Check for node definitions
44
+ node_matches = re.findall(r'([A-Za-z0-9_]+)(?:\[|\(|\{)(.+?)(?:\]|\)|\})', line)
45
+ for node_id, _ in node_matches:
46
+ node_ids.add(node_id)
47
+
48
+ # Check connections
49
+ if '-->' in line or '---' in line:
50
+ parts = re.split(r'-->|---', line)
51
+ for part in parts:
52
+ node_id = part.strip().split('[')[0].strip()
53
+ if node_id and node_id not in node_ids:
54
+ errors.append(f"Node '{node_id}' is used in connection but not defined")
55
+
56
+ return errors
57
+
58
+ def _validate_sequence(self, code: str) -> List[str]:
59
+ """Validate sequence diagram syntax."""
60
+ errors = []
61
+ lines = code.strip().split('\n')
62
+
63
+ # Check if starts with sequenceDiagram
64
+ if not lines[0].strip() == 'sequenceDiagram':
65
+ errors.append("Sequence diagram must start with 'sequenceDiagram' keyword")
66
+
67
+ # Track participants
68
+ participants = set()
69
+
70
+ for line in lines[1:]:
71
+ line = line.strip()
72
+ if not line:
73
+ continue
74
+
75
+ # Check participant definitions
76
+ if line.startswith('participant'):
77
+ participant = line.split()[1]
78
+ participants.add(participant)
79
+
80
+ # Check message syntax
81
+ elif any(op in line for op in ['->>','-->>','->','-->']):
82
+ parts = re.split(r'[-]+>>|[-]+>', line)
83
+ if len(parts) == 2:
84
+ sender = parts[0].strip()
85
+ receiver = parts[1].split(':')[0].strip()
86
+
87
+ if sender not in participants:
88
+ errors.append(f"Undefined participant '{sender}' in message")
89
+ if receiver not in participants:
90
+ errors.append(f"Undefined participant '{receiver}' in message")
91
+ else:
92
+ errors.append(f"Invalid message syntax in line: {line}")
93
+
94
+ return errors
95
+
96
+ def _validate_gantt(self, code: str) -> List[str]:
97
+ """Validate Gantt chart syntax."""
98
+ errors = []
99
+ lines = code.strip().split('\n')
100
+
101
+ # Check if starts with gantt
102
+ if not lines[0].strip() == 'gantt':
103
+ errors.append("Gantt chart must start with 'gantt' keyword")
104
+
105
+ has_title = False
106
+ has_date_format = False
107
+
108
+ for line in lines[1:]:
109
+ line = line.strip()
110
+ if not line:
111
+ continue
112
+
113
+ if line.startswith('title'):
114
+ has_title = True
115
+ elif line.startswith('dateFormat'):
116
+ has_date_format = True
117
+ elif line.startswith('section'):
118
+ section_name = line.replace('section', '').strip()
119
+ if not section_name:
120
+ errors.append("Section must have a name")
121
+ elif ':' in line:
122
+ # Task definition
123
+ parts = line.split(':')
124
+ if len(parts) < 2:
125
+ errors.append(f"Invalid task definition in line: {line}")
126
+
127
+ # Validate date format if present
128
+ dates = re.findall(r'\d{4}-\d{2}-\d{2}', parts[1])
129
+ for date in dates:
130
+ try:
131
+ from datetime import datetime
132
+ datetime.strptime(date, '%Y-%m-%d')
133
+ except ValueError:
134
+ errors.append(f"Invalid date format in line: {line}")
135
+
136
+ if not has_title:
137
+ errors.append("Gantt chart should have a title")
138
+ if not has_date_format:
139
+ errors.append("Gantt chart should specify dateFormat")
140
+
141
+ return errors
142
+
143
+ def _validate_class(self, code: str) -> List[str]:
144
+ """Validate class diagram syntax."""
145
+ errors = []
146
+ lines = code.strip().split('\n')
147
+
148
+ # Check if starts with classDiagram
149
+ if not lines[0].strip().startswith('classDiagram'):
150
+ errors.append("Class diagram must start with 'classDiagram' keyword")
151
+
152
+ class_names = set()
153
+
154
+ for line in lines[1:]:
155
+ line = line.strip()
156
+ if not line:
157
+ continue
158
+
159
+ # Check class definitions
160
+ if 'class' in line and '{' not in line:
161
+ parts = line.split()
162
+ if len(parts) >= 2 and parts[0] == 'class':
163
+ class_names.add(parts[1])
164
+
165
+ # Check relationships
166
+ relationship_symbols = ['-->', '<--', '--', '..>', '<..', '..', '--|>', '<|--', '<|..', '..|>']
167
+ for symbol in relationship_symbols:
168
+ if symbol in line:
169
+ parts = line.split(symbol)
170
+ if len(parts) == 2:
171
+ class1 = parts[0].strip().split()[0]
172
+ class2 = parts[1].strip().split()[0]
173
+
174
+ if class1 not in class_names and not class1.startswith('"'):
175
+ errors.append(f"Undefined class '{class1}' in relationship")
176
+ if class2 not in class_names and not class2.startswith('"'):
177
+ errors.append(f"Undefined class '{class2}' in relationship")
178
+
179
+ # Check method and property syntax in class definitions
180
+ if ':' in line and '--' not in line and '.' not in line:
181
+ parts = line.split(':')
182
+ if len(parts) != 2:
183
+ errors.append(f"Invalid method/property syntax in line: {line}")
184
+
185
+ return errors
186
+
187
+ def _validate_state(self, code: str) -> List[str]:
188
+ """Validate state diagram syntax."""
189
+ errors = []
190
+ lines = code.strip().split('\n')
191
+
192
+ # Check if starts with stateDiagram
193
+ if not (lines[0].strip().startswith('stateDiagram') or lines[0].strip().startswith('stateDiagram-v2')):
194
+ errors.append("State diagram must start with 'stateDiagram' or 'stateDiagram-v2' keyword")
195
+
196
+ state_names = set()
197
+
198
+ for line in lines[1:]:
199
+ line = line.strip()
200
+ if not line:
201
+ continue
202
+
203
+ # Check state definitions
204
+ if line.startswith('state'):
205
+ parts = line.split()
206
+ if len(parts) >= 2:
207
+ state_names.add(parts[1].strip('{}[]()'))
208
+
209
+ # Check transitions
210
+ if '-->' in line:
211
+ parts = line.split('-->')
212
+ if len(parts) == 2:
213
+ state1 = parts[0].strip().split()[0].strip('{}[]()""')
214
+ state2 = parts[1].strip().split()[0].strip('{}[]()""')
215
+
216
+ if state1 not in state_names and state1 != '[*]':
217
+ errors.append(f"Undefined state '{state1}' in transition")
218
+ if state2 not in state_names and state2 != '[*]':
219
+ errors.append(f"Undefined state '{state2}' in transition")
220
+
221
+ # Check composite states
222
+ if '{' in line and '}' not in line:
223
+ errors.append(f"Unclosed composite state in line: {line}")
224
+
225
+ return errors
226
+
227
+ def _validate_er(self, code: str) -> List[str]:
228
+ """Validate ER diagram syntax."""
229
+ errors = []
230
+ lines = code.strip().split('\n')
231
+
232
+ # Check if starts with erDiagram
233
+ if not lines[0].strip() == 'erDiagram':
234
+ errors.append("ER diagram must start with 'erDiagram' keyword")
235
+
236
+ entity_names = set()
237
+ relationship_types = {'one_to_one', 'one_to_many', 'many_to_one', 'many_to_many'}
238
+
239
+ for line in lines[1:]:
240
+ line = line.strip()
241
+ if not line:
242
+ continue
243
+
244
+ # Check entity definitions and relationships
245
+ if '||' in line or 'o{' in line or '}o' in line or '||' in line:
246
+ parts = line.split()
247
+ if len(parts) >= 3:
248
+ entity1 = parts[0]
249
+ entity2 = parts[2]
250
+ entity_names.add(entity1)
251
+ entity_names.add(entity2)
252
+
253
+ # Check relationship syntax
254
+ if len(parts) < 4:
255
+ errors.append(f"Missing relationship type in line: {line}")
256
+ elif not any(rel_type in line for rel_type in ['||--||', '||--o{', '}o--||', '}o--o{']):
257
+ errors.append(f"Invalid relationship syntax in line: {line}")
258
+
259
+ # Check attribute definitions
260
+ if '{' in line and '}' in line and '--' not in line:
261
+ entity = line.split('{')[0].strip()
262
+ if entity not in entity_names:
263
+ errors.append(f"Undefined entity '{entity}' has attributes")
264
+
265
+ # Check attribute syntax
266
+ attributes = line[line.find('{')+1:line.find('}')].split(',')
267
+ for attr in attributes:
268
+ attr = attr.strip()
269
+ if not attr or ' ' not in attr:
270
+ errors.append(f"Invalid attribute syntax in line: {line}")
271
+
272
+ return errors
273
+
274
+ def _validate_journey(self, code: str) -> List[str]:
275
+ """Validate journey diagram syntax."""
276
+ errors = []
277
+ lines = code.strip().split('\n')
278
+
279
+ # Check if starts with journey
280
+ if not lines[0].strip() == 'journey':
281
+ errors.append("Journey diagram must start with 'journey' keyword")
282
+
283
+ has_title = False
284
+ valid_scores = {'1', '2', '3', '4', '5'}
285
+ current_section = None
286
+
287
+ for line in lines[1:]:
288
+ line = line.strip()
289
+ if not line:
290
+ continue
291
+
292
+ # Check title
293
+ if line.startswith('title'):
294
+ has_title = True
295
+ title = line.replace('title', '').strip()
296
+ if not title:
297
+ errors.append("Journey title cannot be empty")
298
+
299
+ # Check sections
300
+ elif line.startswith('section'):
301
+ current_section = line.replace('section', '').strip()
302
+ if not current_section:
303
+ errors.append("Section must have a name")
304
+
305
+ # Check tasks
306
+ elif ':' in line and not line.startswith('title'):
307
+ if not current_section:
308
+ errors.append(f"Task must be within a section: {line}")
309
+
310
+ parts = line.split(':')
311
+ if len(parts) != 2:
312
+ errors.append(f"Invalid task syntax in line: {line}")
313
+ else:
314
+ # Check task score
315
+ score = parts[1].strip().split()[0]
316
+ if score not in valid_scores:
317
+ errors.append(f"Invalid task score in line: {line}. Must be between 1-5")
318
+
319
+ if not has_title:
320
+ errors.append("Journey diagram should have a title")
321
+
322
+ return errors
323
+
324
+ def _validate_pie(self, code: str) -> List[str]:
325
+ """Validate pie chart syntax."""
326
+ errors = []
327
+ lines = code.strip().split('\n')
328
+
329
+ # Check if starts with pie
330
+ if not lines[0].strip() == 'pie':
331
+ errors.append("Pie chart must start with 'pie' keyword")
332
+
333
+ has_title = False
334
+ has_data = False
335
+
336
+ for line in lines[1:]:
337
+ line = line.strip()
338
+ if not line:
339
+ continue
340
+
341
+ # Check title
342
+ if line.startswith('title'):
343
+ has_title = True
344
+ title = line.replace('title', '').strip()
345
+ if not title:
346
+ errors.append("Pie chart title cannot be empty")
347
+
348
+ # Check data entries
349
+ elif ':' in line:
350
+ has_data = True
351
+ parts = line.split(':')
352
+ if len(parts) != 2:
353
+ errors.append(f"Invalid data entry syntax in line: {line}")
354
+ else:
355
+ # Check if value is numeric
356
+ try:
357
+ float(parts[1].strip())
358
+ except ValueError:
359
+ errors.append(f"Value must be numeric in line: {line}")
360
+
361
+ if not has_title:
362
+ errors.append("Pie chart should have a title")
363
+ if not has_data:
364
+ errors.append("Pie chart must have at least one data entry")
365
+
366
+ return errors
367
+
368
+ def _validate_mindmap(self, code: str) -> List[str]:
369
+ """Validate mindmap syntax."""
370
+ errors = []
371
+ lines = code.strip().split('\n')
372
+
373
+ # Check if starts with mindmap
374
+ if not lines[0].strip() == 'mindmap':
375
+ errors.append("Mindmap must start with 'mindmap' keyword")
376
+
377
+ has_root = False
378
+ current_level = 0
379
+ previous_level = 0
380
+
381
+ for line in lines[1:]:
382
+ line = line.strip()
383
+ if not line:
384
+ continue
385
+
386
+ # Count leading spaces to determine level
387
+ indent_level = len(line) - len(line.lstrip())
388
+ current_level = indent_level // 2 # Each level is 2 spaces
389
+
390
+ # First non-empty line should be root
391
+ if not has_root:
392
+ has_root = True
393
+ if current_level != 0:
394
+ errors.append("Root node must not be indented")
395
+ continue
396
+
397
+ # Check indentation consistency
398
+ if current_level > previous_level + 1:
399
+ errors.append(f"Invalid indentation in line: {line}")
400
+
401
+ # Check node syntax
402
+ if line.lstrip().startswith('::'):
403
+ errors.append(f"Node text missing in line: {line}")
404
+ elif '::' in line:
405
+ parts = line.split('::')
406
+ if len(parts) != 2 or not parts[0].strip() or not parts[1].strip():
407
+ errors.append(f"Invalid node format in line: {line}")
408
+
409
+ previous_level = current_level
410
+
411
+ if not has_root:
412
+ errors.append("Mindmap must have a root node")
413
+
414
+ return errors
415
+
416
+ def _validate_timeline(self, code: str) -> List[str]:
417
+ """Validate timeline syntax."""
418
+ errors = []
419
+ lines = code.strip().split('\n')
420
+
421
+ # Check if starts with timeline
422
+ if not lines[0].strip() == 'timeline':
423
+ errors.append("Timeline must start with 'timeline' keyword")
424
+
425
+ has_title = False
426
+ current_section = None
427
+
428
+ for line in lines[1:]:
429
+ line = line.strip()
430
+ if not line:
431
+ continue
432
+
433
+ # Check title
434
+ if line.startswith('title'):
435
+ has_title = True
436
+ title = line.replace('title', '').strip()
437
+ if not title:
438
+ errors.append("Timeline title cannot be empty")
439
+
440
+ # Check sections
441
+ elif line.startswith('section'):
442
+ current_section = line.replace('section', '').strip()
443
+ if not current_section:
444
+ errors.append("Section must have a name")
445
+
446
+ # Check events
447
+ elif ':' in line and not line.startswith('title'):
448
+ if not current_section:
449
+ errors.append(f"Event must be within a section: {line}")
450
+
451
+ parts = line.split(':')
452
+ if len(parts) != 2:
453
+ errors.append(f"Invalid event syntax in line: {line}")
454
+ else:
455
+ # Check date format if present
456
+ date_str = parts[0].strip()
457
+ if date_str:
458
+ try:
459
+ # Support various date formats
460
+ from dateutil import parser
461
+ parser.parse(date_str)
462
+ except (ValueError, ImportError):
463
+ errors.append(f"Invalid date format in line: {line}")
464
+
465
+ if not has_title:
466
+ errors.append("Timeline should have a title")
467
+
468
+ return errors
469
+
470
+ def _validate_git_graph(self, code: str) -> List[str]:
471
+ """Validate git graph syntax."""
472
+ errors = []
473
+ lines = code.strip().split('\n')
474
+
475
+ # Check if starts with gitGraph
476
+ if not lines[0].strip() == 'gitGraph':
477
+ errors.append("Git graph must start with 'gitGraph' keyword")
478
+
479
+ valid_commands = {'commit', 'branch', 'checkout', 'merge', 'reset', 'cherry-pick'}
480
+ branches = {'main'} # main/master is always available
481
+ current_branch = 'main'
482
+ commit_ids = set()
483
+
484
+ for line in lines[1:]:
485
+ line = line.strip()
486
+ if not line:
487
+ continue
488
+
489
+ parts = line.split()
490
+ if not parts:
491
+ continue
492
+
493
+ command = parts[0]
494
+
495
+ if command not in valid_commands:
496
+ errors.append(f"Invalid command '{command}' in line: {line}")
497
+ continue
498
+
499
+ # Check branch operations
500
+ if command == 'branch':
501
+ if len(parts) < 2:
502
+ errors.append(f"Branch name missing in line: {line}")
503
+ else:
504
+ branch_name = parts[1]
505
+ if branch_name in branches:
506
+ errors.append(f"Branch '{branch_name}' already exists")
507
+ else:
508
+ branches.add(branch_name)
509
+
510
+ elif command == 'checkout':
511
+ if len(parts) < 2:
512
+ errors.append(f"Branch name missing in line: {line}")
513
+ else:
514
+ branch_name = parts[1]
515
+ if branch_name not in branches:
516
+ errors.append(f"Cannot checkout non-existent branch '{branch_name}'")
517
+ else:
518
+ current_branch = branch_name
519
+
520
+ # Check commit operations
521
+ elif command == 'commit':
522
+ if len(parts) > 1 and parts[1].startswith('id:'):
523
+ commit_id = parts[1].split(':')[1]
524
+ if commit_id in commit_ids:
525
+ errors.append(f"Duplicate commit ID '{commit_id}'")
526
+ else:
527
+ commit_ids.add(commit_id)
528
+
529
+ # Check merge operations
530
+ elif command == 'merge':
531
+ if len(parts) < 2:
532
+ errors.append(f"Merge target missing in line: {line}")
533
+ else:
534
+ target_branch = parts[1]
535
+ if target_branch not in branches:
536
+ errors.append(f"Cannot merge non-existent branch '{target_branch}'")
537
+ elif target_branch == current_branch:
538
+ errors.append(f"Cannot merge branch '{target_branch}' into itself")
539
+
540
+ return errors
541
+
542
+ def _validate_general(self, code: str) -> List[str]:
543
+ """General validator for any Mermaid diagram type.
544
+
545
+ Performs basic syntax validation that applies to all diagram types:
546
+ - Non-empty content
547
+ - Balanced brackets and quotes
548
+ - Basic syntax elements
549
+ """
550
+ errors = []
551
+ lines = code.strip().split('\n')
552
+
553
+ if not lines:
554
+ errors.append("Diagram cannot be empty")
555
+ return errors
556
+
557
+ # Get diagram type from first line
558
+ diagram_type = lines[0].strip()
559
+ if not diagram_type:
560
+ errors.append("First line must specify diagram type")
561
+
562
+ # Track brackets and quotes
563
+ brackets = {
564
+ '(': ')',
565
+ '[': ']',
566
+ '{': '}',
567
+ '"': '"',
568
+ "'": "'"
569
+ }
570
+ stack = []
571
+
572
+ for i, line in enumerate(lines, 1):
573
+ # Skip empty lines
574
+ if not line.strip():
575
+ continue
576
+
577
+ # Check for unmatched quotes and brackets
578
+ for char in line:
579
+ if char in brackets:
580
+ stack.append((char, i))
581
+ elif char in brackets.values():
582
+ if not stack:
583
+ errors.append(f"Unmatched closing character '{char}' at line {i}")
584
+ else:
585
+ opening, _ = stack.pop()
586
+ if char != brackets[opening]:
587
+ errors.append(f"Mismatched brackets: expected '{brackets[opening]}' but found '{char}' at line {i}")
588
+
589
+ # Check for common syntax errors
590
+ if line.count('--') % 2 != 0:
591
+ errors.append(f"Unmatched connector '--' at line {i}")
592
+
593
+ if '>>' in line and not any(x in line for x in ['->', '-->>']):
594
+ errors.append(f"Invalid arrow syntax at line {i}")
595
+
596
+ # Check for incomplete statements
597
+ if line.strip().endswith(':') and i == len(lines):
598
+ errors.append(f"Incomplete statement at line {i}")
599
+
600
+ # Check for any remaining unclosed brackets or quotes
601
+ while stack:
602
+ char, line_num = stack.pop()
603
+ errors.append(f"Unclosed '{char}' from line {line_num}")
604
+
605
+ return errors
606
+
607
+ def execute(self, mermaid_code: str) -> str:
608
+ """Validates the Mermaid diagram syntax.
609
+
610
+ Args:
611
+ mermaid_code: The Mermaid diagram code to validate.
612
+
613
+ Returns:
614
+ A string containing validation results.
615
+ """
616
+ logger.info("Validating Mermaid diagram")
617
+
618
+ # Remove markdown code block markers if present
619
+ mermaid_code = mermaid_code.replace('```mermaid', '').replace('```', '').strip()
620
+
621
+ # First run general validation
622
+ errors = self._validate_general(mermaid_code)
623
+ if errors:
624
+ return "Validation errors found:\n" + "\n".join(f"- {error}" for error in errors)
625
+
626
+ # Then run specific validation based on diagram type
627
+ if mermaid_code.startswith('flowchart'):
628
+ errors = self._validate_flowchart(mermaid_code)
629
+ elif mermaid_code.startswith('sequenceDiagram'):
630
+ errors = self._validate_sequence(mermaid_code)
631
+ elif mermaid_code.startswith('gantt'):
632
+ errors = self._validate_gantt(mermaid_code)
633
+ elif mermaid_code.startswith('classDiagram'):
634
+ errors = self._validate_class(mermaid_code)
635
+ elif mermaid_code.startswith('stateDiagram'):
636
+ errors = self._validate_state(mermaid_code)
637
+ elif mermaid_code.startswith('erDiagram'):
638
+ errors = self._validate_er(mermaid_code)
639
+ elif mermaid_code.startswith('journey'):
640
+ errors = self._validate_journey(mermaid_code)
641
+ elif mermaid_code.startswith('pie'):
642
+ errors = self._validate_pie(mermaid_code)
643
+ elif mermaid_code.startswith('mindmap'):
644
+ errors = self._validate_mindmap(mermaid_code)
645
+ elif mermaid_code.startswith('timeline'):
646
+ errors = self._validate_timeline(mermaid_code)
647
+ elif mermaid_code.startswith('gitGraph'):
648
+ errors = self._validate_git_graph(mermaid_code)
649
+ else:
650
+ # For unknown diagram types, we've already done general validation
651
+ return "Mermaid diagram syntax is valid! (Note: Using general validation as specific diagram type is not recognized)"
652
+
653
+ if errors:
654
+ return "Validation errors found:\n" + "\n".join(f"- {error}" for error in errors)
655
+
656
+ return "Mermaid diagram syntax is valid!"
657
+
658
+
659
+ if __name__ == "__main__":
660
+ tool = MermaidValidatorTool()
661
+ print(tool.to_markdown())
@@ -7,7 +7,4 @@ This module provides common utility functions used across the quantalogic packag
7
7
  from .create_sample_database import create_sample_database
8
8
  from .generate_database_report import generate_database_report
9
9
 
10
- __all__ = [
11
- 'create_sample_database',
12
- 'generate_database_report'
13
- ]
10
+ __all__ = ["create_sample_database", "generate_database_report"]