Skip to content

Commit 18c72f7

Browse files
vinod0mgoogle-labs-jules[bot]Copilot
authored
Feature/testing and docs (#36)
* This commit introduces a significant number of improvements to the testing suite and documentation for the agents and parsers modules. Key changes include: - Fixed 9 pre-existing failing unit tests in the parsers module. - Added new unit tests to improve test coverage for `deepagent.py` and `database/utils.py`. - Created a new `ParserTool` to integrate the agents and parsers modules. - Added a new integration test for the `ParserTool`. - Added a new end-to-end test to verify the agent-parser workflow. - Updated the root README.md with overviews of the agents and parsers modules. - Added coverage files to .gitignore. The entire test suite is now passing. * This commit introduces a significant number of improvements to the testing suite and documentation for the agents and parsers modules. Key changes include: - Fixed 9 pre-existing failing unit tests in the parsers module. - Added new unit tests to improve test coverage for `deepagent.py` and `database/utils.py`. - Created a new `ParserTool` to integrate the agents and parsers modules. - Added a new integration test for the `ParserTool`. - Added a new end-to-end test to verify the agent-parser workflow. - Updated the root README.md with overviews of the agents and parsers modules. - Added coverage files to .gitignore. The entire test suite is now passing. * Update src/parsers/plantuml_parser.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/parsers/mermaid_parser.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent c7f051d commit 18c72f7

File tree

13 files changed

+423
-130
lines changed

13 files changed

+423
-130
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,3 +289,7 @@ profiles.json
289289
# MSBuildCache
290290
/MSBuildCacheLogs/
291291
*.DS_Store
292+
293+
# Coverage reports
294+
.coverage
295+
coverage.xml

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,18 @@ Documentation](https://github.com/SoftwareDevLabs) repository.
7878
```
7979
---
8080

81+
## 🚀 Modules
82+
83+
### Agents
84+
85+
The `agents` module provides the core components for creating AI agents. It includes a flexible `SDLCFlexibleAgent` that can be configured to use different LLM providers (like OpenAI, Gemini, and Ollama) and a set of tools. The module is designed to be extensible, allowing for the creation of custom agents with specialized skills. Key components include a planner and an executor (currently placeholders for future development) and a `MockAgent` for testing and CI.
86+
87+
### Parsers
88+
89+
The `parsers` module is a powerful utility for parsing various diagram-as-code formats, including PlantUML, Mermaid, and DrawIO. It extracts structured information from diagram files, such as elements, relationships, and metadata, and stores it in a local SQLite database. This allows for complex querying, analysis, and export of diagram data. The module is built on a base parser abstraction, making it easy to extend with new diagram formats. It also includes a suite of utility functions for working with the diagram database, such as exporting to JSON/CSV, finding orphaned elements, and detecting circular dependencies.
90+
91+
---
92+
8193
## ⚡ Best Practices
8294

8395
- Track prompt versions and results

src/agents/deepagent.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""LangChain agent integration using OpenAI LLM and standard tools."""
22

33
import os
4+
import re
45
import yaml
56
from typing import Any, Optional, List
67

@@ -80,7 +81,7 @@ def __init__(
8081

8182
if self.dry_run:
8283
self.tools = tools or [EchoTool()]
83-
self.agent = MockAgent()
84+
self.agent = MockAgent(tools=self.tools)
8485
return
8586

8687
# Configure agent from YAML
@@ -141,13 +142,24 @@ def run(self, input_data: str, session_id: str = "default"):
141142

142143

143144
class MockAgent:
144-
"""A trivial agent used for dry-run and CI that only echoes input."""
145-
def __init__(self):
145+
"""A mock agent for dry-run and CI that can echo or use tools."""
146+
def __init__(self, tools: Optional[List[BaseTool]] = None):
146147
self.last_input = None
148+
self.tools = tools or []
149+
150+
def invoke(self, input_data: dict, config: dict):
151+
self.last_input = input_data["input"]
152+
153+
# Simple logic to simulate tool use for testing
154+
if "parse" in self.last_input.lower():
155+
for tool in self.tools:
156+
if tool.name == "DiagramParserTool":
157+
# Extract file path from prompt (simple parsing)
158+
match = re.search(r"\'(.*?)\'", self.last_input)
159+
if match:
160+
file_path = match.group(1)
161+
return {"output": tool._run(file_path)}
147162

148-
def invoke(self, input_dict: dict, config: dict):
149-
def invoke(self, input: dict, config: dict):
150-
self.last_input = input["input"]
151163
return {"output": f"dry-run-echo:{self.last_input}"}
152164

153165

@@ -183,7 +195,8 @@ def main():
183195
except (ValueError, RuntimeError) as e:
184196
print(f"Error: {e}")
185197

198+
import argparse
199+
from dotenv import load_dotenv
200+
186201
if __name__ == "__main__":
187-
import argparse
188-
from dotenv import load_dotenv
189202
main()

src/parsers/database/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,5 +346,7 @@ def get_all_diagrams(self) -> List[DiagramRecord]:
346346
def delete_diagram(self, diagram_id: int) -> bool:
347347
"""Delete a diagram and all its related records."""
348348
with sqlite3.connect(self.db_path) as conn:
349+
conn.execute("PRAGMA foreign_keys = ON")
349350
cursor = conn.execute('DELETE FROM diagrams WHERE id = ?', (diagram_id,))
351+
conn.commit()
350352
return cursor.rowcount > 0

src/parsers/drawio_parser.py

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -234,22 +234,26 @@ def _determine_element_type(self, style: str, value: str) -> ElementType:
234234

235235
def _determine_relationship_type(self, style: str, value: str) -> str:
236236
"""Determine relationship type based on style and content."""
237-
style_lower = style.lower()
238-
value_lower = value.lower() if value else ''
239-
240-
# Check arrow types and line styles
241-
if 'inheritance' in style_lower or 'extends' in value_lower:
242-
return 'inheritance'
243-
elif 'composition' in style_lower or 'filled' in style_lower:
244-
return 'composition'
245-
elif 'aggregation' in style_lower:
246-
return 'aggregation'
247-
elif 'dashed' in style_lower or 'dotted' in style_lower:
248-
return 'dependency'
249-
elif 'implements' in value_lower:
250-
return 'realization'
251-
else:
252-
return 'association'
237+
style_props = self._parse_style(style)
238+
value_lower = value.lower() if value else ""
239+
240+
end_arrow = style_props.get("endArrow")
241+
end_fill = style_props.get("endFill")
242+
243+
if end_arrow == "block" and end_fill == "0":
244+
return "inheritance"
245+
if end_arrow == "diamond" and end_fill == "1":
246+
return "composition"
247+
if end_arrow == "diamond" and end_fill == "0":
248+
return "aggregation"
249+
if style_props.get("dashed") == "1":
250+
return "dependency"
251+
if "extends" in value_lower:
252+
return "inheritance"
253+
if "implements" in value_lower:
254+
return "realization"
255+
256+
return "association"
253257

254258
def _parse_style(self, style: str) -> Dict[str, str]:
255259
"""Parse DrawIO style string into properties."""

src/parsers/mermaid_parser.py

Lines changed: 89 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -175,76 +175,84 @@ def _parse_class_relationships(self, line: str, diagram: ParsedDiagram):
175175
def _parse_flowchart(self, content: str, diagram: ParsedDiagram):
176176
"""Parse flowchart/graph diagram."""
177177
lines = content.split('\n')[1:] # Skip diagram type line
178-
179-
# Track created nodes to avoid duplicates
180178
created_nodes = set()
181-
182-
for line in lines:
183-
line = line.strip()
184-
if not line:
185-
continue
186-
187-
# Node definitions with labels: A[Label] or A(Label) or A{Label}
179+
180+
def parse_and_create_node(node_str: str):
181+
"""Parse a node string and create a DiagramElement if it doesn't exist."""
182+
node_str = node_str.strip()
188183
node_patterns = [
189-
(r'(\w+)\[([^\]]+)\]', 'rectangular'),
190-
(r'(\w+)\(([^)]+)\)', 'rounded'),
191-
(r'(\w+)\{([^}]+)\}', 'diamond'),
192-
(r'(\w+)\(\(([^)]+)\)\)', 'circle'),
184+
(r'^(\w+)\s*\(\((.*)\)\)$', 'circle'),
185+
(r'^(\w+)\s*\[(.*)\]$', 'rectangular'),
186+
(r'^(\w+)\s*\((.*)\)$', 'rounded'),
187+
(r'^(\w+)\s*\{(.*)\}$', 'diamond'),
193188
]
194-
195189
for pattern, shape in node_patterns:
196-
match = re.search(pattern, line)
190+
match = re.match(pattern, node_str)
197191
if match:
198-
node_id = match.group(1)
199-
label = match.group(2)
200-
192+
node_id, label = match.groups()
201193
if node_id not in created_nodes:
202194
element = DiagramElement(
203-
id=node_id,
204-
element_type=ElementType.COMPONENT,
205-
name=label,
206-
properties={'shape': shape},
207-
tags=[]
195+
id=node_id, element_type=ElementType.COMPONENT,
196+
name=label, properties={'shape': shape}, tags=[]
208197
)
209198
diagram.elements.append(element)
210199
created_nodes.add(node_id)
200+
return node_id
211201

212-
# Connection patterns: A --> B or A --- B
202+
node_id = node_str
203+
if node_id and node_id not in created_nodes:
204+
element = DiagramElement(
205+
id=node_id, element_type=ElementType.COMPONENT,
206+
name=node_id, properties={'shape': 'simple'}, tags=[]
207+
)
208+
diagram.elements.append(element)
209+
created_nodes.add(node_id)
210+
return node_id
211+
212+
for line in lines:
213+
line = line.strip()
214+
if not line:
215+
continue
216+
213217
connection_patterns = [
214-
(r'(\w+)\s*-->\s*(\w+)', 'directed'),
215-
(r'(\w+)\s*---\s*(\w+)', 'undirected'),
216-
(r'(\w+)\s*-\.->\s*(\w+)', 'dotted'),
217-
(r'(\w+)\s*==>\s*(\w+)', 'thick'),
218+
(r'-->', 'directed'), (r'---', 'undirected'),
219+
(r'-.->', 'dotted'), (r'==>', 'thick')
218220
]
219221

220-
for pattern, style in connection_patterns:
221-
match = re.search(pattern, line)
222-
if match:
223-
source = match.group(1)
224-
target = match.group(2)
222+
found_connection = False
223+
for arrow, style in connection_patterns:
224+
if arrow in line:
225+
parts = line.split(arrow, 1)
226+
source_str = parts[0]
227+
target_and_label_str = parts[1]
228+
229+
label_match = re.match(r'\s*\|(.*?)\|(.*)', target_and_label_str)
230+
if label_match:
231+
label = label_match.group(1)
232+
target_str = label_match.group(2).strip()
233+
else:
234+
label = None
235+
target_str = target_and_label_str.strip()
225236

226-
# Create nodes if they don't exist (simple node without labels)
227-
for node_id in [source, target]:
228-
if node_id not in created_nodes:
229-
element = DiagramElement(
230-
id=node_id,
231-
element_type=ElementType.COMPONENT,
232-
name=node_id,
233-
properties={'shape': 'simple'},
234-
tags=[]
235-
)
236-
diagram.elements.append(element)
237-
created_nodes.add(node_id)
237+
source_id = parse_and_create_node(source_str)
238+
target_id = parse_and_create_node(target_str)
238239

239-
relationship = DiagramRelationship(
240-
id=f"rel_{len(diagram.relationships) + 1}",
241-
source_id=source,
242-
target_id=target,
243-
relationship_type='connection',
244-
properties={'style': style},
245-
tags=[]
246-
)
247-
diagram.relationships.append(relationship)
240+
if source_id and target_id:
241+
properties = {'style': style}
242+
if label:
243+
properties['label'] = label
244+
245+
relationship = DiagramRelationship(
246+
id=f"rel_{len(diagram.relationships) + 1}",
247+
source_id=source_id, target_id=target_id,
248+
relationship_type='connection', properties=properties, tags=[]
249+
)
250+
diagram.relationships.append(relationship)
251+
found_connection = True
252+
break
253+
254+
if not found_connection:
255+
parse_and_create_node(line)
248256

249257
def _parse_sequence_diagram(self, content: str, diagram: ParsedDiagram):
250258
"""Parse sequence diagram."""
@@ -314,54 +322,37 @@ def _parse_sequence_diagram(self, content: str, diagram: ParsedDiagram):
314322

315323
def _parse_er_diagram(self, content: str, diagram: ParsedDiagram):
316324
"""Parse entity-relationship diagram."""
317-
lines = content.split('\n')[1:] # Skip diagram type line
325+
# Parse entities first, handling multiline blocks
326+
entity_pattern = r'(\w+)\s*\{([^}]*)\}'
327+
entities_found = re.findall(entity_pattern, content, re.DOTALL)
318328

329+
for entity_name, attributes_text in entities_found:
330+
attributes = []
331+
if attributes_text:
332+
attr_lines = [attr.strip() for attr in attributes_text.split('\n') if attr.strip()]
333+
for attr_line in attr_lines:
334+
if attr_line:
335+
attributes.append(attr_line)
336+
337+
element = DiagramElement(
338+
id=entity_name,
339+
element_type=ElementType.ENTITY,
340+
name=entity_name,
341+
properties={'attributes': attributes},
342+
tags=[]
343+
)
344+
diagram.elements.append(element)
345+
346+
# Remove entity blocks from content to parse relationships
347+
content_after_entities = re.sub(entity_pattern, '', content, flags=re.DOTALL)
348+
lines = content_after_entities.split('\n')
349+
319350
for line in lines:
320351
line = line.strip()
321352
if not line:
322353
continue
323-
324-
# Entity definition with attributes: ENTITY { attr1 attr2 }
325-
entity_match = re.match(r'(\w+)\s*\{([^}]*)\}', line)
326-
if entity_match:
327-
entity_name = entity_match.group(1)
328-
attributes_text = entity_match.group(2)
329-
330-
attributes = []
331-
if attributes_text:
332-
attr_lines = [attr.strip() for attr in attributes_text.split('\n') if attr.strip()]
333-
for attr_line in attr_lines:
334-
if attr_line: # Skip empty lines
335-
attributes.append(attr_line)
336-
337-
element = DiagramElement(
338-
id=entity_name,
339-
element_type=ElementType.ENTITY,
340-
name=entity_name,
341-
properties={'attributes': attributes},
342-
tags=[]
343-
)
344-
diagram.elements.append(element)
345-
continue
346-
347-
# Entity definition without attributes: ENTITY
348-
simple_entity_match = re.match(r'^(\w+)$', line)
349-
if simple_entity_match and not any(rel_pattern in line for rel_pattern in ['||', '}o', 'o{', '--']):
350-
entity_name = simple_entity_match.group(1)
351354

352-
# Check if entity already exists
353-
if not any(elem.id == entity_name for elem in diagram.elements):
354-
element = DiagramElement(
355-
id=entity_name,
356-
element_type=ElementType.ENTITY,
357-
name=entity_name,
358-
properties={'attributes': []},
359-
tags=[]
360-
)
361-
diagram.elements.append(element)
362-
continue
363-
364-
# Relationship patterns: A ||--o{ B
355+
# Relationship patterns
365356
rel_patterns = [
366357
(r'(\w+)\s*\|\|--o\{\s*(\w+)', 'one_to_many'),
367358
(r'(\w+)\s*\}o--\|\|\s*(\w+)', 'many_to_one'),
@@ -370,10 +361,9 @@ def _parse_er_diagram(self, content: str, diagram: ParsedDiagram):
370361
]
371362

372363
for pattern, rel_type in rel_patterns:
373-
match = re.match(pattern, line)
364+
match = re.search(pattern, line)
374365
if match:
375-
source = match.group(1)
376-
target = match.group(2)
366+
source, target = match.groups()
377367

378368
relationship = DiagramRelationship(
379369
id=f"rel_{len(diagram.relationships) + 1}",

src/parsers/plantuml_parser.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,9 @@ def _clean_content(self, content: str) -> str:
5858
# Remove single-line comments
5959
content = re.sub(r"'.*$", "", content, flags=re.MULTILINE)
6060

61-
# Normalize whitespace
62-
content = re.sub(r'\s+', ' ', content)
63-
64-
return content.strip()
61+
# Normalize whitespace but preserve line structure
62+
lines = [line.strip() for line in content.split('\n') if line.strip()]
63+
return '\n'.join(lines)
6564

6665
def _extract_metadata(self, content: str) -> Dict[str, Any]:
6766
"""Extract metadata like title, skinparam, etc."""
@@ -205,7 +204,7 @@ def _extract_relationships(self, content: str) -> List[DiagramRelationship]:
205204
# Association: A -- B, A --> B
206205
(r'(\w+)\s*-->\s*(\w+)', 'association', 'normal'),
207206
(r'(\w+)\s*<--\s*(\w+)', 'association', 'reverse'),
208-
(r'(\w+)\s*--\s*(\w+)(?!\*|o|\|)', 'association', 'normal'),
207+
(r'(\w+)\s*(?<!o)(?<!\*)--\s*(\w+)', 'association', 'normal'),
209208

210209
# Dependency: A ..> B, A <.. B
211210
(r'(\w+)\s*\.\.>\s*(\w+)', 'dependency', 'normal'),

0 commit comments

Comments
 (0)