maris 0.1.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.
maris/__init__.py ADDED
@@ -0,0 +1,20 @@
1
+ """
2
+ MARIS - Multi-Agent Repository Intelligence System
3
+
4
+ A local-first repository intelligence platform for understanding,
5
+ navigating, and analyzing source code.
6
+ """
7
+
8
+ __version__ = "0.1.0"
9
+
10
+ from maris.core.models import Symbol, SymbolType, RetrievalContext, Commit
11
+
12
+ __all__ = [
13
+ "__version__",
14
+ "Symbol",
15
+ "SymbolType",
16
+ "RetrievalContext",
17
+ "Commit",
18
+ ]
19
+
20
+ # Made with Bob
@@ -0,0 +1,17 @@
1
+ """Specialized agents for repository intelligence."""
2
+
3
+ from maris.agents.documentation_agent import DocumentationAgent
4
+ from maris.agents.git_agent import GitAgent
5
+ from maris.agents.impact_analysis_agent import ImpactAnalysisAgent
6
+ from maris.agents.indexing_agent import IndexingAgent
7
+ from maris.agents.qa_agent import QAAgent
8
+
9
+ __all__ = [
10
+ "IndexingAgent",
11
+ "DocumentationAgent",
12
+ "QAAgent",
13
+ "GitAgent",
14
+ "ImpactAnalysisAgent",
15
+ ]
16
+
17
+ # Made with Bob
@@ -0,0 +1,545 @@
1
+ """Documentation Agent - LangGraph-based implementation for generating repository documentation."""
2
+
3
+ import logging
4
+ from dataclasses import dataclass
5
+ from typing import Any, Dict, List, Optional, Set
6
+
7
+ from langgraph.graph import StateGraph, END
8
+
9
+ from maris.core.models import Symbol, SymbolType
10
+ from maris.knowledge.service import RepositoryKnowledgeService
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ @dataclass
16
+ class ModuleDocumentation:
17
+ """Documentation for a single module/file."""
18
+
19
+ file_path: str
20
+ language: str
21
+ summary: str
22
+ classes: List[Dict[str, str]]
23
+ functions: List[Dict[str, str]]
24
+ constants: List[Dict[str, str]]
25
+ dependencies: List[str]
26
+
27
+
28
+ @dataclass
29
+ class ArchitectureOverview:
30
+ """High-level architecture documentation."""
31
+
32
+ total_files: int
33
+ total_symbols: int
34
+ languages: List[str]
35
+ key_modules: List[str]
36
+ dependency_graph_summary: str
37
+
38
+
39
+ class DocumentationAgent:
40
+ """
41
+ LangGraph-based Agent for generating repository documentation.
42
+
43
+ Uses a workflow with explicit state management:
44
+ 1. retrieve_symbols: Get symbols from the file
45
+ 2. categorize_symbols: Organize symbols by type
46
+ 3. find_dependencies: Find file dependencies
47
+ 4. generate_summary: Create file summary
48
+ 5. format_output: Format as ModuleDocumentation or Markdown
49
+
50
+ Capabilities:
51
+ - Generate module-level documentation
52
+ - Create architecture overviews
53
+ - Document component relationships
54
+ - Generate dependency diagrams (text-based)
55
+ """
56
+
57
+ def __init__(self, knowledge_service: RepositoryKnowledgeService):
58
+ """
59
+ Initialize the documentation agent.
60
+
61
+ Args:
62
+ knowledge_service: Repository knowledge service for data access
63
+ """
64
+ self.knowledge_service = knowledge_service
65
+
66
+ # Build the LangGraph workflow
67
+ self.graph = self._build_graph()
68
+
69
+ logger.info("Initialized DocumentationAgent with LangGraph")
70
+
71
+ def _build_graph(self) -> Any:
72
+ """Build the LangGraph workflow for documentation generation."""
73
+
74
+ # Define state schema
75
+ class State(Dict[str, Any]):
76
+ pass
77
+
78
+ workflow = StateGraph(State)
79
+
80
+ # Add nodes
81
+ workflow.add_node("retrieve_symbols", self._retrieve_symbols)
82
+ workflow.add_node("categorize_symbols", self._categorize_symbols)
83
+ workflow.add_node("find_dependencies", self._find_dependencies)
84
+ workflow.add_node("generate_summary", self._generate_summary)
85
+ workflow.add_node("format_output", self._format_output)
86
+
87
+ # Define edges
88
+ workflow.set_entry_point("retrieve_symbols")
89
+ workflow.add_edge("retrieve_symbols", "categorize_symbols")
90
+ workflow.add_edge("categorize_symbols", "find_dependencies")
91
+ workflow.add_edge("find_dependencies", "generate_summary")
92
+ workflow.add_edge("generate_summary", "format_output")
93
+ workflow.add_edge("format_output", END)
94
+
95
+ return workflow.compile()
96
+
97
+ def _retrieve_symbols(self, state: Dict[str, Any]) -> Dict[str, Any]:
98
+ """
99
+ Node: Retrieve symbols from the file.
100
+
101
+ Args:
102
+ state: Current workflow state
103
+
104
+ Returns:
105
+ Updated state with symbols
106
+ """
107
+ try:
108
+ file_path = state.get("file_path", "")
109
+ if not file_path:
110
+ raise ValueError("file_path is required")
111
+ logger.info(f"Retrieving symbols for: {file_path}")
112
+
113
+ symbols = self.knowledge_service.find_symbols_in_file(file_path)
114
+
115
+ state["symbols"] = symbols
116
+ state["language"] = symbols[0].language if symbols else "unknown"
117
+
118
+ logger.info(f"Retrieved {len(symbols)} symbols")
119
+
120
+ except Exception as e:
121
+ logger.error(f"Error retrieving symbols: {e}")
122
+ state["error"] = f"Failed to retrieve symbols: {str(e)}"
123
+ state["symbols"] = []
124
+ state["language"] = "unknown"
125
+
126
+ return state
127
+
128
+ def _categorize_symbols(self, state: Dict[str, Any]) -> Dict[str, Any]:
129
+ """
130
+ Node: Categorize symbols by type.
131
+
132
+ Args:
133
+ state: Current workflow state
134
+
135
+ Returns:
136
+ Updated state with categorized symbols
137
+ """
138
+ if state.get("error"):
139
+ return state
140
+
141
+ try:
142
+ logger.info("Categorizing symbols")
143
+
144
+ symbols = state.get("symbols", [])
145
+ classes = []
146
+ functions = []
147
+ constants = []
148
+
149
+ for symbol in symbols:
150
+ doc_entry = {
151
+ "name": symbol.name,
152
+ "signature": symbol.signature or "",
153
+ "docstring": symbol.docstring or "No documentation available.",
154
+ "line": symbol.start_line,
155
+ }
156
+
157
+ if symbol.type == SymbolType.CLASS:
158
+ # Find methods of this class
159
+ methods = [s for s in symbols if s.parent_id == symbol.id]
160
+ doc_entry["methods"] = [m.name for m in methods]
161
+ classes.append(doc_entry)
162
+ elif symbol.type == SymbolType.FUNCTION and not symbol.parent_id:
163
+ functions.append(doc_entry)
164
+ elif symbol.type == SymbolType.CONSTANT:
165
+ constants.append(doc_entry)
166
+
167
+ state["classes"] = classes
168
+ state["functions"] = functions
169
+ state["constants"] = constants
170
+
171
+ logger.info(
172
+ f"Categorized: {len(classes)} classes, {len(functions)} functions, {len(constants)} constants"
173
+ )
174
+
175
+ except Exception as e:
176
+ logger.error(f"Error categorizing symbols: {e}")
177
+ state["error"] = f"Failed to categorize symbols: {str(e)}"
178
+ state["classes"] = []
179
+ state["functions"] = []
180
+ state["constants"] = []
181
+
182
+ return state
183
+
184
+ def _find_dependencies(self, state: Dict[str, Any]) -> Dict[str, Any]:
185
+ """
186
+ Node: Find file dependencies.
187
+
188
+ Args:
189
+ state: Current workflow state
190
+
191
+ Returns:
192
+ Updated state with dependencies
193
+ """
194
+ if state.get("error"):
195
+ return state
196
+
197
+ try:
198
+ logger.info("Finding dependencies")
199
+
200
+ symbols = state.get("symbols", [])
201
+ dependencies = set()
202
+
203
+ for symbol in symbols:
204
+ # Find callees (symbols this symbol calls)
205
+ callees = self.knowledge_service.find_callees(symbol)
206
+
207
+ for callee in callees:
208
+ if callee.file_path != symbol.file_path:
209
+ dependencies.add(callee.file_path)
210
+
211
+ state["dependencies"] = sorted(list(dependencies))
212
+
213
+ logger.info(f"Found {len(dependencies)} dependencies")
214
+
215
+ except Exception as e:
216
+ logger.error(f"Error finding dependencies: {e}")
217
+ # Don't fail the workflow for dependency errors
218
+ state["dependencies"] = []
219
+ state["dependency_error"] = str(e)
220
+
221
+ return state
222
+
223
+ def _generate_summary(self, state: Dict[str, Any]) -> Dict[str, Any]:
224
+ """
225
+ Node: Generate file summary.
226
+
227
+ Args:
228
+ state: Current workflow state
229
+
230
+ Returns:
231
+ Updated state with summary
232
+ """
233
+ if state.get("error"):
234
+ return state
235
+
236
+ try:
237
+ logger.info("Generating summary")
238
+
239
+ classes = state.get("classes", [])
240
+ functions = state.get("functions", [])
241
+ constants = state.get("constants", [])
242
+
243
+ parts = []
244
+ if classes:
245
+ parts.append(f"{len(classes)} class{'es' if len(classes) != 1 else ''}")
246
+ if functions:
247
+ parts.append(f"{len(functions)} function{'s' if len(functions) != 1 else ''}")
248
+ if constants:
249
+ parts.append(f"{len(constants)} constant{'s' if len(constants) != 1 else ''}")
250
+
251
+ if not parts:
252
+ summary = "This file contains no documented symbols."
253
+ else:
254
+ summary = f"This module defines {', '.join(parts)}."
255
+
256
+ state["summary"] = summary
257
+
258
+ logger.info("Summary generated")
259
+
260
+ except Exception as e:
261
+ logger.error(f"Error generating summary: {e}")
262
+ state["summary"] = "Error generating summary."
263
+
264
+ return state
265
+
266
+ def _format_output(self, state: Dict[str, Any]) -> Dict[str, Any]:
267
+ """
268
+ Node: Format output as ModuleDocumentation or Markdown.
269
+
270
+ Args:
271
+ state: Current workflow state
272
+
273
+ Returns:
274
+ Updated state with formatted output
275
+ """
276
+ try:
277
+ logger.info("Formatting output")
278
+
279
+ file_path = state.get("file_path", "unknown")
280
+ language = state.get("language", "unknown")
281
+ summary = state.get("summary", "No summary available.")
282
+ classes = state.get("classes", [])
283
+ functions = state.get("functions", [])
284
+ constants = state.get("constants", [])
285
+ dependencies = state.get("dependencies", [])
286
+
287
+ # Create ModuleDocumentation object
288
+ doc = ModuleDocumentation(
289
+ file_path=file_path,
290
+ language=language,
291
+ summary=summary,
292
+ classes=classes,
293
+ functions=functions,
294
+ constants=constants,
295
+ dependencies=dependencies,
296
+ )
297
+
298
+ state["documentation"] = doc
299
+
300
+ # Generate markdown if requested
301
+ if state.get("format") == "markdown":
302
+ state["markdown"] = self._generate_markdown(doc)
303
+
304
+ logger.info("Output formatted")
305
+
306
+ except Exception as e:
307
+ logger.error(f"Error formatting output: {e}")
308
+ state["error"] = f"Failed to format output: {str(e)}"
309
+
310
+ return state
311
+
312
+ def _generate_markdown(self, doc: ModuleDocumentation) -> str:
313
+ """
314
+ Generate Markdown documentation from ModuleDocumentation.
315
+
316
+ Args:
317
+ doc: Module documentation object
318
+
319
+ Returns:
320
+ Markdown-formatted documentation
321
+ """
322
+ lines = [
323
+ f"# {doc.file_path}",
324
+ "",
325
+ f"**Language:** {doc.language}",
326
+ "",
327
+ "## Summary",
328
+ "",
329
+ doc.summary,
330
+ "",
331
+ ]
332
+
333
+ # Document classes
334
+ if doc.classes:
335
+ lines.extend(["## Classes", ""])
336
+ for cls in doc.classes:
337
+ lines.append(f"### `{cls['name']}`")
338
+ lines.append("")
339
+ if cls.get("signature"):
340
+ lines.append(f"```{doc.language}")
341
+ lines.append(cls["signature"])
342
+ lines.append("```")
343
+ lines.append("")
344
+ lines.append(cls["docstring"])
345
+ lines.append("")
346
+ if cls.get("methods"):
347
+ lines.append("**Methods:**")
348
+ for method in cls["methods"]:
349
+ lines.append(f"- `{method}()`")
350
+ lines.append("")
351
+
352
+ # Document functions
353
+ if doc.functions:
354
+ lines.extend(["## Functions", ""])
355
+ for func in doc.functions:
356
+ lines.append(f"### `{func['name']}()`")
357
+ lines.append("")
358
+ if func.get("signature"):
359
+ lines.append(f"```{doc.language}")
360
+ lines.append(func["signature"])
361
+ lines.append("```")
362
+ lines.append("")
363
+ lines.append(func["docstring"])
364
+ lines.append("")
365
+
366
+ # Document constants
367
+ if doc.constants:
368
+ lines.extend(["## Constants", ""])
369
+ for const in doc.constants:
370
+ lines.append(f"- **`{const['name']}`**: {const['docstring']}")
371
+ lines.append("")
372
+
373
+ # Document dependencies
374
+ if doc.dependencies:
375
+ lines.extend(
376
+ [
377
+ "## Dependencies",
378
+ "",
379
+ "This module depends on:",
380
+ "",
381
+ ]
382
+ )
383
+ for dep in doc.dependencies:
384
+ lines.append(f"- `{dep}`")
385
+ lines.append("")
386
+
387
+ return "\n".join(lines)
388
+
389
+ def generate_module_documentation(self, file_path: str) -> ModuleDocumentation:
390
+ """
391
+ Generate documentation for a specific module/file.
392
+
393
+ Args:
394
+ file_path: Path to the file to document
395
+
396
+ Returns:
397
+ Structured module documentation
398
+ """
399
+ logger.info(f"Generating documentation for: {file_path}")
400
+
401
+ # Initialize state
402
+ initial_state: Dict[str, Any] = {
403
+ "file_path": file_path,
404
+ "format": "object",
405
+ "symbols": [],
406
+ "language": "unknown",
407
+ "classes": [],
408
+ "functions": [],
409
+ "constants": [],
410
+ "dependencies": [],
411
+ "summary": "",
412
+ "documentation": None,
413
+ "error": None,
414
+ }
415
+
416
+ # Run the workflow
417
+ final_state = self.graph.invoke(initial_state)
418
+
419
+ # Handle None return from graph
420
+ if final_state is None:
421
+ final_state = initial_state
422
+
423
+ # Return documentation or create empty one
424
+ if final_state.get("documentation"):
425
+ return final_state["documentation"]
426
+ else:
427
+ return ModuleDocumentation(
428
+ file_path=file_path,
429
+ language="unknown",
430
+ summary="No symbols found in this file.",
431
+ classes=[],
432
+ functions=[],
433
+ constants=[],
434
+ dependencies=[],
435
+ )
436
+
437
+ def generate_architecture_overview(self) -> ArchitectureOverview:
438
+ """
439
+ Generate high-level architecture documentation.
440
+
441
+ Returns:
442
+ Architecture overview with statistics and key insights
443
+ """
444
+ logger.info("Generating architecture overview")
445
+
446
+ # Get repository statistics
447
+ stats = self.knowledge_service.get_repository_stats()
448
+
449
+ total_files = stats.get("total_files", 0)
450
+ total_symbols = stats.get("total_symbols", 0)
451
+ languages = stats.get("languages", [])
452
+
453
+ # Identify key modules (files with most symbols)
454
+ # This is a simplified version - in production, we'd query the metadata store
455
+ key_modules = []
456
+
457
+ # Generate dependency graph summary
458
+ dep_summary = f"Repository contains {total_symbols} symbols across {total_files} files."
459
+
460
+ return ArchitectureOverview(
461
+ total_files=total_files,
462
+ total_symbols=total_symbols,
463
+ languages=languages,
464
+ key_modules=key_modules,
465
+ dependency_graph_summary=dep_summary,
466
+ )
467
+
468
+ def generate_markdown_documentation(self, file_path: str) -> str:
469
+ """
470
+ Generate Markdown documentation for a file.
471
+
472
+ Args:
473
+ file_path: Path to the file to document
474
+
475
+ Returns:
476
+ Markdown-formatted documentation
477
+ """
478
+ logger.info(f"Generating markdown documentation for: {file_path}")
479
+
480
+ # Initialize state
481
+ initial_state: Dict[str, Any] = {
482
+ "file_path": file_path,
483
+ "format": "markdown",
484
+ "symbols": [],
485
+ "language": "unknown",
486
+ "classes": [],
487
+ "functions": [],
488
+ "constants": [],
489
+ "dependencies": [],
490
+ "summary": "",
491
+ "documentation": None,
492
+ "markdown": None,
493
+ "error": None,
494
+ }
495
+
496
+ # Run the workflow
497
+ final_state = self.graph.invoke(initial_state)
498
+
499
+ # Handle None return from graph
500
+ if final_state is None:
501
+ final_state = initial_state
502
+
503
+ # Return markdown or generate from documentation
504
+ if final_state.get("markdown"):
505
+ return final_state["markdown"]
506
+ elif final_state.get("documentation"):
507
+ return self._generate_markdown(final_state["documentation"])
508
+ else:
509
+ # Return minimal markdown
510
+ return f"# {file_path}\n\nNo symbols found in this file.\n"
511
+
512
+ def generate_architecture_markdown(self) -> str:
513
+ """
514
+ Generate Markdown documentation for repository architecture.
515
+
516
+ Returns:
517
+ Markdown-formatted architecture overview
518
+ """
519
+ overview = self.generate_architecture_overview()
520
+
521
+ lines = [
522
+ "# Repository Architecture",
523
+ "",
524
+ "## Overview",
525
+ "",
526
+ f"- **Total Files:** {overview.total_files}",
527
+ f"- **Total Symbols:** {overview.total_symbols}",
528
+ f"- **Languages:** {', '.join(overview.languages)}",
529
+ "",
530
+ "## Dependency Graph",
531
+ "",
532
+ overview.dependency_graph_summary,
533
+ "",
534
+ ]
535
+
536
+ if overview.key_modules:
537
+ lines.extend(["## Key Modules", ""])
538
+ for module in overview.key_modules:
539
+ lines.append(f"- `{module}`")
540
+ lines.append("")
541
+
542
+ return "\n".join(lines)
543
+
544
+
545
+ # Made with Bob