feat: Add automatic PR summary generator
All checks were successful
Enterprise AI Code Review / ai-review (pull_request) Successful in 7s
All checks were successful
Enterprise AI Code Review / ai-review (pull_request) Successful in 7s
Implements automatic PR summary generation feature that analyzes pull request diffs and generates comprehensive summaries. Features: - Auto-generates summaries for PRs with empty descriptions - Manual trigger via @codebot summarize command in PR comments - Structured output with change type, files affected, and impact assessment - Configurable (enable/disable, comment vs description update) Implementation: - Added pr_summary.md prompt template for LLM - Extended PRAgent with summary generation methods - Added auto_summary configuration in config.yml - Comprehensive test suite with 10 new tests - Updated documentation in README.md and CLAUDE.md Usage: - Automatic: Opens PR with no description → auto-generates summary - Manual: Comment '@codebot summarize' on any PR Related: Issue #2 - Milestone 2 feature delivery
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:
|
||||
|
||||
- `base.md` - Base instructions for all reviews
|
||||
- `pr_summary.md` - PR summary generation template
|
||||
- `issue_triage.md` - Issue classification template
|
||||
- `issue_response.md` - Issue response template
|
||||
|
||||
@@ -407,6 +408,59 @@ pytest tests/test_ai_review.py::TestSecurityScanner -v
|
||||
|
||||
## 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
|
||||
|
||||
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 explain` - Explain the issue
|
||||
- `@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 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 |
|
||||
|---------|-------------|
|
||||
| **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` |
|
||||
| **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 |
|
||||
| **Security Scanner** | 17 OWASP-aligned rules for vulnerability detection |
|
||||
| **Enterprise Ready** | Audit logging, metrics, Prometheus export |
|
||||
@@ -189,8 +190,51 @@ In any PR comment:
|
||||
|
||||
| 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 |
|
||||
|
||||
#### 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:**
|
||||
- ✅ Shows diff from previous review (resolved/new/changed issues)
|
||||
- 🏷️ Updates labels based on new severity
|
||||
|
||||
@@ -588,5 +588,242 @@ class TestLabelSetup:
|
||||
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__":
|
||||
pytest.main([__file__, "-v"])
|
||||
|
||||
@@ -39,6 +39,7 @@ class PRAgent(BaseAgent):
|
||||
|
||||
# Marker specific to PR reviews
|
||||
PR_AI_MARKER = "<!-- AI_PR_REVIEW -->"
|
||||
PR_SUMMARY_MARKER = "<!-- AI_PR_SUMMARY -->"
|
||||
|
||||
def _get_label_config(self, category: str, key: str) -> dict:
|
||||
"""Get full label configuration from config.
|
||||
@@ -83,7 +84,7 @@ class PRAgent(BaseAgent):
|
||||
allowed_events = agent_config.get("events", ["opened", "synchronize"])
|
||||
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":
|
||||
action = event_data.get("action", "")
|
||||
if action == "created":
|
||||
@@ -91,20 +92,28 @@ class PRAgent(BaseAgent):
|
||||
mention_prefix = self.config.get("interaction", {}).get(
|
||||
"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", {})
|
||||
is_pr = issue.get("pull_request") is not None
|
||||
has_review_again = (
|
||||
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
|
||||
|
||||
def execute(self, context: AgentContext) -> AgentResult:
|
||||
"""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":
|
||||
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)
|
||||
|
||||
pr = context.event_data.get("pull_request", {})
|
||||
@@ -114,6 +123,24 @@ class PRAgent(BaseAgent):
|
||||
|
||||
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
|
||||
diff = self._get_diff(context.owner, context.repo, pr_number)
|
||||
if not diff.strip():
|
||||
@@ -791,3 +818,206 @@ class PRAgent(BaseAgent):
|
||||
self.logger.warning(f"Failed to add labels: {e}")
|
||||
|
||||
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:
|
||||
- opened
|
||||
- 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:
|
||||
enabled: true
|
||||
schedule: "0 0 * * 0" # Weekly on Sunday
|
||||
@@ -63,7 +66,7 @@ interaction:
|
||||
- explain
|
||||
- suggest
|
||||
- security
|
||||
- summarize
|
||||
- summarize # Generate PR summary (works on both issues and PRs)
|
||||
- triage
|
||||
- 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