From 10c8cfaa0fd4a4e86b487e19e8baee4ccd858841 Mon Sep 17 00:00:00 2001 From: Andrew Ridgway Date: Fri, 8 May 2026 23:46:17 +1000 Subject: [PATCH] initial --- .env.example | 36 +++ .gitea_soon/workflows/build_push.yml | 72 +++++ .gitignore | 1 + Dockerfile | 64 ++++ README.md | 185 +++++++++++ config/__init__.py | 0 config/agents.yaml | 7 + config/tasks.yaml | 16 + contexts/defaults/__init__.py | 0 contexts/defaults/code_review.md | 19 ++ contexts/defaults/security_review.md | 22 ++ crews/code_review_crew/__init__.py | 0 .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 133 bytes .../code_review_crew.cpython-314.pyc | Bin 0 -> 3232 bytes crews/code_review_crew/code_review_crew.py | 55 ++++ crews/code_review_crew/config/agents.yaml | 7 + crews/code_review_crew/config/tasks.yaml | 16 + crews/infra_review_crew/__init__.py | 0 .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 134 bytes .../infra_review_crew.cpython-314.pyc | Bin 0 -> 3531 bytes crews/infra_review_crew/config/agents.yaml | 7 + crews/infra_review_crew/config/tasks.yaml | 16 + crews/infra_review_crew/infra_review_crew.py | 65 ++++ crews/security_review_crew/__init__.py | 0 .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 137 bytes .../security_review_crew.cpython-314.pyc | Bin 0 -> 2690 bytes crews/security_review_crew/config/agents.yaml | 7 + crews/security_review_crew/config/tasks.yaml | 16 + .../security_review_crew.py | 51 +++ crews/summariser_crew/__init__.py | 0 .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 132 bytes .../summariser_crew.cpython-314.pyc | Bin 0 -> 2576 bytes crews/summariser_crew/config/agents.yaml | 7 + crews/summariser_crew/config/tasks.yaml | 16 + crews/summariser_crew/summariser_crew.py | 43 +++ .../.gitea/workflows/build_push.yml | 72 +++++ example_pipelines/kube/blog_deployment.yaml | 24 ++ example_pipelines/kube/blog_pod.yaml | 13 + example_pipelines/kube/blog_service.yaml | 13 + kube/pr-reviewer_deployment.yaml | 24 ++ kube/pr-reviewer_pod.yaml | 13 + kube/pr-reviewer_service.yaml | 13 + mcp_servers/__init__.py | 0 .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 122 bytes .../__pycache__/checkov_mcp.cpython-314.pyc | Bin 0 -> 6092 bytes .../__pycache__/hadolint_mcp.cpython-314.pyc | Bin 0 -> 5355 bytes mcp_servers/checkov_mcp.py | 146 +++++++++ mcp_servers/hadolint_mcp.py | 133 ++++++++ pyproject.toml | 71 +++++ src/pr_reviewer/__init__.py | 0 .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 126 bytes .../__pycache__/context.cpython-314.pyc | Bin 0 -> 1979 bytes .../__pycache__/flow.cpython-314.pyc | Bin 0 -> 9040 bytes .../__pycache__/llm.cpython-314.pyc | Bin 0 -> 2195 bytes .../__pycache__/main.cpython-314.pyc | Bin 0 -> 11201 bytes .../__pycache__/state.cpython-314.pyc | Bin 0 -> 3400 bytes src/pr_reviewer/context.py | 45 +++ src/pr_reviewer/flow.py | 150 +++++++++ src/pr_reviewer/llm.py | 56 ++++ src/pr_reviewer/main.py | 297 ++++++++++++++++++ src/pr_reviewer/state.py | 45 +++ ...t_mcp_servers.cpython-314-pytest-9.0.3.pyc | Bin 0 -> 7933 bytes tests/integration/test_mcp_servers.py | 70 +++++ .../test_context.cpython-314-pytest-9.0.3.pyc | Bin 0 -> 8517 bytes .../test_state.cpython-314-pytest-9.0.3.pyc | Bin 0 -> 14836 bytes tests/unit/test_context.py | 100 ++++++ tests/unit/test_state.py | 69 ++++ tools/__init__.py | 0 tools/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 116 bytes tools/__pycache__/git_tool.cpython-314.pyc | Bin 0 -> 3810 bytes tools/git_tool.py | 57 ++++ 71 files changed, 2139 insertions(+) create mode 100644 .env.example create mode 100644 .gitea_soon/workflows/build_push.yml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 config/__init__.py create mode 100644 config/agents.yaml create mode 100644 config/tasks.yaml create mode 100644 contexts/defaults/__init__.py create mode 100644 contexts/defaults/code_review.md create mode 100644 contexts/defaults/security_review.md create mode 100644 crews/code_review_crew/__init__.py create mode 100644 crews/code_review_crew/__pycache__/__init__.cpython-314.pyc create mode 100644 crews/code_review_crew/__pycache__/code_review_crew.cpython-314.pyc create mode 100644 crews/code_review_crew/code_review_crew.py create mode 100644 crews/code_review_crew/config/agents.yaml create mode 100644 crews/code_review_crew/config/tasks.yaml create mode 100644 crews/infra_review_crew/__init__.py create mode 100644 crews/infra_review_crew/__pycache__/__init__.cpython-314.pyc create mode 100644 crews/infra_review_crew/__pycache__/infra_review_crew.cpython-314.pyc create mode 100644 crews/infra_review_crew/config/agents.yaml create mode 100644 crews/infra_review_crew/config/tasks.yaml create mode 100644 crews/infra_review_crew/infra_review_crew.py create mode 100644 crews/security_review_crew/__init__.py create mode 100644 crews/security_review_crew/__pycache__/__init__.cpython-314.pyc create mode 100644 crews/security_review_crew/__pycache__/security_review_crew.cpython-314.pyc create mode 100644 crews/security_review_crew/config/agents.yaml create mode 100644 crews/security_review_crew/config/tasks.yaml create mode 100644 crews/security_review_crew/security_review_crew.py create mode 100644 crews/summariser_crew/__init__.py create mode 100644 crews/summariser_crew/__pycache__/__init__.cpython-314.pyc create mode 100644 crews/summariser_crew/__pycache__/summariser_crew.cpython-314.pyc create mode 100644 crews/summariser_crew/config/agents.yaml create mode 100644 crews/summariser_crew/config/tasks.yaml create mode 100644 crews/summariser_crew/summariser_crew.py create mode 100644 example_pipelines/.gitea/workflows/build_push.yml create mode 100644 example_pipelines/kube/blog_deployment.yaml create mode 100644 example_pipelines/kube/blog_pod.yaml create mode 100644 example_pipelines/kube/blog_service.yaml create mode 100644 kube/pr-reviewer_deployment.yaml create mode 100644 kube/pr-reviewer_pod.yaml create mode 100644 kube/pr-reviewer_service.yaml create mode 100644 mcp_servers/__init__.py create mode 100644 mcp_servers/__pycache__/__init__.cpython-314.pyc create mode 100644 mcp_servers/__pycache__/checkov_mcp.cpython-314.pyc create mode 100644 mcp_servers/__pycache__/hadolint_mcp.cpython-314.pyc create mode 100644 mcp_servers/checkov_mcp.py create mode 100644 mcp_servers/hadolint_mcp.py create mode 100644 pyproject.toml create mode 100644 src/pr_reviewer/__init__.py create mode 100644 src/pr_reviewer/__pycache__/__init__.cpython-314.pyc create mode 100644 src/pr_reviewer/__pycache__/context.cpython-314.pyc create mode 100644 src/pr_reviewer/__pycache__/flow.cpython-314.pyc create mode 100644 src/pr_reviewer/__pycache__/llm.cpython-314.pyc create mode 100644 src/pr_reviewer/__pycache__/main.cpython-314.pyc create mode 100644 src/pr_reviewer/__pycache__/state.cpython-314.pyc create mode 100644 src/pr_reviewer/context.py create mode 100644 src/pr_reviewer/flow.py create mode 100644 src/pr_reviewer/llm.py create mode 100644 src/pr_reviewer/main.py create mode 100644 src/pr_reviewer/state.py create mode 100644 tests/integration/__pycache__/test_mcp_servers.cpython-314-pytest-9.0.3.pyc create mode 100644 tests/integration/test_mcp_servers.py create mode 100644 tests/unit/__pycache__/test_context.cpython-314-pytest-9.0.3.pyc create mode 100644 tests/unit/__pycache__/test_state.cpython-314-pytest-9.0.3.pyc create mode 100644 tests/unit/test_context.py create mode 100644 tests/unit/test_state.py create mode 100644 tools/__init__.py create mode 100644 tools/__pycache__/__init__.cpython-314.pyc create mode 100644 tools/__pycache__/git_tool.cpython-314.pyc create mode 100644 tools/git_tool.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6f6b5f7 --- /dev/null +++ b/.env.example @@ -0,0 +1,36 @@ +# LLM Configuration +# Choose one of the following LLM providers: +# For OpenAI: +LLM_MODEL=gpt-4 +LLM_BASE_URL=https://api.openai.com/v1 +LLM_API_KEY=your_openai_api_key_here +LLM_PROVIDER=openai + +# For Anthropic: +# LLM_MODEL=claude-3-opus-20240229 +# LLM_BASE_URL=https://api.anthropic.com +# LLM_API_KEY=your_anthropic_api_key_here +# LLM_PROVIDER=anthropic + +# For Ollama (local): +# LLM_MODEL=llama2 +# LLM_BASE_URL=http://localhost:11434 +# LLM_API_KEY=ollama # Ollama doesn't require a real API key +# LLM_PROVIDER=ollama + +# MCP Server Configuration +# Hadolint MCP Server (installed via pip in Docker) +# Checkov MCP Server (installed via pip in Docker) +# Semgrep MCP Server (native, no configuration needed) +# Trivy MCP Server (native, no configuration needed) + +# Optional: Semgrep App URL and Token for SEMgrep App functionality +SEMGRAPH_APP_URL= +SEMGRAPH_API_TOKEN= + +# Timeout Configuration (in seconds) +TOTAL_FLOW_TIMEOUT=600 +PER_CREW_TIMEOUT=300 + +# Other Configuration +LOG_LEVEL=INFO \ No newline at end of file diff --git a/.gitea_soon/workflows/build_push.yml b/.gitea_soon/workflows/build_push.yml new file mode 100644 index 0000000..0116b1f --- /dev/null +++ b/.gitea_soon/workflows/build_push.yml @@ -0,0 +1,72 @@ +name: Build and Push Image +on: + push: + branches: + - master + +jobs: + build: + name: Build and push image + runs-on: ubuntu-latest + container: catthehacker/ubuntu:act-latest + if: gitea.ref == 'refs/heads/master' + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Create Kubeconfig + run: | + mkdir $HOME/.kube + echo "${{ secrets.KUBEC_CONFIG_BUILDX_NEW }}" > $HOME/.kube/config + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver: kubernetes + driver-opts: | + namespace=gitea-runner + qemu.install=true + + - name: Login to Docker Registry + uses: docker/login-action@v3 + with: + registry: git.aridgwayweb.com + username: armistace + password: ${{ secrets.REG_PASSWORD }} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 + tags: | + git.aridgwayweb.com/armistace/pr-reviewer:latest + + - name: Trivy Scan + run: | + echo "Installing Trivy " + sudo apt-get update + sudo apt-get install -y wget apt-transport-https gnupg lsb-release + wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add - + echo deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main | sudo tee -a /etc/apt/sources.list.d/trivy.list + sudo apt-get update + sudo apt-get install -y trivy + trivy image --format table --exit-code 1 --ignore-unfixed --vuln-type os,library --severity HIGH,CRITICAL git.aridgwayweb.com/armistace/pr-reviewer:latest + + - name: Deploy + run: | + echo "Installing Kubectl" + apt-get update + apt-get install -y apt-transport-https ca-certificates curl gnupg + curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.33/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg + chmod 644 /etc/apt/keyrings/kubernetes-apt-keyring.gpg + echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.33/deb/ /' | tee /etc/apt/sources.list.d/kubernetes.list + chmod 644 /etc/apt/sources.list.d/kubernetes.list + apt-get update + apt-get install kubectl + kubectl delete namespace pr-reviewer + kubectl create namespace pr-reviewer + kubectl create secret docker-registry regcred --docker-server=${{ vars.DOCKER_SERVER }} --docker-username=${{ vars.DOCKER_USERNAME }} --docker-password='${{ secrets.DOCKER_PASSWORD }}' --docker-email=${{ vars.DOCKER_EMAIL }} --namespace=pr-reviewer + kubectl apply -f kube/pr-reviewer_pod.yaml && kubectl apply -f kube/pr-reviewer_deployment.yaml && kubectl apply -f kube/pr-reviewer_service.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d60172d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.spec/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3035931 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,64 @@ +# Stage 1: Base with system dependencies and tool installations +FROM python:3.12-slim as builder + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + git \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Install Hadolint (for Dockerfile linting) +RUN curl -Lo /bin/hadolint https://github.com/hadolint/hadolint/releases/download/v2.12.0/hadolint-Linux-x86_64 && \ + chmod +x /bin/hadolint + +# Install Checkov (for Kubernetes security scanning) +RUN pip install checkov==3.1.123 + +# Install Trivy (for container and IaC scanning) - Native MCP server +RUN curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin v0.47.0 + +# Install Semgrep (for code scanning) - Will use native MCP server +RUN pip install semgrep==1.76.0 + +# Install UV package manager +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +# Stage 2: App with source code and UV sync +FROM python:3.12-slim + +# Create non-root user +RUN useradd --create-home --shell /bin/bash app +WORKDIR /app +USER app + +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Copy UV from builder stage +COPY --from=builder /bin/uv /bin/uv +COPY --from=builder /bin/uvx /bin/uvx + +# Copy application code +COPY --chown=app:app pyproject.toml . +COPY --chown=app:app README.md . +COPY --chown=app:app src/ ./src/ +COPY --chown=app:app mcp_servers/ ./mcp_servers/ +COPY --chown=app:app crews/ ./crews/ +COPY --chown=app:app tools/ ./tools/ +COPY --chown=app:app config/ ./config/ +COPY --chown=app:app contexts/ ./contexts/ + +# Install Python dependencies using UV +RUN uv sync --frozen --no-dev + +# Set environment variables +ENV PYTHONPATH=/app/src +ENV PATH="/app/.venv/bin:$PATH" + +# Expose port +EXPOSE 8000 + +# Set entrypoint +ENTRYPOINT ["uvicorn", "src.pr_reviewer.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5805519 --- /dev/null +++ b/README.md @@ -0,0 +1,185 @@ +# PR Reviewer + +An automated pull request review system using CrewAI and MCP (Model Context Protocol). + +## Overview + +This system provides automated code, security, and infrastructure reviews for pull requests using a multi-agent approach. It leverages CrewAI for orchestrating specialized review agents and MCP (Model Context Protocol) for integrating with various static analysis tools. + +## Features + +- **Code Review**: Uses Semgrep (via MCP) to check code quality, best practices, and maintainability +- **Security Review**: Uses Trivy (native MCP) to identify security vulnerabilities +- **Infrastructure Review**: Uses Hadolint and Checkov (via MCP wrappers) to review Dockerfiles and Kubernetes manifests +- **Contextual Review**: Incorporates customizable guidelines for code, security, and infrastructure reviews +- **Automated Orchestration**: Uses CrewAI Flows to manage the review process +- **REST API**: FastAPI endpoint for triggering reviews +- **Containerized**: Docker support for easy deployment + +## Architecture + +The system follows a modular architecture with: +- State management using Pydantic models +- LLM factory for flexible provider support (OpenAI, Anthropic, Ollama) +- Context resolution system for incorporating review guidelines +- Crew-based implementation for each review type (code, security, infrastructure) +- MCP server integrations for static analysis tools +- Flow-based orchestration for managing the review process +- RESTful API for integration with CI/CD systems + +## Installation + +### Prerequisites +- Python 3.10-3.13 +- UV package manager +- Git +- Docker (optional, for containerized deployment) + +### Local Development +1. Clone the repository +2. Install UV package manager: `curl -LsSf https://astral.sh/uv/install.sh | sh` +3. Activate UV environment: `source $HOME/.local/bin/env` +4. Create virtual environment: `uv venv .venv` +5. Activate virtual environment: `source .venv/bin/activate` +6. Install dependencies: `uv pip install -e .` +7. Configure environment variables (see `.env.example`) + +### Docker Deployment +1. Build the Docker image: `docker build -t pr-reviewer .` +2. Run the container: `docker run -p 8000:8000 --env-file .env pr-reviewer` + +## Usage + +### API Endpoints + +#### Health Check +```bash +GET /api/v1/health +``` +Returns the health status of the service. + +#### Trigger PR Review +```bash +POST /api/v1/review +``` +Initiates a pull request review. + +Request Body: +```json +{ + "pr_id": "123", + "title": "Add new feature", + "description": "This PR adds a new feature to the application", + "repo": { + "name": "my-repo", + "url": "https://github.com/user/my-repo" + }, + "source": { + "branch": "feature/new-feature", + "commit": "abc123" + }, + "target": { + "branch": "main", + "commit": "def456" + }, + "files": [ + { + "path": "src/main.py", + "content": "print('Hello World')", + "status": "modified", + "additions": 1, + "deletions": 0 + } + ], + "context": { + "code_review": "Follow PEP8 guidelines", + "security_review": "Check for SQL injection vulnerabilities", + "infra_review": "Ensure Dockerfile follows best practices" + } +} +``` + +Response: +```json +{ + "review_id": "uuid-string", + "status": "completed", + "timestamp": "2023-05-08T10:00:00Z", + "results": { + "code_review": "Code review results...", + "security_review": "Security review results...", + "infra_review": "Infrastructure review results...", + "summary": "Synthesized review summary..." + }, + "metadata": { + "processing_time_seconds": 45.2, + "pr_id": "123", + "repo": { + "name": "my-repo", + "url": "https://github.com/user/my-repo" + } + } +} +``` + +## Configuration + +### Environment Variables +See `.env.example` for detailed configuration options. + +### Context Files +Default review guidelines are located in `contexts/defaults/`: +- `code_review.md`: Coding practice guidelines +- `security_review.md`: Security guidelines +- `infra_review.md`: Infrastructure guidelines + +These can be overridden via the API context parameter. + +## Development + +### Running Tests +```bash +# Run unit tests +pytest + +# Run tests with coverage +pytest --cov=src.pr_reviewer + +# Run specific test categories +pytest tests/unit/ +pytest tests/integration/ +``` + +### Code Style +The project uses Black for code formatting and Flake8 for linting. + +Run formatting: +```bash +black src/ +``` + +Run linting: +```bash +flake8 src/ +``` + +## Deployment + +### Kubernetes +Kubernetes manifests are available in the `k8s/` directory: +- Secret for LLM configuration +- Deployment for the PR Reviewer service +- Service for exposing the API + +### Gitea Actions +GitHub Actions workflow for CI/CD is available in `.gitea/workflows/deploy.yaml`. + +## License +MIT + +## Contributing +1. Fork the repository +2. Create a feature branch +3. Commit your changes +4. Push to the branch +5. Open a pull request \ No newline at end of file diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/agents.yaml b/config/agents.yaml new file mode 100644 index 0000000..13be677 --- /dev/null +++ b/config/agents.yaml @@ -0,0 +1,7 @@ +# Summarizer Agent Configuration +summariser: + role: Senior Code Review Coordinator + goal: Synthesize individual review results into a cohesive, actionable review report + backstory: You are a senior technical lead with extensive experience in code review practices across multiple domains. You excel at combining feedback from different reviewers into a clear, prioritized, and actionable summary that helps development teams improve their code efficiently. + verbose: true + allow_delegation: false \ No newline at end of file diff --git a/config/tasks.yaml b/config/tasks.yaml new file mode 100644 index 0000000..ce72ea6 --- /dev/null +++ b/config/tasks.yaml @@ -0,0 +1,16 @@ +# Summarizer Task Configuration +summarise_task: + description: | + Synthesize the results from code, security, and infrastructure reviews into a cohesive review report. + Code Review Results: {code_review_results} + Security Review Results: {security_review_results} + Infrastructure Review Results: {infra_review_results} + Context: {context} + expected_output: | + A comprehensive review report that includes: + - Executive summary of all findings + - Prioritized list of issues (critical, high, medium, low) + - Specific recommendations for each domain (code, security, infrastructure) + - Overall assessment and recommendation (e.g., Approved, Approved with Minor Changes, Significant Changes Needed) + - Summary of positive aspects of the PR + agent: summariser \ No newline at end of file diff --git a/contexts/defaults/__init__.py b/contexts/defaults/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/contexts/defaults/code_review.md b/contexts/defaults/code_review.md new file mode 100644 index 0000000..6da6f6a --- /dev/null +++ b/contexts/defaults/code_review.md @@ -0,0 +1,19 @@ +# Code Review Guidelines + +## General Principles +- Write clean, readable, and maintainable code. +- Follow the project's coding standards and style guides. +- Ensure code is well-tested and documented. +- Avoid code duplication; refactor when necessary. +- Use meaningful names for variables, functions, and classes. +- Keep functions and classes focused on a single responsibility. + +## Specific Checks +- [ ] Code follows the project's style guide (e.g., PEP8 for Python). +- [ ] No commented-out code or debug prints in production code. +- [ ] Proper error handling and logging. +- [ ] Resource management (e.g., closing files, releasing network connections). +- [ ] Security best practices (input validation, output encoding, etc.). +- [ ] Performance considerations (avoid unnecessary loops, optimize database queries). +- [ ] Unit tests are present and passing for new code. +- [ ] Changes are backward compatible or have a migration plan. \ No newline at end of file diff --git a/contexts/defaults/security_review.md b/contexts/defaults/security_review.md new file mode 100644 index 0000000..228f334 --- /dev/null +++ b/contexts/defaults/security_review.md @@ -0,0 +1,22 @@ +# Security Review Guidelines + +## General Principles +- Follow the principle of least privilege. +- Validate and sanitize all user inputs. +- Use secure coding practices to prevent common vulnerabilities. +- Keep dependencies up to date and monitor for known security issues. +- Implement proper authentication and authorization mechanisms. +- Encrypt sensitive data at rest and in transit. +- Log security-relevant events and monitor for suspicious activities. + +## Specific Checks +- [ ] Input validation and sanitization (SQL injection, XSS, command injection, etc.). +- [ ] Proper authentication and session management. +- [ ] Authorization checks (users can only access resources they are permitted to). +- [ ] Secure handling of sensitive data (passwords, tokens, PII). +- [ ] Use of up-to-date and secure dependencies (no known vulnerabilities). +- [ ] Proper error handling that does not leak sensitive information. +- [ ] Secure configuration (e.g., not using default passwords, disabling unnecessary services). +- [ ] Communication security (use of HTTPS, proper certificate validation). +- [ ] Protection against CSRF, clickjacking, and other web vulnerabilities. +- [ ] Secure file uploads (if applicable). \ No newline at end of file diff --git a/crews/code_review_crew/__init__.py b/crews/code_review_crew/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/crews/code_review_crew/__pycache__/__init__.cpython-314.pyc b/crews/code_review_crew/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c805a48d1569cd4793eddc8815341ff5cd74dd64 GIT binary patch literal 133 zcmdPq>P{wCAAftgHh(Vb_lhJP_LlF~@{~08CODVZ1wY*qA zIX@*ez9_XUGqpS(B%&W5pP83g5+AQuPWN0yn@rjv{k+Fyw G$N~V>^Bmm( literal 0 HcmV?d00001 diff --git a/crews/code_review_crew/__pycache__/code_review_crew.cpython-314.pyc b/crews/code_review_crew/__pycache__/code_review_crew.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f8a68dfe975140c6b1bbd3399905c080146feb19 GIT binary patch literal 3232 zcmb_e&u<&Y6`uXU-6cgylt@*Et<n2tlH6&^~?8+h1*79~uuHYJ8xKdTQTGhB#)wzBa z?P@JiHMk+iTFtCl+>&FxmgGtBCu*tc03WEPdHO6FBi|ynF-~lAS((?OJ@}xlpCbk9 zOPm&TVO^%)-B(=ZiNwOH7jU6pciCG)M{_|DnX8xUOCG)B(S?@V;T{Fs*b;B~VPvhl z)Lrwy&aM?yp}*=kxzHAZjb^l4)c3X~2fu~6OI&4>EEBHSgsV3#u6=`qimhsLt%d@#1kUg(?aX}d%RIT9l0H-VtS5W}%W3ILN?+;;-=ICfhZ+hW zZjc`+JOjZZzIPWd-u*7l^N}Hsmi$Uc^UW}5tv9(JtilldZ?C&;pKs); ze36yAqp)JdU#++pNmkx)*V^5Yo{d9TBU4j37MwJUAwJf@pI~qiB6H)&xui76FMt~H zjn9%BF;R$3$^h-_N`>4o;uq>xJnBC$Lu8MwkQD5DaRd}mY;|6ZthV;d)mndbMTPI{ z7kcMYdj&Kvd>LB!}WTg|!t`i~c{6*OTs z!?iUxXbIh=t4wHKaOYYf(H$(6fM?WivLA*kD_x^h`Xvj4Zmv{fz{a4|@>!?tZj=I` zqe!!+8w6g9l&Gn-<@iuT z;AB6A?w_0Fp*8p$bK5%g7whyF)8F6AJtszfwy)|VLk~xDzngn+ZeN4(qs-(7uY6d0 zzqVDrbboSZYH=?!^}($V{rCN?Z@+r~*3L}rD-)I-n$}F}9sRG?w7@z+exlq44lOA^Cl%!!xu<@K>xDr*zD|RJLUouzXbeb@HJ6LQ zUT|7a33>sV+@#;cchZgnsD#|*p5ur!PlcfGoPgyl2OWgrF)6JG2@rLtLwSDN%Ojex)??W&ixl(TUIz*H-S%~g zaVsQ*5v43-Gzz&eJ2Y&1jL{OfqctYQ+fqzSijc@8rjjw{ZI`*6(`cb8(_IY5GUGMg7O(udc;(&7?&$gL(esA`2J+HKruI{0 zNqIx*8zem-QSfes5fo9>S*YSE$5{(o>unrEeaAndo@dx`R(#5M8!BfIg4=YQR@emL zEn{qu1yT!TL5xC$aLIT@;d=?DVhltC6vJ;i7(f5kI`=?$#mF(4X?pE8m}2FLrjQTM z;pdL3)TK*NWkse?>`?#No6!C1*AU9T=`TOZ&2C=Z%g$_G`MVC8v45Hv1CLb@o&ya= z`#9Rq5F_`F Agent: + """Senior Software Engineer agent for code review.""" + return Agent( + config=self.agents_config["code_reviewer"], + tools=[], # Tools will be added via MCP adapter in the task + verbose=True + ) + + @Task + def code_review_task(self) -> Task: + """Task for conducting code review.""" + return Task( + config=self.tasks_config["code_review_task"], + ) + + @Crew + def crew(self) -> Crew: + """Create the Code Review crew.""" + # Create MCP server adapter for Semgrep + semgrep_adapter = MCPServerAdapter(self.semgrep_server_params) + + return Crew( + agents=[self.code_reviewer()], + tasks=[self.code_review_task()], + process="sequential", + verbose=True, + tools=semgrep_adapter.tools if hasattr(semgrep_adapter, 'tools') else [], + ) \ No newline at end of file diff --git a/crews/code_review_crew/config/agents.yaml b/crews/code_review_crew/config/agents.yaml new file mode 100644 index 0000000..dcc4082 --- /dev/null +++ b/crews/code_review_crew/config/agents.yaml @@ -0,0 +1,7 @@ +# Code Review Crew Agents Configuration +code_reviewer: + role: Senior Software Engineer + goal: Conduct thorough code reviews focusing on code quality, best practices, and maintainability + backstory: You are an experienced software engineer with a keen eye for detail and a passion for clean code. You have reviewed thousands of pull requests and helped teams improve their code quality. + verbose: true + allow_delegation: false \ No newline at end of file diff --git a/crews/code_review_crew/config/tasks.yaml b/crews/code_review_crew/config/tasks.yaml new file mode 100644 index 0000000..ca96fb8 --- /dev/null +++ b/crews/code_review_crew/config/tasks.yaml @@ -0,0 +1,16 @@ +# Code Review Crew Tasks Configuration +code_review_task: + description: | + Review the code changes in the pull request for quality, best practices, and maintainability. + PR Title: {pr_title} + PR Description: {pr_description} + Files to review: {files} + Context: {context} + expected_output: | + A detailed code review report including: + - Summary of changes + - Code quality issues (if any) + - Best practices violations (if any) + - Suggestions for improvement + - Overall rating (e.g., Approved, Changes Needed) + agent: code_reviewer \ No newline at end of file diff --git a/crews/infra_review_crew/__init__.py b/crews/infra_review_crew/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/crews/infra_review_crew/__pycache__/__init__.cpython-314.pyc b/crews/infra_review_crew/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5d90bd40b55ede26060171446b18c07161c67dc0 GIT binary patch literal 134 zcmdPq>P{wCAAftgHh(Vb_lhJP_LlF~@{~08COF6kHwY*qA zGcT(nPGG$8>A#)7>V$laoBoJ|i`^P5w#frhmX=6SVp36Na#3MQQDy2$q{~Z@ zqQ*3_R+gego#|q&F2z_3_>rY}F~JhWBukzoQ{)sWYty70U6vM=;0`uYR_BPJe~Vi~ z<@&4Ce&-d-w|V6Hs_ina-n0C-xQb*$;^VKsTDoh~4VzxCSxshBpiSLnHOC96C5u`O z8|eORL+0uYr^>i;-QBDPw*_-}HQD_m>@8wR+hmzAsZ5ysKxfJ^;z?zRsb!f($_mpe zEm&w)Kfh@;>RS^bje(5=QdQb#DGk1&-5)UTN)_^Jh?Bnh zC&~SO)Q~d7OJ2HoC)#XdW(mK97QQhk7nQHh4(zGP8+s z?enR=v;$;}blz41ErZ0oc+%{jRLWKAQ5fh=xCX$||fVwn=51Ba>d-?u5 zM}h1A45oi>lPCJfAEIr2=COYKo7|OdI@6uJ)0!wf8K3-9{Jr>IR6Q4es6N)`_L3xI z-)>FZ8M1T0{!~A`3(|-PxqjH;yD>0!voK-ERmcPR<=+uf18Ro+LRy2|yeqvy3ep^T zB!7$Nxkhcs3fGXR3R^IT2078N7#|4@QUe?ajdFUM9>;%@rfIpZ$1G->CeJ*p^pJZ3 z&iiSYK=G$Ui^Tti(+3gpp&4L&SJotVuK@>grmq^PQo`pR8f9q#9~6!}U}{X}M7fN-DpnX|C#lPOii|Fj|8Fir zkbb_4^7yKOi&E$!nBJ?2&$BK<_!KVs4H*0RE?Nv-(v%KlCOZ1i7@U;zq^aDG_SqGH zpv5qg(Iq%)00S9PQA)93zkjYsCDCi`r$OZm-vO0#C|N$DWcfLgNBVaH_Kfw{!*9QO z=b@UHCWiF>YRE@aswidkQdp}Ypd(v3=sy5Ivuw@IidY-A9btolwI)wr1bIB}+i$N! z>)}}Sdj{0!0B8V*u=n86CiSYe?^6TLiXV&^MZ3^rI8CF3=$hqQj8U%Hzh$;t6UQjV zlt3klL2sx6vH19)f?7eN$jAD%vrmXw^|~og&`0Ozve|4<8dQ8WPan=6q+Wo7{$F5% zgwaW6;lmr9gv6Gr_O)hlPSvb(0d|@x4VbW@0(n7kS@q`(T3G6yt z=#Zi0#Z9x})z<5{hGO{kx>XN3W2U*{P@h33WV;@)qo!H&s?e#NM@iqu30nnGF#^+L zR0#VA?)VXG`4m)KD6dY{M5j4#t}`1t6F#x8CRc56fjKDC!4_WpJrp;<@N3tC(vw6g zPMGojuVDJu@4*qDsXzZJ{o?lP-BfP-wZE$)k$CR}(GpK(DAKzMta`ZWjT0^YR0ga^ zTD*s=UQ*BiTmkFellx#}sBBz;NpJ0$-FF}@&YKPRy- SNoG&GEXChl`v-xK5b_^#>>b?z literal 0 HcmV?d00001 diff --git a/crews/infra_review_crew/config/agents.yaml b/crews/infra_review_crew/config/agents.yaml new file mode 100644 index 0000000..7559cbe --- /dev/null +++ b/crews/infra_review_crew/config/agents.yaml @@ -0,0 +1,7 @@ +# Infrastructure Review Crew Agents Configuration +infra_reviewer: + role: DevOps/Platform Engineer + goal: Review infrastructure as code for correctness, security, and best practices + backstory: You are an experienced DevOps engineer with expertise in infrastructure as code, Kubernetes, Docker, and cloud platforms. You help teams ensure their infrastructure is secure, scalable, and follows best practices. + verbose: true + allow_delegation: false \ No newline at end of file diff --git a/crews/infra_review_crew/config/tasks.yaml b/crews/infra_review_crew/config/tasks.yaml new file mode 100644 index 0000000..1cc930b --- /dev/null +++ b/crews/infra_review_crew/config/tasks.yaml @@ -0,0 +1,16 @@ +# Infrastructure Review Crew Tasks Configuration +infra_review_task: + description: | + Review the infrastructure as code (IaC) in the pull request for correctness, security, and best practices. + PR Title: {pr_title} + PR Description: {pr_description} + Files to review: {files} + Context: {context} + expected_output: | + A detailed infrastructure review report including: + - Summary of infrastructure changes + - Issues found (misconfigurations, security vulnerabilities, etc.) + - Best practices violations (if any) + - Suggestions for improvement + - Overall rating (e.g., Approved, Needs Changes) + agent: infra_reviewer \ No newline at end of file diff --git a/crews/infra_review_crew/infra_review_crew.py b/crews/infra_review_crew/infra_review_crew.py new file mode 100644 index 0000000..049be39 --- /dev/null +++ b/crews/infra_review_crew/infra_review_crew.py @@ -0,0 +1,65 @@ +from crewai import CrewBase, Agent, Task, Crew +from crewai_tools import MCPServerAdapter +from mcp import StdioServerParameters +import os +from typing import Dict, Any + + +class InfraReviewCrew(CrewBase): + """Infrastructure Review Crew for conducting infrastructure reviews.""" + + agents_config = "config/agents.yaml" + tasks_config = "config/tasks.yaml" + + def __init__(self): + super().__init__() + # Configure Hadolint MCP server connection + self.hadolint_server_params = StdioServerParameters( + command="python", + args=["/home/armistace/dev/pr_reviewer/mcp_servers/hadolint_mcp.py"], + env=os.environ + ) + # Configure Checkov MCP server connection + self.checkov_server_params = StdioServerParameters( + command="python", + args=["/home/armistace/dev/pr_reviewer/mcp_servers/checkov_mcp.py"], + env=os.environ + ) + + @Agent + def infra_reviewer(self) -> Agent: + """DevOps/Platform Engineer agent for infrastructure review.""" + return Agent( + config=self.agents_config["infra_reviewer"], + tools=[], # Tools will be added via MCP adapter in the task + verbose=True + ) + + @Task + def infra_review_task(self) -> Task: + """Task for conducting infrastructure review.""" + return Task( + config=self.tasks_config["infra_review_task"], + ) + + @Crew + def crew(self) -> Crew: + """Create the Infrastructure Review crew.""" + # Create MCP server adapters for Hadolint and Checkov + hadolint_adapter = MCPServerAdapter(self.hadolint_server_params) + checkov_adapter = MCPServerAdapter(self.checkov_server_params) + + # Combine tools from both adapters + all_tools = [] + if hasattr(hadolint_adapter, 'tools'): + all_tools.extend(hadolint_adapter.tools) + if hasattr(checkov_adapter, 'tools'): + all_tools.extend(checkov_adapter.tools) + + return Crew( + agents=[self.infra_reviewer()], + tasks=[self.infra_review_task()], + process="sequential", + verbose=True, + tools=all_tools, + ) \ No newline at end of file diff --git a/crews/security_review_crew/__init__.py b/crews/security_review_crew/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/crews/security_review_crew/__pycache__/__init__.cpython-314.pyc b/crews/security_review_crew/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ac739b0033978b38134831800e9c817544fa24d8 GIT binary patch literal 137 zcmdPqpnNK`*QJ~J<~BtBlRpz;=nO>TZlX-=wL5i3v|$lziS;}bI@ KBV!RWkOct5N*_!B literal 0 HcmV?d00001 diff --git a/crews/security_review_crew/__pycache__/security_review_crew.cpython-314.pyc b/crews/security_review_crew/__pycache__/security_review_crew.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..49775058c28132b72dd3adc2f4622ee806f9ee60 GIT binary patch literal 2690 zcmb_e-)|E~5Z?RY^VxBN6FVdVsvMAn7*a>2AZUqNQd-H83Q8}?s#L=DIA6l4&Yjs^ zgOVpdRBHR!KJ}GH{+mP;NVi2)>Jx9Sk~Zx_rOxc#ISC*kq^{(-*_pk){pOpQ^_hYG zJb`ii@xQ?llaSwWl8#KGY&SsZkekF-=1EQAN=@ZzP2*Zk=lU4h)%i@#;D*$-`E1SP zrquQM9M6G2GoP>Z@xEGt7skjiIZkY2gxJ|7<(w9OgZJC|Nm4PNb=Ui0ks$)3|fq8M_-{N}RAIgZzK8FL(ldrfx4Sr6c`nG}!{19H&% zN@KOPMi0TO;%FMDz1UULLkz$IHVWeJ4*AvW|1P^}j{amG>E_A!)VKO$)7l0fk-~pDDK?Sr(Nq$c%E5JW!wGdSOtXw`o{Ws18#=X#h{F zE*Jd?&ITlAg7rj)mhhc|fdyNf`C%b;uSY&Pb8rQPgB23k7~FVtz%H0Tlgl_B!#Z z2MQg8gD?a!DT7DHKo#TZLH05K($@~-o0A|qWXB-I;jPlhlhVXyX<}pYy+@_@w}#4F z<)Hzn0pFC>V7P;Gr2+w5z)NmZ0z2&aP`1mIb40Dy-mML~9zgep1? zru{@s;gVCYCra*&tTGEk%vD)(HNIII-@_Lp|;i-Bo6_6zm z-vdkVc$uXvESD_ZkU$QUUQ$xv=#8|qR-w}_U_f6OdwyGyWrKJ9bN?@+oF-@Ge%Fqjunk!KxA!EziD6-YV{8)YJ1`Z)(3qhM2cF|{xU+7xxexb}7~I#oJ-^umQ`*hrDP+zNZr7{t z%a`NkR6r{V_@71C=OF(127>al{^4)ssm|4{(q!k;KXoHB)h!aEZ&!szzO8}QMGbBF zT^0HO+VWl0x&>*2bd$DS6|`~mzhrI?~ZTlHEL zPbk!g34^b-;131O;mi0>QSSPH$o!5kEOR#I;R8AcerzTTBSw literal 0 HcmV?d00001 diff --git a/crews/security_review_crew/config/agents.yaml b/crews/security_review_crew/config/agents.yaml new file mode 100644 index 0000000..5bcbbb3 --- /dev/null +++ b/crews/security_review_crew/config/agents.yaml @@ -0,0 +1,7 @@ +# Security Review Crew Agents Configuration +security_reviewer: + role: Application Security Engineer + goal: Identify security vulnerabilities and ensure security best practices are followed + backstory: You are an experienced security engineer specialized in application security. You have extensive experience in penetration testing, code security analysis, and helping organizations build secure software. + verbose: true + allow_delegation: false \ No newline at end of file diff --git a/crews/security_review_crew/config/tasks.yaml b/crews/security_review_crew/config/tasks.yaml new file mode 100644 index 0000000..02170ce --- /dev/null +++ b/crews/security_review_crew/config/tasks.yaml @@ -0,0 +1,16 @@ +# Security Review Crew Tasks Configuration +security_review_task: + description: | + Review the code changes in the pull request for security vulnerabilities and compliance with security best practices. + PR Title: {pr_title} + PR Description: {pr_description} + Files to review: {files} + Context: {context} + expected_output: | + A detailed security review report including: + - Summary of security-related changes + - Identified vulnerabilities (if any) + - Security best practices violations (if any) + - Suggestions for improving security posture + - Overall security rating (e.g., Secure, Needs Improvement) + agent: security_reviewer \ No newline at end of file diff --git a/crews/security_review_crew/security_review_crew.py b/crews/security_review_crew/security_review_crew.py new file mode 100644 index 0000000..861f63f --- /dev/null +++ b/crews/security_review_crew/security_review_crew.py @@ -0,0 +1,51 @@ +from crewai import CrewBase, Agent, Task, Crew +from crewai_tools import MCPServerAdapter +from mcp import StdioServerParameters +import os +from typing import Dict, Any + + +class SecurityReviewCrew(CrewBase): + """Security Review Crew for conducting security reviews.""" + + agents_config = "config/agents.yaml" + tasks_config = "config/tasks.yaml" + + def __init__(self): + super().__init__() + # Trivy uses native MCP server, so we don't need to configure a wrapper. + # However, we might need to set up connection parameters if required by the native server. + # For now, we assume the native Trivy MCP server is available at a known address or via stdio. + # We'll leave the MCP server configuration empty and rely on the native server being available. + self.trivy_server_params = None # Placeholder for if we need to configure stdio parameters + + @Agent + def security_reviewer(self) -> Agent: + """Application Security Engineer agent for security review.""" + return Agent( + config=self.agents_config["security_reviewer"], + tools=[], # Tools will be added via MCP adapter in the task + verbose=True + ) + + @Task + def security_review_task(self) -> Task: + """Task for conducting security review.""" + return Task( + config=self.tasks_config["security_review_task"], + ) + + @Crew + def crew(self) -> Crew: + """Create the Security Review crew.""" + # If we had an MCP server to wrap, we would create an adapter here. + # Since Trivy is native, we don't add any tools via MCPServerAdapter. + # However, the native server should be available in the MCP ecosystem. + # We'll assume the tools are automatically available or will be handled differently. + return Crew( + agents=[self.security_reviewer()], + tasks=[self.security_review_task()], + process="sequential", + verbose=True, + # No additional tools from MCP wrapper for Trivy (native) + ) \ No newline at end of file diff --git a/crews/summariser_crew/__init__.py b/crews/summariser_crew/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/crews/summariser_crew/__pycache__/__init__.cpython-314.pyc b/crews/summariser_crew/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d3e183a447786087aba8dfd343c37a4ce9a4f201 GIT binary patch literal 132 zcmdPqIYI(7K zacORDVo_#sYEe8$KtDb{GcU6wK3=b&@)n0pZhlH>PO4oID^Lf>%wiDZ6EhNCNO?@@Q?qSLCCK->5fE4Y1cq$l2u~Uc~YT*Ry3hibfH%aVT_|)n@?0sVJcmp zPgX2pDczV)i4^z~^XW>T=&NKzW}FO@)5JDMh@D)a=k@pu(Qg|Qq+~t8X~~e*9P>VV z*X5o}Tw3=+A&n)Me<}?$m#EBMo?BS-*e#D;s<|7&V_+Lz6g5AJtqU%5>mJzohb2uK z@A*|B^-JMqH9jqtouf(nYnZ#lrA@Lz1ht9KRxP0)ClR%&FlDGg-!H!KpL}OEx9*7Hu1%6ihEGQQtDA)F#i*%WM0$I@oJ5Fo@7o`~F1aEA3Oj?fP zhrVzej63&&HJNgpYT$D2I9!9BCYy4q3U}wzd>@>HhNJWH#wI%kFUXM|Y6M;Go-hDT zj^eK-`Pu6KGP!My{%D!ry&(O_YTG6SZ7#)M$kT_O8BcxsST9ZYAMYzW`UIF1{Jk#L3QICAW%d-t3< zSkChx;O=|GOr(ED@$LKS!O(Gt6B7(@*GzjB6kw!_43I3~ZI29_zH+i>DnotR`UQ*t z4B*SydVA3eeaM};sOA;yIA`Xfh}Hbi6<}BC5@%OT(t+gSWtr|#w^Y)l87GuXh$srU zOhQ>+kGNOTROn=XoP18a>JDXDxCTcElPYZNMNs95Bhl&|fuZF^5KXdclI-wKZsb9( zxScD07u?Uy>MT|-*5|BV(b zglE&Dfb3V7g|vXUT~m*zX-UGSik52%$f4>2`_a+uer4!}3mC>DqvyA&syw6}hN`?V zXkpME3ed6(y~q}@0b*TwHyhBXeK%NwkDwVB4%8KPYMI<%QPty|orcXyI>T6~+F?l4 zQTC*Pd&*qz5Y;@YF^&SO*TLH!=Q<=mPO3IJq>zuTL-O$S}RM; z^qvNVj<$3QwN^&iAj_0(PXn!mNvE<+9aS2DMhVe(eTGRDLsn=JsLaGZNA*yKDJk`O zb%Wszr515%ip>r9#IqE>jDOnHuIyFRFxTTxkaaPp;vF^%Hf$IitD&V+N*|N*JyL!| aN{>kKCz5(hPVJf#H2u}pKM6e4CjSD;wM7H~ literal 0 HcmV?d00001 diff --git a/crews/summariser_crew/config/agents.yaml b/crews/summariser_crew/config/agents.yaml new file mode 100644 index 0000000..cfb8775 --- /dev/null +++ b/crews/summariser_crew/config/agents.yaml @@ -0,0 +1,7 @@ +# Summarizer Crew Agents Configuration +summariser: + role: Senior Code Review Coordinator + goal: Synthesize individual review results into a cohesive, actionable review report + backstory: You are a senior technical lead with extensive experience in code review practices across multiple domains. You excel at combining feedback from different reviewers into a clear, prioritized, and actionable summary that helps development teams improve their code efficiently. + verbose: true + allow_delegation: false \ No newline at end of file diff --git a/crews/summariser_crew/config/tasks.yaml b/crews/summariser_crew/config/tasks.yaml new file mode 100644 index 0000000..648fd2b --- /dev/null +++ b/crews/summariser_crew/config/tasks.yaml @@ -0,0 +1,16 @@ +# Summarizer Crew Tasks Configuration +summarise_task: + description: | + Synthesize the results from code, security, and infrastructure reviews into a cohesive review report. + Code Review Results: {code_review_results} + Security Review Results: {security_review_results} + Infrastructure Review Results: {infra_review_results} + Context: {context} + expected_output: | + A comprehensive review report that includes: + - Executive summary of all findings + - Prioritized list of issues (critical, high, medium, low) + - Specific recommendations for each domain (code, security, infrastructure) + - Overall assessment and recommendation (e.g., Approved, Approved with Minor Changes, Significant Changes Needed) + - Summary of positive aspects of the PR + agent: summariser \ No newline at end of file diff --git a/crews/summariser_crew/summariser_crew.py b/crews/summariser_crew/summariser_crew.py new file mode 100644 index 0000000..2ea33c5 --- /dev/null +++ b/crews/summariser_crew/summariser_crew.py @@ -0,0 +1,43 @@ +from crewai import CrewBase, Agent, Task, Crew +from crewai_tools import MCPServerAdapter +from mcp import StdioServerParameters +import os +from typing import Dict, Any + + +class SummariserCrew(CrewBase): + """Summariser Crew for synthesizing review results.""" + + agents_config = "config/agents.yaml" + tasks_config = "config/tasks.yaml" + + def __init__(self): + super().__init__() + # The summarizer doesn't need MCP server connections as it works with text results + + @Agent + def summariser(self) -> Agent: + """Senior Code Review Coordinator agent for summarizing reviews.""" + return Agent( + config=self.agents_config["summariser"], + tools=[], # No tools needed for summarization + verbose=True + ) + + @Task + def summarise_task(self) -> Task: + """Task for synthesizing review results.""" + return Task( + config=self.tasks_config["summarise_task"], + ) + + @Crew + def crew(self) -> Crew: + """Create the Summariser crew.""" + return Crew( + agents=[self.summariser()], + tasks=[self.summarise_task()], + process="sequential", + verbose=True, + # No additional tools needed + ) \ No newline at end of file diff --git a/example_pipelines/.gitea/workflows/build_push.yml b/example_pipelines/.gitea/workflows/build_push.yml new file mode 100644 index 0000000..4f8a462 --- /dev/null +++ b/example_pipelines/.gitea/workflows/build_push.yml @@ -0,0 +1,72 @@ +name: Build and Push Image +on: + push: + branches: + - master + +jobs: + build: + name: Build and push image + runs-on: ubuntu-latest + container: catthehacker/ubuntu:act-latest + if: gitea.ref == 'refs/heads/master' + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Create Kubeconfig + run: | + mkdir $HOME/.kube + echo "${{ secrets.KUBEC_CONFIG_BUILDX_NEW }}" > $HOME/.kube/config + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver: kubernetes + driver-opts: | + namespace=gitea-runner + qemu.install=true + + - name: Login to Docker Registry + uses: docker/login-action@v3 + with: + registry: git.aridgwayweb.com + username: armistace + password: ${{ secrets.REG_PASSWORD }} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 + tags: | + git.aridgwayweb.com/armistace/blog:latest + + - name: Trivy Scan + run: | + echo "Installing Trivy " + sudo apt-get update + sudo apt-get install -y wget apt-transport-https gnupg lsb-release + wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add - + echo deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main | sudo tee -a /etc/apt/sources.list.d/trivy.list + sudo apt-get update + sudo apt-get install -y trivy + trivy image --format table --exit-code 1 --ignore-unfixed --vuln-type os,library --severity HIGH,CRITICAL git.aridgwayweb.com/armistace/blog:latest + + - name: Deploy + run: | + echo "Installing Kubectl" + apt-get update + apt-get install -y apt-transport-https ca-certificates curl gnupg + curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.33/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg + chmod 644 /etc/apt/keyrings/kubernetes-apt-keyring.gpg + echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.33/deb/ /' | tee /etc/apt/sources.list.d/kubernetes.list + chmod 644 /etc/apt/sources.list.d/kubernetes.list + apt-get update + apt-get install kubectl + kubectl delete namespace blog + kubectl create namespace blog + kubectl create secret docker-registry regcred --docker-server=${{ vars.DOCKER_SERVER }} --docker-username=${{ vars.DOCKER_USERNAME }} --docker-password='${{ secrets.DOCKER_PASSWORD }}' --docker-email=${{ vars.DOCKER_EMAIL }} --namespace=blog + kubectl apply -f kube/blog_pod.yaml && kubectl apply -f kube/blog_deployment.yaml && kubectl apply -f kube/blog_service.yaml diff --git a/example_pipelines/kube/blog_deployment.yaml b/example_pipelines/kube/blog_deployment.yaml new file mode 100644 index 0000000..8acae4e --- /dev/null +++ b/example_pipelines/kube/blog_deployment.yaml @@ -0,0 +1,24 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: blog-deployment + labels: + app: blog + namespace: blog +spec: + replicas: 3 + selector: + matchLabels: + app: blog + template: + metadata: + labels: + app: blog + spec: + containers: + - name: blog + image: git.aridgwayweb.com/armistace/blog:latest + ports: + - containerPort: 8000 + imagePullSecrets: + - name: regcred diff --git a/example_pipelines/kube/blog_pod.yaml b/example_pipelines/kube/blog_pod.yaml new file mode 100644 index 0000000..5ee6366 --- /dev/null +++ b/example_pipelines/kube/blog_pod.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Pod +metadata: + name: blog + namespace: blog +spec: + containers: + - name: blog + image: git.aridgwayweb.com/armistace/blog:latest + ports: + - containerPort: 8000 + imagePullSecrets: + - name: regcred diff --git a/example_pipelines/kube/blog_service.yaml b/example_pipelines/kube/blog_service.yaml new file mode 100644 index 0000000..3af5257 --- /dev/null +++ b/example_pipelines/kube/blog_service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: blog-service + namespace: blog +spec: + type: NodePort + selector: + app: blog + ports: + - port: 80 + targetPort: 8000 + nodePort: 30009 diff --git a/kube/pr-reviewer_deployment.yaml b/kube/pr-reviewer_deployment.yaml new file mode 100644 index 0000000..3c0bcba --- /dev/null +++ b/kube/pr-reviewer_deployment.yaml @@ -0,0 +1,24 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: pr-reviewer-deployment + labels: + app: pr-reviewer + namespace: pr-reviewer +spec: + replicas: 3 + selector: + matchLabels: + app: pr-reviewer + template: + metadata: + labels: + app: pr-reviewer + spec: + containers: + - name: pr-reviewer + image: git.aridgwayweb.com/armistace/pr-reviewer:latest + ports: + - containerPort: 8000 + imagePullSecrets: + - name: regcred diff --git a/kube/pr-reviewer_pod.yaml b/kube/pr-reviewer_pod.yaml new file mode 100644 index 0000000..774610c --- /dev/null +++ b/kube/pr-reviewer_pod.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Pod +metadata: + name: pr-reviewer + namespace: pr-reviewer +spec: + containers: + - name: pr-reviewer + image: git.aridgwayweb.com/armistace/pr-reviewer:latest + ports: + - containerPort: 8000 + imagePullSecrets: + - name: regcred diff --git a/kube/pr-reviewer_service.yaml b/kube/pr-reviewer_service.yaml new file mode 100644 index 0000000..9acd7a0 --- /dev/null +++ b/kube/pr-reviewer_service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: pr-reviewer-service + namespace: pr-reviewer +spec: + type: NodePort + selector: + app: pr-reviewer + ports: + - port: 80 + targetPort: 8000 + nodePort: 30009 diff --git a/mcp_servers/__init__.py b/mcp_servers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mcp_servers/__pycache__/__init__.cpython-314.pyc b/mcp_servers/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..07fa215cf55f73dbbd4b729c6fa37d4c43d68575 GIT binary patch literal 122 zcmdPqCuT-Q#v*1Q3jm|L86^M! literal 0 HcmV?d00001 diff --git a/mcp_servers/__pycache__/checkov_mcp.cpython-314.pyc b/mcp_servers/__pycache__/checkov_mcp.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e721c0ee75416a9f3919f49d0cca400220524cba GIT binary patch literal 6092 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 literal 0 HcmV?d00001 diff --git a/mcp_servers/__pycache__/hadolint_mcp.cpython-314.pyc b/mcp_servers/__pycache__/hadolint_mcp.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0334ee142311e03c709488f0cbe5065806143063 GIT binary patch literal 5355 zcmb6dTWlN0aqq$7@p$4xPf8ZWv;2^0%hbbm{DciVku5d0!d@JrA5vJFJj>@$Bzt%C zgGgP$DNs8I>KZ{22S^*I4I1|^Km)XBzp5n7FJ;*!dsYww2~eb8RHPz(q(EnmJd#pU zr&;NCXJ=<;cV}jIX1K%CA`mElx%VIS=N>{n#*SUs8nE(XijX-X6N5ZUWNLykD66Fn z+G-htv0Bz(t=3^Uprt3cai`%N=M8?`Ww=7sxq{)gXvFarqs7886P|Ie;T`uGKC918 z_{Up~))1LyqFe2)G}?`b-`42cknGqCyToG6++Ky7Yna)&yGCPlP5U+ynXC5$(}C!& zX6h=uyM~gTT}0-)NZMy(j2@V=_YpH(Fhf`~qgQr!5hFO=DU18QLrAKJkX|xPMGt+M z3+y$xk=Se5y9|N!H(6h}r7$R*dORxWigrQKq_bH~8c(FMX*FX=Ph^wNDcV^ztw`8c zw1_YnW=!U2=Ca8?p(YKJJ*nzOm^Rxcvxa(BO(qOAn|UgSwQh1xTRg***)fq(4KY{JlS zL;dCk*k8OVDt*Y_bR3Y8++|aY#}k=M)<_siJg)g+0kTy9XV1blO%Z$v*4w-}2Df6;x zh#sP3LFOXV5F@+yq8On>;OKi8l?sc6E&)`&Wqg7qqww+pI*E}FmAuE4_>g()UrW=;sFoY>&Y?WPfvu(@Fpwk{9L}sTr zyElcaS|w^d)^@MjHjSbIt}D$vnq;|a^S*`{xN{E84dfc$3DcF(X7aOO4*H~q zD~5TK0lUypE*e_7bs;THn(oueCFAj$^@tO*i5bQ2`Nn3?D5;c^k`+Ch*OE$@(!4Oc zdSxxWTbI$QMsPre3UA~gN?68fNoc7s*!J|jroZ|4txG?P+}VA_A>`wE=EK$O4~2=IVwgCz;rA#Ae=R{Ofyv!#kH=;tWA-lWAy*z`iYx9pe9yL1HG%?4W@;#bY zj*D^8j;1L!&|a&qKaff`oP4;M`m3`l6 zP5;yud+uqcJrnQK`n|}mob!kiVjrtjDU}>#f<&b(kJwsh7sf3IVR=F+nMk8M)Y^e} z!ShV!xlHzA#u_>x75t}E-Ex8TS4=})6gJuTFS|X-CN+6Gb}*@(;Km%fky1106;s)~ zu1HEo&ufZg?*n*BYDPEU#7auSDQZTF9z8uCDI6Trw5)cZ?&=@Gw8o*% zHB#ubCQDjAlfiJoUak=MYWx8y+-3@~L^`in1ST`17^X9!U(O`etl62=6pXI){22%^ zl8Ua!l}k#}6dDMVjZQ?zOgEl9pTUR}q7o&O%%&7ms787g%%v2o1<^zNQ4JT+4o&}Y zh<72JJ&w|?Zh}i^XJ!=5bSNkZ(=~P}saPS4=~8S^>xao<-tuhPMpOk_gXyg8Q`?TH z9f*Q>Q_rUjlTvha5L>rOR;dOkzFL5Sre_5x+EJhxhEM+~I17wVDjvd%MXz+vDJ^*d zw+`Rge5bS6J-p}{De@zKb+-eu*gd%D*;(XwKJ4y)J@{&{IB;;W`%sbX{Ky@&h7T`# zjuiPL6?V`ed=dzi0*~DdJXRJv%c6IwwfB{wn?p-&!B>vlJW^{`-CVcuIpNxbYtD*~ zNCTzN=t5|;xc|x0etBWPytw~#F*LdudZyI<%yrK-?xDN2enHH?F*RrQ9)c?f5E3T3SnD z@U9pvcWi+FRR`1Jfso55REWbREboBbEc-}LjQYpwRw4#hiPIr0@tzyvb+OoyyvL_L z5&caJvUUe?ny5KSTrm{lh%Ke{xb-^ZQRYnGYz0KcF2j`hnAdu*d+MgdNliy2TeT z>?nZ6{6Fc#S7l34XPsXiupQDtD$eBTJQJsF%n8WDnw_@gWT{5f8gIqq|1l5qAf^V^s@v5uInj(6fY%P^F8F-&HRJiGb_jb)<8ir-CNjJQ_sW?2AN@CU& zFKQ}2!&XnnFhi26?-x6pP%~N-Fgf`2m!Pt8DuMXgZWOK;O5V_dH&pTtE_eq^-U$5P z^F|g%PrZF`$&-IWk;vkiI=D zgz0Ntbvj!-D^9l;UM4r>Tv&b15Z@4RUGzql>85@a^@TnS8@jsZ*g@*I)Ug2nI|iUX z9OaJnIp!(qSPwr>vj}r;z|Z@sW4-Krs}tcK3h})Z!hPJaeXjY<)Ug52{1yS>VH)dE z7mS*GJdQGm$HO7h8OJ0Vehk0|++@$|pdBYV6Yzx^#9jmvp3Cq<3FOX*K|xltA585` zAO5aTRa?YTmo|=Q7ox2nDXOWF&>*vvrD6DP`Wx8DEI7ZkURALBOhQ+akUO1KXH0ft z^7vDx3v_<6>fvDXXi~7_1Gtw7#!e{{p|#b{_x$ literal 0 HcmV?d00001 diff --git a/mcp_servers/checkov_mcp.py b/mcp_servers/checkov_mcp.py new file mode 100644 index 0000000..66d2032 --- /dev/null +++ b/mcp_servers/checkov_mcp.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +""" +MCP server for Checkov Kubernetes security scanner. +""" +import asyncio +import json +import logging +import subprocess +import sys +import tempfile +import os +from typing import Any, Dict, List + +import mcp.server.stdio +import mcp.types as types +from mcp.server import NotificationOptions, Server +from mcp.server.models import InitializationOptions + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Create server instance +server = Server("checkov-mcp") + + +@server.list_tools() +async def handle_list_tools() -> List[types.Tool]: + """ + List available tools. + """ + return [ + types.Tool( + name="scan_kubernetes_manifests", + description="Scan Kubernetes manifests for security issues using Checkov", + inputSchema={ + "type": "object", + "properties": { + "manifest_content": { + "type": "string", + "description": "The content of the Kubernetes manifest(s) to scan" + } + }, + "required": ["manifest_content"] + } + ) + ] + + +@server.call_tool() +async def handle_call_tool( + name: str, arguments: Dict[str, Any] | None +) -> List[types.TextContent | types.ImageContent | types.EmbeddedResource]: + """ + Handle tool calls. + """ + if name != "scan_kubernetes_manifests": + raise ValueError(f"Unknown tool: {name}") + + if not arguments: + raise ValueError("Missing arguments") + + manifest_content = arguments.get("manifest_content") + if not manifest_content: + raise ValueError("Missing manifest_content argument") + + try: + # Create a temporary file to hold the manifest content + with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as temp_file: + temp_file.write(manifest_content) + temp_file_path = temp_file.name + + try: + # Run checkov on the manifest file + process = await asyncio.create_subprocess_exec( + "checkov", + "-f", temp_file_path, + "--quiet", # Reduce verbosity + "--output", "json", # Get JSON output for easier parsing + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + stdout, stderr = await process.communicate() + + if process.returncode not in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]: # Checkov returns various codes + # Some non-zero codes are expected (findings, etc.) + pass + + result = stdout.decode() + if stderr: + result += "\nSTDERR:\n" + stderr.decode() + + # If checkov is not found, we'll get an error from the subprocess + if not result.strip() and process.returncode == 127: # command not found typically returns 127 + result = "Error: Checkov command not found. Please install checkov." + + return [ + types.TextContent( + type="text", + text=result + ) + ] + finally: + # Clean up the temporary file + os.unlink(temp_file_path) + except FileNotFoundError: + logger.error("Checkov command not found. Please ensure checkov is installed and in PATH.") + return [ + types.TextContent( + type="text", + text="Error: Checkov command not found. Please install checkov." + ) + ] + except Exception as e: + logger.exception("Error running checkov") + return [ + types.TextContent( + type="text", + text=f"Error running checkov: {str(e)}" + ) + ] + + +async def main(): + """ + Run the MCP server. + """ + # Run the server using stdio + async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): + await server.run( + read_stream, + write_stream, + InitializationOptions( + server_name="checkov-mcp", + server_version="0.1.0", + capabilities=server.get_capabilities( + notification_options=NotificationOptions(), + experimental_capabilities={}, + ), + ), + ) + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/mcp_servers/hadolint_mcp.py b/mcp_servers/hadolint_mcp.py new file mode 100644 index 0000000..1ef9bca --- /dev/null +++ b/mcp_servers/hadolint_mcp.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +""" +MCP server for Hadolint Dockerfile linter. +""" +import asyncio +import json +import logging +import subprocess +import sys +from typing import Any, Dict, List + +import mcp.server.stdio +import mcp.types as types +from mcp.server import NotificationOptions, Server +from mcp.server.models import InitializationOptions + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Create server instance +server = Server("hadolint-mcp") + + +@server.list_tools() +async def handle_list_tools() -> List[types.Tool]: + """ + List available tools. + """ + return [ + types.Tool( + name="lint_dockerfile", + description="Lint a Dockerfile using Hadolint", + inputSchema={ + "type": "object", + "properties": { + "dockerfile_content": { + "type": "string", + "description": "The content of the Dockerfile to lint" + } + }, + "required": ["dockerfile_content"] + } + ) + ] + + +@server.call_tool() +async def handle_call_tool( + name: str, arguments: Dict[str, Any] | None +) -> List[types.TextContent | types.ImageContent | types.EmbeddedResource]: + """ + Handle tool calls. + """ + if name != "lint_dockerfile": + raise ValueError(f"Unknown tool: {name}") + + if not arguments: + raise ValueError("Missing arguments") + + dockerfile_content = arguments.get("dockerfile_content") + if not dockerfile_content: + raise ValueError("Missing dockerfile_content argument") + + try: + # Run hadolint on the Dockerfile content + process = await asyncio.create_subprocess_exec( + "hadolint", + "-", # Read from stdin + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + stdout, stderr = await process.communicate(input=dockerfile_content.encode()) + + if process.returncode != 0: + # Hadolint returns non-zero for linting errors, which is expected + # We still return the output as the result + result = stdout.decode() + stderr.decode() + else: + result = stdout.decode() + + # If no output, hadolint passed with no issues + if not result.strip(): + result = "Hadolint: No issues found." + + return [ + types.TextContent( + type="text", + text=result + ) + ] + except FileNotFoundError: + logger.error("Hadolint command not found. Please ensure hadolint is installed and in PATH.") + return [ + types.TextContent( + type="text", + text="Error: Hadolint command not found. Please install hadolint." + ) + ] + except Exception as e: + logger.exception("Error running hadolint") + return [ + types.TextContent( + type="text", + text=f"Error running hadolint: {str(e)}" + ) + ] + + +async def main(): + """ + Run the MCP server. + """ + # Run the server using stdio + async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): + await server.run( + read_stream, + write_stream, + InitializationOptions( + server_name="hadolint-mcp", + server_version="0.1.0", + capabilities=server.get_capabilities( + notification_options=NotificationOptions(), + experimental_capabilities={}, + ), + ), + ) + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e77fd6f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,71 @@ +[project] +name = "pr-reviewer" +version = "0.1.0" +description = "A PR Reviewer system using CrewAI and MCP" +readme = "README.md" +requires-python = ">=3.10,<3.14" +authors = [ + {name = "Developer", email = "dev@example.com"} +] +keywords = ["pull-request", "code-review", "security", "infrastructure", "crewai", "mcp"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +dependencies = [ + "crewai>=0.28.0", + "fastapi>=0.104.0", + "uvicorn>=0.24.0", + "mcp>=0.1.0", + "pydantic>=2.5.0", + "python-dotenv>=1.0.0", + "gitpython>=3.1.0" +] + +[project.optional-dependencies] +anthropic = ["anthropic>=0.7.0"] +openai = ["openai>=1.0.0"] +ollama = [] +dev = [ + "pytest>=7.0.0", + "black>=22.0.0", + "flake8>=4.0.0", + "mypy>=0.9.0", +] + +[project.urls] +Homepage = "https://github.com/your-org/pr-reviewer" +Documentation = "https://github.com/your-org/pr-reviewer#readme" +Repository = "https://github.com/your-org/pr-reviewer.git" + +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[tool.black] +line-length = 88 +target-version = ['py310'] +include = '\.py$' +exclude = ''' +/(\.git +| \.hg +| \.mypy_cache +| \.tox +| \.venv +| _build +| buck-out +| build +| dist +)/ +''' + +[tool.mypy] +python_version = "3.10" +warn_return_any = true +warn_unused_configs = true diff --git a/src/pr_reviewer/__init__.py b/src/pr_reviewer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pr_reviewer/__pycache__/__init__.cpython-314.pyc b/src/pr_reviewer/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..72c67ffe7399b11546517d5fd2a7945b1ba8c619 GIT binary patch literal 126 zcmdPqqAk$!x9W?p7Ve7s&kC-;=z+)dx)WpX&YTB4S^ikUfeei)5BIH_A5`F30HkgoTOgwj(p`;qUyL0B;bI)-LTz~~~K^DZh zqy?rzxiAaoA}rDkTCfAOPzRKgtVxr4AQOHnKxZ-%59D>Wl#zMhidkYjGHWx_s_12u zB=VX@R!p+`Hq#m5$qB2%$cJopg-~i5#3@M#As#U>Xi)Ik(~#7l0Q2(sj{ppmor0^< zlB7vL04x^Y) zXu+b(I&(x|DSpDCr6HRZshbF)L#2Zx2JJN-FBbJm#S(zUB5gtaFg{KSNe%WPEww%q zz11G=u^CTX&!XaOy#g@zPc8UkA` zNO`|ruE_xz_V^@Begd$HdD3AmYzGUFKdjPo^?r{vj*H691^tzO*Dfepcv$QU4nz!+ z0bx}L=0&P63_lJna()$euI>Ld-|0ys#`OCDCthfO7VX-Pde zU@BX)iF!kKh@n~))$&_K)hmW-kOjS3X6k}jCeC0qdWkMN7osW-_fTI@uOV<=^Pya! z>qs{%W@S;eEk-KL)XRt75Aj_T3{W(Mhj~dgu%SgS(=~O~WJ~G?WX)0gOO`>rcIeNl z{SGNrsma!kh|J0Y)&05ZLW}S*ZIfbfZV`;b7_jRwILwE~jzHh^*PS6RZ3kQL(DUH3 z!CPK%eY4jq>JgI{RD@alXm~eI|I_B^IvUJy;1bP*dC+8J+2O}Li{fGNczBWECBUe- zU8rgEfMt^k4^W~T{MaP!){Mm_t!l-1i&6`rsJN?nix0G{+cuVfNBr9GxW6Qh1CM(Z zDB5__@@C&}aYZjz36~Ze(Tizu4pfykyU-p|(Ziv=C>2`HE|MBFPC!%pM#sobq;uos zrJYFo#_`ccIM5NQ%{JnYe5sb(O~0|79<8TGe@b7hM^fwY-OjG{=ABq_JC?%#MlAKX ztM~Kk+kIp8zOe`8@2_ljU0t8qjdlJQ>$^R;JuqG$7~dRt^P9_C1CzUb=eGMs>U|@d zeWUBS&3LL23MR6T+d4na-d48TvRiH0d(zI?!MlY!g)iQ{JAY?>^X$c2xq9osz5?xM z>uuR*ozOqt=!JM|ZFWC_GPK%g%l`S>%e}vYoXGBG&fJP@CDV-{PX2IS!z8~QX`M*Q z4-=;^kA)u&Mg%_=n@GeSg#*YxN+^ORTPAvfk5UrSOq9!(!^5J+W%C9VcODO6{c&g9 z?*kP#rMq(n-waVsx2|`9CI#e} KKZKtI*Y0mCx#r{m literal 0 HcmV?d00001 diff --git a/src/pr_reviewer/__pycache__/flow.cpython-314.pyc b/src/pr_reviewer/__pycache__/flow.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..66212c3651f64ad8313ec87e805481972c7ffa0c GIT binary patch literal 9040 zcmeHNUu;v?89)Ai?buEn5<4U&cK&0BI3)xqp@q_9)J_L8CUqy8aA)irxZv3Cxi`?d z+Ei&$7oG~zBtV)(D4j@EO-iL{Wlw{LY1Om`o150@U86E>d)QkTU9~+-+IQ|hJC04d zJyd((C>)=AzVCeJJLlZ<`~F-8oempl#0-~BUu5WHHp4S-@W-K#N&y?s&JkAUcs9kzqZMh* zCz<3tX8AWa^fpE^vpBJ#%XxBsZZ3gYj=^MTvb-9EEXu><&!CZjW0*m_E{1sh=_cOb zMp-mt>_VdoQ%vX4+PrDT+=XULqdJ)w(^t?eRWw5djjEy^ETvLs71k{!ShtX1+fsrActlDa9U4MNeIIiAxPCyHF!eG* zUNr5|=GUJ_mB2F%@F^E{v^FwCMglb2Bl@r-XMi_R--KQ<&g(;HUVjwyd3_w~}3Dlgi&O7N;XFJyPs=wx2t8vs>r$fCZIaA!E>AJUSm8K`G%crc% z3F~Ui0sO}Sain#a$U5q1-c+G^w5H}7$8@*K8|_tF<=-;y)tPCV$~zrUTeUgWov2OC zv_5%s51f-vIx7|hn)1Vu49l~LG@EDq{5i%Sjrp-;RQzCcc*cK;<ohR0S($UY}<;-anhgq_;;jbKVakdw#k|qkiwwpr z$Om%MxNP&jr}lk-0&b!bo8d(R*htBs@(d&dmH>t>F!Q@`x?W7`R8h1b?+E|Xw zz;7teN&}25zvuTwIBbl5dIM?6rCq?vdI5`X(D<2_)hiAb)g>r9- zHY}eSox|exvh644D^w;m6L}wi$$A@oWkyzKp=rDb*7 zq3e(;r1PQMd+q$y^NXIPZoxgY(C}r0`)6Z|7nZwzp7~{})N$}Z$HBka2R?r(CiqU? zf9q~wkt+K4E*)6$@4f$4(RXrT;@!#1lb_nX4}C$>TCsa!GkOAdg13T&p%;YiBL&L$ zWkdJ-`NBx#52K}l=!1dintk~5!Gp!Yc;V11P^&U_J{){ema+3M{+)O1@7ZrVRXumW z_7Ch^PJMj-qw_-l5w*MLVdJ)U^Oy4lYWu(b-h=#8I*!=M-wrfI8jMNJ)qpO5WZ4aA z0%u{P8=}I84^3;Xg6fjxbTNHxScz%|86+zuW~jrf8BUcG+@Ct8A*va;GB21Ly<%2vyNhf0gW{AtQxFKfUubf%VI887|DsiKFLbWx_XuDD2T-wwzVAg87 zg&OSZ0rng6+hg+?3Gb5-2dIdj1b04^_dqy-c{X#_UqK6nLKR3UAzvS8D_Rt25*rA! zf;+ecNFV5M5PtG*f;QR=*|}Vr;h9t@;KW@(#^goCJ)|lFF%d6X&Ly}6&tpvH!8=Js z?5k)bT8fqnZ1O^Oc2+cSJQlZAOb$A5^J$(7m@u)UqE(_vgNy`HC9OoW!bKca2|F>k zJqW_4`3zvRwi2bt0?S#wQ+9R~|E?5B^9G>gOo3TGiYF4Ew7mc7($w<4 zQs3BvzOgH*YwT6_rx)%SAGjxgT5LVDYHz*5U&~+3mpmgy&q&F$ujtvg{KI=^1W#n$ zYHGGEnAh#7x#L>r)lR|HRdNN2uE4Up;0g$?XBW(01a>XOKkQI(rT+`cQE>LJPy>|! z(}>(j-D7g^?{^GM7@SW52DYNAs2BVxz*GUu4i#WHC9R-AfCMrf8pu@9>i`WNfsGd% zRpv^d!Q>3`?P~f$n`aZ$5ZtT_K(_?ikgK?!bxE7ag7xWR{E zc~5}`HleCK1sZijs(_|P1saxQD#06=PxAA)>~TnN)1|>pO|B<`j$InE8FBm&f!3OE z6Yh}I#{Y^tEtQj3gN{umegnj5B8XEdO7zu$n@R>6l;%oMX;`y+g&F~@arX(W{R_5l z6Kj@#bPo%j$qK0PUh`e`39jyvD_C>|m-`E@px`xjB45p>aU-mSNFo?u6ayVnv7{#`#`>s(ijNpC&m3Gks zD^w6JdPhKL4=yx5w0A9z-Wj_!R_ZxY>^ZV(?_1>W154)HFROR9>x=H-on7zkx*bw` ze9|r6xk3fXxA-wAywOh}xhu$L*dKrY2^kM5y8-m-QY*15WjAW^*2;vC>_#nK0xDHS zx~jE!wTn_7AZ00P@z%K3F-sjCwA6ZqT&fOljqHb(YzGe+@+`I2Sz}zStciNXkLhC8 zn5~Yima?8|>l^7%W89WHG}O^SgDO=6godr~@z0)bKbpmJ30_K`%(9t88UhY3m(6fY zDDRbNQu3sV24GT7Hy;D3)!0IocpM*xS~Ni}JBtH$IYS~PN46d-T3Id&33$*=l7go| zR6r*o21n9N{Yr{M<{`d>r1hp0M-}}Y7|MMHm4*+yR;ZzM3OQP?OkF#5^;D^~uh`nR zx<%~qUx7&5UH&)uU*}7Qr{J$}ch!9;{od&B z#_o@m#!tatVf@tf36fDRdG{2(drIEti{9r~yoU=vNI{~dQrq=ywkF#;vRQ3721~I5iLErv!G{$#33$$9_RKuba3Iu0)5&y#<4C?g$)wXX`OxpE z=!9Y95MPiE5fl3?Rpj1rx1svyPvC(5WU~GE)kpdsQ~P5BGCS5u>(QhRne7Xcg30qp z?~*BP>!kIlzf5S-2+(@e`y|5Ui7UQB_uj((mjruQFhw+}le!AA5y9ksq;E9Yh$fz| zNlV^pqI#eW@`d1-C)g1AK!E|bmCcj(l5Cl1mKm^x(4;#+uv9d_|8Kxn(kB^Xf|;T| z%gJ9~2FMsLlq@IkB>T9jL&`de!!mbOkS&j>)rpA*A_nA98+>Kb724E;3Gj-2$}HM{ znouU#_>3igK{{4HjFPu1M8;Fk$?xGId=?(bJHU|`dEKDX>Apm}1+;q&dDf7B4YjSI t-ajJi8rty>Gt!Og*3j`a^qn>I!dK>_dY$dc)b*iHTX%kq$b&S&e*izm%L@Pi literal 0 HcmV?d00001 diff --git a/src/pr_reviewer/__pycache__/llm.cpython-314.pyc b/src/pr_reviewer/__pycache__/llm.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..82ee3b7de32e170c1da6ba1c62777ee066233df3 GIT binary patch literal 2195 zcmbsqT~8ZFaQA#ZpU+_2l9-ebSkpA%HpNxNLqn^mByoaMQs^adT8ZRzu@^XTzH|4^ zQ1awTm0F=zAeD#w1^x|$JVYlV9-5cDka^kM1EIm#~jvl}ge?%#RIlTC5~b#QpdH z(&Zkc$9s^S@f96=iMzmEz(7}ko)C0Z$M2(j@;Sfc>kS;lCMR#?aiC7seam%ByMlqi zKMvfK7<~I@0X0z>-IG51385;``p`Fc9_x4pI3W~3@rLxAp9vI3{RVRwKMdNHS(nvJ z-{X1*gRI9Yg*r1BU9{*DWd+-=jntQe4#O}V$MsF08V2hCSp`0?8&DG+pd3E)f}{1z zV%Y~Ws&Etm1d>}T?_Y#_n&_ov(@{txNmz3b%F&jeb?|Jc#hZhB`_K&6B|w)Vsrf^` zzCmLwlC8g8R&L8mZ?!bumW}C385M9{F5}4JD%jeG?wrsQ=fL8!^a}l~t_phW75b>2 z5|$ApNXldo9PKir@EXK)2&4(I9M3nM3MKbVk5-B65b7*i%ynwi z@yVjeEc3oiy%9|-{!phCA7oxDFs6OX^%*y&t`sN7$gO!wYHpR<#4&4>_%3O9lngWa zeZyk3nr}&88oyaIZtJZB+ynxD1X7Fmpn)4WK3yWiZYz_cb3in{Dt;H zWs4Sgb?oG{K2pXzjw5dS*A#6;Yq1(p#y%wO&A*av$pibN*lV zW4xAHJ&*AQA2vJ@0@2R|8sUtnscE?91p>6*Etb#-5NJcI1PaM=XE}2=^`V5LqK2wHo}|1q51oo+wY(IhjF8cO zQ`Q3(2nWJ9PeAqvCIPx|nhU^#XOPZuS&l zxV`M07eKTMxdrx128t$}&1-Cs>pTT0 zP@q1ROiNr4DE@LCo+rGLSBo%^5x(P7F^|Pbkz8Mfu~xr;vToS)BKrUYgc;n$fA(S+ j<6V?&CZD40Q*`=o1qSHeb&UE4o2hM-J5V6^ggO5Ke67u? literal 0 HcmV?d00001 diff --git a/src/pr_reviewer/__pycache__/main.cpython-314.pyc b/src/pr_reviewer/__pycache__/main.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..00db7c2f8e7d7668bfb991264223e5a4a1aed072 GIT binary patch literal 11201 zcmd5iTW}lKb$79Nzlk>pkXVW@K_UrKq%E1G9yCRXG%1N$K$aX)h9O`nA|e4~u~bZC zGEp9r(0y4<(o%Aq5k2FH;Tdm%STwDlqQw)27MNr^^nPB+LDX`36-b~l>0%2Ve$0OwRK zv{a@Y6&xqvxVGTz2AuE?IPMCL2XMSwaQ4)}X{_M*0LQ-tXKx*xik#_YzzJ-@>8gW6 zRmN!noLyURy3aS!tq0B`l<*+rN9Q%uGY`)WG#8-T=ytk8)@J}#TC4aAc7B8rP6kOq zb1atSMy4kO(|9B@J@#Up$#BVZO3={E7Z;f<_a-42p9!BjNi*3@I+bODNlZ^p}7O(=;vB!(5DG1nsfpJTsA+O$*l1bc$nMnpvVg9dXD&1I zImeQKNWa7lwsnZdGe6qJ!uuhNB(NTJ= zrn$ejH)jsTGRe^8zR(;Ko9E`@c$bxT8d5?T@2~sEAiIK|Mb9e^d=oSYZGQAQVveAR zsxoKFI}D*}sbA662&m;00*wY0A7MB_%`)5~n+hseEv}MT4hvfi5&;QhF$226ONWxtbG-a@yA;Wgd z^Li7p9l!wFf-^i5!6?B;#Sz&~XljOE7PI*gFj>+glk;kF&6_{L8wPph;AdGpm9J}b zL{*;>^)33ur-rCc8VZloC#|UJaR!qY(~Nxbpl!MZc?C%pt3c3{S1#*^T2RTV;32A+ zZNeoTZY5Z8Mxvs{Ig4r(Gd$hw9>}mR&yΠ$!LY>Y0*Mc+2rt{73!yxxZ{K_lTks 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 literal 0 HcmV?d00001 diff --git a/src/pr_reviewer/__pycache__/state.cpython-314.pyc b/src/pr_reviewer/__pycache__/state.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c2ff1bc7485ad4f00164ca8d32e4414f956ae655 GIT binary patch literal 3400 zcmai0+iw(A7(cT+J3D*pZoA#xLR&h$0ZT7P3WAErMX*IEx+EB5%4C?G+DW!E>zT7? zi7{I>(H9bU;GqvB^2nQihL7DK$$Ana#wXrbO4JtR5f;e2z=Zh-i;SQgS%}VyOq`Fg*u2D~`8bQa z`sji@FEe?B6v!y4h$T{qEeS`X-gGSCY9-LdL)xUPl|h>bX;YQtI4P%|;?Q!0C(de) zaoMgKH6FWQ8ntR!qw>WoLvSZig0%T;_XbWf`oBX(?>QP~NM|F6sg-ixrCq6>nziOCTqOCb>qg zM^1cANENh0mj9G=jsRTB>fEW83-EwVA_tgn=#JnAq?B^)0$Ed~!J zdAGqcs;bvC$Em{GsH%fYj5YayL-oUTs#4>oX?$wX!?=nuTVLa8Rn;uZMp0FHA-Lb! zuo~BKJ^=FPH{?t5YcjLG(1{U2=t!iecvru@)DlNKaa718lUskclR!R6a`~26?4(dg z`$7hV9+K|Ad+zp|Epg;|78F}BHF-$NX)dd(rLE#llB&9DR~t3tVUM3Qw3=_ps_L>y z9ab|f!?M8~SJkSmt18XHG_)5Igdya4wC=QVXzV~mhDV$4JSh~L3vHJSjx^8zfv5Y5 z(7|Ao4$OAJx52V?63C9=$n6Xc2nq>mJQNr*1V#b^Bk2c5?i~8hXY9&NgHp3McTBMspd=Ni0l5eAI1S+gvNU;&*Qz<+Jlo0cVEp8-_Ai7=T#t{ zZ};xm7{52%5+?%(FYepW?}3At0|(C!Zp_`AX^H!vqnCS6I|Sp)DLM=d-G^iZiGpMl zNg2r@BvVMnkf86-2_%z1!ajBYl}OyZ;e5W2O}9rUoA0(K%grn8vHi_Uf8Y)R_rd`? z+zasz%ND-p4)-ek+r5_Jqhv|0M34Ju3CJ5k=R)LVX{Z^fJy5eyb5MJs_PGuTSqG3h zUx~4PaL9t|kP^?lSE;yQa}}ZG)`;6E_Og%7iibo>o!YwLIJ1k8LbASps>_vjXsOL~@^(eLezmv?~G< zz75wAyKKpjErxSpor{Efg?U&t6&lfS1O6r^`cMxLXFW7lpPRn@ogd`UL3$khnKqGXWP&phM{e3kLS>;(^y^A z=o(K#ilGb^(HKwb_G%qcxF^K`p>0}@yU^u<7&^C?THM%=qnH4)JFxBD5n?+~%}rYe zAW~dZgFKziu3rogEZet!IY`lk;f<^JF1N&kL7L7K?q0nO3Z>(x>Rf((v4eT6lOyiP zUgZ0{M4v~#pY#=4Vt=QALXo6%cjeosTH^6e2^9lmKxrwjJT4t+i6=UHP&GtKBOC0& zxrh1h=I?*;xH#Jqk9CI8vXAT?doX?<0C=V|f(nJ~+y6kn4*-0mGm45aGC2NV?mhtU zY-b!56Xc&WOrqi-0T3T}oEt;XE~DlU0nk42xNo8*PIabGGadSY%zz>U*O9z|1amok6UiwgZy`C20lvniI^Gev+-aj?1g z1O{GgADC)hYP%m3JamvbaIHoBeAU;gng#zNia_g~QDSR#)B2dY9x9>Qbxs*M^w5yk zy8FgV=v^j$m$-b=`^251?|_K8#W@D#SyT{&U&!PyWcm@A{+SHFkm7>!?b-_h&5M3f WIB*AeXkPRb1?A4d?*tlm+P?u5^!yJ1 literal 0 HcmV?d00001 diff --git a/src/pr_reviewer/context.py b/src/pr_reviewer/context.py new file mode 100644 index 0000000..d303370 --- /dev/null +++ b/src/pr_reviewer/context.py @@ -0,0 +1,45 @@ +import os +from pathlib import Path +from typing import Dict, Optional +from .state import PRReviewState, ContextOverrides + + +def resolve_context(state: PRReviewState) -> Dict[str, str]: + """ + Resolve the context for each review type based on overrides and default files. + + Args: + state: The PR review state containing potential context overrides + + Returns: + A dictionary with keys 'code_review', 'security_review', 'infra_review' + and their resolved context strings. + """ + # Define the mapping of context types to their default file paths + context_mapping = { + 'code_review': 'contexts/defaults/code_review.md', + 'security_review': 'contexts/defaults/security_review.md', + 'infra_review': 'contexts/defaults/infra_review.md' + } + + resolved = {} + + for context_type, default_path in context_mapping.items(): + # Check if there's an override in the state + override_value = None + if state.context_overrides: + override_value = getattr(state.context_overrides, context_type, None) + + if override_value is not None and override_value.strip() != '': + # Use the override if provided and not empty + resolved[context_type] = override_value.strip() + else: + # Use the default file + try: + with open(default_path, 'r') as f: + resolved[context_type] = f.read().strip() + except FileNotFoundError: + # If the default file doesn't exist, use an empty string + resolved[context_type] = '' + + return resolved diff --git a/src/pr_reviewer/flow.py b/src/pr_reviewer/flow.py new file mode 100644 index 0000000..c7b975b --- /dev/null +++ b/src/pr_reviewer/flow.py @@ -0,0 +1,150 @@ +from crewai.flow import Flow, listen, start, and_ +from crewai import Crew +from .state import PRReviewState +from .llm import get_llm +from .context import resolve_context +import os +from datetime import datetime + +# Import the crews +from crews.code_review_crew.code_review_crew import CodeReviewCrew +from crews.security_review_crew.security_review_crew import SecurityReviewCrew +from crews.infra_review_crew.infra_review_crew import InfraReviewCrew +from crews.summariser_crew.summariser_crew import SummariserCrew + + +class CodeReviewFlow(Flow[PRReviewState]): + + @start() + def receive_pr(self, inputs): + """Initialize the PR review state with input data.""" + print(f"Received PR review request for PR #{inputs.get('pr_id')}") + + # Initialize the state + self.state.pr_id = inputs.get("pr_id", "") + self.state.pr_title = inputs.get("pr_title", "") + self.state.pr_description = inputs.get("pr_description", "") + self.state.pr_url = inputs.get("pr_url", "") + self.state.repo_name = inputs.get("repo_name", "") + self.state.repo_url = inputs.get("repo_url", "") + self.state.branch = inputs.get("branch", "") + self.state.base_branch = inputs.get("base_branch", "") + # Convert files from list of dicts to list of FileInfo objects if needed + files_input = inputs.get("files", []) + if files_input and isinstance(files_input[0], dict): + # Convert dicts to FileInfo objects + from .state import FileInfo + self.state.files = [FileInfo(**file_dict) for file_dict in files_input] + else: + self.state.files = files_input + + # Handle context_overrides + context_overrides_input = inputs.get("context_overrides") + if context_overrides_input and isinstance(context_overrides_input, dict): + from .state import ContextOverrides + self.state.context_overrides = ContextOverrides(**context_overrides_input) + else: + self.state.context_overrides = context_overrides_input + + self.state.started_at = datetime.now() + + # Resolve context + self.state.resolved_context = resolve_context(self.state) + + return self.state + + @listen(receive_pr) + def run_code_review(self): + """Run the code review crew.""" + print("Starting code review...") + + # Instantiate and run the code review crew + code_crew = CodeReviewCrew() + # The crew's kickoff method expects inputs matching the task template variables + inputs = { + "pr_title": self.state.pr_title, + "pr_description": self.state.pr_description, + "files": [file.dict() if hasattr(file, 'dict') else file for file in self.state.files], + "context": self.state.resolved_context.get("code_review", "") + } + result = code_crew.crew().kickoff(inputs=inputs) + self.state.code_review_results = str(result) + print("Code review completed.") + + return self.state + + @listen(receive_pr) + def run_security_review(self): + """Run the security review crew.""" + print("Starting security review...") + + # Instantiate and run the security review crew + security_crew = SecurityReviewCrew() + inputs = { + "pr_title": self.state.pr_title, + "pr_description": self.state.pr_description, + "files": [file.dict() if hasattr(file, 'dict') else file for file in self.state.files], + "context": self.state.resolved_context.get("security_review", "") + } + result = security_crew.crew().kickoff(inputs=inputs) + self.state.security_review_results = str(result) + print("Security review completed.") + + return self.state + + @listen(receive_pr) + def run_infra_review(self): + """Run the infrastructure review crew.""" + print("Starting infrastructure review...") + + # Instantiate and run the infrastructure review crew + infra_crew = InfraReviewCrew() + inputs = { + "pr_title": self.state.pr_title, + "pr_description": self.state.pr_description, + "files": [file.dict() if hasattr(file, 'dict') else file for file in self.state.files], + "context": self.state.resolved_context.get("infra_review", "") + } + result = infra_crew.crew().kickoff(inputs=inputs) + self.state.infra_review_results = str(result) + print("Infrastructure review completed.") + + return self.state + + @listen(and_(run_code_review, run_security_review, run_infra_review)) + def summarise(self): + """Summarize the review results.""" + print("Starting summarisation...") + + # Instantiate and run the summariser crew + summariser_crew = SummariserCrew() + inputs = { + "code_review_results": self.state.code_review_results, + "security_review_results": self.state.security_review_results, + "infra_review_results": self.state.infra_review_results, + "context": self.state.resolved_context + } + result = summariser_crew.crew().kickoff(inputs=inputs) + self.state.review_summary = str(result) + self.state.completed_at = datetime.now() + print("Summarisation completed.") + + return self.state + + @listen(summarise) + def format_response(self): + """Format the final response.""" + print("Formatting final response...") + + # Return the final state as the response + return { + "pr_id": self.state.pr_id, + "pr_title": self.state.pr_title, + "review_summary": self.state.review_summary, + "code_review_results": self.state.code_review_results, + "security_review_results": self.state.security_review_results, + "infra_review_results": self.state.infra_review_results, + "started_at": self.state.started_at.isoformat() if self.state.started_at else None, + "completed_at": self.state.completed_at.isoformat() if self.state.completed_at else None, + "error": self.state.error + } \ No newline at end of file diff --git a/src/pr_reviewer/llm.py b/src/pr_reviewer/llm.py new file mode 100644 index 0000000..624620f --- /dev/null +++ b/src/pr_reviewer/llm.py @@ -0,0 +1,56 @@ +import os +from crewai import LLM +from typing import Optional + + +def create_llm() -> LLM: + """ + Create an LLM instance based on environment variables. + + Expected environment variables: + - LLM_MODEL: The model name to use (required) + - LLM_BASE_URL: The base URL for the LLM API (optional) + - LLM_API_KEY: The API key for the LLM service (optional) + - LLM_PROVIDER: The provider (e.g., 'openai', 'anthropic', 'ollama') (optional) + + Returns: + LLM: A CrewAI LLM instance + """ + model = os.getenv("LLM_MODEL") + if not model: + raise ValueError("LLM_MODEL environment variable is required") + + base_url = os.getenv("LLM_BASE_URL") + api_key = os.getenv("LLM_API_KEY") + provider = os.getenv("LLM_PROVIDER") + + # Prepare LLM configuration + llm_config = { + "model": model, + } + + if base_url: + llm_config["base_url"] = base_url + if api_key: + llm_config["api_key"] = api_key + if provider: + llm_config["provider"] = provider + + return LLM(**llm_config) + + +# Shared LLM singleton +_shared_llm: Optional[LLM] = None + + +def get_llm() -> LLM: + """ + Get the shared LLM singleton instance. + + Returns: + LLM: The shared LLM instance + """ + global _shared_llm + if _shared_llm is None: + _shared_llm = create_llm() + return _shared_llm \ No newline at end of file diff --git a/src/pr_reviewer/main.py b/src/pr_reviewer/main.py new file mode 100644 index 0000000..f1082a6 --- /dev/null +++ b/src/pr_reviewer/main.py @@ -0,0 +1,297 @@ +import logging +from fastapi import FastAPI, HTTPException, Request +from fastapi.responses import JSONResponse +import uvicorn +from typing import Dict, Any, List, Optional +import asyncio +from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeoutError +import time +import uuid + +from .flow import CodeReviewFlow +from .state import PRReviewState, FileInfo, ContextOverrides + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +app = FastAPI( + title="PR Reviewer API", + description="API for conducting automated pull request reviews", + version="0.1.0" +) + + +# Configuration for timeouts +TOTAL_FLOW_TIMEOUT = int(os.getenv("TOTAL_FLOW_TIMEOUT", "600")) # Default 10 minutes +PER_CREW_TIMEOUT = int(os.getenv("PER_CREW_TIMEOUT", "300")) # Default 5 minutes + + +@app.get("/api/v1/health") +async def health_check() -> Dict[str, str]: + """ + Health check endpoint to verify the service is running. + """ + return {"status": "healthy", "service": "pr-reviewer"} + + +@app.post("/api/v1/review") +async def review_pr(request: Request) -> Dict[str, Any]: + """ + Endpoint to trigger a PR review. + Implements the full request/response schema as specified in Section 2.2.1. + """ + start_time = time.time() + review_id = str(uuid.uuid4()) + + # Log the incoming request (excluding sensitive data) + logger.info(f"Received PR review request: {review_id}") + + try: + # Parse the JSON payload + try: + payload = await request.json() + except Exception: + raise HTTPException(status_code=422, detail="Invalid JSON payload") + + # Validate and extract required fields according to the API specification + # Request schema: + # { + # "pr_id": "string (required)", + # "title": "string (required)", + # "description": "string (optional)", + # "repo": { + # "name": "string (required)", + # "url": "string (required)" + # }, + # "source": { + # "branch": "string (required)", + # "commit": "string (optional)" + # }, + # "target": { + # "branch": "string (required)", + # "commit": "string (optional)" + # }, + # "files": [ + # { + # "path": "string (required)", + # "content": "string (optional)", + # "status": "string (required)", + # "additions": "integer (optional, default 0)", + # "deletions": "integer (optional, default 0)", + # "patch": "string (optional)" + # } + # ], + # "context": { + # "code_review": "string (optional)", + # "security_review": "string (optional)", + # "infra_review": "string (optional)" + # } + # } + + # Extract top-level fields + pr_id = payload.get("pr_id") + title = payload.get("title") + description = payload.get("description") + + # Extract repo information + repo_data = payload.get("repo", {}) + repo_name = repo_data.get("name") + repo_url = repo_data.get("url") + + # Extract source information + source_data = payload.get("source", {}) + source_branch = source_data.get("branch") + source_commit = source_data.get("commit") + + # Extract target information + target_data = payload.get("target", {}) + target_branch = target_data.get("branch") + target_commit = target_data.get("commit") + + # Extract files + files_data = payload.get("files", []) + + # Extract context overrides + context_data = payload.get("context", {}) + + # Validate required fields + if not pr_id: + raise HTTPException(status_code=422, detail="Missing required field: pr_id") + if not title: + raise HTTPException(status_code=422, detail="Missing required field: title") + if not repo_name: + raise HTTPException(status_code=422, detail="Missing required field: repo.name") + if not repo_url: + raise HTTPException(status_code=422, detail="Missing required field: repo.url") + if not source_branch: + raise HTTPException(status_code=422, detail="Missing required field: source.branch") + if not target_branch: + raise HTTPException(status_code=422, detail="Missing required field: target.branch") + + # Convert files data to FileInfo objects + files = [] + for file_data in files_data: + if not file_data.get("path"): + raise HTTPException(status_code=422, detail="Missing required field: files[].path") + if not file_data.get("status"): + raise HTTPException(status_code=422, detail="Missing required field: files[].status") + + file_info = FileInfo( + path=file_data.get("path", ""), + content=file_data.get("content"), + status=file_data.get("status", "modified"), + additions=file_data.get("additions", 0), + deletions=file_data.get("deletions", 0), + patch=file_data.get("patch") + ) + files.append(file_info) + + # Create context overrides object (only if at least one field is provided) + context_overrides = None + if any([context_data.get("code_review"), + context_data.get("security_review"), + context_data.get("infra_review")]): + context_overrides = ContextOverrides( + code_review=context_data.get("code_review"), + security_review=context_data.get("security_review"), + infra_review=context_data.get("infra_review") + ) + + # Initialize and run the flow with timeout + flow = CodeReviewFlow() + + # Run the flow in a thread pool with timeout to avoid blocking the event loop + loop = asyncio.get_event_loop() + with ThreadPoolExecutor() as pool: + try: + # Wait for the flow to complete with a timeout + flow_result = await asyncio.wait_for( + loop.run_in_executor( + pool, + lambda: flow.kickoff(inputs={ + "pr_id": pr_id, + "pr_title": title, + "pr_description": description, + "pr_url": f"{repo_url}/pull/{pr_id}", # Construct PR URL + "repo_name": repo_name, + "repo_url": repo_url, + "branch": source_branch, # Using source branch as the active branch + "base_branch": target_branch, # Using target branch as base + "files": [file.dict() for file in files], # Convert to dict for flow + "context_overrides": context_overrides.dict() if context_overrides else None + }) + ), + timeout=TOTAL_FLOW_TIMEOUT + ) + except asyncio.TimeoutError: + logger.error(f"PR review timed out: {review_id}") + raise HTTPException( + status_code=504, + detail=f"PR review timed out after {TOTAL_FLOW_TIMEOUT} seconds" + ) + + # Calculate processing time + processing_time = time.time() - start_time + + # Prepare response according to the API specification: + # { + # "review_id": "string (unique identifier for this review)", + # "status": "string (\"completed\", \"failed\", etc.)", + # "timestamp": "string (ISO 8601 timestamp)", + # "results": { + # "code_review": "string (results from code review)", + # "security_review": "string (results from security review)", + # "infra_review": "string (results from infrastructure review)", + # "summary": "string (synthesized review summary)" + # }, + # "metadata": { + # "processing_time_seconds": "number", + # "pr_id": "string", + # "repo": { + # "name": "string", + # "url": "string" + # } + # } + # } + + response = { + "review_id": review_id, + "status": "completed" if not flow_result.get("error") else "failed", + "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "results": { + "code_review": flow_result.get("code_review_results"), + "security_review": flow_result.get("security_review_results"), + "infra_review": flow_result.get("infra_review_results"), + "summary": flow_result.get("review_summary") + }, + "metadata": { + "processing_time_seconds": round(processing_time, 2), + "pr_id": pr_id, + "repo": { + "name": repo_name, + "url": repo_url + } + } + } + + # Include error information if present + if flow_result.get("error"): + response["metadata"]["error"] = flow_result["error"] + logger.error(f"PR review failed: {review_id} - {flow_result['error']}") + else: + logger.info(f"PR review completed successfully: {review_id} in {processing_time:.2f}s") + + return response + + except HTTPException: + # Re-raise HTTP exceptions as they are already properly formatted + raise + except asyncio.TimeoutError: + # This should be caught by the wait_for above, but just in case + logger.error(f"PR review timed out: {review_id}") + raise HTTPException( + status_code=504, + detail=f"PR review timed out after {TOTAL_FLOW_TIMEOUT} seconds" + ) + except Exception as e: + # Log the error for debugging + logger.error(f"Error in PR review: {review_id} - {str(e)}") + raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}") + + +# Error handlers +@app.exception_handler(404) +async def not_found_handler(request: Request, exc: HTTPException): + return JSONResponse( + status_code=404, + content={"message": "Endpoint not found"} + ) + + +@app.exception_handler(422) +async def request_validation_exception_handler(request: Request, exc: HTTPException): + return JSONResponse( + status_code=422, + content={"message": "Invalid request payload", "details": exc.detail} + ) + + +@app.exception_handler(500) +async def internal_error_handler(request: Request, exc: HTTPException): + return JSONResponse( + status_code=500, + content={"message": "Internal server error"} + ) + + +@app.exception_handler(504) +async def timeout_handler(request: Request, exc: HTTPException): + return JSONResponse( + status_code=504, + content={"message": "Request timeout", "details": exc.detail} + ) + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/src/pr_reviewer/state.py b/src/pr_reviewer/state.py new file mode 100644 index 0000000..17f5469 --- /dev/null +++ b/src/pr_reviewer/state.py @@ -0,0 +1,45 @@ +from pydantic import BaseModel, Field +from typing import List, Optional, Dict, Any +from datetime import datetime + + +class FileInfo(BaseModel): + """Information about a file in the PR.""" + path: str + content: Optional[str] = None + status: str # added, modified, removed, etc. + additions: int = 0 + deletions: int = 0 + patch: Optional[str] = None + + +class ContextOverrides(BaseModel): + """Overrides for the default context.""" + code_review: Optional[str] = None + security_review: Optional[str] = None + infra_review: Optional[str] = None + + +class PRReviewState(BaseModel): + """State of the PR review process.""" + # Input fields + pr_id: str + pr_title: str + pr_description: Optional[str] = None + pr_url: Optional[str] = None + repo_name: str + repo_url: str + branch: str + base_branch: str + files: List[FileInfo] = Field(default_factory=list) + context_overrides: Optional[ContextOverrides] = None + # Internal fields + resolved_context: Optional[Dict[str, str]] = None + code_review_results: Optional[str] = None + security_review_results: Optional[str] = None + infra_review_results: Optional[str] = None + review_summary: Optional[str] = None + # Metadata + started_at: Optional[datetime] = None + completed_at: Optional[datetime] = None + error: Optional[str] = None \ No newline at end of file diff --git a/tests/integration/__pycache__/test_mcp_servers.cpython-314-pytest-9.0.3.pyc b/tests/integration/__pycache__/test_mcp_servers.cpython-314-pytest-9.0.3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7d0c4a9ef80f91f0d73e92e10a6f78dd6cd6ee97 GIT binary patch literal 7933 zcmeGhU2hcE_0G=6-u2pEe=J}_>|KLRyaw4|n}8P-V_b+G732(P(+4M`Wp|9XUGENe z#spicO478f`p_hOfxeNah&T6*$D~%JN=>TOZe^PZR{IkOpr&$EspsDL*x9uy21JEa zM!WZ(d*+;T@0^))&&NH`*ieVyy7>2hXTHY>rRc!9V9c(E8_-=tGBVKnNaiLv!^e7l zLYVhO(1d765$lfM@UuJEz^o4#f%ouK*p}nzrgnJ-HuyezFR&$T`)1n5Q101+vT>NR zu(D6~4f7T}?6dj^lKEZ82+0DpO|l4Wvn)Z|BKx6jmAQQ=h8Mf*CUb^*i6o6oJ{L7q z-O!^mc@jN4J{8qfaz!P2f8Du7Uw?m<0{t)vOej43CoJI_+G8mRO>i25iG7!w65QL| z<-M<4YeAR(fY57bTCj&`N^+n|--YBr!Z*apb??Pno8!P7u9HHPRkQRP50mCOv(DL&Y8TnaEQ>)mO?`tjY%Eu7}1_yZQ)2vr^q9V z`_CjZSv4Is^0thvW{F2{a~o1dkLL2mZEkTVm2fDgM=cB!I26-k4QAV1N>iNAde&a9 z0!V8abtkn2Y641R+%tdWZ{~a{f(0xG= zPjTybXsx5Gh`VlHD2BRLvHdIKuEG^}aWK_A-1tfG zH^Ji06L;RZGhPgzT*YH0JodwVp5EPf7=dMdG`4p%tYY2A?~L6UDuzc_@yQZC$*g;f+aT1sw>IDX zp!M271tK}$q;G=PI2Psclfs0kLEHy;VN#m#YkUMkg-U>-2?@?K5)(epNJ{XYkzW=s zf1JP&1pVF#G3+3Sz7uv$ZRyzMrbGaOpyqccJV6iy3l)bx<+^c4(qn?4%1m3j`$`Dm ztk4dEDtx;uGdPep^nf79FXQPa(PJl|s0o7VMPz=BL!Y%Lm#IzO>r^^>b-|wo3c;FF^a!Giu-E z1MSzpwA#P^T-v`{rTvB%pnd-{YTxAp?Kixv+Gpuk)Y+`u^s9cFI!|^@bvsa{hxDuQ zLj9)nOVsK|>tzwrF9v0}>HQARQ%DYNN3xsPwJaW}ggcM~MN>rmAUc;%&u3MAQ$`o# z%*OG2T9sJ_F%G1FX*sW^=1Im_sKSOOb2B7ag=#!MKR1^o8A!?~s}(SrffNW-=(IIq zli8r#g4tcOE<+_+a7cGWF~nkIQ;6k#ka%>l!$XjRbSP(Jl7`A2SFeh_!shFgRqto( zvT`Lh$0h`!uhtx}sNJ=CIkrL)$k34)v$(8B^0I8g5c(0AJ@zcskKJFR215PIGe(DI z;Plm$s?pj|tbsimTPd5~lG{s#$7s_>n*+3=(FUQVEkff5LQ7ZVHE47iomzRQ-CEIR zvlVTEg6%L@U>a-Q8Q>-B$?xBidcW-M55dsCyOLzcTJFEVv^V@Zd@niWSQ3(s%)oyVl)X z#J#tER1Ed5V*6Ldy@j8emxvK*TjE4+J3f^|E9AZ;BO9av_05x{)LtHyt1ntOoJKLNvhvdi0fwphrkM+aqo`#3FD^K@yBn; zez;q;88@-bm`?8G{BHmW{p&ZNp%pz*jIZOPcS9ZJP^2KN?THj|w8^7*8u*%iHGps^=g zyGP!Wteuh)CiU6scrnT1er)VTE030;TS1p)=X6@f&f`FF68`#cp|L9WVCM(dc6SzW z=gn>!3v^n)GVUzs?#Kl^xKf2;g)+M|UI2);Y2K`^7YmzyMM>vVieh5t7={%)&345) zE$O$;Mj%}6r@2GMfI}D;W%_NLwV~27rpfCIy6Ma7rl2K_SyO_89XNtAg`eg#In$rc z5O$D*6=eoap%jIkZV;Nsu?UsWZIMvD-9`_gMV4yu2`g86lS~4Rp3&+v(7*`<$9?DX zal-c!;<%5|FB`r^b>E`EeHaSY@rU?&=)=&b+^2&#j(&FZ#);2P-12?l|J?sY==0DL tE=Nc141XCex1RYMdh3BCai_QkEfO~d?GA3_!682ve-xIuIGKQ{{sXjJh_(O# literal 0 HcmV?d00001 diff --git a/tests/integration/test_mcp_servers.py b/tests/integration/test_mcp_servers.py new file mode 100644 index 0000000..8809efb --- /dev/null +++ b/tests/integration/test_mcp_servers.py @@ -0,0 +1,70 @@ +""" +Integration tests for MCP servers. +""" +import pytest +import sys +import os + +# Add the project root to the path so we can import the MCP servers +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) + + +def test_hadolint_mcp_import(): + """Test that the Hadolint MCP server can be imported.""" + try: + from mcp_servers.hadolint_mcp import server + assert server is not None + except ImportError as e: + pytest.fail(f"Failed to import Hadolint MCP server: {e}") + + +def test_checkov_mcp_import(): + """Test that the Checkov MCP server can be imported.""" + try: + from mcp_servers.checkov_mcp import server + assert server is not None + except ImportError as e: + pytest.fail(f"Failed to import Checkov MCP server: {e}") + + +def test_crew_imports(): + """Test that all crew modules can be imported.""" + try: + from crews.code_review_crew.code_review_crew import CodeReviewCrew + from crews.security_review_crew.security_review_crew import SecurityReviewCrew + from crews.infra_review_crew.infra_review_crew import InfraReviewCrew + from crews.summariser_crew.summariser_crew import SummariserCrew + + # Test that we can instantiate the crews + code_crew = CodeReviewCrew() + security_crew = SecurityReviewCrew() + infra_crew = InfraReviewCrew() + summariser_crew = SummariserCrew() + + assert code_crew is not None + assert security_crew is not None + assert infra_crew is not None + assert summariser_crew is not None + except ImportError as e: + pytest.fail(f"Failed to import crew modules: {e}") + except Exception as e: + pytest.fail(f"Failed to instantiate crews: {e}") + + +def test_flow_import(): + """Test that the flow module can be imported.""" + try: + from src.pr_reviewer.flow import CodeReviewFlow + flow = CodeReviewFlow() + assert flow is not None + except ImportError as e: + pytest.fail(f"Failed to import flow module: {e}") + + +def test_main_import(): + """Test that the main module can be imported.""" + try: + from src.pr_reviewer.main import app + assert app is not None + except ImportError as e: + pytest.fail(f"Failed to import main module: {e}") \ No newline at end of file diff --git a/tests/unit/__pycache__/test_context.cpython-314-pytest-9.0.3.pyc b/tests/unit/__pycache__/test_context.cpython-314-pytest-9.0.3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..91c5270d82d31b22a5852042a81c89376a7b02aa GIT binary patch literal 8517 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 TQ3GH7oNA{++mk_RQt9x2u`lCT literal 0 HcmV?d00001 diff --git a/tests/unit/__pycache__/test_state.cpython-314-pytest-9.0.3.pyc b/tests/unit/__pycache__/test_state.cpython-314-pytest-9.0.3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ca21ceded91e260ff9dd9630c3145ba1da07f28d GIT binary patch literal 14836 zcmeHO-ER}w6`!%MzcP;V0f7JsaR`Ap8{#B{4J=<-D66E~rF3>nRf%F5dqQS$Z10^3 zAFe7PwW{E0U-QW0=B@ulU;QQ98mZcs{s9z_Dj`+sId|@S*aQa%NvY(~&6#uV{LZ~I zbL{bN&OP}o(%mJ%7W(-=>hFVskV3=SJY0F12Ia9J3%YPyki}(D_prTpiL84@ge9Nu zA2E*v^Z*?3EC-iDdT6Oj?^=>{i5>GUhnK>7ctnuNK|$}9eIO&UA7qal0NE>xV?r!g zIMwyrtg1(KMbowDN{&YLRVAwFNnMGqC9}!9%9@hZqiebJdPbSki1G?2)~T~vRwLWWPYriVW(s?m2o)_sH#Bslawls*NXNPf&bmG_)vE7g30V{gW9^&ZT zVH|rpaopoL4m$C(w7huRx5GI0cH+3laXf5A&)Gv9cluoI>%?&{<0wl&$@&Y&z5_BA zO;IJOtJ%BJWYi*DQ8f$9ESplM3z9}t@wKFyoz6chMDkS4>Jukdl}sjgGG+vUuBj`k zl8*Tdl27WZMj(YuEvp-Tq;~6?5lW`hs;=g;pr(}!P&8D%`A0KGXa)FPLPdfH_8f{c zuOmrKQ>Y#tpU6L2h%w~C^-b}$=ifgu7zE90g5sCzl20$2Hi}^=Lr4vbN^d(ZsOeR5< zJWb`+@=2-~VK|()v#w?!u$tk|$~NO{d^NYG#FKPQg+oX1)C2}d zflsP&klh^n1C}s9{yzWX<>$RZ zc;LrNPcIe8-<42R7@`l6164#-5EbQIf{$gtrPAD@sRg(^haE z4k?o{%c{B1RUFR-xfO8uAIu8p^AbLC&F|p$t7!pVKi_;ch1XJeamB6kTH1Ln6@<3w zh0bf~UZ)p2ucc1n>;E=Q#AtADVX~D5w`Ln1I{aGtDhJf}LG;G88)pj#udXVodn}px z?QebssPB~&(xbw@o5_rd3CZX$a;bX?MIHhPO(~U2E1bL-J(`kQr>g$Q>Ief9rb%v5 z%<&n0jH5o(6FDpKN%3aW9u&POFkyC`Vv1~2ESzpbL|s!ivr4hB(8jIhBwJnQ+qk+f zr^#XGLU_0? zO_ZdGB02ZdH%ycyU?RL#$ho34VVkzX=o-hA5d-WE&6*2e#Sv`}w*npw@Q|iT(o~Tw z7iX4DS(29PgtrRxOH;OKD{Lx`DI*4EMziL^SJ{}hi(3H~Hgyb$V3AxYp1NYnl60j` zc&k7^(nP1n)Y)9Tvy2#+8qJyuUu9#y=?gt@h>k%#Qt)SQHeI4y(4_^Q`Q8<()WtIR zQd5S)E0a$&gHL3iD|f!#quIYL_U#$`fE@hj8T`<;8T=4t41f7?)?qqPB=T-YaciQq zvnV#x-A)vDIf`-!vV}c`OYcmRq7PJkH42F+T}@`9kRQyXwP`vI&+?dP{^}#UF@MJ2 z%4g^#U^Yd=qhccO{WyxMv8Iwyul}qzJ*Wl>!q{cglt7CxahRuP2?}YxPI#-3c}63g9`g|U@#dXn#K3Pknl%@`ii6oBxfSqmv5489 zzAF+G(rlgZRw1*DLpVL=AvO{3EF%WyMYHC@S8*_VB)0-b+E9oopw3%mTySAt4lMA6 z>RlldJ&a5=FIxrW&?ibJV3B@Xg5Z<`NZVrHPA0-~_eUoak!{Juhc8>1aco|;>co-A zyB)`^>BNVZ1DJ8_ZzA`dIPQKNw=4&c$p@DMm~q_KiDSz$@@{`0w=4&cKfJ8NjN|@J z9QQbmm`!Whpv=m@!|&rjCysj@N6dw`Yv`>qc>FFT$ky*nL%CLb9m?HU@e+d=0q$lHEV_&N4Ik7ysc9n! zJ6+W?P^*PQnPpo+rmoYB;lD$nUT@Wq?j$uOVH)NdqL0-?y?xyfl53~htm3iRo0jju zVWzzUJI_|1c2#or6}39Go5;HMR-bkSeCCa5^=P+l>bjq;ZtZRxuJ~$A4m1u=0=Rc` zZSEa}it0)_k<`J}Nk)QKyCfzS{Xt>n7JmH{Yl!IQDBg9UbpgR=QQ-fc&@WJ&2VwZw zH5pybNg84I)KCC;0m1LQFgwf=sdhP^S-j^g%PzuTbMU9lLa6~P%Z3j%BKsPBM;raa zjlTIt&tPN!;aA;(ZvW$J&-;aN-;d{?o-dM-jd3PQ(ny`~Rv{x#&fAu)JUL^lWk7JN z=76g>nvHQQfNw~>?_VAWbMOK}EDz+?yO#&{{&@N6Wh~FT$3#nw-jmDj0y|U3Ibekr z8ya(v?)6D8?*o$yv6$DdrVEnC4!IHC*)Zq*!c6-To%ZUyi#qg8i# z;u$RWgD6QS>V&rnIlBA|E_4-#vO#VI9Pm2XM|NoASdoB$!p}P4 ztwM(IBkOdSI!?m-%7EZj%>h?&ARFUW0MD+|Eb|=O(4pWGL~Fq%lzBQOn5K@?xC$=0 z#R@Jt8VVdYbQ|PWSivRS-QkinTqNVq_M1Y2uN7|rgUnA9dksEaF_@UgR5t?f-@t$O!r zg4GvI+nJKS5FR>lp30+n%S`f+K6c6vu!$L)C7CqSzVL=It(ksId(K@+$PR_}%T&Ivv3xSHXNzW@}U?ES=l^i=18REv>tFX4<_Lm3~48p|A zF(!2q4|HMjr3lADTL5F>hakg;kGXyt&{3gEa^$i&@+(5}KJ8GJF&o)hp z_NEC$NkrQawEzlQn{GQ*E&^|`9aIO+T-&r5qwYIp=J-Gc&W3Spr%*7>n*wbI89qMh z4`>Iyc$w8rUIR4Z(Rj9+tv`ACH zkQQkh^6`fX9Xfoe?LZAWYD#YM*-qH;Uk3EJN;b$qv{M>CYjTnv=fGWyC^mom@nx z%s}KEcQYrE-A0WpTf}QT>_qm2-v(VpM;AspafXs@JIgKUZE& zGk8L%X>(kkb$s8NcA!E-rGrdgoaSu|K9-j{|N|cV8kyKTo$NKG4H?cvY9~i{6H>QFjJf>uFw2H@n_1+g}P5?8FidM zf=dUo0qsDb@g~TeqTXzv^27~35X4Shu^ry>&{puZbTp8Cr!e)@#46*>0>SYytL;Sq z-@v*T$R;fpgG3{p!iuNS5pcySdC)@=owwt?tKI#-+x79T1?h|K{R_sO?gLj(ueNlr zwCrAL*?r@cm6pNVErYAQFD^(6ZS&@b+9QP|wp}~?!QpGiJ~%c%xDwxUJHF@3MBBA9 zzd18MxSZJYKu*Mt{H49~qmk<)EA4xi+V|eL@|5CJ$t#}^e%`ZqDz{Q(i|4Jy{JVF~zx#lY^MWVB&{0C1 zEQuQg>e5ef@pIu64&!1*{41_9xZ27Nq|0LQX*-XoY`8d&xZhmLDw3GzphMm)&phoU%t4d=I132`DmHZ0xhN{$Umx4t9b zd@urdQ`yBzz{@VS3r{b*&ulrdB#X^e30hO$f@=yuve!GR0OFUD3yc$@|8;T5?J5UX!33`UZQ$3-uPVt@FD3+rB0Fz(Z^!!#?Q+ z{DG9oWYP(W`IBNHr8^Np5g-(;%I_F9YsYXoif9+2K14dAen8Ew-2f61@w@GT32*II zQ{C0E)vgQGH&+J^RNvf?p-Fg%ox-N(4bP<$fm)i&TSaIsxfys@8YFD$yl=U@xArOa z4iuXY`dvf^00j!0D?wfe?Z-Pzeiu|URGEjMdNk~ckI@sbfqjJkJwV_~g77uj^A+j8 mN3?q+c8?^#B3+M^h;TvpaN-fc@lk4@aOCRD-v|s` literal 0 HcmV?d00001 diff --git a/tools/git_tool.py b/tools/git_tool.py new file mode 100644 index 0000000..c4fa835 --- /dev/null +++ b/tools/git_tool.py @@ -0,0 +1,57 @@ +from pydantic import BaseModel, Field +from crewai_tools import BaseTool +import git +import os +import shutil +from typing import Type + + +class GitCloneInput(BaseModel): + """Input schema for GitTool.""" + repo_url: str = Field(..., description="URL of the Git repository to clone") + branch: str = Field(None, description="Branch to checkout (optional)") + commit: str = Field(None, description="Commit hash to checkout (optional)") + target_dir: str = Field(None, description="Target directory to clone into (optional)") + + +class GitTool(BaseTool): + name: str = "GitTool" + description: str = "Clones a Git repository and checks out a specific branch or commit" + args_schema: Type[BaseModel] = GitCloneInput + + def _run(self, repo_url: str, branch: str = None, commit: str = None, target_dir: str = None) -> str: + """ + Clone a Git repository and checkout a specific branch or commit. + + Args: + repo_url: URL of the Git repository to clone + branch: Branch to checkout (optional) + commit: Commit hash to checkout (optional) + target_dir: Target directory to clone into (optional) + + Returns: + A message indicating the result of the operation. + """ + # If target_dir is not provided, extract the repository name from the URL + if target_dir is None: + target_dir = repo_url.split("/")[-1] + if target_dir.endswith(".git"): + target_dir = target_dir[:-4] + + # Remove the target directory if it already exists to avoid conflicts + if os.path.exists(target_dir): + shutil.rmtree(target_dir) + + try: + # Clone the repository + repo = git.Repo.clone_from(repo_url, target_dir) + + # Checkout the specified branch or commit + if branch: + repo.git.checkout(branch) + elif commit: + repo.git.checkout(commit) + + return f"Successfully cloned {repo_url} into {target_dir} and checked out {'branch: ' + branch if branch else 'commit: ' + commit if commit else 'default branch'}" + except Exception as e: + return f"Error cloning repository: {str(e)}" \ No newline at end of file