Spaces:
Sleeping
Sleeping
github-actions[bot] commited on
Commit Β·
fe15a7c
1
Parent(s): f0bbf9c
Deploy from GitHub commit 1b2eb65
Browse files- .env.example +40 -0
- .spacesignore +69 -0
- Dockerfile +56 -0
- LICENSE +21 -0
- QUICKSTART_HF.md +69 -0
- README.md +161 -7
- app/__init__.py +3 -0
- app/api.py +301 -0
- app/config.py +169 -0
- app/crew/__init__.py +5 -0
- app/crew/agents.yaml +85 -0
- app/crew/crew.py +291 -0
- app/crew/tasks.yaml +202 -0
- app/guardrails.py +250 -0
- app/schemas.py +178 -0
- app/serve.py +74 -0
- app/utils.py +205 -0
- docker-compose.yml +54 -0
- pyproject.toml +92 -0
- requirements.txt +36 -0
- start.sh +84 -0
.env.example
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# LLM Provider Configuration
|
| 2 |
+
# Choose: "openai" or "groq"
|
| 3 |
+
LLM_PROVIDER=openai
|
| 4 |
+
|
| 5 |
+
# OpenAI Configuration
|
| 6 |
+
OPENAI_API_KEY=sk-your-openai-api-key-here
|
| 7 |
+
OPENAI_MODEL=gpt-4o-mini
|
| 8 |
+
|
| 9 |
+
# Groq Configuration (optional, alternative to OpenAI)
|
| 10 |
+
GROQ_API_KEY=gsk_your-groq-api-key-here
|
| 11 |
+
GROQ_MODEL=llama-3.3-70b-versatile
|
| 12 |
+
|
| 13 |
+
# API Authentication
|
| 14 |
+
REVIEW_API_KEY=your-secure-api-key-here
|
| 15 |
+
|
| 16 |
+
# Rate Limiting
|
| 17 |
+
RATE_LIMIT_PER_MINUTE=10
|
| 18 |
+
|
| 19 |
+
# Ray Serve Configuration
|
| 20 |
+
ENABLE_RAY_SERVE=false
|
| 21 |
+
RAY_SERVE_HOST=0.0.0.0
|
| 22 |
+
RAY_SERVE_PORT=8000
|
| 23 |
+
RAY_NUM_REPLICAS=2
|
| 24 |
+
RAY_MAX_CONCURRENT_QUERIES=10
|
| 25 |
+
|
| 26 |
+
# Guardrails Configuration
|
| 27 |
+
MAX_FINDINGS_PER_REVIEW=20
|
| 28 |
+
MAX_TOKENS_PER_REVIEW=15000
|
| 29 |
+
ENABLE_LLM_JUDGE_GUARDRAILS=true
|
| 30 |
+
|
| 31 |
+
# Application Settings
|
| 32 |
+
LOG_LEVEL=INFO
|
| 33 |
+
REQUEST_TIMEOUT_SECONDS=120
|
| 34 |
+
MAX_DIFF_SIZE_BYTES=1048576
|
| 35 |
+
|
| 36 |
+
# CORS Settings (comma-separated origins)
|
| 37 |
+
CORS_ORIGINS=*
|
| 38 |
+
|
| 39 |
+
# Debug Mode
|
| 40 |
+
DEBUG=false
|
.spacesignore
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Exclude files not needed in Hugging Face Space
|
| 2 |
+
# This reduces upload time and space size
|
| 3 |
+
|
| 4 |
+
# Development files
|
| 5 |
+
tests/
|
| 6 |
+
.pytest_cache/
|
| 7 |
+
__pycache__/
|
| 8 |
+
*.pyc
|
| 9 |
+
*.pyo
|
| 10 |
+
*.pyd
|
| 11 |
+
.Python
|
| 12 |
+
|
| 13 |
+
# CI/CD
|
| 14 |
+
.github/
|
| 15 |
+
.git/
|
| 16 |
+
.gitignore
|
| 17 |
+
|
| 18 |
+
# Documentation
|
| 19 |
+
DEPLOYMENT.md
|
| 20 |
+
README.md
|
| 21 |
+
docs/
|
| 22 |
+
|
| 23 |
+
# IDE
|
| 24 |
+
.vscode/
|
| 25 |
+
.idea/
|
| 26 |
+
*.swp
|
| 27 |
+
*.swo
|
| 28 |
+
*~
|
| 29 |
+
|
| 30 |
+
# Environment
|
| 31 |
+
.env
|
| 32 |
+
.env.local
|
| 33 |
+
.env.*.local
|
| 34 |
+
venv/
|
| 35 |
+
env/
|
| 36 |
+
ENV/
|
| 37 |
+
|
| 38 |
+
# Build artifacts
|
| 39 |
+
build/
|
| 40 |
+
dist/
|
| 41 |
+
*.egg-info/
|
| 42 |
+
wheels/
|
| 43 |
+
|
| 44 |
+
# Logs
|
| 45 |
+
logs/
|
| 46 |
+
*.log
|
| 47 |
+
|
| 48 |
+
# Temporary files
|
| 49 |
+
tmp/
|
| 50 |
+
temp/
|
| 51 |
+
*.tmp
|
| 52 |
+
|
| 53 |
+
# Ray (not used in HF deployment)
|
| 54 |
+
/tmp/ray/
|
| 55 |
+
|
| 56 |
+
# MacOS
|
| 57 |
+
.DS_Store
|
| 58 |
+
|
| 59 |
+
# Scripts and tools
|
| 60 |
+
scripts/
|
| 61 |
+
run_e2e_tests.sh
|
| 62 |
+
verify_deployment.py
|
| 63 |
+
|
| 64 |
+
# Original Dockerfile (use Dockerfile.hf instead)
|
| 65 |
+
Dockerfile
|
| 66 |
+
|
| 67 |
+
# Large test fixtures
|
| 68 |
+
tests/fixtures/*.pdf
|
| 69 |
+
tests/fixtures/*.mp4
|
Dockerfile
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Optimized Dockerfile for Hugging Face Spaces Free Tier
|
| 2 |
+
FROM python:3.11-slim
|
| 3 |
+
|
| 4 |
+
# Set working directory
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Install system dependencies
|
| 8 |
+
RUN apt-get update && apt-get install -y \
|
| 9 |
+
git \
|
| 10 |
+
curl \
|
| 11 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 12 |
+
|
| 13 |
+
# Copy dependency files first (for layer caching)
|
| 14 |
+
COPY pyproject.toml ./
|
| 15 |
+
|
| 16 |
+
# Install Python dependencies (minimal for HF free tier)
|
| 17 |
+
# Exclude Ray Serve and heavy dependencies to reduce image size
|
| 18 |
+
RUN pip install --no-cache-dir --upgrade pip && \
|
| 19 |
+
pip install --no-cache-dir \
|
| 20 |
+
fastapi==0.124.0 \
|
| 21 |
+
uvicorn==0.38.0 \
|
| 22 |
+
pydantic==2.11.10 \
|
| 23 |
+
pydantic-settings==2.10.1 \
|
| 24 |
+
python-dotenv==1.1.1 \
|
| 25 |
+
httpx==0.27.0 \
|
| 26 |
+
python-multipart==0.0.20 \
|
| 27 |
+
pyyaml==6.0.1 \
|
| 28 |
+
tiktoken==0.9.0 \
|
| 29 |
+
openai==1.83.0 \
|
| 30 |
+
crewai==1.7.0 \
|
| 31 |
+
crewai-tools==1.7.0
|
| 32 |
+
|
| 33 |
+
# Copy application code
|
| 34 |
+
COPY app ./app
|
| 35 |
+
|
| 36 |
+
# Create necessary directories
|
| 37 |
+
RUN mkdir -p /app/logs
|
| 38 |
+
|
| 39 |
+
# Set environment variables for HF optimization
|
| 40 |
+
ENV PYTHONUNBUFFERED=1 \
|
| 41 |
+
PYTHONDONTWRITEBYTECODE=1 \
|
| 42 |
+
LOG_LEVEL=INFO \
|
| 43 |
+
ENABLE_RAY_SERVE=false \
|
| 44 |
+
RATE_LIMIT_PER_MINUTE=5 \
|
| 45 |
+
REQUEST_TIMEOUT_SECONDS=90 \
|
| 46 |
+
MAX_FINDINGS_PER_REVIEW=15
|
| 47 |
+
|
| 48 |
+
# Expose port 7860 (required by Hugging Face)
|
| 49 |
+
EXPOSE 7860
|
| 50 |
+
|
| 51 |
+
# Health check
|
| 52 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
| 53 |
+
CMD curl -f http://localhost:7860/health || exit 1
|
| 54 |
+
|
| 55 |
+
# Run the application
|
| 56 |
+
CMD ["uvicorn", "app.api:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1"]
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2024 Code Reviewer CI Agent Contributors
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
QUICKSTART_HF.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# π Quick Start: Deploy to Hugging Face
|
| 2 |
+
|
| 3 |
+
Follow these steps to deploy your Code Reviewer CI Agent to Hugging Face Spaces for FREE!
|
| 4 |
+
|
| 5 |
+
## Prerequisites
|
| 6 |
+
|
| 7 |
+
- GitHub account with this repository
|
| 8 |
+
- Hugging Face account (sign up at https://huggingface.co)
|
| 9 |
+
- LLM API key (Groq or OpenAI)
|
| 10 |
+
|
| 11 |
+
## Setup Steps
|
| 12 |
+
|
| 13 |
+
### 1. Create Hugging Face Token
|
| 14 |
+
|
| 15 |
+
1. Go to https://huggingface.co/settings/tokens
|
| 16 |
+
2. Click "New token" β Name: `github-deployment` β Type: **Write**
|
| 17 |
+
3. Copy the token
|
| 18 |
+
|
| 19 |
+
### 2. Create Hugging Face Space
|
| 20 |
+
|
| 21 |
+
1. Go to https://huggingface.co/new-space
|
| 22 |
+
2. Space name: `code-reviewer-ci`
|
| 23 |
+
3. SDK: **Docker**
|
| 24 |
+
4. Create Space
|
| 25 |
+
|
| 26 |
+
### 3. Add GitHub Secrets
|
| 27 |
+
|
| 28 |
+
Go to: **GitHub Repository β Settings β Secrets β Actions**
|
| 29 |
+
|
| 30 |
+
Add 3 secrets:
|
| 31 |
+
- `HF_TOKEN` = your token from step 1
|
| 32 |
+
- `HF_USERNAME` = your HF username
|
| 33 |
+
- `HF_SPACE_NAME` = `code-reviewer-ci`
|
| 34 |
+
|
| 35 |
+
### 4. Configure HF Space
|
| 36 |
+
|
| 37 |
+
Go to: **Your HF Space β Settings β Variables**
|
| 38 |
+
|
| 39 |
+
Add variables:
|
| 40 |
+
- `LLM_PROVIDER` = `groq`
|
| 41 |
+
- `REVIEW_API_KEY` = `(random secure string)`
|
| 42 |
+
- `GROQ_API_KEY` = `gsk_your_groq_key`
|
| 43 |
+
|
| 44 |
+
### 5. Deploy!
|
| 45 |
+
|
| 46 |
+
```bash
|
| 47 |
+
git add .
|
| 48 |
+
git commit -m "Deploy to Hugging Face"
|
| 49 |
+
git push origin main
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
β
**Done!** GitHub Actions will automatically deploy to HF.
|
| 53 |
+
|
| 54 |
+
## Verify Deployment
|
| 55 |
+
|
| 56 |
+
```bash
|
| 57 |
+
# Health check
|
| 58 |
+
curl https://YOUR-USERNAME-YOUR-SPACE.hf.space/health
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
## Full Documentation
|
| 62 |
+
|
| 63 |
+
- **Detailed setup:** [DEPLOYMENT.md](./DEPLOYMENT.md)
|
| 64 |
+
- **Troubleshooting:** See DEPLOYMENT.md#troubleshooting
|
| 65 |
+
- **Setup wizard:** Run `./scripts/setup_hf_deployment.sh`
|
| 66 |
+
|
| 67 |
+
---
|
| 68 |
+
|
| 69 |
+
**Need help?** See [DEPLOYMENT.md](./DEPLOYMENT.md) for complete instructions.
|
README.md
CHANGED
|
@@ -1,11 +1,165 @@
|
|
| 1 |
---
|
| 2 |
-
title: Code Reviewer
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
-
|
| 8 |
-
short_description: a code reviewer ai agent
|
| 9 |
---
|
| 10 |
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Code Reviewer CI Agent
|
| 3 |
+
emoji: π€
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 7860
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
+
# π€ AI Code Reviewer Agent
|
| 11 |
+
|
| 12 |
+
**Production-ready AI-powered code review using CrewAI multi-agent framework**
|
| 13 |
+
|
| 14 |
+
Automatically review code changes with specialized AI agents analyzing security, performance, code quality, and maintainability.
|
| 15 |
+
|
| 16 |
+
## π Quick Start
|
| 17 |
+
|
| 18 |
+
### API Endpoints
|
| 19 |
+
|
| 20 |
+
#### Health Check
|
| 21 |
+
```bash
|
| 22 |
+
curl https://YOUR-USERNAME-YOUR-SPACE.hf.space/health
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
#### Code Review
|
| 26 |
+
```bash
|
| 27 |
+
curl -X POST https://YOUR-USERNAME-YOUR-SPACE.hf.space/review \
|
| 28 |
+
-H "Authorization: Bearer YOUR_API_KEY" \
|
| 29 |
+
-H "Content-Type: application/json" \
|
| 30 |
+
-d '{
|
| 31 |
+
"diff": "diff --git a/app.py b/app.py\n+def login(user, pwd):\n+ query = f\"SELECT * FROM users WHERE user='\''{user}'\''\"",
|
| 32 |
+
"language": "python",
|
| 33 |
+
"context": {
|
| 34 |
+
"repo": "myorg/myrepo",
|
| 35 |
+
"pr_number": 123
|
| 36 |
+
}
|
| 37 |
+
}'
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
## π Multi-Agent Architecture
|
| 41 |
+
|
| 42 |
+
This system uses **5 specialized AI agents** working in parallel:
|
| 43 |
+
|
| 44 |
+
| Agent | Role | Focus |
|
| 45 |
+
|-------|------|-------|
|
| 46 |
+
| π **Code Analyzer** | Senior Engineer | Logic, complexity, architecture |
|
| 47 |
+
| π **Security Reviewer** | AppSec Engineer | Vulnerabilities, injection attacks |
|
| 48 |
+
| β‘ **Performance Reviewer** | Performance Engineer | N+1 queries, algorithmic complexity |
|
| 49 |
+
| β¨ **Style Reviewer** | Staff Engineer | Naming, maintainability, SOLID |
|
| 50 |
+
| π **Review Synthesizer** | Tech Lead | Prioritization, final report |
|
| 51 |
+
|
| 52 |
+
## βοΈ Configuration
|
| 53 |
+
|
| 54 |
+
### Environment Variables
|
| 55 |
+
|
| 56 |
+
**Required** (set in Space Settings β Variables):
|
| 57 |
+
|
| 58 |
+
- `LLM_PROVIDER` - `openai` or `groq`
|
| 59 |
+
- `OPENAI_API_KEY` or `GROQ_API_KEY` - Your LLM API key
|
| 60 |
+
- `REVIEW_API_KEY` - API key for authenticating requests
|
| 61 |
+
|
| 62 |
+
**Optional:**
|
| 63 |
+
|
| 64 |
+
- `RATE_LIMIT_PER_MINUTE` - Max requests per minute (default: 5)
|
| 65 |
+
- `REQUEST_TIMEOUT_SECONDS` - Review timeout (default: 90)
|
| 66 |
+
- `MAX_FINDINGS_PER_REVIEW` - Max findings to return (default: 15)
|
| 67 |
+
|
| 68 |
+
### Recommended LLM Configuration
|
| 69 |
+
|
| 70 |
+
**For Free Tier:**
|
| 71 |
+
```bash
|
| 72 |
+
LLM_PROVIDER=groq
|
| 73 |
+
GROQ_API_KEY=gsk_your_key_here
|
| 74 |
+
GROQ_MODEL=llama-3.3-70b-versatile
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
**For Production:**
|
| 78 |
+
```bash
|
| 79 |
+
LLM_PROVIDER=openai
|
| 80 |
+
OPENAI_API_KEY=sk_your_key_here
|
| 81 |
+
OPENAI_MODEL=gpt-4o-mini
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
## π API Response Format
|
| 85 |
+
|
| 86 |
+
```json
|
| 87 |
+
{
|
| 88 |
+
"summary": "Found 2 security issues and 1 performance concern",
|
| 89 |
+
"score": 7.5,
|
| 90 |
+
"findings": [
|
| 91 |
+
{
|
| 92 |
+
"category": "security",
|
| 93 |
+
"severity": "high",
|
| 94 |
+
"file": "app/auth.py",
|
| 95 |
+
"line": 24,
|
| 96 |
+
"message": "SQL injection vulnerability detected",
|
| 97 |
+
"suggestion": "Use parameterized queries instead of string interpolation"
|
| 98 |
+
}
|
| 99 |
+
],
|
| 100 |
+
"metadata": {
|
| 101 |
+
"execution_time_ms": 15234,
|
| 102 |
+
"tokens_used": 12453,
|
| 103 |
+
"agent_count": 5,
|
| 104 |
+
"model": "gpt-4o-mini"
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
## π§ GitHub Integration
|
| 110 |
+
|
| 111 |
+
Integrate with your CI/CD pipeline:
|
| 112 |
+
|
| 113 |
+
```yaml
|
| 114 |
+
# .github/workflows/code-review.yml
|
| 115 |
+
- name: AI Code Review
|
| 116 |
+
run: |
|
| 117 |
+
curl -X POST https://YOUR-SPACE.hf.space/review \
|
| 118 |
+
-H "Authorization: Bearer ${{ secrets.REVIEW_API_KEY }}" \
|
| 119 |
+
-d @review_request.json
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
## π Performance
|
| 123 |
+
|
| 124 |
+
| Metric | Value |
|
| 125 |
+
|--------|-------|
|
| 126 |
+
| Avg review time | 15-45 seconds |
|
| 127 |
+
| Max diff size | 1 MB |
|
| 128 |
+
| Token usage | ~10K per review |
|
| 129 |
+
| Cost per review | $0.002 - $0.15 |
|
| 130 |
+
|
| 131 |
+
## β οΈ Limitations on Free Tier
|
| 132 |
+
|
| 133 |
+
- **Single worker**: Can handle 1 request at a time
|
| 134 |
+
- **Cold starts**: First request after sleep takes ~60 seconds
|
| 135 |
+
- **Resource limits**: 2 vCPU, 16GB RAM
|
| 136 |
+
- **Timeouts**: Long reviews may timeout (increase `REQUEST_TIMEOUT_SECONDS`)
|
| 137 |
+
|
| 138 |
+
## π Security
|
| 139 |
+
|
| 140 |
+
- API key authentication required for `/review` endpoint
|
| 141 |
+
- Rate limiting prevents abuse
|
| 142 |
+
- No data persistence (stateless reviews)
|
| 143 |
+
- Secrets managed via HF Space settings
|
| 144 |
+
|
| 145 |
+
## π Documentation
|
| 146 |
+
|
| 147 |
+
Full documentation: [GitHub Repository](https://github.com/YOUR-USERNAME/code-reviewer-ci-agent)
|
| 148 |
+
|
| 149 |
+
## π‘ Tips
|
| 150 |
+
|
| 151 |
+
1. **Use Groq for free tier** - Faster and free API calls
|
| 152 |
+
2. **Keep diffs small** - Large changes may timeout
|
| 153 |
+
3. **Set rate limits** - Prevent quota exhaustion
|
| 154 |
+
4. **Monitor usage** - Track LLM API costs
|
| 155 |
+
|
| 156 |
+
## π Credits
|
| 157 |
+
|
| 158 |
+
Built with:
|
| 159 |
+
- [CrewAI](https://www.crewai.com/) - Multi-agent orchestration
|
| 160 |
+
- [FastAPI](https://fastapi.tiangolo.com/) - API framework
|
| 161 |
+
- [OpenAI](https://openai.com/) / [Groq](https://groq.com/) - LLM providers
|
| 162 |
+
|
| 163 |
+
---
|
| 164 |
+
|
| 165 |
+
**Made with β€οΈ for better code reviews**
|
app/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Code Reviewer CI Agent - AI-powered code review using CrewAI."""
|
| 2 |
+
|
| 3 |
+
__version__ = "0.1.0"
|
app/api.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""FastAPI gateway for code review service."""
|
| 2 |
+
|
| 3 |
+
import logging
|
| 4 |
+
import os
|
| 5 |
+
import time
|
| 6 |
+
from contextlib import asynccontextmanager
|
| 7 |
+
from typing import Annotated, Optional
|
| 8 |
+
|
| 9 |
+
from fastapi import Depends, FastAPI, HTTPException, Request, Security, status
|
| 10 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 11 |
+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
| 12 |
+
from fastapi.responses import JSONResponse
|
| 13 |
+
|
| 14 |
+
from app import __version__
|
| 15 |
+
from app.config import config
|
| 16 |
+
# Lazy import: get_crew imported after env cleanup in lifespan
|
| 17 |
+
from app.guardrails import get_guardrail_orchestrator
|
| 18 |
+
from app.schemas import HealthResponse, ReviewRequest, ReviewResponse
|
| 19 |
+
from app.utils import generate_request_id, sanitize_diff
|
| 20 |
+
|
| 21 |
+
# Configure logging
|
| 22 |
+
config.configure_logging()
|
| 23 |
+
logger = logging.getLogger(__name__)
|
| 24 |
+
|
| 25 |
+
# Security
|
| 26 |
+
security = HTTPBearer()
|
| 27 |
+
|
| 28 |
+
# Rate limiting (simple in-memory store for MVP)
|
| 29 |
+
request_timestamps: dict[str, list[float]] = {}
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@asynccontextmanager
|
| 33 |
+
async def lifespan(app: FastAPI):
|
| 34 |
+
"""Application lifespan manager."""
|
| 35 |
+
logger.info("Starting Code Reviewer CI Agent API")
|
| 36 |
+
logger.info(f"Version: {__version__}")
|
| 37 |
+
logger.info(f"LLM Provider: {config.llm_provider}")
|
| 38 |
+
logger.info(f"LLM Model: {config.llm_model}")
|
| 39 |
+
logger.info(f"Ray Serve Enabled: {config.enable_ray_serve}")
|
| 40 |
+
|
| 41 |
+
# CRITICAL: Clean up unused LLM provider API keys BEFORE importing crew
|
| 42 |
+
# CrewAI reads environment variables directly, must remove wrong ones early
|
| 43 |
+
if config.llm_provider == "groq":
|
| 44 |
+
# Set dummy OPENAI_API_KEY to prevent CrewAI errors (it checks even when not used)
|
| 45 |
+
os.environ["OPENAI_API_KEY"] = "sk-dummy-key-not-used"
|
| 46 |
+
logger.info("β Set dummy OPENAI_API_KEY (using Groq - OpenAI not used)")
|
| 47 |
+
elif config.llm_provider == "openai":
|
| 48 |
+
os.environ.pop("GROQ_API_KEY", None)
|
| 49 |
+
logger.info("β Removed GROQ_API_KEY from environment (using OpenAI)")
|
| 50 |
+
|
| 51 |
+
# Initialize crew (warm up) - import here after env cleanup
|
| 52 |
+
try:
|
| 53 |
+
from app.crew.crew import get_crew
|
| 54 |
+
get_crew()
|
| 55 |
+
logger.info("Code review crew initialized successfully")
|
| 56 |
+
except Exception as e:
|
| 57 |
+
logger.error(f"Failed to initialize crew: {e}")
|
| 58 |
+
|
| 59 |
+
yield
|
| 60 |
+
|
| 61 |
+
logger.info("Shutting down Code Reviewer CI Agent API")
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
# Create FastAPI app
|
| 65 |
+
app = FastAPI(
|
| 66 |
+
title="Code Reviewer CI Agent",
|
| 67 |
+
description="AI-powered code review using CrewAI multi-agent framework",
|
| 68 |
+
version=__version__,
|
| 69 |
+
lifespan=lifespan,
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
# Add CORS middleware
|
| 73 |
+
app.add_middleware(
|
| 74 |
+
CORSMiddleware,
|
| 75 |
+
allow_origins=config.cors_origins_list,
|
| 76 |
+
allow_credentials=True,
|
| 77 |
+
allow_methods=["*"],
|
| 78 |
+
allow_headers=["*"],
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
# Middleware for request logging
|
| 83 |
+
@app.middleware("http")
|
| 84 |
+
async def log_requests(request: Request, call_next):
|
| 85 |
+
"""Log all requests with timing."""
|
| 86 |
+
request_id = generate_request_id()
|
| 87 |
+
start_time = time.time()
|
| 88 |
+
|
| 89 |
+
# Add request ID to state
|
| 90 |
+
request.state.request_id = request_id
|
| 91 |
+
|
| 92 |
+
logger.info(
|
| 93 |
+
f"[{request_id}] {request.method} {request.url.path} - "
|
| 94 |
+
f"Client: {request.client.host if request.client else 'unknown'}"
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
response = await call_next(request)
|
| 98 |
+
|
| 99 |
+
duration_ms = int((time.time() - start_time) * 1000)
|
| 100 |
+
logger.info(
|
| 101 |
+
f"[{request_id}] Completed in {duration_ms}ms - Status: {response.status_code}"
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
return response
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def verify_api_key(
|
| 108 |
+
credentials: Optional[HTTPAuthorizationCredentials] = Security(security)
|
| 109 |
+
) -> str:
|
| 110 |
+
"""Verify API key from Authorization header.
|
| 111 |
+
|
| 112 |
+
If review_api_key is empty (demo mode), authentication is disabled.
|
| 113 |
+
"""
|
| 114 |
+
# Skip authentication if API key is not configured (demo mode)
|
| 115 |
+
if not config.review_api_key:
|
| 116 |
+
logger.warning("β οΈ Authentication disabled - review_api_key not configured (DEMO MODE)")
|
| 117 |
+
return "demo-mode"
|
| 118 |
+
|
| 119 |
+
if not credentials:
|
| 120 |
+
logger.warning("Missing authorization header")
|
| 121 |
+
raise HTTPException(
|
| 122 |
+
status_code=401,
|
| 123 |
+
detail="Missing authentication credentials",
|
| 124 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
if credentials.credentials != config.review_api_key:
|
| 128 |
+
# Log first 10 chars only for security
|
| 129 |
+
logger.warning(f"Invalid API key attempt: {credentials.credentials[:10]}...")
|
| 130 |
+
raise HTTPException(
|
| 131 |
+
status_code=401,
|
| 132 |
+
detail="Invalid authentication credentials",
|
| 133 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
return credentials.credentials
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def check_rate_limit(api_key: str) -> None:
|
| 140 |
+
"""
|
| 141 |
+
Check rate limit for API key.
|
| 142 |
+
|
| 143 |
+
Args:
|
| 144 |
+
api_key: API key to check
|
| 145 |
+
|
| 146 |
+
Raises:
|
| 147 |
+
HTTPException: If rate limit exceeded
|
| 148 |
+
"""
|
| 149 |
+
current_time = time.time()
|
| 150 |
+
minute_ago = current_time - 60
|
| 151 |
+
|
| 152 |
+
# Clean up old timestamps
|
| 153 |
+
if api_key in request_timestamps:
|
| 154 |
+
request_timestamps[api_key] = [
|
| 155 |
+
ts for ts in request_timestamps[api_key] if ts > minute_ago
|
| 156 |
+
]
|
| 157 |
+
else:
|
| 158 |
+
request_timestamps[api_key] = []
|
| 159 |
+
|
| 160 |
+
# Check limit
|
| 161 |
+
if len(request_timestamps[api_key]) >= config.rate_limit_per_minute:
|
| 162 |
+
logger.warning(f"Rate limit exceeded for API key: {api_key[:10]}...")
|
| 163 |
+
raise HTTPException(
|
| 164 |
+
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
| 165 |
+
detail=f"Rate limit exceeded. Maximum {config.rate_limit_per_minute} requests per minute.",
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
# Add current request
|
| 169 |
+
request_timestamps[api_key].append(current_time)
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
@app.get("/health", response_model=HealthResponse, tags=["Health"])
|
| 173 |
+
async def health_check() -> HealthResponse:
|
| 174 |
+
"""
|
| 175 |
+
Health check endpoint.
|
| 176 |
+
|
| 177 |
+
Returns:
|
| 178 |
+
Health status information
|
| 179 |
+
"""
|
| 180 |
+
return HealthResponse(
|
| 181 |
+
status="healthy",
|
| 182 |
+
version=__version__,
|
| 183 |
+
ray_serve_enabled=config.enable_ray_serve,
|
| 184 |
+
llm_provider=config.llm_provider,
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
@app.post("/review", response_model=ReviewResponse, tags=["Review"])
|
| 189 |
+
async def review_code(
|
| 190 |
+
request: ReviewRequest,
|
| 191 |
+
api_key: Annotated[str, Depends(verify_api_key)],
|
| 192 |
+
) -> ReviewResponse:
|
| 193 |
+
"""
|
| 194 |
+
Review code changes using AI agents.
|
| 195 |
+
|
| 196 |
+
Args:
|
| 197 |
+
request: Review request with diff and context
|
| 198 |
+
api_key: API key for authentication
|
| 199 |
+
|
| 200 |
+
Returns:
|
| 201 |
+
Structured review response with findings and summary
|
| 202 |
+
|
| 203 |
+
Raises:
|
| 204 |
+
HTTPException: If review fails or timeout occurs
|
| 205 |
+
"""
|
| 206 |
+
# Check rate limit
|
| 207 |
+
check_rate_limit(api_key)
|
| 208 |
+
|
| 209 |
+
logger.info(f"Received review request for {request.language} code")
|
| 210 |
+
|
| 211 |
+
try:
|
| 212 |
+
# Sanitize diff
|
| 213 |
+
sanitized_diff = sanitize_diff(request.diff)
|
| 214 |
+
request.diff = sanitized_diff
|
| 215 |
+
|
| 216 |
+
# Get crew and execute review (lazy import)
|
| 217 |
+
from app.crew.crew import get_crew
|
| 218 |
+
crew = get_crew()
|
| 219 |
+
|
| 220 |
+
# Execute with timeout
|
| 221 |
+
import asyncio
|
| 222 |
+
from concurrent.futures import TimeoutError
|
| 223 |
+
|
| 224 |
+
try:
|
| 225 |
+
# Run crew in thread pool to avoid blocking
|
| 226 |
+
loop = asyncio.get_event_loop()
|
| 227 |
+
response = await asyncio.wait_for(
|
| 228 |
+
loop.run_in_executor(None, crew.review_code, request),
|
| 229 |
+
timeout=config.request_timeout_seconds,
|
| 230 |
+
)
|
| 231 |
+
except asyncio.TimeoutError:
|
| 232 |
+
logger.error("Review timed out")
|
| 233 |
+
raise HTTPException(
|
| 234 |
+
status_code=status.HTTP_504_GATEWAY_TIMEOUT,
|
| 235 |
+
detail=f"Review timed out after {config.request_timeout_seconds} seconds",
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
# Apply guardrails
|
| 239 |
+
orchestrator = get_guardrail_orchestrator()
|
| 240 |
+
response = orchestrator.apply(
|
| 241 |
+
response,
|
| 242 |
+
context={
|
| 243 |
+
"diff": request.diff,
|
| 244 |
+
"language": request.language,
|
| 245 |
+
},
|
| 246 |
+
)
|
| 247 |
+
|
| 248 |
+
logger.info(
|
| 249 |
+
f"Review completed successfully: {len(response.findings)} findings, "
|
| 250 |
+
f"score: {response.score:.1f}"
|
| 251 |
+
)
|
| 252 |
+
|
| 253 |
+
return response
|
| 254 |
+
|
| 255 |
+
except HTTPException:
|
| 256 |
+
raise
|
| 257 |
+
except Exception as e:
|
| 258 |
+
logger.error(f"Error during code review: {e}", exc_info=True)
|
| 259 |
+
raise HTTPException(
|
| 260 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 261 |
+
detail=f"Code review failed: {str(e)}",
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
@app.exception_handler(HTTPException)
|
| 266 |
+
async def http_exception_handler(request: Request, exc: HTTPException):
|
| 267 |
+
"""Handle HTTP exceptions with structured error responses."""
|
| 268 |
+
return JSONResponse(
|
| 269 |
+
status_code=exc.status_code,
|
| 270 |
+
content={
|
| 271 |
+
"error": exc.detail,
|
| 272 |
+
"status_code": exc.status_code,
|
| 273 |
+
"request_id": getattr(request.state, "request_id", "unknown"),
|
| 274 |
+
},
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
@app.exception_handler(Exception)
|
| 279 |
+
async def general_exception_handler(request: Request, exc: Exception):
|
| 280 |
+
"""Handle unexpected exceptions."""
|
| 281 |
+
logger.error(f"Unhandled exception: {exc}", exc_info=True)
|
| 282 |
+
return JSONResponse(
|
| 283 |
+
status_code=500,
|
| 284 |
+
content={
|
| 285 |
+
"error": "Internal server error",
|
| 286 |
+
"status_code": 500,
|
| 287 |
+
"request_id": getattr(request.state, "request_id", "unknown"),
|
| 288 |
+
},
|
| 289 |
+
)
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
if __name__ == "__main__":
|
| 293 |
+
import uvicorn
|
| 294 |
+
|
| 295 |
+
uvicorn.run(
|
| 296 |
+
"app.api:app",
|
| 297 |
+
host="0.0.0.0",
|
| 298 |
+
port=8000,
|
| 299 |
+
reload=config.debug,
|
| 300 |
+
log_level=config.log_level.lower(),
|
| 301 |
+
)
|
app/config.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Application configuration using Pydantic Settings."""
|
| 2 |
+
|
| 3 |
+
import logging
|
| 4 |
+
import os
|
| 5 |
+
from typing import Literal
|
| 6 |
+
|
| 7 |
+
from pydantic import Field, field_validator
|
| 8 |
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def is_huggingface_space() -> bool:
|
| 12 |
+
"""Detect if running in Hugging Face Spaces environment."""
|
| 13 |
+
return os.getenv("SPACE_ID") is not None or os.getenv("SPACE_REPO_NAME") is not None
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class AppConfig(BaseSettings):
|
| 17 |
+
"""Application configuration loaded from environment variables."""
|
| 18 |
+
|
| 19 |
+
model_config = SettingsConfigDict(
|
| 20 |
+
env_file=".env",
|
| 21 |
+
env_file_encoding="utf-8",
|
| 22 |
+
case_sensitive=False,
|
| 23 |
+
extra="ignore",
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
# LLM Provider Configuration
|
| 27 |
+
llm_provider: Literal["openai", "groq"] = Field(
|
| 28 |
+
"openai", description="LLM provider to use"
|
| 29 |
+
)
|
| 30 |
+
openai_api_key: str = Field("", description="OpenAI API key")
|
| 31 |
+
openai_model: str = Field("gpt-4o-mini", description="OpenAI model to use")
|
| 32 |
+
groq_api_key: str = Field("", description="Groq API key")
|
| 33 |
+
groq_model: str = Field("llama-3.3-70b-versatile", description="Groq model to use")
|
| 34 |
+
|
| 35 |
+
# API Authentication
|
| 36 |
+
review_api_key: str = Field(
|
| 37 |
+
"",
|
| 38 |
+
description="API key for authentication. Leave empty to disable authentication (not recommended for production)"
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
# Rate Limiting
|
| 42 |
+
rate_limit_per_minute: int = Field(10, ge=1, le=100, description="Max requests per minute")
|
| 43 |
+
|
| 44 |
+
# Ray Serve Configuration
|
| 45 |
+
enable_ray_serve: bool = Field(False, description="Enable Ray Serve deployment")
|
| 46 |
+
ray_serve_host: str = Field("0.0.0.0", description="Ray Serve host")
|
| 47 |
+
ray_serve_port: int = Field(8000, ge=1024, le=65535, description="Ray Serve port")
|
| 48 |
+
ray_num_replicas: int = Field(2, ge=1, le=10, description="Number of Ray replicas")
|
| 49 |
+
ray_max_concurrent_queries: int = Field(
|
| 50 |
+
10, ge=1, le=100, description="Max concurrent queries per replica"
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
# Guardrails Configuration
|
| 54 |
+
max_findings_per_review: int = Field(
|
| 55 |
+
20, ge=1, le=100, description="Maximum findings to return"
|
| 56 |
+
)
|
| 57 |
+
max_tokens_per_review: int = Field(
|
| 58 |
+
15000, ge=1000, le=100000, description="Maximum tokens per review"
|
| 59 |
+
)
|
| 60 |
+
enable_llm_judge_guardrails: bool = Field(
|
| 61 |
+
True, description="Enable LLM-as-Judge guardrails"
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
# Application Settings
|
| 65 |
+
log_level: str = Field("INFO", description="Logging level")
|
| 66 |
+
request_timeout_seconds: int = Field(
|
| 67 |
+
120, ge=30, le=300, description="Request timeout in seconds"
|
| 68 |
+
)
|
| 69 |
+
max_diff_size_bytes: int = Field(
|
| 70 |
+
1_048_576, ge=1024, le=10_485_760, description="Max diff size in bytes"
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
# CORS Settings
|
| 74 |
+
cors_origins: str = Field("*", description="Comma-separated CORS origins")
|
| 75 |
+
|
| 76 |
+
# Debug Mode
|
| 77 |
+
debug: bool = Field(False, description="Enable debug mode")
|
| 78 |
+
|
| 79 |
+
@field_validator("llm_provider")
|
| 80 |
+
@classmethod
|
| 81 |
+
def validate_llm_provider(cls, v: str) -> str:
|
| 82 |
+
"""Validate LLM provider is supported."""
|
| 83 |
+
if v not in ["openai", "groq"]:
|
| 84 |
+
raise ValueError(f"Unsupported LLM provider: {v}")
|
| 85 |
+
return v
|
| 86 |
+
|
| 87 |
+
@field_validator("log_level")
|
| 88 |
+
@classmethod
|
| 89 |
+
def validate_log_level(cls, v: str) -> str:
|
| 90 |
+
"""Validate log level."""
|
| 91 |
+
valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
| 92 |
+
v_upper = v.upper()
|
| 93 |
+
if v_upper not in valid_levels:
|
| 94 |
+
raise ValueError(f"Invalid log level: {v}. Must be one of {valid_levels}")
|
| 95 |
+
return v_upper
|
| 96 |
+
|
| 97 |
+
@property
|
| 98 |
+
def cors_origins_list(self) -> list[str]:
|
| 99 |
+
"""Parse CORS origins into a list."""
|
| 100 |
+
if self.cors_origins == "*":
|
| 101 |
+
return ["*"]
|
| 102 |
+
return [origin.strip() for origin in self.cors_origins.split(",") if origin.strip()]
|
| 103 |
+
|
| 104 |
+
@property
|
| 105 |
+
def llm_api_key(self) -> str:
|
| 106 |
+
"""Get API key for the active LLM provider."""
|
| 107 |
+
if self.llm_provider == "openai":
|
| 108 |
+
if not self.openai_api_key:
|
| 109 |
+
raise ValueError("OpenAI API key not configured")
|
| 110 |
+
return self.openai_api_key
|
| 111 |
+
elif self.llm_provider == "groq":
|
| 112 |
+
if not self.groq_api_key:
|
| 113 |
+
raise ValueError("Groq API key not configured")
|
| 114 |
+
return self.groq_api_key
|
| 115 |
+
raise ValueError(f"Unknown LLM provider: {self.llm_provider}")
|
| 116 |
+
|
| 117 |
+
@property
|
| 118 |
+
def llm_model(self) -> str:
|
| 119 |
+
"""Get model name for the active LLM provider."""
|
| 120 |
+
if self.llm_provider == "openai":
|
| 121 |
+
return self.openai_model
|
| 122 |
+
elif self.llm_provider == "groq":
|
| 123 |
+
return self.groq_model
|
| 124 |
+
raise ValueError(f"Unknown LLM provider: {self.llm_provider}")
|
| 125 |
+
|
| 126 |
+
def optimize_for_huggingface(self) -> None:
|
| 127 |
+
"""Automatically optimize settings for Hugging Face Spaces free tier."""
|
| 128 |
+
if not is_huggingface_space():
|
| 129 |
+
return
|
| 130 |
+
|
| 131 |
+
logger = logging.getLogger(__name__)
|
| 132 |
+
logger.info("π€ Detected Hugging Face Spaces environment - optimizing configuration")
|
| 133 |
+
|
| 134 |
+
# Disable Ray Serve (not suitable for free tier)
|
| 135 |
+
if self.enable_ray_serve:
|
| 136 |
+
logger.warning("Disabling Ray Serve for HF free tier")
|
| 137 |
+
self.enable_ray_serve = False
|
| 138 |
+
|
| 139 |
+
# Lower rate limits for free tier
|
| 140 |
+
if self.rate_limit_per_minute > 5:
|
| 141 |
+
logger.info(f"Adjusting rate limit from {self.rate_limit_per_minute} to 5 for HF free tier")
|
| 142 |
+
self.rate_limit_per_minute = 5
|
| 143 |
+
|
| 144 |
+
# Shorter timeout to prevent resource exhaustion
|
| 145 |
+
if self.request_timeout_seconds > 90:
|
| 146 |
+
logger.info(f"Adjusting timeout from {self.request_timeout_seconds} to 90s for HF free tier")
|
| 147 |
+
self.request_timeout_seconds = 90
|
| 148 |
+
|
| 149 |
+
# Reduce max findings to save tokens
|
| 150 |
+
if self.max_findings_per_review > 15:
|
| 151 |
+
logger.info(f"Adjusting max findings from {self.max_findings_per_review} to 15 for HF free tier")
|
| 152 |
+
self.max_findings_per_review = 15
|
| 153 |
+
|
| 154 |
+
logger.info("β
Configuration optimized for Hugging Face Spaces")
|
| 155 |
+
|
| 156 |
+
def configure_logging(self) -> None:
|
| 157 |
+
"""Configure application logging."""
|
| 158 |
+
logging.basicConfig(
|
| 159 |
+
level=getattr(logging, self.log_level),
|
| 160 |
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
| 161 |
+
datefmt="%Y-%m-%d %H:%M:%S",
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
# Global config instance
|
| 166 |
+
config = AppConfig()
|
| 167 |
+
|
| 168 |
+
# Auto-optimize for Hugging Face if detected
|
| 169 |
+
config.optimize_for_huggingface()
|
app/crew/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""CrewAI orchestration for code review agents."""
|
| 2 |
+
|
| 3 |
+
from app.crew.crew import CodeReviewCrew
|
| 4 |
+
|
| 5 |
+
__all__ = ["CodeReviewCrew"]
|
app/crew/agents.yaml
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
code_analyzer:
|
| 2 |
+
role: >
|
| 3 |
+
Senior Software Engineer
|
| 4 |
+
goal: >
|
| 5 |
+
Analyze code structure, identify complexity patterns, architectural issues,
|
| 6 |
+
and logical flaws in the provided code diff
|
| 7 |
+
backstory: >
|
| 8 |
+
You are a seasoned software engineer with 15+ years of experience across
|
| 9 |
+
multiple programming languages and paradigms. You have a deep understanding
|
| 10 |
+
of software architecture, design patterns, and code organization. You excel
|
| 11 |
+
at quickly understanding code structure and identifying potential issues
|
| 12 |
+
in logic flow, complexity, and maintainability. You focus on the "what"
|
| 13 |
+
and "why" of code, not just the "how".
|
| 14 |
+
verbose: true
|
| 15 |
+
allow_delegation: false
|
| 16 |
+
max_iter: 3
|
| 17 |
+
|
| 18 |
+
security_reviewer:
|
| 19 |
+
role: >
|
| 20 |
+
Application Security Engineer
|
| 21 |
+
goal: >
|
| 22 |
+
Identify security vulnerabilities, potential attack vectors, and unsafe
|
| 23 |
+
coding practices in the code diff
|
| 24 |
+
backstory: >
|
| 25 |
+
You are an OWASP expert and certified penetration tester with extensive
|
| 26 |
+
experience in application security. You've discovered critical vulnerabilities
|
| 27 |
+
in production systems and understand both common and exotic attack patterns.
|
| 28 |
+
You are particularly skilled at identifying SQL injection, XSS, CSRF,
|
| 29 |
+
authentication/authorization flaws, cryptographic issues, secrets leakage,
|
| 30 |
+
unsafe deserialization, and input validation problems. You think like an
|
| 31 |
+
attacker to find security flaws before they can be exploited.
|
| 32 |
+
verbose: true
|
| 33 |
+
allow_delegation: false
|
| 34 |
+
max_iter: 3
|
| 35 |
+
|
| 36 |
+
performance_reviewer:
|
| 37 |
+
role: >
|
| 38 |
+
Performance Engineering Specialist
|
| 39 |
+
goal: >
|
| 40 |
+
Detect performance bottlenecks, inefficient algorithms, and resource
|
| 41 |
+
management issues in the code diff
|
| 42 |
+
backstory: >
|
| 43 |
+
You are a performance optimization expert who has tuned systems handling
|
| 44 |
+
millions of requests per second. You understand algorithmic complexity,
|
| 45 |
+
database query optimization, caching strategies, and resource management.
|
| 46 |
+
You can quickly spot N+1 query patterns, blocking I/O in async contexts,
|
| 47 |
+
inefficient loops, memory leaks, and suboptimal data structures. You think
|
| 48 |
+
in terms of Big O notation and real-world scalability implications.
|
| 49 |
+
verbose: true
|
| 50 |
+
allow_delegation: false
|
| 51 |
+
max_iter: 3
|
| 52 |
+
|
| 53 |
+
style_reviewer:
|
| 54 |
+
role: >
|
| 55 |
+
Staff Engineer and Code Quality Advocate
|
| 56 |
+
goal: >
|
| 57 |
+
Assess code maintainability, readability, naming conventions, and adherence
|
| 58 |
+
to best practices
|
| 59 |
+
backstory: >
|
| 60 |
+
You are a Staff Engineer who has mentored hundreds of developers and shaped
|
| 61 |
+
coding standards at multiple organizations. You're an advocate for clean code
|
| 62 |
+
principles, SOLID design, and pragmatic refactoring. You understand that code
|
| 63 |
+
is read far more often than it's written, and you prioritize clarity,
|
| 64 |
+
consistency, and testability. You can identify code smells and suggest
|
| 65 |
+
practical improvements that make code easier to understand and modify.
|
| 66 |
+
verbose: true
|
| 67 |
+
allow_delegation: false
|
| 68 |
+
max_iter: 3
|
| 69 |
+
|
| 70 |
+
review_synthesizer:
|
| 71 |
+
role: >
|
| 72 |
+
Engineering Tech Lead
|
| 73 |
+
goal: >
|
| 74 |
+
Synthesize findings from all specialist reviewers into a cohesive, prioritized,
|
| 75 |
+
and actionable code review report
|
| 76 |
+
backstory: >
|
| 77 |
+
You are an experienced Tech Lead who has managed complex code reviews and
|
| 78 |
+
technical decisions for large engineering teams. You excel at synthesizing
|
| 79 |
+
diverse technical feedback, removing duplicates, prioritizing issues by
|
| 80 |
+
impact and urgency, and communicating findings clearly to developers of
|
| 81 |
+
all skill levels. You understand the balance between perfectionism and
|
| 82 |
+
pragmatism, and you know how to focus on changes that truly matter.
|
| 83 |
+
verbose: true
|
| 84 |
+
allow_delegation: false
|
| 85 |
+
max_iter: 5
|
app/crew/crew.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""CrewAI orchestration for code review multi-agent system."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import logging
|
| 5 |
+
import os
|
| 6 |
+
import time
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Any, Optional
|
| 9 |
+
|
| 10 |
+
from crewai import Agent, Crew, LLM, Process, Task
|
| 11 |
+
from crewai.project import CrewBase, agent, crew, task
|
| 12 |
+
import yaml
|
| 13 |
+
|
| 14 |
+
from app.config import config
|
| 15 |
+
from app.schemas import ReviewMetadata, ReviewRequest, ReviewResponse
|
| 16 |
+
from app.utils import count_tokens, detect_language
|
| 17 |
+
|
| 18 |
+
logger = logging.getLogger(__name__)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@CrewBase
|
| 22 |
+
class CodeReviewCrew:
|
| 23 |
+
"""CrewAI-based code review orchestration."""
|
| 24 |
+
|
| 25 |
+
# Use absolute paths to avoid CrewAI path resolution issues
|
| 26 |
+
agents_config = str(Path(__file__).parent / "agents.yaml")
|
| 27 |
+
tasks_config = str(Path(__file__).parent / "tasks.yaml")
|
| 28 |
+
|
| 29 |
+
def __init__(self):
|
| 30 |
+
"""Initialize the code review crew."""
|
| 31 |
+
self.llm = self._initialize_llm()
|
| 32 |
+
logger.info(
|
| 33 |
+
f"Initialized CodeReviewCrew with {config.llm_provider} "
|
| 34 |
+
f"using model {config.llm_model}"
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
def _initialize_llm(self):
|
| 38 |
+
"""Initialize the LLM based on configuration."""
|
| 39 |
+
# CrewAI's LLM class uses format: 'provider/model'
|
| 40 |
+
# For Groq: 'groq/model-name'
|
| 41 |
+
# For OpenAI: 'openai/model-name' or just 'model-name'
|
| 42 |
+
if config.llm_provider == "groq":
|
| 43 |
+
model_string = f"groq/{config.llm_model}"
|
| 44 |
+
elif config.llm_provider == "openai":
|
| 45 |
+
model_string = config.llm_model # OpenAI is default, no prefix needed
|
| 46 |
+
else:
|
| 47 |
+
raise ValueError(f"Unsupported LLM provider: {config.llm_provider}")
|
| 48 |
+
|
| 49 |
+
return LLM(
|
| 50 |
+
model=model_string,
|
| 51 |
+
api_key=config.llm_api_key,
|
| 52 |
+
temperature=0.1, # Low temperature for consistent reviews
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
@agent
|
| 56 |
+
def code_analyzer(self) -> Agent:
|
| 57 |
+
"""Create code analyzer agent."""
|
| 58 |
+
return Agent(
|
| 59 |
+
config=self.agents_config["code_analyzer"],
|
| 60 |
+
llm=self.llm,
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
@agent
|
| 64 |
+
def security_reviewer(self) -> Agent:
|
| 65 |
+
"""Create security reviewer agent."""
|
| 66 |
+
return Agent(
|
| 67 |
+
config=self.agents_config["security_reviewer"],
|
| 68 |
+
llm=self.llm,
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
@agent
|
| 72 |
+
def performance_reviewer(self) -> Agent:
|
| 73 |
+
"""Create performance reviewer agent."""
|
| 74 |
+
return Agent(
|
| 75 |
+
config=self.agents_config["performance_reviewer"],
|
| 76 |
+
llm=self.llm,
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
@agent
|
| 80 |
+
def style_reviewer(self) -> Agent:
|
| 81 |
+
"""Create style reviewer agent."""
|
| 82 |
+
return Agent(
|
| 83 |
+
config=self.agents_config["style_reviewer"],
|
| 84 |
+
llm=self.llm,
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
@agent
|
| 88 |
+
def review_synthesizer(self) -> Agent:
|
| 89 |
+
"""Create review synthesizer agent."""
|
| 90 |
+
return Agent(
|
| 91 |
+
config=self.agents_config["review_synthesizer"],
|
| 92 |
+
llm=self.llm,
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
@task
|
| 96 |
+
def analyze_code_task(self) -> Task:
|
| 97 |
+
"""Create code analysis task."""
|
| 98 |
+
return Task(
|
| 99 |
+
config=self.tasks_config["analyze_code"],
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
@task
|
| 103 |
+
def review_security_task(self) -> Task:
|
| 104 |
+
"""Create security review task."""
|
| 105 |
+
return Task(
|
| 106 |
+
config=self.tasks_config["review_security"],
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
@task
|
| 110 |
+
def review_performance_task(self) -> Task:
|
| 111 |
+
"""Create performance review task."""
|
| 112 |
+
return Task(
|
| 113 |
+
config=self.tasks_config["review_performance"],
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
@task
|
| 117 |
+
def review_style_task(self) -> Task:
|
| 118 |
+
"""Create style review task."""
|
| 119 |
+
return Task(
|
| 120 |
+
config=self.tasks_config["review_style"],
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
@task
|
| 124 |
+
def synthesize_review_task(self) -> Task:
|
| 125 |
+
"""Create review synthesis task."""
|
| 126 |
+
return Task(
|
| 127 |
+
config=self.tasks_config["synthesize_review"],
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
@crew
|
| 131 |
+
def crew(self) -> Crew:
|
| 132 |
+
"""Create the code review crew with hybrid parallel-sequential execution."""
|
| 133 |
+
return Crew(
|
| 134 |
+
agents=self.agents,
|
| 135 |
+
tasks=self.tasks,
|
| 136 |
+
process=Process.sequential, # CrewAI handles async tasks internally
|
| 137 |
+
verbose=config.debug,
|
| 138 |
+
memory=False, # Disable memory for MVP (deterministic behavior)
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
def review_code(self, request: ReviewRequest) -> ReviewResponse:
|
| 142 |
+
"""
|
| 143 |
+
Execute code review using the multi-agent crew.
|
| 144 |
+
|
| 145 |
+
Args:
|
| 146 |
+
request: Review request with diff and context
|
| 147 |
+
|
| 148 |
+
Returns:
|
| 149 |
+
Structured review response
|
| 150 |
+
|
| 151 |
+
Raises:
|
| 152 |
+
Exception: If review execution fails
|
| 153 |
+
"""
|
| 154 |
+
start_time = time.time()
|
| 155 |
+
|
| 156 |
+
# Auto-detect language if not provided or set to default
|
| 157 |
+
language = request.language
|
| 158 |
+
if language == "python" or not language:
|
| 159 |
+
detected = detect_language(request.diff)
|
| 160 |
+
if detected != "unknown":
|
| 161 |
+
language = detected
|
| 162 |
+
|
| 163 |
+
logger.info(
|
| 164 |
+
f"Starting code review for {language} code, "
|
| 165 |
+
f"diff size: {len(request.diff)} chars"
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
# Prepare inputs for the crew
|
| 169 |
+
inputs = {
|
| 170 |
+
"diff": request.diff,
|
| 171 |
+
"language": language,
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
try:
|
| 175 |
+
# Execute the crew
|
| 176 |
+
result = self.crew().kickoff(inputs=inputs)
|
| 177 |
+
|
| 178 |
+
# Parse the result
|
| 179 |
+
review_data = self._parse_crew_output(result)
|
| 180 |
+
|
| 181 |
+
# Calculate execution time
|
| 182 |
+
execution_time_ms = int((time.time() - start_time) * 1000)
|
| 183 |
+
|
| 184 |
+
# Count tokens (approximate)
|
| 185 |
+
total_tokens = count_tokens(request.diff + str(review_data), config.llm_model)
|
| 186 |
+
|
| 187 |
+
# Create metadata
|
| 188 |
+
metadata = ReviewMetadata(
|
| 189 |
+
execution_time_ms=execution_time_ms,
|
| 190 |
+
tokens_used=total_tokens,
|
| 191 |
+
agent_count=5,
|
| 192 |
+
guardrails_applied=[], # Will be populated by guardrails layer
|
| 193 |
+
model=config.llm_model,
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
# Create response
|
| 197 |
+
response = ReviewResponse(
|
| 198 |
+
summary=review_data.get("summary", "Code review completed"),
|
| 199 |
+
score=review_data.get("score", 8.0),
|
| 200 |
+
findings=review_data.get("findings", []),
|
| 201 |
+
metadata=metadata,
|
| 202 |
+
)
|
| 203 |
+
|
| 204 |
+
logger.info(
|
| 205 |
+
f"Review completed: {len(response.findings)} findings, "
|
| 206 |
+
f"score: {response.score:.1f}, time: {execution_time_ms}ms"
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
return response
|
| 210 |
+
|
| 211 |
+
except Exception as e:
|
| 212 |
+
logger.error(f"Error during code review: {e}", exc_info=True)
|
| 213 |
+
# Return a fallback response
|
| 214 |
+
execution_time_ms = int((time.time() - start_time) * 1000)
|
| 215 |
+
metadata = ReviewMetadata(
|
| 216 |
+
execution_time_ms=execution_time_ms,
|
| 217 |
+
tokens_used=0,
|
| 218 |
+
agent_count=5,
|
| 219 |
+
guardrails_applied=[],
|
| 220 |
+
model=config.llm_model,
|
| 221 |
+
)
|
| 222 |
+
return ReviewResponse(
|
| 223 |
+
summary=f"Review failed: {str(e)}",
|
| 224 |
+
score=5.0,
|
| 225 |
+
findings=[],
|
| 226 |
+
metadata=metadata,
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
def _parse_crew_output(self, result: Any) -> dict:
|
| 230 |
+
"""
|
| 231 |
+
Parse crew output into structured data.
|
| 232 |
+
|
| 233 |
+
Args:
|
| 234 |
+
result: Raw crew output
|
| 235 |
+
|
| 236 |
+
Returns:
|
| 237 |
+
Parsed review data
|
| 238 |
+
"""
|
| 239 |
+
try:
|
| 240 |
+
# CrewAI result can be accessed via result.raw
|
| 241 |
+
output_str = str(result.raw) if hasattr(result, "raw") else str(result)
|
| 242 |
+
|
| 243 |
+
# Try to extract JSON from the output
|
| 244 |
+
# The synthesizer should return JSON, but we need to handle markdown code blocks
|
| 245 |
+
json_str = output_str
|
| 246 |
+
|
| 247 |
+
# Remove markdown code blocks if present
|
| 248 |
+
if "```json" in json_str:
|
| 249 |
+
json_str = json_str.split("```json")[1].split("```")[0].strip()
|
| 250 |
+
elif "```" in json_str:
|
| 251 |
+
json_str = json_str.split("```")[1].split("```")[0].strip()
|
| 252 |
+
|
| 253 |
+
# Parse JSON
|
| 254 |
+
data = json.loads(json_str)
|
| 255 |
+
|
| 256 |
+
# Validate structure
|
| 257 |
+
if not isinstance(data, dict):
|
| 258 |
+
raise ValueError("Output is not a dictionary")
|
| 259 |
+
|
| 260 |
+
# Ensure required fields
|
| 261 |
+
if "findings" not in data:
|
| 262 |
+
data["findings"] = []
|
| 263 |
+
if "summary" not in data:
|
| 264 |
+
data["summary"] = "Code review completed"
|
| 265 |
+
if "score" not in data:
|
| 266 |
+
data["score"] = 8.0
|
| 267 |
+
|
| 268 |
+
return data
|
| 269 |
+
|
| 270 |
+
except Exception as e:
|
| 271 |
+
logger.warning(f"Failed to parse crew output as JSON: {e}")
|
| 272 |
+
logger.debug(f"Raw output: {result}")
|
| 273 |
+
|
| 274 |
+
# Return a safe fallback
|
| 275 |
+
return {
|
| 276 |
+
"summary": "Review completed but output parsing failed",
|
| 277 |
+
"score": 7.0,
|
| 278 |
+
"findings": [],
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
# Singleton instance for reuse
|
| 283 |
+
_crew_instance: Optional[CodeReviewCrew] = None
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
def get_crew() -> CodeReviewCrew:
|
| 287 |
+
"""Get or create the singleton crew instance."""
|
| 288 |
+
global _crew_instance
|
| 289 |
+
if _crew_instance is None:
|
| 290 |
+
_crew_instance = CodeReviewCrew()
|
| 291 |
+
return _crew_instance
|
app/crew/tasks.yaml
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
analyze_code:
|
| 2 |
+
description: >
|
| 3 |
+
Analyze the following code diff for code quality issues, architectural
|
| 4 |
+
problems, logical flaws, and complexity concerns.
|
| 5 |
+
|
| 6 |
+
Code Diff:
|
| 7 |
+
{diff}
|
| 8 |
+
|
| 9 |
+
Programming Language: {language}
|
| 10 |
+
|
| 11 |
+
Focus on:
|
| 12 |
+
- Code structure and organization
|
| 13 |
+
- Logical correctness and potential bugs
|
| 14 |
+
- Algorithmic complexity
|
| 15 |
+
- Design patterns usage (appropriate or inappropriate)
|
| 16 |
+
- Error handling
|
| 17 |
+
- Edge cases
|
| 18 |
+
|
| 19 |
+
Return findings in JSON format as a list of objects with these fields:
|
| 20 |
+
- category: "logic" or "maintainability"
|
| 21 |
+
- severity: "low", "medium", "high", or "critical"
|
| 22 |
+
- file: file path where issue is found
|
| 23 |
+
- line: line number (if applicable)
|
| 24 |
+
- message: clear description of the issue
|
| 25 |
+
- suggestion: actionable fix or improvement
|
| 26 |
+
|
| 27 |
+
expected_output: >
|
| 28 |
+
A JSON-formatted list of code analysis findings. Each finding must include
|
| 29 |
+
category, severity, file path, line number (if applicable), a clear message
|
| 30 |
+
explaining the issue, and an actionable suggestion for improvement.
|
| 31 |
+
Focus on high-impact issues. Maximum 10 findings.
|
| 32 |
+
|
| 33 |
+
agent: code_analyzer
|
| 34 |
+
async_execution: true
|
| 35 |
+
|
| 36 |
+
review_security:
|
| 37 |
+
description: >
|
| 38 |
+
Review the following code diff for security vulnerabilities and unsafe
|
| 39 |
+
coding practices.
|
| 40 |
+
|
| 41 |
+
Code Diff:
|
| 42 |
+
{diff}
|
| 43 |
+
|
| 44 |
+
Programming Language: {language}
|
| 45 |
+
|
| 46 |
+
Focus on:
|
| 47 |
+
- SQL injection, NoSQL injection
|
| 48 |
+
- Cross-site scripting (XSS)
|
| 49 |
+
- Authentication and authorization flaws
|
| 50 |
+
- Secrets and credentials in code
|
| 51 |
+
- Unsafe deserialization
|
| 52 |
+
- Path traversal vulnerabilities
|
| 53 |
+
- Cryptographic issues
|
| 54 |
+
- Input validation and sanitization
|
| 55 |
+
- CSRF vulnerabilities
|
| 56 |
+
- Insecure dependencies
|
| 57 |
+
|
| 58 |
+
Return findings in JSON format as a list of objects with these fields:
|
| 59 |
+
- category: "security"
|
| 60 |
+
- severity: "low", "medium", "high", or "critical"
|
| 61 |
+
- file: file path where issue is found
|
| 62 |
+
- line: line number (if applicable)
|
| 63 |
+
- message: clear description of the vulnerability
|
| 64 |
+
- suggestion: actionable remediation
|
| 65 |
+
|
| 66 |
+
expected_output: >
|
| 67 |
+
A JSON-formatted list of security findings. Each finding must include
|
| 68 |
+
category "security", severity level, file path, line number (if applicable),
|
| 69 |
+
a clear description of the vulnerability, and an actionable remediation
|
| 70 |
+
suggestion. Prioritize critical and high-severity issues. Maximum 10 findings.
|
| 71 |
+
|
| 72 |
+
agent: security_reviewer
|
| 73 |
+
async_execution: true
|
| 74 |
+
|
| 75 |
+
review_performance:
|
| 76 |
+
description: >
|
| 77 |
+
Analyze the following code diff for performance issues, inefficiencies,
|
| 78 |
+
and scalability concerns.
|
| 79 |
+
|
| 80 |
+
Code Diff:
|
| 81 |
+
{diff}
|
| 82 |
+
|
| 83 |
+
Programming Language: {language}
|
| 84 |
+
|
| 85 |
+
Focus on:
|
| 86 |
+
- N+1 query patterns
|
| 87 |
+
- Blocking I/O in async contexts
|
| 88 |
+
- Inefficient algorithms (poor Big O complexity)
|
| 89 |
+
- Unnecessary loops or iterations
|
| 90 |
+
- Missing database indexes
|
| 91 |
+
- Inefficient data structures
|
| 92 |
+
- Memory leaks
|
| 93 |
+
- Missing caching opportunities
|
| 94 |
+
- Resource management issues
|
| 95 |
+
|
| 96 |
+
Return findings in JSON format as a list of objects with these fields:
|
| 97 |
+
- category: "performance"
|
| 98 |
+
- severity: "low", "medium", "high", or "critical"
|
| 99 |
+
- file: file path where issue is found
|
| 100 |
+
- line: line number (if applicable)
|
| 101 |
+
- message: clear description of the performance issue
|
| 102 |
+
- suggestion: actionable optimization
|
| 103 |
+
|
| 104 |
+
expected_output: >
|
| 105 |
+
A JSON-formatted list of performance findings. Each finding must include
|
| 106 |
+
category "performance", severity level, file path, line number (if applicable),
|
| 107 |
+
a clear description of the performance problem, and an actionable optimization
|
| 108 |
+
suggestion. Focus on issues with real-world impact. Maximum 10 findings.
|
| 109 |
+
|
| 110 |
+
agent: performance_reviewer
|
| 111 |
+
async_execution: true
|
| 112 |
+
|
| 113 |
+
review_style:
|
| 114 |
+
description: >
|
| 115 |
+
Review the following code diff for code style, maintainability, readability,
|
| 116 |
+
and adherence to best practices.
|
| 117 |
+
|
| 118 |
+
Code Diff:
|
| 119 |
+
{diff}
|
| 120 |
+
|
| 121 |
+
Programming Language: {language}
|
| 122 |
+
|
| 123 |
+
Focus on:
|
| 124 |
+
- Naming conventions (variables, functions, classes)
|
| 125 |
+
- Code duplication
|
| 126 |
+
- Function/method length and complexity
|
| 127 |
+
- Comment quality and documentation
|
| 128 |
+
- Magic numbers and hard-coded values
|
| 129 |
+
- Proper use of language idioms
|
| 130 |
+
- Type hints and annotations (if applicable)
|
| 131 |
+
- Test coverage hints
|
| 132 |
+
- SOLID principle violations
|
| 133 |
+
- Code smells
|
| 134 |
+
|
| 135 |
+
Return findings in JSON format as a list of objects with these fields:
|
| 136 |
+
- category: "style" or "maintainability"
|
| 137 |
+
- severity: "low", "medium", "high", or "critical"
|
| 138 |
+
- file: file path where issue is found
|
| 139 |
+
- line: line number (if applicable)
|
| 140 |
+
- message: clear description of the style/maintainability issue
|
| 141 |
+
- suggestion: actionable improvement
|
| 142 |
+
|
| 143 |
+
expected_output: >
|
| 144 |
+
A JSON-formatted list of style and maintainability findings. Each finding
|
| 145 |
+
must include category ("style" or "maintainability"), severity level, file
|
| 146 |
+
path, line number (if applicable), a clear description of the issue, and an
|
| 147 |
+
actionable improvement suggestion. Focus on high-impact improvements.
|
| 148 |
+
Maximum 10 findings.
|
| 149 |
+
|
| 150 |
+
agent: style_reviewer
|
| 151 |
+
async_execution: true
|
| 152 |
+
|
| 153 |
+
synthesize_review:
|
| 154 |
+
description: >
|
| 155 |
+
Synthesize all review findings from the specialist agents into a comprehensive,
|
| 156 |
+
prioritized code review report.
|
| 157 |
+
|
| 158 |
+
You have received findings from:
|
| 159 |
+
1. Code Analyzer (logic and architecture)
|
| 160 |
+
2. Security Reviewer (vulnerabilities)
|
| 161 |
+
3. Performance Reviewer (efficiency)
|
| 162 |
+
4. Style Reviewer (maintainability)
|
| 163 |
+
|
| 164 |
+
Your tasks:
|
| 165 |
+
1. Merge all findings from the specialist reviewers
|
| 166 |
+
2. Remove any duplicate findings (same issue identified by multiple agents)
|
| 167 |
+
3. Prioritize findings by severity and impact
|
| 168 |
+
4. Calculate an overall code quality score (0-10) based on:
|
| 169 |
+
- Critical issues: -3 points each
|
| 170 |
+
- High severity: -1.5 points each
|
| 171 |
+
- Medium severity: -0.5 points each
|
| 172 |
+
- Low severity: -0.1 points each
|
| 173 |
+
- Start from 10 and subtract
|
| 174 |
+
5. Create a concise summary (2-3 sentences) of the overall review
|
| 175 |
+
|
| 176 |
+
Return a JSON object with:
|
| 177 |
+
- summary: string (concise overview of the review)
|
| 178 |
+
- score: float (0-10, code quality score)
|
| 179 |
+
- findings: array of finding objects (merged, deduplicated, sorted by severity)
|
| 180 |
+
|
| 181 |
+
Each finding must have:
|
| 182 |
+
- category: "security", "performance", "style", "logic", or "maintainability"
|
| 183 |
+
- severity: "low", "medium", "high", or "critical"
|
| 184 |
+
- file: string
|
| 185 |
+
- line: integer or null
|
| 186 |
+
- message: string
|
| 187 |
+
- suggestion: string
|
| 188 |
+
|
| 189 |
+
expected_output: >
|
| 190 |
+
A complete JSON code review report with: (1) a summary string providing
|
| 191 |
+
a high-level overview, (2) a score (0-10) indicating code quality, and
|
| 192 |
+
(3) a findings array containing all unique, prioritized issues from all
|
| 193 |
+
reviewers. The findings must be sorted by severity (critical first) and
|
| 194 |
+
should be comprehensive but concise. Ensure no duplicate findings.
|
| 195 |
+
|
| 196 |
+
agent: review_synthesizer
|
| 197 |
+
context:
|
| 198 |
+
- analyze_code_task
|
| 199 |
+
- review_security_task
|
| 200 |
+
- review_performance_task
|
| 201 |
+
- review_style_task
|
| 202 |
+
async_execution: false
|
app/guardrails.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Guardrails for ensuring review quality and safety."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import logging
|
| 5 |
+
from abc import ABC, abstractmethod
|
| 6 |
+
from typing import Tuple
|
| 7 |
+
|
| 8 |
+
from app.config import config
|
| 9 |
+
from app.schemas import ReviewResponse, FindingSeverity
|
| 10 |
+
from app.utils import extract_files_from_diff
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class Guardrail(ABC):
|
| 16 |
+
"""Base class for guardrails."""
|
| 17 |
+
|
| 18 |
+
@abstractmethod
|
| 19 |
+
def validate(
|
| 20 |
+
self, response: ReviewResponse, context: dict
|
| 21 |
+
) -> Tuple[bool, ReviewResponse, str]:
|
| 22 |
+
"""
|
| 23 |
+
Validate and potentially modify the review response.
|
| 24 |
+
|
| 25 |
+
Args:
|
| 26 |
+
response: Review response to validate
|
| 27 |
+
context: Additional context (e.g., original diff)
|
| 28 |
+
|
| 29 |
+
Returns:
|
| 30 |
+
Tuple of (is_valid, modified_response, guardrail_name)
|
| 31 |
+
"""
|
| 32 |
+
pass
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class MaxFindingsGuardrail(Guardrail):
|
| 36 |
+
"""Limit the maximum number of findings to prevent noise."""
|
| 37 |
+
|
| 38 |
+
def validate(
|
| 39 |
+
self, response: ReviewResponse, context: dict
|
| 40 |
+
) -> Tuple[bool, ReviewResponse, str]:
|
| 41 |
+
"""Limit findings to maximum allowed."""
|
| 42 |
+
max_findings = config.max_findings_per_review
|
| 43 |
+
|
| 44 |
+
if len(response.findings) <= max_findings:
|
| 45 |
+
return True, response, "max_findings"
|
| 46 |
+
|
| 47 |
+
logger.warning(
|
| 48 |
+
f"Truncating findings from {len(response.findings)} to {max_findings}"
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
# Keep highest severity findings
|
| 52 |
+
sorted_findings = sorted(
|
| 53 |
+
response.findings,
|
| 54 |
+
key=lambda f: (
|
| 55 |
+
["low", "medium", "high", "critical"].index(f.severity.value),
|
| 56 |
+
f.category.value,
|
| 57 |
+
),
|
| 58 |
+
reverse=True,
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
response.findings = sorted_findings[:max_findings]
|
| 62 |
+
response.metadata.guardrails_applied.append("max_findings")
|
| 63 |
+
|
| 64 |
+
return True, response, "max_findings"
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
class FileNameValidationGuardrail(Guardrail):
|
| 68 |
+
"""Ensure all file references exist in the original diff."""
|
| 69 |
+
|
| 70 |
+
def validate(
|
| 71 |
+
self, response: ReviewResponse, context: dict
|
| 72 |
+
) -> Tuple[bool, ReviewResponse, str]:
|
| 73 |
+
"""Validate file names against diff."""
|
| 74 |
+
diff = context.get("diff", "")
|
| 75 |
+
valid_files = set(extract_files_from_diff(diff))
|
| 76 |
+
|
| 77 |
+
if not valid_files:
|
| 78 |
+
# If we can't extract files, skip validation
|
| 79 |
+
logger.warning("Could not extract files from diff, skipping file validation")
|
| 80 |
+
return True, response, "file_validation"
|
| 81 |
+
|
| 82 |
+
# Filter findings with invalid file references
|
| 83 |
+
original_count = len(response.findings)
|
| 84 |
+
response.findings = [
|
| 85 |
+
f for f in response.findings if f.file in valid_files or f.file == "unknown"
|
| 86 |
+
]
|
| 87 |
+
|
| 88 |
+
removed = original_count - len(response.findings)
|
| 89 |
+
if removed > 0:
|
| 90 |
+
logger.warning(f"Removed {removed} findings with invalid file references")
|
| 91 |
+
response.metadata.guardrails_applied.append("file_validation")
|
| 92 |
+
|
| 93 |
+
return True, response, "file_validation"
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
class EmptyMessageGuardrail(Guardrail):
|
| 97 |
+
"""Remove findings with empty or trivial messages."""
|
| 98 |
+
|
| 99 |
+
def validate(
|
| 100 |
+
self, response: ReviewResponse, context: dict
|
| 101 |
+
) -> Tuple[bool, ReviewResponse, str]:
|
| 102 |
+
"""Remove findings with empty messages."""
|
| 103 |
+
original_count = len(response.findings)
|
| 104 |
+
|
| 105 |
+
response.findings = [
|
| 106 |
+
f
|
| 107 |
+
for f in response.findings
|
| 108 |
+
if f.message.strip()
|
| 109 |
+
and f.suggestion.strip()
|
| 110 |
+
and len(f.message) > 10
|
| 111 |
+
and len(f.suggestion) > 10
|
| 112 |
+
]
|
| 113 |
+
|
| 114 |
+
removed = original_count - len(response.findings)
|
| 115 |
+
if removed > 0:
|
| 116 |
+
logger.warning(f"Removed {removed} findings with empty/trivial messages")
|
| 117 |
+
response.metadata.guardrails_applied.append("empty_message")
|
| 118 |
+
|
| 119 |
+
return True, response, "empty_message"
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
class DuplicateDetectionGuardrail(Guardrail):
|
| 123 |
+
"""Remove duplicate findings."""
|
| 124 |
+
|
| 125 |
+
def validate(
|
| 126 |
+
self, response: ReviewResponse, context: dict
|
| 127 |
+
) -> Tuple[bool, ReviewResponse, str]:
|
| 128 |
+
"""Remove duplicate findings based on file, line, and message similarity."""
|
| 129 |
+
seen = set()
|
| 130 |
+
unique_findings = []
|
| 131 |
+
|
| 132 |
+
for finding in response.findings:
|
| 133 |
+
# Create a fingerprint for the finding
|
| 134 |
+
fingerprint = (
|
| 135 |
+
finding.file,
|
| 136 |
+
finding.line,
|
| 137 |
+
finding.category.value,
|
| 138 |
+
# Use first 50 chars of message for similarity
|
| 139 |
+
finding.message[:50].lower().strip(),
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
if fingerprint not in seen:
|
| 143 |
+
seen.add(fingerprint)
|
| 144 |
+
unique_findings.append(finding)
|
| 145 |
+
|
| 146 |
+
removed = len(response.findings) - len(unique_findings)
|
| 147 |
+
if removed > 0:
|
| 148 |
+
logger.info(f"Removed {removed} duplicate findings")
|
| 149 |
+
response.findings = unique_findings
|
| 150 |
+
response.metadata.guardrails_applied.append("duplicate_detection")
|
| 151 |
+
|
| 152 |
+
return True, response, "duplicate_detection"
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
class SeverityValidationGuardrail(Guardrail):
|
| 156 |
+
"""Ensure severity levels are reasonable."""
|
| 157 |
+
|
| 158 |
+
def validate(
|
| 159 |
+
self, response: ReviewResponse, context: dict
|
| 160 |
+
) -> Tuple[bool, ReviewResponse, str]:
|
| 161 |
+
"""Validate severity assignments."""
|
| 162 |
+
# Downgrade security findings marked as "low" for serious vulnerabilities
|
| 163 |
+
# This is a simple heuristic-based check
|
| 164 |
+
|
| 165 |
+
serious_keywords = [
|
| 166 |
+
"injection",
|
| 167 |
+
"xss",
|
| 168 |
+
"sql",
|
| 169 |
+
"authentication",
|
| 170 |
+
"authorization",
|
| 171 |
+
"credential",
|
| 172 |
+
"password",
|
| 173 |
+
"secret",
|
| 174 |
+
"token",
|
| 175 |
+
]
|
| 176 |
+
|
| 177 |
+
modified = False
|
| 178 |
+
for finding in response.findings:
|
| 179 |
+
if finding.category.value == "security" and finding.severity == FindingSeverity.LOW:
|
| 180 |
+
# Check if message contains serious keywords
|
| 181 |
+
message_lower = finding.message.lower()
|
| 182 |
+
if any(keyword in message_lower for keyword in serious_keywords):
|
| 183 |
+
logger.warning(
|
| 184 |
+
f"Upgrading security finding severity from LOW to MEDIUM: {finding.message[:50]}"
|
| 185 |
+
)
|
| 186 |
+
finding.severity = FindingSeverity.MEDIUM
|
| 187 |
+
modified = True
|
| 188 |
+
|
| 189 |
+
if modified:
|
| 190 |
+
response.metadata.guardrails_applied.append("severity_validation")
|
| 191 |
+
|
| 192 |
+
return True, response, "severity_validation"
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
class GuardrailOrchestrator:
|
| 196 |
+
"""Orchestrate multiple guardrails."""
|
| 197 |
+
|
| 198 |
+
def __init__(self):
|
| 199 |
+
"""Initialize guardrails."""
|
| 200 |
+
self.guardrails: list[Guardrail] = [
|
| 201 |
+
EmptyMessageGuardrail(),
|
| 202 |
+
DuplicateDetectionGuardrail(),
|
| 203 |
+
FileNameValidationGuardrail(),
|
| 204 |
+
SeverityValidationGuardrail(),
|
| 205 |
+
MaxFindingsGuardrail(), # Run last to limit final count
|
| 206 |
+
]
|
| 207 |
+
logger.info(f"Initialized {len(self.guardrails)} guardrails")
|
| 208 |
+
|
| 209 |
+
def apply(self, response: ReviewResponse, context: dict) -> ReviewResponse:
|
| 210 |
+
"""
|
| 211 |
+
Apply all guardrails to the review response.
|
| 212 |
+
|
| 213 |
+
Args:
|
| 214 |
+
response: Review response to validate
|
| 215 |
+
context: Additional context (original diff, etc.)
|
| 216 |
+
|
| 217 |
+
Returns:
|
| 218 |
+
Validated and potentially modified response
|
| 219 |
+
"""
|
| 220 |
+
logger.info(
|
| 221 |
+
f"Applying guardrails to review with {len(response.findings)} findings"
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
for guardrail in self.guardrails:
|
| 225 |
+
try:
|
| 226 |
+
is_valid, response, name = guardrail.validate(response, context)
|
| 227 |
+
if not is_valid:
|
| 228 |
+
logger.warning(f"Guardrail {name} marked response as invalid")
|
| 229 |
+
except Exception as e:
|
| 230 |
+
logger.error(f"Error in guardrail {guardrail.__class__.__name__}: {e}")
|
| 231 |
+
# Continue with other guardrails
|
| 232 |
+
|
| 233 |
+
logger.info(
|
| 234 |
+
f"Guardrails applied: {response.metadata.guardrails_applied}, "
|
| 235 |
+
f"final findings count: {len(response.findings)}"
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
return response
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
# Singleton instance
|
| 242 |
+
_orchestrator: GuardrailOrchestrator | None = None
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
def get_guardrail_orchestrator() -> GuardrailOrchestrator:
|
| 246 |
+
"""Get or create the singleton guardrail orchestrator."""
|
| 247 |
+
global _orchestrator
|
| 248 |
+
if _orchestrator is None:
|
| 249 |
+
_orchestrator = GuardrailOrchestrator()
|
| 250 |
+
return _orchestrator
|
app/schemas.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pydantic schemas for structured code review output."""
|
| 2 |
+
|
| 3 |
+
from enum import Enum
|
| 4 |
+
from typing import Any, Optional
|
| 5 |
+
|
| 6 |
+
from pydantic import BaseModel, Field, field_validator
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class FindingCategory(str, Enum):
|
| 10 |
+
"""Category of code review finding."""
|
| 11 |
+
|
| 12 |
+
SECURITY = "security"
|
| 13 |
+
PERFORMANCE = "performance"
|
| 14 |
+
STYLE = "style"
|
| 15 |
+
LOGIC = "logic"
|
| 16 |
+
MAINTAINABILITY = "maintainability"
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class FindingSeverity(str, Enum):
|
| 20 |
+
"""Severity level of finding."""
|
| 21 |
+
|
| 22 |
+
LOW = "low"
|
| 23 |
+
MEDIUM = "medium"
|
| 24 |
+
HIGH = "high"
|
| 25 |
+
CRITICAL = "critical"
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class ReviewFinding(BaseModel):
|
| 29 |
+
"""Individual code review finding."""
|
| 30 |
+
|
| 31 |
+
category: FindingCategory = Field(
|
| 32 |
+
..., description="Category of the issue (security, performance, style, logic, maintainability)"
|
| 33 |
+
)
|
| 34 |
+
severity: FindingSeverity = Field(..., description="Severity level of the issue")
|
| 35 |
+
file: str = Field(..., description="File path where the issue was found")
|
| 36 |
+
line: Optional[int] = Field(None, description="Line number where the issue occurs")
|
| 37 |
+
message: str = Field(..., description="Clear description of the issue")
|
| 38 |
+
suggestion: str = Field(..., description="Actionable suggestion for fixing the issue")
|
| 39 |
+
|
| 40 |
+
@field_validator("message", "suggestion")
|
| 41 |
+
@classmethod
|
| 42 |
+
def validate_not_empty(cls, v: str) -> str:
|
| 43 |
+
"""Ensure message and suggestion are not empty."""
|
| 44 |
+
if not v or not v.strip():
|
| 45 |
+
raise ValueError("Field cannot be empty")
|
| 46 |
+
return v.strip()
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class ReviewContext(BaseModel):
|
| 50 |
+
"""Additional context about the code review request."""
|
| 51 |
+
|
| 52 |
+
repo: str = Field(..., description="Repository identifier (org/repo)")
|
| 53 |
+
commit_sha: Optional[str] = Field(None, description="Commit SHA being reviewed")
|
| 54 |
+
pr_number: Optional[int] = Field(None, description="Pull request number if applicable")
|
| 55 |
+
author: Optional[str] = Field(None, description="Code author username")
|
| 56 |
+
branch: Optional[str] = Field(None, description="Branch name")
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class ReviewRequest(BaseModel):
|
| 60 |
+
"""Code review request payload."""
|
| 61 |
+
|
| 62 |
+
diff: str = Field(..., description="Git diff to review", min_length=1)
|
| 63 |
+
language: str = Field(
|
| 64 |
+
"python", description="Primary programming language of the diff"
|
| 65 |
+
)
|
| 66 |
+
context: Optional[ReviewContext] = Field(
|
| 67 |
+
None, description="Additional context about the request"
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
@field_validator("diff")
|
| 71 |
+
@classmethod
|
| 72 |
+
def validate_diff_size(cls, v: str) -> str:
|
| 73 |
+
"""Ensure diff is not too large."""
|
| 74 |
+
max_size = 1_048_576 # 1MB
|
| 75 |
+
if len(v.encode("utf-8")) > max_size:
|
| 76 |
+
raise ValueError(f"Diff exceeds maximum size of {max_size} bytes")
|
| 77 |
+
return v
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
class ReviewMetadata(BaseModel):
|
| 81 |
+
"""Metadata about the review execution."""
|
| 82 |
+
|
| 83 |
+
execution_time_ms: int = Field(..., description="Time taken to execute review in milliseconds")
|
| 84 |
+
tokens_used: int = Field(..., description="Total tokens consumed by LLM")
|
| 85 |
+
agent_count: int = Field(5, description="Number of agents involved in review")
|
| 86 |
+
guardrails_applied: list[str] = Field(
|
| 87 |
+
default_factory=list, description="List of guardrails that were applied"
|
| 88 |
+
)
|
| 89 |
+
model: str = Field(..., description="LLM model used for review")
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class ReviewResponse(BaseModel):
|
| 93 |
+
"""Structured code review response."""
|
| 94 |
+
|
| 95 |
+
summary: str = Field(..., description="High-level summary of the review")
|
| 96 |
+
score: float = Field(
|
| 97 |
+
..., ge=0.0, le=10.0, description="Overall code quality score (0-10)"
|
| 98 |
+
)
|
| 99 |
+
findings: list[ReviewFinding] = Field(
|
| 100 |
+
default_factory=list, description="List of review findings"
|
| 101 |
+
)
|
| 102 |
+
metadata: ReviewMetadata = Field(..., description="Execution metadata")
|
| 103 |
+
|
| 104 |
+
@property
|
| 105 |
+
def critical_count(self) -> int:
|
| 106 |
+
"""Count of critical severity findings."""
|
| 107 |
+
return sum(1 for f in self.findings if f.severity == FindingSeverity.CRITICAL)
|
| 108 |
+
|
| 109 |
+
@property
|
| 110 |
+
def high_count(self) -> int:
|
| 111 |
+
"""Count of high severity findings."""
|
| 112 |
+
return sum(1 for f in self.findings if f.severity == FindingSeverity.HIGH)
|
| 113 |
+
|
| 114 |
+
@property
|
| 115 |
+
def findings_by_category(self) -> dict[FindingCategory, list[ReviewFinding]]:
|
| 116 |
+
"""Group findings by category."""
|
| 117 |
+
result: dict[FindingCategory, list[ReviewFinding]] = {cat: [] for cat in FindingCategory}
|
| 118 |
+
for finding in self.findings:
|
| 119 |
+
result[finding.category].append(finding)
|
| 120 |
+
return result
|
| 121 |
+
|
| 122 |
+
def to_markdown(self) -> str:
|
| 123 |
+
"""Convert review to markdown format for GitHub PR comments."""
|
| 124 |
+
lines = [
|
| 125 |
+
"## π€ AI Code Review",
|
| 126 |
+
"",
|
| 127 |
+
f"**Summary:** {self.summary}",
|
| 128 |
+
f"**Quality Score:** {self.score:.1f}/10",
|
| 129 |
+
"",
|
| 130 |
+
]
|
| 131 |
+
|
| 132 |
+
if not self.findings:
|
| 133 |
+
lines.append("β
No issues found! Great work!")
|
| 134 |
+
return "\n".join(lines)
|
| 135 |
+
|
| 136 |
+
# Group by severity
|
| 137 |
+
critical = [f for f in self.findings if f.severity == FindingSeverity.CRITICAL]
|
| 138 |
+
high = [f for f in self.findings if f.severity == FindingSeverity.HIGH]
|
| 139 |
+
medium = [f for f in self.findings if f.severity == FindingSeverity.MEDIUM]
|
| 140 |
+
low = [f for f in self.findings if f.severity == FindingSeverity.LOW]
|
| 141 |
+
|
| 142 |
+
def format_findings(findings: list[ReviewFinding], emoji: str, title: str) -> list[str]:
|
| 143 |
+
if not findings:
|
| 144 |
+
return []
|
| 145 |
+
result = [f"### {emoji} {title}", ""]
|
| 146 |
+
for f in findings:
|
| 147 |
+
location = f"`{f.file}:{f.line}`" if f.line else f"`{f.file}`"
|
| 148 |
+
result.append(f"- **{f.category.value.title()}** in {location}")
|
| 149 |
+
result.append(f" > {f.message}")
|
| 150 |
+
result.append(f" > **Suggestion:** {f.suggestion}")
|
| 151 |
+
result.append("")
|
| 152 |
+
return result
|
| 153 |
+
|
| 154 |
+
lines.extend(format_findings(critical, "π΄", "Critical Issues"))
|
| 155 |
+
lines.extend(format_findings(high, "π ", "High Severity"))
|
| 156 |
+
lines.extend(format_findings(medium, "π‘", "Medium Severity"))
|
| 157 |
+
lines.extend(format_findings(low, "π’", "Low Severity"))
|
| 158 |
+
|
| 159 |
+
# Footer
|
| 160 |
+
lines.extend(
|
| 161 |
+
[
|
| 162 |
+
"---",
|
| 163 |
+
f"*Reviewed by {self.metadata.agent_count} AI agents "
|
| 164 |
+
f"using {self.metadata.model} "
|
| 165 |
+
f"in {self.metadata.execution_time_ms}ms*",
|
| 166 |
+
]
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
return "\n".join(lines)
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
class HealthResponse(BaseModel):
|
| 173 |
+
"""Health check response."""
|
| 174 |
+
|
| 175 |
+
status: str = Field(..., description="Service status")
|
| 176 |
+
version: str = Field(..., description="Application version")
|
| 177 |
+
ray_serve_enabled: bool = Field(..., description="Whether Ray Serve is enabled")
|
| 178 |
+
llm_provider: str = Field(..., description="Active LLM provider")
|
app/serve.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Ray Serve deployment for scalable code review service."""
|
| 2 |
+
|
| 3 |
+
import logging
|
| 4 |
+
from typing import Dict
|
| 5 |
+
|
| 6 |
+
from ray import serve
|
| 7 |
+
from fastapi import FastAPI
|
| 8 |
+
|
| 9 |
+
from app.api import app as fastapi_app
|
| 10 |
+
from app.config import config
|
| 11 |
+
from app.crew.crew import CodeReviewCrew
|
| 12 |
+
from app.guardrails import GuardrailOrchestrator
|
| 13 |
+
from app.schemas import ReviewRequest, ReviewResponse
|
| 14 |
+
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@serve.deployment(
|
| 19 |
+
num_replicas=config.ray_num_replicas,
|
| 20 |
+
max_concurrent_queries=config.ray_max_concurrent_queries,
|
| 21 |
+
ray_actor_options={
|
| 22 |
+
"num_cpus": 2,
|
| 23 |
+
"num_gpus": 0,
|
| 24 |
+
},
|
| 25 |
+
)
|
| 26 |
+
@serve.ingress(fastapi_app)
|
| 27 |
+
class CodeReviewDeployment:
|
| 28 |
+
"""
|
| 29 |
+
Ray Serve deployment for code review service.
|
| 30 |
+
|
| 31 |
+
This wraps the FastAPI app and provides horizontal scaling capabilities.
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
def __init__(self):
|
| 35 |
+
"""Initialize the deployment."""
|
| 36 |
+
logger.info("Initializing CodeReviewDeployment")
|
| 37 |
+
config.configure_logging()
|
| 38 |
+
|
| 39 |
+
# Initialize crew and guardrails (warm up)
|
| 40 |
+
self.crew = CodeReviewCrew()
|
| 41 |
+
self.guardrails = GuardrailOrchestrator()
|
| 42 |
+
|
| 43 |
+
logger.info(
|
| 44 |
+
f"CodeReviewDeployment initialized with {config.ray_num_replicas} replicas"
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
# Deployment configuration
|
| 49 |
+
deployment = CodeReviewDeployment.bind()
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def start_serve():
|
| 53 |
+
"""Start Ray Serve deployment."""
|
| 54 |
+
import ray
|
| 55 |
+
|
| 56 |
+
# Initialize Ray if not already initialized
|
| 57 |
+
if not ray.is_initialized():
|
| 58 |
+
ray.init()
|
| 59 |
+
|
| 60 |
+
# Deploy the application
|
| 61 |
+
serve.run(
|
| 62 |
+
deployment,
|
| 63 |
+
host=config.ray_serve_host,
|
| 64 |
+
port=config.ray_serve_port,
|
| 65 |
+
name="code_review_service",
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
logger.info(
|
| 69 |
+
f"Ray Serve deployment started at http://{config.ray_serve_host}:{config.ray_serve_port}"
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
if __name__ == "__main__":
|
| 74 |
+
start_serve()
|
app/utils.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Utility functions for code review processing."""
|
| 2 |
+
|
| 3 |
+
import hashlib
|
| 4 |
+
import logging
|
| 5 |
+
import re
|
| 6 |
+
from typing import Optional
|
| 7 |
+
|
| 8 |
+
import tiktoken
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def count_tokens(text: str, model: str = "gpt-4") -> int:
|
| 14 |
+
"""
|
| 15 |
+
Count tokens in text using tiktoken.
|
| 16 |
+
|
| 17 |
+
Args:
|
| 18 |
+
text: Text to count tokens for
|
| 19 |
+
model: Model name for tokenizer
|
| 20 |
+
|
| 21 |
+
Returns:
|
| 22 |
+
Number of tokens
|
| 23 |
+
"""
|
| 24 |
+
try:
|
| 25 |
+
encoding = tiktoken.encoding_for_model(model)
|
| 26 |
+
except KeyError:
|
| 27 |
+
# Fallback to cl100k_base for unknown models
|
| 28 |
+
encoding = tiktoken.get_encoding("cl100k_base")
|
| 29 |
+
|
| 30 |
+
return len(encoding.encode(text))
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def extract_files_from_diff(diff: str) -> list[str]:
|
| 34 |
+
"""
|
| 35 |
+
Extract file paths from a git diff.
|
| 36 |
+
|
| 37 |
+
Args:
|
| 38 |
+
diff: Git diff content
|
| 39 |
+
|
| 40 |
+
Returns:
|
| 41 |
+
List of file paths mentioned in the diff
|
| 42 |
+
"""
|
| 43 |
+
files = set()
|
| 44 |
+
# Match lines like: diff --git a/path/to/file b/path/to/file
|
| 45 |
+
# or +++ b/path/to/file
|
| 46 |
+
patterns = [
|
| 47 |
+
r"^diff --git a/(.*?) b/",
|
| 48 |
+
r"^\+\+\+ b/(.*?)$",
|
| 49 |
+
r"^--- a/(.*?)$",
|
| 50 |
+
]
|
| 51 |
+
|
| 52 |
+
for line in diff.splitlines():
|
| 53 |
+
for pattern in patterns:
|
| 54 |
+
match = re.match(pattern, line)
|
| 55 |
+
if match:
|
| 56 |
+
file_path = match.group(1)
|
| 57 |
+
# Skip /dev/null (for deleted/new files)
|
| 58 |
+
if file_path != "/dev/null":
|
| 59 |
+
files.add(file_path)
|
| 60 |
+
|
| 61 |
+
return sorted(files)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def sanitize_diff(diff: str) -> str:
|
| 65 |
+
"""
|
| 66 |
+
Sanitize diff content to prevent injection attacks.
|
| 67 |
+
|
| 68 |
+
Args:
|
| 69 |
+
diff: Raw diff content
|
| 70 |
+
|
| 71 |
+
Returns:
|
| 72 |
+
Sanitized diff
|
| 73 |
+
"""
|
| 74 |
+
# Remove any potential shell commands or suspicious patterns
|
| 75 |
+
# This is a basic sanitization - in production, use more robust methods
|
| 76 |
+
sanitized = diff
|
| 77 |
+
|
| 78 |
+
# Remove null bytes
|
| 79 |
+
sanitized = sanitized.replace("\x00", "")
|
| 80 |
+
|
| 81 |
+
# Limit line length to prevent DOS
|
| 82 |
+
max_line_length = 1000
|
| 83 |
+
lines = sanitized.splitlines()
|
| 84 |
+
sanitized_lines = [line[:max_line_length] for line in lines]
|
| 85 |
+
|
| 86 |
+
return "\n".join(sanitized_lines)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def detect_language(diff: str) -> str:
|
| 90 |
+
"""
|
| 91 |
+
Detect the primary programming language from a diff.
|
| 92 |
+
|
| 93 |
+
Args:
|
| 94 |
+
diff: Git diff content
|
| 95 |
+
|
| 96 |
+
Returns:
|
| 97 |
+
Detected language (defaults to "python")
|
| 98 |
+
"""
|
| 99 |
+
files = extract_files_from_diff(diff)
|
| 100 |
+
|
| 101 |
+
# Count file extensions
|
| 102 |
+
extension_counts: dict[str, int] = {}
|
| 103 |
+
for file in files:
|
| 104 |
+
if "." in file:
|
| 105 |
+
ext = file.rsplit(".", 1)[-1].lower()
|
| 106 |
+
extension_counts[ext] = extension_counts.get(ext, 0) + 1
|
| 107 |
+
|
| 108 |
+
if not extension_counts:
|
| 109 |
+
return "python"
|
| 110 |
+
|
| 111 |
+
# Map extensions to languages
|
| 112 |
+
extension_map = {
|
| 113 |
+
"py": "python",
|
| 114 |
+
"js": "javascript",
|
| 115 |
+
"ts": "typescript",
|
| 116 |
+
"jsx": "javascript",
|
| 117 |
+
"tsx": "typescript",
|
| 118 |
+
"java": "java",
|
| 119 |
+
"go": "go",
|
| 120 |
+
"rs": "rust",
|
| 121 |
+
"cpp": "c++",
|
| 122 |
+
"c": "c",
|
| 123 |
+
"rb": "ruby",
|
| 124 |
+
"php": "php",
|
| 125 |
+
"swift": "swift",
|
| 126 |
+
"kt": "kotlin",
|
| 127 |
+
"scala": "scala",
|
| 128 |
+
"cs": "csharp",
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
# Find most common extension
|
| 132 |
+
most_common_ext = max(extension_counts, key=extension_counts.get) # type: ignore
|
| 133 |
+
return extension_map.get(most_common_ext, "unknown")
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def generate_request_id() -> str:
|
| 137 |
+
"""
|
| 138 |
+
Generate a unique request ID for tracing.
|
| 139 |
+
|
| 140 |
+
Returns:
|
| 141 |
+
Unique request ID
|
| 142 |
+
"""
|
| 143 |
+
import time
|
| 144 |
+
import uuid
|
| 145 |
+
|
| 146 |
+
timestamp = str(time.time())
|
| 147 |
+
unique_id = str(uuid.uuid4())
|
| 148 |
+
combined = f"{timestamp}-{unique_id}"
|
| 149 |
+
return hashlib.sha256(combined.encode()).hexdigest()[:16]
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def truncate_text(text: str, max_length: int = 1000, suffix: str = "...") -> str:
|
| 153 |
+
"""
|
| 154 |
+
Truncate text to a maximum length.
|
| 155 |
+
|
| 156 |
+
Args:
|
| 157 |
+
text: Text to truncate
|
| 158 |
+
max_length: Maximum length
|
| 159 |
+
suffix: Suffix to add when truncated
|
| 160 |
+
|
| 161 |
+
Returns:
|
| 162 |
+
Truncated text
|
| 163 |
+
"""
|
| 164 |
+
if len(text) <= max_length:
|
| 165 |
+
return text
|
| 166 |
+
return text[: max_length - len(suffix)] + suffix
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def parse_severity_score(severity: str) -> int:
|
| 170 |
+
"""
|
| 171 |
+
Convert severity to numeric score for sorting.
|
| 172 |
+
|
| 173 |
+
Args:
|
| 174 |
+
severity: Severity level (critical, high, medium, low)
|
| 175 |
+
|
| 176 |
+
Returns:
|
| 177 |
+
Numeric score (higher = more severe)
|
| 178 |
+
"""
|
| 179 |
+
severity_map = {
|
| 180 |
+
"critical": 4,
|
| 181 |
+
"high": 3,
|
| 182 |
+
"medium": 2,
|
| 183 |
+
"low": 1,
|
| 184 |
+
}
|
| 185 |
+
return severity_map.get(severity.lower(), 0)
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def format_elapsed_time(milliseconds: int) -> str:
|
| 189 |
+
"""
|
| 190 |
+
Format elapsed time in a human-readable format.
|
| 191 |
+
|
| 192 |
+
Args:
|
| 193 |
+
milliseconds: Time in milliseconds
|
| 194 |
+
|
| 195 |
+
Returns:
|
| 196 |
+
Formatted time string (e.g., "1.5s", "250ms")
|
| 197 |
+
"""
|
| 198 |
+
if milliseconds < 1000:
|
| 199 |
+
return f"{milliseconds}ms"
|
| 200 |
+
elif milliseconds < 60000:
|
| 201 |
+
return f"{milliseconds / 1000:.1f}s"
|
| 202 |
+
else:
|
| 203 |
+
minutes = milliseconds // 60000
|
| 204 |
+
seconds = (milliseconds % 60000) / 1000
|
| 205 |
+
return f"{minutes}m {seconds:.1f}s"
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
api:
|
| 5 |
+
build:
|
| 6 |
+
context: .
|
| 7 |
+
dockerfile: Dockerfile
|
| 8 |
+
ports:
|
| 9 |
+
- "8000:8000"
|
| 10 |
+
env_file:
|
| 11 |
+
- .env
|
| 12 |
+
environment:
|
| 13 |
+
- ENABLE_RAY_SERVE=false
|
| 14 |
+
volumes:
|
| 15 |
+
- ./logs:/app/logs
|
| 16 |
+
restart: unless-stopped
|
| 17 |
+
healthcheck:
|
| 18 |
+
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
| 19 |
+
interval: 30s
|
| 20 |
+
timeout: 10s
|
| 21 |
+
retries: 3
|
| 22 |
+
start_period: 40s
|
| 23 |
+
|
| 24 |
+
# Ray Serve deployment (optional, use with --profile production)
|
| 25 |
+
ray-head:
|
| 26 |
+
profiles:
|
| 27 |
+
- production
|
| 28 |
+
build:
|
| 29 |
+
context: .
|
| 30 |
+
dockerfile: Dockerfile
|
| 31 |
+
ports:
|
| 32 |
+
- "8000:8000"
|
| 33 |
+
- "8265:8265" # Ray dashboard
|
| 34 |
+
env_file:
|
| 35 |
+
- .env
|
| 36 |
+
environment:
|
| 37 |
+
- ENABLE_RAY_SERVE=true
|
| 38 |
+
command: >
|
| 39 |
+
sh -c "
|
| 40 |
+
ray start --head --port=6379 --dashboard-host=0.0.0.0 --dashboard-port=8265 &&
|
| 41 |
+
python -m app.serve
|
| 42 |
+
"
|
| 43 |
+
volumes:
|
| 44 |
+
- ./logs:/app/logs
|
| 45 |
+
restart: unless-stopped
|
| 46 |
+
healthcheck:
|
| 47 |
+
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
| 48 |
+
interval: 30s
|
| 49 |
+
timeout: 10s
|
| 50 |
+
retries: 3
|
| 51 |
+
start_period: 60s
|
| 52 |
+
|
| 53 |
+
volumes:
|
| 54 |
+
logs:
|
pyproject.toml
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "code-reviewer-ci-agent"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "Production-ready AI code review agent using CrewAI multi-agent framework"
|
| 5 |
+
readme = "README.md"
|
| 6 |
+
requires-python = ">=3.11"
|
| 7 |
+
license = { text = "MIT" }
|
| 8 |
+
authors = [
|
| 9 |
+
{ name = "Code Reviewer Team" }
|
| 10 |
+
]
|
| 11 |
+
keywords = ["ai", "code-review", "crewai", "agents", "github-actions"]
|
| 12 |
+
classifiers = [
|
| 13 |
+
"Development Status :: 4 - Beta",
|
| 14 |
+
"Intended Audience :: Developers",
|
| 15 |
+
"License :: OSI Approved :: MIT License",
|
| 16 |
+
"Programming Language :: Python :: 3.11",
|
| 17 |
+
"Programming Language :: Python :: 3.12",
|
| 18 |
+
]
|
| 19 |
+
|
| 20 |
+
dependencies = [
|
| 21 |
+
"crewai>=0.80.0",
|
| 22 |
+
"crewai-tools>=0.14.0",
|
| 23 |
+
"fastapi>=0.110.0",
|
| 24 |
+
"uvicorn[standard]>=0.27.0",
|
| 25 |
+
"pydantic>=2.6.0",
|
| 26 |
+
"pydantic-settings>=2.1.0",
|
| 27 |
+
"python-dotenv>=1.0.0",
|
| 28 |
+
"httpx>=0.27.0",
|
| 29 |
+
"python-multipart>=0.0.9",
|
| 30 |
+
"ray[serve]>=2.9.0",
|
| 31 |
+
"openai>=1.12.0",
|
| 32 |
+
"langchain-openai>=0.1.0",
|
| 33 |
+
"langchain-groq>=0.1.0",
|
| 34 |
+
"tiktoken>=0.6.0",
|
| 35 |
+
"PyYAML>=6.0.1",
|
| 36 |
+
"litellm>=1.0.0",
|
| 37 |
+
]
|
| 38 |
+
|
| 39 |
+
[project.optional-dependencies]
|
| 40 |
+
dev = [
|
| 41 |
+
"pytest>=8.0.0",
|
| 42 |
+
"pytest-asyncio>=0.23.0",
|
| 43 |
+
"pytest-cov>=4.1.0",
|
| 44 |
+
"black>=24.0.0",
|
| 45 |
+
"ruff>=0.2.0",
|
| 46 |
+
"mypy>=1.8.0",
|
| 47 |
+
"httpx>=0.27.0",
|
| 48 |
+
]
|
| 49 |
+
|
| 50 |
+
[build-system]
|
| 51 |
+
requires = ["setuptools>=68.0", "wheel"]
|
| 52 |
+
build-backend = "setuptools.build_meta"
|
| 53 |
+
|
| 54 |
+
[tool.black]
|
| 55 |
+
line-length = 100
|
| 56 |
+
target-version = ["py311"]
|
| 57 |
+
include = '\.pyi?$'
|
| 58 |
+
|
| 59 |
+
[tool.ruff]
|
| 60 |
+
line-length = 100
|
| 61 |
+
target-version = "py311"
|
| 62 |
+
select = [
|
| 63 |
+
"E", # pycodestyle errors
|
| 64 |
+
"W", # pycodestyle warnings
|
| 65 |
+
"F", # pyflakes
|
| 66 |
+
"I", # isort
|
| 67 |
+
"B", # flake8-bugbear
|
| 68 |
+
"C4", # flake8-comprehensions
|
| 69 |
+
"UP", # pyupgrade
|
| 70 |
+
]
|
| 71 |
+
ignore = [
|
| 72 |
+
"E501", # line too long (handled by black)
|
| 73 |
+
"B008", # do not perform function calls in argument defaults
|
| 74 |
+
]
|
| 75 |
+
|
| 76 |
+
[tool.ruff.per-file-ignores]
|
| 77 |
+
"__init__.py" = ["F401"]
|
| 78 |
+
|
| 79 |
+
[tool.pytest.ini_options]
|
| 80 |
+
testpaths = ["tests"]
|
| 81 |
+
python_files = ["test_*.py"]
|
| 82 |
+
python_classes = ["Test*"]
|
| 83 |
+
python_functions = ["test_*"]
|
| 84 |
+
addopts = "-v --cov=app --cov-report=term-missing"
|
| 85 |
+
asyncio_mode = "auto"
|
| 86 |
+
|
| 87 |
+
[tool.mypy]
|
| 88 |
+
python_version = "3.11"
|
| 89 |
+
warn_return_any = true
|
| 90 |
+
warn_unused_configs = true
|
| 91 |
+
disallow_untyped_defs = false
|
| 92 |
+
ignore_missing_imports = true
|
requirements.txt
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Core dependencies - extracted from pyproject.toml
|
| 2 |
+
# This file is for deployment platforms (GitHub Actions, Hugging Face Spaces, etc.)
|
| 3 |
+
# that prefer requirements.txt over pyproject.toml
|
| 4 |
+
|
| 5 |
+
# CrewAI Multi-Agent Framework
|
| 6 |
+
crewai>=0.80.0
|
| 7 |
+
crewai-tools>=0.14.0
|
| 8 |
+
|
| 9 |
+
# Web Framework
|
| 10 |
+
fastapi>=0.110.0
|
| 11 |
+
uvicorn[standard]>=0.27.0
|
| 12 |
+
|
| 13 |
+
# Configuration & Data Validation
|
| 14 |
+
pydantic>=2.6.0
|
| 15 |
+
pydantic-settings>=2.1.0
|
| 16 |
+
python-dotenv>=1.0.0
|
| 17 |
+
|
| 18 |
+
# HTTP Client
|
| 19 |
+
httpx>=0.27.0
|
| 20 |
+
python-multipart>=0.0.9
|
| 21 |
+
|
| 22 |
+
# LLM Providers
|
| 23 |
+
openai>=1.12.0
|
| 24 |
+
langchain-openai>=0.1.0
|
| 25 |
+
langchain-groq>=0.1.0
|
| 26 |
+
litellm>=1.0.0
|
| 27 |
+
|
| 28 |
+
# Tokenization
|
| 29 |
+
tiktoken>=0.6.0
|
| 30 |
+
|
| 31 |
+
# Utilities
|
| 32 |
+
PyYAML>=6.0.1
|
| 33 |
+
|
| 34 |
+
# Optional: Ray Serve (for scaling)
|
| 35 |
+
# Uncomment if deploying with Ray Serve
|
| 36 |
+
# ray[serve]>=2.9.0
|
start.sh
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# Quick start script for Code Reviewer CI Agent
|
| 3 |
+
|
| 4 |
+
set -e
|
| 5 |
+
|
| 6 |
+
echo "π€ Code Reviewer CI Agent - Setup Script"
|
| 7 |
+
echo "=========================================="
|
| 8 |
+
echo ""
|
| 9 |
+
|
| 10 |
+
# Check if .env exists
|
| 11 |
+
if [ ! -f .env ]; then
|
| 12 |
+
echo "β οΈ No .env file found. Creating from template..."
|
| 13 |
+
cp .env.example .env
|
| 14 |
+
echo "β
Created .env file"
|
| 15 |
+
echo ""
|
| 16 |
+
echo "π Please edit .env and add:"
|
| 17 |
+
echo " - Your LLM API key (OPENAI_API_KEY or GROQ_API_KEY)"
|
| 18 |
+
echo " - A secure REVIEW_API_KEY"
|
| 19 |
+
echo ""
|
| 20 |
+
echo "Then run this script again."
|
| 21 |
+
exit 1
|
| 22 |
+
fi
|
| 23 |
+
|
| 24 |
+
# Source environment
|
| 25 |
+
export $(cat .env | grep -v '^#' | xargs)
|
| 26 |
+
|
| 27 |
+
# Check if API key is set
|
| 28 |
+
if [ -z "$OPENAI_API_KEY" ] && [ -z "$GROQ_API_KEY" ]; then
|
| 29 |
+
echo "β No LLM API key configured in .env"
|
| 30 |
+
echo " Please set OPENAI_API_KEY or GROQ_API_KEY"
|
| 31 |
+
exit 1
|
| 32 |
+
fi
|
| 33 |
+
|
| 34 |
+
if [ -z "$REVIEW_API_KEY" ]; then
|
| 35 |
+
echo "β No REVIEW_API_KEY configured in .env"
|
| 36 |
+
echo " Please set a secure API key for authentication"
|
| 37 |
+
exit 1
|
| 38 |
+
fi
|
| 39 |
+
|
| 40 |
+
echo "β
Environment configured"
|
| 41 |
+
echo ""
|
| 42 |
+
|
| 43 |
+
# Check for Docker
|
| 44 |
+
if command -v docker &> /dev/null; then
|
| 45 |
+
echo "π³ Docker detected"
|
| 46 |
+
echo ""
|
| 47 |
+
echo "Choose deployment mode:"
|
| 48 |
+
echo " 1) Development (Docker Compose - no Ray Serve)"
|
| 49 |
+
echo " 2) Production (Docker Compose with Ray Serve)"
|
| 50 |
+
echo " 3) Local development (pip install)"
|
| 51 |
+
echo ""
|
| 52 |
+
read -p "Enter choice [1-3]: " choice
|
| 53 |
+
|
| 54 |
+
case $choice in
|
| 55 |
+
1)
|
| 56 |
+
echo ""
|
| 57 |
+
echo "π Starting in development mode..."
|
| 58 |
+
docker-compose up --build
|
| 59 |
+
;;
|
| 60 |
+
2)
|
| 61 |
+
echo ""
|
| 62 |
+
echo "π Starting in production mode with Ray Serve..."
|
| 63 |
+
docker-compose --profile production up --build
|
| 64 |
+
;;
|
| 65 |
+
3)
|
| 66 |
+
echo ""
|
| 67 |
+
echo "π¦ Installing dependencies..."
|
| 68 |
+
pip install -e .
|
| 69 |
+
echo ""
|
| 70 |
+
echo "π Starting API server..."
|
| 71 |
+
uvicorn app.api:app --host 0.0.0.0 --port 8000 --reload
|
| 72 |
+
;;
|
| 73 |
+
*)
|
| 74 |
+
echo "Invalid choice"
|
| 75 |
+
exit 1
|
| 76 |
+
;;
|
| 77 |
+
esac
|
| 78 |
+
else
|
| 79 |
+
echo "π¦ Installing dependencies..."
|
| 80 |
+
pip install -e .
|
| 81 |
+
echo ""
|
| 82 |
+
echo "π Starting API server..."
|
| 83 |
+
uvicorn app.api:app --host 0.0.0.0 --port 8000 --reload
|
| 84 |
+
fi
|