ai-coding-assistant 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. ai_coding_assistant-0.5.0.dist-info/METADATA +226 -0
  2. ai_coding_assistant-0.5.0.dist-info/RECORD +89 -0
  3. ai_coding_assistant-0.5.0.dist-info/WHEEL +4 -0
  4. ai_coding_assistant-0.5.0.dist-info/entry_points.txt +3 -0
  5. ai_coding_assistant-0.5.0.dist-info/licenses/LICENSE +21 -0
  6. coding_assistant/__init__.py +3 -0
  7. coding_assistant/__main__.py +19 -0
  8. coding_assistant/cli/__init__.py +1 -0
  9. coding_assistant/cli/app.py +158 -0
  10. coding_assistant/cli/commands/__init__.py +19 -0
  11. coding_assistant/cli/commands/ask.py +178 -0
  12. coding_assistant/cli/commands/config.py +438 -0
  13. coding_assistant/cli/commands/diagram.py +267 -0
  14. coding_assistant/cli/commands/document.py +410 -0
  15. coding_assistant/cli/commands/explain.py +192 -0
  16. coding_assistant/cli/commands/fix.py +249 -0
  17. coding_assistant/cli/commands/index.py +162 -0
  18. coding_assistant/cli/commands/refactor.py +245 -0
  19. coding_assistant/cli/commands/search.py +182 -0
  20. coding_assistant/cli/commands/serve_docs.py +128 -0
  21. coding_assistant/cli/repl.py +381 -0
  22. coding_assistant/cli/theme.py +90 -0
  23. coding_assistant/codebase/__init__.py +1 -0
  24. coding_assistant/codebase/crawler.py +93 -0
  25. coding_assistant/codebase/parser.py +266 -0
  26. coding_assistant/config/__init__.py +25 -0
  27. coding_assistant/config/config_manager.py +615 -0
  28. coding_assistant/config/settings.py +82 -0
  29. coding_assistant/context/__init__.py +19 -0
  30. coding_assistant/context/chunker.py +443 -0
  31. coding_assistant/context/enhanced_retriever.py +322 -0
  32. coding_assistant/context/hybrid_search.py +311 -0
  33. coding_assistant/context/ranker.py +355 -0
  34. coding_assistant/context/retriever.py +119 -0
  35. coding_assistant/context/window.py +362 -0
  36. coding_assistant/documentation/__init__.py +23 -0
  37. coding_assistant/documentation/agents/__init__.py +27 -0
  38. coding_assistant/documentation/agents/coordinator.py +510 -0
  39. coding_assistant/documentation/agents/module_documenter.py +111 -0
  40. coding_assistant/documentation/agents/synthesizer.py +139 -0
  41. coding_assistant/documentation/agents/task_delegator.py +100 -0
  42. coding_assistant/documentation/decomposition/__init__.py +21 -0
  43. coding_assistant/documentation/decomposition/context_preserver.py +477 -0
  44. coding_assistant/documentation/decomposition/module_detector.py +302 -0
  45. coding_assistant/documentation/decomposition/partitioner.py +621 -0
  46. coding_assistant/documentation/generators/__init__.py +14 -0
  47. coding_assistant/documentation/generators/dataflow_generator.py +440 -0
  48. coding_assistant/documentation/generators/diagram_generator.py +511 -0
  49. coding_assistant/documentation/graph/__init__.py +13 -0
  50. coding_assistant/documentation/graph/dependency_builder.py +468 -0
  51. coding_assistant/documentation/graph/module_analyzer.py +475 -0
  52. coding_assistant/documentation/writers/__init__.py +11 -0
  53. coding_assistant/documentation/writers/markdown_writer.py +322 -0
  54. coding_assistant/embeddings/__init__.py +0 -0
  55. coding_assistant/embeddings/generator.py +89 -0
  56. coding_assistant/embeddings/store.py +187 -0
  57. coding_assistant/exceptions/__init__.py +50 -0
  58. coding_assistant/exceptions/base.py +110 -0
  59. coding_assistant/exceptions/llm.py +249 -0
  60. coding_assistant/exceptions/recovery.py +263 -0
  61. coding_assistant/exceptions/storage.py +213 -0
  62. coding_assistant/exceptions/validation.py +230 -0
  63. coding_assistant/llm/__init__.py +1 -0
  64. coding_assistant/llm/client.py +277 -0
  65. coding_assistant/llm/gemini_client.py +181 -0
  66. coding_assistant/llm/groq_client.py +160 -0
  67. coding_assistant/llm/prompts.py +98 -0
  68. coding_assistant/llm/together_client.py +160 -0
  69. coding_assistant/operations/__init__.py +13 -0
  70. coding_assistant/operations/differ.py +369 -0
  71. coding_assistant/operations/generator.py +347 -0
  72. coding_assistant/operations/linter.py +430 -0
  73. coding_assistant/operations/validator.py +406 -0
  74. coding_assistant/storage/__init__.py +9 -0
  75. coding_assistant/storage/database.py +363 -0
  76. coding_assistant/storage/session.py +231 -0
  77. coding_assistant/utils/__init__.py +31 -0
  78. coding_assistant/utils/cache.py +477 -0
  79. coding_assistant/utils/hardware.py +132 -0
  80. coding_assistant/utils/keystore.py +206 -0
  81. coding_assistant/utils/logger.py +32 -0
  82. coding_assistant/utils/progress.py +311 -0
  83. coding_assistant/validation/__init__.py +13 -0
  84. coding_assistant/validation/files.py +305 -0
  85. coding_assistant/validation/inputs.py +335 -0
  86. coding_assistant/validation/params.py +280 -0
  87. coding_assistant/validation/sanitizers.py +243 -0
  88. coding_assistant/vcs/__init__.py +5 -0
  89. coding_assistant/vcs/git.py +269 -0
