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,68 @@
|
|
|
1
|
+
"""Pydantic models for agent API."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AgentMode(str, Enum):
|
|
10
|
+
"""Agent operation modes."""
|
|
11
|
+
|
|
12
|
+
CODE = "code"
|
|
13
|
+
RESEARCH = "research"
|
|
14
|
+
REVIEW = "review"
|
|
15
|
+
SPEC = "spec"
|
|
16
|
+
PLAN = "plan"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ImageData(BaseModel):
|
|
20
|
+
"""Image data for vision-capable models."""
|
|
21
|
+
|
|
22
|
+
data: str = Field(..., description="Base64 encoded image data")
|
|
23
|
+
format: str = Field(default="png", description="Image format (png, jpg, etc.)")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AgentChatOptions(BaseModel):
|
|
27
|
+
"""Options for agent chat."""
|
|
28
|
+
|
|
29
|
+
max_iterations: int = Field(default=50, description="Maximum agent iterations")
|
|
30
|
+
verbose: bool = Field(default=True, description="Enable verbose output")
|
|
31
|
+
mode: AgentMode = Field(default=AgentMode.CODE, description="Agent mode")
|
|
32
|
+
context_threshold: float = Field(
|
|
33
|
+
default=0.6,
|
|
34
|
+
description="Context window threshold for summarization (0-1)"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AgentChatRequest(BaseModel):
|
|
39
|
+
"""Request for agent chat endpoint."""
|
|
40
|
+
|
|
41
|
+
message: str = Field(..., description="User message/task")
|
|
42
|
+
session_id: Optional[str] = Field(
|
|
43
|
+
default=None,
|
|
44
|
+
description="Session ID for conversation continuity"
|
|
45
|
+
)
|
|
46
|
+
model: Optional[str] = Field(
|
|
47
|
+
default=None,
|
|
48
|
+
description="Model to use (defaults to server config)"
|
|
49
|
+
)
|
|
50
|
+
images: list[ImageData] = Field(
|
|
51
|
+
default_factory=list,
|
|
52
|
+
description="Images for vision-capable models"
|
|
53
|
+
)
|
|
54
|
+
options: AgentChatOptions = Field(
|
|
55
|
+
default_factory=AgentChatOptions,
|
|
56
|
+
description="Agent options"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class SessionInfo(BaseModel):
|
|
61
|
+
"""Information about an agent session."""
|
|
62
|
+
|
|
63
|
+
session_id: str
|
|
64
|
+
agent_name: str
|
|
65
|
+
model: str
|
|
66
|
+
created_at: str
|
|
67
|
+
message_count: int
|
|
68
|
+
is_active: bool
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Pydantic models for indexing API."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class IndexOptions(BaseModel):
|
|
10
|
+
"""Options for indexing operation."""
|
|
11
|
+
|
|
12
|
+
incremental: bool = Field(
|
|
13
|
+
default=False,
|
|
14
|
+
description="Only index changed files"
|
|
15
|
+
)
|
|
16
|
+
changed_only: bool = Field(
|
|
17
|
+
default=False,
|
|
18
|
+
description="Detect and index only modified files"
|
|
19
|
+
)
|
|
20
|
+
skip_git: bool = Field(
|
|
21
|
+
default=False,
|
|
22
|
+
description="Skip git history analysis"
|
|
23
|
+
)
|
|
24
|
+
pr_limit: int = Field(
|
|
25
|
+
default=100,
|
|
26
|
+
description="Maximum PRs to fetch"
|
|
27
|
+
)
|
|
28
|
+
detect_communities: bool = Field(
|
|
29
|
+
default=True,
|
|
30
|
+
description="Run community detection"
|
|
31
|
+
)
|
|
32
|
+
skip_embeddings: bool = Field(
|
|
33
|
+
default=False,
|
|
34
|
+
description="Skip embedding generation"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class IndexRequest(BaseModel):
|
|
39
|
+
"""Request to start indexing."""
|
|
40
|
+
|
|
41
|
+
repo_path: str = Field(..., description="Path to repository")
|
|
42
|
+
options: IndexOptions = Field(
|
|
43
|
+
default_factory=IndexOptions,
|
|
44
|
+
description="Indexing options"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class IndexStats(BaseModel):
|
|
49
|
+
"""Statistics about indexed content."""
|
|
50
|
+
|
|
51
|
+
files: int = Field(default=0, description="Number of files indexed")
|
|
52
|
+
functions: int = Field(default=0, description="Number of functions")
|
|
53
|
+
classes: int = Field(default=0, description="Number of classes")
|
|
54
|
+
relationships: int = Field(default=0, description="Number of relationships")
|
|
55
|
+
communities: int = Field(default=0, description="Number of communities detected")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class IndexStatus(BaseModel):
|
|
59
|
+
"""Status of indexing operation."""
|
|
60
|
+
|
|
61
|
+
is_running: bool = Field(default=False, description="Whether indexing is in progress")
|
|
62
|
+
last_indexed: Optional[datetime] = Field(
|
|
63
|
+
default=None,
|
|
64
|
+
description="Last successful index timestamp"
|
|
65
|
+
)
|
|
66
|
+
last_commit: Optional[str] = Field(
|
|
67
|
+
default=None,
|
|
68
|
+
description="Last indexed commit hash"
|
|
69
|
+
)
|
|
70
|
+
stats: IndexStats = Field(
|
|
71
|
+
default_factory=IndexStats,
|
|
72
|
+
description="Index statistics"
|
|
73
|
+
)
|
|
74
|
+
error: Optional[str] = Field(
|
|
75
|
+
default=None,
|
|
76
|
+
description="Error message if failed"
|
|
77
|
+
)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Pydantic models for query API."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EntityType(str, Enum):
|
|
10
|
+
"""Types of code entities."""
|
|
11
|
+
|
|
12
|
+
FILE = "File"
|
|
13
|
+
CLASS = "Class"
|
|
14
|
+
FUNCTION = "Function"
|
|
15
|
+
METHOD = "Method"
|
|
16
|
+
MODULE = "Module"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SearchType(str, Enum):
|
|
20
|
+
"""Types of search operations."""
|
|
21
|
+
|
|
22
|
+
SEMANTIC = "semantic"
|
|
23
|
+
TEXT = "text"
|
|
24
|
+
GREP = "grep"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SearchFilters(BaseModel):
|
|
28
|
+
"""Filters for search results."""
|
|
29
|
+
|
|
30
|
+
entity_types: list[EntityType] = Field(
|
|
31
|
+
default_factory=list,
|
|
32
|
+
description="Filter by entity types"
|
|
33
|
+
)
|
|
34
|
+
limit: int = Field(default=20, description="Maximum results to return")
|
|
35
|
+
min_score: float = Field(default=0.0, description="Minimum similarity score")
|
|
36
|
+
file_patterns: list[str] = Field(
|
|
37
|
+
default_factory=list,
|
|
38
|
+
description="Glob patterns to filter files"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SearchRequest(BaseModel):
|
|
43
|
+
"""Request for search endpoint."""
|
|
44
|
+
|
|
45
|
+
query: str = Field(..., description="Search query")
|
|
46
|
+
type: SearchType = Field(default=SearchType.SEMANTIC, description="Search type")
|
|
47
|
+
filters: SearchFilters = Field(
|
|
48
|
+
default_factory=SearchFilters,
|
|
49
|
+
description="Search filters"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class SearchResult(BaseModel):
|
|
54
|
+
"""A single search result."""
|
|
55
|
+
|
|
56
|
+
qualified_name: str = Field(..., description="Fully qualified name")
|
|
57
|
+
name: str = Field(..., description="Short name")
|
|
58
|
+
type: EntityType = Field(..., description="Entity type")
|
|
59
|
+
file_path: str = Field(..., description="File path relative to repo")
|
|
60
|
+
line_number: Optional[int] = Field(default=None, description="Line number")
|
|
61
|
+
score: float = Field(..., description="Relevance score (0-1)")
|
|
62
|
+
snippet: Optional[str] = Field(default=None, description="Code snippet")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class SearchResponse(BaseModel):
|
|
66
|
+
"""Response from search endpoint."""
|
|
67
|
+
|
|
68
|
+
results: list[SearchResult] = Field(default_factory=list)
|
|
69
|
+
total: int = Field(..., description="Total number of matches")
|
|
70
|
+
query: str = Field(..., description="Original query")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ExpandRequest(BaseModel):
|
|
74
|
+
"""Request to expand a node."""
|
|
75
|
+
|
|
76
|
+
node_type: EntityType = Field(..., description="Type of node to expand")
|
|
77
|
+
identifier: str = Field(..., description="Qualified name or identifier")
|
|
78
|
+
max_hops: int = Field(default=2, description="Maximum traversal depth")
|
|
79
|
+
include_source: bool = Field(default=True, description="Include source code")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class CallersRequest(BaseModel):
|
|
83
|
+
"""Request to get callers of a function."""
|
|
84
|
+
|
|
85
|
+
qualified_name: str = Field(..., description="Qualified name of function")
|
|
86
|
+
max_depth: int = Field(default=1, description="Maximum call depth")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class CalleesRequest(BaseModel):
|
|
90
|
+
"""Request to get callees of a function."""
|
|
91
|
+
|
|
92
|
+
qualified_name: str = Field(..., description="Qualified name of function")
|
|
93
|
+
max_depth: int = Field(default=1, description="Maximum call depth")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class HierarchyRequest(BaseModel):
|
|
97
|
+
"""Request to get class hierarchy."""
|
|
98
|
+
|
|
99
|
+
class_name: str = Field(..., description="Qualified name of class")
|
|
100
|
+
direction: str = Field(
|
|
101
|
+
default="both",
|
|
102
|
+
description="Direction: 'up' (parents), 'down' (children), 'both'"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class DependenciesRequest(BaseModel):
|
|
107
|
+
"""Request to get file dependencies."""
|
|
108
|
+
|
|
109
|
+
file_path: str = Field(..., description="File path to analyze")
|
|
110
|
+
direction: str = Field(
|
|
111
|
+
default="both",
|
|
112
|
+
description="Direction: 'imports', 'imported_by', 'both'"
|
|
113
|
+
)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Planning and context building for AI agents."""
|
|
2
|
+
|
|
3
|
+
from .similarity import SimilaritySearch
|
|
4
|
+
from .context_builder import ContextBuilder, PlanningContext
|
|
5
|
+
from .agent_api import AgentAPI
|
|
6
|
+
|
|
7
|
+
__all__ = ["SimilaritySearch", "ContextBuilder", "PlanningContext", "AgentAPI"]
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
"""Graph traversal API for AI agents."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from ..graph.connection import KuzuConnection, get_connection
|
|
6
|
+
from ..utils.logger import log
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AgentAPI:
|
|
10
|
+
"""Graph traversal API for AI coding agents."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, connection: Optional[KuzuConnection] = None):
|
|
13
|
+
"""Initialize agent API.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
connection: Neo4j connection. If None, uses global connection.
|
|
17
|
+
"""
|
|
18
|
+
self.connection = connection or get_connection()
|
|
19
|
+
|
|
20
|
+
def get_file_dependencies(self, file_path: str) -> dict:
|
|
21
|
+
"""Get files that import/are imported by this file.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
file_path: Path to the file
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Dictionary with imports and imported_by lists
|
|
28
|
+
"""
|
|
29
|
+
with self.connection.session() as session:
|
|
30
|
+
# Get files this file imports
|
|
31
|
+
imports_result = session.run("""
|
|
32
|
+
MATCH (f:File)-[:IMPORTS]->(m:Module)
|
|
33
|
+
WHERE f.path ENDS WITH $file_path
|
|
34
|
+
RETURN m.name as module_name,
|
|
35
|
+
m.is_external as is_external
|
|
36
|
+
""", file_path=file_path)
|
|
37
|
+
imports = [dict(r) for r in imports_result]
|
|
38
|
+
|
|
39
|
+
# Get files that import modules from this file
|
|
40
|
+
# Query functions and classes separately (Kuzu doesn't support | in rel types)
|
|
41
|
+
func_result = session.run("""
|
|
42
|
+
MATCH (f:File)-[:CONTAINS_FUNCTION]->(entity:Function)
|
|
43
|
+
WHERE f.path ENDS WITH $file_path
|
|
44
|
+
WITH entity.qualified_name as qn
|
|
45
|
+
MATCH (other:File)-[:IMPORTS]->(m:Module)
|
|
46
|
+
WHERE m.name CONTAINS qn OR m.import_path CONTAINS qn
|
|
47
|
+
RETURN DISTINCT other.path as file_path
|
|
48
|
+
""", file_path=file_path)
|
|
49
|
+
|
|
50
|
+
class_result = session.run("""
|
|
51
|
+
MATCH (f:File)-[:CONTAINS_CLASS]->(entity:Class)
|
|
52
|
+
WHERE f.path ENDS WITH $file_path
|
|
53
|
+
WITH entity.qualified_name as qn
|
|
54
|
+
MATCH (other:File)-[:IMPORTS]->(m:Module)
|
|
55
|
+
WHERE m.name CONTAINS qn OR m.import_path CONTAINS qn
|
|
56
|
+
RETURN DISTINCT other.path as file_path
|
|
57
|
+
""", file_path=file_path)
|
|
58
|
+
|
|
59
|
+
imported_by = list(set(
|
|
60
|
+
[r["file_path"] for r in func_result] +
|
|
61
|
+
[r["file_path"] for r in class_result]
|
|
62
|
+
))
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
"file_path": file_path,
|
|
66
|
+
"imports": imports,
|
|
67
|
+
"imported_by": imported_by,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
def get_function_callers(self, qualified_name: str) -> list[dict]:
|
|
71
|
+
"""Find all functions that call this function.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
qualified_name: Qualified name of the function
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
List of calling functions with metadata
|
|
78
|
+
"""
|
|
79
|
+
with self.connection.session() as session:
|
|
80
|
+
result = session.run("""
|
|
81
|
+
MATCH (caller:Function)-[:CALLS]->(f:Function {qualified_name: $qualified_name})
|
|
82
|
+
RETURN caller.name as name,
|
|
83
|
+
caller.qualified_name as qualified_name,
|
|
84
|
+
caller.file_path as file_path,
|
|
85
|
+
caller.is_method as is_method
|
|
86
|
+
ORDER BY caller.name
|
|
87
|
+
""", qualified_name=qualified_name)
|
|
88
|
+
|
|
89
|
+
return [dict(r) for r in result]
|
|
90
|
+
|
|
91
|
+
def get_function_callees(self, qualified_name: str) -> list[dict]:
|
|
92
|
+
"""Find all functions called by this function.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
qualified_name: Qualified name of the function
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
List of called functions with metadata
|
|
99
|
+
"""
|
|
100
|
+
with self.connection.session() as session:
|
|
101
|
+
result = session.run("""
|
|
102
|
+
MATCH (f:Function {qualified_name: $qualified_name})-[:CALLS]->(callee:Function)
|
|
103
|
+
RETURN callee.name as name,
|
|
104
|
+
callee.qualified_name as qualified_name,
|
|
105
|
+
callee.file_path as file_path,
|
|
106
|
+
callee.is_method as is_method
|
|
107
|
+
ORDER BY callee.name
|
|
108
|
+
""", qualified_name=qualified_name)
|
|
109
|
+
|
|
110
|
+
return [dict(r) for r in result]
|
|
111
|
+
|
|
112
|
+
def get_class_hierarchy(self, class_name: str) -> dict:
|
|
113
|
+
"""Get inheritance tree for a class.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
class_name: Name or qualified name of the class
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Dictionary with parents and children
|
|
120
|
+
"""
|
|
121
|
+
with self.connection.session() as session:
|
|
122
|
+
# Get parent classes
|
|
123
|
+
parents_result = session.run("""
|
|
124
|
+
MATCH (c:Class)-[:INHERITS_FROM]->(parent:Class)
|
|
125
|
+
WHERE c.name = $class_name OR c.qualified_name = $class_name
|
|
126
|
+
RETURN parent.name as name,
|
|
127
|
+
parent.qualified_name as qualified_name,
|
|
128
|
+
parent.file_path as file_path
|
|
129
|
+
""", class_name=class_name)
|
|
130
|
+
parents = [dict(r) for r in parents_result]
|
|
131
|
+
|
|
132
|
+
# Get child classes
|
|
133
|
+
children_result = session.run("""
|
|
134
|
+
MATCH (child:Class)-[:INHERITS_FROM]->(c:Class)
|
|
135
|
+
WHERE c.name = $class_name OR c.qualified_name = $class_name
|
|
136
|
+
RETURN child.name as name,
|
|
137
|
+
child.qualified_name as qualified_name,
|
|
138
|
+
child.file_path as file_path
|
|
139
|
+
""", class_name=class_name)
|
|
140
|
+
children = [dict(r) for r in children_result]
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
"class_name": class_name,
|
|
144
|
+
"parents": parents,
|
|
145
|
+
"children": children,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
def get_file_history(self, file_path: str, limit: int = 10) -> dict:
|
|
149
|
+
"""Get recent commits that modified this file.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
file_path: Path to the file
|
|
153
|
+
limit: Maximum number of commits to return
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Dictionary with file_path and commits list
|
|
157
|
+
"""
|
|
158
|
+
with self.connection.session() as session:
|
|
159
|
+
result = session.run("""
|
|
160
|
+
MATCH (c:GitCommit)-[mod:COMMIT_MODIFIES]->(f:File)
|
|
161
|
+
WHERE f.path ENDS WITH $file_path
|
|
162
|
+
RETURN c.sha as sha,
|
|
163
|
+
c.message as message,
|
|
164
|
+
c.author_name as author,
|
|
165
|
+
c.timestamp as timestamp,
|
|
166
|
+
mod.change_type as change_type,
|
|
167
|
+
mod.insertions as insertions,
|
|
168
|
+
mod.deletions as deletions
|
|
169
|
+
ORDER BY c.timestamp DESC
|
|
170
|
+
LIMIT $limit
|
|
171
|
+
""", file_path=file_path, limit=limit)
|
|
172
|
+
|
|
173
|
+
commits = [dict(r) for r in result]
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
"file_path": file_path,
|
|
177
|
+
"commits": commits,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
def get_community_overview(self, community_id: int) -> dict:
|
|
181
|
+
"""Get summary of a code community.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
community_id: The community ID
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Dictionary with community summary
|
|
188
|
+
"""
|
|
189
|
+
with self.connection.session() as session:
|
|
190
|
+
# Get member counts by type
|
|
191
|
+
result = session.run("""
|
|
192
|
+
MATCH (n)
|
|
193
|
+
WHERE n.community = $community_id
|
|
194
|
+
AND (n:Class OR n:Function)
|
|
195
|
+
WITH label(n) as type, n
|
|
196
|
+
RETURN type,
|
|
197
|
+
count(n) as count,
|
|
198
|
+
collect(n.name)[0:10] as sample_names
|
|
199
|
+
""", community_id=community_id)
|
|
200
|
+
|
|
201
|
+
members_by_type = {r["type"]: {"count": r["count"], "samples": r["sample_names"]}
|
|
202
|
+
for r in result}
|
|
203
|
+
|
|
204
|
+
# Get files in this community (separate queries for Kuzu compatibility)
|
|
205
|
+
func_files = session.run("""
|
|
206
|
+
MATCH (f:File)-[:CONTAINS_FUNCTION]->(n:Function)
|
|
207
|
+
WHERE n.community = $community_id
|
|
208
|
+
RETURN DISTINCT f.path as file_path
|
|
209
|
+
LIMIT 10
|
|
210
|
+
""", community_id=community_id)
|
|
211
|
+
|
|
212
|
+
class_files = session.run("""
|
|
213
|
+
MATCH (f:File)-[:CONTAINS_CLASS]->(n:Class)
|
|
214
|
+
WHERE n.community = $community_id
|
|
215
|
+
RETURN DISTINCT f.path as file_path
|
|
216
|
+
LIMIT 10
|
|
217
|
+
""", community_id=community_id)
|
|
218
|
+
|
|
219
|
+
files = list(set(
|
|
220
|
+
[r["file_path"] for r in func_files] +
|
|
221
|
+
[r["file_path"] for r in class_files]
|
|
222
|
+
))[:10]
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
"community_id": community_id,
|
|
226
|
+
"members_by_type": members_by_type,
|
|
227
|
+
"sample_files": files,
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
def get_author_expertise(self, email: str) -> dict:
|
|
231
|
+
"""Get files and areas an author has worked on.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
email: Author's email
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Dictionary with author expertise summary
|
|
238
|
+
"""
|
|
239
|
+
with self.connection.session() as session:
|
|
240
|
+
# Get author info
|
|
241
|
+
author_result = session.run("""
|
|
242
|
+
MATCH (a:Author {email: $email})
|
|
243
|
+
RETURN a.name as name,
|
|
244
|
+
a.total_commits as total_commits,
|
|
245
|
+
a.total_lines_added as lines_added,
|
|
246
|
+
a.total_lines_deleted as lines_deleted
|
|
247
|
+
""", email=email)
|
|
248
|
+
author = author_result.single()
|
|
249
|
+
|
|
250
|
+
if not author:
|
|
251
|
+
return {"error": f"Author not found: {email}"}
|
|
252
|
+
|
|
253
|
+
# Get most modified files
|
|
254
|
+
files_result = session.run("""
|
|
255
|
+
MATCH (a:Author {email: $email})<-[:AUTHORED_BY]-(c:GitCommit)-[:COMMIT_MODIFIES]->(f:File)
|
|
256
|
+
WITH f.path as file_path, count(c) as commit_count
|
|
257
|
+
RETURN file_path, commit_count
|
|
258
|
+
ORDER BY commit_count DESC
|
|
259
|
+
LIMIT 10
|
|
260
|
+
""", email=email)
|
|
261
|
+
top_files = [dict(r) for r in files_result]
|
|
262
|
+
|
|
263
|
+
# Get communities the author has worked in (separate queries for Kuzu)
|
|
264
|
+
func_communities = session.run("""
|
|
265
|
+
MATCH (a:Author {email: $email})<-[:AUTHORED_BY]-(c:GitCommit)-[:COMMIT_MODIFIES]->(f:File)
|
|
266
|
+
MATCH (f)-[:CONTAINS_FUNCTION]->(entity:Function)
|
|
267
|
+
WHERE entity.community IS NOT NULL
|
|
268
|
+
WITH entity.community as community_id, count(DISTINCT c) as commit_count
|
|
269
|
+
RETURN community_id, commit_count
|
|
270
|
+
ORDER BY commit_count DESC
|
|
271
|
+
LIMIT 5
|
|
272
|
+
""", email=email)
|
|
273
|
+
|
|
274
|
+
class_communities = session.run("""
|
|
275
|
+
MATCH (a:Author {email: $email})<-[:AUTHORED_BY]-(c:GitCommit)-[:COMMIT_MODIFIES]->(f:File)
|
|
276
|
+
MATCH (f)-[:CONTAINS_CLASS]->(entity:Class)
|
|
277
|
+
WHERE entity.community IS NOT NULL
|
|
278
|
+
WITH entity.community as community_id, count(DISTINCT c) as commit_count
|
|
279
|
+
RETURN community_id, commit_count
|
|
280
|
+
ORDER BY commit_count DESC
|
|
281
|
+
LIMIT 5
|
|
282
|
+
""", email=email)
|
|
283
|
+
|
|
284
|
+
# Combine and deduplicate by community_id, keeping highest commit_count
|
|
285
|
+
community_map = {}
|
|
286
|
+
for r in list(func_communities) + list(class_communities):
|
|
287
|
+
cid = r["community_id"]
|
|
288
|
+
cc = r["commit_count"]
|
|
289
|
+
if cid not in community_map or cc > community_map[cid]:
|
|
290
|
+
community_map[cid] = cc
|
|
291
|
+
communities = [
|
|
292
|
+
{"community_id": cid, "commit_count": cc}
|
|
293
|
+
for cid, cc in sorted(community_map.items(), key=lambda x: -x[1])[:5]
|
|
294
|
+
]
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
"email": email,
|
|
298
|
+
"name": author["name"],
|
|
299
|
+
"total_commits": author["total_commits"],
|
|
300
|
+
"lines_added": author["lines_added"],
|
|
301
|
+
"lines_deleted": author["lines_deleted"],
|
|
302
|
+
"top_files": top_files,
|
|
303
|
+
"communities": communities,
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
def expand_from_files(
|
|
307
|
+
self,
|
|
308
|
+
file_paths: list[str],
|
|
309
|
+
hops: int = 1,
|
|
310
|
+
) -> dict:
|
|
311
|
+
"""Expand to related files within N relationship hops.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
file_paths: Starting file paths
|
|
315
|
+
hops: Number of relationship hops to follow
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
Dictionary with expanded file set and relationships
|
|
319
|
+
"""
|
|
320
|
+
with self.connection.session() as session:
|
|
321
|
+
# Get directly related files (via imports, function calls)
|
|
322
|
+
result = session.run("""
|
|
323
|
+
UNWIND $file_paths as fp
|
|
324
|
+
MATCH (f:File)
|
|
325
|
+
WHERE f.path ENDS WITH fp
|
|
326
|
+
|
|
327
|
+
// Follow imports
|
|
328
|
+
OPTIONAL MATCH (f)-[:IMPORTS]->(m:Module)<-[:IMPORTS]-(related:File)
|
|
329
|
+
WHERE related.path <> f.path
|
|
330
|
+
|
|
331
|
+
// Follow function calls
|
|
332
|
+
OPTIONAL MATCH (f)-[:CONTAINS_FUNCTION]->(func:Function)-[:CALLS]->(called:Function)<-[:CONTAINS_FUNCTION]-(related2:File)
|
|
333
|
+
WHERE related2.path <> f.path
|
|
334
|
+
|
|
335
|
+
WITH collect(DISTINCT related.path) + collect(DISTINCT related2.path) as related_paths
|
|
336
|
+
UNWIND related_paths as rp
|
|
337
|
+
WITH rp WHERE rp IS NOT NULL
|
|
338
|
+
RETURN DISTINCT rp as file_path
|
|
339
|
+
LIMIT 20
|
|
340
|
+
""", file_paths=file_paths)
|
|
341
|
+
|
|
342
|
+
related_files = [r["file_path"] for r in result]
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
"starting_files": file_paths,
|
|
346
|
+
"hops": hops,
|
|
347
|
+
"related_files": related_files,
|
|
348
|
+
"total_files": len(file_paths) + len(related_files),
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
def get_impact_analysis(self, file_path: str) -> dict:
|
|
352
|
+
"""Analyze potential impact of changing a file.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
file_path: Path to the file
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
Dictionary with impact analysis
|
|
359
|
+
"""
|
|
360
|
+
with self.connection.session() as session:
|
|
361
|
+
# Get functions in this file and their callers
|
|
362
|
+
callers_result = session.run("""
|
|
363
|
+
MATCH (f:File)-[:CONTAINS_FUNCTION]->(func:Function)
|
|
364
|
+
WHERE f.path ENDS WITH $file_path
|
|
365
|
+
OPTIONAL MATCH (caller:Function)-[:CALLS]->(func)
|
|
366
|
+
RETURN func.name as function_name,
|
|
367
|
+
func.qualified_name as qualified_name,
|
|
368
|
+
collect(DISTINCT caller.qualified_name) as called_by
|
|
369
|
+
""", file_path=file_path)
|
|
370
|
+
|
|
371
|
+
functions_impact = []
|
|
372
|
+
total_callers = set()
|
|
373
|
+
for r in callers_result:
|
|
374
|
+
callers = [c for c in r["called_by"] if c is not None]
|
|
375
|
+
total_callers.update(callers)
|
|
376
|
+
functions_impact.append({
|
|
377
|
+
"name": r["function_name"],
|
|
378
|
+
"qualified_name": r["qualified_name"],
|
|
379
|
+
"caller_count": len(callers),
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
# Get files that import from this file (separate queries for Kuzu)
|
|
383
|
+
func_names = session.run("""
|
|
384
|
+
MATCH (f:File)-[:CONTAINS_FUNCTION]->(entity:Function)
|
|
385
|
+
WHERE f.path ENDS WITH $file_path
|
|
386
|
+
RETURN DISTINCT entity.name as name
|
|
387
|
+
""", file_path=file_path)
|
|
388
|
+
|
|
389
|
+
class_names = session.run("""
|
|
390
|
+
MATCH (f:File)-[:CONTAINS_CLASS]->(entity:Class)
|
|
391
|
+
WHERE f.path ENDS WITH $file_path
|
|
392
|
+
RETURN DISTINCT entity.name as name
|
|
393
|
+
""", file_path=file_path)
|
|
394
|
+
|
|
395
|
+
exported_names = [r["name"] for r in func_names] + [r["name"] for r in class_names]
|
|
396
|
+
|
|
397
|
+
# Find files that import these names
|
|
398
|
+
dependent_files = []
|
|
399
|
+
if exported_names:
|
|
400
|
+
dependents_result = session.run("""
|
|
401
|
+
MATCH (other:File)-[:IMPORTS]->(m:Module)
|
|
402
|
+
WHERE any(name IN $exported_names WHERE m.name CONTAINS name)
|
|
403
|
+
RETURN DISTINCT other.path as file_path
|
|
404
|
+
""", exported_names=exported_names)
|
|
405
|
+
dependent_files = [r["file_path"] for r in dependents_result]
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
"file_path": file_path,
|
|
409
|
+
"functions": functions_impact,
|
|
410
|
+
"total_callers": len(total_callers),
|
|
411
|
+
"dependent_files": dependent_files,
|
|
412
|
+
"risk_level": "high" if len(total_callers) > 10 else "medium" if len(total_callers) > 3 else "low",
|
|
413
|
+
}
|