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,475 @@
|
|
|
1
|
+
"""Reviewer profile agent for analyzing repository reviewers and generating profiles."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections import Counter
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
13
|
+
|
|
14
|
+
from .toolkit import AgentToolkit
|
|
15
|
+
from .providers import get_provider
|
|
16
|
+
from .providers.factory import DEFAULT_MODEL
|
|
17
|
+
from ..graph.connection import get_connection
|
|
18
|
+
from ..utils.logger import log
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class ReviewerData:
|
|
23
|
+
"""Data about a reviewer's patterns."""
|
|
24
|
+
|
|
25
|
+
username: str
|
|
26
|
+
review_count: int
|
|
27
|
+
prs_reviewed: list[int] = field(default_factory=list)
|
|
28
|
+
review_comments: list[dict] = field(default_factory=list)
|
|
29
|
+
review_verdicts: list[str] = field(default_factory=list) # APPROVED, CHANGES_REQUESTED, etc.
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class ContributorData:
|
|
34
|
+
"""Data about a cross-team contributor."""
|
|
35
|
+
|
|
36
|
+
name: str
|
|
37
|
+
email: str
|
|
38
|
+
communities_touched: int
|
|
39
|
+
commit_count: int
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
SYNTHESIS_PROMPT = """You are analyzing code review patterns from a repository to create a reviewer profile template.
|
|
43
|
+
|
|
44
|
+
Based on the following data about top reviewers and cross-team contributors, create a comprehensive reviewer profile that captures:
|
|
45
|
+
|
|
46
|
+
1. **Review Focus Areas**: What aspects of code do reviewers commonly focus on?
|
|
47
|
+
2. **Feedback Patterns**: What types of issues do they commonly point out?
|
|
48
|
+
3. **Code Quality Expectations**: What standards do they enforce?
|
|
49
|
+
4. **Style Preferences**: What coding patterns do they prefer?
|
|
50
|
+
5. **Tone & Communication**: How do they phrase their feedback?
|
|
51
|
+
6. **Example Comments**: Representative examples of good review comments
|
|
52
|
+
7. **Review Checklist**: Key items reviewers check before approving
|
|
53
|
+
|
|
54
|
+
IMPORTANT:
|
|
55
|
+
- Extract patterns from the actual review comments provided
|
|
56
|
+
- Be specific about the types of issues raised
|
|
57
|
+
- Capture the tone and phrasing style
|
|
58
|
+
- Generate a checklist based on what reviewers actually check
|
|
59
|
+
- The output should be markdown that can be used as a template for future reviews
|
|
60
|
+
|
|
61
|
+
OUTPUT FORMAT:
|
|
62
|
+
Return a complete markdown document that follows this structure:
|
|
63
|
+
|
|
64
|
+
# Reviewer Profile
|
|
65
|
+
|
|
66
|
+
## Identity
|
|
67
|
+
- Primary reviewers analyzed: {list of usernames}
|
|
68
|
+
- Cross-team contributors analyzed: {list of names}
|
|
69
|
+
- PRs analyzed: {count}
|
|
70
|
+
|
|
71
|
+
## Review Focus Areas
|
|
72
|
+
{bullet points of what reviewers focus on}
|
|
73
|
+
|
|
74
|
+
## Feedback Patterns
|
|
75
|
+
### What they commonly comment on:
|
|
76
|
+
{patterns}
|
|
77
|
+
|
|
78
|
+
### Code quality expectations:
|
|
79
|
+
{expectations}
|
|
80
|
+
|
|
81
|
+
### Style preferences:
|
|
82
|
+
{preferences}
|
|
83
|
+
|
|
84
|
+
## Tone & Communication
|
|
85
|
+
{description of tone and communication style}
|
|
86
|
+
|
|
87
|
+
## Example Comments
|
|
88
|
+
{3-5 representative example comments from the data}
|
|
89
|
+
|
|
90
|
+
## Review Checklist
|
|
91
|
+
{checklist items based on what reviewers check}
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class ReviewerProfileAgent:
|
|
96
|
+
"""Agent that analyzes repository reviewers and generates a profile template."""
|
|
97
|
+
|
|
98
|
+
def __init__(
|
|
99
|
+
self,
|
|
100
|
+
model: str = DEFAULT_MODEL,
|
|
101
|
+
verbose: bool = True,
|
|
102
|
+
):
|
|
103
|
+
self.provider = get_provider(model)
|
|
104
|
+
self.toolkit = AgentToolkit(enable_session=False)
|
|
105
|
+
self.model = model
|
|
106
|
+
self.verbose = verbose
|
|
107
|
+
self.console = Console()
|
|
108
|
+
|
|
109
|
+
# Graph connection for Neo4j queries
|
|
110
|
+
try:
|
|
111
|
+
self.graph = get_connection()
|
|
112
|
+
except Exception:
|
|
113
|
+
self.graph = None
|
|
114
|
+
log.warning("Neo4j connection not available - cross-team contributor analysis will be skipped")
|
|
115
|
+
|
|
116
|
+
def analyze(
|
|
117
|
+
self,
|
|
118
|
+
top_n_reviewers: int = 5,
|
|
119
|
+
top_n_contributors: int = 5,
|
|
120
|
+
max_prs: int = 100,
|
|
121
|
+
) -> str:
|
|
122
|
+
"""Analyze repository reviewers and generate a profile.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
top_n_reviewers: Number of top reviewers to analyze
|
|
126
|
+
top_n_contributors: Number of cross-team contributors to include
|
|
127
|
+
max_prs: Maximum PRs to fetch for analysis
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Generated reviewer profile as markdown
|
|
131
|
+
"""
|
|
132
|
+
with Progress(
|
|
133
|
+
SpinnerColumn(),
|
|
134
|
+
TextColumn("[progress.description]{task.description}"),
|
|
135
|
+
console=self.console,
|
|
136
|
+
disable=not self.verbose,
|
|
137
|
+
) as progress:
|
|
138
|
+
# 1. Fetch all PRs
|
|
139
|
+
task = progress.add_task("Fetching PRs...", total=None)
|
|
140
|
+
prs = self._fetch_all_prs(max_prs=max_prs)
|
|
141
|
+
progress.update(task, description=f"Fetched {len(prs)} PRs")
|
|
142
|
+
|
|
143
|
+
# 2. Count reviews, get top reviewers
|
|
144
|
+
progress.update(task, description="Identifying top reviewers...")
|
|
145
|
+
top_reviewers = self._get_top_reviewers(prs, top_n_reviewers)
|
|
146
|
+
progress.update(task, description=f"Found {len(top_reviewers)} top reviewers")
|
|
147
|
+
|
|
148
|
+
# 3. Fetch review comments for each top reviewer
|
|
149
|
+
progress.update(task, description="Fetching review comments...")
|
|
150
|
+
reviewer_data = self._fetch_reviewer_details(top_reviewers, prs)
|
|
151
|
+
|
|
152
|
+
# 4. Query Neo4j for multi-community contributors
|
|
153
|
+
cross_team = []
|
|
154
|
+
if self.graph:
|
|
155
|
+
progress.update(task, description="Finding cross-team contributors...")
|
|
156
|
+
cross_team = self._get_cross_team_contributors(top_n_contributors)
|
|
157
|
+
|
|
158
|
+
# 5. Synthesize with LLM
|
|
159
|
+
progress.update(task, description="Synthesizing reviewer profile...")
|
|
160
|
+
profile = self._synthesize_profile(reviewer_data, cross_team, len(prs))
|
|
161
|
+
|
|
162
|
+
progress.update(task, description="Done!")
|
|
163
|
+
|
|
164
|
+
return profile
|
|
165
|
+
|
|
166
|
+
def _fetch_all_prs(self, max_prs: int = 100) -> list[dict]:
|
|
167
|
+
"""Fetch PRs from the repository.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
max_prs: Maximum number of PRs to fetch
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
List of PR data dictionaries
|
|
174
|
+
"""
|
|
175
|
+
all_prs = []
|
|
176
|
+
|
|
177
|
+
# Fetch closed/merged PRs (more likely to have reviews)
|
|
178
|
+
for state in ["closed", "open"]:
|
|
179
|
+
result = self.toolkit.execute(
|
|
180
|
+
"github_list_prs",
|
|
181
|
+
state=state,
|
|
182
|
+
per_page=min(100, max_prs - len(all_prs)),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if result.success:
|
|
186
|
+
prs = result.data.get("prs", [])
|
|
187
|
+
all_prs.extend(prs)
|
|
188
|
+
|
|
189
|
+
if len(all_prs) >= max_prs:
|
|
190
|
+
break
|
|
191
|
+
|
|
192
|
+
return all_prs[:max_prs]
|
|
193
|
+
|
|
194
|
+
def _get_top_reviewers(
|
|
195
|
+
self,
|
|
196
|
+
prs: list[dict],
|
|
197
|
+
top_n: int = 5,
|
|
198
|
+
) -> list[tuple[str, int, list[int]]]:
|
|
199
|
+
"""Identify top reviewers by counting reviews across PRs.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
prs: List of PR data
|
|
203
|
+
top_n: Number of top reviewers to return
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
List of (username, review_count, pr_numbers) tuples
|
|
207
|
+
"""
|
|
208
|
+
reviewer_counts: Counter = Counter()
|
|
209
|
+
reviewer_prs: dict[str, list[int]] = {}
|
|
210
|
+
|
|
211
|
+
for pr in prs:
|
|
212
|
+
pr_number = pr.get("number")
|
|
213
|
+
if not pr_number:
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
# Fetch PR details to get reviewers
|
|
217
|
+
details = self.toolkit.execute(
|
|
218
|
+
"github_pr_details",
|
|
219
|
+
pull_number=pr_number,
|
|
220
|
+
include_diff=False,
|
|
221
|
+
include_comments=False,
|
|
222
|
+
include_reviews=True,
|
|
223
|
+
include_review_comments=False,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
if not details.success:
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
reviews = details.data.get("reviews", [])
|
|
230
|
+
if not isinstance(reviews, list):
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
seen_reviewers = set()
|
|
234
|
+
for review in reviews:
|
|
235
|
+
if not isinstance(review, dict):
|
|
236
|
+
continue
|
|
237
|
+
user = review.get("user", {})
|
|
238
|
+
if isinstance(user, dict):
|
|
239
|
+
username = user.get("login")
|
|
240
|
+
if username and username not in seen_reviewers:
|
|
241
|
+
reviewer_counts[username] += 1
|
|
242
|
+
if username not in reviewer_prs:
|
|
243
|
+
reviewer_prs[username] = []
|
|
244
|
+
reviewer_prs[username].append(pr_number)
|
|
245
|
+
seen_reviewers.add(username)
|
|
246
|
+
|
|
247
|
+
# Get top N reviewers
|
|
248
|
+
top = reviewer_counts.most_common(top_n)
|
|
249
|
+
return [(username, count, reviewer_prs.get(username, [])) for username, count in top]
|
|
250
|
+
|
|
251
|
+
def _fetch_reviewer_details(
|
|
252
|
+
self,
|
|
253
|
+
top_reviewers: list[tuple[str, int, list[int]]],
|
|
254
|
+
prs: list[dict],
|
|
255
|
+
) -> list[ReviewerData]:
|
|
256
|
+
"""Fetch detailed review data for top reviewers.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
top_reviewers: List of (username, count, pr_numbers) tuples
|
|
260
|
+
prs: List of PR data
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
List of ReviewerData objects
|
|
264
|
+
"""
|
|
265
|
+
reviewer_data = []
|
|
266
|
+
|
|
267
|
+
for username, count, pr_numbers in top_reviewers:
|
|
268
|
+
data = ReviewerData(
|
|
269
|
+
username=username,
|
|
270
|
+
review_count=count,
|
|
271
|
+
prs_reviewed=pr_numbers,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Fetch review comments for each PR they reviewed
|
|
275
|
+
for pr_number in pr_numbers[:10]: # Limit to 10 PRs per reviewer
|
|
276
|
+
details = self.toolkit.execute(
|
|
277
|
+
"github_pr_details",
|
|
278
|
+
pull_number=pr_number,
|
|
279
|
+
include_diff=False,
|
|
280
|
+
include_comments=False,
|
|
281
|
+
include_reviews=True,
|
|
282
|
+
include_review_comments=True,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
if not details.success:
|
|
286
|
+
continue
|
|
287
|
+
|
|
288
|
+
# Get their reviews
|
|
289
|
+
reviews = details.data.get("reviews", [])
|
|
290
|
+
for review in reviews:
|
|
291
|
+
if not isinstance(review, dict):
|
|
292
|
+
continue
|
|
293
|
+
user = review.get("user", {})
|
|
294
|
+
if isinstance(user, dict) and user.get("login") == username:
|
|
295
|
+
state = review.get("state")
|
|
296
|
+
if state:
|
|
297
|
+
data.review_verdicts.append(state)
|
|
298
|
+
body = review.get("body")
|
|
299
|
+
if body:
|
|
300
|
+
data.review_comments.append({
|
|
301
|
+
"type": "review",
|
|
302
|
+
"pr": pr_number,
|
|
303
|
+
"body": body,
|
|
304
|
+
"state": state,
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
# Get their inline comments
|
|
308
|
+
review_comments = details.data.get("review_comments", [])
|
|
309
|
+
for comment in review_comments:
|
|
310
|
+
if not isinstance(comment, dict):
|
|
311
|
+
continue
|
|
312
|
+
user = comment.get("user", {})
|
|
313
|
+
if isinstance(user, dict) and user.get("login") == username:
|
|
314
|
+
data.review_comments.append({
|
|
315
|
+
"type": "inline",
|
|
316
|
+
"pr": pr_number,
|
|
317
|
+
"path": comment.get("path"),
|
|
318
|
+
"line": comment.get("line") or comment.get("position"),
|
|
319
|
+
"body": comment.get("body"),
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
reviewer_data.append(data)
|
|
323
|
+
|
|
324
|
+
return reviewer_data
|
|
325
|
+
|
|
326
|
+
def _get_cross_team_contributors(self, top_n: int = 5) -> list[ContributorData]:
|
|
327
|
+
"""Query Neo4j for contributors who touch multiple communities.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
top_n: Number of contributors to return
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
List of ContributorData objects
|
|
334
|
+
"""
|
|
335
|
+
if not self.graph:
|
|
336
|
+
return []
|
|
337
|
+
|
|
338
|
+
try:
|
|
339
|
+
with self.graph.session() as session:
|
|
340
|
+
result = session.run(
|
|
341
|
+
"""
|
|
342
|
+
MATCH (c:GitCommit)-[:AUTHORED_BY]->(a:Author)
|
|
343
|
+
MATCH (c)-[:COMMIT_MODIFIES]->(f:File)
|
|
344
|
+
MATCH (f)-[:CONTAINS_CLASS|CONTAINS_FUNCTION]->(entity)
|
|
345
|
+
WHERE entity.community IS NOT NULL
|
|
346
|
+
WITH a, count(DISTINCT entity.community) as communities, count(DISTINCT c) as commits
|
|
347
|
+
WHERE communities >= 2
|
|
348
|
+
RETURN a.name as name, a.email as email, communities, commits
|
|
349
|
+
ORDER BY communities DESC, commits DESC
|
|
350
|
+
LIMIT $top_n
|
|
351
|
+
""",
|
|
352
|
+
top_n=top_n,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
contributors = []
|
|
356
|
+
for record in result:
|
|
357
|
+
contributors.append(ContributorData(
|
|
358
|
+
name=record["name"],
|
|
359
|
+
email=record["email"],
|
|
360
|
+
communities_touched=record["communities"],
|
|
361
|
+
commit_count=record["commits"],
|
|
362
|
+
))
|
|
363
|
+
|
|
364
|
+
return contributors
|
|
365
|
+
|
|
366
|
+
except Exception as e:
|
|
367
|
+
log.warning(f"Failed to query cross-team contributors: {e}")
|
|
368
|
+
return []
|
|
369
|
+
|
|
370
|
+
def _synthesize_profile(
|
|
371
|
+
self,
|
|
372
|
+
reviewer_data: list[ReviewerData],
|
|
373
|
+
cross_team: list[ContributorData],
|
|
374
|
+
pr_count: int,
|
|
375
|
+
) -> str:
|
|
376
|
+
"""Use LLM to synthesize a reviewer profile from the data.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
reviewer_data: Data about top reviewers
|
|
380
|
+
cross_team: Data about cross-team contributors
|
|
381
|
+
pr_count: Total number of PRs analyzed
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
Generated reviewer profile markdown
|
|
385
|
+
"""
|
|
386
|
+
# Build context for the LLM
|
|
387
|
+
context = {
|
|
388
|
+
"pr_count": pr_count,
|
|
389
|
+
"reviewers": [],
|
|
390
|
+
"cross_team_contributors": [],
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
for data in reviewer_data:
|
|
394
|
+
context["reviewers"].append({
|
|
395
|
+
"username": data.username,
|
|
396
|
+
"review_count": data.review_count,
|
|
397
|
+
"verdicts": dict(Counter(data.review_verdicts)),
|
|
398
|
+
"sample_comments": data.review_comments[:20], # Limit to 20 comments
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
for contrib in cross_team:
|
|
402
|
+
context["cross_team_contributors"].append({
|
|
403
|
+
"name": contrib.name,
|
|
404
|
+
"communities_touched": contrib.communities_touched,
|
|
405
|
+
"commit_count": contrib.commit_count,
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
# Call LLM
|
|
409
|
+
response = self.provider.chat(
|
|
410
|
+
[
|
|
411
|
+
{"role": "system", "content": SYNTHESIS_PROMPT},
|
|
412
|
+
{"role": "user", "content": f"Analyze this reviewer data and generate a profile:\n\n{json.dumps(context, indent=2)}"},
|
|
413
|
+
]
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
return response.content or ""
|
|
417
|
+
|
|
418
|
+
def save_template(
|
|
419
|
+
self,
|
|
420
|
+
profile: str,
|
|
421
|
+
output_path: Optional[Path] = None,
|
|
422
|
+
) -> Path:
|
|
423
|
+
"""Save the generated profile to .emdash-rules/reviewer.md.template.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
profile: Generated profile markdown
|
|
427
|
+
output_path: Optional custom output path
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
Path where the template was saved
|
|
431
|
+
"""
|
|
432
|
+
if output_path is None:
|
|
433
|
+
output_path = Path.cwd() / ".emdash-rules" / "reviewer.md.template"
|
|
434
|
+
|
|
435
|
+
# Ensure directory exists
|
|
436
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
437
|
+
|
|
438
|
+
# Write the template
|
|
439
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
440
|
+
f.write(profile)
|
|
441
|
+
|
|
442
|
+
return output_path
|
|
443
|
+
|
|
444
|
+
# Alias for API compatibility
|
|
445
|
+
def build(
|
|
446
|
+
self,
|
|
447
|
+
top_reviewers: int = 5,
|
|
448
|
+
top_contributors: int = 10,
|
|
449
|
+
max_prs: int = 50,
|
|
450
|
+
) -> dict:
|
|
451
|
+
"""Build reviewer profile (API compatibility method).
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
top_reviewers: Number of top reviewers to analyze
|
|
455
|
+
top_contributors: Number of cross-team contributors to include
|
|
456
|
+
max_prs: Maximum PRs to fetch for analysis
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
Dictionary with profile results
|
|
460
|
+
"""
|
|
461
|
+
profile = self.analyze(
|
|
462
|
+
top_n_reviewers=top_reviewers,
|
|
463
|
+
top_n_contributors=top_contributors,
|
|
464
|
+
max_prs=max_prs,
|
|
465
|
+
)
|
|
466
|
+
return {
|
|
467
|
+
"profile": profile,
|
|
468
|
+
"reviewers_analyzed": top_reviewers,
|
|
469
|
+
"contributors_analyzed": top_contributors,
|
|
470
|
+
"prs_analyzed": max_prs,
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
# Alias for backwards compatibility with API
|
|
475
|
+
ReviewerProfileBuilder = ReviewerProfileAgent
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Rules loader from .emdash/rules/*.md files.
|
|
2
|
+
|
|
3
|
+
Allows users to define custom rules and guidelines that are
|
|
4
|
+
injected into agent system prompts.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from ..utils.logger import log
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_rules(rules_dir: Optional[Path] = None) -> str:
|
|
14
|
+
"""Load rules from .emdash/rules/ directory.
|
|
15
|
+
|
|
16
|
+
Rules files are markdown that get concatenated and injected
|
|
17
|
+
into the agent's system prompt.
|
|
18
|
+
|
|
19
|
+
Example rules file:
|
|
20
|
+
|
|
21
|
+
```markdown
|
|
22
|
+
# Code Review Guidelines
|
|
23
|
+
|
|
24
|
+
- Always check for security implications
|
|
25
|
+
- Prefer composition over inheritance
|
|
26
|
+
- Document all public APIs
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
rules_dir: Directory containing rule .md files.
|
|
31
|
+
Defaults to .emdash/rules/ in cwd.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Combined rules as a string
|
|
35
|
+
"""
|
|
36
|
+
if rules_dir is None:
|
|
37
|
+
rules_dir = Path.cwd() / ".emdash" / "rules"
|
|
38
|
+
|
|
39
|
+
if not rules_dir.exists():
|
|
40
|
+
return ""
|
|
41
|
+
|
|
42
|
+
rules_parts = []
|
|
43
|
+
|
|
44
|
+
# Load all .md files in order
|
|
45
|
+
for md_file in sorted(rules_dir.glob("*.md")):
|
|
46
|
+
try:
|
|
47
|
+
content = md_file.read_text().strip()
|
|
48
|
+
if content:
|
|
49
|
+
rules_parts.append(content)
|
|
50
|
+
log.debug(f"Loaded rules from: {md_file.name}")
|
|
51
|
+
except Exception as e:
|
|
52
|
+
log.warning(f"Failed to load rules from {md_file}: {e}")
|
|
53
|
+
|
|
54
|
+
if rules_parts:
|
|
55
|
+
combined = "\n\n---\n\n".join(rules_parts)
|
|
56
|
+
log.info(f"Loaded {len(rules_parts)} rule files")
|
|
57
|
+
return combined
|
|
58
|
+
|
|
59
|
+
return ""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_rules_for_agent(
|
|
63
|
+
agent_name: str,
|
|
64
|
+
rules_dir: Optional[Path] = None,
|
|
65
|
+
) -> str:
|
|
66
|
+
"""Get rules specific to an agent.
|
|
67
|
+
|
|
68
|
+
Looks for:
|
|
69
|
+
1. Agent-specific rules in {rules_dir}/{agent_name}.md
|
|
70
|
+
2. General rules in {rules_dir}/*.md
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
agent_name: Name of the agent
|
|
74
|
+
rules_dir: Optional rules directory
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Combined rules string
|
|
78
|
+
"""
|
|
79
|
+
if rules_dir is None:
|
|
80
|
+
rules_dir = Path.cwd() / ".emdash" / "rules"
|
|
81
|
+
|
|
82
|
+
parts = []
|
|
83
|
+
|
|
84
|
+
# Load general rules first
|
|
85
|
+
general_rules = load_rules(rules_dir)
|
|
86
|
+
if general_rules:
|
|
87
|
+
parts.append(general_rules)
|
|
88
|
+
|
|
89
|
+
# Look for agent-specific rules
|
|
90
|
+
agent_rules_file = rules_dir / f"{agent_name}.md"
|
|
91
|
+
if agent_rules_file.exists():
|
|
92
|
+
try:
|
|
93
|
+
agent_rules = agent_rules_file.read_text().strip()
|
|
94
|
+
if agent_rules:
|
|
95
|
+
parts.append(f"# Agent-Specific Rules: {agent_name}\n\n{agent_rules}")
|
|
96
|
+
log.debug(f"Loaded agent-specific rules for: {agent_name}")
|
|
97
|
+
except Exception as e:
|
|
98
|
+
log.warning(f"Failed to load agent rules: {e}")
|
|
99
|
+
|
|
100
|
+
return "\n\n---\n\n".join(parts)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def format_rules_for_prompt(rules: str) -> str:
|
|
104
|
+
"""Format rules for inclusion in a system prompt.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
rules: Raw rules content
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Formatted rules section
|
|
111
|
+
"""
|
|
112
|
+
if not rules:
|
|
113
|
+
return ""
|
|
114
|
+
|
|
115
|
+
return f"""
|
|
116
|
+
## Project Guidelines
|
|
117
|
+
|
|
118
|
+
The following rules and guidelines should be followed:
|
|
119
|
+
|
|
120
|
+
{rules}
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
"""
|