@@ -0,0 +1,468 @@
1
+ """Build comprehensive dependency graphs for code repositories."""
2
+
3
+ from typing import Dict, List, Set, Tuple, Optional
4
+ from pathlib import Path
5
+ from dataclasses import dataclass, field
6
+ import networkx as nx
7
+ from collections import defaultdict
8
+
9
+ from coding_assistant.codebase.parser import CodeParser
10
+ from coding_assistant.codebase.crawler import CodebaseCrawler
11
+ from coding_assistant.utils.logger import get_logger
12
+
13
+ logger = get_logger(__name__)
14
+
15
+
16
+ @dataclass
17
+ class CodeEntity:
18
+ """Represents a code entity (file, module, class, function)."""
19
+ type: str # 'file', 'module', 'class', 'function'
20
+ name: str
21
+ path: str
22
+ imports: List[str] = field(default_factory=list)
23
+ exports: List[str] = field(default_factory=list)
24
+ calls: List[str] = field(default_factory=list) # Function/method calls
25
+ line_count: int = 0
26
+ language: str = 'python'
27
+ metadata: Dict = field(default_factory=dict)
28
+
29
+ def __hash__(self):
30
+ """Make entity hashable for use in sets and as dict keys."""
31
+ return hash((self.type, self.name, self.path))
32
+
33
+
34
+ class DependencyGraphBuilder:
35
+ """
36
+ Build multi-level dependency graphs for code analysis.
37
+
38
+ Creates three types of graphs:
39
+ 1. File-level: Dependencies between files based on imports
40
+ 2. Module-level: Dependencies between logical modules/packages
41
+ 3. Call-level: Function/method call relationships
42
+ """
43
+
44
+ def __init__(self, codebase_path: Optional[Path] = None):
45
+ """
46
+ Initialize the dependency graph builder.
47
+
48
+ Args:
49
+ codebase_path: Path to the codebase root (defaults to current directory)
50
+ """
51
+ self.codebase_path = codebase_path or Path.cwd()
52
+ self.file_graph = nx.DiGraph()
53
+ self.module_graph = nx.DiGraph()
54
+ self.call_graph = nx.DiGraph()
55
+ self.parser = CodeParser()
56
+ self.crawler = CodebaseCrawler(self.codebase_path)
57
+
58
+ # Cache for parsed files
59
+ self._parsed_cache: Dict[str, Dict] = {}
60
+ self._entities: Dict[str, CodeEntity] = {}
61
+
62
+ def build_file_graph(self, codebase_path: Optional[Path] = None) -> nx.DiGraph:
63
+ """
64
+ Build file-level dependency graph based on imports.
65
+
66
+ Args:
67
+ codebase_path: Path to analyze (overrides instance path if provided)
68
+
69
+ Returns:
70
+ NetworkX DiGraph with files as nodes and imports as edges
71
+ """
72
+ path = codebase_path or self.codebase_path
73
+ logger.info(f"Building file dependency graph for {path}")
74
+
75
+ # Update crawler path if different
76
+ if path != self.codebase_path:
77
+ self.crawler = CodebaseCrawler(path)
78
+ self.codebase_path = path
79
+
80
+ # Crawl for all code files
81
+ file_info_list = self.crawler.scan(max_files=1000)
82
+
83
+ # Filter for Python files (we'll add multi-language support later)
84
+ python_files = [
85
+ f['absolute_path'] for f in file_info_list
86
+ if f['extension'] == '.py'
87
+ ]
88
+
89
+ logger.info(f"Found {len(python_files)} Python files to analyze")
90
+
91
+ # Parse each file and extract imports
92
+ for file_path in python_files:
93
+ try:
94
+ self._process_file_for_graph(file_path)
95
+ except Exception as e:
96
+ logger.warning(f"Failed to process {file_path}: {e}")
97
+ continue
98
+
99
+ # Build edges based on imports
100
+ self._build_file_edges()
101
+
102
+ logger.info(
103
+ f"File graph built: {self.file_graph.number_of_nodes()} nodes, "
104
+ f"{self.file_graph.number_of_edges()} edges"
105
+ )
106
+
107
+ return self.file_graph
108
+
109
+ def _process_file_for_graph(self, file_path: str):
110
+ """Process a single file and add to graph."""
111
+ try:
112
+ with open(file_path, 'r', encoding='utf-8') as f:
113
+ content = f.read()
114
+ except Exception as e:
115
+ logger.warning(f"Could not read {file_path}: {e}")
116
+ return
117
+
118
+ # Parse file
119
+ parsed = self.parser.parse_file(file_path, content)
120
+ self._parsed_cache[file_path] = parsed
121
+
122
+ # Create entity for this file
123
+ entity = CodeEntity(
124
+ type='file',
125
+ name=Path(file_path).name,
126
+ path=file_path,
127
+ imports=parsed.get('imports', []),
128
+ line_count=len(content.split('\n')),
129
+ language='python',
130
+ metadata={
131
+ 'functions': len(parsed.get('functions', [])),
132
+ 'classes': len(parsed.get('classes', []))
133
+ }
134
+ )
135
+
136
+ self._entities[file_path] = entity
137
+
138
+ # Add node to graph
139
+ self.file_graph.add_node(
140
+ file_path,
141
+ entity=entity,
142
+ label=entity.name,
143
+ imports=entity.imports,
144
+ line_count=entity.line_count
145
+ )
146
+
147
+ def _build_file_edges(self):
148
+ """Build edges between files based on import relationships."""
149
+ # Build mapping of module names to file paths
150
+ module_to_file = self._build_module_mapping()
151
+
152
+ for file_path, entity in self._entities.items():
153
+ for import_stmt in entity.imports:
154
+ # Parse import statement to extract module name
155
+ imported_module = self._extract_module_from_import(import_stmt)
156
+
157
+ if imported_module in module_to_file:
158
+ target_file = module_to_file[imported_module]
159
+ if target_file != file_path: # Avoid self-loops
160
+ self.file_graph.add_edge(
161
+ file_path,
162
+ target_file,
163
+ import_type='direct'
164
+ )
165
+
166
+ def _build_module_mapping(self) -> Dict[str, str]:
167
+ """
168
+ Build mapping from Python module names to file paths.
169
+
170
+ Example: 'coding_assistant.llm.client' -> 'src/coding_assistant/llm/client.py'
171
+ """
172
+ module_to_file = {}
173
+
174
+ for file_path in self._entities.keys():
175
+ # Convert file path to module name
176
+ path_obj = Path(file_path)
177
+
178
+ # Find the root of the Python package
179
+ parts = path_obj.parts
180
+
181
+ # Look for src/ or the project root
182
+ try:
183
+ if 'src' in parts:
184
+ src_idx = parts.index('src')
185
+ module_parts = parts[src_idx + 1:]
186
+ else:
187
+ # Use relative to codebase path
188
+ rel_path = path_obj.relative_to(self.codebase_path)
189
+ module_parts = rel_path.parts
190
+
191
+ # Remove .py extension
192
+ if module_parts[-1].endswith('.py'):
193
+ module_parts = list(module_parts[:-1]) + [module_parts[-1][:-3]]
194
+
195
+ # Remove __init__ from module name
196
+ if module_parts[-1] == '__init__':
197
+ module_parts = module_parts[:-1]
198
+
199
+ module_name = '.'.join(module_parts)
200
+ module_to_file[module_name] = file_path
201
+
202
+ except ValueError:
203
+ # Path is not relative to codebase_path, skip
204
+ continue
205
+
206
+ return module_to_file
207
+
208
+ def _extract_module_from_import(self, import_stmt: str) -> str:
209
+ """
210
+ Extract module name from import statement.
211
+
212
+ Examples:
213
+ 'import foo.bar' -> 'foo.bar'
214
+ 'from foo.bar import baz' -> 'foo.bar'
215
+ 'from . import something' -> '' (relative import, skip)
216
+ """
217
+ import_stmt = import_stmt.strip()
218
+
219
+ if import_stmt.startswith('from '):
220
+ # from X import Y
221
+ parts = import_stmt.split()
222
+ if len(parts) >= 2:
223
+ module = parts[1]
224
+ # Skip relative imports for now
225
+ if module.startswith('.'):
226
+ return ''
227
+ return module
228
+ elif import_stmt.startswith('import '):
229
+ # import X
230
+ parts = import_stmt.split()
231
+ if len(parts) >= 2:
232
+ module = parts[1]
233
+ # Handle 'import X as Y'
234
+ if 'as' in parts:
235
+ as_idx = parts.index('as')
236
+ module = parts[as_idx - 1]
237
+ return module
238
+
239
+ return ''
240
+
241
+ def build_module_graph(self, file_graph: Optional[nx.DiGraph] = None) -> nx.DiGraph:
242
+ """
243
+ Build module-level graph by clustering related files.
244
+
245
+ Groups files into logical modules (packages/directories) and
246
+ creates module-level dependencies.
247
+
248
+ Args:
249
+ file_graph: File graph to use (uses instance graph if not provided)
250
+
251
+ Returns:
252
+ NetworkX DiGraph with modules as nodes
253
+ """
254
+ graph = file_graph or self.file_graph
255
+
256
+ if graph.number_of_nodes() == 0:
257
+ logger.warning("File graph is empty, building it first")
258
+ self.build_file_graph()
259
+ graph = self.file_graph
260
+
261
+ logger.info("Building module-level graph")
262
+
263
+ # Group files by directory (module)
264
+ module_files: Dict[str, List[str]] = defaultdict(list)
265
+
266
+ for file_path in graph.nodes():
267
+ module_name = self._get_module_name(file_path)
268
+ module_files[module_name].append(file_path)
269
+
270
+ # Create module nodes
271
+ for module_name, files in module_files.items():
272
+ total_lines = sum(
273
+ graph.nodes[f].get('line_count', 0) for f in files
274
+ )
275
+
276
+ self.module_graph.add_node(
277
+ module_name,
278
+ files=files,
279
+ file_count=len(files),
280
+ line_count=total_lines
281
+ )
282
+
283
+ # Create module edges based on file dependencies
284
+ for source_file, target_file in graph.edges():
285
+ source_module = self._get_module_name(source_file)
286
+ target_module = self._get_module_name(target_file)
287
+
288
+ if source_module != target_module:
289
+ # Add or strengthen edge
290
+ if self.module_graph.has_edge(source_module, target_module):
291
+ self.module_graph[source_module][target_module]['weight'] += 1
292
+ else:
293
+ self.module_graph.add_edge(
294
+ source_module,
295
+ target_module,
296
+ weight=1
297
+ )
298
+
299
+ logger.info(
300
+ f"Module graph built: {self.module_graph.number_of_nodes()} modules, "
301
+ f"{self.module_graph.number_of_edges()} dependencies"
302
+ )
303
+
304
+ return self.module_graph
305
+
306
+ def _get_module_name(self, file_path: str) -> str:
307
+ """
308
+ Get module name from file path.
309
+
310
+ Example: 'src/coding_assistant/llm/client.py' -> 'coding_assistant.llm'
311
+ """
312
+ path_obj = Path(file_path)
313
+
314
+ try:
315
+ parts = path_obj.parts
316
+
317
+ # Find src or project root
318
+ if 'src' in parts:
319
+ src_idx = parts.index('src')
320
+ module_parts = parts[src_idx + 1:-1] # Exclude filename
321
+ else:
322
+ rel_path = path_obj.relative_to(self.codebase_path)
323
+ module_parts = rel_path.parts[:-1] # Exclude filename
324
+
325
+ if not module_parts:
326
+ return 'root'
327
+
328
+ return '.'.join(module_parts)
329
+
330
+ except ValueError:
331
+ return 'external'
332
+
333
+ def detect_cycles(self, graph: Optional[nx.DiGraph] = None) -> List[List[str]]:
334
+ """
335
+ Detect circular dependencies in the graph.
336
+
337
+ Args:
338
+ graph: Graph to analyze (defaults to file_graph)
339
+
340
+ Returns:
341
+ List of cycles, where each cycle is a list of node names
342
+ """
343
+ graph = graph or self.file_graph
344
+
345
+ try:
346
+ cycles = list(nx.simple_cycles(graph))
347
+
348
+ if cycles:
349
+ logger.warning(f"Found {len(cycles)} circular dependencies")
350
+ for i, cycle in enumerate(cycles[:5]): # Log first 5
351
+ logger.warning(f" Cycle {i+1}: {' -> '.join(cycle)}")
352
+ else:
353
+ logger.info("No circular dependencies found")
354
+
355
+ return cycles
356
+
357
+ except Exception as e:
358
+ logger.error(f"Error detecting cycles: {e}")
359
+ return []
360
+
361
+ def compute_metrics(self, graph: Optional[nx.DiGraph] = None) -> Dict:
362
+ """
363
+ Compute graph metrics (centrality, coupling, complexity, etc.).
364
+
365
+ Args:
366
+ graph: Graph to analyze (defaults to file_graph)
367
+
368
+ Returns:
369
+ Dictionary of metrics
370
+ """
371
+ graph = graph or self.file_graph
372
+
373
+ if graph.number_of_nodes() == 0:
374
+ return {
375
+ 'nodes': 0,
376
+ 'edges': 0,
377
+ 'density': 0.0,
378
+ 'avg_degree': 0.0,
379
+ }
380
+
381
+ metrics = {
382
+ 'nodes': graph.number_of_nodes(),
383
+ 'edges': graph.number_of_edges(),
384
+ 'density': nx.density(graph),
385
+ 'avg_degree': sum(dict(graph.degree()).values()) / graph.number_of_nodes(),
386
+ }
387
+
388
+ # Weakly connected components
389
+ if graph.is_directed():
390
+ metrics['connected_components'] = nx.number_weakly_connected_components(graph)
391
+ else:
392
+ metrics['connected_components'] = nx.number_connected_components(graph)
393
+
394
+ # Compute centrality for important nodes
395
+ try:
396
+ centrality = nx.betweenness_centrality(graph)
397
+ metrics['max_centrality'] = max(centrality.values()) if centrality else 0
398
+ metrics['avg_centrality'] = sum(centrality.values()) / len(centrality) if centrality else 0
399
+ except:
400
+ metrics['max_centrality'] = 0
401
+ metrics['avg_centrality'] = 0
402
+
403
+ logger.info(f"Graph metrics: {metrics}")
404
+
405
+ return metrics
406
+
407
+ def get_important_files(self, top_n: int = 10, graph: Optional[nx.DiGraph] = None) -> List[Tuple[str, float]]:
408
+ """
409
+ Get the most important/central files in the codebase.
410
+
411
+ Uses betweenness centrality to identify files that are
412
+ crucial connectors in the dependency graph.
413
+
414
+ Args:
415
+ top_n: Number of files to return
416
+ graph: Graph to analyze (defaults to file_graph)
417
+
418
+ Returns:
419
+ List of (file_path, centrality_score) tuples
420
+ """
421
+ graph = graph or self.file_graph
422
+
423
+ if graph.number_of_nodes() == 0:
424
+ return []
425
+
426
+ try:
427
+ centrality = nx.betweenness_centrality(graph)
428
+ sorted_files = sorted(
429
+ centrality.items(),
430
+ key=lambda x: x[1],
431
+ reverse=True
432
+ )
433
+
434
+ return sorted_files[:top_n]
435
+
436
+ except Exception as e:
437
+ logger.error(f"Error computing important files: {e}")
438
+ return []
439
+
440
+ def export_graph(self, output_path: str, graph: Optional[nx.DiGraph] = None, format: str = 'gml'):
441
+ """
442
+ Export graph to file for visualization or external analysis.
443
+
444
+ Args:
445
+ output_path: Path to save the graph
446
+ graph: Graph to export (defaults to file_graph)
447
+ format: Output format ('gml', 'graphml', 'json')
448
+ """
449
+ graph = graph or self.file_graph
450
+
451
+ try:
452
+ if format == 'gml':
453
+ nx.write_gml(graph, output_path)
454
+ elif format == 'graphml':
455
+ nx.write_graphml(graph, output_path)
456
+ elif format == 'json':
457
+ from networkx.readwrite import json_graph
458
+ import json
459
+ data = json_graph.node_link_data(graph)
460
+ with open(output_path, 'w') as f:
461
+ json.dump(data, f, indent=2)
462
+ else:
463
+ raise ValueError(f"Unsupported format: {format}")
464
+
465
+ logger.info(f"Graph exported to {output_path}")
466
+
467
+ except Exception as e:
468
+ logger.error(f"Failed to export graph: {e}")