Merge pull request 'dev' (#11) from dev into main
Reviewed-on: #11
This commit was merged in pull request #11.
This commit is contained in:
@@ -1,41 +1,50 @@
|
|||||||
name: AI Comment Reply
|
name: AI Comment Reply
|
||||||
|
|
||||||
on:
|
on:
|
||||||
issue_comment:
|
issue_comment:
|
||||||
types: [created]
|
types: [created]
|
||||||
|
|
||||||
# CUSTOMIZE YOUR BOT NAME:
|
# CUSTOMIZE YOUR BOT NAME:
|
||||||
# Change '@ai-bot' below to match your config.yml mention_prefix
|
# Change '@ai-bot' below to match your config.yml mention_prefix
|
||||||
# Examples: '@bartender', '@uni', '@joey', '@codebot'
|
# Examples: '@bartender', '@uni', '@joey', '@codebot'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ai-reply:
|
ai-reply:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: contains(github.event.comment.body, '@codebot') # <-- Change this to your bot name
|
if: contains(github.event.comment.body, '@codebot') # <-- Change this to your bot name
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: Hiddenden/openrabbit
|
repository: Hiddenden/openrabbit
|
||||||
path: .ai-review
|
path: .ai-review
|
||||||
token: ${{ secrets.AI_REVIEW_TOKEN }}
|
token: ${{ secrets.AI_REVIEW_TOKEN }}
|
||||||
|
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
|
|
||||||
- run: pip install requests pyyaml
|
- run: pip install requests pyyaml
|
||||||
|
|
||||||
- name: Run AI Comment Response
|
- name: Run AI Comment Response
|
||||||
env:
|
env:
|
||||||
AI_REVIEW_TOKEN: ${{ secrets.AI_REVIEW_TOKEN }}
|
AI_REVIEW_TOKEN: ${{ secrets.AI_REVIEW_TOKEN }}
|
||||||
AI_REVIEW_REPO: ${{ gitea.repository }}
|
AI_REVIEW_REPO: ${{ gitea.repository }}
|
||||||
AI_REVIEW_API_URL: https://git.hiddenden.cafe/api/v1
|
AI_REVIEW_API_URL: https://git.hiddenden.cafe/api/v1
|
||||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||||
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||||
OLLAMA_HOST: ${{ secrets.OLLAMA_HOST }}
|
OLLAMA_HOST: ${{ secrets.OLLAMA_HOST }}
|
||||||
run: |
|
run: |
|
||||||
cd .ai-review/tools/ai-review
|
cd .ai-review/tools/ai-review
|
||||||
python main.py comment ${{ gitea.repository }} ${{ gitea.event.issue.number }} \
|
|
||||||
"${{ gitea.event.comment.body }}"
|
# Check if this is a PR or an issue
|
||||||
|
if [ "${{ gitea.event.issue.pull_request }}" != "" ]; then
|
||||||
|
# This is a PR comment - dispatch as issue_comment event
|
||||||
|
python main.py dispatch ${{ gitea.repository }} issue_comment \
|
||||||
|
'{"action":"created","issue":${{ toJSON(gitea.event.issue) }},"comment":${{ toJSON(gitea.event.comment) }}}'
|
||||||
|
else
|
||||||
|
# This is an issue comment - use the comment command
|
||||||
|
python main.py comment ${{ gitea.repository }} ${{ gitea.event.issue.number }} \
|
||||||
|
"${{ gitea.event.comment.body }}"
|
||||||
|
fi
|
||||||
|
|||||||
119
CLAUDE.md
119
CLAUDE.md
@@ -145,6 +145,7 @@ interaction:
|
|||||||
- security # Security analysis
|
- security # Security analysis
|
||||||
- summarize # Summarize the issue
|
- summarize # Summarize the issue
|
||||||
- triage # Full triage with labeling
|
- triage # Full triage with labeling
|
||||||
|
- review-again # Re-run PR review (PR comments only)
|
||||||
|
|
||||||
review:
|
review:
|
||||||
fail_on_severity: HIGH # Fail CI if HIGH severity issues found
|
fail_on_severity: HIGH # Fail CI if HIGH severity issues found
|
||||||
@@ -309,6 +310,49 @@ pytest tests/test_ai_review.py::TestSecurityScanner -v
|
|||||||
|
|
||||||
## Common Development Tasks
|
## Common Development Tasks
|
||||||
|
|
||||||
|
### Review-Again Command Implementation
|
||||||
|
|
||||||
|
The `@codebot review-again` command allows manual re-triggering of PR reviews without new commits.
|
||||||
|
|
||||||
|
**Key Features:**
|
||||||
|
- Detects `@codebot review-again` in PR comments (not issue comments)
|
||||||
|
- Compares new review with previous review to show resolved/new issues
|
||||||
|
- Updates existing AI review comment instead of creating duplicates
|
||||||
|
- Updates PR labels based on new severity assessment
|
||||||
|
|
||||||
|
**Implementation Details:**
|
||||||
|
|
||||||
|
1. **PRAgent.can_handle()** - Handles `issue_comment` events on PRs containing "review-again"
|
||||||
|
2. **PRAgent._handle_review_again()** - Main handler that:
|
||||||
|
- Fetches previous review comment
|
||||||
|
- Re-runs full PR review (security scan + AI analysis)
|
||||||
|
- Compares findings using `_compare_reviews()`
|
||||||
|
- Generates diff report with `_format_review_update()`
|
||||||
|
- Updates comment and labels
|
||||||
|
|
||||||
|
3. **Review Comparison** - Uses finding keys (file:line:description) to match issues:
|
||||||
|
- **Resolved**: Issues in previous but not in current review
|
||||||
|
- **New**: Issues in current but not in previous review
|
||||||
|
- **Still Present**: Issues in both reviews
|
||||||
|
- **Severity Changed**: Same issue with different severity
|
||||||
|
|
||||||
|
4. **Workflow Integration** - `.gitea/workflows/ai-comment-reply.yml`:
|
||||||
|
- Detects if comment is on PR or issue
|
||||||
|
- Uses `dispatch` command for PRs to route to PRAgent
|
||||||
|
- Preserves backward compatibility with issue commands
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```bash
|
||||||
|
# In a PR comment:
|
||||||
|
@codebot review-again
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common Use Cases:**
|
||||||
|
- Re-evaluate after explaining false positives in comments
|
||||||
|
- Test new `.ai-review.yml` configuration
|
||||||
|
- Update severity after code clarification
|
||||||
|
- Faster iteration without empty commits
|
||||||
|
|
||||||
### Adding a New Command to @codebot
|
### Adding a New Command to @codebot
|
||||||
|
|
||||||
1. Add command to `config.yml` under `interaction.commands`
|
1. Add command to `config.yml` under `interaction.commands`
|
||||||
@@ -318,9 +362,12 @@ pytest tests/test_ai_review.py::TestSecurityScanner -v
|
|||||||
5. Add tests in `tests/test_ai_review.py`
|
5. Add tests in `tests/test_ai_review.py`
|
||||||
|
|
||||||
Example commands:
|
Example commands:
|
||||||
|
- `@codebot help` - Show all available commands with examples
|
||||||
- `@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 setup-labels` - Automatic label setup (built-in, not in config)
|
||||||
|
- `@codebot review-again` - Re-run PR review without new commits (PR comments only)
|
||||||
|
|
||||||
### Changing the Bot Name
|
### Changing the Bot Name
|
||||||
|
|
||||||
@@ -338,10 +385,78 @@ Example commands:
|
|||||||
|
|
||||||
## Repository Labels
|
## Repository Labels
|
||||||
|
|
||||||
|
### Automatic Label Setup (Recommended)
|
||||||
|
|
||||||
|
Use the `@codebot setup-labels` command to automatically configure labels. This command:
|
||||||
|
|
||||||
|
**For repositories with existing labels:**
|
||||||
|
- Detects naming patterns: `Kind/Bug`, `Priority - High`, `type: bug`
|
||||||
|
- Maps existing labels to OpenRabbit schema using aliases
|
||||||
|
- Creates only missing labels following detected pattern
|
||||||
|
- Zero duplicate labels
|
||||||
|
|
||||||
|
**For fresh repositories:**
|
||||||
|
- Creates OpenRabbit's default label set
|
||||||
|
- Uses standard naming: `type:`, `priority:`, status labels
|
||||||
|
|
||||||
|
**Example with existing `Kind/` and `Priority -` labels:**
|
||||||
|
```
|
||||||
|
@codebot setup-labels
|
||||||
|
|
||||||
|
✅ Found 18 existing labels with pattern: prefix_slash
|
||||||
|
|
||||||
|
Proposed Mapping:
|
||||||
|
| OpenRabbit Expected | Your Existing Label | Status |
|
||||||
|
|---------------------|---------------------|--------|
|
||||||
|
| type: bug | Kind/Bug | ✅ Map |
|
||||||
|
| type: feature | Kind/Feature | ✅ Map |
|
||||||
|
| priority: high | Priority - High | ✅ Map |
|
||||||
|
| ai-reviewed | (missing) | ⚠️ Create |
|
||||||
|
|
||||||
|
✅ Created Kind/Question
|
||||||
|
✅ Created Status - AI Reviewed
|
||||||
|
|
||||||
|
Setup Complete! Auto-labeling will use your existing label schema.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Label Setup
|
||||||
|
|
||||||
The system expects these labels to exist in repositories for auto-labeling:
|
The system expects these labels to exist in repositories for auto-labeling:
|
||||||
|
|
||||||
- `priority: high`, `priority: medium`, `priority: low`
|
- `priority: critical`, `priority: high`, `priority: medium`, `priority: low`
|
||||||
- `type: bug`, `type: feature`, `type: question`, `type: documentation`
|
- `type: bug`, `type: feature`, `type: question`, `type: documentation`, `type: security`, `type: testing`
|
||||||
- `ai-approved`, `ai-changes-required`, `ai-reviewed`
|
- `ai-approved`, `ai-changes-required`, `ai-reviewed`
|
||||||
|
|
||||||
Labels are mapped in `config.yml` under the `labels` section.
|
Labels are mapped in `config.yml` under the `labels` section.
|
||||||
|
|
||||||
|
### Label Configuration Format
|
||||||
|
|
||||||
|
Labels support two formats for backwards compatibility:
|
||||||
|
|
||||||
|
**New format (with colors and aliases):**
|
||||||
|
```yaml
|
||||||
|
labels:
|
||||||
|
type:
|
||||||
|
bug:
|
||||||
|
name: "type: bug"
|
||||||
|
color: "d73a4a" # Red
|
||||||
|
description: "Something isn't working"
|
||||||
|
aliases: ["Kind/Bug", "bug", "Type: Bug"] # For auto-detection
|
||||||
|
```
|
||||||
|
|
||||||
|
**Old format (strings only):**
|
||||||
|
```yaml
|
||||||
|
labels:
|
||||||
|
type:
|
||||||
|
bug: "type: bug" # Still works, uses default blue color
|
||||||
|
```
|
||||||
|
|
||||||
|
### Label Pattern Detection
|
||||||
|
|
||||||
|
The `setup-labels` command detects these patterns (configured in `label_patterns`):
|
||||||
|
|
||||||
|
1. **prefix_slash**: `Kind/Bug`, `Type/Feature`, `Category/X`
|
||||||
|
2. **prefix_dash**: `Priority - High`, `Status - Blocked`
|
||||||
|
3. **colon**: `type: bug`, `priority: high`
|
||||||
|
|
||||||
|
When creating missing labels, the bot follows the detected pattern to maintain consistency.
|
||||||
|
|||||||
92
README.md
92
README.md
@@ -82,12 +82,27 @@ jobs:
|
|||||||
|
|
||||||
See `.gitea/workflows/` for all workflow examples.
|
See `.gitea/workflows/` for all workflow examples.
|
||||||
|
|
||||||
### 3. Create Labels
|
### 3. Create Labels (Automatic Setup)
|
||||||
|
|
||||||
|
**Option A: Automatic Setup (Recommended)**
|
||||||
|
|
||||||
|
Create an issue and comment:
|
||||||
|
```
|
||||||
|
@codebot setup-labels
|
||||||
|
```
|
||||||
|
|
||||||
|
The bot will automatically:
|
||||||
|
- Detect your existing label schema (e.g., `Kind/Bug`, `Priority - High`)
|
||||||
|
- Map existing labels to OpenRabbit's auto-labeling system
|
||||||
|
- Create only the missing labels you need
|
||||||
|
- Follow your repository's naming convention
|
||||||
|
|
||||||
|
**Option B: Manual Setup**
|
||||||
|
|
||||||
Create these labels in your repository for auto-labeling:
|
Create these labels in your repository for auto-labeling:
|
||||||
- `priority: high`, `priority: medium`, `priority: low`
|
- `priority: critical`, `priority: high`, `priority: medium`, `priority: low`
|
||||||
- `type: bug`, `type: feature`, `type: question`
|
- `type: bug`, `type: feature`, `type: question`, `type: documentation`
|
||||||
- `ai-approved`, `ai-changes-required`
|
- `ai-approved`, `ai-changes-required`, `ai-reviewed`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -154,16 +169,85 @@ python main.py chat owner/repo "Find all API endpoints" --issue 789
|
|||||||
|
|
||||||
## @codebot Commands
|
## @codebot Commands
|
||||||
|
|
||||||
|
### Issue Commands
|
||||||
|
|
||||||
In any issue comment:
|
In any issue comment:
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
|
| `@codebot help` | **Help:** Show all available commands with examples |
|
||||||
|
| `@codebot setup-labels` | **Setup:** Automatically create/map repository labels for auto-labeling |
|
||||||
| `@codebot triage` | Full issue triage with auto-labeling and analysis |
|
| `@codebot triage` | Full issue triage with auto-labeling and analysis |
|
||||||
| `@codebot summarize` | Summarize the issue in 2-3 sentences |
|
| `@codebot summarize` | Summarize the issue in 2-3 sentences |
|
||||||
| `@codebot explain` | Explain what the issue is about |
|
| `@codebot explain` | Explain what the issue is about |
|
||||||
| `@codebot suggest` | Suggest solutions or next steps |
|
| `@codebot suggest` | Suggest solutions or next steps |
|
||||||
| `@codebot` (any question) | Chat with AI using codebase/web search tools |
|
| `@codebot` (any question) | Chat with AI using codebase/web search tools |
|
||||||
|
|
||||||
|
### Pull Request Commands
|
||||||
|
|
||||||
|
In any PR comment:
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `@codebot review-again` | Re-run AI code review on current PR state without new commits |
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- ✅ Shows diff from previous review (resolved/new/changed issues)
|
||||||
|
- 🏷️ Updates labels based on new severity
|
||||||
|
- ⚡ No need for empty commits to trigger review
|
||||||
|
- 🔧 Respects latest `.ai-review.yml` configuration
|
||||||
|
|
||||||
|
**When to use:**
|
||||||
|
- After addressing review feedback in comments
|
||||||
|
- When AI flagged a false positive and you explained it
|
||||||
|
- After updating `.ai-review.yml` security rules
|
||||||
|
- To re-evaluate severity after code clarification
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```
|
||||||
|
The hardcoded string at line 45 is a public API URL, not a secret.
|
||||||
|
@codebot review-again
|
||||||
|
```
|
||||||
|
|
||||||
|
**New to OpenRabbit?** Just type `@codebot help` in any issue to see all available commands!
|
||||||
|
|
||||||
|
### Label Setup Command
|
||||||
|
|
||||||
|
The `@codebot setup-labels` command intelligently detects your existing label schema and sets up auto-labeling:
|
||||||
|
|
||||||
|
**For repositories with existing labels (e.g., `Kind/Bug`, `Priority - High`):**
|
||||||
|
- Detects your naming pattern (prefix/slash, prefix-dash, or colon-style)
|
||||||
|
- Maps your existing labels to OpenRabbit's schema
|
||||||
|
- Creates only missing labels following your pattern
|
||||||
|
- Zero duplicate labels created
|
||||||
|
|
||||||
|
**For fresh repositories:**
|
||||||
|
- Creates OpenRabbit's default label set
|
||||||
|
- Uses `type:`, `priority:`, and status labels
|
||||||
|
|
||||||
|
**Example output:**
|
||||||
|
```
|
||||||
|
@codebot setup-labels
|
||||||
|
|
||||||
|
✅ Found 18 existing labels with pattern: prefix_slash
|
||||||
|
|
||||||
|
Detected Categories:
|
||||||
|
- Kind (7 labels): Bug, Feature, Documentation, Security, Testing
|
||||||
|
- Priority (4 labels): Critical, High, Medium, Low
|
||||||
|
|
||||||
|
Proposed Mapping:
|
||||||
|
| OpenRabbit Expected | Your Existing Label | Status |
|
||||||
|
|---------------------|---------------------|--------|
|
||||||
|
| type: bug | Kind/Bug | ✅ Map |
|
||||||
|
| priority: high | Priority - High | ✅ Map |
|
||||||
|
| ai-reviewed | (missing) | ⚠️ Create |
|
||||||
|
|
||||||
|
✅ Created Kind/Question (#cc317c)
|
||||||
|
✅ Created Status - AI Reviewed (#1d76db)
|
||||||
|
|
||||||
|
Setup Complete! Auto-labeling will use your existing label schema.
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Interactive Chat
|
## Interactive Chat
|
||||||
|
|||||||
440
docs/feature-ideas.md
Normal file
440
docs/feature-ideas.md
Normal file
@@ -0,0 +1,440 @@
|
|||||||
|
# Feature Ideas & Roadmap
|
||||||
|
|
||||||
|
This document outlines recommended feature additions for OpenRabbit, ordered by value/effort ratio.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| Feature | Value | Effort | Time Estimate | Status |
|
||||||
|
|---------|-------|--------|---------------|--------|
|
||||||
|
| [@codebot help Command](#1-codebot-help-command) | HIGH | LOW | 1-2 hours | ⭐ Recommended |
|
||||||
|
| [Automatic Label Creator](#2-automatic-label-creator) | HIGH | MEDIUM | 2-3 hours | Planned |
|
||||||
|
| [PR Changelog Generator](#3-pr-changelog-generator) | MEDIUM | MEDIUM | 3-4 hours | Planned |
|
||||||
|
| [Code Diff Explainer](#4-code-diff-explainer) | MEDIUM-HIGH | MEDIUM | 2-3 hours | Planned |
|
||||||
|
| [Smart Test Suggestions](#5-smart-test-suggestions) | HIGH | HIGH | 5-6 hours | Planned |
|
||||||
|
| [@codebot review-again](#6-codebot-review-again) | MEDIUM | LOW | 1-2 hours | Planned |
|
||||||
|
| [Dependency Update Advisor](#7-dependency-update-advisor) | VERY HIGH | HIGH | 6-8 hours | Planned |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. @codebot help Command
|
||||||
|
|
||||||
|
**⭐ HIGHEST PRIORITY - Quick Win**
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
Users have no way to discover what commands are available. They don't know what the bot can do without reading documentation.
|
||||||
|
|
||||||
|
### Solution
|
||||||
|
Add a `@codebot help` command that lists all available commands with descriptions and examples.
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
- Add `help` to `config.yml` commands list
|
||||||
|
- Add `_command_help()` method to IssueAgent
|
||||||
|
- Format response with all commands + descriptions
|
||||||
|
|
||||||
|
### Example Output
|
||||||
|
```markdown
|
||||||
|
@username
|
||||||
|
|
||||||
|
**Available @codebot Commands:**
|
||||||
|
|
||||||
|
**Issue Triage & Analysis:**
|
||||||
|
- `@codebot triage` - Full issue triage with auto-labeling and priority assignment
|
||||||
|
- `@codebot summarize` - Generate 2-3 sentence summary
|
||||||
|
- `@codebot explain` - Detailed explanation of the issue
|
||||||
|
- `@codebot suggest` - Solution suggestions or next steps
|
||||||
|
|
||||||
|
**Interactive Chat:**
|
||||||
|
- `@codebot [question]` - Ask questions about the codebase
|
||||||
|
|
||||||
|
**Codebase Analysis:**
|
||||||
|
- `@codebot codebase` - Trigger full codebase health analysis
|
||||||
|
|
||||||
|
**Utility:**
|
||||||
|
- `@codebot help` - Show this message
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
- `@codebot explain` - Get detailed explanation
|
||||||
|
- `@codebot how does authentication work?` - Chat about codebase
|
||||||
|
```
|
||||||
|
|
||||||
|
### Impact
|
||||||
|
- Immediate UX improvement
|
||||||
|
- Reduces support burden
|
||||||
|
- Makes all future commands discoverable
|
||||||
|
- Foundation for growth
|
||||||
|
|
||||||
|
### Files to Modify
|
||||||
|
- `/tools/ai-review/config.yml`
|
||||||
|
- `/tools/ai-review/agents/issue_agent.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Automatic Label Creator
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
Major setup pain point: users must manually create 10+ labels (`priority: high`, `type: bug`, etc.). Bot silently fails to apply labels if they don't exist.
|
||||||
|
|
||||||
|
### Solution
|
||||||
|
Add `@codebot setup-labels` command that:
|
||||||
|
1. Checks which required labels are missing
|
||||||
|
2. Creates them with proper colors
|
||||||
|
3. Or provides CLI commands for manual creation
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
- Add `setup-labels` command
|
||||||
|
- Query repository labels via Gitea API
|
||||||
|
- Compare against required labels in config
|
||||||
|
- Auto-create missing labels or show creation commands
|
||||||
|
|
||||||
|
### Example Output
|
||||||
|
```markdown
|
||||||
|
@username
|
||||||
|
|
||||||
|
**Label Setup Analysis:**
|
||||||
|
|
||||||
|
**Missing Labels:**
|
||||||
|
- `priority: high` (color: #d73a4a)
|
||||||
|
- `priority: medium` (color: #fbca04)
|
||||||
|
- `type: bug` (color: #d73a4a)
|
||||||
|
|
||||||
|
**Creating labels...**
|
||||||
|
✅ Created `priority: high`
|
||||||
|
✅ Created `priority: medium`
|
||||||
|
✅ Created `type: bug`
|
||||||
|
|
||||||
|
All required labels are now set up!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Impact
|
||||||
|
- Removes major setup friction
|
||||||
|
- Ensures auto-labeling works immediately
|
||||||
|
- Better onboarding experience
|
||||||
|
|
||||||
|
### Files to Modify
|
||||||
|
- `/tools/ai-review/config.yml`
|
||||||
|
- `/tools/ai-review/agents/issue_agent.py`
|
||||||
|
- `/tools/ai-review/clients/gitea_client.py` (add create_label method)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. PR Changelog Generator
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
Developers spend time writing release notes and changelogs. Bot already analyzes PR content.
|
||||||
|
|
||||||
|
### Solution
|
||||||
|
Add `@codebot changelog` command that generates human-readable changelog from PR.
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
- Add `changelog` command for PRs
|
||||||
|
- Analyze PR diff + commit messages
|
||||||
|
- Generate bullet-point summary
|
||||||
|
- Format for CHANGELOG.md
|
||||||
|
|
||||||
|
### Example Output
|
||||||
|
```markdown
|
||||||
|
@username
|
||||||
|
|
||||||
|
**Changelog for PR #123:**
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- User authentication system with JWT tokens
|
||||||
|
- Password reset functionality via email
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Updated database schema for user table
|
||||||
|
- Refactored login endpoint for better error handling
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Session timeout bug causing premature logouts
|
||||||
|
- Security vulnerability in password validation
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
- 15 files changed, 450 insertions, 120 deletions
|
||||||
|
- Main components: auth/, api/users/, database/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Impact
|
||||||
|
- Saves time on release documentation
|
||||||
|
- Consistent changelog format
|
||||||
|
- Can copy-paste into CHANGELOG.md
|
||||||
|
|
||||||
|
### Files to Modify
|
||||||
|
- `/tools/ai-review/config.yml`
|
||||||
|
- `/tools/ai-review/agents/pr_agent.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Code Diff Explainer
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
Complex PRs are hard to understand, especially for:
|
||||||
|
- Non-expert reviewers
|
||||||
|
- New team members
|
||||||
|
- Documentation purposes
|
||||||
|
|
||||||
|
### Solution
|
||||||
|
Add `@codebot explain-diff` command that breaks down what changed in simple terms.
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
- Add `explain-diff` command for PRs
|
||||||
|
- Analyze PR diff section by section
|
||||||
|
- Generate plain-language explanations
|
||||||
|
- Identify architectural changes
|
||||||
|
|
||||||
|
### Example Output
|
||||||
|
```markdown
|
||||||
|
@username
|
||||||
|
|
||||||
|
**PR Explanation:**
|
||||||
|
|
||||||
|
**Overview:**
|
||||||
|
This PR adds user authentication to the application using JWT tokens.
|
||||||
|
|
||||||
|
**Key Changes:**
|
||||||
|
|
||||||
|
1. **auth/jwt.py** (new file)
|
||||||
|
- Creates JSON Web Tokens for authenticated users
|
||||||
|
- Validates tokens on protected endpoints
|
||||||
|
- Handles token expiration (24 hour lifetime)
|
||||||
|
|
||||||
|
2. **api/users.py** (modified)
|
||||||
|
- Added `/login` endpoint that accepts username/password
|
||||||
|
- Returns JWT token on successful authentication
|
||||||
|
- Added `/logout` endpoint to invalidate tokens
|
||||||
|
|
||||||
|
3. **database/schema.sql** (modified)
|
||||||
|
- Added `users` table with columns: id, username, password_hash, email
|
||||||
|
- Passwords are hashed using bcrypt (secure)
|
||||||
|
|
||||||
|
**Architecture Impact:**
|
||||||
|
- Introduces authentication layer across all API endpoints
|
||||||
|
- Adds dependency on PyJWT library
|
||||||
|
- Requires database migration to create users table
|
||||||
|
```
|
||||||
|
|
||||||
|
### Impact
|
||||||
|
- Makes code review accessible
|
||||||
|
- Great for learning/onboarding
|
||||||
|
- Documentation generation
|
||||||
|
|
||||||
|
### Files to Modify
|
||||||
|
- `/tools/ai-review/config.yml`
|
||||||
|
- `/tools/ai-review/agents/pr_agent.py`
|
||||||
|
- `/tools/ai-review/prompts/` (add explain_diff.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Smart Test Suggestions
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
Test coverage is critical but developers often miss edge cases or forget to update tests.
|
||||||
|
|
||||||
|
### Solution
|
||||||
|
Add `@codebot suggest-tests` command that:
|
||||||
|
1. Analyzes changed functions/classes
|
||||||
|
2. Identifies what needs testing
|
||||||
|
3. Suggests specific test cases
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
- Add `suggest-tests` command for PRs
|
||||||
|
- Parse changed code to identify functions
|
||||||
|
- Use LLM to suggest test scenarios
|
||||||
|
- Could integrate with coverage reports
|
||||||
|
|
||||||
|
### Example Output
|
||||||
|
```markdown
|
||||||
|
@username
|
||||||
|
|
||||||
|
**Test Suggestions for PR #123:**
|
||||||
|
|
||||||
|
### auth/jwt.py - `create_token()` function
|
||||||
|
|
||||||
|
**Recommended Test Cases:**
|
||||||
|
1. ✅ Valid user creates token successfully
|
||||||
|
2. ⚠️ **Missing:** Token expiration after 24 hours
|
||||||
|
3. ⚠️ **Missing:** Invalid user ID handling
|
||||||
|
4. ⚠️ **Missing:** Token creation with special characters in username
|
||||||
|
|
||||||
|
### api/users.py - `/login` endpoint
|
||||||
|
|
||||||
|
**Recommended Test Cases:**
|
||||||
|
1. ✅ Successful login with correct credentials
|
||||||
|
2. ⚠️ **Missing:** Login with wrong password
|
||||||
|
3. ⚠️ **Missing:** Login with non-existent user
|
||||||
|
4. ⚠️ **Missing:** SQL injection attempt in username field
|
||||||
|
5. ⚠️ **Missing:** Rate limiting after failed attempts
|
||||||
|
|
||||||
|
**Coverage Impact:**
|
||||||
|
- Current coverage: ~60%
|
||||||
|
- With suggested tests: ~85%
|
||||||
|
```
|
||||||
|
|
||||||
|
### Impact
|
||||||
|
- Improves test coverage
|
||||||
|
- Catches edge cases
|
||||||
|
- Reduces production bugs
|
||||||
|
|
||||||
|
### Files to Modify
|
||||||
|
- `/tools/ai-review/config.yml`
|
||||||
|
- `/tools/ai-review/agents/pr_agent.py`
|
||||||
|
- `/tools/ai-review/prompts/` (add test_suggestions.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. @codebot review-again
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
Current workflow: developer fixes issues → pushes commit → bot auto-reviews. Sometimes developers want re-review without creating new commits (e.g., after only changing comments).
|
||||||
|
|
||||||
|
### Solution
|
||||||
|
Add `@codebot review-again` command that re-runs PR review on current state.
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
- Add `review-again` command for PRs
|
||||||
|
- Re-run PR agent on current diff
|
||||||
|
- Update existing review comment
|
||||||
|
- Compare with previous review (show what changed)
|
||||||
|
|
||||||
|
### Example Output
|
||||||
|
```markdown
|
||||||
|
@username
|
||||||
|
|
||||||
|
**Re-review Complete:**
|
||||||
|
|
||||||
|
**Previous Review:** 5 issues (2 HIGH, 3 MEDIUM)
|
||||||
|
**Current Review:** 1 issue (1 MEDIUM)
|
||||||
|
|
||||||
|
✅ Fixed: SQL injection in login endpoint
|
||||||
|
✅ Fixed: Hardcoded JWT secret
|
||||||
|
⚠️ Remaining: Missing error handling in password reset
|
||||||
|
|
||||||
|
**Status:** Changes Required → Approved (pending fix)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Impact
|
||||||
|
- Smoother PR workflow
|
||||||
|
- No unnecessary commits
|
||||||
|
- Faster feedback loop
|
||||||
|
|
||||||
|
### Files to Modify
|
||||||
|
- `/tools/ai-review/config.yml`
|
||||||
|
- `/tools/ai-review/agents/pr_agent.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Dependency Update Advisor
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
Security vulnerabilities often exist in dependencies. Keeping dependencies up-to-date is critical but tedious.
|
||||||
|
|
||||||
|
### Solution
|
||||||
|
Add `@codebot check-deps` command that:
|
||||||
|
1. Parses requirements.txt, package.json, etc.
|
||||||
|
2. Checks for outdated packages
|
||||||
|
3. Warns about CVEs
|
||||||
|
4. Suggests upgrade commands
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
- Add `check-deps` command
|
||||||
|
- Support multiple package formats (pip, npm, cargo, go)
|
||||||
|
- Integrate with vulnerability databases (CVE, npm audit)
|
||||||
|
- Generate upgrade instructions
|
||||||
|
|
||||||
|
### Example Output
|
||||||
|
```markdown
|
||||||
|
@username
|
||||||
|
|
||||||
|
**Dependency Analysis:**
|
||||||
|
|
||||||
|
### Outdated Packages (5)
|
||||||
|
|
||||||
|
| Package | Current | Latest | Severity |
|
||||||
|
|---------|---------|--------|----------|
|
||||||
|
| requests | 2.28.0 | 2.31.0 | <20><> HIGH - CVE-2023-32681 |
|
||||||
|
| django | 3.2.0 | 4.2.8 | 🟡 MEDIUM - Multiple CVEs |
|
||||||
|
| flask | 2.0.0 | 3.0.0 | 🟢 LOW - New features |
|
||||||
|
| pyyaml | 5.4.1 | 6.0.1 | 🔴 HIGH - CVE-2022-38752 |
|
||||||
|
| sqlalchemy | 1.4.0 | 2.0.23 | 🟢 LOW - Performance improvements |
|
||||||
|
|
||||||
|
### Recommended Actions
|
||||||
|
|
||||||
|
**Immediate (Security Vulnerabilities):**
|
||||||
|
```bash
|
||||||
|
pip install --upgrade requests==2.31.0
|
||||||
|
pip install --upgrade pyyaml==6.0.1
|
||||||
|
pip install --upgrade django==4.2.8
|
||||||
|
```
|
||||||
|
|
||||||
|
**Optional (Feature Updates):**
|
||||||
|
```bash
|
||||||
|
pip install --upgrade flask==3.0.0
|
||||||
|
pip install --upgrade sqlalchemy==2.0.23
|
||||||
|
```
|
||||||
|
|
||||||
|
### Breaking Changes to Review
|
||||||
|
- **Django 4.x:** Requires Python 3.8+, check compatibility
|
||||||
|
- **Flask 3.x:** Async support added, review async patterns
|
||||||
|
- **SQLAlchemy 2.x:** ORM API changes, review queries
|
||||||
|
|
||||||
|
### Resources
|
||||||
|
- [requests CVE-2023-32681](https://nvd.nist.gov/vuln/detail/CVE-2023-32681)
|
||||||
|
- [pyyaml CVE-2022-38752](https://nvd.nist.gov/vuln/detail/CVE-2022-38752)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Impact
|
||||||
|
- Critical for security
|
||||||
|
- Keeps projects up-to-date
|
||||||
|
- Prevents technical debt
|
||||||
|
- Reduces manual checking
|
||||||
|
|
||||||
|
### Files to Modify
|
||||||
|
- `/tools/ai-review/config.yml`
|
||||||
|
- `/tools/ai-review/agents/issue_agent.py`
|
||||||
|
- Add new module: `/tools/ai-review/dependency_checker.py`
|
||||||
|
|
||||||
|
### External APIs Needed
|
||||||
|
- PyPI JSON API for Python packages
|
||||||
|
- npm registry API for JavaScript
|
||||||
|
- NVD (National Vulnerability Database) for CVEs
|
||||||
|
- Or use `pip-audit`, `npm audit` CLI tools
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Priority
|
||||||
|
|
||||||
|
### Phase 1: Quick Wins (1-3 hours total)
|
||||||
|
1. `@codebot help` command
|
||||||
|
2. `@codebot review-again` command
|
||||||
|
|
||||||
|
### Phase 2: High Impact (5-8 hours total)
|
||||||
|
3. Automatic Label Creator
|
||||||
|
4. Code Diff Explainer
|
||||||
|
|
||||||
|
### Phase 3: Strategic Features (10-15 hours total)
|
||||||
|
5. Smart Test Suggestions
|
||||||
|
6. PR Changelog Generator
|
||||||
|
7. Dependency Update Advisor
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Have an idea for a new feature? Please:
|
||||||
|
1. Check if it's already listed here
|
||||||
|
2. Consider value/effort ratio
|
||||||
|
3. Open an issue describing:
|
||||||
|
- Problem it solves
|
||||||
|
- Proposed solution
|
||||||
|
- Expected impact
|
||||||
|
- Example use case
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [future_roadmap.md](future_roadmap.md) - Long-term vision (SAST, RAG, etc.)
|
||||||
|
- [configuration.md](configuration.md) - How to configure existing features
|
||||||
|
- [agents.md](agents.md) - Current agent capabilities
|
||||||
@@ -20,7 +20,11 @@ class TestPromptFormatting:
|
|||||||
"""Get the full path to a prompt file."""
|
"""Get the full path to a prompt file."""
|
||||||
return os.path.join(
|
return os.path.join(
|
||||||
os.path.dirname(__file__),
|
os.path.dirname(__file__),
|
||||||
"..", "tools", "ai-review", "prompts", f"{name}.md"
|
"..",
|
||||||
|
"tools",
|
||||||
|
"ai-review",
|
||||||
|
"prompts",
|
||||||
|
f"{name}.md",
|
||||||
)
|
)
|
||||||
|
|
||||||
def load_prompt(self, name: str) -> str:
|
def load_prompt(self, name: str) -> str:
|
||||||
@@ -32,15 +36,15 @@ class TestPromptFormatting:
|
|||||||
def test_issue_triage_prompt_formatting(self):
|
def test_issue_triage_prompt_formatting(self):
|
||||||
"""Test that issue_triage.md can be formatted with placeholders."""
|
"""Test that issue_triage.md can be formatted with placeholders."""
|
||||||
prompt = self.load_prompt("issue_triage")
|
prompt = self.load_prompt("issue_triage")
|
||||||
|
|
||||||
# This should NOT raise a KeyError
|
# This should NOT raise a KeyError
|
||||||
formatted = prompt.format(
|
formatted = prompt.format(
|
||||||
title="Test Issue Title",
|
title="Test Issue Title",
|
||||||
body="This is the issue body content",
|
body="This is the issue body content",
|
||||||
author="testuser",
|
author="testuser",
|
||||||
existing_labels="bug, urgent"
|
existing_labels="bug, urgent",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert "Test Issue Title" in formatted
|
assert "Test Issue Title" in formatted
|
||||||
assert "This is the issue body content" in formatted
|
assert "This is the issue body content" in formatted
|
||||||
assert "testuser" in formatted
|
assert "testuser" in formatted
|
||||||
@@ -52,15 +56,15 @@ class TestPromptFormatting:
|
|||||||
def test_issue_response_prompt_formatting(self):
|
def test_issue_response_prompt_formatting(self):
|
||||||
"""Test that issue_response.md can be formatted with placeholders."""
|
"""Test that issue_response.md can be formatted with placeholders."""
|
||||||
prompt = self.load_prompt("issue_response")
|
prompt = self.load_prompt("issue_response")
|
||||||
|
|
||||||
formatted = prompt.format(
|
formatted = prompt.format(
|
||||||
issue_type="bug",
|
issue_type="bug",
|
||||||
priority="high",
|
priority="high",
|
||||||
title="Bug Report",
|
title="Bug Report",
|
||||||
body="Description of the bug",
|
body="Description of the bug",
|
||||||
triage_analysis="This is a high priority bug"
|
triage_analysis="This is a high priority bug",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert "bug" in formatted
|
assert "bug" in formatted
|
||||||
assert "high" in formatted
|
assert "high" in formatted
|
||||||
assert "Bug Report" in formatted
|
assert "Bug Report" in formatted
|
||||||
@@ -70,7 +74,7 @@ class TestPromptFormatting:
|
|||||||
def test_base_prompt_no_placeholders(self):
|
def test_base_prompt_no_placeholders(self):
|
||||||
"""Test that base.md loads correctly (no placeholders needed)."""
|
"""Test that base.md loads correctly (no placeholders needed)."""
|
||||||
prompt = self.load_prompt("base")
|
prompt = self.load_prompt("base")
|
||||||
|
|
||||||
# Should contain key elements
|
# Should contain key elements
|
||||||
assert "security" in prompt.lower()
|
assert "security" in prompt.lower()
|
||||||
assert "JSON" in prompt
|
assert "JSON" in prompt
|
||||||
@@ -80,14 +84,20 @@ class TestPromptFormatting:
|
|||||||
"""Verify JSON examples use double curly braces."""
|
"""Verify JSON examples use double curly braces."""
|
||||||
for prompt_name in ["issue_triage", "issue_response"]:
|
for prompt_name in ["issue_triage", "issue_response"]:
|
||||||
prompt = self.load_prompt(prompt_name)
|
prompt = self.load_prompt(prompt_name)
|
||||||
|
|
||||||
# Check that format() doesn't fail
|
# Check that format() doesn't fail
|
||||||
try:
|
try:
|
||||||
# Try with minimal placeholders
|
# Try with minimal placeholders
|
||||||
if prompt_name == "issue_triage":
|
if prompt_name == "issue_triage":
|
||||||
prompt.format(title="t", body="b", author="a", existing_labels="l")
|
prompt.format(title="t", body="b", author="a", existing_labels="l")
|
||||||
elif prompt_name == "issue_response":
|
elif prompt_name == "issue_response":
|
||||||
prompt.format(issue_type="t", priority="p", title="t", body="b", triage_analysis="a")
|
prompt.format(
|
||||||
|
issue_type="t",
|
||||||
|
priority="p",
|
||||||
|
title="t",
|
||||||
|
body="b",
|
||||||
|
triage_analysis="a",
|
||||||
|
)
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
pytest.fail(f"Prompt {prompt_name} has unescaped curly braces: {e}")
|
pytest.fail(f"Prompt {prompt_name} has unescaped curly braces: {e}")
|
||||||
|
|
||||||
@@ -97,11 +107,11 @@ class TestImports:
|
|||||||
|
|
||||||
def test_import_agents(self):
|
def test_import_agents(self):
|
||||||
"""Test importing agent classes."""
|
"""Test importing agent classes."""
|
||||||
from agents.base_agent import BaseAgent, AgentContext, AgentResult
|
from agents.base_agent import AgentContext, AgentResult, BaseAgent
|
||||||
|
from agents.codebase_agent import CodebaseAgent
|
||||||
from agents.issue_agent import IssueAgent
|
from agents.issue_agent import IssueAgent
|
||||||
from agents.pr_agent import PRAgent
|
from agents.pr_agent import PRAgent
|
||||||
from agents.codebase_agent import CodebaseAgent
|
|
||||||
|
|
||||||
assert BaseAgent is not None
|
assert BaseAgent is not None
|
||||||
assert IssueAgent is not None
|
assert IssueAgent is not None
|
||||||
assert PRAgent is not None
|
assert PRAgent is not None
|
||||||
@@ -111,28 +121,28 @@ class TestImports:
|
|||||||
"""Test importing client classes."""
|
"""Test importing client classes."""
|
||||||
from clients.gitea_client import GiteaClient
|
from clients.gitea_client import GiteaClient
|
||||||
from clients.llm_client import LLMClient
|
from clients.llm_client import LLMClient
|
||||||
|
|
||||||
assert GiteaClient is not None
|
assert GiteaClient is not None
|
||||||
assert LLMClient is not None
|
assert LLMClient is not None
|
||||||
|
|
||||||
def test_import_security(self):
|
def test_import_security(self):
|
||||||
"""Test importing security scanner."""
|
"""Test importing security scanner."""
|
||||||
from security.security_scanner import SecurityScanner
|
from security.security_scanner import SecurityScanner
|
||||||
|
|
||||||
assert SecurityScanner is not None
|
assert SecurityScanner is not None
|
||||||
|
|
||||||
def test_import_enterprise(self):
|
def test_import_enterprise(self):
|
||||||
"""Test importing enterprise features."""
|
"""Test importing enterprise features."""
|
||||||
from enterprise.audit_logger import AuditLogger
|
from enterprise.audit_logger import AuditLogger
|
||||||
from enterprise.metrics import MetricsCollector
|
from enterprise.metrics import MetricsCollector
|
||||||
|
|
||||||
assert AuditLogger is not None
|
assert AuditLogger is not None
|
||||||
assert MetricsCollector is not None
|
assert MetricsCollector is not None
|
||||||
|
|
||||||
def test_import_dispatcher(self):
|
def test_import_dispatcher(self):
|
||||||
"""Test importing dispatcher."""
|
"""Test importing dispatcher."""
|
||||||
from dispatcher import Dispatcher
|
from dispatcher import Dispatcher
|
||||||
|
|
||||||
assert Dispatcher is not None
|
assert Dispatcher is not None
|
||||||
|
|
||||||
|
|
||||||
@@ -142,11 +152,11 @@ class TestSecurityScanner:
|
|||||||
def test_detects_hardcoded_secret(self):
|
def test_detects_hardcoded_secret(self):
|
||||||
"""Test detection of hardcoded secrets."""
|
"""Test detection of hardcoded secrets."""
|
||||||
from security.security_scanner import SecurityScanner
|
from security.security_scanner import SecurityScanner
|
||||||
|
|
||||||
scanner = SecurityScanner()
|
scanner = SecurityScanner()
|
||||||
code = '''
|
code = """
|
||||||
API_KEY = "sk-1234567890abcdef"
|
API_KEY = "sk-1234567890abcdef"
|
||||||
'''
|
"""
|
||||||
findings = list(scanner.scan_content(code, "test.py"))
|
findings = list(scanner.scan_content(code, "test.py"))
|
||||||
assert len(findings) >= 1
|
assert len(findings) >= 1
|
||||||
assert any(f.severity == "HIGH" for f in findings)
|
assert any(f.severity == "HIGH" for f in findings)
|
||||||
@@ -154,11 +164,11 @@ API_KEY = "sk-1234567890abcdef"
|
|||||||
def test_detects_eval(self):
|
def test_detects_eval(self):
|
||||||
"""Test detection of eval usage."""
|
"""Test detection of eval usage."""
|
||||||
from security.security_scanner import SecurityScanner
|
from security.security_scanner import SecurityScanner
|
||||||
|
|
||||||
scanner = SecurityScanner()
|
scanner = SecurityScanner()
|
||||||
code = '''
|
code = """
|
||||||
result = eval(user_input)
|
result = eval(user_input)
|
||||||
'''
|
"""
|
||||||
findings = list(scanner.scan_content(code, "test.py"))
|
findings = list(scanner.scan_content(code, "test.py"))
|
||||||
assert len(findings) >= 1
|
assert len(findings) >= 1
|
||||||
assert any("eval" in f.rule_name.lower() for f in findings)
|
assert any("eval" in f.rule_name.lower() for f in findings)
|
||||||
@@ -166,13 +176,13 @@ result = eval(user_input)
|
|||||||
def test_no_false_positives_on_clean_code(self):
|
def test_no_false_positives_on_clean_code(self):
|
||||||
"""Test that clean code doesn't trigger false positives."""
|
"""Test that clean code doesn't trigger false positives."""
|
||||||
from security.security_scanner import SecurityScanner
|
from security.security_scanner import SecurityScanner
|
||||||
|
|
||||||
scanner = SecurityScanner()
|
scanner = SecurityScanner()
|
||||||
code = '''
|
code = """
|
||||||
def hello():
|
def hello():
|
||||||
print("Hello, world!")
|
print("Hello, world!")
|
||||||
return 42
|
return 42
|
||||||
'''
|
"""
|
||||||
findings = list(scanner.scan_content(code, "test.py"))
|
findings = list(scanner.scan_content(code, "test.py"))
|
||||||
# Should have no HIGH severity issues for clean code
|
# Should have no HIGH severity issues for clean code
|
||||||
high_findings = [f for f in findings if f.severity == "HIGH"]
|
high_findings = [f for f in findings if f.severity == "HIGH"]
|
||||||
@@ -185,15 +195,15 @@ class TestAgentContext:
|
|||||||
def test_agent_context_creation(self):
|
def test_agent_context_creation(self):
|
||||||
"""Test creating AgentContext."""
|
"""Test creating AgentContext."""
|
||||||
from agents.base_agent import AgentContext
|
from agents.base_agent import AgentContext
|
||||||
|
|
||||||
context = AgentContext(
|
context = AgentContext(
|
||||||
owner="testowner",
|
owner="testowner",
|
||||||
repo="testrepo",
|
repo="testrepo",
|
||||||
event_type="issues",
|
event_type="issues",
|
||||||
event_data={"action": "opened"},
|
event_data={"action": "opened"},
|
||||||
config={}
|
config={},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert context.owner == "testowner"
|
assert context.owner == "testowner"
|
||||||
assert context.repo == "testrepo"
|
assert context.repo == "testrepo"
|
||||||
assert context.event_type == "issues"
|
assert context.event_type == "issues"
|
||||||
@@ -201,14 +211,14 @@ class TestAgentContext:
|
|||||||
def test_agent_result_creation(self):
|
def test_agent_result_creation(self):
|
||||||
"""Test creating AgentResult."""
|
"""Test creating AgentResult."""
|
||||||
from agents.base_agent import AgentResult
|
from agents.base_agent import AgentResult
|
||||||
|
|
||||||
result = AgentResult(
|
result = AgentResult(
|
||||||
success=True,
|
success=True,
|
||||||
message="Test passed",
|
message="Test passed",
|
||||||
data={"key": "value"},
|
data={"key": "value"},
|
||||||
actions_taken=["action1", "action2"]
|
actions_taken=["action1", "action2"],
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result.success is True
|
assert result.success is True
|
||||||
assert result.message == "Test passed"
|
assert result.message == "Test passed"
|
||||||
assert len(result.actions_taken) == 2
|
assert len(result.actions_taken) == 2
|
||||||
@@ -220,7 +230,7 @@ class TestMetrics:
|
|||||||
def test_counter_increment(self):
|
def test_counter_increment(self):
|
||||||
"""Test counter metrics."""
|
"""Test counter metrics."""
|
||||||
from enterprise.metrics import Counter
|
from enterprise.metrics import Counter
|
||||||
|
|
||||||
counter = Counter("test_counter")
|
counter = Counter("test_counter")
|
||||||
assert counter.value == 0
|
assert counter.value == 0
|
||||||
counter.inc()
|
counter.inc()
|
||||||
@@ -231,27 +241,352 @@ class TestMetrics:
|
|||||||
def test_histogram_observation(self):
|
def test_histogram_observation(self):
|
||||||
"""Test histogram metrics."""
|
"""Test histogram metrics."""
|
||||||
from enterprise.metrics import Histogram
|
from enterprise.metrics import Histogram
|
||||||
|
|
||||||
hist = Histogram("test_histogram")
|
hist = Histogram("test_histogram")
|
||||||
hist.observe(0.1)
|
hist.observe(0.1)
|
||||||
hist.observe(0.5)
|
hist.observe(0.5)
|
||||||
hist.observe(1.0)
|
hist.observe(1.0)
|
||||||
|
|
||||||
assert hist.count == 3
|
assert hist.count == 3
|
||||||
assert hist.sum == 1.6
|
assert hist.sum == 1.6
|
||||||
|
|
||||||
def test_metrics_collector_summary(self):
|
def test_metrics_collector_summary(self):
|
||||||
"""Test metrics collector summary."""
|
"""Test metrics collector summary."""
|
||||||
from enterprise.metrics import MetricsCollector
|
from enterprise.metrics import MetricsCollector
|
||||||
|
|
||||||
collector = MetricsCollector()
|
collector = MetricsCollector()
|
||||||
collector.record_request_start("TestAgent")
|
collector.record_request_start("TestAgent")
|
||||||
collector.record_request_end("TestAgent", success=True, duration_seconds=0.5)
|
collector.record_request_end("TestAgent", success=True, duration_seconds=0.5)
|
||||||
|
|
||||||
summary = collector.get_summary()
|
summary = collector.get_summary()
|
||||||
assert summary["requests"]["total"] == 1
|
assert summary["requests"]["total"] == 1
|
||||||
assert summary["requests"]["success"] == 1
|
assert summary["requests"]["success"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
class TestHelpCommand:
|
||||||
|
"""Test help command functionality."""
|
||||||
|
|
||||||
|
def test_help_command_returns_text(self):
|
||||||
|
"""Test that help command returns formatted help text."""
|
||||||
|
from agents.issue_agent import IssueAgent
|
||||||
|
|
||||||
|
agent = IssueAgent(
|
||||||
|
gitea_client=None,
|
||||||
|
llm_client=None,
|
||||||
|
config={
|
||||||
|
"interaction": {
|
||||||
|
"mention_prefix": "@codebot",
|
||||||
|
"commands": ["help", "triage", "explain"],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
help_text = agent._command_help()
|
||||||
|
|
||||||
|
assert help_text is not None
|
||||||
|
assert len(help_text) > 100
|
||||||
|
assert "@codebot" in help_text
|
||||||
|
assert "help" in help_text.lower()
|
||||||
|
assert "triage" in help_text.lower()
|
||||||
|
|
||||||
|
def test_help_includes_all_sections(self):
|
||||||
|
"""Test that help text includes all major sections."""
|
||||||
|
from agents.issue_agent import IssueAgent
|
||||||
|
|
||||||
|
agent = IssueAgent(
|
||||||
|
gitea_client=None,
|
||||||
|
llm_client=None,
|
||||||
|
config={"interaction": {"mention_prefix": "@codebot"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
help_text = agent._command_help()
|
||||||
|
|
||||||
|
# Check for main sections
|
||||||
|
assert "Issue Triage" in help_text
|
||||||
|
assert "Interactive Chat" in help_text
|
||||||
|
assert "Setup & Utility" in help_text
|
||||||
|
assert "Pull Request" in help_text
|
||||||
|
assert "Quick Examples" in help_text
|
||||||
|
|
||||||
|
def test_help_uses_custom_bot_name(self):
|
||||||
|
"""Test that help command uses custom bot name from config."""
|
||||||
|
from agents.issue_agent import IssueAgent
|
||||||
|
|
||||||
|
agent = IssueAgent(
|
||||||
|
gitea_client=None,
|
||||||
|
llm_client=None,
|
||||||
|
config={"interaction": {"mention_prefix": "@mybot"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
help_text = agent._command_help()
|
||||||
|
|
||||||
|
assert "@mybot" in help_text
|
||||||
|
assert "@codebot" not in help_text
|
||||||
|
|
||||||
|
def test_help_includes_examples(self):
|
||||||
|
"""Test that help text includes usage examples."""
|
||||||
|
from agents.issue_agent import IssueAgent
|
||||||
|
|
||||||
|
agent = IssueAgent(
|
||||||
|
gitea_client=None,
|
||||||
|
llm_client=None,
|
||||||
|
config={"interaction": {"mention_prefix": "@codebot"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
help_text = agent._command_help()
|
||||||
|
|
||||||
|
# Check for example commands
|
||||||
|
assert "triage" in help_text
|
||||||
|
assert "explain" in help_text
|
||||||
|
assert "setup-labels" in help_text
|
||||||
|
|
||||||
|
|
||||||
|
class TestLabelSetup:
|
||||||
|
"""Test label setup and schema detection."""
|
||||||
|
|
||||||
|
def test_detect_prefix_slash_schema(self):
|
||||||
|
"""Test detection of Kind/Bug style labels."""
|
||||||
|
from agents.issue_agent import IssueAgent
|
||||||
|
|
||||||
|
# Create mock agent with config
|
||||||
|
agent = IssueAgent(
|
||||||
|
gitea_client=None,
|
||||||
|
llm_client=None,
|
||||||
|
config={
|
||||||
|
"label_patterns": {
|
||||||
|
"prefix_slash": r"^(Kind|Type|Category)/(.+)$",
|
||||||
|
"prefix_dash": r"^(Priority|Status|Reviewed) - (.+)$",
|
||||||
|
"colon": r"^(type|priority|status): (.+)$",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
labels = [
|
||||||
|
{"name": "Kind/Bug", "color": "d73a4a"},
|
||||||
|
{"name": "Kind/Feature", "color": "1d76db"},
|
||||||
|
{"name": "Kind/Documentation", "color": "0075ca"},
|
||||||
|
{"name": "Priority - High", "color": "d73a4a"},
|
||||||
|
{"name": "Priority - Low", "color": "28a745"},
|
||||||
|
]
|
||||||
|
|
||||||
|
schema = agent._detect_label_schema(labels)
|
||||||
|
|
||||||
|
assert schema is not None
|
||||||
|
assert schema["pattern"] == "prefix_slash"
|
||||||
|
assert "type" in schema["categories"]
|
||||||
|
assert "priority" in schema["categories"]
|
||||||
|
assert len(schema["categories"]["type"]) == 3
|
||||||
|
|
||||||
|
def test_detect_prefix_dash_schema(self):
|
||||||
|
"""Test detection of Priority - High style labels."""
|
||||||
|
from agents.issue_agent import IssueAgent
|
||||||
|
|
||||||
|
agent = IssueAgent(
|
||||||
|
gitea_client=None,
|
||||||
|
llm_client=None,
|
||||||
|
config={
|
||||||
|
"label_patterns": {
|
||||||
|
"prefix_slash": r"^(Kind|Type|Category)/(.+)$",
|
||||||
|
"prefix_dash": r"^(Priority|Status|Reviewed) - (.+)$",
|
||||||
|
"colon": r"^(type|priority|status): (.+)$",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
labels = [
|
||||||
|
{"name": "Priority - Critical", "color": "b60205"},
|
||||||
|
{"name": "Priority - High", "color": "d73a4a"},
|
||||||
|
{"name": "Status - Blocked", "color": "fef2c0"},
|
||||||
|
]
|
||||||
|
|
||||||
|
schema = agent._detect_label_schema(labels)
|
||||||
|
|
||||||
|
assert schema is not None
|
||||||
|
assert schema["pattern"] == "prefix_dash"
|
||||||
|
assert "priority" in schema["categories"]
|
||||||
|
assert "status" in schema["categories"]
|
||||||
|
|
||||||
|
def test_detect_colon_schema(self):
|
||||||
|
"""Test detection of type: bug style labels."""
|
||||||
|
from agents.issue_agent import IssueAgent
|
||||||
|
|
||||||
|
agent = IssueAgent(
|
||||||
|
gitea_client=None,
|
||||||
|
llm_client=None,
|
||||||
|
config={
|
||||||
|
"label_patterns": {
|
||||||
|
"prefix_slash": r"^(Kind|Type|Category)/(.+)$",
|
||||||
|
"prefix_dash": r"^(Priority|Status|Reviewed) - (.+)$",
|
||||||
|
"colon": r"^(type|priority|status): (.+)$",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
labels = [
|
||||||
|
{"name": "type: bug", "color": "d73a4a"},
|
||||||
|
{"name": "type: feature", "color": "1d76db"},
|
||||||
|
{"name": "priority: high", "color": "d73a4a"},
|
||||||
|
]
|
||||||
|
|
||||||
|
schema = agent._detect_label_schema(labels)
|
||||||
|
|
||||||
|
assert schema is not None
|
||||||
|
assert schema["pattern"] == "colon"
|
||||||
|
assert "type" in schema["categories"]
|
||||||
|
assert "priority" in schema["categories"]
|
||||||
|
|
||||||
|
def test_build_label_mapping(self):
|
||||||
|
"""Test building label mapping from existing labels."""
|
||||||
|
from agents.issue_agent import IssueAgent
|
||||||
|
|
||||||
|
agent = IssueAgent(
|
||||||
|
gitea_client=None,
|
||||||
|
llm_client=None,
|
||||||
|
config={
|
||||||
|
"labels": {
|
||||||
|
"type": {
|
||||||
|
"bug": {"name": "type: bug", "aliases": ["Kind/Bug", "bug"]},
|
||||||
|
"feature": {
|
||||||
|
"name": "type: feature",
|
||||||
|
"aliases": ["Kind/Feature", "feature"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"priority": {
|
||||||
|
"high": {
|
||||||
|
"name": "priority: high",
|
||||||
|
"aliases": ["Priority - High", "P1"],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
existing_labels = [
|
||||||
|
{"name": "Kind/Bug", "color": "d73a4a"},
|
||||||
|
{"name": "Kind/Feature", "color": "1d76db"},
|
||||||
|
{"name": "Priority - High", "color": "d73a4a"},
|
||||||
|
]
|
||||||
|
|
||||||
|
schema = {
|
||||||
|
"pattern": "prefix_slash",
|
||||||
|
"categories": {
|
||||||
|
"type": ["Kind/Bug", "Kind/Feature"],
|
||||||
|
"priority": ["Priority - High"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mapping = agent._build_label_mapping(existing_labels, schema)
|
||||||
|
|
||||||
|
assert "type" in mapping
|
||||||
|
assert "bug" in mapping["type"]
|
||||||
|
assert mapping["type"]["bug"] == "Kind/Bug"
|
||||||
|
assert "feature" in mapping["type"]
|
||||||
|
assert mapping["type"]["feature"] == "Kind/Feature"
|
||||||
|
assert "priority" in mapping
|
||||||
|
assert "high" in mapping["priority"]
|
||||||
|
assert mapping["priority"]["high"] == "Priority - High"
|
||||||
|
|
||||||
|
def test_suggest_label_name_prefix_slash(self):
|
||||||
|
"""Test label name suggestion for prefix_slash pattern."""
|
||||||
|
from agents.issue_agent import IssueAgent
|
||||||
|
|
||||||
|
agent = IssueAgent(
|
||||||
|
gitea_client=None,
|
||||||
|
llm_client=None,
|
||||||
|
config={
|
||||||
|
"labels": {
|
||||||
|
"type": {"bug": {"name": "type: bug", "color": "d73a4a"}},
|
||||||
|
"priority": {},
|
||||||
|
"status": {
|
||||||
|
"ai_approved": {"name": "ai-approved", "color": "28a745"}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test type category
|
||||||
|
suggested = agent._suggest_label_name("type", "bug", "prefix_slash")
|
||||||
|
assert suggested == "Kind/Bug"
|
||||||
|
|
||||||
|
# Test status category
|
||||||
|
suggested = agent._suggest_label_name("status", "ai_approved", "prefix_slash")
|
||||||
|
assert suggested == "Status/Ai Approved"
|
||||||
|
|
||||||
|
def test_suggest_label_name_prefix_dash(self):
|
||||||
|
"""Test label name suggestion for prefix_dash pattern."""
|
||||||
|
from agents.issue_agent import IssueAgent
|
||||||
|
|
||||||
|
agent = IssueAgent(
|
||||||
|
gitea_client=None,
|
||||||
|
llm_client=None,
|
||||||
|
config={
|
||||||
|
"labels": {
|
||||||
|
"type": {},
|
||||||
|
"priority": {"high": {"name": "priority: high", "color": "d73a4a"}},
|
||||||
|
"status": {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
suggested = agent._suggest_label_name("priority", "high", "prefix_dash")
|
||||||
|
assert suggested == "Priority - High"
|
||||||
|
|
||||||
|
def test_get_label_config_backwards_compatibility(self):
|
||||||
|
"""Test that old string format still works."""
|
||||||
|
from agents.issue_agent import IssueAgent
|
||||||
|
|
||||||
|
# Old config format (strings)
|
||||||
|
agent = IssueAgent(
|
||||||
|
gitea_client=None,
|
||||||
|
llm_client=None,
|
||||||
|
config={
|
||||||
|
"labels": {
|
||||||
|
"type": {
|
||||||
|
"bug": "type: bug" # Old format
|
||||||
|
},
|
||||||
|
"priority": {},
|
||||||
|
"status": {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
config = agent._get_label_config("type", "bug")
|
||||||
|
|
||||||
|
assert config["name"] == "type: bug"
|
||||||
|
assert config["color"] == "1d76db" # Default color
|
||||||
|
assert config["aliases"] == []
|
||||||
|
|
||||||
|
def test_get_label_config_new_format(self):
|
||||||
|
"""Test that new dict format works."""
|
||||||
|
from agents.issue_agent import IssueAgent
|
||||||
|
|
||||||
|
agent = IssueAgent(
|
||||||
|
gitea_client=None,
|
||||||
|
llm_client=None,
|
||||||
|
config={
|
||||||
|
"labels": {
|
||||||
|
"type": {
|
||||||
|
"bug": {
|
||||||
|
"name": "type: bug",
|
||||||
|
"color": "d73a4a",
|
||||||
|
"description": "Something isn't working",
|
||||||
|
"aliases": ["Kind/Bug", "bug"],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"priority": {},
|
||||||
|
"status": {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
config = agent._get_label_config("type", "bug")
|
||||||
|
|
||||||
|
assert config["name"] == "type: bug"
|
||||||
|
assert config["color"] == "d73a4a"
|
||||||
|
assert config["description"] == "Something isn't working"
|
||||||
|
assert "Kind/Bug" in config["aliases"]
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
pytest.main([__file__, "-v"])
|
pytest.main([__file__, "-v"])
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import re
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
from clients.llm_client import ToolCall
|
||||||
|
|
||||||
from agents.base_agent import AgentContext, AgentResult, BaseAgent
|
from agents.base_agent import AgentContext, AgentResult, BaseAgent
|
||||||
from clients.llm_client import ToolCall
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -114,8 +114,10 @@ Repository context: {owner}/{repo}
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self._searxng_url = self.config.get("agents", {}).get("chat", {}).get(
|
self._searxng_url = (
|
||||||
"searxng_url", os.environ.get("SEARXNG_URL", "")
|
self.config.get("agents", {})
|
||||||
|
.get("chat", {})
|
||||||
|
.get("searxng_url", os.environ.get("SEARXNG_URL", ""))
|
||||||
)
|
)
|
||||||
|
|
||||||
def can_handle(self, event_type: str, event_data: dict) -> bool:
|
def can_handle(self, event_type: str, event_data: dict) -> bool:
|
||||||
@@ -133,7 +135,13 @@ Repository context: {owner}/{repo}
|
|||||||
# Check if this is a chat request (any @ai-bot mention that isn't a specific command)
|
# Check if this is a chat request (any @ai-bot mention that isn't a specific command)
|
||||||
if mention_prefix in comment_body:
|
if mention_prefix in comment_body:
|
||||||
# Check it's not another specific command
|
# Check it's not another specific command
|
||||||
specific_commands = ["summarize", "explain", "suggest", "security", "codebase"]
|
specific_commands = [
|
||||||
|
"summarize",
|
||||||
|
"explain",
|
||||||
|
"suggest",
|
||||||
|
"security",
|
||||||
|
"codebase",
|
||||||
|
]
|
||||||
body_lower = comment_body.lower()
|
body_lower = comment_body.lower()
|
||||||
for cmd in specific_commands:
|
for cmd in specific_commands:
|
||||||
if f"{mention_prefix} {cmd}" in body_lower:
|
if f"{mention_prefix} {cmd}" in body_lower:
|
||||||
@@ -150,18 +158,24 @@ Repository context: {owner}/{repo}
|
|||||||
"""Execute the chat agent."""
|
"""Execute the chat agent."""
|
||||||
self.logger.info(f"Starting chat for {context.owner}/{context.repo}")
|
self.logger.info(f"Starting chat for {context.owner}/{context.repo}")
|
||||||
|
|
||||||
# Extract user message
|
# Extract user message and author
|
||||||
if context.event_type == "issue_comment":
|
if context.event_type == "issue_comment":
|
||||||
user_message = context.event_data.get("comment", {}).get("body", "")
|
user_message = context.event_data.get("comment", {}).get("body", "")
|
||||||
issue_index = context.event_data.get("issue", {}).get("number")
|
issue_index = context.event_data.get("issue", {}).get("number")
|
||||||
# Remove the @ai-bot prefix
|
comment_author = (
|
||||||
|
context.event_data.get("comment", {})
|
||||||
|
.get("user", {})
|
||||||
|
.get("login", "user")
|
||||||
|
)
|
||||||
|
# Remove the @codebot prefix
|
||||||
mention_prefix = self.config.get("interaction", {}).get(
|
mention_prefix = self.config.get("interaction", {}).get(
|
||||||
"mention_prefix", "@ai-bot"
|
"mention_prefix", "@codebot"
|
||||||
)
|
)
|
||||||
user_message = user_message.replace(mention_prefix, "").strip()
|
user_message = user_message.replace(mention_prefix, "").strip()
|
||||||
else:
|
else:
|
||||||
user_message = context.event_data.get("message", "")
|
user_message = context.event_data.get("message", "")
|
||||||
issue_index = context.event_data.get("issue_number")
|
issue_index = context.event_data.get("issue_number")
|
||||||
|
comment_author = None
|
||||||
|
|
||||||
if not user_message:
|
if not user_message:
|
||||||
return AgentResult(
|
return AgentResult(
|
||||||
@@ -191,13 +205,10 @@ Repository context: {owner}/{repo}
|
|||||||
|
|
||||||
# Post response if this is an issue comment
|
# Post response if this is an issue comment
|
||||||
if issue_index:
|
if issue_index:
|
||||||
comment_body = self._format_response(response_content)
|
comment_body = self._format_response(response_content, comment_author)
|
||||||
self.upsert_comment(
|
# Create a new comment instead of upserting to make conversation flow better
|
||||||
context.owner,
|
self.gitea.create_issue_comment(
|
||||||
context.repo,
|
context.owner, context.repo, issue_index, comment_body
|
||||||
issue_index,
|
|
||||||
comment_body,
|
|
||||||
marker=self.CHAT_AI_MARKER,
|
|
||||||
)
|
)
|
||||||
actions_taken.append("Posted chat response")
|
actions_taken.append("Posted chat response")
|
||||||
|
|
||||||
@@ -230,21 +241,23 @@ Repository context: {owner}/{repo}
|
|||||||
return response.content, tools_used
|
return response.content, tools_used
|
||||||
|
|
||||||
# Add assistant message with tool calls
|
# Add assistant message with tool calls
|
||||||
messages.append({
|
messages.append(
|
||||||
"role": "assistant",
|
{
|
||||||
"content": response.content or "",
|
"role": "assistant",
|
||||||
"tool_calls": [
|
"content": response.content or "",
|
||||||
{
|
"tool_calls": [
|
||||||
"id": tc.id,
|
{
|
||||||
"type": "function",
|
"id": tc.id,
|
||||||
"function": {
|
"type": "function",
|
||||||
"name": tc.name,
|
"function": {
|
||||||
"arguments": str(tc.arguments),
|
"name": tc.name,
|
||||||
},
|
"arguments": str(tc.arguments),
|
||||||
}
|
},
|
||||||
for tc in response.tool_calls
|
}
|
||||||
],
|
for tc in response.tool_calls
|
||||||
})
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Execute each tool call
|
# Execute each tool call
|
||||||
for tool_call in response.tool_calls:
|
for tool_call in response.tool_calls:
|
||||||
@@ -252,11 +265,13 @@ Repository context: {owner}/{repo}
|
|||||||
tools_used.append(tool_call.name)
|
tools_used.append(tool_call.name)
|
||||||
|
|
||||||
# Add tool result to messages
|
# Add tool result to messages
|
||||||
messages.append({
|
messages.append(
|
||||||
"role": "tool",
|
{
|
||||||
"tool_call_id": tool_call.id,
|
"role": "tool",
|
||||||
"content": tool_result,
|
"tool_call_id": tool_call.id,
|
||||||
})
|
"content": tool_result,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# If we hit max iterations, make one final call without tools
|
# If we hit max iterations, make one final call without tools
|
||||||
self._rate_limit()
|
self._rate_limit()
|
||||||
@@ -357,15 +372,38 @@ Repository context: {owner}/{repo}
|
|||||||
|
|
||||||
# Code extensions to search
|
# Code extensions to search
|
||||||
code_extensions = {
|
code_extensions = {
|
||||||
".py", ".js", ".ts", ".go", ".rs", ".java", ".rb",
|
".py",
|
||||||
".php", ".c", ".cpp", ".h", ".cs", ".swift", ".kt",
|
".js",
|
||||||
".md", ".yml", ".yaml", ".json", ".toml",
|
".ts",
|
||||||
|
".go",
|
||||||
|
".rs",
|
||||||
|
".java",
|
||||||
|
".rb",
|
||||||
|
".php",
|
||||||
|
".c",
|
||||||
|
".cpp",
|
||||||
|
".h",
|
||||||
|
".cs",
|
||||||
|
".swift",
|
||||||
|
".kt",
|
||||||
|
".md",
|
||||||
|
".yml",
|
||||||
|
".yaml",
|
||||||
|
".json",
|
||||||
|
".toml",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Patterns to ignore
|
# Patterns to ignore
|
||||||
ignore_patterns = [
|
ignore_patterns = [
|
||||||
"node_modules/", "vendor/", ".git/", "__pycache__/",
|
"node_modules/",
|
||||||
".venv/", "dist/", "build/", ".min.js", ".min.css",
|
"vendor/",
|
||||||
|
".git/",
|
||||||
|
"__pycache__/",
|
||||||
|
".venv/",
|
||||||
|
"dist/",
|
||||||
|
"build/",
|
||||||
|
".min.js",
|
||||||
|
".min.css",
|
||||||
]
|
]
|
||||||
|
|
||||||
def traverse(path: str = ""):
|
def traverse(path: str = ""):
|
||||||
@@ -397,6 +435,7 @@ Repository context: {owner}/{repo}
|
|||||||
def _match_pattern(self, filepath: str, pattern: str) -> bool:
|
def _match_pattern(self, filepath: str, pattern: str) -> bool:
|
||||||
"""Check if filepath matches a simple glob pattern."""
|
"""Check if filepath matches a simple glob pattern."""
|
||||||
import fnmatch
|
import fnmatch
|
||||||
|
|
||||||
return fnmatch.fnmatch(filepath, pattern)
|
return fnmatch.fnmatch(filepath, pattern)
|
||||||
|
|
||||||
def _tool_read_file(self, context: AgentContext, filepath: str) -> str:
|
def _tool_read_file(self, context: AgentContext, filepath: str) -> str:
|
||||||
@@ -458,13 +497,22 @@ Repository context: {owner}/{repo}
|
|||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
return f"Web search failed: {e}"
|
return f"Web search failed: {e}"
|
||||||
|
|
||||||
def _format_response(self, content: str) -> str:
|
def _format_response(self, content: str, user: str | None = None) -> str:
|
||||||
"""Format the chat response with disclaimer."""
|
"""Format the chat response with disclaimer."""
|
||||||
lines = [
|
lines = []
|
||||||
f"{self.AI_DISCLAIMER}",
|
|
||||||
"",
|
# Add user mention if available
|
||||||
"---",
|
if user:
|
||||||
"",
|
lines.append(f"@{user}")
|
||||||
content,
|
lines.append("")
|
||||||
]
|
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
f"{self.AI_DISCLAIMER}",
|
||||||
|
"",
|
||||||
|
"---",
|
||||||
|
"",
|
||||||
|
content,
|
||||||
|
]
|
||||||
|
)
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ Handles issue.opened, issue.labeled, and issue_comment events.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from agents.base_agent import AgentContext, AgentResult, BaseAgent
|
from agents.base_agent import AgentContext, AgentResult, BaseAgent
|
||||||
@@ -153,14 +154,17 @@ class IssueAgent(BaseAgent):
|
|||||||
comment = context.event_data.get("comment", {})
|
comment = context.event_data.get("comment", {})
|
||||||
issue_index = issue.get("number")
|
issue_index = issue.get("number")
|
||||||
comment_body = comment.get("body", "")
|
comment_body = comment.get("body", "")
|
||||||
|
comment_author = comment.get("user", {}).get("login", "user")
|
||||||
|
|
||||||
# Parse command from mention
|
# Parse command from mention
|
||||||
command = self._parse_command(comment_body)
|
command = self._parse_command(comment_body)
|
||||||
|
|
||||||
if command:
|
if command:
|
||||||
response = self._handle_command(context, issue, command)
|
response = self._handle_command(context, issue, command)
|
||||||
|
# Add user mention at the top
|
||||||
|
response_with_mention = f"@{comment_author}\n\n{response}"
|
||||||
self.gitea.create_issue_comment(
|
self.gitea.create_issue_comment(
|
||||||
context.owner, context.repo, issue_index, response
|
context.owner, context.repo, issue_index, response_with_mention
|
||||||
)
|
)
|
||||||
return AgentResult(
|
return AgentResult(
|
||||||
success=True,
|
success=True,
|
||||||
@@ -221,6 +225,52 @@ class IssueAgent(BaseAgent):
|
|||||||
reasoning="Automatic triage failed, needs human review",
|
reasoning="Automatic triage failed, needs human review",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _get_label_name(self, label_config: str | dict) -> str:
|
||||||
|
"""Get label name from config (supports both old string and new dict format).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
label_config: Either a string (old format) or dict with 'name' key (new format)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Label name as string
|
||||||
|
"""
|
||||||
|
if isinstance(label_config, str):
|
||||||
|
return label_config
|
||||||
|
elif isinstance(label_config, dict):
|
||||||
|
return label_config.get("name", "")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _get_label_config(self, category: str, key: str) -> dict:
|
||||||
|
"""Get full label configuration from config.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
category: Label category (type, priority, status)
|
||||||
|
key: Label key within category (bug, high, etc.)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with name, color, description, aliases
|
||||||
|
"""
|
||||||
|
labels_config = self.config.get("labels", {})
|
||||||
|
category_config = labels_config.get(category, {})
|
||||||
|
label_config = category_config.get(key, {})
|
||||||
|
|
||||||
|
# Handle old string format
|
||||||
|
if isinstance(label_config, str):
|
||||||
|
return {
|
||||||
|
"name": label_config,
|
||||||
|
"color": "1d76db", # Default blue
|
||||||
|
"description": "",
|
||||||
|
"aliases": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Handle new dict format
|
||||||
|
return {
|
||||||
|
"name": label_config.get("name", ""),
|
||||||
|
"color": label_config.get("color", "1d76db"),
|
||||||
|
"description": label_config.get("description", ""),
|
||||||
|
"aliases": label_config.get("aliases", []),
|
||||||
|
}
|
||||||
|
|
||||||
def _apply_labels(
|
def _apply_labels(
|
||||||
self,
|
self,
|
||||||
owner: str,
|
owner: str,
|
||||||
@@ -229,8 +279,6 @@ class IssueAgent(BaseAgent):
|
|||||||
triage: TriageResult,
|
triage: TriageResult,
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
"""Apply labels based on triage result."""
|
"""Apply labels based on triage result."""
|
||||||
labels_config = self.config.get("labels", {})
|
|
||||||
|
|
||||||
# Get all repo labels
|
# Get all repo labels
|
||||||
try:
|
try:
|
||||||
repo_labels = self.gitea.get_repo_labels(owner, repo)
|
repo_labels = self.gitea.get_repo_labels(owner, repo)
|
||||||
@@ -241,23 +289,23 @@ class IssueAgent(BaseAgent):
|
|||||||
|
|
||||||
labels_to_add = []
|
labels_to_add = []
|
||||||
|
|
||||||
# Map priority
|
# Map priority using new helper
|
||||||
priority_labels = labels_config.get("priority", {})
|
priority_config = self._get_label_config("priority", triage.priority)
|
||||||
priority_label = priority_labels.get(triage.priority)
|
priority_label_name = priority_config["name"]
|
||||||
if priority_label and priority_label in label_map:
|
if priority_label_name and priority_label_name in label_map:
|
||||||
labels_to_add.append(label_map[priority_label])
|
labels_to_add.append(label_map[priority_label_name])
|
||||||
|
|
||||||
# Map type
|
# Map type using new helper
|
||||||
type_labels = labels_config.get("type", {})
|
type_config = self._get_label_config("type", triage.issue_type)
|
||||||
type_label = type_labels.get(triage.issue_type)
|
type_label_name = type_config["name"]
|
||||||
if type_label and type_label in label_map:
|
if type_label_name and type_label_name in label_map:
|
||||||
labels_to_add.append(label_map[type_label])
|
labels_to_add.append(label_map[type_label_name])
|
||||||
|
|
||||||
# Add AI reviewed label
|
# Add AI reviewed label using new helper
|
||||||
status_labels = labels_config.get("status", {})
|
reviewed_config = self._get_label_config("status", "ai_reviewed")
|
||||||
reviewed_label = status_labels.get("ai_reviewed")
|
reviewed_label_name = reviewed_config["name"]
|
||||||
if reviewed_label and reviewed_label in label_map:
|
if reviewed_label_name and reviewed_label_name in label_map:
|
||||||
labels_to_add.append(label_map[reviewed_label])
|
labels_to_add.append(label_map[reviewed_label_name])
|
||||||
|
|
||||||
if labels_to_add:
|
if labels_to_add:
|
||||||
try:
|
try:
|
||||||
@@ -314,9 +362,13 @@ class IssueAgent(BaseAgent):
|
|||||||
"mention_prefix", "@ai-bot"
|
"mention_prefix", "@ai-bot"
|
||||||
)
|
)
|
||||||
commands = self.config.get("interaction", {}).get(
|
commands = self.config.get("interaction", {}).get(
|
||||||
"commands", ["explain", "suggest", "security", "summarize"]
|
"commands", ["explain", "suggest", "security", "summarize", "triage"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Also check for setup-labels command (not in config since it's a setup command)
|
||||||
|
if f"{mention_prefix} setup-labels" in body.lower():
|
||||||
|
return "setup-labels"
|
||||||
|
|
||||||
for command in commands:
|
for command in commands:
|
||||||
if f"{mention_prefix} {command}" in body.lower():
|
if f"{mention_prefix} {command}" in body.lower():
|
||||||
return command
|
return command
|
||||||
@@ -328,7 +380,9 @@ class IssueAgent(BaseAgent):
|
|||||||
title = issue.get("title", "")
|
title = issue.get("title", "")
|
||||||
body = issue.get("body", "")
|
body = issue.get("body", "")
|
||||||
|
|
||||||
if command == "summarize":
|
if command == "help":
|
||||||
|
return self._command_help()
|
||||||
|
elif command == "summarize":
|
||||||
return self._command_summarize(title, body)
|
return self._command_summarize(title, body)
|
||||||
elif command == "explain":
|
elif command == "explain":
|
||||||
return self._command_explain(title, body)
|
return self._command_explain(title, body)
|
||||||
@@ -336,6 +390,8 @@ class IssueAgent(BaseAgent):
|
|||||||
return self._command_suggest(title, body)
|
return self._command_suggest(title, body)
|
||||||
elif command == "triage":
|
elif command == "triage":
|
||||||
return self._command_triage(context, issue)
|
return self._command_triage(context, issue)
|
||||||
|
elif command == "setup-labels":
|
||||||
|
return self._command_setup_labels(context, issue)
|
||||||
|
|
||||||
return f"{self.AI_DISCLAIMER}\n\nSorry, I don't understand the command `{command}`."
|
return f"{self.AI_DISCLAIMER}\n\nSorry, I don't understand the command `{command}`."
|
||||||
|
|
||||||
@@ -391,6 +447,74 @@ Be practical and concise."""
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"{self.AI_DISCLAIMER}\n\nSorry, I was unable to generate suggestions. Error: {e}"
|
return f"{self.AI_DISCLAIMER}\n\nSorry, I was unable to generate suggestions. Error: {e}"
|
||||||
|
|
||||||
|
def _command_help(self) -> str:
|
||||||
|
"""Generate help message with all available commands."""
|
||||||
|
mention_prefix = self.config.get("interaction", {}).get(
|
||||||
|
"mention_prefix", "@codebot"
|
||||||
|
)
|
||||||
|
|
||||||
|
help_text = f"""{self.AI_DISCLAIMER}
|
||||||
|
|
||||||
|
## Available {mention_prefix} Commands
|
||||||
|
|
||||||
|
### Issue Triage & Analysis
|
||||||
|
- `{mention_prefix} triage` - Full issue triage with auto-labeling and priority assignment
|
||||||
|
- `{mention_prefix} summarize` - Generate 2-3 sentence summary of the issue
|
||||||
|
- `{mention_prefix} explain` - Detailed explanation of what the issue is about
|
||||||
|
- `{mention_prefix} suggest` - Solution suggestions or next steps
|
||||||
|
- `{mention_prefix} security` - Security-focused analysis of the issue
|
||||||
|
|
||||||
|
### Interactive Chat
|
||||||
|
- `{mention_prefix} [question]` - Ask questions about the codebase (uses search & file reading tools)
|
||||||
|
- Example: `{mention_prefix} how does authentication work?`
|
||||||
|
- Example: `{mention_prefix} find all API endpoints`
|
||||||
|
|
||||||
|
### Setup & Utility
|
||||||
|
- `{mention_prefix} help` - Show this help message
|
||||||
|
- `{mention_prefix} setup-labels` - Auto-create/map repository labels for auto-labeling
|
||||||
|
|
||||||
|
### Pull Request Analysis
|
||||||
|
PR reviews run automatically when you open or update a pull request. The bot provides:
|
||||||
|
- Inline code review comments
|
||||||
|
- Security vulnerability scanning
|
||||||
|
- Approval or change-request recommendations
|
||||||
|
|
||||||
|
**Manual re-review:**
|
||||||
|
- `{mention_prefix} review-again` - Re-run AI review on current PR state (in PR comments)
|
||||||
|
- Shows diff from previous review (resolved/new issues)
|
||||||
|
- Updates labels and recommendations
|
||||||
|
- Useful after addressing feedback or updating config
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Quick Examples
|
||||||
|
|
||||||
|
**Triage an issue:**
|
||||||
|
```
|
||||||
|
{mention_prefix} triage
|
||||||
|
```
|
||||||
|
|
||||||
|
**Get help understanding:**
|
||||||
|
```
|
||||||
|
{mention_prefix} explain
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ask about the codebase:**
|
||||||
|
```
|
||||||
|
{mention_prefix} how does the authentication system work?
|
||||||
|
```
|
||||||
|
|
||||||
|
**Setup repository labels:**
|
||||||
|
```
|
||||||
|
{mention_prefix} setup-labels
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*For full documentation, see the [README](https://github.com/YourOrg/OpenRabbit/blob/main/README.md)*
|
||||||
|
"""
|
||||||
|
return help_text
|
||||||
|
|
||||||
def _command_triage(self, context: AgentContext, issue: dict) -> str:
|
def _command_triage(self, context: AgentContext, issue: dict) -> str:
|
||||||
"""Perform full triage analysis on the issue."""
|
"""Perform full triage analysis on the issue."""
|
||||||
title = issue.get("title", "")
|
title = issue.get("title", "")
|
||||||
@@ -420,3 +544,313 @@ Be practical and concise."""
|
|||||||
return response
|
return response
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"{self.AI_DISCLAIMER}\n\nSorry, I was unable to triage this issue. Error: {e}"
|
return f"{self.AI_DISCLAIMER}\n\nSorry, I was unable to triage this issue. Error: {e}"
|
||||||
|
|
||||||
|
def _command_setup_labels(self, context: AgentContext, issue: dict) -> str:
|
||||||
|
"""Setup repository labels for auto-labeling."""
|
||||||
|
owner = context.owner
|
||||||
|
repo = context.repo
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get existing labels
|
||||||
|
existing_labels = self.gitea.get_repo_labels(owner, repo)
|
||||||
|
existing_names = {
|
||||||
|
label["name"].lower(): label["name"] for label in existing_labels
|
||||||
|
}
|
||||||
|
|
||||||
|
# Detect schema
|
||||||
|
schema = self._detect_label_schema(existing_labels)
|
||||||
|
|
||||||
|
# Determine mode
|
||||||
|
if schema and len(existing_labels) >= 5:
|
||||||
|
# Repository has existing labels, use mapping mode
|
||||||
|
return self._setup_labels_map_mode(
|
||||||
|
owner, repo, existing_labels, schema, existing_names
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Fresh repository or few labels, use create mode
|
||||||
|
return self._setup_labels_create_mode(owner, repo, existing_names)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Label setup failed: {e}")
|
||||||
|
return f"{self.AI_DISCLAIMER}\n\n**Label Setup Failed**\n\nError: {e}\n\nPlease ensure the bot has write access to this repository."
|
||||||
|
|
||||||
|
def _detect_label_schema(self, labels: list[dict]) -> dict | None:
|
||||||
|
"""Detect the naming pattern used in existing labels.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
"pattern": "prefix_slash" | "prefix_dash" | "colon",
|
||||||
|
"categories": {
|
||||||
|
"type": ["Kind/Bug", "Kind/Feature", ...],
|
||||||
|
"priority": ["Priority - High", ...],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
patterns_config = self.config.get("label_patterns", {})
|
||||||
|
patterns = {
|
||||||
|
"prefix_slash": re.compile(
|
||||||
|
patterns_config.get("prefix_slash", r"^(Kind|Type|Category)/(.+)$")
|
||||||
|
),
|
||||||
|
"prefix_dash": re.compile(
|
||||||
|
patterns_config.get(
|
||||||
|
"prefix_dash", r"^(Priority|Status|Reviewed) - (.+)$"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"colon": re.compile(
|
||||||
|
patterns_config.get("colon", r"^(type|priority|status): (.+)$")
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
categorized = {}
|
||||||
|
detected_pattern = None
|
||||||
|
|
||||||
|
for label in labels:
|
||||||
|
name = label["name"]
|
||||||
|
|
||||||
|
for pattern_name, regex in patterns.items():
|
||||||
|
match = regex.match(name)
|
||||||
|
if match:
|
||||||
|
category = match.group(1).lower()
|
||||||
|
# Normalize category names
|
||||||
|
if category == "kind":
|
||||||
|
category = "type"
|
||||||
|
elif category == "reviewed":
|
||||||
|
category = "status"
|
||||||
|
|
||||||
|
if category not in categorized:
|
||||||
|
categorized[category] = []
|
||||||
|
|
||||||
|
categorized[category].append(name)
|
||||||
|
detected_pattern = pattern_name
|
||||||
|
break
|
||||||
|
|
||||||
|
if not categorized:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return {"pattern": detected_pattern, "categories": categorized}
|
||||||
|
|
||||||
|
def _build_label_mapping(self, existing_labels: list[dict], schema: dict) -> dict:
|
||||||
|
"""Build mapping from OpenRabbit schema to existing labels.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
"type": {
|
||||||
|
"bug": "Kind/Bug",
|
||||||
|
"feature": "Kind/Feature",
|
||||||
|
},
|
||||||
|
"priority": {
|
||||||
|
"high": "Priority - High",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
mapping = {}
|
||||||
|
label_names_lower = {
|
||||||
|
label["name"].lower(): label["name"] for label in existing_labels
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get all configured labels with their aliases
|
||||||
|
labels_config = self.config.get("labels", {})
|
||||||
|
|
||||||
|
for category in ["type", "priority", "status"]:
|
||||||
|
category_config = labels_config.get(category, {})
|
||||||
|
mapping[category] = {}
|
||||||
|
|
||||||
|
for key, label_def in category_config.items():
|
||||||
|
config = self._get_label_config(category, key)
|
||||||
|
aliases = config.get("aliases", [])
|
||||||
|
|
||||||
|
# Try to find a match using aliases
|
||||||
|
for alias in aliases:
|
||||||
|
if alias.lower() in label_names_lower:
|
||||||
|
mapping[category][key] = label_names_lower[alias.lower()]
|
||||||
|
break
|
||||||
|
|
||||||
|
return mapping
|
||||||
|
|
||||||
|
def _setup_labels_map_mode(
|
||||||
|
self,
|
||||||
|
owner: str,
|
||||||
|
repo: str,
|
||||||
|
existing_labels: list[dict],
|
||||||
|
schema: dict,
|
||||||
|
existing_names: dict,
|
||||||
|
) -> str:
|
||||||
|
"""Map existing labels to OpenRabbit schema."""
|
||||||
|
|
||||||
|
# Build mapping
|
||||||
|
mapping = self._build_label_mapping(existing_labels, schema)
|
||||||
|
|
||||||
|
# Get required labels
|
||||||
|
required_labels = self._get_required_labels()
|
||||||
|
|
||||||
|
# Find missing labels
|
||||||
|
missing = []
|
||||||
|
for category, items in required_labels.items():
|
||||||
|
for key in items:
|
||||||
|
if key not in mapping.get(category, {}):
|
||||||
|
missing.append((category, key))
|
||||||
|
|
||||||
|
# Format report
|
||||||
|
lines = [f"{self.AI_DISCLAIMER}\n"]
|
||||||
|
lines.append("## Label Schema Detected\n")
|
||||||
|
lines.append(
|
||||||
|
f"Found {len(existing_labels)} existing labels with pattern: `{schema['pattern']}`\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
lines.append("**Detected Categories:**")
|
||||||
|
for category, labels in schema["categories"].items():
|
||||||
|
lines.append(f"- **{category.title()}** ({len(labels)} labels)")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
lines.append("**Proposed Mapping:**\n")
|
||||||
|
lines.append("| OpenRabbit Expected | Your Existing Label | Status |")
|
||||||
|
lines.append("|---------------------|---------------------|--------|")
|
||||||
|
|
||||||
|
for category, items in required_labels.items():
|
||||||
|
for key in items:
|
||||||
|
openrabbit_config = self._get_label_config(category, key)
|
||||||
|
openrabbit_name = openrabbit_config["name"]
|
||||||
|
|
||||||
|
if key in mapping.get(category, {}):
|
||||||
|
existing_name = mapping[category][key]
|
||||||
|
lines.append(
|
||||||
|
f"| `{openrabbit_name}` | `{existing_name}` | ✅ Map |"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
lines.append(f"| `{openrabbit_name}` | *(missing)* | ⚠️ Create |")
|
||||||
|
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# Create missing labels
|
||||||
|
if missing:
|
||||||
|
lines.append(f"**Creating Missing Labels ({len(missing)}):**\n")
|
||||||
|
created_count = 0
|
||||||
|
|
||||||
|
for category, key in missing:
|
||||||
|
config = self._get_label_config(category, key)
|
||||||
|
suggested_name = self._suggest_label_name(
|
||||||
|
category, key, schema["pattern"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if label already exists (case-insensitive)
|
||||||
|
if suggested_name.lower() not in existing_names:
|
||||||
|
try:
|
||||||
|
self.gitea.create_label(
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
suggested_name,
|
||||||
|
config["color"],
|
||||||
|
config["description"],
|
||||||
|
)
|
||||||
|
lines.append(
|
||||||
|
f"✅ Created `{suggested_name}` (#{config['color']})"
|
||||||
|
)
|
||||||
|
created_count += 1
|
||||||
|
except Exception as e:
|
||||||
|
lines.append(f"❌ Failed to create `{suggested_name}`: {e}")
|
||||||
|
else:
|
||||||
|
lines.append(f"⚠️ `{suggested_name}` already exists")
|
||||||
|
|
||||||
|
lines.append("")
|
||||||
|
if created_count > 0:
|
||||||
|
lines.append(f"**✅ Created {created_count} new labels!**")
|
||||||
|
else:
|
||||||
|
lines.append("**✅ All Required Labels Present!**")
|
||||||
|
|
||||||
|
lines.append("\n**Setup Complete!**")
|
||||||
|
lines.append("Auto-labeling will use your existing label schema.")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def _setup_labels_create_mode(
|
||||||
|
self, owner: str, repo: str, existing_names: dict
|
||||||
|
) -> str:
|
||||||
|
"""Create OpenRabbit default labels."""
|
||||||
|
|
||||||
|
lines = [f"{self.AI_DISCLAIMER}\n"]
|
||||||
|
lines.append("## Creating OpenRabbit Labels\n")
|
||||||
|
|
||||||
|
# Get all required labels
|
||||||
|
required_labels = self._get_required_labels()
|
||||||
|
|
||||||
|
created = []
|
||||||
|
skipped = []
|
||||||
|
failed = []
|
||||||
|
|
||||||
|
for category, items in required_labels.items():
|
||||||
|
for key in items:
|
||||||
|
config = self._get_label_config(category, key)
|
||||||
|
label_name = config["name"]
|
||||||
|
|
||||||
|
# Check if already exists (case-insensitive)
|
||||||
|
if label_name.lower() in existing_names:
|
||||||
|
skipped.append(label_name)
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.gitea.create_label(
|
||||||
|
owner, repo, label_name, config["color"], config["description"]
|
||||||
|
)
|
||||||
|
created.append((label_name, config["color"]))
|
||||||
|
except Exception as e:
|
||||||
|
failed.append((label_name, str(e)))
|
||||||
|
|
||||||
|
if created:
|
||||||
|
lines.append(f"**✅ Created {len(created)} Labels:**\n")
|
||||||
|
for name, color in created:
|
||||||
|
lines.append(f"- `{name}` (#{color})")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
if skipped:
|
||||||
|
lines.append(f"**⚠️ Skipped {len(skipped)} Existing Labels:**\n")
|
||||||
|
for name in skipped:
|
||||||
|
lines.append(f"- `{name}`")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
if failed:
|
||||||
|
lines.append(f"**❌ Failed to Create {len(failed)} Labels:**\n")
|
||||||
|
for name, error in failed:
|
||||||
|
lines.append(f"- `{name}`: {error}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
lines.append("**✅ Setup Complete!**")
|
||||||
|
lines.append("Auto-labeling is now configured.")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def _get_required_labels(self) -> dict:
|
||||||
|
"""Get all required label categories and keys.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
"type": ["bug", "feature", "question", "docs"],
|
||||||
|
"priority": ["high", "medium", "low"],
|
||||||
|
"status": ["ai_approved", "ai_changes_required", "ai_reviewed"]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
labels_config = self.config.get("labels", {})
|
||||||
|
required = {}
|
||||||
|
|
||||||
|
for category in ["type", "priority", "status"]:
|
||||||
|
category_config = labels_config.get(category, {})
|
||||||
|
required[category] = list(category_config.keys())
|
||||||
|
|
||||||
|
return required
|
||||||
|
|
||||||
|
def _suggest_label_name(self, category: str, key: str, pattern: str) -> str:
|
||||||
|
"""Suggest a label name based on detected pattern."""
|
||||||
|
|
||||||
|
# Get the configured name first
|
||||||
|
config = self._get_label_config(category, key)
|
||||||
|
base_name = config["name"]
|
||||||
|
|
||||||
|
if pattern == "prefix_slash":
|
||||||
|
prefix = "Kind" if category == "type" else category.title()
|
||||||
|
value = key.replace("_", " ").title()
|
||||||
|
return f"{prefix}/{value}"
|
||||||
|
elif pattern == "prefix_dash":
|
||||||
|
prefix = "Kind" if category == "type" else category.title()
|
||||||
|
value = key.replace("_", " ").title()
|
||||||
|
return f"{prefix} - {value}"
|
||||||
|
else: # colon or unknown
|
||||||
|
return base_name
|
||||||
|
|||||||
@@ -40,6 +40,37 @@ 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 -->"
|
||||||
|
|
||||||
|
def _get_label_config(self, category: str, key: str) -> dict:
|
||||||
|
"""Get full label configuration from config.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
category: Label category (type, priority, status)
|
||||||
|
key: Label key within category (bug, high, ai_approved, etc.)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with name, color, description, aliases
|
||||||
|
"""
|
||||||
|
labels_config = self.config.get("labels", {})
|
||||||
|
category_config = labels_config.get(category, {})
|
||||||
|
label_config = category_config.get(key, {})
|
||||||
|
|
||||||
|
# Handle old string format
|
||||||
|
if isinstance(label_config, str):
|
||||||
|
return {
|
||||||
|
"name": label_config,
|
||||||
|
"color": "1d76db", # Default blue
|
||||||
|
"description": "",
|
||||||
|
"aliases": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Handle new dict format
|
||||||
|
return {
|
||||||
|
"name": label_config.get("name", ""),
|
||||||
|
"color": label_config.get("color", "1d76db"),
|
||||||
|
"description": label_config.get("description", ""),
|
||||||
|
"aliases": label_config.get("aliases", []),
|
||||||
|
}
|
||||||
|
|
||||||
def can_handle(self, event_type: str, event_data: dict) -> bool:
|
def can_handle(self, event_type: str, event_data: dict) -> bool:
|
||||||
"""Check if this agent handles the given event."""
|
"""Check if this agent handles the given event."""
|
||||||
# Check if agent is enabled
|
# Check if agent is enabled
|
||||||
@@ -52,10 +83,30 @@ 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)
|
||||||
|
if event_type == "issue_comment":
|
||||||
|
action = event_data.get("action", "")
|
||||||
|
if action == "created":
|
||||||
|
comment_body = event_data.get("comment", {}).get("body", "")
|
||||||
|
mention_prefix = self.config.get("interaction", {}).get(
|
||||||
|
"mention_prefix", "@codebot"
|
||||||
|
)
|
||||||
|
# Only handle if this is a PR and contains review-again command
|
||||||
|
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
|
||||||
|
|
||||||
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
|
||||||
|
if context.event_type == "issue_comment":
|
||||||
|
return self._handle_review_again(context)
|
||||||
|
|
||||||
pr = context.event_data.get("pull_request", {})
|
pr = context.event_data.get("pull_request", {})
|
||||||
pr_number = pr.get("number")
|
pr_number = pr.get("number")
|
||||||
|
|
||||||
@@ -185,7 +236,7 @@ class PRAgent(BaseAgent):
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Hardcoded IP",
|
"name": "Hardcoded IP",
|
||||||
"pattern": r'\b(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b',
|
"pattern": r"\b(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b",
|
||||||
"severity": "LOW",
|
"severity": "LOW",
|
||||||
"category": "Security",
|
"category": "Security",
|
||||||
"description": "Hardcoded IP address detected",
|
"description": "Hardcoded IP address detected",
|
||||||
@@ -193,7 +244,7 @@ class PRAgent(BaseAgent):
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Eval Usage",
|
"name": "Eval Usage",
|
||||||
"pattern": r'\beval\s*\(',
|
"pattern": r"\beval\s*\(",
|
||||||
"severity": "HIGH",
|
"severity": "HIGH",
|
||||||
"category": "Security",
|
"category": "Security",
|
||||||
"description": "Use of eval() detected - potential code injection risk",
|
"description": "Use of eval() detected - potential code injection risk",
|
||||||
@@ -201,7 +252,7 @@ class PRAgent(BaseAgent):
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Shell Injection",
|
"name": "Shell Injection",
|
||||||
"pattern": r'(?i)(?:subprocess\.call|os\.system|shell\s*=\s*True)',
|
"pattern": r"(?i)(?:subprocess\.call|os\.system|shell\s*=\s*True)",
|
||||||
"severity": "MEDIUM",
|
"severity": "MEDIUM",
|
||||||
"category": "Security",
|
"category": "Security",
|
||||||
"description": "Potential shell command execution - verify input is sanitized",
|
"description": "Potential shell command execution - verify input is sanitized",
|
||||||
@@ -373,7 +424,9 @@ class PRAgent(BaseAgent):
|
|||||||
lines.append("### Security Issues")
|
lines.append("### Security Issues")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
for issue in review.security_issues[:5]:
|
for issue in review.security_issues[:5]:
|
||||||
lines.append(f"- **[{issue.severity}]** `{issue.file}:{issue.line}` - {issue.description}")
|
lines.append(
|
||||||
|
f"- **[{issue.severity}]** `{issue.file}:{issue.line}` - {issue.description}"
|
||||||
|
)
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
# Other issues (limit display)
|
# Other issues (limit display)
|
||||||
@@ -382,7 +435,9 @@ class PRAgent(BaseAgent):
|
|||||||
lines.append("### Review Findings")
|
lines.append("### Review Findings")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
for issue in other_issues[:10]:
|
for issue in other_issues[:10]:
|
||||||
loc = f"`{issue.file}:{issue.line}`" if issue.line else f"`{issue.file}`"
|
loc = (
|
||||||
|
f"`{issue.file}:{issue.line}`" if issue.line else f"`{issue.file}`"
|
||||||
|
)
|
||||||
lines.append(f"- **[{issue.severity}]** {loc} - {issue.description}")
|
lines.append(f"- **[{issue.severity}]** {loc} - {issue.description}")
|
||||||
if len(other_issues) > 10:
|
if len(other_issues) > 10:
|
||||||
lines.append(f"- ...and {len(other_issues) - 10} more issues")
|
lines.append(f"- ...and {len(other_issues) - 10} more issues")
|
||||||
@@ -406,8 +461,6 @@ class PRAgent(BaseAgent):
|
|||||||
review: PRReviewResult,
|
review: PRReviewResult,
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
"""Apply labels based on review result."""
|
"""Apply labels based on review result."""
|
||||||
labels_config = self.config.get("labels", {}).get("status", {})
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
repo_labels = self.gitea.get_repo_labels(owner, repo)
|
repo_labels = self.gitea.get_repo_labels(owner, repo)
|
||||||
label_map = {l["name"]: l["id"] for l in repo_labels}
|
label_map = {l["name"]: l["id"] for l in repo_labels}
|
||||||
@@ -415,15 +468,319 @@ class PRAgent(BaseAgent):
|
|||||||
self.logger.warning(f"Failed to get repo labels: {e}")
|
self.logger.warning(f"Failed to get repo labels: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def _handle_review_again(self, context: AgentContext) -> AgentResult:
|
||||||
|
"""Re-run PR review on current state."""
|
||||||
|
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"Re-reviewing PR #{pr_number} at user request")
|
||||||
|
|
||||||
|
# Get previous review comment
|
||||||
|
previous_comment = self._find_previous_review(
|
||||||
|
context.owner, context.repo, pr_number
|
||||||
|
)
|
||||||
|
previous_findings = []
|
||||||
|
if previous_comment:
|
||||||
|
previous_findings = self._parse_review_comment(previous_comment)
|
||||||
|
|
||||||
|
# Run new review (reuse existing review logic)
|
||||||
|
actions_taken = []
|
||||||
|
|
||||||
|
# Step 1: Get PR diff
|
||||||
|
diff = self._get_diff(context.owner, context.repo, pr_number)
|
||||||
|
if not diff.strip():
|
||||||
|
response = f"@{comment_author}\n\n{self.AI_DISCLAIMER}\n\n**🔄 Re-review Requested**\n\nPR has no changes to review."
|
||||||
|
self.gitea.create_issue_comment(
|
||||||
|
context.owner, context.repo, pr_number, response
|
||||||
|
)
|
||||||
|
return AgentResult(
|
||||||
|
success=True,
|
||||||
|
message="PR has no changes to review",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 2: Parse changed files
|
||||||
|
changed_files = self._parse_diff_files(diff)
|
||||||
|
|
||||||
|
# Step 3: Run security scan if enabled
|
||||||
|
security_issues = []
|
||||||
|
agent_config = self.config.get("agents", {}).get("pr", {})
|
||||||
|
if agent_config.get("security_scan", True):
|
||||||
|
security_issues = self._run_security_scan(changed_files, diff)
|
||||||
|
|
||||||
|
# Step 4: Run AI review
|
||||||
|
review_result = self._run_ai_review(diff, context, security_issues)
|
||||||
|
|
||||||
|
# Step 5: Compare with previous review
|
||||||
|
current_findings = self._extract_findings_from_review(review_result)
|
||||||
|
diff_result = self._compare_reviews(previous_findings, current_findings)
|
||||||
|
|
||||||
|
# Step 6: Generate updated review with comparison
|
||||||
|
updated_review = self._format_review_update(
|
||||||
|
review_result, diff_result, comment_author
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 7: Update existing comment (or create new one)
|
||||||
|
self.upsert_comment(
|
||||||
|
context.owner,
|
||||||
|
context.repo,
|
||||||
|
pr_number,
|
||||||
|
updated_review,
|
||||||
|
marker=self.PR_AI_MARKER,
|
||||||
|
)
|
||||||
|
actions_taken.append("Updated review comment")
|
||||||
|
|
||||||
|
# Step 8: Update PR labels
|
||||||
|
labels_applied = self._apply_review_labels(
|
||||||
|
context.owner, context.repo, pr_number, review_result
|
||||||
|
)
|
||||||
|
if labels_applied:
|
||||||
|
actions_taken.append(f"Updated labels: {labels_applied}")
|
||||||
|
|
||||||
|
return AgentResult(
|
||||||
|
success=True,
|
||||||
|
message=f"Re-reviewed PR #{pr_number}: {review_result.overall_severity} severity",
|
||||||
|
data={
|
||||||
|
"severity": review_result.overall_severity,
|
||||||
|
"approval": review_result.approval,
|
||||||
|
"issues_count": len(review_result.issues),
|
||||||
|
"security_issues_count": len(review_result.security_issues),
|
||||||
|
"resolved_count": len(diff_result.get("resolved", [])),
|
||||||
|
"new_count": len(diff_result.get("new", [])),
|
||||||
|
},
|
||||||
|
actions_taken=actions_taken,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _find_previous_review(
|
||||||
|
self, owner: str, repo: str, pr_number: int
|
||||||
|
) -> str | None:
|
||||||
|
"""Find the previous AI review comment."""
|
||||||
|
comment_id = self.find_ai_comment(
|
||||||
|
owner, repo, pr_number, marker=self.PR_AI_MARKER
|
||||||
|
)
|
||||||
|
if not comment_id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get the comment content
|
||||||
|
comments = self.gitea.list_issue_comments(owner, repo, pr_number)
|
||||||
|
for comment in comments:
|
||||||
|
if comment.get("id") == comment_id:
|
||||||
|
return comment.get("body", "")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _parse_review_comment(self, comment_text: str) -> list[dict]:
|
||||||
|
"""Parse previous review comment to extract findings.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of findings with file, line, severity, description
|
||||||
|
"""
|
||||||
|
findings = []
|
||||||
|
|
||||||
|
if not comment_text:
|
||||||
|
return findings
|
||||||
|
|
||||||
|
# Look for patterns like: **[HIGH]** `src/file.py:45` - Description
|
||||||
|
pattern = r"\*\*\[(\w+)\]\*\*\s+`([^:]+):(\d+)`\s+-\s+(.+?)(?:\n|$)"
|
||||||
|
|
||||||
|
for match in re.finditer(pattern, comment_text):
|
||||||
|
findings.append(
|
||||||
|
{
|
||||||
|
"severity": match.group(1),
|
||||||
|
"file": match.group(2),
|
||||||
|
"line": int(match.group(3)),
|
||||||
|
"description": match.group(4).strip(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return findings
|
||||||
|
|
||||||
|
def _extract_findings_from_review(self, review: PRReviewResult) -> list[dict]:
|
||||||
|
"""Extract findings from PRReviewResult into comparable format."""
|
||||||
|
findings = []
|
||||||
|
all_issues = review.issues + review.security_issues
|
||||||
|
|
||||||
|
for issue in all_issues:
|
||||||
|
findings.append(
|
||||||
|
{
|
||||||
|
"severity": issue.severity,
|
||||||
|
"file": issue.file,
|
||||||
|
"line": issue.line or 0,
|
||||||
|
"description": issue.description,
|
||||||
|
"category": issue.category,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return findings
|
||||||
|
|
||||||
|
def _finding_key(self, finding: dict) -> str:
|
||||||
|
"""Create unique key for a finding."""
|
||||||
|
file_path = finding.get("file", "unknown")
|
||||||
|
line = finding.get("line", 0)
|
||||||
|
# Use first 50 chars of description for matching
|
||||||
|
desc_key = finding.get("description", "")[:50]
|
||||||
|
return f"{file_path}:{line}:{desc_key}"
|
||||||
|
|
||||||
|
def _compare_reviews(
|
||||||
|
self, previous_findings: list[dict], new_findings: list[dict]
|
||||||
|
) -> dict:
|
||||||
|
"""Compare previous and new review to show what changed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
"resolved": [...], # Issues that disappeared
|
||||||
|
"new": [...], # New issues found
|
||||||
|
"still_present": [...], # Issues that remain
|
||||||
|
"severity_changed": {...} # OLD severity -> NEW severity
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
# Create lookup keys (file:line:description)
|
||||||
|
prev_keys = {self._finding_key(f): f for f in previous_findings}
|
||||||
|
new_keys = {self._finding_key(f): f for f in new_findings}
|
||||||
|
|
||||||
|
resolved = [prev_keys[key] for key in prev_keys if key not in new_keys]
|
||||||
|
|
||||||
|
new = [new_keys[key] for key in new_keys if key not in prev_keys]
|
||||||
|
|
||||||
|
still_present = [new_keys[key] for key in new_keys if key in prev_keys]
|
||||||
|
|
||||||
|
severity_changed = {}
|
||||||
|
for key in prev_keys:
|
||||||
|
if key in new_keys:
|
||||||
|
prev_severity = prev_keys[key].get("severity")
|
||||||
|
new_severity = new_keys[key].get("severity")
|
||||||
|
if prev_severity != new_severity:
|
||||||
|
severity_changed[key] = {
|
||||||
|
"old": prev_severity,
|
||||||
|
"new": new_severity,
|
||||||
|
"finding": new_keys[key],
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"resolved": resolved,
|
||||||
|
"new": new,
|
||||||
|
"still_present": still_present,
|
||||||
|
"severity_changed": severity_changed,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _format_review_update(
|
||||||
|
self, review: PRReviewResult, diff: dict, comment_author: str
|
||||||
|
) -> str:
|
||||||
|
"""Format review with comparison to previous run."""
|
||||||
|
lines = [f"@{comment_author}\n"]
|
||||||
|
lines.append(f"{self.AI_DISCLAIMER}\n")
|
||||||
|
lines.append("**🔄 Re-review Requested**\n")
|
||||||
|
lines.append("---\n")
|
||||||
|
lines.append("## AI Code Review (Updated)\n")
|
||||||
|
|
||||||
|
# Summary of changes
|
||||||
|
prev_total = len(diff["resolved"]) + len(diff["still_present"])
|
||||||
|
curr_total = len(diff["new"]) + len(diff["still_present"])
|
||||||
|
|
||||||
|
if prev_total > 0:
|
||||||
|
lines.append(f"**Previous Review:** {prev_total} issues")
|
||||||
|
lines.append(f"**Current Review:** {curr_total} issues\n")
|
||||||
|
else:
|
||||||
|
lines.append("**First Review** - No previous review found\n")
|
||||||
|
|
||||||
|
# Changes section (only if there was a previous review)
|
||||||
|
if prev_total > 0:
|
||||||
|
lines.append("### Changes from Previous Review\n")
|
||||||
|
|
||||||
|
if diff["resolved"]:
|
||||||
|
lines.append(f"**✅ Resolved ({len(diff['resolved'])}):**")
|
||||||
|
for finding in diff["resolved"][:5]: # Show max 5
|
||||||
|
lines.append(
|
||||||
|
f"- **[{finding['severity']}]** `{finding['file']}:{finding['line']}` - {finding['description']}"
|
||||||
|
)
|
||||||
|
if len(diff["resolved"]) > 5:
|
||||||
|
lines.append(f"- ... and {len(diff['resolved']) - 5} more")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
if diff["new"]:
|
||||||
|
lines.append(f"**⚠️ New Issues ({len(diff['new'])}):**")
|
||||||
|
for finding in diff["new"][:5]:
|
||||||
|
lines.append(
|
||||||
|
f"- **[{finding['severity']}]** `{finding['file']}:{finding['line']}` - {finding['description']}"
|
||||||
|
)
|
||||||
|
if len(diff["new"]) > 5:
|
||||||
|
lines.append(f"- ... and {len(diff['new']) - 5} more")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
if diff["severity_changed"]:
|
||||||
|
lines.append(
|
||||||
|
f"**🔄 Severity Changed ({len(diff['severity_changed'])}):**"
|
||||||
|
)
|
||||||
|
for key, change in list(diff["severity_changed"].items())[:5]:
|
||||||
|
finding = change["finding"]
|
||||||
|
lines.append(
|
||||||
|
f"- `{finding['file']}:{finding['line']}` - {change['old']} → {change['new']}"
|
||||||
|
)
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# Summary table
|
||||||
|
all_issues = review.issues + review.security_issues
|
||||||
|
high = sum(1 for i in all_issues if i.severity == "HIGH")
|
||||||
|
medium = sum(1 for i in all_issues if i.severity == "MEDIUM")
|
||||||
|
low = sum(1 for i in all_issues if i.severity == "LOW")
|
||||||
|
|
||||||
|
lines.append("### Summary\n")
|
||||||
|
lines.append("| Severity | Count |")
|
||||||
|
lines.append("|----------|-------|")
|
||||||
|
lines.append(f"| HIGH | {high} |")
|
||||||
|
lines.append(f"| MEDIUM | {medium} |")
|
||||||
|
lines.append(f"| LOW | {low} |")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# Security issues section (if any)
|
||||||
|
if review.security_issues:
|
||||||
|
lines.append("### Security Issues\n")
|
||||||
|
for issue in review.security_issues[:5]:
|
||||||
|
loc = (
|
||||||
|
f"`{issue.file}:{issue.line}`" if issue.line else f"`{issue.file}`"
|
||||||
|
)
|
||||||
|
lines.append(f"- **[{issue.severity}]** {loc} - {issue.description}")
|
||||||
|
if len(review.security_issues) > 5:
|
||||||
|
lines.append(f"- ... and {len(review.security_issues) - 5} more")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# Other issues (limit display)
|
||||||
|
other_issues = [i for i in review.issues if i not in review.security_issues]
|
||||||
|
if other_issues:
|
||||||
|
lines.append("### Review Findings\n")
|
||||||
|
for issue in other_issues[:10]:
|
||||||
|
loc = (
|
||||||
|
f"`{issue.file}:{issue.line}`" if issue.line else f"`{issue.file}`"
|
||||||
|
)
|
||||||
|
lines.append(f"- **[{issue.severity}]** {loc} - {issue.description}")
|
||||||
|
if len(other_issues) > 10:
|
||||||
|
lines.append(f"- ... and {len(other_issues) - 10} more issues")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# Verdict
|
||||||
|
lines.append("---")
|
||||||
|
lines.append(f"**Overall Severity:** `{review.overall_severity}`")
|
||||||
|
if review.approval:
|
||||||
|
lines.append("**AI Recommendation:** Approved ✅")
|
||||||
|
else:
|
||||||
|
lines.append("**AI Recommendation:** Changes Requested ⚠️")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
labels_to_add = []
|
labels_to_add = []
|
||||||
|
|
||||||
# Add approval/changes required label
|
# Add approval/changes required label
|
||||||
|
# Use helper to support both old string and new dict format
|
||||||
if review.approval:
|
if review.approval:
|
||||||
label_name = labels_config.get("ai_approved", "ai-approved")
|
label_config = self._get_label_config("status", "ai_approved")
|
||||||
else:
|
else:
|
||||||
label_name = labels_config.get("ai_changes_required", "ai-changes-required")
|
label_config = self._get_label_config("status", "ai_changes_required")
|
||||||
|
|
||||||
if label_name in label_map:
|
label_name = label_config.get("name", "")
|
||||||
|
|
||||||
|
if label_name and label_name in label_map:
|
||||||
labels_to_add.append(label_map[label_name])
|
labels_to_add.append(label_map[label_name])
|
||||||
|
|
||||||
if labels_to_add:
|
if labels_to_add:
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ class GiteaClient:
|
|||||||
timeout=self.timeout,
|
timeout=self.timeout,
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
if response.status_code == 204:
|
if response.status_code == 204:
|
||||||
return {}
|
return {}
|
||||||
return response.json()
|
return response.json()
|
||||||
@@ -293,10 +293,45 @@ class GiteaClient:
|
|||||||
repo: Repository name.
|
repo: Repository name.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of label objects.
|
List of label objects with 'id', 'name', 'color', 'description' fields.
|
||||||
"""
|
"""
|
||||||
return self._request("GET", f"/repos/{owner}/{repo}/labels")
|
return self._request("GET", f"/repos/{owner}/{repo}/labels")
|
||||||
|
|
||||||
|
def create_label(
|
||||||
|
self,
|
||||||
|
owner: str,
|
||||||
|
repo: str,
|
||||||
|
name: str,
|
||||||
|
color: str,
|
||||||
|
description: str = "",
|
||||||
|
) -> dict:
|
||||||
|
"""Create a new label in the repository.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Repository owner.
|
||||||
|
repo: Repository name.
|
||||||
|
name: Label name (e.g., "priority: high").
|
||||||
|
color: Hex color code without # (e.g., "d73a4a").
|
||||||
|
description: Optional label description.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Created label object.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
requests.HTTPError: If label creation fails (e.g., already exists).
|
||||||
|
"""
|
||||||
|
payload = {
|
||||||
|
"name": name,
|
||||||
|
"color": color,
|
||||||
|
"description": description,
|
||||||
|
}
|
||||||
|
|
||||||
|
return self._request(
|
||||||
|
"POST",
|
||||||
|
f"/repos/{owner}/{repo}/labels",
|
||||||
|
json=payload,
|
||||||
|
)
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# Pull Request Operations
|
# Pull Request Operations
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -59,11 +59,13 @@ interaction:
|
|||||||
respond_to_mentions: true
|
respond_to_mentions: true
|
||||||
mention_prefix: "@codebot" # Change this to customize your bot's name!
|
mention_prefix: "@codebot" # Change this to customize your bot's name!
|
||||||
commands:
|
commands:
|
||||||
|
- help
|
||||||
- explain
|
- explain
|
||||||
- suggest
|
- suggest
|
||||||
- security
|
- security
|
||||||
- summarize
|
- summarize
|
||||||
- triage
|
- triage
|
||||||
|
- review-again
|
||||||
|
|
||||||
# Enterprise settings
|
# Enterprise settings
|
||||||
enterprise:
|
enterprise:
|
||||||
@@ -75,20 +77,149 @@ enterprise:
|
|||||||
max_concurrent: 4
|
max_concurrent: 4
|
||||||
|
|
||||||
# Label mappings for auto-labeling
|
# Label mappings for auto-labeling
|
||||||
|
# Each label has:
|
||||||
|
# name: The label name to use/create (string) or full config (dict)
|
||||||
|
# aliases: Alternative names for auto-detection (optional)
|
||||||
|
# color: Hex color code without # (optional, for label creation)
|
||||||
|
# description: Label description (optional, for label creation)
|
||||||
labels:
|
labels:
|
||||||
priority:
|
priority:
|
||||||
high: "priority: high"
|
critical:
|
||||||
medium: "priority: medium"
|
name: "priority: critical"
|
||||||
low: "priority: low"
|
color: "b60205" # Dark Red
|
||||||
|
description: "Critical priority - immediate attention required"
|
||||||
|
aliases:
|
||||||
|
["Priority - Critical", "P0", "critical", "Priority/Critical"]
|
||||||
|
high:
|
||||||
|
name: "priority: high"
|
||||||
|
color: "d73a4a" # Red
|
||||||
|
description: "High priority issue"
|
||||||
|
aliases: ["Priority - High", "P1", "high", "Priority/High"]
|
||||||
|
medium:
|
||||||
|
name: "priority: medium"
|
||||||
|
color: "fbca04" # Yellow
|
||||||
|
description: "Medium priority issue"
|
||||||
|
aliases: ["Priority - Medium", "P2", "medium", "Priority/Medium"]
|
||||||
|
low:
|
||||||
|
name: "priority: low"
|
||||||
|
color: "28a745" # Green
|
||||||
|
description: "Low priority issue"
|
||||||
|
aliases: ["Priority - Low", "P3", "low", "Priority/Low"]
|
||||||
type:
|
type:
|
||||||
bug: "type: bug"
|
bug:
|
||||||
feature: "type: feature"
|
name: "type: bug"
|
||||||
question: "type: question"
|
color: "d73a4a" # Red
|
||||||
docs: "type: documentation"
|
description: "Something isn't working"
|
||||||
|
aliases: ["Kind/Bug", "bug", "Type: Bug", "Type/Bug", "Kind - Bug"]
|
||||||
|
feature:
|
||||||
|
name: "type: feature"
|
||||||
|
color: "1d76db" # Blue
|
||||||
|
description: "New feature request"
|
||||||
|
aliases:
|
||||||
|
[
|
||||||
|
"Kind/Feature",
|
||||||
|
"feature",
|
||||||
|
"enhancement",
|
||||||
|
"Kind/Enhancement",
|
||||||
|
"Type: Feature",
|
||||||
|
"Type/Feature",
|
||||||
|
"Kind - Feature",
|
||||||
|
]
|
||||||
|
question:
|
||||||
|
name: "type: question"
|
||||||
|
color: "cc317c" # Purple
|
||||||
|
description: "Further information is requested"
|
||||||
|
aliases:
|
||||||
|
[
|
||||||
|
"Kind/Question",
|
||||||
|
"question",
|
||||||
|
"Type: Question",
|
||||||
|
"Type/Question",
|
||||||
|
"Kind - Question",
|
||||||
|
]
|
||||||
|
docs:
|
||||||
|
name: "type: documentation"
|
||||||
|
color: "0075ca" # Light Blue
|
||||||
|
description: "Documentation improvements"
|
||||||
|
aliases:
|
||||||
|
[
|
||||||
|
"Kind/Documentation",
|
||||||
|
"documentation",
|
||||||
|
"docs",
|
||||||
|
"Type: Documentation",
|
||||||
|
"Type/Documentation",
|
||||||
|
"Kind - Documentation",
|
||||||
|
]
|
||||||
|
security:
|
||||||
|
name: "type: security"
|
||||||
|
color: "b60205" # Dark Red
|
||||||
|
description: "Security vulnerability or concern"
|
||||||
|
aliases:
|
||||||
|
[
|
||||||
|
"Kind/Security",
|
||||||
|
"security",
|
||||||
|
"Type: Security",
|
||||||
|
"Type/Security",
|
||||||
|
"Kind - Security",
|
||||||
|
]
|
||||||
|
testing:
|
||||||
|
name: "type: testing"
|
||||||
|
color: "0e8a16" # Green
|
||||||
|
description: "Related to testing"
|
||||||
|
aliases:
|
||||||
|
[
|
||||||
|
"Kind/Testing",
|
||||||
|
"testing",
|
||||||
|
"tests",
|
||||||
|
"Type: Testing",
|
||||||
|
"Type/Testing",
|
||||||
|
"Kind - Testing",
|
||||||
|
]
|
||||||
status:
|
status:
|
||||||
ai_approved: "ai-approved"
|
ai_approved:
|
||||||
ai_changes_required: "ai-changes-required"
|
name: "ai-approved"
|
||||||
ai_reviewed: "ai-reviewed"
|
color: "28a745" # Green
|
||||||
|
description: "AI review approved this PR"
|
||||||
|
aliases:
|
||||||
|
[
|
||||||
|
"Status - Approved",
|
||||||
|
"approved",
|
||||||
|
"Status/Approved",
|
||||||
|
"Status - AI Approved",
|
||||||
|
]
|
||||||
|
ai_changes_required:
|
||||||
|
name: "ai-changes-required"
|
||||||
|
color: "d73a4a" # Red
|
||||||
|
description: "AI review found issues requiring changes"
|
||||||
|
aliases:
|
||||||
|
[
|
||||||
|
"Status - Changes Required",
|
||||||
|
"changes-required",
|
||||||
|
"Status/Changes Required",
|
||||||
|
"Status - AI Changes Required",
|
||||||
|
]
|
||||||
|
ai_reviewed:
|
||||||
|
name: "ai-reviewed"
|
||||||
|
color: "1d76db" # Blue
|
||||||
|
description: "This issue/PR has been reviewed by AI"
|
||||||
|
aliases:
|
||||||
|
[
|
||||||
|
"Reviewed - Confirmed",
|
||||||
|
"reviewed",
|
||||||
|
"Status/Reviewed",
|
||||||
|
"Reviewed/Confirmed",
|
||||||
|
"Status - Reviewed",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Label schema detection patterns
|
||||||
|
# Used by setup-labels command to detect existing naming conventions
|
||||||
|
label_patterns:
|
||||||
|
# Detect prefix-based naming (e.g., Kind/Bug, Type/Feature)
|
||||||
|
prefix_slash: "^(Kind|Type|Category)/(.+)$"
|
||||||
|
# Detect dash-separated naming (e.g., Priority - High, Status - Blocked)
|
||||||
|
prefix_dash: "^(Priority|Status|Reviewed) - (.+)$"
|
||||||
|
# Detect colon-separated naming (e.g., type: bug, priority: high)
|
||||||
|
colon: "^(type|priority|status): (.+)$"
|
||||||
|
|
||||||
# Security scanning rules
|
# Security scanning rules
|
||||||
security:
|
security:
|
||||||
|
|||||||
Reference in New Issue
Block a user