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.
- emdash_core/__init__.py +3 -0
- emdash_core/agent/__init__.py +37 -0
- emdash_core/agent/agents.py +225 -0
- emdash_core/agent/code_reviewer.py +476 -0
- emdash_core/agent/compaction.py +143 -0
- emdash_core/agent/context_manager.py +140 -0
- emdash_core/agent/events.py +338 -0
- emdash_core/agent/handlers.py +224 -0
- emdash_core/agent/inprocess_subagent.py +377 -0
- emdash_core/agent/mcp/__init__.py +50 -0
- emdash_core/agent/mcp/client.py +346 -0
- emdash_core/agent/mcp/config.py +302 -0
- emdash_core/agent/mcp/manager.py +496 -0
- emdash_core/agent/mcp/tool_factory.py +213 -0
- emdash_core/agent/prompts/__init__.py +38 -0
- emdash_core/agent/prompts/main_agent.py +104 -0
- emdash_core/agent/prompts/subagents.py +131 -0
- emdash_core/agent/prompts/workflow.py +136 -0
- emdash_core/agent/providers/__init__.py +34 -0
- emdash_core/agent/providers/base.py +143 -0
- emdash_core/agent/providers/factory.py +80 -0
- emdash_core/agent/providers/models.py +220 -0
- emdash_core/agent/providers/openai_provider.py +463 -0
- emdash_core/agent/providers/transformers_provider.py +217 -0
- emdash_core/agent/research/__init__.py +81 -0
- emdash_core/agent/research/agent.py +143 -0
- emdash_core/agent/research/controller.py +254 -0
- emdash_core/agent/research/critic.py +428 -0
- emdash_core/agent/research/macros.py +469 -0
- emdash_core/agent/research/planner.py +449 -0
- emdash_core/agent/research/researcher.py +436 -0
- emdash_core/agent/research/state.py +523 -0
- emdash_core/agent/research/synthesizer.py +594 -0
- emdash_core/agent/reviewer_profile.py +475 -0
- emdash_core/agent/rules.py +123 -0
- emdash_core/agent/runner.py +601 -0
- emdash_core/agent/session.py +262 -0
- emdash_core/agent/spec_schema.py +66 -0
- emdash_core/agent/specification.py +479 -0
- emdash_core/agent/subagent.py +397 -0
- emdash_core/agent/subagent_prompts.py +13 -0
- emdash_core/agent/toolkit.py +482 -0
- emdash_core/agent/toolkits/__init__.py +64 -0
- emdash_core/agent/toolkits/base.py +96 -0
- emdash_core/agent/toolkits/explore.py +47 -0
- emdash_core/agent/toolkits/plan.py +55 -0
- emdash_core/agent/tools/__init__.py +141 -0
- emdash_core/agent/tools/analytics.py +436 -0
- emdash_core/agent/tools/base.py +131 -0
- emdash_core/agent/tools/coding.py +484 -0
- emdash_core/agent/tools/github_mcp.py +592 -0
- emdash_core/agent/tools/history.py +13 -0
- emdash_core/agent/tools/modes.py +153 -0
- emdash_core/agent/tools/plan.py +206 -0
- emdash_core/agent/tools/plan_write.py +135 -0
- emdash_core/agent/tools/search.py +412 -0
- emdash_core/agent/tools/spec.py +341 -0
- emdash_core/agent/tools/task.py +262 -0
- emdash_core/agent/tools/task_output.py +204 -0
- emdash_core/agent/tools/tasks.py +454 -0
- emdash_core/agent/tools/traversal.py +588 -0
- emdash_core/agent/tools/web.py +179 -0
- emdash_core/analytics/__init__.py +5 -0
- emdash_core/analytics/engine.py +1286 -0
- emdash_core/api/__init__.py +5 -0
- emdash_core/api/agent.py +308 -0
- emdash_core/api/agents.py +154 -0
- emdash_core/api/analyze.py +264 -0
- emdash_core/api/auth.py +173 -0
- emdash_core/api/context.py +77 -0
- emdash_core/api/db.py +121 -0
- emdash_core/api/embed.py +131 -0
- emdash_core/api/feature.py +143 -0
- emdash_core/api/health.py +93 -0
- emdash_core/api/index.py +162 -0
- emdash_core/api/plan.py +110 -0
- emdash_core/api/projectmd.py +210 -0
- emdash_core/api/query.py +320 -0
- emdash_core/api/research.py +122 -0
- emdash_core/api/review.py +161 -0
- emdash_core/api/router.py +76 -0
- emdash_core/api/rules.py +116 -0
- emdash_core/api/search.py +119 -0
- emdash_core/api/spec.py +99 -0
- emdash_core/api/swarm.py +223 -0
- emdash_core/api/tasks.py +109 -0
- emdash_core/api/team.py +120 -0
- emdash_core/auth/__init__.py +17 -0
- emdash_core/auth/github.py +389 -0
- emdash_core/config.py +74 -0
- emdash_core/context/__init__.py +52 -0
- emdash_core/context/models.py +50 -0
- emdash_core/context/providers/__init__.py +11 -0
- emdash_core/context/providers/base.py +74 -0
- emdash_core/context/providers/explored_areas.py +183 -0
- emdash_core/context/providers/touched_areas.py +360 -0
- emdash_core/context/registry.py +73 -0
- emdash_core/context/reranker.py +199 -0
- emdash_core/context/service.py +260 -0
- emdash_core/context/session.py +352 -0
- emdash_core/core/__init__.py +104 -0
- emdash_core/core/config.py +454 -0
- emdash_core/core/exceptions.py +55 -0
- emdash_core/core/models.py +265 -0
- emdash_core/core/review_config.py +57 -0
- emdash_core/db/__init__.py +67 -0
- emdash_core/db/auth.py +134 -0
- emdash_core/db/models.py +91 -0
- emdash_core/db/provider.py +222 -0
- emdash_core/db/providers/__init__.py +5 -0
- emdash_core/db/providers/supabase.py +452 -0
- emdash_core/embeddings/__init__.py +24 -0
- emdash_core/embeddings/indexer.py +534 -0
- emdash_core/embeddings/models.py +192 -0
- emdash_core/embeddings/providers/__init__.py +7 -0
- emdash_core/embeddings/providers/base.py +112 -0
- emdash_core/embeddings/providers/fireworks.py +141 -0
- emdash_core/embeddings/providers/openai.py +104 -0
- emdash_core/embeddings/registry.py +146 -0
- emdash_core/embeddings/service.py +215 -0
- emdash_core/graph/__init__.py +26 -0
- emdash_core/graph/builder.py +134 -0
- emdash_core/graph/connection.py +692 -0
- emdash_core/graph/schema.py +416 -0
- emdash_core/graph/writer.py +667 -0
- emdash_core/ingestion/__init__.py +7 -0
- emdash_core/ingestion/change_detector.py +150 -0
- emdash_core/ingestion/git/__init__.py +5 -0
- emdash_core/ingestion/git/commit_analyzer.py +196 -0
- emdash_core/ingestion/github/__init__.py +6 -0
- emdash_core/ingestion/github/pr_fetcher.py +296 -0
- emdash_core/ingestion/github/task_extractor.py +100 -0
- emdash_core/ingestion/orchestrator.py +540 -0
- emdash_core/ingestion/parsers/__init__.py +10 -0
- emdash_core/ingestion/parsers/base_parser.py +66 -0
- emdash_core/ingestion/parsers/call_graph_builder.py +121 -0
- emdash_core/ingestion/parsers/class_extractor.py +154 -0
- emdash_core/ingestion/parsers/function_extractor.py +202 -0
- emdash_core/ingestion/parsers/import_analyzer.py +119 -0
- emdash_core/ingestion/parsers/python_parser.py +123 -0
- emdash_core/ingestion/parsers/registry.py +72 -0
- emdash_core/ingestion/parsers/ts_ast_parser.js +313 -0
- emdash_core/ingestion/parsers/typescript_parser.py +278 -0
- emdash_core/ingestion/repository.py +346 -0
- emdash_core/models/__init__.py +38 -0
- emdash_core/models/agent.py +68 -0
- emdash_core/models/index.py +77 -0
- emdash_core/models/query.py +113 -0
- emdash_core/planning/__init__.py +7 -0
- emdash_core/planning/agent_api.py +413 -0
- emdash_core/planning/context_builder.py +265 -0
- emdash_core/planning/feature_context.py +232 -0
- emdash_core/planning/feature_expander.py +646 -0
- emdash_core/planning/llm_explainer.py +198 -0
- emdash_core/planning/similarity.py +509 -0
- emdash_core/planning/team_focus.py +821 -0
- emdash_core/server.py +153 -0
- emdash_core/sse/__init__.py +5 -0
- emdash_core/sse/stream.py +196 -0
- emdash_core/swarm/__init__.py +17 -0
- emdash_core/swarm/merge_agent.py +383 -0
- emdash_core/swarm/session_manager.py +274 -0
- emdash_core/swarm/swarm_runner.py +226 -0
- emdash_core/swarm/task_definition.py +137 -0
- emdash_core/swarm/worker_spawner.py +319 -0
- emdash_core/swarm/worktree_manager.py +278 -0
- emdash_core/templates/__init__.py +10 -0
- emdash_core/templates/defaults/agent-builder.md.template +82 -0
- emdash_core/templates/defaults/focus.md.template +115 -0
- emdash_core/templates/defaults/pr-review-enhanced.md.template +309 -0
- emdash_core/templates/defaults/pr-review.md.template +80 -0
- emdash_core/templates/defaults/project.md.template +85 -0
- emdash_core/templates/defaults/research_critic.md.template +112 -0
- emdash_core/templates/defaults/research_planner.md.template +85 -0
- emdash_core/templates/defaults/research_synthesizer.md.template +128 -0
- emdash_core/templates/defaults/reviewer.md.template +81 -0
- emdash_core/templates/defaults/spec.md.template +41 -0
- emdash_core/templates/defaults/tasks.md.template +78 -0
- emdash_core/templates/loader.py +296 -0
- emdash_core/utils/__init__.py +45 -0
- emdash_core/utils/git.py +84 -0
- emdash_core/utils/image.py +502 -0
- emdash_core/utils/logger.py +51 -0
- emdash_core-0.1.7.dist-info/METADATA +35 -0
- emdash_core-0.1.7.dist-info/RECORD +187 -0
- emdash_core-0.1.7.dist-info/WHEEL +4 -0
- 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
|
+
)
|