github-actions[bot] commited on
Commit
fe15a7c
Β·
1 Parent(s): f0bbf9c

Deploy from GitHub commit 1b2eb65

Browse files
.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 Ci
3
- emoji: πŸ†
4
- colorFrom: yellow
5
- colorTo: red
6
  sdk: docker
7
- pinned: false
8
- short_description: a code reviewer ai agent
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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