quantalogic 0.33.4__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.
- quantalogic/__init__.py +0 -4
- quantalogic/agent.py +603 -362
- quantalogic/agent_config.py +260 -28
- quantalogic/agent_factory.py +43 -17
- quantalogic/coding_agent.py +20 -12
- quantalogic/config.py +7 -4
- quantalogic/console_print_events.py +4 -8
- quantalogic/console_print_token.py +2 -2
- quantalogic/docs_cli.py +15 -10
- quantalogic/event_emitter.py +258 -83
- quantalogic/flow/__init__.py +23 -0
- quantalogic/flow/flow.py +595 -0
- quantalogic/flow/flow_extractor.py +672 -0
- quantalogic/flow/flow_generator.py +89 -0
- quantalogic/flow/flow_manager.py +407 -0
- quantalogic/flow/flow_manager_schema.py +169 -0
- quantalogic/flow/flow_yaml.md +419 -0
- quantalogic/generative_model.py +109 -77
- quantalogic/get_model_info.py +6 -6
- quantalogic/interactive_text_editor.py +100 -73
- quantalogic/main.py +36 -23
- quantalogic/model_info_list.py +12 -0
- quantalogic/model_info_litellm.py +14 -14
- quantalogic/prompts.py +2 -1
- quantalogic/{llm.py → quantlitellm.py} +29 -39
- quantalogic/search_agent.py +4 -4
- quantalogic/server/models.py +4 -1
- quantalogic/task_file_reader.py +5 -5
- quantalogic/task_runner.py +21 -20
- quantalogic/tool_manager.py +10 -21
- quantalogic/tools/__init__.py +98 -68
- quantalogic/tools/composio/composio.py +416 -0
- quantalogic/tools/{generate_database_report_tool.py → database/generate_database_report_tool.py} +4 -9
- quantalogic/tools/database/sql_query_tool_advanced.py +261 -0
- quantalogic/tools/document_tools/markdown_to_docx_tool.py +620 -0
- quantalogic/tools/document_tools/markdown_to_epub_tool.py +438 -0
- quantalogic/tools/document_tools/markdown_to_html_tool.py +362 -0
- quantalogic/tools/document_tools/markdown_to_ipynb_tool.py +319 -0
- quantalogic/tools/document_tools/markdown_to_latex_tool.py +420 -0
- quantalogic/tools/document_tools/markdown_to_pdf_tool.py +623 -0
- quantalogic/tools/document_tools/markdown_to_pptx_tool.py +319 -0
- quantalogic/tools/duckduckgo_search_tool.py +2 -4
- quantalogic/tools/finance/alpha_vantage_tool.py +440 -0
- quantalogic/tools/finance/ccxt_tool.py +373 -0
- quantalogic/tools/finance/finance_llm_tool.py +387 -0
- quantalogic/tools/finance/google_finance.py +192 -0
- quantalogic/tools/finance/market_intelligence_tool.py +520 -0
- quantalogic/tools/finance/technical_analysis_tool.py +491 -0
- quantalogic/tools/finance/tradingview_tool.py +336 -0
- quantalogic/tools/finance/yahoo_finance.py +236 -0
- quantalogic/tools/git/bitbucket_clone_repo_tool.py +181 -0
- quantalogic/tools/git/bitbucket_operations_tool.py +326 -0
- quantalogic/tools/git/clone_repo_tool.py +189 -0
- quantalogic/tools/git/git_operations_tool.py +532 -0
- quantalogic/tools/google_packages/google_news_tool.py +480 -0
- quantalogic/tools/grep_app_tool.py +123 -186
- quantalogic/tools/{dalle_e.py → image_generation/dalle_e.py} +37 -27
- quantalogic/tools/jinja_tool.py +6 -10
- quantalogic/tools/language_handlers/__init__.py +22 -9
- quantalogic/tools/list_directory_tool.py +131 -42
- quantalogic/tools/llm_tool.py +45 -15
- quantalogic/tools/llm_vision_tool.py +59 -7
- quantalogic/tools/markitdown_tool.py +17 -5
- quantalogic/tools/nasa_packages/models.py +47 -0
- quantalogic/tools/nasa_packages/nasa_apod_tool.py +232 -0
- quantalogic/tools/nasa_packages/nasa_neows_tool.py +147 -0
- quantalogic/tools/nasa_packages/services.py +82 -0
- quantalogic/tools/presentation_tools/presentation_llm_tool.py +396 -0
- quantalogic/tools/product_hunt/product_hunt_tool.py +258 -0
- quantalogic/tools/product_hunt/services.py +63 -0
- quantalogic/tools/rag_tool/__init__.py +48 -0
- quantalogic/tools/rag_tool/document_metadata.py +15 -0
- quantalogic/tools/rag_tool/query_response.py +20 -0
- quantalogic/tools/rag_tool/rag_tool.py +566 -0
- quantalogic/tools/rag_tool/rag_tool_beta.py +264 -0
- quantalogic/tools/read_html_tool.py +24 -38
- quantalogic/tools/replace_in_file_tool.py +10 -10
- quantalogic/tools/safe_python_interpreter_tool.py +10 -24
- quantalogic/tools/search_definition_names.py +2 -2
- quantalogic/tools/sequence_tool.py +14 -23
- quantalogic/tools/sql_query_tool.py +17 -19
- quantalogic/tools/tool.py +39 -15
- quantalogic/tools/unified_diff_tool.py +1 -1
- quantalogic/tools/utilities/csv_processor_tool.py +234 -0
- quantalogic/tools/utilities/download_file_tool.py +179 -0
- quantalogic/tools/utilities/mermaid_validator_tool.py +661 -0
- quantalogic/tools/utils/__init__.py +1 -4
- quantalogic/tools/utils/create_sample_database.py +24 -38
- quantalogic/tools/utils/generate_database_report.py +74 -82
- quantalogic/tools/wikipedia_search_tool.py +17 -21
- quantalogic/utils/ask_user_validation.py +1 -1
- quantalogic/utils/async_utils.py +35 -0
- quantalogic/utils/check_version.py +3 -5
- quantalogic/utils/get_all_models.py +2 -1
- quantalogic/utils/git_ls.py +21 -7
- quantalogic/utils/lm_studio_model_info.py +9 -7
- quantalogic/utils/python_interpreter.py +113 -43
- quantalogic/utils/xml_utility.py +178 -0
- quantalogic/version_check.py +1 -1
- quantalogic/welcome_message.py +7 -7
- quantalogic/xml_parser.py +0 -1
- {quantalogic-0.33.4.dist-info → quantalogic-0.40.0.dist-info}/METADATA +44 -1
- quantalogic-0.40.0.dist-info/RECORD +148 -0
- quantalogic-0.33.4.dist-info/RECORD +0 -102
- {quantalogic-0.33.4.dist-info → quantalogic-0.40.0.dist-info}/LICENSE +0 -0
- {quantalogic-0.33.4.dist-info → quantalogic-0.40.0.dist-info}/WHEEL +0 -0
- {quantalogic-0.33.4.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"]
|