Merge pull request 'feat: Add automatic PR summary generator' (#20) from feature/pr-summary-generator into dev
Reviewed-on: #20
This commit was merged in pull request #20.
This commit is contained in:
55
CLAUDE.md
55
CLAUDE.md
@@ -208,6 +208,7 @@ Key workflow pattern:
|
|||||||
Prompts are stored in `tools/ai-review/prompts/` as Markdown files:
|
Prompts are stored in `tools/ai-review/prompts/` as Markdown files:
|
||||||
|
|
||||||
- `base.md` - Base instructions for all reviews
|
- `base.md` - Base instructions for all reviews
|
||||||
|
- `pr_summary.md` - PR summary generation template
|
||||||
- `issue_triage.md` - Issue classification template
|
- `issue_triage.md` - Issue classification template
|
||||||
- `issue_response.md` - Issue response template
|
- `issue_response.md` - Issue response template
|
||||||
|
|
||||||
@@ -407,6 +408,59 @@ pytest tests/test_ai_review.py::TestSecurityScanner -v
|
|||||||
|
|
||||||
## Common Development Tasks
|
## Common Development Tasks
|
||||||
|
|
||||||
|
### PR Summary Generation
|
||||||
|
|
||||||
|
The PR summary feature automatically generates comprehensive summaries for pull requests.
|
||||||
|
|
||||||
|
**Key Features:**
|
||||||
|
- Auto-generates summary for PRs with empty descriptions
|
||||||
|
- Can be manually triggered with `@codebot summarize` in PR comments
|
||||||
|
- Analyzes diff to extract key changes, files affected, and impact
|
||||||
|
- Categorizes change type (Feature/Bugfix/Refactor/Documentation/Testing)
|
||||||
|
- Posts as comment or updates PR description (configurable)
|
||||||
|
|
||||||
|
**Implementation Details:**
|
||||||
|
|
||||||
|
1. **Auto-Summary on PR Open** - `PRAgent.execute()`:
|
||||||
|
- Checks if PR body is empty and `auto_summary.enabled` is true
|
||||||
|
- Calls `_generate_pr_summary()` automatically
|
||||||
|
- Continues with normal PR review after posting summary
|
||||||
|
|
||||||
|
2. **Manual Trigger** - `@codebot summarize` in PR comments:
|
||||||
|
- `PRAgent.can_handle()` detects `summarize` command in PR comments
|
||||||
|
- Routes to `_handle_summarize_command()`
|
||||||
|
- Generates and posts summary on demand
|
||||||
|
|
||||||
|
3. **Summary Generation** - `_generate_pr_summary()`:
|
||||||
|
- Fetches PR diff using `_get_diff()`
|
||||||
|
- Loads `prompts/pr_summary.md` template
|
||||||
|
- Calls LLM with diff to analyze changes
|
||||||
|
- Returns structured JSON with summary data
|
||||||
|
- Formats using `_format_pr_summary()`
|
||||||
|
- Posts as comment or updates description based on config
|
||||||
|
|
||||||
|
4. **Configuration** - `config.yml`:
|
||||||
|
```yaml
|
||||||
|
agents:
|
||||||
|
pr:
|
||||||
|
auto_summary:
|
||||||
|
enabled: true # Auto-generate for empty PRs
|
||||||
|
post_as_comment: true # true = comment, false = update description
|
||||||
|
```
|
||||||
|
|
||||||
|
**Summary Structure:**
|
||||||
|
- Brief 2-3 sentence overview
|
||||||
|
- Change type categorization (Feature/Bugfix/Refactor/etc)
|
||||||
|
- Key changes (Added/Modified/Removed)
|
||||||
|
- Files affected with descriptions
|
||||||
|
- Impact assessment (scope: small/medium/large)
|
||||||
|
|
||||||
|
**Common Use Cases:**
|
||||||
|
- Developers who forget to write PR descriptions
|
||||||
|
- Quick understanding of complex changes
|
||||||
|
- Standardized documentation format
|
||||||
|
- Pre-review context for reviewers
|
||||||
|
|
||||||
### Review-Again Command Implementation
|
### Review-Again Command Implementation
|
||||||
|
|
||||||
The `@codebot review-again` command allows manual re-triggering of PR reviews without new commits.
|
The `@codebot review-again` command allows manual re-triggering of PR reviews without new commits.
|
||||||
@@ -463,6 +517,7 @@ Example commands:
|
|||||||
- `@codebot triage` - Full issue triage with labeling
|
- `@codebot triage` - Full issue triage with labeling
|
||||||
- `@codebot explain` - Explain the issue
|
- `@codebot explain` - Explain the issue
|
||||||
- `@codebot suggest` - Suggest solutions
|
- `@codebot suggest` - Suggest solutions
|
||||||
|
- `@codebot summarize` - Generate PR summary or issue summary (works on both)
|
||||||
- `@codebot setup-labels` - Automatic label setup (built-in, not in config)
|
- `@codebot setup-labels` - Automatic label setup (built-in, not in config)
|
||||||
- `@codebot review-again` - Re-run PR review without new commits (PR comments only)
|
- `@codebot review-again` - Re-run PR review without new commits (PR comments only)
|
||||||
|
|
||||||
|
|||||||
46
README.md
46
README.md
@@ -9,9 +9,10 @@ Enterprise-grade AI code review system for **Gitea** with automated PR review, i
|
|||||||
| Feature | Description |
|
| Feature | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| **PR Review** | Inline comments, security scanning, severity-based CI failure |
|
| **PR Review** | Inline comments, security scanning, severity-based CI failure |
|
||||||
|
| **PR Summaries** | Auto-generate comprehensive PR summaries with change analysis and impact assessment |
|
||||||
| **Issue Triage** | On-demand classification, labeling, priority assignment via `@codebot triage` |
|
| **Issue Triage** | On-demand classification, labeling, priority assignment via `@codebot triage` |
|
||||||
| **Chat** | Interactive AI chat with codebase search and web search tools |
|
| **Chat** | Interactive AI chat with codebase search and web search tools |
|
||||||
| **@codebot Commands** | `@codebot summarize`, `explain`, `suggest`, `triage` in issue comments |
|
| **@codebot Commands** | `@codebot summarize`, `explain`, `suggest`, `triage`, `review-again` in comments |
|
||||||
| **Codebase Analysis** | Health scores, tech debt tracking, weekly reports |
|
| **Codebase Analysis** | Health scores, tech debt tracking, weekly reports |
|
||||||
| **Security Scanner** | 17 OWASP-aligned rules for vulnerability detection |
|
| **Security Scanner** | 17 OWASP-aligned rules for vulnerability detection |
|
||||||
| **Enterprise Ready** | Audit logging, metrics, Prometheus export |
|
| **Enterprise Ready** | Audit logging, metrics, Prometheus export |
|
||||||
@@ -189,8 +190,51 @@ In any PR comment:
|
|||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
|
| `@codebot summarize` | Generate a comprehensive PR summary with changes, files affected, and impact |
|
||||||
| `@codebot review-again` | Re-run AI code review on current PR state without new commits |
|
| `@codebot review-again` | Re-run AI code review on current PR state without new commits |
|
||||||
|
|
||||||
|
#### PR Summary (`@codebot summarize`)
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- 📋 Generates structured summary of PR changes
|
||||||
|
- ✨ Categorizes change type (Feature/Bugfix/Refactor/Documentation/Testing)
|
||||||
|
- 📝 Lists what was added, modified, and removed
|
||||||
|
- 📁 Shows all affected files with descriptions
|
||||||
|
- 🎯 Assesses impact scope (small/medium/large)
|
||||||
|
- 🤖 Automatically generates on PRs with empty descriptions
|
||||||
|
|
||||||
|
**When to use:**
|
||||||
|
- When a PR lacks a description
|
||||||
|
- To quickly understand what changed
|
||||||
|
- For standardized PR documentation
|
||||||
|
- Before reviewing complex PRs
|
||||||
|
|
||||||
|
**Example output:**
|
||||||
|
```markdown
|
||||||
|
## 📋 Pull Request Summary
|
||||||
|
This PR implements automatic PR summary generation...
|
||||||
|
|
||||||
|
**Type:** ✨ Feature
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
✅ Added:
|
||||||
|
- PR summary generation in PRAgent
|
||||||
|
- Auto-summary for empty PR descriptions
|
||||||
|
|
||||||
|
📝 Modified:
|
||||||
|
- Updated config.yml with new settings
|
||||||
|
|
||||||
|
## Files Affected
|
||||||
|
- ➕ `tools/ai-review/prompts/pr_summary.md` - New prompt template
|
||||||
|
- 📝 `tools/ai-review/agents/pr_agent.py` - Added summary methods
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
🟡 **Scope:** Medium
|
||||||
|
Adds new feature without affecting existing functionality
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Review Again (`@codebot review-again`)
|
||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
- ✅ Shows diff from previous review (resolved/new/changed issues)
|
- ✅ Shows diff from previous review (resolved/new/changed issues)
|
||||||
- 🏷️ Updates labels based on new severity
|
- 🏷️ Updates labels based on new severity
|
||||||
|
|||||||
@@ -588,5 +588,242 @@ class TestLabelSetup:
|
|||||||
assert "Kind/Bug" in config["aliases"]
|
assert "Kind/Bug" in config["aliases"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestPRSummaryGeneration:
|
||||||
|
"""Test PR summary generation functionality."""
|
||||||
|
|
||||||
|
def test_pr_summary_prompt_exists(self):
|
||||||
|
"""Verify pr_summary.md prompt file exists."""
|
||||||
|
prompt_path = os.path.join(
|
||||||
|
os.path.dirname(__file__),
|
||||||
|
"..",
|
||||||
|
"tools",
|
||||||
|
"ai-review",
|
||||||
|
"prompts",
|
||||||
|
"pr_summary.md",
|
||||||
|
)
|
||||||
|
assert os.path.exists(prompt_path), "pr_summary.md prompt file not found"
|
||||||
|
|
||||||
|
def test_pr_summary_prompt_formatting(self):
|
||||||
|
"""Test that pr_summary.md can be loaded without errors."""
|
||||||
|
prompt_path = os.path.join(
|
||||||
|
os.path.dirname(__file__),
|
||||||
|
"..",
|
||||||
|
"tools",
|
||||||
|
"ai-review",
|
||||||
|
"prompts",
|
||||||
|
"pr_summary.md",
|
||||||
|
)
|
||||||
|
with open(prompt_path) as f:
|
||||||
|
prompt = f.read()
|
||||||
|
|
||||||
|
# Check for key elements
|
||||||
|
assert "summary" in prompt.lower()
|
||||||
|
assert "change_type" in prompt.lower()
|
||||||
|
assert "files_affected" in prompt.lower()
|
||||||
|
assert "impact" in prompt.lower()
|
||||||
|
assert "JSON" in prompt
|
||||||
|
|
||||||
|
# Verify JSON examples use double curly braces (escaped)
|
||||||
|
# Should not raise KeyError when formatted with empty string
|
||||||
|
try:
|
||||||
|
formatted = prompt.format()
|
||||||
|
except KeyError as e:
|
||||||
|
pytest.fail(f"Prompt has unescaped placeholders: {e}")
|
||||||
|
|
||||||
|
def test_pr_agent_has_summary_marker(self):
|
||||||
|
"""Verify PRAgent has PR_SUMMARY_MARKER constant."""
|
||||||
|
from agents.pr_agent import PRAgent
|
||||||
|
|
||||||
|
assert hasattr(PRAgent, "PR_SUMMARY_MARKER")
|
||||||
|
assert PRAgent.PR_SUMMARY_MARKER == "<!-- AI_PR_SUMMARY -->"
|
||||||
|
|
||||||
|
def test_pr_agent_can_handle_summarize_command(self):
|
||||||
|
"""Test that PRAgent can handle @codebot summarize in PR comments."""
|
||||||
|
from agents.pr_agent import PRAgent
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"agents": {
|
||||||
|
"pr": {
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"interaction": {
|
||||||
|
"mention_prefix": "@codebot",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
agent = PRAgent(config=config)
|
||||||
|
|
||||||
|
# Test summarize command in PR comment
|
||||||
|
event_data = {
|
||||||
|
"action": "created",
|
||||||
|
"issue": {
|
||||||
|
"number": 123,
|
||||||
|
"pull_request": {}, # Indicates this is a PR
|
||||||
|
},
|
||||||
|
"comment": {
|
||||||
|
"body": "@codebot summarize this PR please",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert agent.can_handle("issue_comment", event_data) is True
|
||||||
|
|
||||||
|
def test_pr_agent_can_handle_summarize_case_insensitive(self):
|
||||||
|
"""Test that summarize command is case-insensitive."""
|
||||||
|
from agents.pr_agent import PRAgent
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"agents": {
|
||||||
|
"pr": {
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"interaction": {
|
||||||
|
"mention_prefix": "@codebot",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
agent = PRAgent(config=config)
|
||||||
|
|
||||||
|
# Test various casings
|
||||||
|
for body in [
|
||||||
|
"@codebot SUMMARIZE",
|
||||||
|
"@codebot Summarize",
|
||||||
|
"@codebot SuMmArIzE",
|
||||||
|
]:
|
||||||
|
event_data = {
|
||||||
|
"action": "created",
|
||||||
|
"issue": {
|
||||||
|
"number": 123,
|
||||||
|
"pull_request": {},
|
||||||
|
},
|
||||||
|
"comment": {"body": body},
|
||||||
|
}
|
||||||
|
assert agent.can_handle("issue_comment", event_data) is True
|
||||||
|
|
||||||
|
def test_pr_agent_ignores_summarize_on_non_pr(self):
|
||||||
|
"""Test that summarize command is ignored on regular issues."""
|
||||||
|
from agents.pr_agent import PRAgent
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"agents": {
|
||||||
|
"pr": {
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"interaction": {
|
||||||
|
"mention_prefix": "@codebot",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
agent = PRAgent(config=config)
|
||||||
|
|
||||||
|
# Regular issue (no pull_request field)
|
||||||
|
event_data = {
|
||||||
|
"action": "created",
|
||||||
|
"issue": {
|
||||||
|
"number": 123,
|
||||||
|
# No pull_request field
|
||||||
|
},
|
||||||
|
"comment": {
|
||||||
|
"body": "@codebot summarize",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert agent.can_handle("issue_comment", event_data) is False
|
||||||
|
|
||||||
|
def test_format_pr_summary_structure(self):
|
||||||
|
"""Test _format_pr_summary generates correct markdown structure."""
|
||||||
|
from agents.pr_agent import PRAgent
|
||||||
|
|
||||||
|
agent = PRAgent(config={})
|
||||||
|
|
||||||
|
summary_data = {
|
||||||
|
"summary": "This PR adds a new feature",
|
||||||
|
"change_type": "Feature",
|
||||||
|
"key_changes": {
|
||||||
|
"added": ["New authentication module", "User login endpoint"],
|
||||||
|
"modified": ["Updated config file"],
|
||||||
|
"removed": ["Deprecated legacy auth"],
|
||||||
|
},
|
||||||
|
"files_affected": [
|
||||||
|
{
|
||||||
|
"path": "src/auth.py",
|
||||||
|
"description": "New authentication module",
|
||||||
|
"change_type": "added",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "config.yml",
|
||||||
|
"description": "Added auth settings",
|
||||||
|
"change_type": "modified",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"impact": {
|
||||||
|
"scope": "medium",
|
||||||
|
"description": "Adds authentication without breaking existing features",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result = agent._format_pr_summary(summary_data)
|
||||||
|
|
||||||
|
# Verify structure
|
||||||
|
assert "## 📋 Pull Request Summary" in result
|
||||||
|
assert "This PR adds a new feature" in result
|
||||||
|
assert "**Type:** ✨ Feature" in result
|
||||||
|
assert "## Changes" in result
|
||||||
|
assert "**✅ Added:**" in result
|
||||||
|
assert "**📝 Modified:**" in result
|
||||||
|
assert "**❌ Removed:**" in result
|
||||||
|
assert "## Files Affected" in result
|
||||||
|
assert "➕ `src/auth.py`" in result
|
||||||
|
assert "📝 `config.yml`" in result
|
||||||
|
assert "## Impact" in result
|
||||||
|
assert "🟡 **Scope:** Medium" in result
|
||||||
|
|
||||||
|
def test_format_pr_summary_change_types(self):
|
||||||
|
"""Test that all change types have correct emojis."""
|
||||||
|
from agents.pr_agent import PRAgent
|
||||||
|
|
||||||
|
agent = PRAgent(config={})
|
||||||
|
|
||||||
|
change_types = {
|
||||||
|
"Feature": "✨",
|
||||||
|
"Bugfix": "🐛",
|
||||||
|
"Refactor": "♻️",
|
||||||
|
"Documentation": "📚",
|
||||||
|
"Testing": "🧪",
|
||||||
|
"Mixed": "🔀",
|
||||||
|
}
|
||||||
|
|
||||||
|
for change_type, expected_emoji in change_types.items():
|
||||||
|
summary_data = {
|
||||||
|
"summary": "Test",
|
||||||
|
"change_type": change_type,
|
||||||
|
"key_changes": {},
|
||||||
|
"files_affected": [],
|
||||||
|
"impact": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
result = agent._format_pr_summary(summary_data)
|
||||||
|
assert f"**Type:** {expected_emoji} {change_type}" in result
|
||||||
|
|
||||||
|
def test_config_has_auto_summary_settings(self):
|
||||||
|
"""Verify config.yml has auto_summary configuration."""
|
||||||
|
config_path = os.path.join(
|
||||||
|
os.path.dirname(__file__), "..", "tools", "ai-review", "config.yml"
|
||||||
|
)
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
with open(config_path) as f:
|
||||||
|
config = yaml.safe_load(f)
|
||||||
|
|
||||||
|
assert "agents" in config
|
||||||
|
assert "pr" in config["agents"]
|
||||||
|
assert "auto_summary" in config["agents"]["pr"]
|
||||||
|
assert "enabled" in config["agents"]["pr"]["auto_summary"]
|
||||||
|
assert "post_as_comment" in config["agents"]["pr"]["auto_summary"]
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
pytest.main([__file__, "-v"])
|
pytest.main([__file__, "-v"])
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class PRAgent(BaseAgent):
|
|||||||
|
|
||||||
# Marker specific to PR reviews
|
# Marker specific to PR reviews
|
||||||
PR_AI_MARKER = "<!-- AI_PR_REVIEW -->"
|
PR_AI_MARKER = "<!-- AI_PR_REVIEW -->"
|
||||||
|
PR_SUMMARY_MARKER = "<!-- AI_PR_SUMMARY -->"
|
||||||
|
|
||||||
def _get_label_config(self, category: str, key: str) -> dict:
|
def _get_label_config(self, category: str, key: str) -> dict:
|
||||||
"""Get full label configuration from config.
|
"""Get full label configuration from config.
|
||||||
@@ -83,7 +84,7 @@ class PRAgent(BaseAgent):
|
|||||||
allowed_events = agent_config.get("events", ["opened", "synchronize"])
|
allowed_events = agent_config.get("events", ["opened", "synchronize"])
|
||||||
return action in allowed_events
|
return action in allowed_events
|
||||||
|
|
||||||
# Handle issue comments on PRs (for review-again command)
|
# Handle issue comments on PRs (for review-again and summarize commands)
|
||||||
if event_type == "issue_comment":
|
if event_type == "issue_comment":
|
||||||
action = event_data.get("action", "")
|
action = event_data.get("action", "")
|
||||||
if action == "created":
|
if action == "created":
|
||||||
@@ -91,20 +92,28 @@ class PRAgent(BaseAgent):
|
|||||||
mention_prefix = self.config.get("interaction", {}).get(
|
mention_prefix = self.config.get("interaction", {}).get(
|
||||||
"mention_prefix", "@codebot"
|
"mention_prefix", "@codebot"
|
||||||
)
|
)
|
||||||
# Only handle if this is a PR and contains review-again command
|
# Only handle if this is a PR
|
||||||
issue = event_data.get("issue", {})
|
issue = event_data.get("issue", {})
|
||||||
is_pr = issue.get("pull_request") is not None
|
is_pr = issue.get("pull_request") is not None
|
||||||
has_review_again = (
|
has_review_again = (
|
||||||
f"{mention_prefix} review-again" in comment_body.lower()
|
f"{mention_prefix} review-again" in comment_body.lower()
|
||||||
)
|
)
|
||||||
return is_pr and has_review_again
|
has_summarize = f"{mention_prefix} summarize" in comment_body.lower()
|
||||||
|
return is_pr and (has_review_again or has_summarize)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def execute(self, context: AgentContext) -> AgentResult:
|
def execute(self, context: AgentContext) -> AgentResult:
|
||||||
"""Execute the PR review agent."""
|
"""Execute the PR review agent."""
|
||||||
# Check if this is a review-again command
|
# Check if this is a comment-based command
|
||||||
if context.event_type == "issue_comment":
|
if context.event_type == "issue_comment":
|
||||||
|
comment_body = context.event_data.get("comment", {}).get("body", "")
|
||||||
|
mention_prefix = self.config.get("interaction", {}).get(
|
||||||
|
"mention_prefix", "@codebot"
|
||||||
|
)
|
||||||
|
if f"{mention_prefix} summarize" in comment_body.lower():
|
||||||
|
return self._handle_summarize_command(context)
|
||||||
|
elif f"{mention_prefix} review-again" in comment_body.lower():
|
||||||
return self._handle_review_again(context)
|
return self._handle_review_again(context)
|
||||||
|
|
||||||
pr = context.event_data.get("pull_request", {})
|
pr = context.event_data.get("pull_request", {})
|
||||||
@@ -114,6 +123,24 @@ class PRAgent(BaseAgent):
|
|||||||
|
|
||||||
actions_taken = []
|
actions_taken = []
|
||||||
|
|
||||||
|
# Check if PR has empty description and auto-summary is enabled
|
||||||
|
pr_body = pr.get("body", "").strip()
|
||||||
|
agent_config = self.config.get("agents", {}).get("pr", {})
|
||||||
|
auto_summary_enabled = agent_config.get("auto_summary", {}).get("enabled", True)
|
||||||
|
|
||||||
|
if (
|
||||||
|
not pr_body
|
||||||
|
and auto_summary_enabled
|
||||||
|
and context.event_data.get("action") == "opened"
|
||||||
|
):
|
||||||
|
# Generate and post summary for empty PR descriptions
|
||||||
|
summary_result = self._generate_pr_summary(
|
||||||
|
context.owner, context.repo, pr_number
|
||||||
|
)
|
||||||
|
if summary_result:
|
||||||
|
actions_taken.append("Generated PR summary for empty description")
|
||||||
|
# Don't return here - continue with regular review
|
||||||
|
|
||||||
# Step 1: Get PR diff
|
# Step 1: Get PR diff
|
||||||
diff = self._get_diff(context.owner, context.repo, pr_number)
|
diff = self._get_diff(context.owner, context.repo, pr_number)
|
||||||
if not diff.strip():
|
if not diff.strip():
|
||||||
@@ -791,3 +818,206 @@ class PRAgent(BaseAgent):
|
|||||||
self.logger.warning(f"Failed to add labels: {e}")
|
self.logger.warning(f"Failed to add labels: {e}")
|
||||||
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def _generate_pr_summary(self, owner: str, repo: str, pr_number: int) -> bool:
|
||||||
|
"""Generate and post a summary for a PR.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Repository owner
|
||||||
|
repo: Repository name
|
||||||
|
pr_number: PR number
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if summary was generated successfully, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Get PR diff
|
||||||
|
diff = self._get_diff(owner, repo, pr_number)
|
||||||
|
if not diff.strip():
|
||||||
|
self.logger.info(f"No diff to summarize for PR #{pr_number}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Load summary prompt
|
||||||
|
prompt_template = self.load_prompt("pr_summary")
|
||||||
|
prompt = f"{prompt_template}\n{diff}"
|
||||||
|
|
||||||
|
# Call LLM to generate summary
|
||||||
|
result = self.call_llm_json(prompt)
|
||||||
|
|
||||||
|
# Format the summary comment
|
||||||
|
summary_comment = self._format_pr_summary(result)
|
||||||
|
|
||||||
|
# Post as first comment (or update PR description based on config)
|
||||||
|
agent_config = self.config.get("agents", {}).get("pr", {})
|
||||||
|
auto_summary_config = agent_config.get("auto_summary", {})
|
||||||
|
post_as_comment = auto_summary_config.get("post_as_comment", True)
|
||||||
|
|
||||||
|
if post_as_comment:
|
||||||
|
# Post as comment
|
||||||
|
self.gitea.create_issue_comment(owner, repo, pr_number, summary_comment)
|
||||||
|
self.logger.info(f"Posted PR summary as comment for PR #{pr_number}")
|
||||||
|
else:
|
||||||
|
# Update PR description (requires different API call)
|
||||||
|
# Note: Gitea API may not support updating PR description
|
||||||
|
# In that case, fall back to posting as comment
|
||||||
|
try:
|
||||||
|
self.gitea.update_pull_request(
|
||||||
|
owner, repo, pr_number, body=summary_comment
|
||||||
|
)
|
||||||
|
self.logger.info(
|
||||||
|
f"Updated PR description with summary for PR #{pr_number}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(
|
||||||
|
f"Could not update PR description, posting as comment: {e}"
|
||||||
|
)
|
||||||
|
self.gitea.create_issue_comment(
|
||||||
|
owner, repo, pr_number, summary_comment
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Failed to generate PR summary: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _format_pr_summary(self, summary_data: dict) -> str:
|
||||||
|
"""Format the PR summary data into a readable comment.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
summary_data: JSON data from LLM containing summary information
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Formatted markdown comment
|
||||||
|
"""
|
||||||
|
lines = [
|
||||||
|
self.AI_DISCLAIMER,
|
||||||
|
"",
|
||||||
|
"## 📋 Pull Request Summary",
|
||||||
|
"",
|
||||||
|
summary_data.get("summary", "Summary unavailable"),
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Change type
|
||||||
|
change_type = summary_data.get("change_type", "Unknown")
|
||||||
|
change_type_emoji = {
|
||||||
|
"Feature": "✨",
|
||||||
|
"Bugfix": "🐛",
|
||||||
|
"Refactor": "♻️",
|
||||||
|
"Documentation": "📚",
|
||||||
|
"Testing": "🧪",
|
||||||
|
"Mixed": "🔀",
|
||||||
|
}
|
||||||
|
emoji = change_type_emoji.get(change_type, "🔀")
|
||||||
|
lines.append(f"**Type:** {emoji} {change_type}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# Key changes
|
||||||
|
key_changes = summary_data.get("key_changes", {})
|
||||||
|
if key_changes:
|
||||||
|
lines.append("## Changes")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
added = key_changes.get("added", [])
|
||||||
|
if added:
|
||||||
|
lines.append("**✅ Added:**")
|
||||||
|
for item in added:
|
||||||
|
lines.append(f"- {item}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
modified = key_changes.get("modified", [])
|
||||||
|
if modified:
|
||||||
|
lines.append("**📝 Modified:**")
|
||||||
|
for item in modified:
|
||||||
|
lines.append(f"- {item}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
removed = key_changes.get("removed", [])
|
||||||
|
if removed:
|
||||||
|
lines.append("**❌ Removed:**")
|
||||||
|
for item in removed:
|
||||||
|
lines.append(f"- {item}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# Files affected
|
||||||
|
files = summary_data.get("files_affected", [])
|
||||||
|
if files:
|
||||||
|
lines.append("## Files Affected")
|
||||||
|
lines.append("")
|
||||||
|
for file_info in files[:10]: # Limit to first 10 files
|
||||||
|
path = file_info.get("path", "unknown")
|
||||||
|
desc = file_info.get("description", "")
|
||||||
|
change_type = file_info.get("change_type", "modified")
|
||||||
|
|
||||||
|
type_icon = {"added": "➕", "modified": "📝", "deleted": "➖"}
|
||||||
|
icon = type_icon.get(change_type, "📝")
|
||||||
|
|
||||||
|
lines.append(f"- {icon} `{path}` - {desc}")
|
||||||
|
|
||||||
|
if len(files) > 10:
|
||||||
|
lines.append(f"- ... and {len(files) - 10} more files")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# Impact assessment
|
||||||
|
impact = summary_data.get("impact", {})
|
||||||
|
if impact:
|
||||||
|
scope = impact.get("scope", "unknown")
|
||||||
|
description = impact.get("description", "")
|
||||||
|
|
||||||
|
scope_emoji = {"small": "🟢", "medium": "🟡", "large": "🔴"}
|
||||||
|
emoji = scope_emoji.get(scope, "⚪")
|
||||||
|
|
||||||
|
lines.append("## Impact")
|
||||||
|
lines.append(f"{emoji} **Scope:** {scope.capitalize()}")
|
||||||
|
lines.append(f"{description}")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def _handle_summarize_command(self, context: AgentContext) -> AgentResult:
|
||||||
|
"""Handle @codebot summarize command from PR comments.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context: Agent context with event data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
AgentResult with success status and actions taken
|
||||||
|
"""
|
||||||
|
issue = context.event_data.get("issue", {})
|
||||||
|
pr_number = issue.get("number")
|
||||||
|
comment_author = (
|
||||||
|
context.event_data.get("comment", {}).get("user", {}).get("login", "user")
|
||||||
|
)
|
||||||
|
|
||||||
|
self.logger.info(f"Generating PR summary for PR #{pr_number} at user request")
|
||||||
|
|
||||||
|
# Generate and post summary
|
||||||
|
success = self._generate_pr_summary(context.owner, context.repo, pr_number)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return AgentResult(
|
||||||
|
success=True,
|
||||||
|
message=f"Generated PR summary for PR #{pr_number}",
|
||||||
|
actions_taken=["Posted PR summary comment"],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Post error message
|
||||||
|
error_msg = (
|
||||||
|
f"@{comment_author}\n\n"
|
||||||
|
f"{self.AI_DISCLAIMER}\n\n"
|
||||||
|
"**⚠️ Summary Generation Failed**\n\n"
|
||||||
|
"I was unable to generate a summary for this PR. "
|
||||||
|
"This could be because:\n"
|
||||||
|
"- The PR has no changes\n"
|
||||||
|
"- There was an error accessing the diff\n"
|
||||||
|
"- The LLM service is unavailable"
|
||||||
|
)
|
||||||
|
self.gitea.create_issue_comment(
|
||||||
|
context.owner, context.repo, pr_number, error_msg
|
||||||
|
)
|
||||||
|
|
||||||
|
return AgentResult(
|
||||||
|
success=False,
|
||||||
|
message=f"Failed to generate PR summary for PR #{pr_number}",
|
||||||
|
error="Summary generation failed",
|
||||||
|
)
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ agents:
|
|||||||
events:
|
events:
|
||||||
- opened
|
- opened
|
||||||
- synchronize
|
- synchronize
|
||||||
|
auto_summary:
|
||||||
|
enabled: true # Auto-generate summary for PRs with empty descriptions
|
||||||
|
post_as_comment: true # true = post as comment, false = update PR description
|
||||||
codebase:
|
codebase:
|
||||||
enabled: true
|
enabled: true
|
||||||
schedule: "0 0 * * 0" # Weekly on Sunday
|
schedule: "0 0 * * 0" # Weekly on Sunday
|
||||||
@@ -63,7 +66,7 @@ interaction:
|
|||||||
- explain
|
- explain
|
||||||
- suggest
|
- suggest
|
||||||
- security
|
- security
|
||||||
- summarize
|
- summarize # Generate PR summary (works on both issues and PRs)
|
||||||
- triage
|
- triage
|
||||||
- review-again
|
- review-again
|
||||||
|
|
||||||
|
|||||||
67
tools/ai-review/prompts/pr_summary.md
Normal file
67
tools/ai-review/prompts/pr_summary.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
You are an experienced senior software engineer analyzing a pull request diff to generate a comprehensive, informative summary.
|
||||||
|
|
||||||
|
Your goal is to create a **clear, structured summary** that helps reviewers quickly understand:
|
||||||
|
- What changes were made
|
||||||
|
- Why these changes matter
|
||||||
|
- Which files and components are affected
|
||||||
|
- The type of change (feature/bugfix/refactor/documentation)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
Analyze the PR diff and generate a summary that includes:
|
||||||
|
|
||||||
|
1. **Brief Overview**: 2-3 sentence summary of the changes
|
||||||
|
2. **Key Changes**: Bullet points of the most important modifications
|
||||||
|
- What was added
|
||||||
|
- What was modified
|
||||||
|
- What was removed (if applicable)
|
||||||
|
3. **Files Affected**: List of changed files with brief descriptions
|
||||||
|
4. **Change Type**: Classify as Feature, Bugfix, Refactor, Documentation, Testing, or Mixed
|
||||||
|
5. **Impact Assessment**: Brief note on the scope and potential impact
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
Return a JSON object with this structure:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{{{{
|
||||||
|
"summary": "Brief 2-3 sentence overview of what this PR accomplishes",
|
||||||
|
"change_type": "Feature" | "Bugfix" | "Refactor" | "Documentation" | "Testing" | "Mixed",
|
||||||
|
"key_changes": {{{{
|
||||||
|
"added": ["List of new features/files/functionality added"],
|
||||||
|
"modified": ["List of existing components that were changed"],
|
||||||
|
"removed": ["List of removed features/files (if any)"]
|
||||||
|
}}}},
|
||||||
|
"files_affected": [
|
||||||
|
{{{{
|
||||||
|
"path": "path/to/file.py",
|
||||||
|
"description": "Brief description of changes in this file",
|
||||||
|
"change_type": "added" | "modified" | "deleted"
|
||||||
|
}}}}
|
||||||
|
],
|
||||||
|
"impact": {{{{
|
||||||
|
"scope": "small" | "medium" | "large",
|
||||||
|
"description": "Brief assessment of the impact and scope of changes"
|
||||||
|
}}}}
|
||||||
|
}}}}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
1. **Be concise**: Keep descriptions clear and to the point
|
||||||
|
2. **Focus on intent**: Explain *what* and *why*, not just *how*
|
||||||
|
3. **Identify patterns**: Group related changes together
|
||||||
|
4. **Highlight significance**: Emphasize important architectural or behavioral changes
|
||||||
|
5. **Be objective**: Base analysis purely on the code changes
|
||||||
|
6. **Output only JSON**: No additional text before or after the JSON object
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Diff to Analyze
|
||||||
|
|
||||||
Reference in New Issue
Block a user