INTRODUCTION
The analysis of large-scale software systems presents fundamental challenges that traditional tooling struggles to address effectively. When developers attempt to understand complex codebases spanning millions of lines across hundreds of modules, they encounter three primary obstacles: limited context memory in language models, systematic bias in analysis results, and hallucinations where the AI generates plausible but incorrect information. These challenges become particularly acute when analyzing database-intensive architectures where the interplay between data models, business logic, and infrastructure creates intricate dependency networks.
Modern multi-agent AI systems offer promising solutions to these challenges through architectural patterns that distribute cognitive load, validate information through multiple perspectives, and ground analysis in verifiable facts. This article explores how developers building such analysis tools can leverage specific patterns and practices to create robust, accurate, and scalable solutions for understanding large software systems.
THE CHALLENGE LANDSCAPE
Before diving into solutions, we must understand why analyzing large codebases presents unique difficulties. A typical enterprise database application might contain thousands of classes, hundreds of database tables, dozens of microservices, and complex deployment pipelines. When an AI attempts to analyze such a system holistically, it faces the context window limitation where only a fraction of the codebase fits into the model's working memory at once. This is analogous to trying to understand a novel by reading only three pages at a time without notes.
Bias emerges when the training data of language models contains patterns that don't match the specific architectural decisions of the system under analysis. For instance, a model trained predominantly on open-source projects might misinterpret proprietary patterns or domain-specific conventions. Hallucinations occur when the model fills knowledge gaps with plausible-sounding but factually incorrect information, such as inventing method signatures that don't exist or describing architectural layers that aren't present in the actual system.
HUMAN IN THE LOOP PATTERN
The human-in-the-loop pattern represents a fundamental acknowledgment that AI systems should not operate in isolation when making critical architectural assessments. This pattern involves strategically placing decision points where human experts validate assumptions, provide missing context, or approve proposed changes before they propagate through the analysis pipeline.
Consider a scenario where the AI encounters an ambiguous class relationship. Rather than making an assumption that might cascade into incorrect conclusions, the system should pause and query the human operator. The implementation requires careful design of the interaction protocol to avoid overwhelming users with trivial questions while ensuring critical decisions receive human oversight.
A practical implementation might look like this in Python:
class HumanValidationService:
"""
Service for requesting human validation of AI assumptions
during architectural analysis.
"""
def __init__(self, validation_threshold=0.7):
"""
Initialize the validation service.
Args:
validation_threshold: Confidence level below which
human validation is required
"""
self.validation_threshold = validation_threshold
self.pending_validations = []
self.validation_history = {}
def request_validation(self, context, assumption, confidence):
"""
Request human validation for an assumption made during analysis.
Args:
context: Dictionary containing relevant code context
assumption: String describing the assumption being made
confidence: Float between 0 and 1 indicating AI confidence
Returns:
Boolean indicating whether assumption was validated
"""
if confidence >= self.validation_threshold:
# High confidence, proceed without human validation
return True
validation_request = {
'context': context,
'assumption': assumption,
'confidence': confidence,
'timestamp': datetime.now()
}
self.pending_validations.append(validation_request)
# Present to human operator
response = self._present_to_human(validation_request)
# Store in history for learning
self.validation_history[assumption] = response
return response['approved']
def _present_to_human(self, request):
"""
Present validation request to human operator.
This would integrate with a UI in production.
"""
print(f"\nValidation Required (Confidence: {request['confidence']:.2f})")
print(f"Context: {request['context']['description']}")
print(f"Assumption: {request['assumption']}")
print("\nRelevant Code:")
print(request['context']['code_snippet'])
# In production, this would be an async UI interaction
user_input = input("\nApprove this assumption? (y/n): ")
return {
'approved': user_input.lower() == 'y',
'feedback': input("Additional context (optional): ")
}
This service integrates into the broader analysis pipeline at decision points. For example, when determining whether a class implements a particular design pattern, the system checks its confidence level and requests human validation if uncertainty exceeds acceptable thresholds. The validation history allows the system to learn from past decisions and potentially adjust its confidence calibration over time.
The key insight is that human validation should be selective and contextual. Asking for validation on every minor detail creates cognitive overload and defeats the purpose of automation. Instead, the system should identify high-impact decisions where errors would cascade through the analysis, such as identifying system boundaries, determining the primary architectural style, or classifying critical business entities.
LIGHTWEIGHT PROMPT DECOMPOSITION
Rather than constructing massive prompts that attempt to analyze entire subsystems in a single pass, effective analysis tools decompose complex analytical tasks into sequences of focused, lightweight prompts. This approach offers multiple advantages: it reduces the cognitive load on each individual model invocation, allows for better error isolation and recovery, enables parallel processing of independent subtasks, and makes the analysis process more transparent and debuggable.
Consider the task of analyzing a database access layer. A monolithic prompt might attempt to understand the entire layer's architecture, identify all data access patterns, detect potential performance issues, and suggest improvements simultaneously. This creates a complex analytical burden that increases the likelihood of hallucinations and makes it difficult to validate individual conclusions.
Instead, we decompose this into a sequence of focused prompts. First, we identify the structural components of the data access layer. Second, we analyze the patterns used in each component independently. Third, we examine cross-component interactions. Fourth, we assess performance characteristics. Finally, we synthesize findings into recommendations. Each step builds on validated results from previous steps, creating a more reliable analytical chain.
Here is an implementation demonstrating this decomposition:
class PromptDecompositionEngine:
"""
Engine for decomposing complex analysis tasks into
sequences of lightweight, focused prompts.
"""
def __init__(self, llm_client):
"""
Initialize the decomposition engine.
Args:
llm_client: Client for interacting with language model
"""
self.llm_client = llm_client
self.task_graph = {}
self.results_cache = {}
def analyze_data_access_layer(self, code_artifacts):
"""
Analyze a data access layer through decomposed prompts.
Args:
code_artifacts: List of code files in the data access layer
Returns:
Comprehensive analysis results
"""
# Step 1: Identify structural components
components = self._identify_components(code_artifacts)
# Step 2: Analyze patterns in each component
pattern_analysis = {}
for component in components:
pattern_analysis[component['name']] = \
self._analyze_component_patterns(component)
# Step 3: Examine cross-component interactions
interactions = self._analyze_interactions(
components,
pattern_analysis
)
# Step 4: Assess performance characteristics
performance = self._assess_performance(
components,
interactions
)
# Step 5: Synthesize recommendations
recommendations = self._synthesize_recommendations(
components,
pattern_analysis,
interactions,
performance
)
return {
'components': components,
'patterns': pattern_analysis,
'interactions': interactions,
'performance': performance,
'recommendations': recommendations
}
def _identify_components(self, code_artifacts):
"""
First decomposed step: identify structural components.
"""
prompt = self._build_component_identification_prompt(
code_artifacts
)
response = self.llm_client.generate(
prompt,
max_tokens=1000,
temperature=0.3 # Lower temperature for factual analysis
)
# Parse and validate response
components = self._parse_component_response(response)
# Cache results for potential reuse
self.results_cache['components'] = components
return components
def _build_component_identification_prompt(self, artifacts):
"""
Build focused prompt for component identification.
"""
# Include only relevant code snippets, not entire files
code_summary = self._summarize_artifacts(artifacts)
prompt = f"""Analyze the following data access layer code and identify
the distinct structural components (repositories, DAOs, query builders, etc.).
For each component, provide:
1. Component name
2. Primary responsibility
3. Files involved
Code Summary:
{code_summary}
Respond in JSON format with an array of components."""
return prompt
def _analyze_component_patterns(self, component):
"""
Second decomposed step: analyze patterns in a single component.
"""
prompt = f"""Analyze the design patterns used in this component:
Component: {component['name']}
Responsibility: {component['responsibility']}
Code:
{component['code']}
Identify:
1. Primary design pattern (Repository, DAO, Active Record, etc.)
2. Secondary patterns used
3. Pattern implementation quality
Respond in JSON format."""
response = self.llm_client.generate(
prompt,
max_tokens=800,
temperature=0.3
)
return self._parse_pattern_response(response)
def _summarize_artifacts(self, artifacts):
"""
Create concise summary of code artifacts for prompt inclusion.
"""
summary_parts = []
for artifact in artifacts[:10]: # Limit to prevent context overflow
summary_parts.append(
f"File: {artifact['path']}\n"
f"Classes: {', '.join(artifact['classes'])}\n"
f"Key Methods: {', '.join(artifact['key_methods'][:5])}\n"
)
return '\n'.join(summary_parts)
This decomposition approach ensures that each prompt has a clear, focused objective and receives only the context necessary to accomplish that objective. The sequential nature allows each step to build on validated results from previous steps, reducing the compounding of errors that can occur in monolithic analysis.
The temperature parameter in the language model calls deserves attention. Lower temperatures around 0.3 produce more deterministic, factual outputs suitable for structural analysis. Higher temperatures might be appropriate for creative tasks like generating alternative architectural suggestions, but for factual code analysis, consistency and accuracy take precedence over creativity.
PROMPT OPTIMIZATION STRATEGIES
Effective prompt engineering forms the foundation of reliable AI-driven code analysis. Optimization involves both the structure of individual prompts and the orchestration of prompt sequences. The goal is to maximize the accuracy and relevance of responses while minimizing token usage and latency.
Several key principles guide prompt optimization. First, prompts should be specific and unambiguous, leaving no room for interpretation about what information is requested. Second, prompts should provide sufficient context without overwhelming the model with irrelevant details. Third, prompts should specify the desired output format explicitly, particularly when the response will be parsed programmatically. Fourth, prompts should include examples when the task involves pattern recognition or classification.
Consider the difference between a poorly optimized prompt and an optimized version for identifying architectural layers:
Poor version: "Look at this code and tell me about the architecture."
This prompt is vague about what architectural aspects matter, provides no context about the system, and doesn't specify output format. The model might respond with generic architectural principles rather than specific analysis of the provided code.
Optimized version:
def build_layer_identification_prompt(code_context, system_type):
"""
Build optimized prompt for architectural layer identification.
Args:
code_context: Dictionary with code samples and metadata
system_type: String indicating system type (web, batch, etc.)
Returns:
Optimized prompt string
"""
prompt = f"""You are analyzing a {system_type} application to identify
its architectural layers.
System Context:
- Technology Stack: {code_context['tech_stack']}
- Primary Language: {code_context['language']}
- Framework: {code_context['framework']}
Code Sample from Module: {code_context['module_name']}
{code_context['code_sample']}
Task: Identify which architectural layer this module belongs to.
Common layers in {system_type} applications:
- Presentation Layer: UI components, controllers, view models
- Application Layer: Use cases, application services, DTOs
- Domain Layer: Business entities, domain services, business rules
- Infrastructure Layer: Database access, external services, frameworks
Respond in this exact JSON format:
{{
"primary_layer": "layer name",
"confidence": 0.0 to 1.0,
"reasoning": "explanation of classification",
"indicators": ["specific code elements that indicate this layer"]
}}
Base your classification on actual code patterns, not assumptions."""
return prompt
This optimized prompt provides clear context about the system type, includes relevant background about common architectural layers, specifies the exact JSON output format expected, and explicitly instructs the model to base conclusions on observable code patterns rather than assumptions. The confidence field allows the system to trigger human validation when uncertainty is high.
Prompt optimization also involves iterative refinement based on observed model behavior. If the model consistently misclassifies certain patterns, the prompt can be enhanced with negative examples or additional clarifying instructions. This creates a feedback loop where prompt quality improves over time based on real-world usage.
Another optimization technique involves prompt templates with variable injection points. Rather than constructing prompts from scratch for each analysis task, we maintain a library of proven templates and inject specific context at runtime:
class PromptTemplateLibrary:
"""
Library of optimized prompt templates for various analysis tasks.
"""
def __init__(self):
self.templates = self._initialize_templates()
self.performance_metrics = {}
def _initialize_templates(self):
"""
Initialize library with proven prompt templates.
"""
return {
'layer_identification': {
'template': """Analyze the following {language} code module
to determine its architectural layer.
System Type: {system_type}
Module: {module_name}
Code:
{code_sample}
Identify the layer based on these characteristics:
{layer_characteristics}
Output Format:
{output_format}""",
'required_vars': [
'language',
'system_type',
'module_name',
'code_sample',
'layer_characteristics',
'output_format'
]
},
'pattern_detection': {
'template': """Identify design patterns in this {language} code.
Context: {context}
Code:
{code_sample}
Focus on these pattern categories:
{pattern_categories}
For each pattern found, provide:
1. Pattern name
2. Confidence level
3. Code elements demonstrating the pattern
4. Pattern quality assessment
{output_format}""",
'required_vars': [
'language',
'context',
'code_sample',
'pattern_categories',
'output_format'
]
}
}
def get_prompt(self, template_name, variables):
"""
Generate prompt from template with variable substitution.
Args:
template_name: Name of template to use
variables: Dictionary of variables to inject
Returns:
Formatted prompt string
"""
if template_name not in self.templates:
raise ValueError(f"Unknown template: {template_name}")
template_config = self.templates[template_name]
# Validate all required variables are provided
missing = set(template_config['required_vars']) - set(variables.keys())
if missing:
raise ValueError(f"Missing required variables: {missing}")
# Substitute variables into template
prompt = template_config['template'].format(**variables)
return prompt
def track_performance(self, template_name, accuracy, latency):
"""
Track performance metrics for prompt templates.
Args:
template_name: Name of template used
accuracy: Accuracy score for this usage
latency: Response latency in seconds
"""
if template_name not in self.performance_metrics:
self.performance_metrics[template_name] = {
'uses': 0,
'total_accuracy': 0.0,
'total_latency': 0.0
}
metrics = self.performance_metrics[template_name]
metrics['uses'] += 1
metrics['total_accuracy'] += accuracy
metrics['total_latency'] += latency
def get_best_template(self, task_type):
"""
Get best performing template for a task type.
Args:
task_type: Type of analysis task
Returns:
Name of best performing template
"""
candidates = [
name for name in self.templates.keys()
if task_type in name
]
if not candidates:
return None
# Select based on accuracy-weighted performance
best = max(
candidates,
key=lambda name: (
self.performance_metrics.get(name, {}).get('total_accuracy', 0) /
max(self.performance_metrics.get(name, {}).get('uses', 1), 1)
)
)
return best
This template library approach provides several benefits. Templates are tested and refined over time, reducing the need to engineer prompts from scratch for each new analysis task. Performance tracking enables data-driven decisions about which templates work best for specific scenarios. The variable injection mechanism ensures consistency while allowing customization for specific contexts.
REASONING STRATEGIES FOR ENHANCED ACCURACY
Advanced reasoning strategies significantly improve the accuracy and reliability of AI-driven code analysis. Three particularly effective approaches are self-reflection, chain-of-thought reasoning, and tree-of-thought exploration. Each strategy addresses different aspects of the analytical challenge.
Self-reflection involves having the AI critique and validate its own conclusions. After generating an initial analysis, the system prompts the model to review its reasoning, identify potential errors or biases, and revise conclusions if necessary. This metacognitive approach often catches logical inconsistencies or unsupported assumptions that would otherwise go unnoticed.
Implementation of self-reflection requires a two-phase process. The first phase generates the initial analysis. The second phase presents that analysis back to the model with instructions to critically evaluate it:
class SelfReflectionAnalyzer:
"""
Analyzer that uses self-reflection to validate conclusions.
"""
def __init__(self, llm_client):
self.llm_client = llm_client
self.reflection_history = []
def analyze_with_reflection(self, code_sample, analysis_type):
"""
Perform analysis with self-reflection validation.
Args:
code_sample: Code to analyze
analysis_type: Type of analysis to perform
Returns:
Validated analysis results
"""
# Phase 1: Initial analysis
initial_prompt = self._build_analysis_prompt(
code_sample,
analysis_type
)
initial_response = self.llm_client.generate(
initial_prompt,
max_tokens=1500,
temperature=0.4
)
initial_analysis = self._parse_response(initial_response)
# Phase 2: Self-reflection
reflection_prompt = self._build_reflection_prompt(
code_sample,
initial_analysis,
analysis_type
)
reflection_response = self.llm_client.generate(
reflection_prompt,
max_tokens=1000,
temperature=0.3
)
reflection_result = self._parse_reflection(reflection_response)
# Combine initial analysis with reflection insights
final_analysis = self._integrate_reflection(
initial_analysis,
reflection_result
)
# Store for learning
self.reflection_history.append({
'initial': initial_analysis,
'reflection': reflection_result,
'final': final_analysis
})
return final_analysis
def _build_reflection_prompt(self, code, initial_analysis, analysis_type):
"""
Build prompt for self-reflection phase.
"""
prompt = f"""Review the following analysis of {analysis_type} and
critically evaluate its accuracy.
Original Code:
{code}
Initial Analysis:
{json.dumps(initial_analysis, indent=2)}
Critically assess this analysis by answering:
1. Are the conclusions supported by observable code patterns?
2. Are there alternative interpretations that better fit the evidence?
3. Are there any logical inconsistencies in the reasoning?
4. What assumptions were made, and are they justified?
5. What evidence contradicts or weakens the conclusions?
Provide your reflection in this format:
{{
"validity_assessment": "strong/moderate/weak",
"unsupported_claims": ["list any claims not supported by code"],
"alternative_interpretations": ["list better interpretations if any"],
"logical_issues": ["list any logical problems"],
"unjustified_assumptions": ["list problematic assumptions"],
"recommended_revisions": ["specific changes to improve accuracy"]
}}"""
return prompt
def _integrate_reflection(self, initial, reflection):
"""
Integrate reflection insights into final analysis.
"""
final = initial.copy()
# Apply recommended revisions
if reflection.get('recommended_revisions'):
for revision in reflection['recommended_revisions']:
final = self._apply_revision(final, revision)
# Add confidence adjustment based on reflection
if reflection.get('validity_assessment') == 'weak':
final['confidence'] *= 0.6
elif reflection.get('validity_assessment') == 'moderate':
final['confidence'] *= 0.8
# Include reflection metadata
final['reflection_metadata'] = {
'validity': reflection.get('validity_assessment'),
'issues_found': len(reflection.get('logical_issues', [])),
'revisions_applied': len(reflection.get('recommended_revisions', []))
}
return final
Chain-of-thought reasoning makes the analytical process explicit by requiring the model to articulate its reasoning steps before reaching conclusions. This approach reduces errors by forcing the model to construct a logical argument rather than jumping directly to answers. For architectural analysis, chain-of-thought prompts might ask the model to first identify relevant code patterns, then explain how those patterns relate to architectural principles, and finally conclude what architectural style is present.
Here is an implementation of chain-of-thought reasoning for dependency analysis:
class ChainOfThoughtAnalyzer:
"""
Analyzer using chain-of-thought reasoning for dependency analysis.
"""
def __init__(self, llm_client):
self.llm_client = llm_client
def analyze_dependencies(self, module_code, module_name):
"""
Analyze module dependencies using chain-of-thought reasoning.
Args:
module_code: Source code of the module
module_name: Name of the module
Returns:
Dependency analysis with reasoning chain
"""
prompt = f"""Analyze the dependencies of this module using step-by-step
reasoning. Work through each step explicitly before reaching conclusions.
Module: {module_name}
Code:
{module_code}
Step 1: Identify all import statements and external references.
List each import and what it provides.
Step 2: Classify each dependency by type.
For each dependency, determine if it is:
- Framework dependency (infrastructure)
- Domain dependency (business logic)
- Utility dependency (helpers, tools)
- External service dependency (APIs, databases)
Step 3: Analyze dependency direction and layering.
For each dependency, determine:
- Which architectural layer does it belong to?
- Is the dependency direction appropriate (following dependency inversion)?
- Are there any circular dependencies?
Step 4: Assess dependency quality.
For each dependency, evaluate:
- Is it necessary or could it be eliminated?
- Does it create tight coupling?
- Does it violate architectural principles?
Step 5: Synthesize conclusions.
Based on the above analysis, provide:
- Overall dependency health score (0-100)
- Key issues identified
- Recommended improvements
Work through each step explicitly, showing your reasoning."""
response = self.llm_client.generate(
prompt,
max_tokens=2500,
temperature=0.3
)
# Parse the step-by-step reasoning
reasoning_chain = self._parse_reasoning_chain(response)
return reasoning_chain
def _parse_reasoning_chain(self, response):
"""
Parse chain-of-thought response into structured format.
"""
# Extract each reasoning step
steps = {}
current_step = None
for line in response.split('\n'):
if line.startswith('Step '):
current_step = line.split(':')[0].strip()
steps[current_step] = []
elif current_step and line.strip():
steps[current_step].append(line.strip())
# Extract final conclusions
conclusions = steps.get('Step 5', [])
return {
'reasoning_steps': steps,
'conclusions': conclusions,
'full_response': response
}
Tree-of-thought reasoning extends chain-of-thought by exploring multiple reasoning paths simultaneously. Rather than following a single analytical path, the system generates several alternative interpretations and evaluates them comparatively. This is particularly valuable when analyzing ambiguous code where multiple architectural interpretations might be plausible.
Implementation of tree-of-thought requires generating multiple reasoning branches and then evaluating them:
class TreeOfThoughtAnalyzer:
"""
Analyzer using tree-of-thought reasoning to explore multiple
interpretations of architectural patterns.
"""
def __init__(self, llm_client, num_branches=3):
self.llm_client = llm_client
self.num_branches = num_branches
def analyze_architecture(self, code_artifacts):
"""
Analyze architecture by exploring multiple interpretation branches.
Args:
code_artifacts: Collection of code files to analyze
Returns:
Best interpretation with comparative analysis
"""
# Generate multiple interpretation branches
branches = []
for i in range(self.num_branches):
branch = self._generate_interpretation_branch(
code_artifacts,
branch_id=i
)
branches.append(branch)
# Evaluate and compare branches
evaluation = self._evaluate_branches(branches, code_artifacts)
# Select best interpretation
best_branch = self._select_best_branch(branches, evaluation)
return {
'selected_interpretation': best_branch,
'alternative_interpretations': [
b for b in branches if b != best_branch
],
'evaluation': evaluation
}
def _generate_interpretation_branch(self, artifacts, branch_id):
"""
Generate one interpretation branch.
"""
prompt = f"""Analyze this codebase and propose an interpretation of
its architectural style. This is interpretation branch {branch_id + 1}.
Consider these possible architectural styles:
- Layered Architecture
- Hexagonal Architecture (Ports and Adapters)
- Clean Architecture
- Microservices Architecture
- Event-Driven Architecture
- CQRS (Command Query Responsibility Segregation)
Code Artifacts Summary:
{self._summarize_artifacts(artifacts)}
Provide your interpretation including:
1. Primary architectural style identified
2. Key evidence supporting this interpretation
3. How major components fit this style
4. Confidence level in this interpretation
Focus on evidence-based reasoning. Consider alternative explanations."""
response = self.llm_client.generate(
prompt,
max_tokens=1500,
temperature=0.6 + (branch_id * 0.1) # Vary temperature per branch
)
return self._parse_interpretation(response, branch_id)
def _evaluate_branches(self, branches, artifacts):
"""
Evaluate and compare interpretation branches.
"""
prompt = f"""Compare these {len(branches)} different interpretations
of the same codebase's architecture.
Code Context:
{self._summarize_artifacts(artifacts)}
Interpretations:
"""
for i, branch in enumerate(branches):
prompt += f"\nInterpretation {i + 1}:\n"
prompt += f"Style: {branch['style']}\n"
prompt += f"Evidence: {branch['evidence']}\n"
prompt += f"Confidence: {branch['confidence']}\n"
prompt += """
Evaluate each interpretation by:
1. How well the evidence supports the claimed style
2. Whether the interpretation accounts for all major components
3. Consistency of the interpretation across the codebase
4. Plausibility given common architectural patterns
Rank the interpretations from most to least plausible and explain why."""
response = self.llm_client.generate(
prompt,
max_tokens=1200,
temperature=0.3
)
return self._parse_evaluation(response)
def _select_best_branch(self, branches, evaluation):
"""
Select best interpretation based on evaluation.
"""
# Extract ranking from evaluation
ranking = evaluation.get('ranking', [])
if not ranking:
# Fallback to highest confidence
return max(branches, key=lambda b: b.get('confidence', 0))
# Return top-ranked interpretation
best_id = ranking[0]
return branches[best_id]
def _summarize_artifacts(self, artifacts):
"""
Create summary of code artifacts for prompt.
"""
summary = []
for artifact in artifacts[:15]:
summary.append(
f"File: {artifact['path']}\n"
f"Type: {artifact['type']}\n"
f"Key Elements: {', '.join(artifact['key_elements'][:5])}"
)
return '\n\n'.join(summary)
These reasoning strategies significantly improve analytical accuracy by making the reasoning process explicit and subject to validation. Self-reflection catches errors in initial analysis. Chain-of-thought ensures logical consistency. Tree-of-thought explores alternative interpretations to avoid premature conclusions. Together, they create a robust analytical framework that produces more reliable results than single-pass analysis.
LANGUAGE PARSERS AND ABSTRACT SYNTAX TREES
Accurate code analysis requires parsing source code into structured representations that AI systems can reliably process. Abstract Syntax Trees provide this structure by representing code as hierarchical trees of syntactic elements. While Python has excellent built-in parsing support through the ast module, analyzing polyglot codebases requires parsers for multiple languages.
For Python, the ast module provides comprehensive parsing capabilities. Here is an implementation that extracts architectural information from Python code:
import ast
import os
from typing import Dict, List, Set
class PythonCodeParser:
"""
Parser for extracting architectural information from Python code.
"""
def __init__(self):
self.parsed_modules = {}
self.dependency_graph = {}
def parse_file(self, filepath):
"""
Parse a Python file and extract architectural elements.
Args:
filepath: Path to Python source file
Returns:
Dictionary containing parsed architectural information
"""
with open(filepath, 'r', encoding='utf-8') as f:
source_code = f.read()
try:
tree = ast.parse(source_code, filename=filepath)
except SyntaxError as e:
return {
'error': f"Syntax error in {filepath}: {str(e)}",
'filepath': filepath
}
# Extract architectural elements
visitor = ArchitectureVisitor(filepath)
visitor.visit(tree)
result = {
'filepath': filepath,
'classes': visitor.classes,
'functions': visitor.functions,
'imports': visitor.imports,
'dependencies': visitor.dependencies,
'complexity_metrics': visitor.complexity_metrics
}
self.parsed_modules[filepath] = result
return result
def parse_directory(self, directory_path):
"""
Recursively parse all Python files in a directory.
Args:
directory_path: Root directory to parse
Returns:
Dictionary mapping filepaths to parse results
"""
results = {}
for root, dirs, files in os.walk(directory_path):
# Skip common non-source directories
dirs[:] = [d for d in dirs if d not in [
'__pycache__', '.git', 'venv', 'env', '.tox'
]]
for filename in files:
if filename.endswith('.py'):
filepath = os.path.join(root, filename)
results[filepath] = self.parse_file(filepath)
return results
def build_dependency_graph(self):
"""
Build dependency graph from parsed modules.
Returns:
Dictionary representing module dependencies
"""
graph = {}
for filepath, module_info in self.parsed_modules.items():
module_name = self._filepath_to_module_name(filepath)
dependencies = set()
for imp in module_info.get('imports', []):
dep_module = imp.get('module')
if dep_module:
dependencies.add(dep_module)
graph[module_name] = list(dependencies)
self.dependency_graph = graph
return graph
def _filepath_to_module_name(self, filepath):
"""
Convert filepath to Python module name.
"""
# Remove .py extension and convert path separators to dots
module = filepath.replace('.py', '').replace(os.sep, '.')
# Remove leading dots
while module.startswith('.'):
module = module[1:]
return module
class ArchitectureVisitor(ast.NodeVisitor):
"""
AST visitor for extracting architectural information.
"""
def __init__(self, filepath):
self.filepath = filepath
self.classes = []
self.functions = []
self.imports = []
self.dependencies = set()
self.complexity_metrics = {
'total_lines': 0,
'code_lines': 0,
'cyclomatic_complexity': 0
}
self.current_class = None
def visit_ClassDef(self, node):
"""
Visit class definition and extract information.
"""
class_info = {
'name': node.name,
'line_number': node.lineno,
'bases': [self._get_name(base) for base in node.bases],
'methods': [],
'decorators': [self._get_name(dec) for dec in node.decorator_list],
'docstring': ast.get_docstring(node)
}
# Store current class context for method processing
previous_class = self.current_class
self.current_class = class_info
# Visit child nodes (methods)
self.generic_visit(node)
# Restore previous context
self.current_class = previous_class
self.classes.append(class_info)
def visit_FunctionDef(self, node):
"""
Visit function/method definition and extract information.
"""
func_info = {
'name': node.name,
'line_number': node.lineno,
'parameters': [arg.arg for arg in node.args.args],
'decorators': [self._get_name(dec) for dec in node.decorator_list],
'docstring': ast.get_docstring(node),
'is_async': isinstance(node, ast.AsyncFunctionDef),
'complexity': self._calculate_complexity(node)
}
if self.current_class:
# This is a method
self.current_class['methods'].append(func_info)
else:
# This is a module-level function
self.functions.append(func_info)
# Update complexity metrics
self.complexity_metrics['cyclomatic_complexity'] += func_info['complexity']
self.generic_visit(node)
def visit_Import(self, node):
"""
Visit import statement and track dependencies.
"""
for alias in node.names:
import_info = {
'type': 'import',
'module': alias.name,
'alias': alias.asname,
'line_number': node.lineno
}
self.imports.append(import_info)
self.dependencies.add(alias.name.split('.')[0])
self.generic_visit(node)
def visit_ImportFrom(self, node):
"""
Visit from-import statement and track dependencies.
"""
module = node.module if node.module else ''
for alias in node.names:
import_info = {
'type': 'from_import',
'module': module,
'name': alias.name,
'alias': alias.asname,
'line_number': node.lineno
}
self.imports.append(import_info)
if module:
self.dependencies.add(module.split('.')[0])
self.generic_visit(node)
def _get_name(self, node):
"""
Extract name from AST node.
"""
if isinstance(node, ast.Name):
return node.id
elif isinstance(node, ast.Attribute):
return f"{self._get_name(node.value)}.{node.attr}"
elif isinstance(node, ast.Call):
return self._get_name(node.func)
else:
return str(node)
def _calculate_complexity(self, node):
"""
Calculate cyclomatic complexity of a function.
Cyclomatic complexity counts decision points in code.
"""
complexity = 1 # Base complexity
for child in ast.walk(node):
# Each decision point adds to complexity
if isinstance(child, (ast.If, ast.While, ast.For, ast.ExceptHandler)):
complexity += 1
elif isinstance(child, ast.BoolOp):
# Each boolean operator adds a decision point
complexity += len(child.values) - 1
return complexity
This Python parser extracts classes, methods, functions, imports, and calculates basic complexity metrics. The AST visitor pattern allows systematic traversal of the syntax tree to collect architectural information. The dependency graph construction enables analysis of module relationships and potential circular dependencies.
For languages beyond Python, tree-sitter provides a universal parsing framework. Tree-sitter generates parsers from grammar definitions and produces consistent ASTs across languages. Here is an implementation using tree-sitter for multi-language parsing:
from tree_sitter import Language, Parser
import tree_sitter_python
import tree_sitter_java
import tree_sitter_javascript
import tree_sitter_go
class MultiLanguageParser:
"""
Parser supporting multiple programming languages via tree-sitter.
"""
def __init__(self):
# Initialize parsers for supported languages
self.parsers = {
'python': self._init_python_parser(),
'java': self._init_java_parser(),
'javascript': self._init_javascript_parser(),
'go': self._init_go_parser()
}
self.language_extensions = {
'.py': 'python',
'.java': 'java',
'.js': 'javascript',
'.jsx': 'javascript',
'.go': 'go'
}
def _init_python_parser(self):
"""Initialize Python parser."""
parser = Parser()
parser.set_language(Language(tree_sitter_python.language()))
return parser
def _init_java_parser(self):
"""Initialize Java parser."""
parser = Parser()
parser.set_language(Language(tree_sitter_java.language()))
return parser
def _init_javascript_parser(self):
"""Initialize JavaScript parser."""
parser = Parser()
parser.set_language(Language(tree_sitter_javascript.language()))
return parser
def _init_go_parser(self):
"""Initialize Go parser."""
parser = Parser()
parser.set_language(Language(tree_sitter_go.language()))
return parser
def parse_file(self, filepath):
"""
Parse a source file in any supported language.
Args:
filepath: Path to source file
Returns:
Dictionary containing parsed information
"""
# Determine language from file extension
extension = os.path.splitext(filepath)[1]
language = self.language_extensions.get(extension)
if not language:
return {
'error': f"Unsupported file type: {extension}",
'filepath': filepath
}
# Read source code
with open(filepath, 'rb') as f:
source_code = f.read()
# Parse with appropriate parser
parser = self.parsers[language]
tree = parser.parse(source_code)
# Extract architectural elements based on language
extractor = self._get_extractor(language)
elements = extractor.extract(tree, source_code)
return {
'filepath': filepath,
'language': language,
'elements': elements,
'ast': tree
}
def _get_extractor(self, language):
"""
Get appropriate element extractor for language.
"""
extractors = {
'python': PythonElementExtractor(),
'java': JavaElementExtractor(),
'javascript': JavaScriptElementExtractor(),
'go': GoElementExtractor()
}
return extractors[language]
class PythonElementExtractor:
"""
Extract architectural elements from Python AST.
"""
def extract(self, tree, source_code):
"""
Extract classes, functions, and imports from Python code.
"""
root_node = tree.root_node
elements = {
'classes': [],
'functions': [],
'imports': []
}
# Query for class definitions
class_query = """
(class_definition
name: (identifier) @class_name
body: (block) @class_body)
"""
# Query for function definitions
function_query = """
(function_definition
name: (identifier) @func_name
parameters: (parameters) @params)
"""
# Query for imports
import_query = """
(import_statement
name: (dotted_name) @module)
"""
# Execute queries and extract information
# Note: Actual tree-sitter query execution would use the query API
# This is a simplified representation
elements['classes'] = self._extract_classes(root_node, source_code)
elements['functions'] = self._extract_functions(root_node, source_code)
elements['imports'] = self._extract_imports(root_node, source_code)
return elements
def _extract_classes(self, node, source_code):
"""Extract class definitions from AST."""
classes = []
def visit(n):
if n.type == 'class_definition':
class_name = None
for child in n.children:
if child.type == 'identifier':
class_name = source_code[child.start_byte:child.end_byte].decode('utf-8')
break
if class_name:
classes.append({
'name': class_name,
'line': n.start_point[0] + 1,
'methods': self._extract_methods(n, source_code)
})
for child in n.children:
visit(child)
visit(node)
return classes
def _extract_methods(self, class_node, source_code):
"""Extract methods from class node."""
methods = []
for node in class_node.children:
if node.type == 'block':
for child in node.children:
if child.type == 'function_definition':
method_name = None
for subchild in child.children:
if subchild.type == 'identifier':
method_name = source_code[subchild.start_byte:subchild.end_byte].decode('utf-8')
break
if method_name:
methods.append({
'name': method_name,
'line': child.start_point[0] + 1
})
return methods
def _extract_functions(self, node, source_code):
"""Extract top-level function definitions."""
functions = []
for child in node.children:
if child.type == 'function_definition':
func_name = None
for subchild in child.children:
if subchild.type == 'identifier':
func_name = source_code[subchild.start_byte:subchild.end_byte].decode('utf-8')
break
if func_name:
functions.append({
'name': func_name,
'line': child.start_point[0] + 1
})
return functions
def _extract_imports(self, node, source_code):
"""Extract import statements."""
imports = []
def visit(n):
if n.type in ['import_statement', 'import_from_statement']:
import_text = source_code[n.start_byte:n.end_byte].decode('utf-8')
imports.append({
'statement': import_text,
'line': n.start_point[0] + 1
})
for child in n.children:
visit(child)
visit(node)
return imports
class JavaElementExtractor:
"""
Extract architectural elements from Java AST.
"""
def extract(self, tree, source_code):
"""
Extract classes, interfaces, methods from Java code.
"""
root_node = tree.root_node
elements = {
'classes': [],
'interfaces': [],
'methods': [],
'imports': []
}
elements['classes'] = self._extract_classes(root_node, source_code)
elements['interfaces'] = self._extract_interfaces(root_node, source_code)
elements['imports'] = self._extract_imports(root_node, source_code)
return elements
def _extract_classes(self, node, source_code):
"""Extract class declarations from Java AST."""
classes = []
def visit(n):
if n.type == 'class_declaration':
class_name = None
modifiers = []
for child in n.children:
if child.type == 'identifier':
class_name = source_code[child.start_byte:child.end_byte].decode('utf-8')
elif child.type == 'modifiers':
modifiers = self._extract_modifiers(child, source_code)
if class_name:
classes.append({
'name': class_name,
'modifiers': modifiers,
'line': n.start_point[0] + 1,
'methods': self._extract_class_methods(n, source_code)
})
for child in n.children:
visit(child)
visit(node)
return classes
def _extract_interfaces(self, node, source_code):
"""Extract interface declarations from Java AST."""
interfaces = []
def visit(n):
if n.type == 'interface_declaration':
interface_name = None
for child in n.children:
if child.type == 'identifier':
interface_name = source_code[child.start_byte:child.end_byte].decode('utf-8')
break
if interface_name:
interfaces.append({
'name': interface_name,
'line': n.start_point[0] + 1
})
for child in n.children:
visit(child)
visit(node)
return interfaces
def _extract_class_methods(self, class_node, source_code):
"""Extract methods from Java class."""
methods = []
def visit(n):
if n.type == 'method_declaration':
method_name = None
return_type = None
for child in n.children:
if child.type == 'identifier':
method_name = source_code[child.start_byte:child.end_byte].decode('utf-8')
elif child.type in ['type_identifier', 'void_type']:
return_type = source_code[child.start_byte:child.end_byte].decode('utf-8')
if method_name:
methods.append({
'name': method_name,
'return_type': return_type,
'line': n.start_point[0] + 1
})
for child in n.children:
visit(child)
visit(class_node)
return methods
def _extract_modifiers(self, modifiers_node, source_code):
"""Extract access modifiers from Java code."""
modifiers = []
for child in modifiers_node.children:
modifier = source_code[child.start_byte:child.end_byte].decode('utf-8')
modifiers.append(modifier)
return modifiers
def _extract_imports(self, node, source_code):
"""Extract import statements from Java code."""
imports = []
def visit(n):
if n.type == 'import_declaration':
import_text = source_code[n.start_byte:n.end_byte].decode('utf-8')
imports.append({
'statement': import_text.strip(),
'line': n.start_point[0] + 1
})
for child in n.children:
visit(child)
visit(node)
return imports
This multi-language parsing infrastructure provides consistent extraction of architectural elements across different programming languages. The tree-sitter approach generates parsers from grammar files, ensuring accurate parsing even as languages evolve. The extracted AST information feeds into downstream analysis agents that identify patterns, assess quality, and map architectural relationships.
The key advantage of using proper parsers rather than regex-based extraction is accuracy and completeness. Parsers understand language syntax and can correctly handle edge cases like nested structures, comments, and string literals that would confuse simpler extraction methods. This accuracy is essential for building reliable knowledge graphs and vector databases that subsequent analysis depends upon.
SPECIALIZED AGENT ARCHITECTURE
A robust code analysis system employs multiple specialized agents, each focused on a specific analytical domain. This multi-agent architecture distributes cognitive load, enables parallel processing, and allows each agent to develop deep expertise in its domain. The agents collaborate through shared knowledge stores and coordinated workflows.
The agent architecture includes several key roles. The code analysis agent focuses on understanding implementation details, identifying patterns, and assessing code quality. The architecture analysis agent examines system structure, component relationships, and architectural styles. The quality attribute agent evaluates non-functional requirements like performance, security, and maintainability. Domain analysis agents understand business concepts and map them to technical implementations. The requirements analysis agent traces requirements through the system. The repository agent analyzes version control history, issues, and development patterns. The knowledge graph agent maintains the semantic network of system entities and relationships. The vector database agent manages embedding-based retrieval. The report agent synthesizes findings into human-readable documentation. The test and quality agent examines test coverage and quality metrics. The operations agent understands deployment and runtime characteristics. The project agent tracks project management aspects. Technology-specific agents provide expertise in particular frameworks or platforms.
Here is an implementation framework for this multi-agent system:
from abc import ABC, abstractmethod
from typing import Dict, List, Any
import asyncio
from dataclasses import dataclass
from enum import Enum
class AgentType(Enum):
"""Types of specialized agents in the system."""
CODE_ANALYSIS = "code_analysis"
ARCHITECTURE_ANALYSIS = "architecture_analysis"
QUALITY_ATTRIBUTE = "quality_attribute"
DOMAIN_ANALYSIS = "domain_analysis"
REQUIREMENTS_ANALYSIS = "requirements_analysis"
REPOSITORY_ANALYSIS = "repository_analysis"
KNOWLEDGE_GRAPH = "knowledge_graph"
VECTOR_DATABASE = "vector_database"
REPORT_GENERATION = "report_generation"
TEST_QUALITY = "test_quality"
OPERATIONS = "operations"
PROJECT_MANAGEMENT = "project_management"
TECHNOLOGY_SPECIFIC = "technology_specific"
@dataclass
class AgentMessage:
"""Message passed between agents."""
sender: str
recipient: str
message_type: str
payload: Dict[str, Any]
correlation_id: str
timestamp: float
class BaseAgent(ABC):
"""
Abstract base class for all specialized agents.
"""
def __init__(self, agent_id: str, agent_type: AgentType, llm_client):
self.agent_id = agent_id
self.agent_type = agent_type
self.llm_client = llm_client
self.message_queue = asyncio.Queue()
self.knowledge_store = {}
self.is_running = False
async def start(self):
"""Start the agent's message processing loop."""
self.is_running = True
await self.process_messages()
async def stop(self):
"""Stop the agent."""
self.is_running = False
async def process_messages(self):
"""Process incoming messages from message queue."""
while self.is_running:
try:
message = await asyncio.wait_for(
self.message_queue.get(),
timeout=1.0
)
await self.handle_message(message)
except asyncio.TimeoutError:
continue
@abstractmethod
async def handle_message(self, message: AgentMessage):
"""
Handle incoming message. Must be implemented by subclasses.
Args:
message: Message to process
"""
pass
@abstractmethod
async def analyze(self, context: Dict[str, Any]) -> Dict[str, Any]:
"""
Perform agent-specific analysis.
Args:
context: Analysis context and input data
Returns:
Analysis results
"""
pass
async def send_message(self, recipient: str, message_type: str,
payload: Dict[str, Any], correlation_id: str):
"""
Send message to another agent.
Args:
recipient: ID of recipient agent
message_type: Type of message
payload: Message data
correlation_id: ID for tracking related messages
"""
message = AgentMessage(
sender=self.agent_id,
recipient=recipient,
message_type=message_type,
payload=payload,
correlation_id=correlation_id,
timestamp=time.time()
)
# In production, this would route through a message broker
await self._route_message(message)
async def _route_message(self, message: AgentMessage):
"""Route message to recipient agent."""
# This would integrate with message broker in production
pass
class CodeAnalysisAgent(BaseAgent):
"""
Agent specialized in analyzing code implementation details.
"""
def __init__(self, agent_id: str, llm_client, code_parser):
super().__init__(agent_id, AgentType.CODE_ANALYSIS, llm_client)
self.code_parser = code_parser
self.pattern_library = self._initialize_pattern_library()
async def handle_message(self, message: AgentMessage):
"""Handle incoming analysis requests."""
if message.message_type == "analyze_code":
result = await self.analyze(message.payload)
# Send results to requesting agent
await self.send_message(
recipient=message.sender,
message_type="analysis_complete",
payload=result,
correlation_id=message.correlation_id
)
async def analyze(self, context: Dict[str, Any]) -> Dict[str, Any]:
"""
Analyze code artifacts for patterns, quality, and structure.
Args:
context: Dictionary containing code files and analysis parameters
Returns:
Analysis results including patterns, metrics, and issues
"""
code_files = context.get('code_files', [])
analysis_depth = context.get('depth', 'standard')
results = {
'patterns': [],
'quality_metrics': {},
'issues': [],
'dependencies': []
}
for code_file in code_files:
# Parse code into AST
parsed = self.code_parser.parse_file(code_file['path'])
# Identify design patterns
patterns = await self._identify_patterns(parsed)
results['patterns'].extend(patterns)
# Calculate quality metrics
metrics = self._calculate_metrics(parsed)
results['quality_metrics'][code_file['path']] = metrics
# Detect code issues
issues = await self._detect_issues(parsed, code_file)
results['issues'].extend(issues)
# Extract dependencies
deps = self._extract_dependencies(parsed)
results['dependencies'].extend(deps)
# Perform cross-file analysis if deep analysis requested
if analysis_depth == 'deep':
cross_file_analysis = await self._cross_file_analysis(code_files)
results['cross_file_insights'] = cross_file_analysis
return results
async def _identify_patterns(self, parsed_code):
"""
Identify design patterns in parsed code using LLM.
"""
# Extract relevant code structures
classes = parsed_code.get('classes', [])
patterns_found = []
for cls in classes:
# Build prompt for pattern identification
prompt = f"""Analyze this class for design patterns:
Class: {cls['name']}
Methods: {', '.join([m['name'] for m in cls.get('methods', [])])}
Base Classes: {', '.join(cls.get('bases', []))}
Common patterns to check:
- Singleton
- Factory
- Observer
- Strategy
- Decorator
- Adapter
Identify any patterns present and explain the evidence."""
response = await self.llm_client.generate_async(
prompt,
max_tokens=500,
temperature=0.3
)
# Parse response and extract patterns
identified = self._parse_pattern_response(response)
if identified:
patterns_found.append({
'class': cls['name'],
'patterns': identified,
'file': parsed_code.get('filepath')
})
return patterns_found
def _calculate_metrics(self, parsed_code):
"""Calculate code quality metrics."""
metrics = {
'lines_of_code': 0,
'cyclomatic_complexity': 0,
'class_count': len(parsed_code.get('classes', [])),
'function_count': len(parsed_code.get('functions', [])),
'average_method_length': 0,
'max_method_complexity': 0
}
# Calculate from parsed data
complexity_values = []
for cls in parsed_code.get('classes', []):
for method in cls.get('methods', []):
complexity = method.get('complexity', 1)
complexity_values.append(complexity)
metrics['cyclomatic_complexity'] += complexity
metrics['max_method_complexity'] = max(
metrics['max_method_complexity'],
complexity
)
if complexity_values:
metrics['average_method_complexity'] = (
sum(complexity_values) / len(complexity_values)
)
return metrics
async def _detect_issues(self, parsed_code, code_file):
"""Detect potential code issues using LLM analysis."""
issues = []
# Check for high complexity methods
for cls in parsed_code.get('classes', []):
for method in cls.get('methods', []):
if method.get('complexity', 0) > 10:
issues.append({
'type': 'high_complexity',
'severity': 'warning',
'location': f"{cls['name']}.{method['name']}",
'message': f"Method has cyclomatic complexity of {method['complexity']}",
'file': code_file['path'],
'line': method.get('line_number')
})
# Use LLM to detect code smells
code_sample = code_file.get('content', '')[:2000] # Limit size
prompt = f"""Analyze this code for common code smells and issues:
{code_sample}
Identify:
1. Long methods (over 50 lines)
2. Large classes (over 500 lines)
3. Duplicate code
4. Poor naming
5. Missing error handling
6. Security concerns
List each issue found with location and severity."""
response = await self.llm_client.generate_async(
prompt,
max_tokens=800,
temperature=0.3
)
llm_issues = self._parse_issues_response(response)
issues.extend(llm_issues)
return issues
def _extract_dependencies(self, parsed_code):
"""Extract code dependencies from imports."""
dependencies = []
for imp in parsed_code.get('imports', []):
dependencies.append({
'module': imp.get('module'),
'type': imp.get('type'),
'file': parsed_code.get('filepath'),
'line': imp.get('line_number')
})
return dependencies
async def _cross_file_analysis(self, code_files):
"""Perform analysis across multiple files."""
# Detect duplicate code across files
duplicates = await self._detect_duplicates(code_files)
# Identify coupling between modules
coupling = self._analyze_coupling(code_files)
return {
'duplicates': duplicates,
'coupling': coupling
}
def _initialize_pattern_library(self):
"""Initialize library of known design patterns."""
return {
'singleton': {
'indicators': ['__new__', 'instance', '_instance'],
'structure': 'single_instance'
},
'factory': {
'indicators': ['create', 'build', 'make'],
'structure': 'object_creation'
},
'observer': {
'indicators': ['subscribe', 'notify', 'observer'],
'structure': 'event_handling'
}
}
class ArchitectureAnalysisAgent(BaseAgent):
"""
Agent specialized in analyzing system architecture.
"""
def __init__(self, agent_id: str, llm_client):
super().__init__(agent_id, AgentType.ARCHITECTURE_ANALYSIS, llm_client)
self.architectural_patterns = self._load_architectural_patterns()
async def handle_message(self, message: AgentMessage):
"""Handle architecture analysis requests."""
if message.message_type == "analyze_architecture":
result = await self.analyze(message.payload)
await self.send_message(
recipient=message.sender,
message_type="architecture_analysis_complete",
payload=result,
correlation_id=message.correlation_id
)
async def analyze(self, context: Dict[str, Any]) -> Dict[str, Any]:
"""
Analyze system architecture including layers, components, and patterns.
Args:
context: System information including code structure and dependencies
Returns:
Architecture analysis results
"""
system_structure = context.get('system_structure', {})
dependency_graph = context.get('dependency_graph', {})
results = {
'architectural_style': None,
'layers': [],
'components': [],
'patterns': [],
'quality_assessment': {}
}
# Identify architectural style
style = await self._identify_architectural_style(
system_structure,
dependency_graph
)
results['architectural_style'] = style
# Identify layers
layers = await self._identify_layers(system_structure)
results['layers'] = layers
# Identify major components
components = await self._identify_components(
system_structure,
dependency_graph
)
results['components'] = components
# Assess architectural quality
quality = await self._assess_architecture_quality(
style,
layers,
components,
dependency_graph
)
results['quality_assessment'] = quality
return results
async def _identify_architectural_style(self, structure, dependencies):
"""
Identify the primary architectural style of the system.
"""
# Analyze structure to determine style
prompt = f"""Analyze this system structure to identify its architectural style.
System has {len(structure.get('modules', []))} modules
Dependency relationships: {len(dependencies)} connections
Module organization:
{self._summarize_structure(structure)}
Common architectural styles:
- Layered Architecture
- Microservices Architecture
- Hexagonal Architecture (Ports and Adapters)
- Event-Driven Architecture
- Clean Architecture
- Monolithic Architecture
Identify the primary style and explain the evidence."""
response = await self.llm_client.generate_async(
prompt,
max_tokens=800,
temperature=0.3
)
return self._parse_style_response(response)
async def _identify_layers(self, structure):
"""Identify architectural layers in the system."""
layers = []
# Group modules by likely layer
module_groups = self._group_modules_by_layer(structure)
for layer_name, modules in module_groups.items():
layers.append({
'name': layer_name,
'modules': modules,
'responsibilities': self._infer_layer_responsibilities(
layer_name,
modules
)
})
return layers
async def _identify_components(self, structure, dependencies):
"""Identify major architectural components."""
components = []
# Use clustering to identify component boundaries
clusters = self._cluster_modules(structure, dependencies)
for cluster in clusters:
component = {
'name': cluster['name'],
'modules': cluster['modules'],
'interfaces': cluster.get('interfaces', []),
'dependencies': cluster.get('dependencies', [])
}
components.append(component)
return components
async def _assess_architecture_quality(self, style, layers,
components, dependencies):
"""Assess quality of the architecture."""
quality_metrics = {}
# Assess layer separation
quality_metrics['layer_separation'] = self._assess_layer_separation(
layers,
dependencies
)
# Assess component cohesion
quality_metrics['component_cohesion'] = self._assess_cohesion(
components
)
# Assess coupling
quality_metrics['coupling'] = self._assess_coupling(
components,
dependencies
)
# Check for architectural violations
violations = await self._detect_violations(
style,
layers,
dependencies
)
quality_metrics['violations'] = violations
# Calculate overall quality score
quality_metrics['overall_score'] = self._calculate_quality_score(
quality_metrics
)
return quality_metrics
def _load_architectural_patterns(self):
"""Load known architectural patterns for reference."""
return {
'layered': {
'layers': ['presentation', 'application', 'domain', 'infrastructure'],
'dependency_rules': 'top_down_only'
},
'hexagonal': {
'core': 'domain',
'adapters': ['primary', 'secondary'],
'dependency_rules': 'inward_only'
},
'microservices': {
'characteristics': ['independent_deployment', 'bounded_context'],
'communication': ['async', 'api']
}
}
def _summarize_structure(self, structure):
"""Create summary of system structure for prompt."""
summary = []
modules = structure.get('modules', [])[:20] # Limit for context
for module in modules:
summary.append(f"- {module['name']}: {module.get('type', 'unknown')}")
return '\n'.join(summary)
def _group_modules_by_layer(self, structure):
"""Group modules into likely architectural layers."""
groups = {
'presentation': [],
'application': [],
'domain': [],
'infrastructure': []
}
for module in structure.get('modules', []):
# Heuristic layer assignment based on module characteristics
module_name = module['name'].lower()
if any(term in module_name for term in ['view', 'controller', 'ui', 'web']):
groups['presentation'].append(module)
elif any(term in module_name for term in ['service', 'usecase', 'application']):
groups['application'].append(module)
elif any(term in module_name for term in ['model', 'entity', 'domain']):
groups['domain'].append(module)
elif any(term in module_name for term in ['repository', 'dao', 'database', 'api']):
groups['infrastructure'].append(module)
return groups
def _cluster_modules(self, structure, dependencies):
"""Cluster related modules into components."""
# Simple clustering based on dependency relationships
# In production, use more sophisticated clustering algorithms
clusters = []
visited = set()
modules = structure.get('modules', [])
for module in modules:
if module['name'] in visited:
continue
# Find connected modules
cluster = self._find_connected_modules(
module,
modules,
dependencies,
visited
)
if cluster:
clusters.append(cluster)
return clusters
def _find_connected_modules(self, start_module, all_modules,
dependencies, visited):
"""Find modules connected to start module."""
cluster_modules = [start_module]
visited.add(start_module['name'])
# Find directly connected modules
connected = [
dep for dep in dependencies.get(start_module['name'], [])
if dep not in visited
]
for conn in connected[:5]: # Limit cluster size
for module in all_modules:
if module['name'] == conn and conn not in visited:
cluster_modules.append(module)
visited.add(conn)
return {
'name': f"Component_{start_module['name']}",
'modules': cluster_modules
}
class MultiAgentOrchestrator:
"""
Orchestrates collaboration between specialized agents.
"""
def __init__(self):
self.agents = {}
self.message_broker = MessageBroker()
self.workflow_engine = WorkflowEngine()
def register_agent(self, agent: BaseAgent):
"""Register an agent with the orchestrator."""
self.agents[agent.agent_id] = agent
self.message_broker.register_agent(agent)
async def start_all_agents(self):
"""Start all registered agents."""
tasks = [agent.start() for agent in self.agents.values()]
await asyncio.gather(*tasks)
async def execute_analysis_workflow(self, system_path: str):
"""
Execute complete analysis workflow across all agents.
Args:
system_path: Path to system to analyze
Returns:
Comprehensive analysis results
"""
workflow = self.workflow_engine.create_workflow([
('parse_code', 'code_analysis'),
('analyze_architecture', 'architecture_analysis'),
('assess_quality', 'quality_attribute'),
('analyze_domain', 'domain_analysis'),
('generate_report', 'report_generation')
])
results = await workflow.execute({
'system_path': system_path
})
return results
class MessageBroker:
"""
Message broker for inter-agent communication.
"""
def __init__(self):
self.agent_queues = {}
def register_agent(self, agent: BaseAgent):
"""Register agent's message queue."""
self.agent_queues[agent.agent_id] = agent.message_queue
async def route_message(self, message: AgentMessage):
"""Route message to recipient agent."""
recipient_queue = self.agent_queues.get(message.recipient)
if recipient_queue:
await recipient_queue.put(message)
class WorkflowEngine:
"""
Engine for orchestrating multi-agent workflows.
"""
def create_workflow(self, steps: List[tuple]):
"""
Create workflow from step definitions.
Args:
steps: List of (step_name, agent_type) tuples
Returns:
Workflow instance
"""
return Workflow(steps)
class Workflow:
"""
Represents a multi-agent analysis workflow.
"""
def __init__(self, steps: List[tuple]):
self.steps = steps
self.results = {}
async def execute(self, initial_context: Dict[str, Any]):
"""Execute workflow steps sequentially."""
context = initial_context.copy()
for step_name, agent_type in self.steps:
# Execute step with current context
step_result = await self._execute_step(
step_name,
agent_type,
context
)
# Store results
self.results[step_name] = step_result
# Update context for next step
context.update(step_result)
return self.results
async def _execute_step(self, step_name, agent_type, context):
"""Execute single workflow step."""
# In production, this would send message to appropriate agent
# and wait for response
pass
This multi-agent architecture provides specialized expertise while enabling collaboration through message passing and shared knowledge stores. Each agent focuses on its domain, reducing the complexity any single agent must handle. The orchestrator coordinates workflows that span multiple agents, ensuring analysis proceeds in logical sequence and results are properly integrated.
The asynchronous message-based communication allows agents to work concurrently when analyzing independent aspects of the system. For example, the code analysis agent can examine implementation patterns while the architecture agent analyzes system structure, with both working in parallel until their results need to be synthesized.
NAMED ENTITY RECOGNITION FOR INFORMATION EXTRACTION
Named Entity Recognition plays a crucial role in extracting structured information from various document types encountered during system analysis. Different document types require different NER approaches because they contain distinct entity types and follow different conventions. Source code entities differ from requirement document entities, which differ from architecture documentation entities.
For source code, entities include classes, methods, variables, packages, and modules. For requirements documents, entities include features, user stories, acceptance criteria, and stakeholders. For architecture documents, entities include components, layers, connectors, and quality attributes. Each requires specialized extraction logic.
Here is an implementation of multi-domain NER for code analysis:
import re
from typing import List, Dict, Set
from dataclasses import dataclass
@dataclass
class Entity:
"""Represents a recognized entity."""
text: str
entity_type: str
start_position: int
end_position: int
confidence: float
context: str
metadata: Dict[str, any]
class CodeEntityRecognizer:
"""
NER for extracting entities from source code.
"""
def __init__(self, llm_client):
self.llm_client = llm_client
self.entity_patterns = self._initialize_patterns()
def _initialize_patterns(self):
"""Initialize regex patterns for common code entities."""
return {
'class_name': re.compile(r'class\s+([A-Z][a-zA-Z0-9_]*)\s*[:(]'),
'function_name': re.compile(r'def\s+([a-z_][a-zA-Z0-9_]*)\s*\('),
'variable_name': re.compile(r'([a-z_][a-zA-Z0-9_]*)\s*='),
'import_module': re.compile(r'import\s+([a-zA-Z0-9_.]+)'),
'decorator': re.compile(r'@([a-zA-Z_][a-zA-Z0-9_.]*)'),
}
def recognize_entities(self, code_text: str, filepath: str) -> List[Entity]:
"""
Extract entities from source code.
Args:
code_text: Source code text
filepath: Path to source file
Returns:
List of recognized entities
"""
entities = []
# Extract pattern-based entities
for entity_type, pattern in self.entity_patterns.items():
matches = pattern.finditer(code_text)
for match in matches:
entity = Entity(
text=match.group(1),
entity_type=entity_type,
start_position=match.start(),
end_position=match.end(),
confidence=0.95, # High confidence for pattern matches
context=self._extract_context(code_text, match.start()),
metadata={'filepath': filepath}
)
entities.append(entity)
# Use LLM for complex entity recognition
llm_entities = self._llm_entity_extraction(code_text, filepath)
entities.extend(llm_entities)
# Deduplicate and resolve conflicts
entities = self._deduplicate_entities(entities)
return entities
def _extract_context(self, text: str, position: int, window: int = 100):
"""Extract surrounding context for an entity."""
start = max(0, position - window)
end = min(len(text), position + window)
return text[start:end]
def _llm_entity_extraction(self, code_text: str, filepath: str) -> List[Entity]:
"""
Use LLM to extract complex entities that patterns might miss.
"""
# Limit code size for LLM processing
if len(code_text) > 3000:
code_text = code_text[:3000]
prompt = f"""Extract architectural entities from this code:
{code_text}
Identify:
1. Design pattern implementations (name the pattern and the classes involved)
2. Architectural layer indicators (presentation, domain, infrastructure)
3. Business domain concepts (entities, value objects, aggregates)
4. Integration points (external APIs, databases, message queues)
For each entity, provide:
- Entity text (exact name from code)
- Entity type
- Confidence (0.0 to 1.0)
- Brief context
Respond in JSON format."""
response = self.llm_client.generate(
prompt,
max_tokens=1000,
temperature=0.3
)
# Parse LLM response into entities
return self._parse_llm_entities(response, filepath)
def _parse_llm_entities(self, response: str, filepath: str) -> List[Entity]:
"""Parse LLM response into Entity objects."""
entities = []
try:
import json
data = json.loads(response)
for item in data.get('entities', []):
entity = Entity(
text=item['text'],
entity_type=item['type'],
start_position=-1, # Position unknown from LLM
end_position=-1,
confidence=item.get('confidence', 0.7),
context=item.get('context', ''),
metadata={'filepath': filepath, 'source': 'llm'}
)
entities.append(entity)
except json.JSONDecodeError:
# LLM response wasn't valid JSON
pass
return entities
def _deduplicate_entities(self, entities: List[Entity]) -> List[Entity]:
"""Remove duplicate entities, keeping highest confidence."""
seen = {}
for entity in entities:
key = (entity.text, entity.entity_type)
if key not in seen or entity.confidence > seen[key].confidence:
seen[key] = entity
return list(seen.values())
class RequirementsEntityRecognizer:
"""
NER for extracting entities from requirements documents.
"""
def __init__(self, llm_client):
self.llm_client = llm_client
def recognize_entities(self, requirements_text: str) -> List[Entity]:
"""
Extract entities from requirements document.
Args:
requirements_text: Requirements document text
Returns:
List of recognized entities
"""
entities = []
# Extract user stories
user_stories = self._extract_user_stories(requirements_text)
entities.extend(user_stories)
# Extract features
features = self._extract_features(requirements_text)
entities.extend(features)
# Extract actors/stakeholders
actors = self._extract_actors(requirements_text)
entities.extend(actors)
# Extract quality attributes
quality_attrs = self._extract_quality_attributes(requirements_text)
entities.extend(quality_attrs)
return entities
def _extract_user_stories(self, text: str) -> List[Entity]:
"""Extract user story entities."""
# Pattern: "As a [role], I want [feature] so that [benefit]"
pattern = re.compile(
r'As\s+a\s+([^,]+),\s+I\s+want\s+([^,]+)(?:,?\s+so\s+that\s+(.+))?',
re.IGNORECASE
)
entities = []
for match in pattern.finditer(text):
entity = Entity(
text=match.group(0),
entity_type='user_story',
start_position=match.start(),
end_position=match.end(),
confidence=0.9,
context=self._extract_context(text, match.start()),
metadata={
'role': match.group(1).strip(),
'feature': match.group(2).strip(),
'benefit': match.group(3).strip() if match.group(3) else None
}
)
entities.append(entity)
return entities
def _extract_features(self, text: str) -> List[Entity]:
"""Extract feature entities using LLM."""
prompt = f"""Extract feature descriptions from this requirements text:
{text[:2000]}
A feature is a distinct capability or functionality the system should provide.
For each feature, provide:
- Feature name
- Brief description
- Priority (if mentioned)
Respond in JSON format."""
response = self.llm_client.generate(
prompt,
max_tokens=800,
temperature=0.3
)
return self._parse_feature_response(response)
def _extract_actors(self, text: str) -> List[Entity]:
"""Extract actor/stakeholder entities."""
# Common actor indicators
actor_patterns = [
r'(?:user|customer|administrator|manager|operator|developer)',
r'(?:system|service|application|component)',
]
entities = []
for pattern_str in actor_patterns:
pattern = re.compile(pattern_str, re.IGNORECASE)
for match in pattern.finditer(text):
entity = Entity(
text=match.group(0),
entity_type='actor',
start_position=match.start(),
end_position=match.end(),
confidence=0.7,
context=self._extract_context(text, match.start()),
metadata={}
)
entities.append(entity)
return self._deduplicate_by_text(entities)
def _extract_quality_attributes(self, text: str) -> List[Entity]:
"""Extract quality attribute requirements."""
qa_keywords = {
'performance': ['fast', 'response time', 'throughput', 'latency'],
'security': ['secure', 'authentication', 'authorization', 'encryption'],
'scalability': ['scale', 'concurrent users', 'load'],
'reliability': ['available', 'uptime', 'fault tolerance'],
'usability': ['user-friendly', 'intuitive', 'easy to use']
}
entities = []
for qa_type, keywords in qa_keywords.items():
for keyword in keywords:
pattern = re.compile(r'\b' + re.escape(keyword) + r'\b', re.IGNORECASE)
for match in pattern.finditer(text):
entity = Entity(
text=match.group(0),
entity_type='quality_attribute',
start_position=match.start(),
end_position=match.end(),
confidence=0.8,
context=self._extract_context(text, match.start()),
metadata={'category': qa_type}
)
entities.append(entity)
return entities
def _extract_context(self, text: str, position: int, window: int = 150):
"""Extract surrounding context."""
start = max(0, position - window)
end = min(len(text), position + window)
return text[start:end]
def _deduplicate_by_text(self, entities: List[Entity]) -> List[Entity]:
"""Remove duplicate entities based on text."""
seen = set()
unique = []
for entity in entities:
if entity.text.lower() not in seen:
seen.add(entity.text.lower())
unique.append(entity)
return unique
def _parse_feature_response(self, response: str) -> List[Entity]:
"""Parse LLM response for features."""
entities = []
try:
import json
data = json.loads(response)
for feature in data.get('features', []):
entity = Entity(
text=feature['name'],
entity_type='feature',
start_position=-1,
end_position=-1,
confidence=0.75,
context=feature.get('description', ''),
metadata={
'description': feature.get('description'),
'priority': feature.get('priority')
}
)
entities.append(entity)
except json.JSONDecodeError:
pass
return entities
class ArchitectureDocumentEntityRecognizer:
"""
NER for extracting entities from architecture documentation.
"""
def __init__(self, llm_client):
self.llm_client = llm_client
def recognize_entities(self, doc_text: str) -> List[Entity]:
"""
Extract architectural entities from documentation.
Args:
doc_text: Architecture documentation text
Returns:
List of recognized entities
"""
entities = []
# Extract components
components = self._extract_components(doc_text)
entities.extend(components)
# Extract layers
layers = self._extract_layers(doc_text)
entities.extend(layers)
# Extract connectors/interfaces
connectors = self._extract_connectors(doc_text)
entities.extend(connectors)
# Extract quality attributes
quality_attrs = self._extract_quality_attributes(doc_text)
entities.extend(quality_attrs)
# Extract architectural patterns
patterns = self._extract_patterns(doc_text)
entities.extend(patterns)
return entities
def _extract_components(self, text: str) -> List[Entity]:
"""Extract component entities."""
prompt = f"""Extract architectural components from this documentation:
{text[:2500]}
Components are major structural elements like services, modules, subsystems.
For each component, provide:
- Component name
- Component type (service, module, subsystem, etc.)
- Responsibilities
Respond in JSON format."""
response = self.llm_client.generate(
prompt,
max_tokens=1000,
temperature=0.3
)
return self._parse_component_response(response)
def _extract_layers(self, text: str) -> List[Entity]:
"""Extract architectural layer entities."""
layer_keywords = [
'presentation layer',
'application layer',
'business layer',
'domain layer',
'data layer',
'infrastructure layer',
'persistence layer'
]
entities = []
for keyword in layer_keywords:
pattern = re.compile(r'\b' + re.escape(keyword) + r'\b', re.IGNORECASE)
for match in pattern.finditer(text):
entity = Entity(
text=match.group(0),
entity_type='architectural_layer',
start_position=match.start(),
end_position=match.end(),
confidence=0.9,
context=self._extract_context(text, match.start()),
metadata={}
)
entities.append(entity)
return entities
def _extract_connectors(self, text: str) -> List[Entity]:
"""Extract connector/interface entities."""
connector_patterns = [
r'(?:REST|HTTP|gRPC|SOAP)\s+(?:API|interface|endpoint)',
r'message\s+(?:queue|bus|broker)',
r'database\s+connection',
r'event\s+(?:bus|stream)'
]
entities = []
for pattern_str in connector_patterns:
pattern = re.compile(pattern_str, re.IGNORECASE)
for match in pattern.finditer(text):
entity = Entity(
text=match.group(0),
entity_type='connector',
start_position=match.start(),
end_position=match.end(),
confidence=0.85,
context=self._extract_context(text, match.start()),
metadata={}
)
entities.append(entity)
return entities
def _extract_quality_attributes(self, text: str) -> List[Entity]:
"""Extract quality attribute mentions."""
qa_patterns = {
'performance': r'(?:performance|throughput|latency|response\s+time)',
'scalability': r'(?:scalability|horizontal\s+scaling|load\s+balancing)',
'availability': r'(?:availability|uptime|fault\s+tolerance)',
'security': r'(?:security|authentication|authorization|encryption)',
'maintainability': r'(?:maintainability|modularity|testability)'
}
entities = []
for qa_type, pattern_str in qa_patterns.items():
pattern = re.compile(pattern_str, re.IGNORECASE)
for match in pattern.finditer(text):
entity = Entity(
text=match.group(0),
entity_type='quality_attribute',
start_position=match.start(),
end_position=match.end(),
confidence=0.8,
context=self._extract_context(text, match.start()),
metadata={'category': qa_type}
)
entities.append(entity)
return entities
def _extract_patterns(self, text: str) -> List[Entity]:
"""Extract architectural pattern mentions."""
pattern_names = [
'microservices',
'layered architecture',
'hexagonal architecture',
'event-driven',
'CQRS',
'event sourcing',
'service mesh',
'API gateway'
]
entities = []
for pattern_name in pattern_names:
pattern = re.compile(r'\b' + re.escape(pattern_name) + r'\b', re.IGNORECASE)
for match in pattern.finditer(text):
entity = Entity(
text=match.group(0),
entity_type='architectural_pattern',
start_position=match.start(),
end_position=match.end(),
confidence=0.9,
context=self._extract_context(text, match.start()),
metadata={'pattern_name': pattern_name}
)
entities.append(entity)
return entities
def _extract_context(self, text: str, position: int, window: int = 200):
"""Extract surrounding context."""
start = max(0, position - window)
end = min(len(text), position + window)
return text[start:end]
def _parse_component_response(self, response: str) -> List[Entity]:
"""Parse LLM response for components."""
entities = []
try:
import json
data = json.loads(response)
for comp in data.get('components', []):
entity = Entity(
text=comp['name'],
entity_type='component',
start_position=-1,
end_position=-1,
confidence=0.8,
context=comp.get('responsibilities', ''),
metadata={
'component_type': comp.get('type'),
'responsibilities': comp.get('responsibilities')
}
)
entities.append(entity)
except json.JSONDecodeError:
pass
return entities
These specialized NER implementations extract domain-specific entities from different document types. The combination of pattern-based extraction for well-defined entities and LLM-based extraction for complex or contextual entities provides comprehensive coverage. The extracted entities feed into knowledge graphs and vector databases where they become queryable architectural knowledge.
KNOWLEDGE STORAGE WITH RAG AND KNOWLEDGE GRAPHS
Effective storage and retrieval of extracted information requires dual approaches: vector databases for semantic similarity search and knowledge graphs for relationship-based queries. RAG systems use vector embeddings to find relevant information based on semantic similarity, while knowledge graphs explicitly model entities and their relationships for structured queries.
The integration of both approaches creates a powerful information retrieval system. Vector databases excel at finding conceptually similar code or documentation even when exact keywords don't match. Knowledge graphs excel at traversing relationships like finding all classes that implement a particular interface or all requirements that trace to a specific architectural component.
Here is an implementation of the dual storage system:
import numpy as np
from typing import List, Dict, Tuple
import json
class VectorDatabase:
"""
Vector database for semantic search using embeddings.
"""
def __init__(self, embedding_model, dimension=768):
"""
Initialize vector database.
Args:
embedding_model: Model for generating embeddings
dimension: Dimension of embedding vectors
"""
self.embedding_model = embedding_model
self.dimension = dimension
self.vectors = []
self.metadata = []
self.index = None
def add_document(self, text: str, metadata: Dict):
"""
Add document to vector database.
Args:
text: Document text to embed
metadata: Associated metadata
"""
# Generate embedding
embedding = self.embedding_model.embed(text)
# Store vector and metadata
self.vectors.append(embedding)
self.metadata.append({
'text': text,
**metadata
})
# Rebuild index if using approximate search
if len(self.vectors) % 100 == 0:
self._rebuild_index()
def add_code_chunk(self, code: str, file_path: str,
chunk_type: str, entities: List[str]):
"""
Add code chunk with metadata.
Args:
code: Code text
file_path: Source file path
chunk_type: Type of code chunk (class, function, etc.)
entities: Entities present in chunk
"""
metadata = {
'type': 'code',
'file_path': file_path,
'chunk_type': chunk_type,
'entities': entities,
'language': self._detect_language(file_path)
}
self.add_document(code, metadata)
def search(self, query: str, top_k: int = 5,
filters: Dict = None) -> List[Dict]:
"""
Search for similar documents.
Args:
query: Search query
top_k: Number of results to return
filters: Optional metadata filters
Returns:
List of matching documents with scores
"""
# Generate query embedding
query_embedding = self.embedding_model.embed(query)
# Calculate similarities
similarities = []
for i, vec in enumerate(self.vectors):
# Apply filters if specified
if filters and not self._matches_filters(self.metadata[i], filters):
continue
similarity = self._cosine_similarity(query_embedding, vec)
similarities.append((i, similarity))
# Sort by similarity and return top k
similarities.sort(key=lambda x: x[1], reverse=True)
top_results = similarities[:top_k]
results = []
for idx, score in top_results:
result = self.metadata[idx].copy()
result['similarity_score'] = score
results.append(result)
return results
def _cosine_similarity(self, vec1, vec2):
"""Calculate cosine similarity between vectors."""
dot_product = np.dot(vec1, vec2)
norm1 = np.linalg.norm(vec1)
norm2 = np.linalg.norm(vec2)
if norm1 == 0 or norm2 == 0:
return 0.0
return dot_product / (norm1 * norm2)
def _matches_filters(self, metadata: Dict, filters: Dict) -> bool:
"""Check if metadata matches filter criteria."""
for key, value in filters.items():
if key not in metadata:
return False
if isinstance(value, list):
if metadata[key] not in value:
return False
else:
if metadata[key] != value:
return False
return True
def _detect_language(self, file_path: str) -> str:
"""Detect programming language from file extension."""
extension_map = {
'.py': 'python',
'.java': 'java',
'.js': 'javascript',
'.go': 'go',
'.cpp': 'cpp',
'.cs': 'csharp'
}
import os
ext = os.path.splitext(file_path)[1]
return extension_map.get(ext, 'unknown')
def _rebuild_index(self):
"""Rebuild search index for efficiency."""
# In production, use FAISS or similar for approximate nearest neighbor
pass
class KnowledgeGraph:
"""
Knowledge graph for storing entities and relationships.
"""
def __init__(self):
self.nodes = {} # node_id -> node_data
self.edges = [] # list of (source, target, relationship_type, properties)
self.node_types = {} # node_type -> set of node_ids
self.relationship_index = {} # relationship_type -> list of edges
def add_node(self, node_id: str, node_type: str, properties: Dict):
"""
Add node to knowledge graph.
Args:
node_id: Unique identifier for node
node_type: Type of node (class, method, component, etc.)
properties: Node properties
"""
self.nodes[node_id] = {
'id': node_id,
'type': node_type,
'properties': properties
}
# Index by type
if node_type not in self.node_types:
self.node_types[node_type] = set()
self.node_types[node_type].add(node_id)
def add_edge(self, source_id: str, target_id: str,
relationship_type: str, properties: Dict = None):
"""
Add relationship between nodes.
Args:
source_id: Source node ID
target_id: Target node ID
relationship_type: Type of relationship
properties: Optional relationship properties
"""
edge = {
'source': source_id,
'target': target_id,
'type': relationship_type,
'properties': properties or {}
}
self.edges.append(edge)
# Index by relationship type
if relationship_type not in self.relationship_index:
self.relationship_index[relationship_type] = []
self.relationship_index[relationship_type].append(edge)
def add_class_node(self, class_name: str, file_path: str,
methods: List[str], base_classes: List[str]):
"""
Add class node with standard properties.
Args:
class_name: Name of the class
file_path: Source file path
methods: List of method names
base_classes: List of base class names
"""
node_id = f"class:{class_name}"
properties = {
'name': class_name,
'file_path': file_path,
'methods': methods,
'base_classes': base_classes
}
self.add_node(node_id, 'class', properties)
# Add inheritance relationships
for base_class in base_classes:
base_id = f"class:{base_class}"
self.add_edge(node_id, base_id, 'inherits_from')
# Add method relationships
for method in methods:
method_id = f"method:{class_name}.{method}"
self.add_node(method_id, 'method', {'name': method})
self.add_edge(node_id, method_id, 'has_method')
def add_component_node(self, component_name: str,
component_type: str, modules: List[str]):
"""
Add architectural component node.
Args:
component_name: Name of component
component_type: Type of component
modules: List of modules in component
"""
node_id = f"component:{component_name}"
properties = {
'name': component_name,
'type': component_type,
'modules': modules
}
self.add_node(node_id, 'component', properties)
# Add module relationships
for module in modules:
module_id = f"module:{module}"
self.add_edge(node_id, module_id, 'contains')
def query_nodes_by_type(self, node_type: str) -> List[Dict]:
"""
Query all nodes of a specific type.
Args:
node_type: Type of nodes to retrieve
Returns:
List of matching nodes
"""
node_ids = self.node_types.get(node_type, set())
return [self.nodes[nid] for nid in node_ids]
def query_relationships(self, relationship_type: str) -> List[Dict]:
"""
Query all relationships of a specific type.
Args:
relationship_type: Type of relationships to retrieve
Returns:
List of matching relationships
"""
return self.relationship_index.get(relationship_type, [])
def find_related_nodes(self, node_id: str,
relationship_type: str = None,
direction: str = 'outgoing') -> List[Dict]:
"""
Find nodes related to a given node.
Args:
node_id: Starting node ID
relationship_type: Optional filter by relationship type
direction: 'outgoing', 'incoming', or 'both'
Returns:
List of related nodes
"""
related = []
for edge in self.edges:
# Filter by relationship type if specified
if relationship_type and edge['type'] != relationship_type:
continue
# Check direction
if direction in ['outgoing', 'both'] and edge['source'] == node_id:
target_node = self.nodes.get(edge['target'])
if target_node:
related.append({
'node': target_node,
'relationship': edge['type'],
'direction': 'outgoing'
})
if direction in ['incoming', 'both'] and edge['target'] == node_id:
source_node = self.nodes.get(edge['source'])
if source_node:
related.append({
'node': source_node,
'relationship': edge['type'],
'direction': 'incoming'
})
return related
def find_path(self, start_id: str, end_id: str,
max_depth: int = 5) -> List[List[str]]:
"""
Find paths between two nodes.
Args:
start_id: Starting node ID
end_id: Ending node ID
max_depth: Maximum path length
Returns:
List of paths (each path is a list of node IDs)
"""
paths = []
visited = set()
def dfs(current_id, target_id, path, depth):
if depth > max_depth:
return
if current_id == target_id:
paths.append(path.copy())
return
if current_id in visited:
return
visited.add(current_id)
# Explore outgoing edges
for edge in self.edges:
if edge['source'] == current_id:
next_id = edge['target']
dfs(next_id, target_id, path + [next_id], depth + 1)
visited.remove(current_id)
dfs(start_id, end_id, [start_id], 0)
return paths
def export_to_json(self, filepath: str):
"""Export knowledge graph to JSON file."""
data = {
'nodes': list(self.nodes.values()),
'edges': self.edges
}
with open(filepath, 'w') as f:
json.dump(data, f, indent=2)
def import_from_json(self, filepath: str):
"""Import knowledge graph from JSON file."""
with open(filepath, 'r') as f:
data = json.load(f)
# Clear existing data
self.nodes = {}
self.edges = []
self.node_types = {}
self.relationship_index = {}
# Import nodes
for node in data.get('nodes', []):
self.add_node(
node['id'],
node['type'],
node['properties']
)
# Import edges
for edge in data.get('edges', []):
self.add_edge(
edge['source'],
edge['target'],
edge['type'],
edge.get('properties')
)
class HybridKnowledgeStore:
"""
Combines vector database and knowledge graph for comprehensive retrieval.
"""
def __init__(self, embedding_model):
self.vector_db = VectorDatabase(embedding_model)
self.knowledge_graph = KnowledgeGraph()
self.entity_to_vector_index = {} # Maps KG node IDs to vector indices
def add_code_artifact(self, code: str, file_path: str,
parsed_data: Dict):
"""
Add code artifact to both stores.
Args:
code: Source code text
file_path: Path to source file
parsed_data: Parsed AST data
"""
# Add to vector database
entities = []
for cls in parsed_data.get('classes', []):
entities.append(cls['name'])
# Add class to knowledge graph
self.knowledge_graph.add_class_node(
cls['name'],
file_path,
[m['name'] for m in cls.get('methods', [])],
cls.get('bases', [])
)
self.vector_db.add_code_chunk(
code,
file_path,
'module',
entities
)
# Create links between vector DB and knowledge graph
vector_index = len(self.vector_db.vectors) - 1
for entity in entities:
node_id = f"class:{entity}"
if node_id not in self.entity_to_vector_index:
self.entity_to_vector_index[node_id] = []
self.entity_to_vector_index[node_id].append(vector_index)
def hybrid_search(self, query: str, top_k: int = 5) -> List[Dict]:
"""
Perform hybrid search using both vector similarity and graph traversal.
Args:
query: Search query
top_k: Number of results
Returns:
Combined search results
"""
# Vector search for semantic similarity
vector_results = self.vector_db.search(query, top_k=top_k * 2)
# Extract entities from top vector results
entities_found = set()
for result in vector_results[:top_k]:
entities_found.update(result.get('entities', []))
# Graph traversal to find related entities
graph_results = []
for entity in list(entities_found)[:5]: # Limit to avoid explosion
node_id = f"class:{entity}"
if node_id in self.knowledge_graph.nodes:
related = self.knowledge_graph.find_related_nodes(
node_id,
direction='both'
)
graph_results.extend(related)
# Combine and rank results
combined = self._combine_results(vector_results, graph_results)
return combined[:top_k]
def _combine_results(self, vector_results: List[Dict],
graph_results: List[Dict]) -> List[Dict]:
"""Combine and rank results from both sources."""
combined = {}
# Add vector results with their scores
for i, result in enumerate(vector_results):
key = result.get('file_path', str(i))
combined[key] = {
**result,
'vector_score': result.get('similarity_score', 0),
'graph_score': 0,
'combined_score': result.get('similarity_score', 0)
}
# Enhance with graph results
for graph_result in graph_results:
node = graph_result['node']
file_path = node['properties'].get('file_path')
if file_path in combined:
# Boost score for graph-connected results
combined[file_path]['graph_score'] += 0.2
combined[file_path]['combined_score'] += 0.2
# Sort by combined score
results = list(combined.values())
results.sort(key=lambda x: x['combined_score'], reverse=True)
return results
This hybrid storage system leverages the strengths of both approaches. The vector database enables finding semantically similar code even when exact keywords don't match. The knowledge graph enables precise relationship queries like finding all classes that implement an interface or all components that depend on a particular module. The integration between the two allows queries that combine semantic similarity with structural relationships, producing more relevant and comprehensive results than either approach alone.
HIERARCHICAL SUMMARIZATION FOR CONTEXT MANAGEMENT
When analyzing large systems, the volume of information quickly exceeds the context window of even the largest language models. Hierarchical summarization addresses this by creating multi-level summaries where high-level summaries provide overview information and progressively detailed summaries are available on demand.
The approach works by first summarizing individual code files or components. These file-level summaries are then aggregated into module-level summaries. Module summaries aggregate into subsystem summaries, and subsystem summaries aggregate into system-level summaries. This creates a pyramid structure where the analyst can start with a high-level system overview and drill down into specific areas of interest without loading the entire codebase into context.
Here is an implementation of hierarchical summarization:
class HierarchicalSummarizer:
"""
Creates multi-level summaries of code artifacts.
"""
def __init__(self, llm_client):
self.llm_client = llm_client
self.summaries = {
'file': {},
'module': {},
'subsystem': {},
'system': None
}
def summarize_file(self, file_path: str, code: str,
parsed_data: Dict) -> Dict:
"""
Create file-level summary.
Args:
file_path: Path to file
code: Source code
parsed_data: Parsed AST data
Returns:
File summary
"""
# Extract key information
classes = [cls['name'] for cls in parsed_data.get('classes', [])]
functions = [fn['name'] for fn in parsed_data.get('functions', [])]
imports = [imp['module'] for imp in parsed_data.get('imports', [])]
# Build summarization prompt
prompt = f"""Summarize this code file concisely.
File: {file_path}
Classes: {', '.join(classes) if classes else 'None'}
Functions: {', '.join(functions) if functions else 'None'}
Key Imports: {', '.join(imports[:10])}
Code excerpt:
{code[:1500]}
Provide a 2-3 sentence summary covering:
1. Primary purpose of this file
2. Key classes or functions it provides
3. Its likely role in the larger system"""
response = self.llm_client.generate(
prompt,
max_tokens=200,
temperature=0.3
)
summary = {
'file_path': file_path,
'summary_text': response.strip(),
'classes': classes,
'functions': functions,
'key_imports': imports[:10],
'lines_of_code': len(code.split('\n'))
}
self.summaries['file'][file_path] = summary
return summary
def summarize_module(self, module_name: str,
file_summaries: List[Dict]) -> Dict:
"""
Create module-level summary from file summaries.
Args:
module_name: Name of module
file_summaries: List of file-level summaries
Returns:
Module summary
"""
# Aggregate information from file summaries
all_classes = []
all_functions = []
total_loc = 0
for file_sum in file_summaries:
all_classes.extend(file_sum.get('classes', []))
all_functions.extend(file_sum.get('functions', []))
total_loc += file_sum.get('lines_of_code', 0)
# Build module summary prompt
file_summaries_text = '\n'.join([
f"- {fs['file_path']}: {fs['summary_text']}"
for fs in file_summaries[:10] # Limit to prevent context overflow
])
prompt = f"""Summarize this module based on its file summaries.
Module: {module_name}
Number of files: {len(file_summaries)}
Total lines of code: {total_loc}
Key classes: {', '.join(all_classes[:15])}
File summaries:
{file_summaries_text}
Provide a 3-4 sentence summary covering:
1. Overall purpose of this module
2. Main responsibilities and functionality
3. Key architectural patterns or design approaches
4. How it likely fits into the larger system"""
response = self.llm_client.generate(
prompt,
max_tokens=300,
temperature=0.3
)
summary = {
'module_name': module_name,
'summary_text': response.strip(),
'file_count': len(file_summaries),
'total_lines': total_loc,
'key_classes': all_classes[:20],
'key_functions': all_functions[:20]
}
self.summaries['module'][module_name] = summary
return summary
def summarize_subsystem(self, subsystem_name: str,
module_summaries: List[Dict]) -> Dict:
"""
Create subsystem-level summary from module summaries.
Args:
subsystem_name: Name of subsystem
module_summaries: List of module summaries
Returns:
Subsystem summary
"""
total_files = sum(ms.get('file_count', 0) for ms in module_summaries)
total_loc = sum(ms.get('total_lines', 0) for ms in module_summaries)
module_summaries_text = '\n'.join([
f"- {ms['module_name']}: {ms['summary_text']}"
for ms in module_summaries[:8]
])
prompt = f"""Summarize this subsystem based on its module summaries.
Subsystem: {subsystem_name}
Number of modules: {len(module_summaries)}
Total files: {total_files}
Total lines of code: {total_loc}
Module summaries:
{module_summaries_text}
Provide a 4-5 sentence summary covering:
1. High-level purpose of this subsystem
2. Major functional areas it covers
3. Architectural approach and key patterns
4. Dependencies and integration points
5. Quality characteristics (if evident)"""
response = self.llm_client.generate(
prompt,
max_tokens=400,
temperature=0.3
)
summary = {
'subsystem_name': subsystem_name,
'summary_text': response.strip(),
'module_count': len(module_summaries),
'total_files': total_files,
'total_lines': total_loc
}
self.summaries['subsystem'][subsystem_name] = summary
return summary
def summarize_system(self, system_name: str,
subsystem_summaries: List[Dict]) -> Dict:
"""
Create system-level summary from subsystem summaries.
Args:
system_name: Name of system
subsystem_summaries: List of subsystem summaries
Returns:
System summary
"""
total_modules = sum(
ss.get('module_count', 0) for ss in subsystem_summaries
)
total_files = sum(
ss.get('total_files', 0) for ss in subsystem_summaries
)
total_loc = sum(
ss.get('total_lines', 0) for ss in subsystem_summaries
)
subsystem_summaries_text = '\n'.join([
f"- {ss['subsystem_name']}: {ss['summary_text']}"
for ss in subsystem_summaries
])
prompt = f"""Summarize this entire system based on its subsystem summaries.
System: {system_name}
Number of subsystems: {len(subsystem_summaries)}
Total modules: {total_modules}
Total files: {total_files}
Total lines of code: {total_loc}
Subsystem summaries:
{subsystem_summaries_text}
Provide a comprehensive 5-7 sentence summary covering:
1. Overall system purpose and domain
2. Major subsystems and their roles
3. Architectural style and key patterns
4. Technology stack and frameworks
5. Integration approach between subsystems
6. Notable quality attributes or design decisions"""
response = self.llm_client.generate(
prompt,
max_tokens=600,
temperature=0.3
)
summary = {
'system_name': system_name,
'summary_text': response.strip(),
'subsystem_count': len(subsystem_summaries),
'total_modules': total_modules,
'total_files': total_files,
'total_lines': total_loc
}
self.summaries['system'] = summary
return summary
def get_contextual_summary(self, target_entity: str,
max_context_tokens: int = 2000) -> str:
"""
Get contextual summary for a specific entity, including relevant
higher-level context without exceeding token budget.
Args:
target_entity: Entity to get context for (file, module, etc.)
max_context_tokens: Maximum tokens for context
Returns:
Contextual summary text
"""
context_parts = []
estimated_tokens = 0
# Start with system-level summary if available
if self.summaries['system']:
system_text = self.summaries['system']['summary_text']
tokens = len(system_text.split()) * 1.3 # Rough token estimate
if estimated_tokens + tokens < max_context_tokens:
context_parts.append(f"System Overview: {system_text}")
estimated_tokens += tokens
# Add relevant subsystem summary
relevant_subsystem = self._find_containing_subsystem(target_entity)
if relevant_subsystem and relevant_subsystem in self.summaries['subsystem']:
subsys_text = self.summaries['subsystem'][relevant_subsystem]['summary_text']
tokens = len(subsys_text.split()) * 1.3
if estimated_tokens + tokens < max_context_tokens:
context_parts.append(
f"Subsystem ({relevant_subsystem}): {subsys_text}"
)
estimated_tokens += tokens
# Add relevant module summary
relevant_module = self._find_containing_module(target_entity)
if relevant_module and relevant_module in self.summaries['module']:
module_text = self.summaries['module'][relevant_module]['summary_text']
tokens = len(module_text.split()) * 1.3
if estimated_tokens + tokens < max_context_tokens:
context_parts.append(
f"Module ({relevant_module}): {module_text}"
)
estimated_tokens += tokens
# Add file-level summary if target is a file
if target_entity in self.summaries['file']:
file_text = self.summaries['file'][target_entity]['summary_text']
tokens = len(file_text.split()) * 1.3
if estimated_tokens + tokens < max_context_tokens:
context_parts.append(
f"File ({target_entity}): {file_text}"
)
estimated_tokens += tokens
return '\n\n'.join(context_parts)
def _find_containing_subsystem(self, entity: str) -> str:
"""Find which subsystem contains the entity."""
# Simple heuristic based on path structure
# In production, use proper module hierarchy tracking
if '/' in entity:
parts = entity.split('/')
if len(parts) > 2:
return parts[0]
return None
def _find_containing_module(self, entity: str) -> str:
"""Find which module contains the entity."""
if '/' in entity:
parts = entity.split('/')
if len(parts) > 1:
return '/'.join(parts[:-1])
return None
This hierarchical approach ensures that analysts always have appropriate context without overwhelming the model's context window. When examining a specific class, the system provides file-level, module-level, and system-level context to situate that class within the broader architecture. This contextual awareness significantly improves the quality of analysis and recommendations.
REQUIREMENT TO CODE TRACEABILITY
Establishing bidirectional traceability between requirements, architecture, code, and tests provides critical insights into system coverage and implementation completeness. This traceability enables impact analysis when requirements change, verification that all requirements have implementations, and identification of orphaned code that doesn't trace to any requirement.
The implementation requires extracting requirements from various sources including user stories, feature specifications, and acceptance criteria. These requirements are then mapped to architectural components that implement them. Architectural components map to code artifacts including classes, modules, and services. Code artifacts map to test cases that verify their behavior. The complete traceability chain connects business requirements through implementation to verification.
Here is an implementation of the traceability system:
class TraceabilityManager:
"""
Manages traceability links between requirements, architecture, code, and tests.
"""
def __init__(self, knowledge_graph: KnowledgeGraph):
self.knowledge_graph = knowledge_graph
self.trace_links = {
'requirement_to_architecture': [],
'architecture_to_code': [],
'code_to_test': [],
'requirement_to_code': [], # Derived from above
'requirement_to_test': [] # Derived from above
}
def add_requirement(self, req_id: str, req_text: str,
req_type: str, priority: str):
"""
Add requirement to traceability system.
Args:
req_id: Unique requirement identifier
req_text: Requirement text
req_type: Type of requirement (functional, non-functional, etc.)
priority: Requirement priority
"""
self.knowledge_graph.add_node(
req_id,
'requirement',
{
'text': req_text,
'type': req_type,
'priority': priority
}
)
def link_requirement_to_architecture(self, req_id: str,
component_id: str,
llm_client):
"""
Create traceability link from requirement to architectural component.
Args:
req_id: Requirement identifier
component_id: Component identifier
llm_client: LLM client for validation
"""
# Validate the link makes sense
req_node = self.knowledge_graph.nodes.get(req_id)
comp_node = self.knowledge_graph.nodes.get(component_id)
if not req_node or not comp_node:
return False
# Use LLM to verify the mapping is reasonable
validation = self._validate_requirement_mapping(
req_node,
comp_node,
llm_client
)
if validation['is_valid']:
self.knowledge_graph.add_edge(
req_id,
component_id,
'implemented_by',
{'confidence': validation['confidence']}
)
self.trace_links['requirement_to_architecture'].append({
'requirement': req_id,
'component': component_id,
'confidence': validation['confidence']
})
return True
return False
def link_architecture_to_code(self, component_id: str,
code_artifact_id: str):
"""
Link architectural component to code artifact.
Args:
component_id: Component identifier
code_artifact_id: Code artifact identifier (class, module, etc.)
"""
self.knowledge_graph.add_edge(
component_id,
code_artifact_id,
'realized_by'
)
self.trace_links['architecture_to_code'].append({
'component': component_id,
'code_artifact': code_artifact_id
})
def link_code_to_test(self, code_artifact_id: str, test_id: str,
coverage_percentage: float):
"""
Link code artifact to test case.
Args:
code_artifact_id: Code artifact identifier
test_id: Test case identifier
coverage_percentage: Test coverage percentage
"""
self.knowledge_graph.add_edge(
code_artifact_id,
test_id,
'tested_by',
{'coverage': coverage_percentage}
)
self.trace_links['code_to_test'].append({
'code_artifact': code_artifact_id,
'test': test_id,
'coverage': coverage_percentage
})
def compute_derived_traces(self):
"""
Compute derived traceability links by following chains.
"""
# Requirement to Code (via Architecture)
for req_to_arch in self.trace_links['requirement_to_architecture']:
req_id = req_to_arch['requirement']
comp_id = req_to_arch['component']
# Find code artifacts for this component
for arch_to_code in self.trace_links['architecture_to_code']:
if arch_to_code['component'] == comp_id:
code_id = arch_to_code['code_artifact']
self.trace_links['requirement_to_code'].append({
'requirement': req_id,
'code_artifact': code_id,
'via_component': comp_id
})
# Requirement to Test (via Code)
for req_to_code in self.trace_links['requirement_to_code']:
req_id = req_to_code['requirement']
code_id = req_to_code['code_artifact']
# Find tests for this code
for code_to_test in self.trace_links['code_to_test']:
if code_to_test['code_artifact'] == code_id:
test_id = code_to_test['test']
self.trace_links['requirement_to_test'].append({
'requirement': req_id,
'test': test_id,
'via_code': code_id,
'coverage': code_to_test['coverage']
})
def find_untraceable_requirements(self) -> List[str]:
"""
Find requirements with no implementation trace.
Returns:
List of requirement IDs with no implementation
"""
all_requirements = set(
self.knowledge_graph.node_types.get('requirement', set())
)
traced_requirements = set(
link['requirement']
for link in self.trace_links['requirement_to_architecture']
)
return list(all_requirements - traced_requirements)
def find_orphaned_code(self) -> List[str]:
"""
Find code artifacts not traced to any requirement.
Returns:
List of code artifact IDs with no requirement trace
"""
all_code = set()
for node_type in ['class', 'module', 'function']:
all_code.update(
self.knowledge_graph.node_types.get(node_type, set())
)
traced_code = set(
link['code_artifact']
for link in self.trace_links['requirement_to_code']
)
return list(all_code - traced_code)
def find_untested_code(self) -> List[str]:
"""
Find code artifacts with no test coverage.
Returns:
List of code artifact IDs with no tests
"""
all_code = set()
for node_type in ['class', 'module', 'function']:
all_code.update(
self.knowledge_graph.node_types.get(node_type, set())
)
tested_code = set(
link['code_artifact']
for link in self.trace_links['code_to_test']
)
return list(all_code - tested_code)
def generate_traceability_report(self) -> Dict:
"""
Generate comprehensive traceability report.
Returns:
Report with traceability metrics and gaps
"""
self.compute_derived_traces()
total_requirements = len(
self.knowledge_graph.node_types.get('requirement', set())
)
traced_requirements = len(set(
link['requirement']
for link in self.trace_links['requirement_to_architecture']
))
total_code = sum(
len(self.knowledge_graph.node_types.get(t, set()))
for t in ['class', 'module', 'function']
)
traced_code = len(set(
link['code_artifact']
for link in self.trace_links['requirement_to_code']
))
tested_code = len(set(
link['code_artifact']
for link in self.trace_links['code_to_test']
))
report = {
'requirements': {
'total': total_requirements,
'traced': traced_requirements,
'untraced': total_requirements - traced_requirements,
'trace_percentage': (
(traced_requirements / total_requirements * 100)
if total_requirements > 0 else 0
)
},
'code': {
'total': total_code,
'traced_to_requirements': traced_code,
'orphaned': total_code - traced_code,
'tested': tested_code,
'untested': total_code - tested_code,
'requirement_trace_percentage': (
(traced_code / total_code * 100)
if total_code > 0 else 0
),
'test_coverage_percentage': (
(tested_code / total_code * 100)
if total_code > 0 else 0
)
},
'gaps': {
'untraceable_requirements': self.find_untraceable_requirements(),
'orphaned_code': self.find_orphaned_code(),
'untested_code': self.find_untested_code()
}
}
return report
def _validate_requirement_mapping(self, req_node: Dict,
comp_node: Dict,
llm_client) -> Dict:
"""
Validate that requirement to component mapping is reasonable.
Args:
req_node: Requirement node data
comp_node: Component node data
llm_client: LLM client for validation
Returns:
Validation result with confidence score
"""
prompt = f"""Evaluate if this requirement-to-component mapping is valid.
Requirement: {req_node['properties']['text']}
Requirement Type: {req_node['properties']['type']}
Component: {comp_node['properties']['name']}
Component Type: {comp_node['properties'].get('type', 'unknown')}
Component Responsibilities: {comp_node['properties'].get('responsibilities', 'unknown')}
Is this component a reasonable implementation of this requirement?
Provide:
1. is_valid: true/false
2. confidence: 0.0 to 1.0
3. reasoning: brief explanation
Respond in JSON format."""
response = llm_client.generate(
prompt,
max_tokens=300,
temperature=0.3
)
try:
import json
result = json.loads(response)
return result
except json.JSONDecodeError:
return {
'is_valid': False,
'confidence': 0.0,
'reasoning': 'Failed to parse validation response'
}
This traceability system provides visibility into the completeness and correctness of the implementation. It identifies requirements that have no implementation, code that serves no documented requirement, and code that lacks test coverage. These insights guide development priorities and quality improvement efforts.
MULTI-MODEL LLM SUPPORT AND GPU ACCELERATION
Supporting both local and remote LLMs with various GPU architectures provides flexibility in deployment scenarios. Some organizations prefer local models for data privacy and cost control. Others prefer remote models for access to the latest capabilities without infrastructure management. The system should support both seamlessly.
GPU acceleration is essential for acceptable performance with local models. Different hardware platforms require different acceleration libraries. NVIDIA GPUs use CUDA. AMD GPUs use ROCm. Intel GPUs use their oneAPI toolkit. Apple Silicon uses Metal Performance Shaders. The system must detect available hardware and configure acceleration appropriately.
Here is an implementation supporting multiple LLM backends and GPU platforms:
import os
import platform
from abc import ABC, abstractmethod
from typing import Optional, Dict, List
import asyncio
class LLMBackend(ABC):
"""
Abstract base for LLM backends.
"""
@abstractmethod
def generate(self, prompt: str, max_tokens: int = 1000,
temperature: float = 0.7) -> str:
"""Generate text from prompt."""
pass
@abstractmethod
async def generate_async(self, prompt: str, max_tokens: int = 1000,
temperature: float = 0.7) -> str:
"""Async text generation."""
pass
@abstractmethod
def embed(self, text: str) -> List[float]:
"""Generate embedding for text."""
pass
class LocalLLMBackend(LLMBackend):
"""
Backend for local LLM inference with GPU acceleration.
"""
def __init__(self, model_path: str, device: str = 'auto'):
"""
Initialize local LLM backend.
Args:
model_path: Path to model files
device: Device to use ('cuda', 'rocm', 'mps', 'cpu', 'auto')
"""
self.model_path = model_path
self.device = self._detect_device() if device == 'auto' else device
self.model = None
self.tokenizer = None
self._initialize_model()
def _detect_device(self) -> str:
"""
Detect available GPU acceleration.
Returns:
Device string ('cuda', 'rocm', 'mps', or 'cpu')
"""
# Check for NVIDIA CUDA
try:
import torch
if torch.cuda.is_available():
return 'cuda'
except ImportError:
pass
# Check for AMD ROCm
try:
import torch
if hasattr(torch.version, 'hip') and torch.version.hip is not None:
return 'rocm'
except (ImportError, AttributeError):
pass
# Check for Apple Metal
if platform.system() == 'Darwin':
try:
import torch
if hasattr(torch.backends, 'mps') and torch.backends.mps.is_available():
return 'mps'
except (ImportError, AttributeError):
pass
# Fallback to CPU
return 'cpu'
def _initialize_model(self):
"""Initialize model with appropriate acceleration."""
if self.device == 'cuda':
self._initialize_cuda_model()
elif self.device == 'rocm':
self._initialize_rocm_model()
elif self.device == 'mps':
self._initialize_mps_model()
else:
self._initialize_cpu_model()
def _initialize_cuda_model(self):
"""Initialize model with NVIDIA CUDA acceleration."""
try:
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
self.tokenizer = AutoTokenizer.from_pretrained(self.model_path)
self.model = AutoModelForCausalLM.from_pretrained(
self.model_path,
torch_dtype=torch.float16, # Use FP16 for efficiency
device_map='auto' # Automatically distribute across GPUs
)
print(f"Model loaded on CUDA device: {torch.cuda.get_device_name(0)}")
except Exception as e:
print(f"Failed to initialize CUDA model: {e}")
self._initialize_cpu_model()
def _initialize_rocm_model(self):
"""Initialize model with AMD ROCm acceleration."""
try:
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
self.tokenizer = AutoTokenizer.from_pretrained(self.model_path)
self.model = AutoModelForCausalLM.from_pretrained(
self.model_path,
torch_dtype=torch.float16,
device_map='auto'
)
# Move to ROCm device
if torch.cuda.is_available():
self.model = self.model.to('cuda')
print("Model loaded with ROCm acceleration")
except Exception as e:
print(f"Failed to initialize ROCm model: {e}")
self._initialize_cpu_model()
def _initialize_mps_model(self):
"""Initialize model with Apple Metal Performance Shaders."""
try:
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
self.tokenizer = AutoTokenizer.from_pretrained(self.model_path)
self.model = AutoModelForCausalLM.from_pretrained(
self.model_path,
torch_dtype=torch.float32 # MPS may not support FP16 for all ops
)
# Move to MPS device
self.model = self.model.to('mps')
print("Model loaded with Apple Metal acceleration")
except Exception as e:
print(f"Failed to initialize MPS model: {e}")
self._initialize_cpu_model()
def _initialize_cpu_model(self):
"""Initialize model for CPU inference."""
try:
from transformers import AutoModelForCausalLM, AutoTokenizer
self.tokenizer = AutoTokenizer.from_pretrained(self.model_path)
self.model = AutoModelForCausalLM.from_pretrained(
self.model_path,
torch_dtype='auto'
)
print("Model loaded on CPU (no GPU acceleration available)")
except Exception as e:
print(f"Failed to initialize CPU model: {e}")
raise
def generate(self, prompt: str, max_tokens: int = 1000,
temperature: float = 0.7) -> str:
"""
Generate text from prompt.
Args:
prompt: Input prompt
max_tokens: Maximum tokens to generate
temperature: Sampling temperature
Returns:
Generated text
"""
import torch
inputs = self.tokenizer(prompt, return_tensors='pt')
# Move inputs to same device as model
if self.device != 'cpu':
inputs = {k: v.to(self.model.device) for k, v in inputs.items()}
with torch.no_grad():
outputs = self.model.generate(
**inputs,
max_new_tokens=max_tokens,
temperature=temperature,
do_sample=temperature > 0,
pad_token_id=self.tokenizer.eos_token_id
)
generated_text = self.tokenizer.decode(
outputs[0],
skip_special_tokens=True
)
# Remove the prompt from output
if generated_text.startswith(prompt):
generated_text = generated_text[len(prompt):].strip()
return generated_text
async def generate_async(self, prompt: str, max_tokens: int = 1000,
temperature: float = 0.7) -> str:
"""
Async text generation (runs in thread pool to avoid blocking).
"""
loop = asyncio.get_event_loop()
return await loop.run_in_executor(
None,
self.generate,
prompt,
max_tokens,
temperature
)
def embed(self, text: str) -> List[float]:
"""
Generate embedding for text.
Args:
text: Input text
Returns:
Embedding vector
"""
import torch
inputs = self.tokenizer(
text,
return_tensors='pt',
truncation=True,
max_length=512
)
if self.device != 'cpu':
inputs = {k: v.to(self.model.device) for k, v in inputs.items()}
with torch.no_grad():
outputs = self.model(**inputs, output_hidden_states=True)
# Use last hidden state, mean pooling
hidden_states = outputs.hidden_states[-1]
embedding = torch.mean(hidden_states[0], dim=0)
return embedding.cpu().numpy().tolist()
class RemoteLLMBackend(LLMBackend):
"""
Backend for remote LLM APIs (OpenAI, Anthropic, etc.).
"""
def __init__(self, api_key: str, model_name: str,
provider: str = 'openai'):
"""
Initialize remote LLM backend.
Args:
api_key: API key for service
model_name: Model identifier
provider: Service provider ('openai', 'anthropic', etc.)
"""
self.api_key = api_key
self.model_name = model_name
self.provider = provider
self.client = self._initialize_client()
def _initialize_client(self):
"""Initialize API client based on provider."""
if self.provider == 'openai':
try:
import openai
openai.api_key = self.api_key
return openai
except ImportError:
raise ImportError("OpenAI package not installed")
elif self.provider == 'anthropic':
try:
import anthropic
return anthropic.Anthropic(api_key=self.api_key)
except ImportError:
raise ImportError("Anthropic package not installed")
else:
raise ValueError(f"Unsupported provider: {self.provider}")
def generate(self, prompt: str, max_tokens: int = 1000,
temperature: float = 0.7) -> str:
"""Generate text using remote API."""
if self.provider == 'openai':
response = self.client.ChatCompletion.create(
model=self.model_name,
messages=[{'role': 'user', 'content': prompt}],
max_tokens=max_tokens,
temperature=temperature
)
return response.choices[0].message.content
elif self.provider == 'anthropic':
message = self.client.messages.create(
model=self.model_name,
max_tokens=max_tokens,
temperature=temperature,
messages=[{'role': 'user', 'content': prompt}]
)
return message.content[0].text
async def generate_async(self, prompt: str, max_tokens: int = 1000,
temperature: float = 0.7) -> str:
"""Async text generation."""
loop = asyncio.get_event_loop()
return await loop.run_in_executor(
None,
self.generate,
prompt,
max_tokens,
temperature
)
def embed(self, text: str) -> List[float]:
"""Generate embedding using remote API."""
if self.provider == 'openai':
response = self.client.Embedding.create(
model='text-embedding-ada-002',
input=text
)
return response.data[0].embedding
else:
raise NotImplementedError(
f"Embedding not implemented for {self.provider}"
)
class LLMManager:
"""
Manages multiple LLM backends and routes requests appropriately.
"""
def __init__(self):
self.backends = {}
self.default_backend = None
def register_backend(self, name: str, backend: LLMBackend,
is_default: bool = False):
"""
Register an LLM backend.
Args:
name: Backend identifier
backend: LLM backend instance
is_default: Whether this is the default backend
"""
self.backends[name] = backend
if is_default or self.default_backend is None:
self.default_backend = name
def get_backend(self, name: Optional[str] = None) -> LLMBackend:
"""
Get LLM backend by name.
Args:
name: Backend name (uses default if None)
Returns:
LLM backend instance
"""
backend_name = name or self.default_backend
if backend_name not in self.backends:
raise ValueError(f"Unknown backend: {backend_name}")
return self.backends[backend_name]
def generate(self, prompt: str, backend: Optional[str] = None,
**kwargs) -> str:
"""
Generate text using specified or default backend.
Args:
prompt: Input prompt
backend: Backend name (optional)
**kwargs: Additional generation parameters
Returns:
Generated text
"""
llm = self.get_backend(backend)
return llm.generate(prompt, **kwargs)
async def generate_async(self, prompt: str,
backend: Optional[str] = None,
**kwargs) -> str:
"""Async text generation."""
llm = self.get_backend(backend)
return await llm.generate_async(prompt, **kwargs)
This multi-backend architecture allows the system to use local models for privacy-sensitive analysis while leveraging remote models for tasks requiring the latest capabilities. The automatic GPU detection and configuration ensures optimal performance across different hardware platforms without manual configuration.
CONCLUSION
Building effective multi-agent AI systems for analyzing large database architectures and codebases requires careful attention to numerous technical challenges. The patterns and practices described in this article address the fundamental issues of limited context memory, bias, and hallucinations through systematic approaches.
Human-in-the-loop validation ensures critical decisions receive expert oversight. Lightweight prompt decomposition breaks complex analysis into manageable pieces. Advanced reasoning strategies including self-reflection, chain-of-thought, and tree-of-thought improve analytical accuracy. Multi-language parsers extract structured information from diverse codebases. Specialized agents provide focused expertise while collaborating through shared knowledge stores. Named entity recognition extracts domain-specific information from various document types. Dual storage using vector databases and knowledge graphs enables both semantic and structural queries. Hierarchical summarization manages context windows effectively. Requirement-to-code traceability provides visibility into implementation completeness. Multi-model LLM support with GPU acceleration offers deployment flexibility.
Together, these patterns create a comprehensive framework for building analysis tools that provide accurate, reliable insights into complex software systems. The implementations provided demonstrate practical approaches to each challenge, offering starting points for developers building such systems. While each pattern addresses specific issues, their real power emerges from integration into cohesive systems where they complement and reinforce each other.
The field of AI-assisted code analysis continues evolving rapidly. New language models with larger context windows, better reasoning capabilities, and improved factual accuracy will enhance these systems. However, the fundamental architectural patterns described here will remain relevant because they address inherent challenges in analyzing large, complex systems regardless of the specific AI technologies employed. By combining human expertise with AI capabilities through well-designed patterns, we can build tools that significantly improve our ability to understand, maintain, and evolve large software systems.