emdash-core 0.1.7__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 (187) hide show
  1. emdash_core/__init__.py +3 -0
  2. emdash_core/agent/__init__.py +37 -0
  3. emdash_core/agent/agents.py +225 -0
  4. emdash_core/agent/code_reviewer.py +476 -0
  5. emdash_core/agent/compaction.py +143 -0
  6. emdash_core/agent/context_manager.py +140 -0
  7. emdash_core/agent/events.py +338 -0
  8. emdash_core/agent/handlers.py +224 -0
  9. emdash_core/agent/inprocess_subagent.py +377 -0
  10. emdash_core/agent/mcp/__init__.py +50 -0
  11. emdash_core/agent/mcp/client.py +346 -0
  12. emdash_core/agent/mcp/config.py +302 -0
  13. emdash_core/agent/mcp/manager.py +496 -0
  14. emdash_core/agent/mcp/tool_factory.py +213 -0
  15. emdash_core/agent/prompts/__init__.py +38 -0
  16. emdash_core/agent/prompts/main_agent.py +104 -0
  17. emdash_core/agent/prompts/subagents.py +131 -0
  18. emdash_core/agent/prompts/workflow.py +136 -0
  19. emdash_core/agent/providers/__init__.py +34 -0
  20. emdash_core/agent/providers/base.py +143 -0
  21. emdash_core/agent/providers/factory.py +80 -0
  22. emdash_core/agent/providers/models.py +220 -0
  23. emdash_core/agent/providers/openai_provider.py +463 -0
  24. emdash_core/agent/providers/transformers_provider.py +217 -0
  25. emdash_core/agent/research/__init__.py +81 -0
  26. emdash_core/agent/research/agent.py +143 -0
  27. emdash_core/agent/research/controller.py +254 -0
  28. emdash_core/agent/research/critic.py +428 -0
  29. emdash_core/agent/research/macros.py +469 -0
  30. emdash_core/agent/research/planner.py +449 -0
  31. emdash_core/agent/research/researcher.py +436 -0
  32. emdash_core/agent/research/state.py +523 -0
  33. emdash_core/agent/research/synthesizer.py +594 -0
  34. emdash_core/agent/reviewer_profile.py +475 -0
  35. emdash_core/agent/rules.py +123 -0
  36. emdash_core/agent/runner.py +601 -0
  37. emdash_core/agent/session.py +262 -0
  38. emdash_core/agent/spec_schema.py +66 -0
  39. emdash_core/agent/specification.py +479 -0
  40. emdash_core/agent/subagent.py +397 -0
  41. emdash_core/agent/subagent_prompts.py +13 -0
  42. emdash_core/agent/toolkit.py +482 -0
  43. emdash_core/agent/toolkits/__init__.py +64 -0
  44. emdash_core/agent/toolkits/base.py +96 -0
  45. emdash_core/agent/toolkits/explore.py +47 -0
  46. emdash_core/agent/toolkits/plan.py +55 -0
  47. emdash_core/agent/tools/__init__.py +141 -0
  48. emdash_core/agent/tools/analytics.py +436 -0
  49. emdash_core/agent/tools/base.py +131 -0
  50. emdash_core/agent/tools/coding.py +484 -0
  51. emdash_core/agent/tools/github_mcp.py +592 -0
  52. emdash_core/agent/tools/history.py +13 -0
  53. emdash_core/agent/tools/modes.py +153 -0
  54. emdash_core/agent/tools/plan.py +206 -0
  55. emdash_core/agent/tools/plan_write.py +135 -0
  56. emdash_core/agent/tools/search.py +412 -0
  57. emdash_core/agent/tools/spec.py +341 -0
  58. emdash_core/agent/tools/task.py +262 -0
  59. emdash_core/agent/tools/task_output.py +204 -0
  60. emdash_core/agent/tools/tasks.py +454 -0
  61. emdash_core/agent/tools/traversal.py +588 -0
  62. emdash_core/agent/tools/web.py +179 -0
  63. emdash_core/analytics/__init__.py +5 -0
  64. emdash_core/analytics/engine.py +1286 -0
  65. emdash_core/api/__init__.py +5 -0
  66. emdash_core/api/agent.py +308 -0
  67. emdash_core/api/agents.py +154 -0
  68. emdash_core/api/analyze.py +264 -0
  69. emdash_core/api/auth.py +173 -0
  70. emdash_core/api/context.py +77 -0
  71. emdash_core/api/db.py +121 -0
  72. emdash_core/api/embed.py +131 -0
  73. emdash_core/api/feature.py +143 -0
  74. emdash_core/api/health.py +93 -0
  75. emdash_core/api/index.py +162 -0
  76. emdash_core/api/plan.py +110 -0
  77. emdash_core/api/projectmd.py +210 -0
  78. emdash_core/api/query.py +320 -0
  79. emdash_core/api/research.py +122 -0
  80. emdash_core/api/review.py +161 -0
  81. emdash_core/api/router.py +76 -0
  82. emdash_core/api/rules.py +116 -0
  83. emdash_core/api/search.py +119 -0
  84. emdash_core/api/spec.py +99 -0
  85. emdash_core/api/swarm.py +223 -0
  86. emdash_core/api/tasks.py +109 -0
  87. emdash_core/api/team.py +120 -0
  88. emdash_core/auth/__init__.py +17 -0
  89. emdash_core/auth/github.py +389 -0
  90. emdash_core/config.py +74 -0
  91. emdash_core/context/__init__.py +52 -0
  92. emdash_core/context/models.py +50 -0
  93. emdash_core/context/providers/__init__.py +11 -0
  94. emdash_core/context/providers/base.py +74 -0
  95. emdash_core/context/providers/explored_areas.py +183 -0
  96. emdash_core/context/providers/touched_areas.py +360 -0
  97. emdash_core/context/registry.py +73 -0
  98. emdash_core/context/reranker.py +199 -0
  99. emdash_core/context/service.py +260 -0
  100. emdash_core/context/session.py +352 -0
  101. emdash_core/core/__init__.py +104 -0
  102. emdash_core/core/config.py +454 -0
  103. emdash_core/core/exceptions.py +55 -0
  104. emdash_core/core/models.py +265 -0
  105. emdash_core/core/review_config.py +57 -0
  106. emdash_core/db/__init__.py +67 -0
  107. emdash_core/db/auth.py +134 -0
  108. emdash_core/db/models.py +91 -0
  109. emdash_core/db/provider.py +222 -0
  110. emdash_core/db/providers/__init__.py +5 -0
  111. emdash_core/db/providers/supabase.py +452 -0
  112. emdash_core/embeddings/__init__.py +24 -0
  113. emdash_core/embeddings/indexer.py +534 -0
  114. emdash_core/embeddings/models.py +192 -0
  115. emdash_core/embeddings/providers/__init__.py +7 -0
  116. emdash_core/embeddings/providers/base.py +112 -0
  117. emdash_core/embeddings/providers/fireworks.py +141 -0
  118. emdash_core/embeddings/providers/openai.py +104 -0
  119. emdash_core/embeddings/registry.py +146 -0
  120. emdash_core/embeddings/service.py +215 -0
  121. emdash_core/graph/__init__.py +26 -0
  122. emdash_core/graph/builder.py +134 -0
  123. emdash_core/graph/connection.py +692 -0
  124. emdash_core/graph/schema.py +416 -0
  125. emdash_core/graph/writer.py +667 -0
  126. emdash_core/ingestion/__init__.py +7 -0
  127. emdash_core/ingestion/change_detector.py +150 -0
  128. emdash_core/ingestion/git/__init__.py +5 -0
  129. emdash_core/ingestion/git/commit_analyzer.py +196 -0
  130. emdash_core/ingestion/github/__init__.py +6 -0
  131. emdash_core/ingestion/github/pr_fetcher.py +296 -0
  132. emdash_core/ingestion/github/task_extractor.py +100 -0
  133. emdash_core/ingestion/orchestrator.py +540 -0
  134. emdash_core/ingestion/parsers/__init__.py +10 -0
  135. emdash_core/ingestion/parsers/base_parser.py +66 -0
  136. emdash_core/ingestion/parsers/call_graph_builder.py +121 -0
  137. emdash_core/ingestion/parsers/class_extractor.py +154 -0
  138. emdash_core/ingestion/parsers/function_extractor.py +202 -0
  139. emdash_core/ingestion/parsers/import_analyzer.py +119 -0
  140. emdash_core/ingestion/parsers/python_parser.py +123 -0
  141. emdash_core/ingestion/parsers/registry.py +72 -0
  142. emdash_core/ingestion/parsers/ts_ast_parser.js +313 -0
  143. emdash_core/ingestion/parsers/typescript_parser.py +278 -0
  144. emdash_core/ingestion/repository.py +346 -0
  145. emdash_core/models/__init__.py +38 -0
  146. emdash_core/models/agent.py +68 -0
  147. emdash_core/models/index.py +77 -0
  148. emdash_core/models/query.py +113 -0
  149. emdash_core/planning/__init__.py +7 -0
  150. emdash_core/planning/agent_api.py +413 -0
  151. emdash_core/planning/context_builder.py +265 -0
  152. emdash_core/planning/feature_context.py +232 -0
  153. emdash_core/planning/feature_expander.py +646 -0
  154. emdash_core/planning/llm_explainer.py +198 -0
  155. emdash_core/planning/similarity.py +509 -0
  156. emdash_core/planning/team_focus.py +821 -0
  157. emdash_core/server.py +153 -0
  158. emdash_core/sse/__init__.py +5 -0
  159. emdash_core/sse/stream.py +196 -0
  160. emdash_core/swarm/__init__.py +17 -0
  161. emdash_core/swarm/merge_agent.py +383 -0
  162. emdash_core/swarm/session_manager.py +274 -0
  163. emdash_core/swarm/swarm_runner.py +226 -0
  164. emdash_core/swarm/task_definition.py +137 -0
  165. emdash_core/swarm/worker_spawner.py +319 -0
  166. emdash_core/swarm/worktree_manager.py +278 -0
  167. emdash_core/templates/__init__.py +10 -0
  168. emdash_core/templates/defaults/agent-builder.md.template +82 -0
  169. emdash_core/templates/defaults/focus.md.template +115 -0
  170. emdash_core/templates/defaults/pr-review-enhanced.md.template +309 -0
  171. emdash_core/templates/defaults/pr-review.md.template +80 -0
  172. emdash_core/templates/defaults/project.md.template +85 -0
  173. emdash_core/templates/defaults/research_critic.md.template +112 -0
  174. emdash_core/templates/defaults/research_planner.md.template +85 -0
  175. emdash_core/templates/defaults/research_synthesizer.md.template +128 -0
  176. emdash_core/templates/defaults/reviewer.md.template +81 -0
  177. emdash_core/templates/defaults/spec.md.template +41 -0
  178. emdash_core/templates/defaults/tasks.md.template +78 -0
  179. emdash_core/templates/loader.py +296 -0
  180. emdash_core/utils/__init__.py +45 -0
  181. emdash_core/utils/git.py +84 -0
  182. emdash_core/utils/image.py +502 -0
  183. emdash_core/utils/logger.py +51 -0
  184. emdash_core-0.1.7.dist-info/METADATA +35 -0
  185. emdash_core-0.1.7.dist-info/RECORD +187 -0
  186. emdash_core-0.1.7.dist-info/WHEEL +4 -0
  187. emdash_core-0.1.7.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,55 @@
