Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -289,3 +289,7 @@ profiles.json
# MSBuildCache
/MSBuildCacheLogs/
*.DS_Store

# Coverage reports
.coverage
coverage.xml
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,18 @@ Documentation](https://github.com/SoftwareDevLabs) repository.
```
---

## 🚀 Modules

### Agents

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.

### Parsers

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.

---

## ⚡ Best Practices

- Track prompt versions and results
Expand Down
29 changes: 21 additions & 8 deletions src/agents/deepagent.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""LangChain agent integration using OpenAI LLM and standard tools."""

import os
import re
import yaml
from typing import Any, Optional, List

Expand Down Expand Up @@ -80,7 +81,7 @@ def __init__(

if self.dry_run:
self.tools = tools or [EchoTool()]
self.agent = MockAgent()
self.agent = MockAgent(tools=self.tools)
return

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


class MockAgent:
"""A trivial agent used for dry-run and CI that only echoes input."""
def __init__(self):
"""A mock agent for dry-run and CI that can echo or use tools."""
def __init__(self, tools: Optional[List[BaseTool]] = None):
self.last_input = None
self.tools = tools or []

def invoke(self, input_data: dict, config: dict):
self.last_input = input_data["input"]

# Simple logic to simulate tool use for testing
if "parse" in self.last_input.lower():
for tool in self.tools:
if tool.name == "DiagramParserTool":
# Extract file path from prompt (simple parsing)
match = re.search(r"\'(.*?)\'", self.last_input)
if match:
file_path = match.group(1)
return {"output": tool._run(file_path)}

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


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

import argparse
from dotenv import load_dotenv

if __name__ == "__main__":
import argparse
from dotenv import load_dotenv
main()
2 changes: 2 additions & 0 deletions src/parsers/database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,5 +346,7 @@ def get_all_diagrams(self) -> List[DiagramRecord]:
def delete_diagram(self, diagram_id: int) -> bool:
"""Delete a diagram and all its related records."""
with sqlite3.connect(self.db_path) as conn:
conn.execute("PRAGMA foreign_keys = ON")
cursor = conn.execute('DELETE FROM diagrams WHERE id = ?', (diagram_id,))
conn.commit()
return cursor.rowcount > 0
36 changes: 20 additions & 16 deletions src/parsers/drawio_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,22 +234,26 @@ def _determine_element_type(self, style: str, value: str) -> ElementType:

def _determine_relationship_type(self, style: str, value: str) -> str:
"""Determine relationship type based on style and content."""
style_lower = style.lower()
value_lower = value.lower() if value else ''

# Check arrow types and line styles
if 'inheritance' in style_lower or 'extends' in value_lower:
return 'inheritance'
elif 'composition' in style_lower or 'filled' in style_lower:
return 'composition'
elif 'aggregation' in style_lower:
return 'aggregation'
elif 'dashed' in style_lower or 'dotted' in style_lower:
return 'dependency'
elif 'implements' in value_lower:
return 'realization'
else:
return 'association'
style_props = self._parse_style(style)
value_lower = value.lower() if value else ""

end_arrow = style_props.get("endArrow")
end_fill = style_props.get("endFill")

if end_arrow == "block" and end_fill == "0":
return "inheritance"
if end_arrow == "diamond" and end_fill == "1":
return "composition"
if end_arrow == "diamond" and end_fill == "0":
return "aggregation"
if style_props.get("dashed") == "1":
return "dependency"
if "extends" in value_lower:
return "inheritance"
if "implements" in value_lower:
return "realization"

return "association"

def _parse_style(self, style: str) -> Dict[str, str]:
"""Parse DrawIO style string into properties."""
Expand Down
190 changes: 90 additions & 100 deletions src/parsers/mermaid_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,76 +175,84 @@ def _parse_class_relationships(self, line: str, diagram: ParsedDiagram):
def _parse_flowchart(self, content: str, diagram: ParsedDiagram):
"""Parse flowchart/graph diagram."""
lines = content.split('\n')[1:] # Skip diagram type line

# Track created nodes to avoid duplicates
created_nodes = set()

for line in lines:
line = line.strip()
if not line:
continue

# Node definitions with labels: A[Label] or A(Label) or A{Label}

def parse_and_create_node(node_str: str):
"""Parse a node string and create a DiagramElement if it doesn't exist."""
node_str = node_str.strip()
node_patterns = [
(r'(\w+)\[([^\]]+)\]', 'rectangular'),
(r'(\w+)\(([^)]+)\)', 'rounded'),
(r'(\w+)\{([^}]+)\}', 'diamond'),
(r'(\w+)\(\(([^)]+)\)\)', 'circle'),
(r'^(\w+)\s*\(\((.*)\)\)$', 'circle'),
(r'^(\w+)\s*\[(.*)\]$', 'rectangular'),
(r'^(\w+)\s*\((.*)\)$', 'rounded'),
(r'^(\w+)\s*\{(.*)\}$', 'diamond'),
]

for pattern, shape in node_patterns:
match = re.search(pattern, line)
match = re.match(pattern, node_str)
if match:
node_id = match.group(1)
label = match.group(2)

node_id, label = match.groups()
if node_id not in created_nodes:
element = DiagramElement(
id=node_id,
element_type=ElementType.COMPONENT,
name=label,
properties={'shape': shape},
tags=[]
id=node_id, element_type=ElementType.COMPONENT,
name=label, properties={'shape': shape}, tags=[]
)
diagram.elements.append(element)
created_nodes.add(node_id)
return node_id

# Connection patterns: A --> B or A --- B
node_id = node_str
if node_id and node_id not in created_nodes:
element = DiagramElement(
id=node_id, element_type=ElementType.COMPONENT,
name=node_id, properties={'shape': 'simple'}, tags=[]
)
diagram.elements.append(element)
created_nodes.add(node_id)
return node_id

for line in lines:
line = line.strip()
if not line:
continue

connection_patterns = [
(r'(\w+)\s*-->\s*(\w+)', 'directed'),
(r'(\w+)\s*---\s*(\w+)', 'undirected'),
(r'(\w+)\s*-\.->\s*(\w+)', 'dotted'),
(r'(\w+)\s*==>\s*(\w+)', 'thick'),
(r'-->', 'directed'), (r'---', 'undirected'),
(r'-.->', 'dotted'), (r'==>', 'thick')
]

for pattern, style in connection_patterns:
match = re.search(pattern, line)
if match:
source = match.group(1)
target = match.group(2)
found_connection = False
for arrow, style in connection_patterns:
if arrow in line:
parts = line.split(arrow, 1)
source_str = parts[0]
target_and_label_str = parts[1]

label_match = re.match(r'\s*\|(.*?)\|(.*)', target_and_label_str)
if label_match:
label = label_match.group(1)
target_str = label_match.group(2).strip()
else:
label = None
target_str = target_and_label_str.strip()

# Create nodes if they don't exist (simple node without labels)
for node_id in [source, target]:
if node_id not in created_nodes:
element = DiagramElement(
id=node_id,
element_type=ElementType.COMPONENT,
name=node_id,
properties={'shape': 'simple'},
tags=[]
)
diagram.elements.append(element)
created_nodes.add(node_id)
source_id = parse_and_create_node(source_str)
target_id = parse_and_create_node(target_str)

relationship = DiagramRelationship(
id=f"rel_{len(diagram.relationships) + 1}",
source_id=source,
target_id=target,
relationship_type='connection',
properties={'style': style},
tags=[]
)
diagram.relationships.append(relationship)
if source_id and target_id:
properties = {'style': style}
if label:
properties['label'] = label

relationship = DiagramRelationship(
id=f"rel_{len(diagram.relationships) + 1}",
source_id=source_id, target_id=target_id,
relationship_type='connection', properties=properties, tags=[]
)
diagram.relationships.append(relationship)
found_connection = True
break

if not found_connection:
parse_and_create_node(line)

def _parse_sequence_diagram(self, content: str, diagram: ParsedDiagram):
"""Parse sequence diagram."""
Expand Down Expand Up @@ -278,7 +286,7 @@ def _parse_sequence_diagram(self, content: str, diagram: ParsedDiagram):
message_patterns = [
(r'(\w+)\s*->>\s*(\w+)\s*:\s*(.+)', 'async_message'),
(r'(\w+)\s*->\s*(\w+)\s*:\s*(.+)', 'sync_message'),
(r'(\w+)\s*-->\s*(\w+)\s*:\s*(.+)', 'return_message'),
(r'(\w+)\s*-->>\s*(\w+)\s*:\s*(.+)', 'return_message'),
]

for pattern, msg_type in message_patterns:
Expand Down Expand Up @@ -314,54 +322,37 @@ def _parse_sequence_diagram(self, content: str, diagram: ParsedDiagram):

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

for entity_name, attributes_text in entities_found:
attributes = []
if attributes_text:
attr_lines = [attr.strip() for attr in attributes_text.split('\n') if attr.strip()]
for attr_line in attr_lines:
if attr_line:
attributes.append(attr_line)

element = DiagramElement(
id=entity_name,
element_type=ElementType.ENTITY,
name=entity_name,
properties={'attributes': attributes},
tags=[]
)
diagram.elements.append(element)

# Remove entity blocks from content to parse relationships
content_after_entities = re.sub(entity_pattern, '', content, flags=re.DOTALL)
lines = content_after_entities.split('\n')

for line in lines:
line = line.strip()
if not line:
continue

# Entity definition with attributes: ENTITY { attr1 attr2 }
entity_match = re.match(r'(\w+)\s*\{([^}]*)\}', line)
if entity_match:
entity_name = entity_match.group(1)
attributes_text = entity_match.group(2)

attributes = []
if attributes_text:
attr_lines = [attr.strip() for attr in attributes_text.split('\n') if attr.strip()]
for attr_line in attr_lines:
if attr_line: # Skip empty lines
attributes.append(attr_line)

element = DiagramElement(
id=entity_name,
element_type=ElementType.ENTITY,
name=entity_name,
properties={'attributes': attributes},
tags=[]
)
diagram.elements.append(element)
continue

# Entity definition without attributes: ENTITY
simple_entity_match = re.match(r'^(\w+)$', line)
if simple_entity_match and not any(rel_pattern in line for rel_pattern in ['||', '}o', 'o{', '--']):
entity_name = simple_entity_match.group(1)

# Check if entity already exists
if not any(elem.id == entity_name for elem in diagram.elements):
element = DiagramElement(
id=entity_name,
element_type=ElementType.ENTITY,
name=entity_name,
properties={'attributes': []},
tags=[]
)
diagram.elements.append(element)
continue

# Relationship patterns: A ||--o{ B
# Relationship patterns
rel_patterns = [
(r'(\w+)\s*\|\|--o\{\s*(\w+)', 'one_to_many'),
(r'(\w+)\s*\}o--\|\|\s*(\w+)', 'many_to_one'),
Expand All @@ -370,10 +361,9 @@ def _parse_er_diagram(self, content: str, diagram: ParsedDiagram):
]

for pattern, rel_type in rel_patterns:
match = re.match(pattern, line)
match = re.search(pattern, line)
if match:
source = match.group(1)
target = match.group(2)
source, target = match.groups()

relationship = DiagramRelationship(
id=f"rel_{len(diagram.relationships) + 1}",
Expand Down
Loading
Loading