archilens 0.2.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.
archilens/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """
2
+ ArchiLens - AI-powered layered architecture & process flow visualization
3
+ for Git repositories.
4
+
5
+ Provides hierarchical drill-down from system context to process flows,
6
+ with AI-augmented analysis, architecture drift detection, and
7
+ business capability mapping.
8
+ """
9
+
10
+ __version__ = "0.1.0"
archilens/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from archilens.cli import main
2
+
3
+ main()
@@ -0,0 +1,441 @@
1
+ """
2
+ AI-powered analysis using LLMs for capabilities beyond static analysis.
3
+
4
+ Handles:
5
+ - Process flow inference from controller/handler code
6
+ - Natural-language annotations for diagram nodes
7
+ - Business capability mapping suggestions
8
+ - Architectural pattern detection
9
+ - Module responsibility summaries
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import logging
16
+ from concurrent.futures import ThreadPoolExecutor, as_completed
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ from archilens.config import AIConfig
21
+ from archilens.models import (
22
+ ArchPattern,
23
+ ArchSnapshot,
24
+ FlowStep,
25
+ ProcessFlow,
26
+ )
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Tool-use schemas (Anthropic structured output)
33
+ # ---------------------------------------------------------------------------
34
+
35
+ _FLOW_TOOL: dict[str, Any] = {
36
+ "name": "record_flow",
37
+ "description": "Record the extracted process flow with all its steps.",
38
+ "input_schema": {
39
+ "type": "object",
40
+ "properties": {
41
+ "flow_name": {"type": "string", "description": "Descriptive name, e.g. 'Create Order Flow'"},
42
+ "trigger": {"type": "string", "description": "What initiates this flow, e.g. 'POST /api/orders'"},
43
+ "description": {"type": "string", "description": "One-sentence summary"},
44
+ "steps": {
45
+ "type": "array",
46
+ "items": {
47
+ "type": "object",
48
+ "properties": {
49
+ "order": {"type": "integer"},
50
+ "actor": {"type": "string"},
51
+ "action": {"type": "string"},
52
+ "target": {"type": "string"},
53
+ "data": {"type": "string"},
54
+ "is_async": {"type": "boolean"},
55
+ "condition": {"type": "string"},
56
+ },
57
+ "required": ["order", "actor", "action", "target"],
58
+ },
59
+ },
60
+ },
61
+ "required": ["flow_name", "trigger", "steps"],
62
+ },
63
+ }
64
+
65
+ _SUMMARY_TOOL: dict[str, Any] = {
66
+ "name": "record_summary",
67
+ "description": "Record a concise module summary.",
68
+ "input_schema": {
69
+ "type": "object",
70
+ "properties": {
71
+ "summary": {"type": "string", "description": "One sentence describing what this module does"},
72
+ "responsibility": {"type": "string", "description": "Primary responsibility in 3-5 words"},
73
+ "suggested_capability": {
74
+ "type": "string",
75
+ "description": "Business capability this maps to (e.g. 'Order Management', 'Authentication')",
76
+ },
77
+ },
78
+ "required": ["summary", "responsibility"],
79
+ },
80
+ }
81
+
82
+ _PATTERN_TOOL: dict[str, Any] = {
83
+ "name": "record_patterns",
84
+ "description": "Record detected architectural patterns.",
85
+ "input_schema": {
86
+ "type": "object",
87
+ "properties": {
88
+ "patterns": {
89
+ "type": "array",
90
+ "items": {
91
+ "type": "string",
92
+ "enum": [
93
+ "mvc", "hexagonal", "layered", "microservices",
94
+ "event_driven", "cqrs", "repository", "factory",
95
+ "observer", "middleware_chain", "saga", "clean_architecture",
96
+ ],
97
+ },
98
+ "description": "List of detected pattern names",
99
+ },
100
+ "reasoning": {
101
+ "type": "object",
102
+ "additionalProperties": {"type": "string"},
103
+ "description": "Brief explanation per detected pattern",
104
+ },
105
+ },
106
+ "required": ["patterns"],
107
+ },
108
+ }
109
+
110
+
111
+ # ---------------------------------------------------------------------------
112
+ # Prompt templates
113
+ # ---------------------------------------------------------------------------
114
+
115
+ PROCESS_FLOW_PROMPT = """You are an expert software architect analyzing source code to extract runtime process flows.
116
+
117
+ Given the following source code from an entry point (controller/handler/route), identify the end-to-end request lifecycle.
118
+
119
+ **File:** {file_path}
120
+ **Entry Point Type:** {entry_type}
121
+
122
+ ```{language}
123
+ {code}
124
+ ```
125
+
126
+ Additional context - files that this entry point imports or calls:
127
+ {dependency_context}
128
+
129
+ Analyze this code and use the record_flow tool to capture the complete process flow."""
130
+
131
+ MODULE_SUMMARY_PROMPT = """You are an expert software architect. Given the following module information, provide a concise summary.
132
+
133
+ **Module:** {module_name}
134
+ **Files:**
135
+ {file_list}
136
+
137
+ **Key classes/functions found:**
138
+ {symbols}
139
+
140
+ **Dependencies (imports from other modules):**
141
+ {dependencies}
142
+
143
+ Use the record_summary tool to record a one-sentence summary of this module's responsibility."""
144
+
145
+ PATTERN_DETECTION_PROMPT = """You are an expert software architect. Given the following architecture overview, identify which architectural patterns are present.
146
+
147
+ **Project:** {project_name}
148
+ **Modules and their relationships:**
149
+ {module_graph}
150
+
151
+ **Directory structure:**
152
+ {directory_structure}
153
+
154
+ Use the record_patterns tool to record all detected patterns."""
155
+
156
+
157
+ # ---------------------------------------------------------------------------
158
+ # AI Client abstraction
159
+ # ---------------------------------------------------------------------------
160
+
161
+
162
+ class AIClient:
163
+ """
164
+ Abstraction over LLM providers.
165
+
166
+ Supports Anthropic (Claude), OpenAI, Ollama, and any
167
+ OpenAI-compatible endpoint via LiteLLM.
168
+ """
169
+
170
+ def __init__(self, config: AIConfig):
171
+ self.config = config
172
+ self._client: Any = None
173
+
174
+ def _get_client(self) -> Any:
175
+ """Lazy-initialize the LLM client."""
176
+ if self._client is not None:
177
+ return self._client
178
+
179
+ if self.config.provider == "anthropic":
180
+ try:
181
+ import anthropic
182
+
183
+ self._client = anthropic.Anthropic()
184
+ return self._client
185
+ except ImportError as exc:
186
+ raise RuntimeError("anthropic package not installed. Run: pip install anthropic") from exc
187
+ else:
188
+ try:
189
+ import litellm
190
+
191
+ self._client = litellm
192
+ return self._client
193
+ except ImportError as exc:
194
+ raise RuntimeError("litellm package not installed. Run: pip install litellm") from exc
195
+
196
+ def complete(self, prompt: str, max_tokens: int = 4096) -> str:
197
+ """Send a prompt and return the response text."""
198
+ client = self._get_client()
199
+
200
+ if self.config.provider == "anthropic":
201
+ response = client.messages.create(
202
+ model=self.config.model,
203
+ max_tokens=max_tokens,
204
+ messages=[{"role": "user", "content": prompt}],
205
+ )
206
+ return response.content[0].text
207
+ else:
208
+ response = client.completion(
209
+ model=self.config.model,
210
+ messages=[{"role": "user", "content": prompt}],
211
+ max_tokens=max_tokens,
212
+ )
213
+ return response.choices[0].message.content
214
+
215
+ def complete_with_tool(
216
+ self,
217
+ prompt: str,
218
+ tool: dict[str, Any],
219
+ max_tokens: int = 1024,
220
+ ) -> dict[str, Any] | None:
221
+ """
222
+ Use tool use (structured output) to get deterministic JSON.
223
+
224
+ For Anthropic: uses tool_choice={"type":"tool"} for a guaranteed
225
+ structured response — no fence-stripping or JSON hunting needed.
226
+ For other providers: falls back to text completion + JSON parsing.
227
+
228
+ Returns the tool input dict, or None on failure.
229
+ """
230
+ client = self._get_client()
231
+
232
+ if self.config.provider == "anthropic":
233
+ try:
234
+ response = client.messages.create(
235
+ model=self.config.model,
236
+ max_tokens=max_tokens,
237
+ tools=[{
238
+ "name": tool["name"],
239
+ "description": tool.get("description", ""),
240
+ "input_schema": tool["input_schema"],
241
+ }],
242
+ tool_choice={"type": "tool", "name": tool["name"]},
243
+ messages=[{"role": "user", "content": prompt}],
244
+ )
245
+ for block in response.content:
246
+ if hasattr(block, "input"):
247
+ return block.input # type: ignore[return-value]
248
+ except Exception as exc:
249
+ logger.warning("Tool use failed, falling back to text: %s", exc)
250
+
251
+ # Fallback: plain text + JSON parsing (non-Anthropic or on tool-use error)
252
+ text = self.complete(prompt, max_tokens)
253
+ return _parse_json_response(text)
254
+
255
+
256
+ # ---------------------------------------------------------------------------
257
+ # AI Analysis Functions
258
+ # ---------------------------------------------------------------------------
259
+
260
+
261
+ def infer_process_flows(
262
+ entry_point_files: list[dict[str, str]],
263
+ ai_client: AIClient,
264
+ repo_path: Path,
265
+ ) -> list[ProcessFlow]:
266
+ """
267
+ Use AI to infer process flows from entry point source code.
268
+
269
+ Args:
270
+ entry_point_files: List of dicts with keys: path, language, entry_type, dependencies
271
+ ai_client: Configured AI client
272
+ repo_path: Repository root path
273
+
274
+ Returns:
275
+ List of ProcessFlow objects
276
+ """
277
+ flows: list[ProcessFlow] = []
278
+
279
+ for ep in entry_point_files:
280
+ file_path = repo_path / ep["path"]
281
+ if not file_path.exists():
282
+ continue
283
+
284
+ try:
285
+ code = file_path.read_text(encoding="utf-8", errors="ignore")
286
+ except OSError:
287
+ continue
288
+
289
+ if len(code) > 15000:
290
+ code = code[:15000] + "\n# ... (truncated)"
291
+
292
+ prompt = PROCESS_FLOW_PROMPT.format(
293
+ file_path=ep["path"],
294
+ entry_type=ep.get("entry_type", "http_handler"),
295
+ language=ep.get("language", "python"),
296
+ code=code,
297
+ dependency_context=ep.get("dependencies", "No additional context available."),
298
+ )
299
+
300
+ try:
301
+ flow_data = ai_client.complete_with_tool(prompt, _FLOW_TOOL, max_tokens=2048)
302
+
303
+ if flow_data:
304
+ flow = ProcessFlow(
305
+ id=f"flow:{ep['path']}",
306
+ name=flow_data.get("flow_name", f"Flow: {ep['path']}"),
307
+ description=flow_data.get("description", ""),
308
+ trigger=flow_data.get("trigger", ""),
309
+ entry_point=ep["path"],
310
+ steps=[
311
+ FlowStep(
312
+ order=s.get("order", i),
313
+ actor=s.get("actor", "Unknown"),
314
+ action=s.get("action", ""),
315
+ target=s.get("target", ""),
316
+ data=s.get("data") or None,
317
+ is_async=s.get("is_async", False),
318
+ condition=s.get("condition") or None,
319
+ )
320
+ for i, s in enumerate(flow_data.get("steps", []))
321
+ ],
322
+ )
323
+ flows.append(flow)
324
+ logger.info("Inferred flow: %s (%d steps)", flow.name, len(flow.steps))
325
+ except Exception as exc:
326
+ logger.warning("AI flow inference failed for %s: %s", ep["path"], exc)
327
+
328
+ return flows
329
+
330
+
331
+ def generate_module_summaries(
332
+ snapshot: ArchSnapshot,
333
+ ai_client: AIClient,
334
+ max_workers: int = 5,
335
+ ) -> dict[str, dict[str, str]]:
336
+ """
337
+ Generate AI-powered summaries for each module, running up to *max_workers*
338
+ completions in parallel via ThreadPoolExecutor.
339
+
340
+ Returns dict: module_id -> {"summary": ..., "responsibility": ..., "suggested_capability": ...}
341
+ """
342
+ summaries: dict[str, dict[str, str]] = {}
343
+ module_nodes = snapshot.get_module_nodes()
344
+
345
+ def _summarize_one(node) -> tuple[str, dict[str, str] | None]:
346
+ children = snapshot.get_children(node.id)
347
+ symbols = ", ".join(c.name for c in children[:20])
348
+ deps = [e.target.replace("module:", "") for e in snapshot.edges if e.source == node.id]
349
+
350
+ prompt = MODULE_SUMMARY_PROMPT.format(
351
+ module_name=node.name,
352
+ file_list=node.file_path or "N/A",
353
+ symbols=symbols or "N/A",
354
+ dependencies=", ".join(deps) if deps else "None",
355
+ )
356
+
357
+ try:
358
+ result = ai_client.complete_with_tool(prompt, _SUMMARY_TOOL, max_tokens=500)
359
+ return node.id, result
360
+ except Exception as exc:
361
+ logger.warning("AI summary failed for %s: %s", node.name, exc)
362
+ return node.id, None
363
+
364
+ with ThreadPoolExecutor(max_workers=max_workers) as pool:
365
+ futures = {pool.submit(_summarize_one, node): node for node in module_nodes}
366
+ for future in as_completed(futures):
367
+ try:
368
+ node_id, result = future.result()
369
+ if result:
370
+ summaries[node_id] = result
371
+ # Write ai_summary back onto the node in-place
372
+ for node in module_nodes:
373
+ if node.id == node_id:
374
+ node.ai_summary = result.get("summary", "")
375
+ break
376
+ except Exception as exc:
377
+ logger.warning("Summary future failed: %s", exc)
378
+
379
+ return summaries
380
+
381
+
382
+ def detect_patterns(
383
+ snapshot: ArchSnapshot,
384
+ directory_tree: str,
385
+ ai_client: AIClient,
386
+ ) -> list[ArchPattern]:
387
+ """Use AI to detect architectural patterns in the codebase."""
388
+ module_graph_lines: list[str] = []
389
+ for node in snapshot.get_module_nodes():
390
+ deps = [e.target.replace("module:", "") for e in snapshot.edges if e.source == node.id]
391
+ dep_str = " -> " + ", ".join(deps) if deps else " (no dependencies)"
392
+ module_graph_lines.append(f" {node.name}{dep_str}")
393
+
394
+ prompt = PATTERN_DETECTION_PROMPT.format(
395
+ project_name=snapshot.project_name,
396
+ module_graph="\n".join(module_graph_lines),
397
+ directory_structure=directory_tree[:3000],
398
+ )
399
+
400
+ try:
401
+ result = ai_client.complete_with_tool(prompt, _PATTERN_TOOL, max_tokens=1000)
402
+ if result and "patterns" in result:
403
+ valid_values = {e.value for e in ArchPattern}
404
+ return [ArchPattern(p) for p in result["patterns"] if p in valid_values]
405
+ except Exception as exc:
406
+ logger.warning("AI pattern detection failed: %s", exc)
407
+
408
+ return []
409
+
410
+
411
+ # ---------------------------------------------------------------------------
412
+ # Helpers
413
+ # ---------------------------------------------------------------------------
414
+
415
+
416
+ def _parse_json_response(text: str) -> dict[str, Any] | None:
417
+ """
418
+ Parse JSON from an LLM response for non-Anthropic providers that don't
419
+ support tool use. Handles markdown code fences and partial wrapping.
420
+ """
421
+ text = text.strip()
422
+ if text.startswith("```"):
423
+ lines = text.split("\n")
424
+ text = "\n".join(lines[1:])
425
+ if text.endswith("```"):
426
+ text = text[:-3]
427
+ text = text.strip()
428
+
429
+ try:
430
+ return json.loads(text)
431
+ except json.JSONDecodeError:
432
+ start = text.find("{")
433
+ end = text.rfind("}") + 1
434
+ if start >= 0 and end > start:
435
+ try:
436
+ return json.loads(text[start:end])
437
+ except json.JSONDecodeError:
438
+ pass
439
+
440
+ logger.warning("Failed to parse JSON from AI response")
441
+ return None
@@ -0,0 +1,6 @@
1
+ """
2
+ Code analyzers for extracting architecture information from source code.
3
+
4
+ Uses Tree-sitter for multi-language AST parsing and NetworkX for
5
+ building the dependency graph.
6
+ """