1
+ """Plan toolkit - exploration tools plus plan writing capability."""
2
+
3
+ from pathlib import Path
4
+
5
+ from .base import BaseToolkit
6
+ from ..tools.coding import ReadFileTool, ListFilesTool
7
+ from ..tools.search import SemanticSearchTool, GrepTool, GlobTool
8
+ from ..tools.plan_write import WritePlanTool
9
+ from ...utils.logger import log
10
+
11
+
12
+ class PlanToolkit(BaseToolkit):
13
+ """Toolkit for planning with limited write access (plan files only).
14
+
15
+ Provides all read-only exploration tools plus the ability to write
16
+ implementation plans to .emdash/plans/*.md.
17
+
18
+ Tools available:
19
+ - read_file: Read file contents
20
+ - list_files: List directory contents
21
+ - glob: Find files by pattern
22
+ - grep: Search file contents
23
+ - semantic_search: AI-powered code search
24
+ - write_plan: Write implementation plans (restricted to .emdash/plans/)
25
+ """
26
+
27
+ TOOLS = [
28
+ "read_file",
29
+ "list_files",
30
+ "glob",
31
+ "grep",
32
+ "semantic_search",
33
+ "write_plan",
34
+ ]
35
+
36
+ def _register_tools(self) -> None:
37
+ """Register exploration and plan writing tools."""
38
+ # All read-only exploration tools
39
+ self.register_tool(ReadFileTool(repo_root=self.repo_root))
40
+ self.register_tool(ListFilesTool(repo_root=self.repo_root))
41
+
42
+ # Pattern-based search
43
+ self.register_tool(GlobTool(connection=None))
44
+ self.register_tool(GrepTool(connection=None))
45
+
46
+ # Semantic search (if available)
47
+ try:
48
+ self.register_tool(SemanticSearchTool(connection=None))
49
+ except Exception as e:
50
+ log.debug(f"Semantic search not available: {e}")
51
+
52
+ # Special: can only write to .emdash/plans/*.md
53
+ self.register_tool(WritePlanTool(repo_root=self.repo_root))
54
+
55
+ log.debug(f"PlanToolkit registered {len(self._tools)} tools")
@@ -0,0 +1,141 @@
1
+ """Agent tools for graph exploration and code analysis.
2
+
3
+ This module provides tools that LLM agents can use to explore
4
+ code graphs, search for code, and analyze dependencies.
5
+ """
6
+
7
+ from .base import BaseTool, ToolResult, ToolCategory
8
+
9
+ # Search tools
10
+ from .search import SemanticSearchTool, TextSearchTool, GrepTool
11
+
12
+ # Traversal tools
13
+ from .traversal import (
14
+ ExpandNodeTool,
15
+ GetCallersTool,
16
+ GetCalleesTool,
17
+ GetClassHierarchyTool,
18
+ GetFileDependenciesTool,
19
+ GetImpactAnalysisTool,
20
+ GetNeighborsTool,
21
+ )
22
+
23
+ # Analytics tools
24
+ from .analytics import (
25
+ GetAreaImportanceTool,
26
+ GetTopPageRankTool,
27
+ GetCommunitiesTool,
28
+ GetCommunityMembersTool,
29
+ )
30
+
31
+ # Task tools
32
+ from .tasks import (
33
+ TaskState,
34
+ TaskStatus,
35
+ Task,
36
+ WriteTodoTool,
37
+ UpdateTodoListTool,
38
+ AskFollowupQuestionTool,
39
+ AttemptCompletionTool,
40
+ )
41
+
42
+ # Plan tools
43
+ from .plan import PlanExplorationTool
44
+
45
+ # Mode tools
46
+ from .modes import AgentMode, ModeState, SwitchModeTool, GetModeTool
47
+
48
+ # Spec tools
49
+ from .spec import SubmitSpecTool, GetSpecTool, UpdateSpecTool
50
+
51
+ # Web tools
52
+ from .web import WebTool
53
+
54
+ # Coding tools
55
+ from .coding import (
56
+ CodingTool,
57
+ ReadFileTool,
58
+ WriteToFileTool,
59
+ ApplyDiffTool,
60
+ DeleteFileTool,
61
+ ListFilesTool,
62
+ ExecuteCommandTool,
63
+ )
64
+
65
+ # GitHub MCP tools
66
+ from .github_mcp import (
67
+ MCPBaseTool,
68
+ GitHubSearchCodeTool,
69
+ GitHubGetFileContentTool,
70
+ GitHubPRDetailsTool,
71
+ GitHubListPRsTool,
72
+ GitHubSearchReposTool,
73
+ GitHubSearchPRsTool,
74
+ GitHubGetIssueTool,
75
+ GitHubViewRepoStructureTool,
76
+ GitHubCreateReviewTool,
77
+ )
78
+
79
+ __all__ = [
80
+ # Base
81
+ "BaseTool",
82
+ "ToolResult",
83
+ "ToolCategory",
84
+ # Search
85
+ "SemanticSearchTool",
86
+ "TextSearchTool",
87
+ "GrepTool",
88
+ # Traversal
89
+ "ExpandNodeTool",
90
+ "GetCallersTool",
91
+ "GetCalleesTool",
92
+ "GetClassHierarchyTool",
93
+ "GetFileDependenciesTool",
94
+ "GetImpactAnalysisTool",
95
+ "GetNeighborsTool",
96
+ # Analytics
97
+ "GetAreaImportanceTool",
98
+ "GetTopPageRankTool",
99
+ "GetCommunitiesTool",
100
+ "GetCommunityMembersTool",
101
+ # Tasks
102
+ "TaskState",
103
+ "TaskStatus",
104
+ "Task",
105
+ "WriteTodoTool",
106
+ "UpdateTodoListTool",
107
+ "AskFollowupQuestionTool",
108
+ "AttemptCompletionTool",
109
+ # Plan
110
+ "PlanExplorationTool",
111
+ # Mode
112
+ "AgentMode",
113
+ "ModeState",
114
+ "SwitchModeTool",
115
+ "GetModeTool",
116
+ # Spec
117
+ "SubmitSpecTool",
118
+ "GetSpecTool",
119
+ "UpdateSpecTool",
120
+ # Web
121
+ "WebTool",
122
+ # Coding
123
+ "CodingTool",
124
+ "ReadFileTool",
125
+ "WriteToFileTool",
126
+ "ApplyDiffTool",
127
+ "DeleteFileTool",
128
+ "ListFilesTool",
129
+ "ExecuteCommandTool",
130
+ # GitHub MCP tools
131
+ "MCPBaseTool",
132
+ "GitHubSearchCodeTool",
133
+ "GitHubGetFileContentTool",
134
+ "GitHubPRDetailsTool",
135
+ "GitHubListPRsTool",
136
+ "GitHubSearchReposTool",
137
+ "GitHubSearchPRsTool",
138
+ "GitHubGetIssueTool",
139
+ "GitHubViewRepoStructureTool",
140
+ "GitHubCreateReviewTool",
141
+ ]
@@ -0,0 +1,436 @@
1
+ """Analytics tools for code graph metrics."""
2
+
3
+ from typing import Optional
4
+
5
+ from .base import BaseTool, ToolResult, ToolCategory
6
+ from ...utils.logger import log
7
+
8
+
9
+ class GetAreaImportanceTool(BaseTool):
10
+ """Get importance metrics for code areas."""
11
+
12
+ name = "get_area_importance"
13
+ description = """Get importance metrics for areas of the codebase.
14
+ Shows which directories or modules are most central to the codebase.
15
+ Sort by 'focus' for recent activity or 'importance' for overall historical importance."""
16
+ category = ToolCategory.ANALYTICS
17
+
18
+ def execute(
19
+ self,
20
+ area_type: str = "directory",
21
+ sort: str = "focus",
22
+ depth: int = 2,
23
+ days: int = 30,
24
+ limit: int = 10,
25
+ files: bool = False,
26
+ ) -> ToolResult:
27
+ """Get area importance metrics.
28
+
29
+ Args:
30
+ area_type: Type of area (directory, module)
31
+ sort: Sort by 'focus' (recent activity) or 'importance' (overall)
32
+ depth: Directory depth for grouping (default 2)
33
+ days: Time window for recent activity (default 30)
34
+ limit: Maximum areas to return
35
+ files: If True, return file-level instead of directory-level
36
+
37
+ Returns:
38
+ ToolResult with importance metrics
39
+ """
40
+ try:
41
+ # Aggregate importance by directory
42
+ # Note: Using range() and list comprehension instead of [0..-1] slice
43
+ # because Memgraph doesn't support negative slice indices
44
+ cypher = """
45
+ MATCH (f:File)
46
+ WHERE f.file_path IS NOT NULL AND f.file_path CONTAINS '/'
47
+ WITH split(f.file_path, '/') as parts, f
48
+ WITH [i IN range(0, size(parts)-2) | parts[i]] as dir_parts, f
49
+ WITH reduce(s = '', p IN dir_parts | s + '/' + p) as directory, count(f) as file_count
50
+ WHERE directory <> ''
51
+ RETURN directory, file_count
52
+ ORDER BY file_count DESC
53
+ LIMIT $limit
54
+ """
55
+
56
+ areas = []
57
+ with self.connection.session() as session:
58
+ result = session.run(cypher, limit=limit)
59
+ for record in result:
60
+ areas.append({
61
+ "directory": record["directory"],
62
+ "file_count": record["file_count"],
63
+ })
64
+
65
+ return ToolResult.success_result(
66
+ data={
67
+ "area_type": area_type,
68
+ "areas": areas,
69
+ "count": len(areas),
70
+ },
71
+ )
72
+
73
+ except Exception as e:
74
+ log.exception("Get area importance failed")
75
+ return ToolResult.error_result(f"Failed: {str(e)}")
76
+
77
+ def get_schema(self) -> dict:
78
+ """Get OpenAI function schema."""
79
+ return self._make_schema(
80
+ properties={
81
+ "area_type": {
82
+ "type": "string",
83
+ "enum": ["directory", "module"],
84
+ "description": "Type of area to analyze",
85
+ "default": "directory",
86
+ },
87
+ "sort": {
88
+ "type": "string",
89
+ "enum": ["focus", "importance"],
90
+ "description": "Sort by 'focus' (recent hot spots) or 'importance' (overall activity)",
91
+ "default": "focus",
92
+ },
93
+ "depth": {
94
+ "type": "integer",
95
+ "description": "Directory depth for grouping",
96
+ "default": 2,
97
+ },
98
+ "days": {
99
+ "type": "integer",
100
+ "description": "Time window for recent activity",
101
+ "default": 30,
102
+ },
103
+ "limit": {
104
+ "type": "integer",
105
+ "description": "Maximum areas to return",
106
+ "default": 10,
107
+ },
108
+ "files": {
109
+ "type": "boolean",
110
+ "description": "If true, return file-level instead of directory-level",
111
+ "default": False,
112
+ },
113
+ },
114
+ required=[],
115
+ )
116
+
117
+
118
+ class GetTopPageRankTool(BaseTool):
119
+ """Get entities with highest PageRank centrality."""
120
+
121
+ name = "get_top_pagerank"
122
+ description = """Get the most central/important code entities by PageRank.
123
+ PageRank identifies code that is most connected and depended upon.
124
+ High PageRank entities are often critical infrastructure."""
125
+ category = ToolCategory.ANALYTICS
126
+
127
+ def execute(
128
+ self,
129
+ entity_types: Optional[list[str]] = None,
130
+ limit: int = 10,
131
+ ) -> ToolResult:
132
+ """Get top PageRank entities.
133
+
134
+ Args:
135
+ entity_types: Types to include (Function, Class, File)
136
+ limit: Maximum results
137
+
138
+ Returns:
139
+ ToolResult with PageRank results
140
+ """
141
+ try:
142
+ # Check if pagerank property exists
143
+ check_query = """
144
+ MATCH (n)
145
+ WHERE n.pagerank IS NOT NULL
146
+ RETURN count(n) as count
147
+ LIMIT 1
148
+ """
149
+
150
+ has_pagerank = False
151
+ with self.connection.session() as session:
152
+ result = session.run(check_query)
153
+ record = result.single()
154
+ has_pagerank = record and record["count"] > 0
155
+
156
+ if not has_pagerank:
157
+ # Fall back to degree centrality
158
+ return self._get_by_degree(entity_types, limit)
159
+
160
+ # Get by PageRank
161
+ type_filter = ""
162
+ if entity_types:
163
+ type_filter = "WHERE " + " OR ".join([f"n:{t}" for t in entity_types])
164
+
165
+ cypher = f"""
166
+ MATCH (n)
167
+ {type_filter}
168
+ {'AND' if type_filter else 'WHERE'} n.pagerank IS NOT NULL
169
+ RETURN n.qualified_name as qualified_name,
170
+ n.file_path as file_path,
171
+ labels(n)[0] as node_type,
172
+ n.pagerank as pagerank
173
+ ORDER BY n.pagerank DESC
174
+ LIMIT $limit
175
+ """
176
+
177
+ results = []
178
+ with self.connection.session() as session:
179
+ result = session.run(cypher, limit=limit)
180
+ for record in result:
181
+ results.append({
182
+ "qualified_name": record["qualified_name"],
183
+ "file_path": record["file_path"],
184
+ "node_type": record["node_type"],
185
+ "pagerank": record["pagerank"],
186
+ })
187
+
188
+ return ToolResult.success_result(
189
+ data={
190
+ "results": results,
191
+ "count": len(results),
192
+ "metric": "pagerank",
193
+ },
194
+ )
195
+
196
+ except Exception as e:
197
+ log.exception("Get top PageRank failed")
198
+ return ToolResult.error_result(f"Failed: {str(e)}")
199
+
200
+ def _get_by_degree(
201
+ self,
202
+ entity_types: Optional[list[str]],
203
+ limit: int,
204
+ ) -> ToolResult:
205
+ """Fall back to degree centrality."""
206
+ try:
207
+ type_filter = ""
208
+ if entity_types:
209
+ type_filter = "WHERE " + " OR ".join([f"n:{t}" for t in entity_types])
210
+
211
+ cypher = f"""
212
+ MATCH (n)
213
+ {type_filter}
214
+ WITH n, size((n)--()) as degree
215
+ WHERE degree > 0
216
+ RETURN n.qualified_name as qualified_name,
217
+ n.file_path as file_path,
218
+ labels(n)[0] as node_type,
219
+ degree
220
+ ORDER BY degree DESC
221
+ LIMIT $limit
222
+ """
223
+
224
+ results = []
225
+ with self.connection.session() as session:
226
+ result = session.run(cypher, limit=limit)
227
+ for record in result:
228
+ results.append({
229
+ "qualified_name": record["qualified_name"],
230
+ "file_path": record["file_path"],
231
+ "node_type": record["node_type"],
232
+ "degree": record["degree"],
233
+ })
234
+
235
+ return ToolResult.success_result(
236
+ data={
237
+ "results": results,
238
+ "count": len(results),
239
+ "metric": "degree",
240
+ "note": "PageRank not computed, using degree centrality",
241
+ },
242
+ )
243
+
244
+ except Exception as e:
245
+ return ToolResult.error_result(f"Failed: {str(e)}")
246
+
247
+ def get_schema(self) -> dict:
248
+ """Get OpenAI function schema."""
249
+ return self._make_schema(
250
+ properties={
251
+ "entity_types": {
252
+ "type": "array",
253
+ "items": {"type": "string", "enum": ["Function", "Class", "File"]},
254
+ "description": "Types to include",
255
+ },
256
+ "limit": {
257
+ "type": "integer",
258
+ "description": "Maximum results",
259
+ "default": 10,
260
+ },
261
+ },
262
+ required=[],
263
+ )
264
+
265
+
266
+ class GetCommunitiesTool(BaseTool):
267
+ """Get code communities (clusters) in the graph."""
268
+
269
+ name = "get_communities"
270
+ description = """Get code communities (clusters) detected in the codebase.
271
+ Communities are groups of closely related code entities.
272
+ Useful for understanding code organization and module boundaries."""
273
+ category = ToolCategory.ANALYTICS
274
+
275
+ def execute(
276
+ self,
277
+ limit: int = 10,
278
+ include_members: bool = False,
279
+ ) -> ToolResult:
280
+ """Get code communities.
281
+
282
+ Args:
283
+ limit: Maximum communities to return
284
+ include_members: Whether to include sample members
285
+
286
+ Returns:
287
+ ToolResult with community information
288
+ """
289
+ try:
290
+ # Check if community property exists
291
+ check_query = """
292
+ MATCH (n)
293
+ WHERE n.community IS NOT NULL
294
+ RETURN count(n) as count
295
+ LIMIT 1
296
+ """
297
+
298
+ has_communities = False
299
+ with self.connection.session() as session:
300
+ result = session.run(check_query)
301
+ record = result.single()
302
+ has_communities = record and record["count"] > 0
303
+
304
+ if not has_communities:
305
+ return ToolResult.success_result(
306
+ data={
307
+ "communities": [],
308
+ "count": 0,
309
+ "note": "Community detection not run",
310
+ },
311
+ suggestions=["Run analytics to detect communities"],
312
+ )
313
+
314
+ # Get community counts
315
+ cypher = """
316
+ MATCH (n)
317
+ WHERE n.community IS NOT NULL
318
+ WITH n.community as community, count(n) as size,
319
+ collect(n.qualified_name)[0..5] as sample
320
+ RETURN community, size, sample
321
+ ORDER BY size DESC
322
+ LIMIT $limit
323
+ """
324
+
325
+ communities = []
326
+ with self.connection.session() as session:
327
+ result = session.run(cypher, limit=limit)
328
+ for record in result:
329
+ comm = {
330
+ "community_id": record["community"],
331
+ "size": record["size"],
332
+ }
333
+ if include_members:
334
+ comm["sample_members"] = [m for m in record["sample"] if m]
335
+ communities.append(comm)
336
+
337
+ return ToolResult.success_result(
338
+ data={
339
+ "communities": communities,
340
+ "count": len(communities),
341
+ },
342
+ )
343
+
344
+ except Exception as e:
345
+ log.exception("Get communities failed")
346
+ return ToolResult.error_result(f"Failed: {str(e)}")
347
+
348
+ def get_schema(self) -> dict:
349
+ """Get OpenAI function schema."""
350
+ return self._make_schema(
351
+ properties={
352
+ "limit": {
353
+ "type": "integer",
354
+ "description": "Maximum communities to return",
355
+ "default": 10,
356
+ },
357
+ "include_members": {
358
+ "type": "boolean",
359
+ "description": "Include sample member names",
360
+ "default": False,
361
+ },
362
+ },
363
+ required=[],
364
+ )
365
+
366
+
367
+ class GetCommunityMembersTool(BaseTool):
368
+ """Get members of a specific community."""
369
+
370
+ name = "get_community_members"
371
+ description = """Get all members of a specific code community.
372
+ Useful for understanding what code belongs to a detected cluster."""
373
+ category = ToolCategory.ANALYTICS
374
+
375
+ def execute(
376
+ self,
377
+ community_id: int,
378
+ limit: int = 50,
379
+ ) -> ToolResult:
380
+ """Get community members.
381
+
382
+ Args:
383
+ community_id: Community ID
384
+ limit: Maximum members to return
385
+
386
+ Returns:
387
+ ToolResult with member information
388
+ """
389
+ try:
390
+ cypher = """
391
+ MATCH (n)
392
+ WHERE n.community = $community_id
393
+ RETURN n.qualified_name as qualified_name,
394
+ n.file_path as file_path,
395
+ labels(n)[0] as node_type
396
+ LIMIT $limit
397
+ """
398
+
399
+ members = []
400
+ with self.connection.session() as session:
401
+ result = session.run(cypher, community_id=community_id, limit=limit)
402
+ for record in result:
403
+ members.append({
404
+ "qualified_name": record["qualified_name"],
405
+ "file_path": record["file_path"],
406
+ "node_type": record["node_type"],
407
+ })
408
+
409
+ return ToolResult.success_result(
410
+ data={
411
+ "community_id": community_id,
412
+ "members": members,
413
+ "count": len(members),
414
+ },
415
+ )
416
+
417
+ except Exception as e:
418
+ log.exception("Get community members failed")
419
+ return ToolResult.error_result(f"Failed: {str(e)}")
420
+
421
+ def get_schema(self) -> dict:
422
+ """Get OpenAI function schema."""
423
+ return self._make_schema(
424
+ properties={
425
+ "community_id": {
426
+ "type": "integer",
427
+ "description": "Community ID to get members for",
428
+ },
429
+ "limit": {
430
+ "type": "integer",
431
+ "description": "Maximum members to return",
432
+ "default": 50,
433
+ },
434
+ },
435
+ required=["community_id"],
436
+ )