Initial CodePilot deployment - Multi-agent AI coding assistant
Browse files- .dockerignore +46 -0
- Dockerfile +39 -0
- README.md +67 -5
- chainlit.md +14 -0
- chainlit_app.py +346 -0
- codepilot/__init__.py +0 -0
- codepilot/agents/__init__.py +0 -0
- codepilot/agents/base_agent.py +161 -0
- codepilot/agents/coder_agent.py +202 -0
- codepilot/agents/conversation.py +137 -0
- codepilot/agents/orchestrator.py +227 -0
- codepilot/agents/planner_agent.py +157 -0
- codepilot/agents/reviewer_agent.py +238 -0
- codepilot/context/__init__.py +8 -0
- codepilot/context/bm25_retriever.py +175 -0
- codepilot/context/embedding_retriever.py +185 -0
- codepilot/context/hybrid_retriever.py +194 -0
- codepilot/context/indexer.py +145 -0
- codepilot/context/parser.py +327 -0
- codepilot/context/selector.py +102 -0
- codepilot/llm/__init__.py +0 -0
- codepilot/llm/client.py +84 -0
- codepilot/sandbox/__init__.py +5 -0
- codepilot/sandbox/e2b_sandbox.py +213 -0
- codepilot/sandbox/sandbox_tools.py +184 -0
- codepilot/tools/__init__.py +0 -0
- codepilot/tools/context_tools.py +143 -0
- codepilot/tools/file_tools.py +255 -0
- codepilot/tools/registry.py +278 -0
- requirements.txt +22 -0
.dockerignore
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Virtual environment
|
| 2 |
+
venv/
|
| 3 |
+
.venv/
|
| 4 |
+
env/
|
| 5 |
+
|
| 6 |
+
# Python cache
|
| 7 |
+
__pycache__/
|
| 8 |
+
*.pyc
|
| 9 |
+
*.pyo
|
| 10 |
+
*.pyd
|
| 11 |
+
.Python
|
| 12 |
+
|
| 13 |
+
# Environment files (secrets)
|
| 14 |
+
.env
|
| 15 |
+
.env.local
|
| 16 |
+
|
| 17 |
+
# IDE
|
| 18 |
+
.vscode/
|
| 19 |
+
.idea/
|
| 20 |
+
*.swp
|
| 21 |
+
*.swo
|
| 22 |
+
|
| 23 |
+
# Git
|
| 24 |
+
.git/
|
| 25 |
+
.gitignore
|
| 26 |
+
|
| 27 |
+
# Cache directories
|
| 28 |
+
.codepilot_cache/
|
| 29 |
+
.cache/
|
| 30 |
+
.chroma/
|
| 31 |
+
|
| 32 |
+
# Test files
|
| 33 |
+
tests/
|
| 34 |
+
test_*.py
|
| 35 |
+
*_test.py
|
| 36 |
+
|
| 37 |
+
# Documentation (not needed in container)
|
| 38 |
+
*.md
|
| 39 |
+
!README.md
|
| 40 |
+
docs/
|
| 41 |
+
|
| 42 |
+
# Misc
|
| 43 |
+
*.log
|
| 44 |
+
*.tmp
|
| 45 |
+
.DS_Store
|
| 46 |
+
Thumbs.db
|
Dockerfile
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# HuggingFace Spaces Dockerfile for CodePilot
|
| 2 |
+
FROM python:3.11-slim
|
| 3 |
+
|
| 4 |
+
# Set working directory
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Install git (needed for cloning repos) and other system dependencies
|
| 8 |
+
RUN apt-get update && apt-get install -y \
|
| 9 |
+
git \
|
| 10 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 11 |
+
|
| 12 |
+
# Create non-root user for security (HF Spaces requirement)
|
| 13 |
+
RUN useradd -m -u 1000 user
|
| 14 |
+
USER user
|
| 15 |
+
ENV HOME=/home/user \
|
| 16 |
+
PATH=/home/user/.local/bin:$PATH
|
| 17 |
+
|
| 18 |
+
# Set working directory for user
|
| 19 |
+
WORKDIR $HOME/app
|
| 20 |
+
|
| 21 |
+
# Copy requirements first (for better caching)
|
| 22 |
+
COPY --chown=user requirements-cloud.txt ./requirements.txt
|
| 23 |
+
|
| 24 |
+
# Install Python dependencies
|
| 25 |
+
RUN pip install --no-cache-dir --upgrade pip && \
|
| 26 |
+
pip install --no-cache-dir -r requirements.txt
|
| 27 |
+
|
| 28 |
+
# Copy application code
|
| 29 |
+
COPY --chown=user . .
|
| 30 |
+
|
| 31 |
+
# Expose port 7860 (HuggingFace Spaces default)
|
| 32 |
+
EXPOSE 7860
|
| 33 |
+
|
| 34 |
+
# Set environment variables
|
| 35 |
+
ENV PORT=7860
|
| 36 |
+
ENV HOST=0.0.0.0
|
| 37 |
+
|
| 38 |
+
# Run Chainlit
|
| 39 |
+
CMD ["chainlit", "run", "chainlit_app.py", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -1,11 +1,73 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
| 9 |
---
|
| 10 |
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: CodePilot
|
| 3 |
+
emoji: "\U0001F916"
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# CodePilot - AI Coding Assistant
|
| 12 |
+
|
| 13 |
+
**Multi-agent AI system that plans, writes, tests, and reviews code autonomously**
|
| 14 |
+
|
| 15 |
+
## What Makes This Different
|
| 16 |
+
|
| 17 |
+
| Feature | CodePilot | GitHub Copilot | Cursor |
|
| 18 |
+
|---------|-----------|----------------|--------|
|
| 19 |
+
| Multi-agent workflow | Planner > Coder > Reviewer | Single agent | Single agent |
|
| 20 |
+
| Sandboxed execution | Code tested before presenting | No | No |
|
| 21 |
+
| Codebase understanding | Hybrid search (BM25 + semantic) | Limited | Good |
|
| 22 |
+
| Quality report | Confidence, security, complexity | No | No |
|
| 23 |
+
|
| 24 |
+
## How It Works
|
| 25 |
+
|
| 26 |
+
```
|
| 27 |
+
User Request
|
| 28 |
+
|
|
| 29 |
+
v
|
| 30 |
+
+---------------------------------------+
|
| 31 |
+
| ORCHESTRATOR |
|
| 32 |
+
| +--------+ +--------+ +--------+ |
|
| 33 |
+
| |Planner |->| Coder |->|Reviewer| |
|
| 34 |
+
| +--------+ +--------+ +--------+ |
|
| 35 |
+
+---------------------------------------+
|
| 36 |
+
| |
|
| 37 |
+
v v
|
| 38 |
+
+---------+ +----------+
|
| 39 |
+
| Context | | E2B |
|
| 40 |
+
| Engine | | Sandbox |
|
| 41 |
+
+---------+ +----------+
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
1. **Planner Agent** - Searches codebase, understands context, creates implementation plan
|
| 45 |
+
2. **Coder Agent** - Writes code, uploads to sandbox, runs tests iteratively
|
| 46 |
+
3. **Reviewer Agent** - Reviews tested code, approves or requests changes
|
| 47 |
+
|
| 48 |
+
## Features
|
| 49 |
+
|
| 50 |
+
- **Autonomous coding** - Give it a task, it figures out the rest
|
| 51 |
+
- **Sandboxed execution** - Code runs in isolated E2B containers
|
| 52 |
+
- **Multi-agent architecture** - Specialized agents for planning, coding, reviewing
|
| 53 |
+
- **Codebase search** - Hybrid retrieval with BM25 + semantic search
|
| 54 |
+
- **Real-time feedback** - See what each agent is doing as it works
|
| 55 |
+
|
| 56 |
+
## Tech Stack
|
| 57 |
+
|
| 58 |
+
- **Python** - Core language
|
| 59 |
+
- **OpenAI GPT-4** - LLM for agent reasoning
|
| 60 |
+
- **LangChain/LangGraph** - Agent orchestration
|
| 61 |
+
- **E2B** - Sandboxed code execution
|
| 62 |
+
- **Chainlit** - Chat UI
|
| 63 |
+
|
| 64 |
+
## Environment Variables
|
| 65 |
+
|
| 66 |
+
| Variable | Description |
|
| 67 |
+
|----------|-------------|
|
| 68 |
+
| `OPENAI_API_KEY` | Your OpenAI API key |
|
| 69 |
+
| `E2B_API_KEY` | Your E2B sandbox API key |
|
| 70 |
+
|
| 71 |
+
## License
|
| 72 |
+
|
| 73 |
+
MIT
|
chainlit.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Welcome to Chainlit! 🚀🤖
|
| 2 |
+
|
| 3 |
+
Hi there, Developer! 👋 We're excited to have you on board. Chainlit is a powerful tool designed to help you prototype, debug and share applications built on top of LLMs.
|
| 4 |
+
|
| 5 |
+
## Useful Links 🔗
|
| 6 |
+
|
| 7 |
+
- **Documentation:** Get started with our comprehensive [Chainlit Documentation](https://docs.chainlit.io) 📚
|
| 8 |
+
- **Discord Community:** Join our friendly [Chainlit Discord](https://discord.gg/k73SQ3FyUh) to ask questions, share your projects, and connect with other developers! 💬
|
| 9 |
+
|
| 10 |
+
We can't wait to see what you create with Chainlit! Happy coding! 💻😊
|
| 11 |
+
|
| 12 |
+
## Welcome screen
|
| 13 |
+
|
| 14 |
+
To modify the welcome screen, edit the `chainlit.md` file at the root of your project. If you do not want a welcome screen, just leave this file empty.
|
chainlit_app.py
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Chainlit UI for CodePilot Multi-Agent System
|
| 3 |
+
|
| 4 |
+
This provides a chat interface showing detailed agent workflow:
|
| 5 |
+
- Planner creates implementation plans
|
| 6 |
+
- Coder writes code, uploads to sandbox, runs tests
|
| 7 |
+
- Reviewer checks and approves code
|
| 8 |
+
|
| 9 |
+
User can see every step in real-time.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import chainlit as cl
|
| 13 |
+
import os
|
| 14 |
+
import sys
|
| 15 |
+
import io
|
| 16 |
+
from contextlib import redirect_stdout, redirect_stderr
|
| 17 |
+
import asyncio
|
| 18 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 19 |
+
|
| 20 |
+
# Check if running in production BEFORE importing heavy dependencies
|
| 21 |
+
# Detects: Render, HuggingFace Spaces, or any cloud with PORT env var
|
| 22 |
+
IS_PRODUCTION = os.getenv('RENDER_SERVICE_NAME') or os.getenv('RENDER') or os.getenv('SPACE_ID') or os.getenv('PORT')
|
| 23 |
+
|
| 24 |
+
# Only import heavy ML dependencies in local development
|
| 25 |
+
if not IS_PRODUCTION:
|
| 26 |
+
from codepilot.tools.context_tools import index_codebase
|
| 27 |
+
|
| 28 |
+
# Import orchestrator (lighter weight)
|
| 29 |
+
from codepilot.agents.orchestrator import Orchestrator
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
# Authentication disabled for now - uncomment to enable password protection
|
| 33 |
+
# @cl.password_auth_callback
|
| 34 |
+
# def auth_callback(username: str, password: str):
|
| 35 |
+
# """
|
| 36 |
+
# Simple password authentication for CodePilot.
|
| 37 |
+
#
|
| 38 |
+
# For production, use environment variables and proper password hashing.
|
| 39 |
+
# """
|
| 40 |
+
# # Get password from environment variable (more secure)
|
| 41 |
+
# required_password = os.getenv('CHAINLIT_PASSWORD', 'codepilot2024')
|
| 42 |
+
#
|
| 43 |
+
# # In production, you should hash passwords and use a proper auth system
|
| 44 |
+
# if password == required_password:
|
| 45 |
+
# return cl.User(
|
| 46 |
+
# identifier=username,
|
| 47 |
+
# metadata={"role": "user", "provider": "credentials"}
|
| 48 |
+
# )
|
| 49 |
+
# return None
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
@cl.on_chat_start
|
| 53 |
+
async def start():
|
| 54 |
+
"""Initialize the agent system when chat starts."""
|
| 55 |
+
|
| 56 |
+
print("[CHAINLIT] on_chat_start triggered") # Debug log
|
| 57 |
+
|
| 58 |
+
await cl.Message(
|
| 59 |
+
content="# 🤖 CodePilot - Autonomous AI Coding Agent\n\n"
|
| 60 |
+
"I can help you write code, fix bugs, and implement features!\n\n"
|
| 61 |
+
"**How it works:**\n"
|
| 62 |
+
"1. 🤔 **Planner** - Searches codebase and creates implementation plan\n"
|
| 63 |
+
"2. 💻 **Coder** - Writes code locally, uploads to sandbox, runs tests\n"
|
| 64 |
+
"3. 👁️ **Reviewer** - Reviews tested code and decides approval\n\n"
|
| 65 |
+
"**What I can do:**\n"
|
| 66 |
+
"- Write new functions and features\n"
|
| 67 |
+
"- Fix bugs and add error handling\n"
|
| 68 |
+
"- Create tests and verify code works\n"
|
| 69 |
+
"- Search and understand your codebase\n\n"
|
| 70 |
+
"**Ready!** What would you like me to build?"
|
| 71 |
+
).send()
|
| 72 |
+
|
| 73 |
+
print("[CHAINLIT] Welcome message sent") # Debug log
|
| 74 |
+
|
| 75 |
+
# Skip indexing on deployment to avoid startup issues (using module-level constant)
|
| 76 |
+
if IS_PRODUCTION:
|
| 77 |
+
print(f"[CHAINLIT] Running in production mode (PORT={os.getenv('PORT')}) - skipping codebase indexing")
|
| 78 |
+
await cl.Message(content="ℹ️ Running in cloud mode - codebase indexing disabled").send()
|
| 79 |
+
cl.user_session.set("orchestrator", Orchestrator(max_iterations=3))
|
| 80 |
+
cl.user_session.set("ready", True)
|
| 81 |
+
print("[CHAINLIT] Orchestrator created, ready=True")
|
| 82 |
+
return
|
| 83 |
+
|
| 84 |
+
# Index codebase in background (only in local development)
|
| 85 |
+
index_msg = await cl.Message(content="🔍 Indexing codebase...").send()
|
| 86 |
+
|
| 87 |
+
try:
|
| 88 |
+
# Get project root
|
| 89 |
+
project_root = os.path.dirname(os.path.abspath(__file__))
|
| 90 |
+
index_result = index_codebase(project_root)
|
| 91 |
+
|
| 92 |
+
# Update message content
|
| 93 |
+
index_msg.content = f"✅ Codebase indexed!\n```\n{index_result}\n```"
|
| 94 |
+
await index_msg.update()
|
| 95 |
+
|
| 96 |
+
# Store orchestrator in session (reduced iterations to save API credits)
|
| 97 |
+
cl.user_session.set("orchestrator", Orchestrator(max_iterations=3))
|
| 98 |
+
cl.user_session.set("ready", True)
|
| 99 |
+
|
| 100 |
+
except Exception as e:
|
| 101 |
+
# Update message content
|
| 102 |
+
index_msg.content = f"⚠️ Indexing failed (will continue anyway):\n```\n{str(e)}\n```"
|
| 103 |
+
await index_msg.update()
|
| 104 |
+
# Still create orchestrator even if indexing fails
|
| 105 |
+
cl.user_session.set("orchestrator", Orchestrator(max_iterations=10))
|
| 106 |
+
cl.user_session.set("ready", True)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
@cl.on_message
|
| 110 |
+
async def main(message: cl.Message):
|
| 111 |
+
"""Handle user messages and run the agent workflow."""
|
| 112 |
+
|
| 113 |
+
# Check if ready
|
| 114 |
+
if not cl.user_session.get("ready"):
|
| 115 |
+
await cl.Message(content="⚠️ System is still initializing, please wait...").send()
|
| 116 |
+
return
|
| 117 |
+
|
| 118 |
+
# Get orchestrator
|
| 119 |
+
orchestrator: Orchestrator = cl.user_session.get("orchestrator")
|
| 120 |
+
|
| 121 |
+
# Create a message for streaming logs
|
| 122 |
+
log_msg = cl.Message(content="")
|
| 123 |
+
await log_msg.send()
|
| 124 |
+
|
| 125 |
+
try:
|
| 126 |
+
# Capture stdout/stderr to stream logs
|
| 127 |
+
captured_output = io.StringIO()
|
| 128 |
+
|
| 129 |
+
def run_orchestrator():
|
| 130 |
+
"""Run orchestrator in thread and capture output."""
|
| 131 |
+
try:
|
| 132 |
+
with redirect_stdout(captured_output), redirect_stderr(captured_output):
|
| 133 |
+
return orchestrator.run(message.content)
|
| 134 |
+
except Exception as e:
|
| 135 |
+
# Capture any exceptions from orchestrator
|
| 136 |
+
print(f"❌ Error in orchestrator: {str(e)}")
|
| 137 |
+
import traceback
|
| 138 |
+
traceback.print_exc()
|
| 139 |
+
raise
|
| 140 |
+
|
| 141 |
+
# Run in thread pool to avoid blocking
|
| 142 |
+
loop = asyncio.get_event_loop()
|
| 143 |
+
executor = ThreadPoolExecutor(max_workers=1)
|
| 144 |
+
|
| 145 |
+
# Start the orchestrator in background
|
| 146 |
+
future = loop.run_in_executor(executor, run_orchestrator)
|
| 147 |
+
|
| 148 |
+
# Track API usage
|
| 149 |
+
total_prompt_tokens = 0
|
| 150 |
+
total_completion_tokens = 0
|
| 151 |
+
total_tokens = 0
|
| 152 |
+
seen_token_lines = set() # Track which token lines we've already counted
|
| 153 |
+
|
| 154 |
+
# Stream logs while orchestrator is running - FILTERED
|
| 155 |
+
accumulated_logs = ""
|
| 156 |
+
while not future.done():
|
| 157 |
+
await asyncio.sleep(0.5) # Check every 500ms
|
| 158 |
+
|
| 159 |
+
# Get new output
|
| 160 |
+
current_output = captured_output.getvalue()
|
| 161 |
+
if current_output != accumulated_logs:
|
| 162 |
+
accumulated_logs = current_output
|
| 163 |
+
|
| 164 |
+
# Filter logs to show only important lines
|
| 165 |
+
filtered_lines = []
|
| 166 |
+
for line in accumulated_logs.split('\n'):
|
| 167 |
+
# Extract token usage before filtering (only count each line once!)
|
| 168 |
+
if '📊 Tokens:' in line and line not in seen_token_lines:
|
| 169 |
+
seen_token_lines.add(line) # Mark as counted
|
| 170 |
+
try:
|
| 171 |
+
# Parse: "📊 Tokens: 505 prompt + 20 completion = 525 total"
|
| 172 |
+
parts = line.split('Tokens:')[1].strip()
|
| 173 |
+
prompt = int(parts.split('prompt')[0].strip())
|
| 174 |
+
completion = int(parts.split('+')[1].split('completion')[0].strip())
|
| 175 |
+
total_prompt_tokens += prompt
|
| 176 |
+
total_completion_tokens += completion
|
| 177 |
+
total_tokens += (prompt + completion)
|
| 178 |
+
except:
|
| 179 |
+
pass
|
| 180 |
+
|
| 181 |
+
# Skip token counts, progress bars, and verbose details
|
| 182 |
+
if any(skip in line for skip in ['📊 Tokens:', 'Batches:', '|##', 'it/s]']):
|
| 183 |
+
continue
|
| 184 |
+
# Keep important lines
|
| 185 |
+
if any(keep in line for keep in [
|
| 186 |
+
'[ORCHESTRATOR]', '[PLANNER]', '[CODER]', '[REVIEWER]',
|
| 187 |
+
'Calling tool:', '✅ Tool', 'Transitioning', 'APPROVED', 'REJECTED'
|
| 188 |
+
]):
|
| 189 |
+
filtered_lines.append(line)
|
| 190 |
+
|
| 191 |
+
filtered_output = '\n'.join(filtered_lines)
|
| 192 |
+
|
| 193 |
+
# Calculate cost (GPT-3.5-turbo pricing: $0.0015/1K input, $0.002/1K output)
|
| 194 |
+
input_cost = (total_prompt_tokens / 1000) * 0.0015
|
| 195 |
+
output_cost = (total_completion_tokens / 1000) * 0.002
|
| 196 |
+
total_cost = input_cost + output_cost
|
| 197 |
+
|
| 198 |
+
# Add usage summary to logs
|
| 199 |
+
usage_summary = f"\n\n💰 CREDITS USED:\n"
|
| 200 |
+
usage_summary += f" Input: {total_prompt_tokens:,} tokens (${input_cost:.4f})\n"
|
| 201 |
+
usage_summary += f" Output: {total_completion_tokens:,} tokens (${output_cost:.4f})\n"
|
| 202 |
+
usage_summary += f" Total: {total_tokens:,} tokens (${total_cost:.4f})"
|
| 203 |
+
|
| 204 |
+
# Update message with filtered logs + usage
|
| 205 |
+
log_msg.content = f"```\n{filtered_output}\n{usage_summary}\n```"
|
| 206 |
+
await log_msg.update()
|
| 207 |
+
|
| 208 |
+
# Get final result
|
| 209 |
+
result = await future
|
| 210 |
+
|
| 211 |
+
# Get final logs
|
| 212 |
+
final_logs = captured_output.getvalue()
|
| 213 |
+
|
| 214 |
+
# Update with final logs
|
| 215 |
+
log_msg.content = f"## 📋 Execution Log\n```\n{final_logs}\n```"
|
| 216 |
+
await log_msg.update()
|
| 217 |
+
|
| 218 |
+
# Send results summary
|
| 219 |
+
summary_lines = []
|
| 220 |
+
|
| 221 |
+
if result.get('plan'):
|
| 222 |
+
summary_lines.append("## 🤔 Planner")
|
| 223 |
+
summary_lines.append(f"✅ Plan created ({len(result['plan'])} chars)\n")
|
| 224 |
+
|
| 225 |
+
if result.get('code_changes'):
|
| 226 |
+
summary_lines.append("## 💻 Coder")
|
| 227 |
+
summary_lines.append(f"✅ Created {len(result['code_changes'])} file(s):")
|
| 228 |
+
for file_path in result['code_changes'].keys():
|
| 229 |
+
summary_lines.append(f" - {file_path}")
|
| 230 |
+
summary_lines.append("")
|
| 231 |
+
|
| 232 |
+
if result.get('review_feedback'):
|
| 233 |
+
summary_lines.append("## 👁️ Reviewer")
|
| 234 |
+
if result.get('success'):
|
| 235 |
+
summary_lines.append("✅ Code approved")
|
| 236 |
+
else:
|
| 237 |
+
summary_lines.append("⚠️ Needs revision")
|
| 238 |
+
summary_lines.append("")
|
| 239 |
+
|
| 240 |
+
summary_lines.append("## 🎯 Result")
|
| 241 |
+
if result.get('success'):
|
| 242 |
+
summary_lines.append(f"✅ **Success** (Iterations: {result.get('iterations', 'N/A')})")
|
| 243 |
+
else:
|
| 244 |
+
summary_lines.append(f"⚠️ **Incomplete** (Iterations: {result.get('iterations', 'N/A')})")
|
| 245 |
+
|
| 246 |
+
# Add final cost summary
|
| 247 |
+
summary_lines.append("\n## 💰 API Credits Used (GPT-3.5-Turbo)")
|
| 248 |
+
summary_lines.append(f"**Total Tokens:** {total_tokens:,}")
|
| 249 |
+
summary_lines.append(f"- Input: {total_prompt_tokens:,} tokens (${(total_prompt_tokens/1000)*0.0015:.4f})")
|
| 250 |
+
summary_lines.append(f"- Output: {total_completion_tokens:,} tokens (${(total_completion_tokens/1000)*0.002:.4f})")
|
| 251 |
+
summary_lines.append(f"\n**Estimated Cost:** ${total_cost:.4f}")
|
| 252 |
+
|
| 253 |
+
await cl.Message(content="\n".join(summary_lines)).send()
|
| 254 |
+
|
| 255 |
+
except Exception as e:
|
| 256 |
+
# Determine error type and provide specific guidance
|
| 257 |
+
error_message = str(e)
|
| 258 |
+
error_type = type(e).__name__
|
| 259 |
+
|
| 260 |
+
if "rate_limit" in error_message.lower() or "429" in error_message:
|
| 261 |
+
user_message = f"""## ⏱️ Rate Limit Reached
|
| 262 |
+
|
| 263 |
+
OpenAI API rate limit exceeded. This happens when too many requests are made in a short time.
|
| 264 |
+
|
| 265 |
+
**What to do:**
|
| 266 |
+
- Wait a few minutes and try again
|
| 267 |
+
- Reduce max_iterations (currently: {orchestrator.max_iterations})
|
| 268 |
+
- Your request will work once the rate limit resets
|
| 269 |
+
|
| 270 |
+
**Error details:**
|
| 271 |
+
```
|
| 272 |
+
{error_message}
|
| 273 |
+
```
|
| 274 |
+
"""
|
| 275 |
+
elif "insufficient_quota" in error_message.lower():
|
| 276 |
+
user_message = f"""## 💳 API Credits Exhausted
|
| 277 |
+
|
| 278 |
+
Your OpenAI API credits have been exhausted.
|
| 279 |
+
|
| 280 |
+
**What to do:**
|
| 281 |
+
- Add credits to your OpenAI account at https://platform.openai.com/account/billing
|
| 282 |
+
- Check your usage at https://platform.openai.com/usage
|
| 283 |
+
- Current model: GPT-3.5-turbo (~$0.02 per task)
|
| 284 |
+
|
| 285 |
+
**Error details:**
|
| 286 |
+
```
|
| 287 |
+
{error_message}
|
| 288 |
+
```
|
| 289 |
+
"""
|
| 290 |
+
elif "api_key" in error_message.lower() or "authentication" in error_message.lower():
|
| 291 |
+
user_message = f"""## 🔑 API Key Error
|
| 292 |
+
|
| 293 |
+
There's an issue with your OpenAI API key.
|
| 294 |
+
|
| 295 |
+
**What to do:**
|
| 296 |
+
- Verify your OPENAI_API_KEY in .env file
|
| 297 |
+
- Check that the key is valid at https://platform.openai.com/api-keys
|
| 298 |
+
- Restart the application after updating .env
|
| 299 |
+
|
| 300 |
+
**Error details:**
|
| 301 |
+
```
|
| 302 |
+
{error_message}
|
| 303 |
+
```
|
| 304 |
+
"""
|
| 305 |
+
elif "timeout" in error_message.lower():
|
| 306 |
+
user_message = f"""## ⏰ Request Timeout
|
| 307 |
+
|
| 308 |
+
The operation took too long and timed out.
|
| 309 |
+
|
| 310 |
+
**What to do:**
|
| 311 |
+
- Try again with a simpler task
|
| 312 |
+
- The task may be too complex for one iteration
|
| 313 |
+
- Consider breaking it into smaller steps
|
| 314 |
+
|
| 315 |
+
**Error details:**
|
| 316 |
+
```
|
| 317 |
+
{error_message}
|
| 318 |
+
```
|
| 319 |
+
"""
|
| 320 |
+
else:
|
| 321 |
+
# Generic error with helpful context
|
| 322 |
+
user_message = f"""## ❌ Error Occurred
|
| 323 |
+
|
| 324 |
+
An unexpected error occurred during execution.
|
| 325 |
+
|
| 326 |
+
**Error type:** {error_type}
|
| 327 |
+
|
| 328 |
+
**What to do:**
|
| 329 |
+
- Try rephrasing your request
|
| 330 |
+
- Check if all required files/dependencies exist
|
| 331 |
+
- Verify your .env file has all required API keys
|
| 332 |
+
|
| 333 |
+
**Error details:**
|
| 334 |
+
```
|
| 335 |
+
{error_message}
|
| 336 |
+
```
|
| 337 |
+
|
| 338 |
+
If this persists, please report the issue with the error details above.
|
| 339 |
+
"""
|
| 340 |
+
|
| 341 |
+
await cl.Message(content=user_message).send()
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
if __name__ == "__main__":
|
| 345 |
+
import sys
|
| 346 |
+
sys.exit("Run with: chainlit run chainlit_app.py")
|
codepilot/__init__.py
ADDED
|
File without changes
|
codepilot/agents/__init__.py
ADDED
|
File without changes
|
codepilot/agents/base_agent.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Base Agent
|
| 3 |
+
The main agent loop that orchestrates LLM calls and tool execution
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
from codepilot.llm.client import OpenAIClient
|
| 8 |
+
from codepilot.agents.conversation import ConversationManager
|
| 9 |
+
from codepilot.tools.registry import get_tools, get_tool_function
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class Agent:
|
| 13 |
+
"""Main agent that executes tasks using LLM and tools"""
|
| 14 |
+
|
| 15 |
+
def __init__(self, model: str = "gpt-3.5-turbo", max_iterations: int = 10):
|
| 16 |
+
"""
|
| 17 |
+
Initialize the agent
|
| 18 |
+
|
| 19 |
+
Args:
|
| 20 |
+
model: OpenAI model to use
|
| 21 |
+
max_iterations: Maximum number of LLM calls to prevent infinite loops
|
| 22 |
+
"""
|
| 23 |
+
print("🚀 Initializing Agent...")
|
| 24 |
+
|
| 25 |
+
# Initialize components
|
| 26 |
+
self.client = OpenAIClient(model=model)
|
| 27 |
+
self.conversation = ConversationManager()
|
| 28 |
+
self.tools = get_tools()
|
| 29 |
+
self.max_iterations = max_iterations
|
| 30 |
+
|
| 31 |
+
print(f"✅ Agent ready with {len(self.tools)} tools")
|
| 32 |
+
print(f" Max iterations: {max_iterations}\n")
|
| 33 |
+
|
| 34 |
+
def run(self, user_prompt: str) -> str:
|
| 35 |
+
"""
|
| 36 |
+
Run the agent with a user prompt
|
| 37 |
+
|
| 38 |
+
Args:
|
| 39 |
+
user_prompt: The user's request
|
| 40 |
+
|
| 41 |
+
Returns:
|
| 42 |
+
Final response from the agent
|
| 43 |
+
"""
|
| 44 |
+
print("=" * 60)
|
| 45 |
+
print("🤖 AGENT STARTING")
|
| 46 |
+
print("=" * 60)
|
| 47 |
+
|
| 48 |
+
# Add user message to conversation
|
| 49 |
+
self.conversation.add_user_message(user_prompt)
|
| 50 |
+
|
| 51 |
+
# Main agent loop
|
| 52 |
+
for iteration in range(1, self.max_iterations + 1):
|
| 53 |
+
print(f"\n--- Iteration {iteration}/{self.max_iterations} ---")
|
| 54 |
+
|
| 55 |
+
# Call OpenAI with current conversation and tools
|
| 56 |
+
response = self.client.chat(
|
| 57 |
+
messages=self.conversation.get_messages(),
|
| 58 |
+
tools=self.tools
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
# Get the assistant's response
|
| 62 |
+
message = response.choices[0].message
|
| 63 |
+
finish_reason = response.choices[0].finish_reason
|
| 64 |
+
|
| 65 |
+
print(f"🎯 Finish reason: {finish_reason}")
|
| 66 |
+
|
| 67 |
+
# Check what the assistant wants to do
|
| 68 |
+
if finish_reason == "stop":
|
| 69 |
+
# Assistant is done, has a text response
|
| 70 |
+
final_response = message.content
|
| 71 |
+
self.conversation.add_assistant_message(final_response)
|
| 72 |
+
|
| 73 |
+
print("\n" + "=" * 60)
|
| 74 |
+
print("✅ AGENT COMPLETE")
|
| 75 |
+
print("=" * 60)
|
| 76 |
+
|
| 77 |
+
return final_response
|
| 78 |
+
|
| 79 |
+
elif finish_reason == "tool_calls":
|
| 80 |
+
# Assistant wants to use tools
|
| 81 |
+
tool_calls = message.tool_calls
|
| 82 |
+
|
| 83 |
+
# Add the assistant's tool calls to conversation
|
| 84 |
+
self.conversation.add_assistant_tool_calls(tool_calls)
|
| 85 |
+
|
| 86 |
+
# Execute each tool call
|
| 87 |
+
for tool_call in tool_calls:
|
| 88 |
+
self._execute_tool_call(tool_call)
|
| 89 |
+
|
| 90 |
+
# Continue loop - send results back to OpenAI
|
| 91 |
+
continue
|
| 92 |
+
|
| 93 |
+
else:
|
| 94 |
+
# Unexpected finish reason
|
| 95 |
+
error_msg = f"Unexpected finish_reason: {finish_reason}"
|
| 96 |
+
print(f"⚠️ {error_msg}")
|
| 97 |
+
return error_msg
|
| 98 |
+
|
| 99 |
+
# Max iterations reached
|
| 100 |
+
max_iter_msg = f"⚠️ Reached maximum iterations ({self.max_iterations})"
|
| 101 |
+
print(f"\n{max_iter_msg}")
|
| 102 |
+
return max_iter_msg
|
| 103 |
+
|
| 104 |
+
def _execute_tool_call(self, tool_call):
|
| 105 |
+
"""
|
| 106 |
+
Execute a single tool call
|
| 107 |
+
|
| 108 |
+
Args:
|
| 109 |
+
tool_call: Tool call object from OpenAI response
|
| 110 |
+
"""
|
| 111 |
+
tool_id = tool_call.id
|
| 112 |
+
tool_name = tool_call.function.name
|
| 113 |
+
tool_args_json = tool_call.function.arguments
|
| 114 |
+
|
| 115 |
+
print(f"\n🔧 Executing tool: {tool_name}")
|
| 116 |
+
print(f" ID: {tool_id}")
|
| 117 |
+
print(f" Arguments: {tool_args_json}")
|
| 118 |
+
|
| 119 |
+
try:
|
| 120 |
+
# Parse arguments from JSON string
|
| 121 |
+
tool_args = json.loads(tool_args_json)
|
| 122 |
+
|
| 123 |
+
# Get the tool function
|
| 124 |
+
tool_function = get_tool_function(tool_name)
|
| 125 |
+
|
| 126 |
+
if tool_function is None:
|
| 127 |
+
result = f"Error: Tool '{tool_name}' not found in registry"
|
| 128 |
+
print(f"❌ {result}")
|
| 129 |
+
else:
|
| 130 |
+
# Execute the tool
|
| 131 |
+
result = tool_function(**tool_args)
|
| 132 |
+
|
| 133 |
+
# Add result to conversation
|
| 134 |
+
self.conversation.add_tool_result(
|
| 135 |
+
tool_call_id=tool_id,
|
| 136 |
+
tool_name=tool_name,
|
| 137 |
+
result=result
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
except json.JSONDecodeError as e:
|
| 141 |
+
error_msg = f"Error parsing tool arguments: {e}"
|
| 142 |
+
print(f"❌ {error_msg}")
|
| 143 |
+
self.conversation.add_tool_result(
|
| 144 |
+
tool_call_id=tool_id,
|
| 145 |
+
tool_name=tool_name,
|
| 146 |
+
result=error_msg
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
except Exception as e:
|
| 150 |
+
error_msg = f"Error executing tool: {str(e)}"
|
| 151 |
+
print(f"❌ {error_msg}")
|
| 152 |
+
self.conversation.add_tool_result(
|
| 153 |
+
tool_call_id=tool_id,
|
| 154 |
+
tool_name=tool_name,
|
| 155 |
+
result=error_msg
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
def reset(self):
|
| 159 |
+
"""Reset the agent's conversation history"""
|
| 160 |
+
self.conversation.clear()
|
| 161 |
+
print("🔄 Agent conversation reset")
|
codepilot/agents/coder_agent.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Coder Agent - Implements code based on plans
|
| 3 |
+
|
| 4 |
+
The Coder's job:
|
| 5 |
+
1. Read the plan from Planner
|
| 6 |
+
2. Search/read existing code to understand it
|
| 7 |
+
3. Write code changes to implement the plan
|
| 8 |
+
4. Follow best practices and coding standards
|
| 9 |
+
|
| 10 |
+
Tools it has access to:
|
| 11 |
+
- search_codebase (find relevant files)
|
| 12 |
+
- read_file (understand existing code)
|
| 13 |
+
- write_file (implement changes)
|
| 14 |
+
- list_files (explore structure)
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
from codepilot.llm.client import OpenAIClient
|
| 18 |
+
from codepilot.tools.registry import get_tools, get_tool_function
|
| 19 |
+
from codepilot.agents.conversation import ConversationManager
|
| 20 |
+
from typing import Dict, Any
|
| 21 |
+
import json
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# Coder's specialized system prompt
|
| 25 |
+
CODER_SYSTEM_PROMPT = """You are an expert software engineer and implementation specialist.
|
| 26 |
+
|
| 27 |
+
Your ONLY job is to write code that implements the given plan. You do NOT create plans yourself.
|
| 28 |
+
|
| 29 |
+
When given a plan:
|
| 30 |
+
1. Read and understand each step carefully
|
| 31 |
+
2. Search the codebase to find relevant files
|
| 32 |
+
3. Read existing files to understand the current implementation
|
| 33 |
+
4. Write clean, well-structured code that follows the plan
|
| 34 |
+
5. Make incremental changes, one step at a time
|
| 35 |
+
|
| 36 |
+
Your code should be:
|
| 37 |
+
- Clean and readable (follow existing code style)
|
| 38 |
+
- Well-tested (add error handling)
|
| 39 |
+
- Documented (add comments for complex logic)
|
| 40 |
+
- Minimal (only change what's necessary)
|
| 41 |
+
|
| 42 |
+
IMPORTANT RULES:
|
| 43 |
+
- Follow the plan exactly - don't add extra features
|
| 44 |
+
- Match the existing code style in each file
|
| 45 |
+
- Test your changes mentally before writing
|
| 46 |
+
- If you need clarification on the plan, state what's unclear
|
| 47 |
+
|
| 48 |
+
Tools available to you:
|
| 49 |
+
- search_codebase: Find existing code
|
| 50 |
+
- read_file: Understand current implementation
|
| 51 |
+
- write_file: Create or modify files
|
| 52 |
+
- list_files: Explore directory structure
|
| 53 |
+
- upload_to_sandbox: Upload files to isolated testing environment
|
| 54 |
+
- run_command_in_sandbox: Run commands safely in sandbox (e.g., pytest, python test.py)
|
| 55 |
+
- execute_in_sandbox: Execute Python code snippets for quick testing
|
| 56 |
+
|
| 57 |
+
IMPORTANT: Always test your code in the sandbox before submitting!
|
| 58 |
+
1. Write the file locally (write_file)
|
| 59 |
+
2. Upload to sandbox (upload_to_sandbox)
|
| 60 |
+
3. Run tests in sandbox (run_command_in_sandbox)
|
| 61 |
+
4. Fix any issues before marking as complete
|
| 62 |
+
"""
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class CoderAgent:
|
| 66 |
+
"""
|
| 67 |
+
Coder Agent - Implements code based on plans.
|
| 68 |
+
|
| 69 |
+
This agent is specialized for coding. It has:
|
| 70 |
+
- Custom system prompt (engineer mindset)
|
| 71 |
+
- Write access tools (can modify files)
|
| 72 |
+
- Single responsibility (implementation only)
|
| 73 |
+
"""
|
| 74 |
+
|
| 75 |
+
def __init__(self, model: str = "gpt-3.5-turbo"):
|
| 76 |
+
"""
|
| 77 |
+
Initialize Coder agent.
|
| 78 |
+
|
| 79 |
+
Args:
|
| 80 |
+
model: LLM model to use
|
| 81 |
+
"""
|
| 82 |
+
self.client = OpenAIClient(model=model)
|
| 83 |
+
self.conversation = ConversationManager()
|
| 84 |
+
|
| 85 |
+
# Coder gets read + write tools + sandbox execution (safe testing)
|
| 86 |
+
self.allowed_tools = [
|
| 87 |
+
"search_codebase",
|
| 88 |
+
"read_file",
|
| 89 |
+
"write_file",
|
| 90 |
+
"list_files",
|
| 91 |
+
"upload_to_sandbox",
|
| 92 |
+
"run_command_in_sandbox",
|
| 93 |
+
"execute_in_sandbox"
|
| 94 |
+
]
|
| 95 |
+
|
| 96 |
+
def run(self, plan: str, task: str, review_feedback: str = None) -> Dict[str, str]:
|
| 97 |
+
"""
|
| 98 |
+
Implement the given plan.
|
| 99 |
+
|
| 100 |
+
Args:
|
| 101 |
+
plan: Implementation plan from Planner
|
| 102 |
+
task: Original task description (for context)
|
| 103 |
+
review_feedback: Optional feedback from Reviewer if code was rejected
|
| 104 |
+
|
| 105 |
+
Returns:
|
| 106 |
+
Dictionary mapping file paths to their new content
|
| 107 |
+
"""
|
| 108 |
+
# Reset conversation
|
| 109 |
+
self.conversation = ConversationManager()
|
| 110 |
+
|
| 111 |
+
# Add system prompt
|
| 112 |
+
self.conversation.add_message("system", CODER_SYSTEM_PROMPT)
|
| 113 |
+
|
| 114 |
+
# Build user prompt with task, plan, and optionally review feedback
|
| 115 |
+
user_prompt = f"""Original Task: {task}
|
| 116 |
+
|
| 117 |
+
Implementation Plan:
|
| 118 |
+
{plan}"""
|
| 119 |
+
|
| 120 |
+
# If this is a rework (Reviewer rejected the code), include feedback
|
| 121 |
+
if review_feedback:
|
| 122 |
+
user_prompt += f"""
|
| 123 |
+
|
| 124 |
+
IMPORTANT - REVIEWER FEEDBACK (CODE WAS REJECTED):
|
| 125 |
+
{review_feedback}
|
| 126 |
+
|
| 127 |
+
Please fix the issues mentioned by the Reviewer and resubmit the code."""
|
| 128 |
+
else:
|
| 129 |
+
user_prompt += """
|
| 130 |
+
|
| 131 |
+
Please implement this plan step by step. Write clean, well-structured code that follows the plan."""
|
| 132 |
+
|
| 133 |
+
self.conversation.add_message("user", user_prompt)
|
| 134 |
+
|
| 135 |
+
# Get only the tools this agent is allowed to use
|
| 136 |
+
all_tools = get_tools()
|
| 137 |
+
coder_tools = [
|
| 138 |
+
tool for tool in all_tools
|
| 139 |
+
if tool['function']['name'] in self.allowed_tools
|
| 140 |
+
]
|
| 141 |
+
|
| 142 |
+
# Track which files were modified
|
| 143 |
+
modified_files = {}
|
| 144 |
+
|
| 145 |
+
# Run coding loop (agent reads code, writes changes)
|
| 146 |
+
max_iterations = 15 # Coder might need more iterations than planner
|
| 147 |
+
for iteration in range(max_iterations):
|
| 148 |
+
# Call LLM
|
| 149 |
+
response = self.client.chat(
|
| 150 |
+
messages=self.conversation.get_messages(),
|
| 151 |
+
tools=coder_tools
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
finish_reason = response.choices[0].finish_reason
|
| 155 |
+
message = response.choices[0].message
|
| 156 |
+
|
| 157 |
+
# Add assistant response to conversation
|
| 158 |
+
self.conversation.add_message(
|
| 159 |
+
role="assistant",
|
| 160 |
+
content=message.content,
|
| 161 |
+
tool_calls=message.tool_calls
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
# Check if done
|
| 165 |
+
if finish_reason == "stop":
|
| 166 |
+
# Agent finished coding
|
| 167 |
+
print(f"[CODER] Finished implementation")
|
| 168 |
+
return modified_files
|
| 169 |
+
|
| 170 |
+
# Execute tool calls
|
| 171 |
+
if finish_reason == "tool_calls":
|
| 172 |
+
for tool_call in message.tool_calls:
|
| 173 |
+
tool_name = tool_call.function.name
|
| 174 |
+
tool_args = json.loads(tool_call.function.arguments)
|
| 175 |
+
|
| 176 |
+
print(f"[CODER] Calling tool: {tool_name}({tool_args})")
|
| 177 |
+
|
| 178 |
+
# Execute tool
|
| 179 |
+
tool_func = get_tool_function(tool_name)
|
| 180 |
+
if tool_func:
|
| 181 |
+
result = tool_func(**tool_args)
|
| 182 |
+
|
| 183 |
+
# Track file modifications
|
| 184 |
+
if tool_name == "write_file" and "path" in tool_args:
|
| 185 |
+
modified_files[tool_args["path"]] = tool_args.get("content", "")
|
| 186 |
+
else:
|
| 187 |
+
result = f"Error: Tool {tool_name} not found"
|
| 188 |
+
|
| 189 |
+
# Add tool result to conversation
|
| 190 |
+
self.conversation.add_tool_result(
|
| 191 |
+
tool_call_id=tool_call.id,
|
| 192 |
+
tool_name=tool_name,
|
| 193 |
+
result=str(result)
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
# If we hit max iterations, return what we have
|
| 197 |
+
print(f"[CODER] Warning: Hit max iterations ({max_iterations})")
|
| 198 |
+
return modified_files
|
| 199 |
+
|
| 200 |
+
def get_tool_access(self) -> list:
|
| 201 |
+
"""Return list of tools this agent can access."""
|
| 202 |
+
return self.allowed_tools
|
codepilot/agents/conversation.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Conversation Manager
|
| 3 |
+
Handles conversation history in OpenAI's message format
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from typing import List, Dict, Any
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class ConversationManager:
|
| 10 |
+
"""Manages conversation history for the agent"""
|
| 11 |
+
|
| 12 |
+
def __init__(self):
|
| 13 |
+
"""Initialize with empty message history"""
|
| 14 |
+
self.messages: List[Dict[str, Any]] = []
|
| 15 |
+
|
| 16 |
+
def add_message(self, role: str, content: str, tool_calls=None):
|
| 17 |
+
"""
|
| 18 |
+
Generic method to add any message to conversation.
|
| 19 |
+
|
| 20 |
+
Args:
|
| 21 |
+
role: Message role ("system", "user", "assistant", "tool")
|
| 22 |
+
content: Message content
|
| 23 |
+
tool_calls: Optional tool calls for assistant messages
|
| 24 |
+
"""
|
| 25 |
+
message = {"role": role, "content": content}
|
| 26 |
+
if tool_calls:
|
| 27 |
+
message["tool_calls"] = tool_calls
|
| 28 |
+
self.messages.append(message)
|
| 29 |
+
|
| 30 |
+
def add_user_message(self, content: str):
|
| 31 |
+
"""
|
| 32 |
+
Add a user message to the conversation
|
| 33 |
+
|
| 34 |
+
Args:
|
| 35 |
+
content: The user's message text
|
| 36 |
+
"""
|
| 37 |
+
self.messages.append({
|
| 38 |
+
"role": "user",
|
| 39 |
+
"content": content
|
| 40 |
+
})
|
| 41 |
+
print(f"👤 User: {content[:100]}..." if len(content) > 100 else f"👤 User: {content}")
|
| 42 |
+
|
| 43 |
+
def add_assistant_message(self, content: str):
|
| 44 |
+
"""
|
| 45 |
+
Add an assistant text response to the conversation
|
| 46 |
+
|
| 47 |
+
Args:
|
| 48 |
+
content: The assistant's response text
|
| 49 |
+
"""
|
| 50 |
+
self.messages.append({
|
| 51 |
+
"role": "assistant",
|
| 52 |
+
"content": content
|
| 53 |
+
})
|
| 54 |
+
print(f"🤖 Assistant: {content[:100]}..." if len(content) > 100 else f"🤖 Assistant: {content}")
|
| 55 |
+
|
| 56 |
+
def add_assistant_tool_calls(self, tool_calls: List[Any]):
|
| 57 |
+
"""
|
| 58 |
+
Add an assistant message with tool calls
|
| 59 |
+
|
| 60 |
+
Args:
|
| 61 |
+
tool_calls: List of tool call objects from OpenAI response
|
| 62 |
+
"""
|
| 63 |
+
# Extract tool call info for logging
|
| 64 |
+
tool_names = [tc.function.name for tc in tool_calls]
|
| 65 |
+
print(f"🔧 Assistant calling tools: {tool_names}")
|
| 66 |
+
|
| 67 |
+
# OpenAI requires this specific format
|
| 68 |
+
self.messages.append({
|
| 69 |
+
"role": "assistant",
|
| 70 |
+
"content": None, # No text content when making tool calls
|
| 71 |
+
"tool_calls": [
|
| 72 |
+
{
|
| 73 |
+
"id": tc.id,
|
| 74 |
+
"type": "function",
|
| 75 |
+
"function": {
|
| 76 |
+
"name": tc.function.name,
|
| 77 |
+
"arguments": tc.function.arguments
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
for tc in tool_calls
|
| 81 |
+
]
|
| 82 |
+
})
|
| 83 |
+
|
| 84 |
+
def add_tool_result(self, tool_call_id: str, tool_name: str, result: str):
|
| 85 |
+
"""
|
| 86 |
+
Add a tool execution result to the conversation
|
| 87 |
+
|
| 88 |
+
Args:
|
| 89 |
+
tool_call_id: The ID of the tool call (from OpenAI)
|
| 90 |
+
tool_name: Name of the tool that was executed
|
| 91 |
+
result: The result string from the tool
|
| 92 |
+
"""
|
| 93 |
+
self.messages.append({
|
| 94 |
+
"role": "tool",
|
| 95 |
+
"tool_call_id": tool_call_id,
|
| 96 |
+
"name": tool_name,
|
| 97 |
+
"content": result
|
| 98 |
+
})
|
| 99 |
+
# Truncate long results for logging
|
| 100 |
+
result_preview = result[:100] + "..." if len(result) > 100 else result
|
| 101 |
+
print(f"✅ Tool {tool_name} result: {result_preview}")
|
| 102 |
+
|
| 103 |
+
def get_messages(self) -> List[Dict[str, Any]]:
|
| 104 |
+
"""
|
| 105 |
+
Get the full conversation history
|
| 106 |
+
|
| 107 |
+
Returns:
|
| 108 |
+
List of message dictionaries
|
| 109 |
+
"""
|
| 110 |
+
return self.messages
|
| 111 |
+
|
| 112 |
+
def clear(self):
|
| 113 |
+
"""Clear all messages from history"""
|
| 114 |
+
self.messages = []
|
| 115 |
+
print("🗑️ Conversation cleared")
|
| 116 |
+
|
| 117 |
+
def get_message_count(self) -> int:
|
| 118 |
+
"""
|
| 119 |
+
Get the number of messages in the conversation
|
| 120 |
+
|
| 121 |
+
Returns:
|
| 122 |
+
Number of messages
|
| 123 |
+
"""
|
| 124 |
+
return len(self.messages)
|
| 125 |
+
|
| 126 |
+
def print_summary(self):
|
| 127 |
+
"""Print a summary of the conversation"""
|
| 128 |
+
print(f"\n📊 Conversation Summary:")
|
| 129 |
+
print(f" Total messages: {len(self.messages)}")
|
| 130 |
+
|
| 131 |
+
role_counts = {}
|
| 132 |
+
for msg in self.messages:
|
| 133 |
+
role = msg.get("role", "unknown")
|
| 134 |
+
role_counts[role] = role_counts.get(role, 0) + 1
|
| 135 |
+
|
| 136 |
+
for role, count in role_counts.items():
|
| 137 |
+
print(f" {role}: {count}")
|
codepilot/agents/orchestrator.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Orchestrator - Manages multi-agent workflow
|
| 3 |
+
|
| 4 |
+
The orchestrator is the "brain" that:
|
| 5 |
+
1. Tracks current state (planning, coding, reviewing, etc.)
|
| 6 |
+
2. Decides which agent to call next
|
| 7 |
+
3. Manages communication between agents
|
| 8 |
+
4. Handles the overall task flow
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from enum import Enum
|
| 12 |
+
from typing import Dict, Any, Optional
|
| 13 |
+
from dataclasses import dataclass
|
| 14 |
+
from codepilot.agents.planner_agent import PlannerAgent
|
| 15 |
+
from codepilot.agents.coder_agent import CoderAgent
|
| 16 |
+
from codepilot.agents.reviewer_agent import ReviewerAgent
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class AgentState(Enum):
|
| 20 |
+
"""Possible states in the multi-agent workflow"""
|
| 21 |
+
PLANNING = "planning"
|
| 22 |
+
CODING = "coding"
|
| 23 |
+
REVIEWING = "reviewing"
|
| 24 |
+
COMPLETE = "complete"
|
| 25 |
+
FAILED = "failed"
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@dataclass
|
| 29 |
+
class TaskContext:
|
| 30 |
+
"""
|
| 31 |
+
Shared context passed between agents.
|
| 32 |
+
|
| 33 |
+
Think of this as a clipboard that agents write to and read from.
|
| 34 |
+
"""
|
| 35 |
+
task_description: str # Original task from user
|
| 36 |
+
plan: Optional[str] = None # Created by Planner
|
| 37 |
+
code_changes: Optional[Dict[str, str]] = None # Created by Coder
|
| 38 |
+
review_feedback: Optional[str] = None # Created by Reviewer
|
| 39 |
+
error_message: Optional[str] = None # Set if something fails
|
| 40 |
+
|
| 41 |
+
# Metadata
|
| 42 |
+
current_step: int = 0
|
| 43 |
+
total_steps: int = 0
|
| 44 |
+
iterations: int = 0 # How many times we've looped
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class Orchestrator:
|
| 48 |
+
"""
|
| 49 |
+
Orchestrator manages the multi-agent workflow.
|
| 50 |
+
|
| 51 |
+
Flow:
|
| 52 |
+
1. Start in PLANNING state
|
| 53 |
+
2. Call Planner agent → get plan
|
| 54 |
+
3. Transition to CODING state
|
| 55 |
+
4. Call Coder agent → get code
|
| 56 |
+
5. Transition to REVIEWING state
|
| 57 |
+
6. Call Reviewer agent → get feedback
|
| 58 |
+
7. If approved → COMPLETE
|
| 59 |
+
If rejected → back to CODING (loop)
|
| 60 |
+
"""
|
| 61 |
+
|
| 62 |
+
def __init__(self, max_iterations: int = 5):
|
| 63 |
+
"""
|
| 64 |
+
Initialize orchestrator.
|
| 65 |
+
|
| 66 |
+
Args:
|
| 67 |
+
max_iterations: Max loops between coding and reviewing
|
| 68 |
+
(prevents infinite loops if code keeps failing)
|
| 69 |
+
"""
|
| 70 |
+
self.state = AgentState.PLANNING
|
| 71 |
+
self.max_iterations = max_iterations
|
| 72 |
+
self.context = None
|
| 73 |
+
|
| 74 |
+
# Create agent instances
|
| 75 |
+
self.planner = PlannerAgent()
|
| 76 |
+
self.coder = CoderAgent()
|
| 77 |
+
self.reviewer = ReviewerAgent()
|
| 78 |
+
|
| 79 |
+
def run(self, task: str) -> Dict[str, Any]:
|
| 80 |
+
"""
|
| 81 |
+
Run the multi-agent workflow for a task.
|
| 82 |
+
|
| 83 |
+
Args:
|
| 84 |
+
task: User's task description (e.g., "Add a login feature")
|
| 85 |
+
|
| 86 |
+
Returns:
|
| 87 |
+
Result dict with status, changes, and messages
|
| 88 |
+
"""
|
| 89 |
+
# Initialize context
|
| 90 |
+
self.context = TaskContext(task_description=task)
|
| 91 |
+
self.state = AgentState.PLANNING
|
| 92 |
+
|
| 93 |
+
# Main state machine loop
|
| 94 |
+
while self.state not in [AgentState.COMPLETE, AgentState.FAILED]:
|
| 95 |
+
# Safety: prevent infinite loops
|
| 96 |
+
if self.context.iterations >= self.max_iterations:
|
| 97 |
+
self.state = AgentState.FAILED
|
| 98 |
+
self.context.error_message = f"Max iterations ({self.max_iterations}) exceeded"
|
| 99 |
+
break
|
| 100 |
+
|
| 101 |
+
# Execute current state
|
| 102 |
+
if self.state == AgentState.PLANNING:
|
| 103 |
+
self._execute_planning()
|
| 104 |
+
|
| 105 |
+
elif self.state == AgentState.CODING:
|
| 106 |
+
self._execute_coding()
|
| 107 |
+
|
| 108 |
+
elif self.state == AgentState.REVIEWING:
|
| 109 |
+
self._execute_reviewing()
|
| 110 |
+
|
| 111 |
+
self.context.iterations += 1
|
| 112 |
+
|
| 113 |
+
# Return final result
|
| 114 |
+
return self._build_result()
|
| 115 |
+
|
| 116 |
+
def _execute_planning(self):
|
| 117 |
+
"""
|
| 118 |
+
Execute planning state: call Planner agent.
|
| 119 |
+
|
| 120 |
+
Planner's job:
|
| 121 |
+
- Understand the task
|
| 122 |
+
- Search codebase for relevant files
|
| 123 |
+
- Create step-by-step plan
|
| 124 |
+
|
| 125 |
+
Transition: Always go to CODING next
|
| 126 |
+
"""
|
| 127 |
+
print(f"\n[ORCHESTRATOR] State: PLANNING")
|
| 128 |
+
print(f"[ORCHESTRATOR] Task: {self.context.task_description}")
|
| 129 |
+
|
| 130 |
+
# Call the real Planner agent!
|
| 131 |
+
self.context.plan = self.planner.run(self.context.task_description)
|
| 132 |
+
|
| 133 |
+
# Transition to coding
|
| 134 |
+
self.state = AgentState.CODING
|
| 135 |
+
print(f"[ORCHESTRATOR] Plan created. Transitioning to CODING")
|
| 136 |
+
|
| 137 |
+
def _execute_coding(self):
|
| 138 |
+
"""
|
| 139 |
+
Execute coding state: call Coder agent.
|
| 140 |
+
|
| 141 |
+
Coder's job:
|
| 142 |
+
- Read the plan
|
| 143 |
+
- Read relevant files
|
| 144 |
+
- Write code changes
|
| 145 |
+
|
| 146 |
+
Transition: Always go to REVIEWING next
|
| 147 |
+
"""
|
| 148 |
+
print(f"\n[ORCHESTRATOR] State: CODING")
|
| 149 |
+
|
| 150 |
+
# Check if this is a rework (Reviewer rejected previous code)
|
| 151 |
+
if self.context.review_feedback:
|
| 152 |
+
print(f"[ORCHESTRATOR] Passing plan + REVIEWER FEEDBACK to Coder agent...")
|
| 153 |
+
else:
|
| 154 |
+
print(f"[ORCHESTRATOR] Passing plan to Coder agent...")
|
| 155 |
+
|
| 156 |
+
# Call the real Coder agent (with review feedback if available)!
|
| 157 |
+
self.context.code_changes = self.coder.run(
|
| 158 |
+
plan=self.context.plan,
|
| 159 |
+
task=self.context.task_description,
|
| 160 |
+
review_feedback=self.context.review_feedback
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
# Transition to reviewing
|
| 164 |
+
self.state = AgentState.REVIEWING
|
| 165 |
+
print(f"[ORCHESTRATOR] Code written. Transitioning to REVIEWING")
|
| 166 |
+
|
| 167 |
+
def _execute_reviewing(self):
|
| 168 |
+
"""
|
| 169 |
+
Execute reviewing state: call Reviewer agent.
|
| 170 |
+
|
| 171 |
+
Reviewer's job:
|
| 172 |
+
- Read the code changes
|
| 173 |
+
- Check for bugs, style issues
|
| 174 |
+
- Approve or reject
|
| 175 |
+
|
| 176 |
+
Transition:
|
| 177 |
+
- If approved → COMPLETE
|
| 178 |
+
- If rejected → back to CODING (with feedback)
|
| 179 |
+
"""
|
| 180 |
+
print(f"\n[ORCHESTRATOR] State: REVIEWING")
|
| 181 |
+
print(f"[ORCHESTRATOR] Passing code changes to Reviewer agent...")
|
| 182 |
+
|
| 183 |
+
# Call the real Reviewer agent!
|
| 184 |
+
approved, feedback = self.reviewer.run(
|
| 185 |
+
code_changes=self.context.code_changes,
|
| 186 |
+
plan=self.context.plan,
|
| 187 |
+
task=self.context.task_description
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
# Store the feedback
|
| 191 |
+
self.context.review_feedback = feedback
|
| 192 |
+
|
| 193 |
+
if approved:
|
| 194 |
+
print(f"[ORCHESTRATOR] Code APPROVED. Transitioning to COMPLETE")
|
| 195 |
+
self.state = AgentState.COMPLETE
|
| 196 |
+
else:
|
| 197 |
+
print(f"[ORCHESTRATOR] Code REJECTED. Transitioning back to CODING")
|
| 198 |
+
self.state = AgentState.CODING
|
| 199 |
+
|
| 200 |
+
def _build_result(self) -> Dict[str, Any]:
|
| 201 |
+
"""
|
| 202 |
+
Build final result dictionary.
|
| 203 |
+
|
| 204 |
+
Returns:
|
| 205 |
+
Dict with status, code changes, and metadata
|
| 206 |
+
"""
|
| 207 |
+
return {
|
| 208 |
+
'status': self.state.value,
|
| 209 |
+
'success': self.state == AgentState.COMPLETE,
|
| 210 |
+
'task': self.context.task_description,
|
| 211 |
+
'plan': self.context.plan,
|
| 212 |
+
'code_changes': self.context.code_changes,
|
| 213 |
+
'review_feedback': self.context.review_feedback,
|
| 214 |
+
'error': self.context.error_message,
|
| 215 |
+
'iterations': self.context.iterations
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
def get_state_history(self) -> str:
|
| 219 |
+
"""Get a summary of the orchestration flow."""
|
| 220 |
+
return f"""
|
| 221 |
+
Orchestrator Summary:
|
| 222 |
+
- Final State: {self.state.value}
|
| 223 |
+
- Iterations: {self.context.iterations}
|
| 224 |
+
- Task: {self.context.task_description}
|
| 225 |
+
- Plan Created: {'Yes' if self.context.plan else 'No'}
|
| 226 |
+
- Code Written: {'Yes' if self.context.code_changes else 'No'}
|
| 227 |
+
"""
|
codepilot/agents/planner_agent.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Planner Agent - Creates implementation plans
|
| 3 |
+
|
| 4 |
+
The Planner's job:
|
| 5 |
+
1. Understand the task
|
| 6 |
+
2. Search the codebase to see what exists
|
| 7 |
+
3. Create a detailed, step-by-step plan
|
| 8 |
+
|
| 9 |
+
Tools it has access to:
|
| 10 |
+
- search_codebase (hybrid retrieval)
|
| 11 |
+
- read_file (to understand existing code)
|
| 12 |
+
- list_files (to explore structure)
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
from codepilot.llm.client import OpenAIClient
|
| 16 |
+
from codepilot.tools.registry import get_tools, get_tool_function
|
| 17 |
+
from codepilot.agents.conversation import ConversationManager
|
| 18 |
+
from typing import Dict, Any
|
| 19 |
+
import json
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# Planner's specialized system prompt
|
| 23 |
+
PLANNER_SYSTEM_PROMPT = """You are a senior software architect and planning expert.
|
| 24 |
+
|
| 25 |
+
Your ONLY job is to create detailed implementation plans. You do NOT write code.
|
| 26 |
+
|
| 27 |
+
When given a task:
|
| 28 |
+
1. First, search the codebase to understand what already exists
|
| 29 |
+
2. Identify which files need to be modified or created
|
| 30 |
+
3. Break down the task into clear, specific steps
|
| 31 |
+
4. Consider dependencies and potential risks
|
| 32 |
+
|
| 33 |
+
Your plan should be:
|
| 34 |
+
- Specific (mention exact file names, function names)
|
| 35 |
+
- Ordered (steps build on each other)
|
| 36 |
+
- Complete (covers all aspects of the task)
|
| 37 |
+
- Realistic (considers existing code structure)
|
| 38 |
+
|
| 39 |
+
Output your plan as a numbered list of steps.
|
| 40 |
+
|
| 41 |
+
Tools available to you:
|
| 42 |
+
- search_codebase: Search for existing code (use this first!)
|
| 43 |
+
- read_file: Read specific files to understand them
|
| 44 |
+
- list_files: Explore directory structure
|
| 45 |
+
|
| 46 |
+
You do NOT have write_file or run_command - you only plan, never execute.
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class PlannerAgent:
|
| 51 |
+
"""
|
| 52 |
+
Planner Agent - Creates implementation plans.
|
| 53 |
+
|
| 54 |
+
This agent is specialized for planning. It has:
|
| 55 |
+
- Custom system prompt (architect mindset)
|
| 56 |
+
- Limited tools (read-only)
|
| 57 |
+
- Single responsibility (planning only)
|
| 58 |
+
"""
|
| 59 |
+
|
| 60 |
+
def __init__(self, model: str = "gpt-3.5-turbo"):
|
| 61 |
+
"""
|
| 62 |
+
Initialize Planner agent.
|
| 63 |
+
|
| 64 |
+
Args:
|
| 65 |
+
model: LLM model to use
|
| 66 |
+
"""
|
| 67 |
+
self.client = OpenAIClient(model=model)
|
| 68 |
+
self.conversation = ConversationManager()
|
| 69 |
+
|
| 70 |
+
# Planner only gets read-only tools
|
| 71 |
+
self.allowed_tools = [
|
| 72 |
+
"search_codebase",
|
| 73 |
+
"read_file",
|
| 74 |
+
"list_files"
|
| 75 |
+
]
|
| 76 |
+
|
| 77 |
+
def run(self, task: str) -> str:
|
| 78 |
+
"""
|
| 79 |
+
Create a plan for the given task.
|
| 80 |
+
|
| 81 |
+
Args:
|
| 82 |
+
task: Task description (e.g., "Add login feature")
|
| 83 |
+
|
| 84 |
+
Returns:
|
| 85 |
+
Detailed implementation plan as a string
|
| 86 |
+
"""
|
| 87 |
+
# Reset conversation
|
| 88 |
+
self.conversation = ConversationManager()
|
| 89 |
+
|
| 90 |
+
# Add system prompt
|
| 91 |
+
self.conversation.add_message("system", PLANNER_SYSTEM_PROMPT)
|
| 92 |
+
|
| 93 |
+
# Add user task
|
| 94 |
+
user_prompt = f"""Task: {task}
|
| 95 |
+
|
| 96 |
+
Please create a detailed implementation plan. Start by searching the codebase to understand what exists."""
|
| 97 |
+
self.conversation.add_message("user", user_prompt)
|
| 98 |
+
|
| 99 |
+
# Get only the tools this agent is allowed to use
|
| 100 |
+
all_tools = get_tools()
|
| 101 |
+
planner_tools = [
|
| 102 |
+
tool for tool in all_tools
|
| 103 |
+
if tool['function']['name'] in self.allowed_tools
|
| 104 |
+
]
|
| 105 |
+
|
| 106 |
+
# Run planning loop (agent explores codebase, then creates plan)
|
| 107 |
+
max_iterations = 10
|
| 108 |
+
for iteration in range(max_iterations):
|
| 109 |
+
# Call LLM
|
| 110 |
+
response = self.client.chat(
|
| 111 |
+
messages=self.conversation.get_messages(),
|
| 112 |
+
tools=planner_tools
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
finish_reason = response.choices[0].finish_reason
|
| 116 |
+
message = response.choices[0].message
|
| 117 |
+
|
| 118 |
+
# Add assistant response to conversation
|
| 119 |
+
self.conversation.add_message(
|
| 120 |
+
role="assistant",
|
| 121 |
+
content=message.content,
|
| 122 |
+
tool_calls=message.tool_calls
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
# Check if done
|
| 126 |
+
if finish_reason == "stop":
|
| 127 |
+
# Agent finished planning
|
| 128 |
+
return message.content
|
| 129 |
+
|
| 130 |
+
# Execute tool calls
|
| 131 |
+
if finish_reason == "tool_calls":
|
| 132 |
+
for tool_call in message.tool_calls:
|
| 133 |
+
tool_name = tool_call.function.name
|
| 134 |
+
tool_args = json.loads(tool_call.function.arguments)
|
| 135 |
+
|
| 136 |
+
print(f"[PLANNER] Calling tool: {tool_name}({tool_args})")
|
| 137 |
+
|
| 138 |
+
# Execute tool
|
| 139 |
+
tool_func = get_tool_function(tool_name)
|
| 140 |
+
if tool_func:
|
| 141 |
+
result = tool_func(**tool_args)
|
| 142 |
+
else:
|
| 143 |
+
result = f"Error: Tool {tool_name} not found"
|
| 144 |
+
|
| 145 |
+
# Add tool result to conversation
|
| 146 |
+
self.conversation.add_tool_result(
|
| 147 |
+
tool_call_id=tool_call.id,
|
| 148 |
+
tool_name=tool_name,
|
| 149 |
+
result=str(result)
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
# If we hit max iterations, return what we have
|
| 153 |
+
return "Error: Planner exceeded max iterations"
|
| 154 |
+
|
| 155 |
+
def get_tool_access(self) -> list:
|
| 156 |
+
"""Return list of tools this agent can access."""
|
| 157 |
+
return self.allowed_tools
|
codepilot/agents/reviewer_agent.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Reviewer Agent - Reviews code for quality and correctness
|
| 3 |
+
|
| 4 |
+
The Reviewer's job:
|
| 5 |
+
1. Read the code changes from Coder
|
| 6 |
+
2. Check for bugs, security issues, style problems
|
| 7 |
+
3. Verify the code matches the plan
|
| 8 |
+
4. Either approve or reject with specific feedback
|
| 9 |
+
|
| 10 |
+
Tools it has access to:
|
| 11 |
+
- read_file (to see full context of changed files)
|
| 12 |
+
- search_codebase (to check for similar patterns)
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
from codepilot.llm.client import OpenAIClient
|
| 16 |
+
from codepilot.tools.registry import get_tools, get_tool_function
|
| 17 |
+
from codepilot.agents.conversation import ConversationManager
|
| 18 |
+
from typing import Dict, Any, Tuple
|
| 19 |
+
import json
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# Reviewer's specialized system prompt
|
| 23 |
+
REVIEWER_SYSTEM_PROMPT = """You are a senior code reviewer and quality assurance expert.
|
| 24 |
+
|
| 25 |
+
Your ONLY job is to review code changes and provide feedback. You do NOT write code yourself.
|
| 26 |
+
|
| 27 |
+
When given code changes:
|
| 28 |
+
1. Read each changed file carefully
|
| 29 |
+
2. Check for common issues:
|
| 30 |
+
- Bugs and logic errors
|
| 31 |
+
- Security vulnerabilities (SQL injection, XSS, etc.)
|
| 32 |
+
- Missing error handling
|
| 33 |
+
- Poor naming or unclear code
|
| 34 |
+
- Code that doesn't match the plan
|
| 35 |
+
3. Decide: APPROVE or REJECT
|
| 36 |
+
4. If rejecting, provide specific, actionable feedback
|
| 37 |
+
|
| 38 |
+
Your review should be:
|
| 39 |
+
- Thorough (check all aspects of the code)
|
| 40 |
+
- Specific (point to exact issues with line numbers if possible)
|
| 41 |
+
- Constructive (explain WHY something is wrong and HOW to fix it)
|
| 42 |
+
- Fair (don't reject for minor style issues)
|
| 43 |
+
|
| 44 |
+
DECISION CRITERIA:
|
| 45 |
+
✅ APPROVE if:
|
| 46 |
+
- Code works correctly
|
| 47 |
+
- No security issues
|
| 48 |
+
- Follows the plan
|
| 49 |
+
- Has basic error handling
|
| 50 |
+
- Is reasonably readable
|
| 51 |
+
|
| 52 |
+
❌ REJECT if:
|
| 53 |
+
- Code has bugs
|
| 54 |
+
- Security vulnerabilities exist
|
| 55 |
+
- Doesn't implement the plan
|
| 56 |
+
- Missing critical error handling
|
| 57 |
+
- Code is unclear or confusing
|
| 58 |
+
|
| 59 |
+
Tools available to you:
|
| 60 |
+
- read_file: Read files to understand full context
|
| 61 |
+
- search_codebase: Check for similar patterns in the codebase
|
| 62 |
+
|
| 63 |
+
You do NOT have write_file - you only review, never modify code.
|
| 64 |
+
"""
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
class ReviewerAgent:
|
| 68 |
+
"""
|
| 69 |
+
Reviewer Agent - Reviews code for quality and correctness.
|
| 70 |
+
|
| 71 |
+
This agent is specialized for code review. It has:
|
| 72 |
+
- Custom system prompt (quality assurance mindset)
|
| 73 |
+
- Read-only tools (cannot modify code)
|
| 74 |
+
- Single responsibility (review only)
|
| 75 |
+
"""
|
| 76 |
+
|
| 77 |
+
def __init__(self, model: str = "gpt-3.5-turbo"):
|
| 78 |
+
"""
|
| 79 |
+
Initialize Reviewer agent.
|
| 80 |
+
|
| 81 |
+
Args:
|
| 82 |
+
model: LLM model to use
|
| 83 |
+
"""
|
| 84 |
+
self.client = OpenAIClient(model=model)
|
| 85 |
+
self.conversation = ConversationManager()
|
| 86 |
+
|
| 87 |
+
# Reviewer only gets read-only tools
|
| 88 |
+
self.allowed_tools = [
|
| 89 |
+
"read_file",
|
| 90 |
+
"search_codebase"
|
| 91 |
+
]
|
| 92 |
+
|
| 93 |
+
def run(self, code_changes: Dict[str, str], plan: str, task: str) -> Tuple[bool, str]:
|
| 94 |
+
"""
|
| 95 |
+
Review the code changes.
|
| 96 |
+
|
| 97 |
+
Args:
|
| 98 |
+
code_changes: Dictionary mapping file paths to new content
|
| 99 |
+
plan: The original plan (to verify code matches)
|
| 100 |
+
task: The original task (for context)
|
| 101 |
+
|
| 102 |
+
Returns:
|
| 103 |
+
Tuple of (approved: bool, feedback: str)
|
| 104 |
+
- approved: True if code is good, False if needs changes
|
| 105 |
+
- feedback: Explanation of decision and any issues found
|
| 106 |
+
"""
|
| 107 |
+
# Reset conversation
|
| 108 |
+
self.conversation = ConversationManager()
|
| 109 |
+
|
| 110 |
+
# Add system prompt
|
| 111 |
+
self.conversation.add_message("system", REVIEWER_SYSTEM_PROMPT)
|
| 112 |
+
|
| 113 |
+
# Format code changes for review
|
| 114 |
+
changes_text = self._format_code_changes(code_changes)
|
| 115 |
+
|
| 116 |
+
# Add user prompt with task, plan, and code changes
|
| 117 |
+
user_prompt = f"""Original Task: {task}
|
| 118 |
+
|
| 119 |
+
Implementation Plan:
|
| 120 |
+
{plan}
|
| 121 |
+
|
| 122 |
+
Code Changes to Review:
|
| 123 |
+
{changes_text}
|
| 124 |
+
|
| 125 |
+
Please review these code changes carefully. Check for bugs, security issues, and whether the code correctly implements the plan.
|
| 126 |
+
|
| 127 |
+
End your review with a clear decision:
|
| 128 |
+
- "DECISION: APPROVE" if the code is good
|
| 129 |
+
- "DECISION: REJECT" if changes are needed
|
| 130 |
+
|
| 131 |
+
If rejecting, provide specific feedback on what needs to be fixed."""
|
| 132 |
+
self.conversation.add_message("user", user_prompt)
|
| 133 |
+
|
| 134 |
+
# Get only the tools this agent is allowed to use
|
| 135 |
+
all_tools = get_tools()
|
| 136 |
+
reviewer_tools = [
|
| 137 |
+
tool for tool in all_tools
|
| 138 |
+
if tool['function']['name'] in self.allowed_tools
|
| 139 |
+
]
|
| 140 |
+
|
| 141 |
+
# Run review loop
|
| 142 |
+
max_iterations = 10
|
| 143 |
+
for iteration in range(max_iterations):
|
| 144 |
+
# Call LLM
|
| 145 |
+
response = self.client.chat(
|
| 146 |
+
messages=self.conversation.get_messages(),
|
| 147 |
+
tools=reviewer_tools
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
finish_reason = response.choices[0].finish_reason
|
| 151 |
+
message = response.choices[0].message
|
| 152 |
+
|
| 153 |
+
# Add assistant response to conversation
|
| 154 |
+
self.conversation.add_message(
|
| 155 |
+
role="assistant",
|
| 156 |
+
content=message.content,
|
| 157 |
+
tool_calls=message.tool_calls
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
# Check if done
|
| 161 |
+
if finish_reason == "stop":
|
| 162 |
+
# Agent finished review, parse decision
|
| 163 |
+
return self._parse_review_decision(message.content)
|
| 164 |
+
|
| 165 |
+
# Execute tool calls
|
| 166 |
+
if finish_reason == "tool_calls":
|
| 167 |
+
for tool_call in message.tool_calls:
|
| 168 |
+
tool_name = tool_call.function.name
|
| 169 |
+
tool_args = json.loads(tool_call.function.arguments)
|
| 170 |
+
|
| 171 |
+
print(f"[REVIEWER] Calling tool: {tool_name}({tool_args})")
|
| 172 |
+
|
| 173 |
+
# Execute tool
|
| 174 |
+
tool_func = get_tool_function(tool_name)
|
| 175 |
+
if tool_func:
|
| 176 |
+
result = tool_func(**tool_args)
|
| 177 |
+
else:
|
| 178 |
+
result = f"Error: Tool {tool_name} not found"
|
| 179 |
+
|
| 180 |
+
# Add tool result to conversation
|
| 181 |
+
self.conversation.add_tool_result(
|
| 182 |
+
tool_call_id=tool_call.id,
|
| 183 |
+
tool_name=tool_name,
|
| 184 |
+
result=str(result)
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
# If we hit max iterations, default to reject
|
| 188 |
+
return False, "Review timed out - please try again"
|
| 189 |
+
|
| 190 |
+
def _format_code_changes(self, code_changes: Dict[str, str]) -> str:
|
| 191 |
+
"""
|
| 192 |
+
Format code changes into readable text.
|
| 193 |
+
|
| 194 |
+
Args:
|
| 195 |
+
code_changes: Dict mapping file paths to content
|
| 196 |
+
|
| 197 |
+
Returns:
|
| 198 |
+
Formatted string showing all changes
|
| 199 |
+
"""
|
| 200 |
+
if not code_changes:
|
| 201 |
+
return "No code changes to review."
|
| 202 |
+
|
| 203 |
+
formatted = []
|
| 204 |
+
for file_path, content in code_changes.items():
|
| 205 |
+
formatted.append(f"\n{'='*60}")
|
| 206 |
+
formatted.append(f"File: {file_path}")
|
| 207 |
+
formatted.append('='*60)
|
| 208 |
+
formatted.append(content)
|
| 209 |
+
|
| 210 |
+
return '\n'.join(formatted)
|
| 211 |
+
|
| 212 |
+
def _parse_review_decision(self, review_text: str) -> Tuple[bool, str]:
|
| 213 |
+
"""
|
| 214 |
+
Parse the review text to extract decision.
|
| 215 |
+
|
| 216 |
+
Args:
|
| 217 |
+
review_text: The reviewer's final response
|
| 218 |
+
|
| 219 |
+
Returns:
|
| 220 |
+
Tuple of (approved, feedback)
|
| 221 |
+
"""
|
| 222 |
+
if review_text is None:
|
| 223 |
+
return False, "No review provided"
|
| 224 |
+
|
| 225 |
+
# Look for decision in the text
|
| 226 |
+
review_lower = review_text.lower()
|
| 227 |
+
|
| 228 |
+
if "decision: approve" in review_lower:
|
| 229 |
+
return True, review_text
|
| 230 |
+
elif "decision: reject" in review_lower:
|
| 231 |
+
return False, review_text
|
| 232 |
+
else:
|
| 233 |
+
# No clear decision - default to reject for safety
|
| 234 |
+
return False, f"Unclear decision. Review:\n{review_text}"
|
| 235 |
+
|
| 236 |
+
def get_tool_access(self) -> list:
|
| 237 |
+
"""Return list of tools this agent can access."""
|
| 238 |
+
return self.allowed_tools
|
codepilot/context/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Context Engineering Module
|
| 3 |
+
Provides code parsing, indexing, and intelligent context selection
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from codepilot.context.parser import CodeParser
|
| 7 |
+
|
| 8 |
+
__all__ = ['CodeParser']
|
codepilot/context/bm25_retriever.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
BM25 Retriever - Keyword-based code search
|
| 3 |
+
|
| 4 |
+
BM25 (Best Matching 25) is a ranking function that scores documents by:
|
| 5 |
+
1. Term Frequency (TF) - How often the search term appears in a document
|
| 6 |
+
2. Inverse Document Frequency (IDF) - Rarer terms get higher scores
|
| 7 |
+
3. Document Length Normalization - Longer docs don't unfairly dominate
|
| 8 |
+
|
| 9 |
+
This is the "keyword" half of our hybrid retrieval system.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import re
|
| 13 |
+
from typing import List, Dict, Any, Tuple
|
| 14 |
+
from rank_bm25 import BM25Okapi
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class CodeTokenizer:
|
| 18 |
+
"""
|
| 19 |
+
Tokenize code for searchability.
|
| 20 |
+
|
| 21 |
+
Handles:
|
| 22 |
+
- camelCase: getUserById -> get, user, by, id
|
| 23 |
+
- snake_case: get_user_by_id -> get, user, by, id
|
| 24 |
+
- Removes common Python keywords (they appear everywhere, low signal)
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
# Python keywords that appear in almost every file (low IDF = useless for search)
|
| 28 |
+
STOP_WORDS = {
|
| 29 |
+
'def', 'class', 'return', 'self', 'if', 'else', 'elif', 'for', 'while',
|
| 30 |
+
'try', 'except', 'finally', 'with', 'as', 'import', 'from', 'in', 'is',
|
| 31 |
+
'not', 'and', 'or', 'none', 'true', 'false', 'pass', 'break', 'continue',
|
| 32 |
+
'lambda', 'yield', 'raise', 'assert', 'global', 'nonlocal', 'del',
|
| 33 |
+
'the', 'a', 'an', 'of', 'to', 'args', 'kwargs', 'init', 'str', 'int',
|
| 34 |
+
'list', 'dict', 'bool', 'float', 'type', 'any', 'optional'
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
def tokenize(self, text: str) -> List[str]:
|
| 38 |
+
"""
|
| 39 |
+
Convert code text into searchable tokens.
|
| 40 |
+
|
| 41 |
+
Example:
|
| 42 |
+
"def getUserById(user_id):" -> ['get', 'user', 'by', 'id', 'user', 'id']
|
| 43 |
+
"""
|
| 44 |
+
# Step 1: Split camelCase and PascalCase
|
| 45 |
+
# "getUserById" -> "get User By Id"
|
| 46 |
+
text = re.sub(r'([a-z])([A-Z])', r'\1 \2', text)
|
| 47 |
+
|
| 48 |
+
# Step 2: Split snake_case and other separators
|
| 49 |
+
# "get_user_by_id" -> "get user by id"
|
| 50 |
+
text = re.sub(r'[_\-./\\(){}[\]:,;"\']', ' ', text)
|
| 51 |
+
|
| 52 |
+
# Step 3: Lowercase and split into words
|
| 53 |
+
words = text.lower().split()
|
| 54 |
+
|
| 55 |
+
# Step 4: Remove stop words and very short tokens (1-2 chars)
|
| 56 |
+
tokens = [
|
| 57 |
+
word for word in words
|
| 58 |
+
if word not in self.STOP_WORDS and len(word) > 2
|
| 59 |
+
]
|
| 60 |
+
|
| 61 |
+
return tokens
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
class BM25Retriever:
|
| 65 |
+
"""
|
| 66 |
+
BM25-based code search.
|
| 67 |
+
|
| 68 |
+
How it works:
|
| 69 |
+
1. Index: Convert each code chunk into tokens, build BM25 index
|
| 70 |
+
2. Search: Tokenize query, score each document, return top-K
|
| 71 |
+
"""
|
| 72 |
+
|
| 73 |
+
def __init__(self):
|
| 74 |
+
self.tokenizer = CodeTokenizer()
|
| 75 |
+
self.documents = [] # List of original documents
|
| 76 |
+
self.doc_tokens = [] # List of tokenized documents
|
| 77 |
+
self.bm25 = None # BM25 index (built after indexing)
|
| 78 |
+
self.doc_metadata = [] # Metadata for each document (file path, line numbers, etc.)
|
| 79 |
+
|
| 80 |
+
def index_documents(self, documents: List[Dict[str, Any]]) -> int:
|
| 81 |
+
"""
|
| 82 |
+
Build BM25 index from code documents.
|
| 83 |
+
|
| 84 |
+
Args:
|
| 85 |
+
documents: List of dicts with 'content' and optional metadata
|
| 86 |
+
Example: {'content': 'def get_user()...', 'file': 'users.py', 'type': 'function'}
|
| 87 |
+
|
| 88 |
+
Returns:
|
| 89 |
+
Number of documents indexed
|
| 90 |
+
"""
|
| 91 |
+
self.documents = []
|
| 92 |
+
self.doc_tokens = []
|
| 93 |
+
self.doc_metadata = []
|
| 94 |
+
|
| 95 |
+
for doc in documents:
|
| 96 |
+
content = doc.get('content', '')
|
| 97 |
+
|
| 98 |
+
# Tokenize the content
|
| 99 |
+
tokens = self.tokenizer.tokenize(content)
|
| 100 |
+
|
| 101 |
+
# Only index if we got meaningful tokens
|
| 102 |
+
if tokens:
|
| 103 |
+
self.documents.append(content)
|
| 104 |
+
self.doc_tokens.append(tokens)
|
| 105 |
+
self.doc_metadata.append({
|
| 106 |
+
'file': doc.get('file', 'unknown'),
|
| 107 |
+
'name': doc.get('name', 'unknown'),
|
| 108 |
+
'type': doc.get('type', 'unknown'),
|
| 109 |
+
'start_line': doc.get('start_line', 0),
|
| 110 |
+
'end_line': doc.get('end_line', 0)
|
| 111 |
+
})
|
| 112 |
+
|
| 113 |
+
# Build BM25 index from tokenized documents
|
| 114 |
+
if self.doc_tokens:
|
| 115 |
+
self.bm25 = BM25Okapi(self.doc_tokens)
|
| 116 |
+
|
| 117 |
+
return len(self.documents)
|
| 118 |
+
|
| 119 |
+
def search(self, query: str, top_k: int = 10) -> List[Dict[str, Any]]:
|
| 120 |
+
"""
|
| 121 |
+
Search for relevant code using BM25 scoring.
|
| 122 |
+
|
| 123 |
+
Args:
|
| 124 |
+
query: Search query (natural language or code terms)
|
| 125 |
+
top_k: Number of results to return
|
| 126 |
+
|
| 127 |
+
Returns:
|
| 128 |
+
List of results with scores and metadata, sorted by relevance
|
| 129 |
+
"""
|
| 130 |
+
if not self.bm25:
|
| 131 |
+
return []
|
| 132 |
+
|
| 133 |
+
# Tokenize the query the same way we tokenized documents
|
| 134 |
+
query_tokens = self.tokenizer.tokenize(query)
|
| 135 |
+
|
| 136 |
+
if not query_tokens:
|
| 137 |
+
return []
|
| 138 |
+
|
| 139 |
+
# Get BM25 scores for all documents
|
| 140 |
+
scores = self.bm25.get_scores(query_tokens)
|
| 141 |
+
|
| 142 |
+
# Get top-K document indices (sorted by score descending)
|
| 143 |
+
top_indices = sorted(
|
| 144 |
+
range(len(scores)),
|
| 145 |
+
key=lambda i: scores[i],
|
| 146 |
+
reverse=True
|
| 147 |
+
)[:top_k]
|
| 148 |
+
|
| 149 |
+
# Build results with scores and metadata
|
| 150 |
+
results = []
|
| 151 |
+
for rank, idx in enumerate(top_indices):
|
| 152 |
+
if scores[idx] > 0: # Only include if there's some match
|
| 153 |
+
results.append({
|
| 154 |
+
'rank': rank + 1,
|
| 155 |
+
'score': float(scores[idx]),
|
| 156 |
+
'content': self.documents[idx],
|
| 157 |
+
**self.doc_metadata[idx]
|
| 158 |
+
})
|
| 159 |
+
|
| 160 |
+
return results
|
| 161 |
+
|
| 162 |
+
def get_stats(self) -> Dict[str, Any]:
|
| 163 |
+
"""Get statistics about the index."""
|
| 164 |
+
if not self.doc_tokens:
|
| 165 |
+
return {'indexed': False}
|
| 166 |
+
|
| 167 |
+
total_tokens = sum(len(tokens) for tokens in self.doc_tokens)
|
| 168 |
+
avg_tokens = total_tokens / len(self.doc_tokens) if self.doc_tokens else 0
|
| 169 |
+
|
| 170 |
+
return {
|
| 171 |
+
'indexed': True,
|
| 172 |
+
'num_documents': len(self.documents),
|
| 173 |
+
'total_tokens': total_tokens,
|
| 174 |
+
'avg_tokens_per_doc': round(avg_tokens, 2)
|
| 175 |
+
}
|
codepilot/context/embedding_retriever.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Embedding Retriever - Semantic code search using vector embeddings
|
| 3 |
+
|
| 4 |
+
How it works:
|
| 5 |
+
1. Use a pre-trained model to convert code → vectors (embeddings)
|
| 6 |
+
2. Store vectors in ChromaDB (a vector database)
|
| 7 |
+
3. When searching, convert query → vector, find similar vectors
|
| 8 |
+
|
| 9 |
+
This is the "semantic" half of our hybrid retrieval system.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import os
|
| 13 |
+
from typing import List, Dict, Any, Optional
|
| 14 |
+
|
| 15 |
+
# ChromaDB for vector storage and similarity search
|
| 16 |
+
import chromadb
|
| 17 |
+
from chromadb.config import Settings
|
| 18 |
+
|
| 19 |
+
# Sentence Transformers for creating embeddings
|
| 20 |
+
# (Same pattern as our simple example: model.encode(text) → vector)
|
| 21 |
+
from sentence_transformers import SentenceTransformer
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class EmbeddingRetriever:
|
| 25 |
+
"""
|
| 26 |
+
Semantic search using vector embeddings.
|
| 27 |
+
|
| 28 |
+
Pattern from our example:
|
| 29 |
+
model.encode("login auth") → [0.2, 0.8, ...]
|
| 30 |
+
|
| 31 |
+
But instead of manual cosine_similarity, ChromaDB does it efficiently.
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
def __init__(
|
| 35 |
+
self,
|
| 36 |
+
model_name: str = "all-MiniLM-L6-v2",
|
| 37 |
+
persist_directory: str = ".codepilot_cache/chromadb"
|
| 38 |
+
):
|
| 39 |
+
"""
|
| 40 |
+
Initialize the embedding retriever.
|
| 41 |
+
|
| 42 |
+
Args:
|
| 43 |
+
model_name: Which sentence-transformer model to use
|
| 44 |
+
"all-MiniLM-L6-v2" is small (80MB) but effective
|
| 45 |
+
persist_directory: Where to save the vector database
|
| 46 |
+
"""
|
| 47 |
+
# Load the pre-trained model (same as example: SentenceTransformer(...))
|
| 48 |
+
self.model = SentenceTransformer(model_name)
|
| 49 |
+
|
| 50 |
+
# Create ChromaDB client
|
| 51 |
+
# persist_directory means vectors are saved to disk (survives restarts)
|
| 52 |
+
os.makedirs(persist_directory, exist_ok=True)
|
| 53 |
+
self.client = chromadb.PersistentClient(path=persist_directory)
|
| 54 |
+
|
| 55 |
+
# Get or create a "collection" (like a table in a database)
|
| 56 |
+
# This is where we store our code vectors
|
| 57 |
+
self.collection = self.client.get_or_create_collection(
|
| 58 |
+
name="code_embeddings",
|
| 59 |
+
metadata={"description": "Code chunks for semantic search"}
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
def index_documents(self, documents: List[Dict[str, Any]]) -> int:
|
| 63 |
+
"""
|
| 64 |
+
Convert code chunks to vectors and store in ChromaDB.
|
| 65 |
+
|
| 66 |
+
This is like our example:
|
| 67 |
+
vec = model.encode(text)
|
| 68 |
+
But we store many vectors at once in a database.
|
| 69 |
+
|
| 70 |
+
Args:
|
| 71 |
+
documents: List of dicts with 'content' and metadata
|
| 72 |
+
Example: {'content': 'def login()...', 'file': 'auth.py'}
|
| 73 |
+
|
| 74 |
+
Returns:
|
| 75 |
+
Number of documents indexed
|
| 76 |
+
"""
|
| 77 |
+
if not documents:
|
| 78 |
+
return 0
|
| 79 |
+
|
| 80 |
+
# Prepare data for ChromaDB
|
| 81 |
+
ids = [] # Unique ID for each document
|
| 82 |
+
texts = [] # The actual code content
|
| 83 |
+
metadatas = [] # Extra info (file path, line numbers, etc.)
|
| 84 |
+
|
| 85 |
+
for i, doc in enumerate(documents):
|
| 86 |
+
content = doc.get('content', '')
|
| 87 |
+
if not content.strip():
|
| 88 |
+
continue
|
| 89 |
+
|
| 90 |
+
# Create unique ID (ChromaDB requires string IDs)
|
| 91 |
+
doc_id = f"{doc.get('file', 'unknown')}::{doc.get('name', i)}"
|
| 92 |
+
|
| 93 |
+
ids.append(doc_id)
|
| 94 |
+
texts.append(content)
|
| 95 |
+
metadatas.append({
|
| 96 |
+
'file': doc.get('file', 'unknown'),
|
| 97 |
+
'name': doc.get('name', 'unknown'),
|
| 98 |
+
'type': doc.get('type', 'unknown'),
|
| 99 |
+
'start_line': doc.get('start_line', 0),
|
| 100 |
+
'end_line': doc.get('end_line', 0)
|
| 101 |
+
})
|
| 102 |
+
|
| 103 |
+
if not texts:
|
| 104 |
+
return 0
|
| 105 |
+
|
| 106 |
+
# Generate embeddings for all texts at once
|
| 107 |
+
# (Same as example: model.encode(text), but batched for efficiency)
|
| 108 |
+
embeddings = self.model.encode(texts, show_progress_bar=False)
|
| 109 |
+
|
| 110 |
+
# Store in ChromaDB
|
| 111 |
+
# ChromaDB handles: storing vectors, building search index, similarity math
|
| 112 |
+
self.collection.add(
|
| 113 |
+
ids=ids,
|
| 114 |
+
embeddings=embeddings.tolist(), # ChromaDB wants Python lists
|
| 115 |
+
documents=texts,
|
| 116 |
+
metadatas=metadatas
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
return len(texts)
|
| 120 |
+
|
| 121 |
+
def search(self, query: str, top_k: int = 10) -> List[Dict[str, Any]]:
|
| 122 |
+
"""
|
| 123 |
+
Find code semantically similar to the query.
|
| 124 |
+
|
| 125 |
+
This is like our example:
|
| 126 |
+
query_vec = model.encode(query)
|
| 127 |
+
similarity = cosine_similarity(query_vec, stored_vecs)
|
| 128 |
+
But ChromaDB does the similarity search efficiently.
|
| 129 |
+
|
| 130 |
+
Args:
|
| 131 |
+
query: Natural language or code description
|
| 132 |
+
top_k: Number of results to return
|
| 133 |
+
|
| 134 |
+
Returns:
|
| 135 |
+
List of results with similarity scores and metadata
|
| 136 |
+
"""
|
| 137 |
+
# Convert query to vector (same as example: model.encode(...))
|
| 138 |
+
query_embedding = self.model.encode(query)
|
| 139 |
+
|
| 140 |
+
# ChromaDB finds the most similar stored vectors
|
| 141 |
+
# Internally, it computes cosine similarity against all stored vectors
|
| 142 |
+
results = self.collection.query(
|
| 143 |
+
query_embeddings=[query_embedding.tolist()],
|
| 144 |
+
n_results=top_k,
|
| 145 |
+
include=['documents', 'metadatas', 'distances']
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
# Format results
|
| 149 |
+
# Note: ChromaDB returns "distances" (lower = more similar)
|
| 150 |
+
# We convert to "scores" (higher = more similar) for consistency with BM25
|
| 151 |
+
formatted = []
|
| 152 |
+
|
| 153 |
+
if results['ids'] and results['ids'][0]:
|
| 154 |
+
for i, doc_id in enumerate(results['ids'][0]):
|
| 155 |
+
# Convert distance to similarity score (1 - distance for cosine)
|
| 156 |
+
distance = results['distances'][0][i]
|
| 157 |
+
similarity = 1 - distance # Higher = more similar
|
| 158 |
+
|
| 159 |
+
formatted.append({
|
| 160 |
+
'rank': i + 1,
|
| 161 |
+
'score': float(similarity),
|
| 162 |
+
'content': results['documents'][0][i],
|
| 163 |
+
**results['metadatas'][0][i]
|
| 164 |
+
})
|
| 165 |
+
|
| 166 |
+
return formatted
|
| 167 |
+
|
| 168 |
+
def clear_index(self):
|
| 169 |
+
"""Remove all documents from the index."""
|
| 170 |
+
# Delete and recreate the collection
|
| 171 |
+
self.client.delete_collection("code_embeddings")
|
| 172 |
+
self.collection = self.client.get_or_create_collection(
|
| 173 |
+
name="code_embeddings",
|
| 174 |
+
metadata={"description": "Code chunks for semantic search"}
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
def get_stats(self) -> Dict[str, Any]:
|
| 178 |
+
"""Get statistics about the index."""
|
| 179 |
+
count = self.collection.count()
|
| 180 |
+
return {
|
| 181 |
+
'indexed': count > 0,
|
| 182 |
+
'num_documents': count,
|
| 183 |
+
'model': 'all-MiniLM-L6-v2',
|
| 184 |
+
'embedding_dimension': 384
|
| 185 |
+
}
|
codepilot/context/hybrid_retriever.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Hybrid Retriever - Combines BM25 and Embeddings using Reciprocal Rank Fusion
|
| 3 |
+
|
| 4 |
+
RRF (Reciprocal Rank Fusion) solves the problem of merging ranked lists
|
| 5 |
+
with different score scales by using ranks instead of raw scores.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from typing import List, Dict, Any
|
| 9 |
+
from codepilot.context.bm25_retriever import BM25Retriever
|
| 10 |
+
from codepilot.context.embedding_retriever import EmbeddingRetriever
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class HybridRetriever:
|
| 14 |
+
"""
|
| 15 |
+
Combines keyword search (BM25) and semantic search (Embeddings).
|
| 16 |
+
|
| 17 |
+
Why hybrid?
|
| 18 |
+
- BM25 finds exact matches (function names, variable names)
|
| 19 |
+
- Embeddings find semantic matches (related concepts)
|
| 20 |
+
- Together they cover both precision and recall
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
def __init__(self, bm25_weight: float = 0.5, embedding_weight: float = 0.5):
|
| 24 |
+
"""
|
| 25 |
+
Initialize hybrid retriever with both search methods.
|
| 26 |
+
|
| 27 |
+
Args:
|
| 28 |
+
bm25_weight: Weight for BM25 scores (0-1, default 0.5)
|
| 29 |
+
embedding_weight: Weight for embedding scores (0-1, default 0.5)
|
| 30 |
+
"""
|
| 31 |
+
# Create both retrievers
|
| 32 |
+
self.bm25 = BM25Retriever()
|
| 33 |
+
self.embeddings = EmbeddingRetriever()
|
| 34 |
+
|
| 35 |
+
# Weights (can be tuned based on your needs)
|
| 36 |
+
self.bm25_weight = bm25_weight
|
| 37 |
+
self.embedding_weight = embedding_weight
|
| 38 |
+
|
| 39 |
+
# RRF constant (k=60 is standard in literature)
|
| 40 |
+
self.k = 60
|
| 41 |
+
|
| 42 |
+
def index_documents(self, documents: List[Dict[str, Any]]) -> Dict[str, int]:
|
| 43 |
+
"""
|
| 44 |
+
Index documents in BOTH retrievers.
|
| 45 |
+
|
| 46 |
+
This is the unified entry point - call this once and both
|
| 47 |
+
BM25 and Embeddings get indexed automatically.
|
| 48 |
+
|
| 49 |
+
Args:
|
| 50 |
+
documents: List of code chunks with metadata
|
| 51 |
+
|
| 52 |
+
Returns:
|
| 53 |
+
Statistics from both indexers
|
| 54 |
+
"""
|
| 55 |
+
bm25_count = self.bm25.index_documents(documents)
|
| 56 |
+
embedding_count = self.embeddings.index_documents(documents)
|
| 57 |
+
|
| 58 |
+
return {
|
| 59 |
+
'bm25_indexed': bm25_count,
|
| 60 |
+
'embedding_indexed': embedding_count
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
def search(self, query: str, top_k: int = 10) -> List[Dict[str, Any]]:
|
| 64 |
+
"""
|
| 65 |
+
Search using both BM25 and Embeddings, merge with RRF.
|
| 66 |
+
|
| 67 |
+
Process:
|
| 68 |
+
1. Get results from both retrievers
|
| 69 |
+
2. Convert to rank maps (doc_id → rank)
|
| 70 |
+
3. Calculate RRF score for each unique document
|
| 71 |
+
4. Sort by RRF score and return top K
|
| 72 |
+
|
| 73 |
+
Args:
|
| 74 |
+
query: Search query (natural language or code terms)
|
| 75 |
+
top_k: Number of final results to return
|
| 76 |
+
|
| 77 |
+
Returns:
|
| 78 |
+
Merged results sorted by RRF score
|
| 79 |
+
"""
|
| 80 |
+
# Step 1: Get results from BOTH retrievers
|
| 81 |
+
# We fetch 2x top_k to have more candidates for fusion
|
| 82 |
+
bm25_results = self.bm25.search(query, top_k=top_k * 2)
|
| 83 |
+
embedding_results = self.embeddings.search(query, top_k=top_k * 2)
|
| 84 |
+
|
| 85 |
+
# Step 2: Build rank maps (document ID → rank position)
|
| 86 |
+
bm25_ranks = {}
|
| 87 |
+
for i, result in enumerate(bm25_results):
|
| 88 |
+
# Create unique ID from file + name
|
| 89 |
+
doc_id = f"{result['file']}::{result['name']}"
|
| 90 |
+
bm25_ranks[doc_id] = i + 1 # Ranks start at 1, not 0
|
| 91 |
+
|
| 92 |
+
embedding_ranks = {}
|
| 93 |
+
for i, result in enumerate(embedding_results):
|
| 94 |
+
doc_id = f"{result['file']}::{result['name']}"
|
| 95 |
+
embedding_ranks[doc_id] = i + 1
|
| 96 |
+
|
| 97 |
+
# Step 3: Collect ALL unique documents from both lists
|
| 98 |
+
all_doc_ids = set(bm25_ranks.keys()) | set(embedding_ranks.keys())
|
| 99 |
+
|
| 100 |
+
# Step 4: Calculate RRF score for each document
|
| 101 |
+
rrf_scores = {}
|
| 102 |
+
for doc_id in all_doc_ids:
|
| 103 |
+
score = 0.0
|
| 104 |
+
|
| 105 |
+
# Add BM25 contribution (if document appeared in BM25 results)
|
| 106 |
+
if doc_id in bm25_ranks:
|
| 107 |
+
# RRF formula: 1 / (k + rank)
|
| 108 |
+
score += self.bm25_weight * (1 / (self.k + bm25_ranks[doc_id]))
|
| 109 |
+
|
| 110 |
+
# Add Embedding contribution (if document appeared in Embedding results)
|
| 111 |
+
if doc_id in embedding_ranks:
|
| 112 |
+
score += self.embedding_weight * (1 / (self.k + embedding_ranks[doc_id]))
|
| 113 |
+
|
| 114 |
+
rrf_scores[doc_id] = score
|
| 115 |
+
|
| 116 |
+
# Step 5: Sort by RRF score (highest first) and take top K
|
| 117 |
+
sorted_doc_ids = sorted(
|
| 118 |
+
rrf_scores.keys(),
|
| 119 |
+
key=lambda doc_id: rrf_scores[doc_id],
|
| 120 |
+
reverse=True
|
| 121 |
+
)[:top_k]
|
| 122 |
+
|
| 123 |
+
# Step 6: Build final results with metadata
|
| 124 |
+
results = []
|
| 125 |
+
for rank, doc_id in enumerate(sorted_doc_ids):
|
| 126 |
+
# Get metadata from whichever retriever had this document
|
| 127 |
+
metadata = self._get_metadata(doc_id, bm25_results, embedding_results)
|
| 128 |
+
|
| 129 |
+
results.append({
|
| 130 |
+
'rank': rank + 1,
|
| 131 |
+
'rrf_score': round(rrf_scores[doc_id], 4),
|
| 132 |
+
'in_bm25': doc_id in bm25_ranks,
|
| 133 |
+
'in_embeddings': doc_id in embedding_ranks,
|
| 134 |
+
'bm25_rank': bm25_ranks.get(doc_id, None),
|
| 135 |
+
'embedding_rank': embedding_ranks.get(doc_id, None),
|
| 136 |
+
**metadata
|
| 137 |
+
})
|
| 138 |
+
|
| 139 |
+
return results
|
| 140 |
+
|
| 141 |
+
def _get_metadata(
|
| 142 |
+
self,
|
| 143 |
+
doc_id: str,
|
| 144 |
+
bm25_results: List[Dict],
|
| 145 |
+
embedding_results: List[Dict]
|
| 146 |
+
) -> Dict[str, Any]:
|
| 147 |
+
"""
|
| 148 |
+
Extract metadata for a document from whichever list contains it.
|
| 149 |
+
|
| 150 |
+
Args:
|
| 151 |
+
doc_id: Document identifier (file::name)
|
| 152 |
+
bm25_results: Results from BM25 search
|
| 153 |
+
embedding_results: Results from embedding search
|
| 154 |
+
|
| 155 |
+
Returns:
|
| 156 |
+
Metadata dict with file, name, content, etc.
|
| 157 |
+
"""
|
| 158 |
+
# Try BM25 results first
|
| 159 |
+
for result in bm25_results:
|
| 160 |
+
if f"{result['file']}::{result['name']}" == doc_id:
|
| 161 |
+
return {
|
| 162 |
+
'file': result['file'],
|
| 163 |
+
'name': result['name'],
|
| 164 |
+
'type': result.get('type', 'unknown'),
|
| 165 |
+
'content': result.get('content', ''),
|
| 166 |
+
'start_line': result.get('start_line', 0),
|
| 167 |
+
'end_line': result.get('end_line', 0)
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
# Try embedding results
|
| 171 |
+
for result in embedding_results:
|
| 172 |
+
if f"{result['file']}::{result['name']}" == doc_id:
|
| 173 |
+
return {
|
| 174 |
+
'file': result['file'],
|
| 175 |
+
'name': result['name'],
|
| 176 |
+
'type': result.get('type', 'unknown'),
|
| 177 |
+
'content': result.get('content', ''),
|
| 178 |
+
'start_line': result.get('start_line', 0),
|
| 179 |
+
'end_line': result.get('end_line', 0)
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
# Shouldn't happen, but return empty dict as fallback
|
| 183 |
+
return {}
|
| 184 |
+
|
| 185 |
+
def get_stats(self) -> Dict[str, Any]:
|
| 186 |
+
"""Get statistics from both retrievers."""
|
| 187 |
+
return {
|
| 188 |
+
'bm25': self.bm25.get_stats(),
|
| 189 |
+
'embeddings': self.embeddings.get_stats(),
|
| 190 |
+
'weights': {
|
| 191 |
+
'bm25': self.bm25_weight,
|
| 192 |
+
'embeddings': self.embedding_weight
|
| 193 |
+
}
|
| 194 |
+
}
|
codepilot/context/indexer.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Codebase Indexer
|
| 3 |
+
Scans entire project and builds searchable index of all Python files
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import json
|
| 8 |
+
import hashlib
|
| 9 |
+
from typing import Dict, List, Any, Optional
|
| 10 |
+
from codepilot.context.parser import CodeParser
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class CodebaseIndexer:
|
| 14 |
+
"""
|
| 15 |
+
Index an entire codebase for fast retrieval
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
def __init__(self, root_path: str, cache_dir: str = ".codepilot_cache"):
|
| 19 |
+
"""
|
| 20 |
+
Initialize indexer
|
| 21 |
+
|
| 22 |
+
Args:
|
| 23 |
+
root_path: Root directory to index
|
| 24 |
+
cache_dir: Where to store cached index
|
| 25 |
+
"""
|
| 26 |
+
self.root_path = root_path
|
| 27 |
+
self.cache_dir = cache_dir
|
| 28 |
+
self.parser = CodeParser()
|
| 29 |
+
self.index = {} # file_path -> parsed_data
|
| 30 |
+
|
| 31 |
+
def build_index(self, file_extensions: List[str] = ['.py']) -> Dict[str, Any]:
|
| 32 |
+
"""
|
| 33 |
+
Scan directory and index all matching files
|
| 34 |
+
|
| 35 |
+
Args:
|
| 36 |
+
file_extensions: List of extensions to index (default: ['.py'])
|
| 37 |
+
|
| 38 |
+
Returns:
|
| 39 |
+
Statistics about the indexing process
|
| 40 |
+
"""
|
| 41 |
+
total_files = 0
|
| 42 |
+
total_functions = 0
|
| 43 |
+
total_classes = 0
|
| 44 |
+
errors = []
|
| 45 |
+
|
| 46 |
+
# Walk through directory tree
|
| 47 |
+
for root, dirs, files in os.walk(self.root_path):
|
| 48 |
+
# Skip unwanted directories (modify dirs in-place)
|
| 49 |
+
dirs[:] = [d for d in dirs if d not in [
|
| 50 |
+
'__pycache__', 'venv', 'node_modules', '.git',
|
| 51 |
+
'.pytest_cache', '.mypy_cache'
|
| 52 |
+
]]
|
| 53 |
+
|
| 54 |
+
# Process each file
|
| 55 |
+
for file in files:
|
| 56 |
+
# Check if file has matching extension
|
| 57 |
+
if any(file.endswith(ext) for ext in file_extensions):
|
| 58 |
+
file_path = os.path.join(root, file)
|
| 59 |
+
|
| 60 |
+
# Parse the file
|
| 61 |
+
result = self.parser.parse_file(file_path)
|
| 62 |
+
|
| 63 |
+
if result.get('parse_errors'):
|
| 64 |
+
errors.append({
|
| 65 |
+
'file': file_path,
|
| 66 |
+
'error': result['parse_errors'][0]
|
| 67 |
+
})
|
| 68 |
+
else:
|
| 69 |
+
# Store in index
|
| 70 |
+
self.index[file_path] = result
|
| 71 |
+
total_files += 1
|
| 72 |
+
total_functions += len(result.get('functions', []))
|
| 73 |
+
total_classes += len(result.get('classes', []))
|
| 74 |
+
|
| 75 |
+
return {
|
| 76 |
+
'total_files': total_files,
|
| 77 |
+
'total_functions': total_functions,
|
| 78 |
+
'total_classes': total_classes,
|
| 79 |
+
'errors': errors
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
def find_definition(self, name: str) -> List[Dict[str, Any]]:
|
| 83 |
+
"""
|
| 84 |
+
Find where a function or class is defined
|
| 85 |
+
|
| 86 |
+
Args:
|
| 87 |
+
name: Function or class name to search for
|
| 88 |
+
|
| 89 |
+
Returns:
|
| 90 |
+
List of locations where name is defined
|
| 91 |
+
"""
|
| 92 |
+
results = []
|
| 93 |
+
|
| 94 |
+
for file_path, data in self.index.items():
|
| 95 |
+
# Check functions
|
| 96 |
+
for func in data.get('functions', []):
|
| 97 |
+
if func['name'] == name:
|
| 98 |
+
results.append({
|
| 99 |
+
'file': file_path,
|
| 100 |
+
'line': func['start_line'],
|
| 101 |
+
'type': 'function'
|
| 102 |
+
})
|
| 103 |
+
|
| 104 |
+
# Check classes
|
| 105 |
+
for cls in data.get('classes', []):
|
| 106 |
+
if cls['name'] == name:
|
| 107 |
+
results.append({
|
| 108 |
+
'file': file_path,
|
| 109 |
+
'line': cls['start_line'],
|
| 110 |
+
'type': 'class'
|
| 111 |
+
})
|
| 112 |
+
|
| 113 |
+
return results
|
| 114 |
+
|
| 115 |
+
def save_index(self, output_path: Optional[str] = None):
|
| 116 |
+
"""
|
| 117 |
+
Save index to disk as JSON
|
| 118 |
+
|
| 119 |
+
Args:
|
| 120 |
+
output_path: Where to save (default: cache_dir/index.json)
|
| 121 |
+
"""
|
| 122 |
+
if output_path is None:
|
| 123 |
+
# Create cache directory if it doesn't exist
|
| 124 |
+
os.makedirs(self.cache_dir, exist_ok=True)
|
| 125 |
+
output_path = os.path.join(self.cache_dir, 'index.json')
|
| 126 |
+
|
| 127 |
+
with open(output_path, 'w') as f:
|
| 128 |
+
json.dump(self.index, f, indent=2)
|
| 129 |
+
|
| 130 |
+
print(f"Index saved to {output_path}")
|
| 131 |
+
|
| 132 |
+
def load_index(self, input_path: Optional[str] = None):
|
| 133 |
+
"""
|
| 134 |
+
Load index from disk
|
| 135 |
+
|
| 136 |
+
Args:
|
| 137 |
+
input_path: Where to load from (default: cache_dir/index.json)
|
| 138 |
+
"""
|
| 139 |
+
if input_path is None:
|
| 140 |
+
input_path = os.path.join(self.cache_dir, 'index.json')
|
| 141 |
+
|
| 142 |
+
with open(input_path, 'r') as f:
|
| 143 |
+
self.index = json.load(f)
|
| 144 |
+
|
| 145 |
+
print(f"Index loaded from {input_path}")
|
codepilot/context/parser.py
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Python Code Parser using AST
|
| 3 |
+
Extracts structured information from Python files
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import ast
|
| 7 |
+
import os
|
| 8 |
+
from typing import Dict, List, Any, Optional
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class CodeParser:
|
| 12 |
+
"""
|
| 13 |
+
Parse Python code using AST to extract structured information
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
def parse_file(self, file_path: str) -> Dict[str, Any]:
|
| 17 |
+
"""
|
| 18 |
+
Parse a Python file and extract all structural elements
|
| 19 |
+
|
| 20 |
+
Args:
|
| 21 |
+
file_path: Path to the Python file to parse
|
| 22 |
+
|
| 23 |
+
Returns:
|
| 24 |
+
Dictionary containing:
|
| 25 |
+
- file_path: str
|
| 26 |
+
- language: 'python'
|
| 27 |
+
- imports: List of import statements
|
| 28 |
+
- functions: List of function definitions
|
| 29 |
+
- classes: List of class definitions
|
| 30 |
+
- globals: List of global variables
|
| 31 |
+
- total_lines: int
|
| 32 |
+
- parse_errors: List of error messages (empty if successful)
|
| 33 |
+
"""
|
| 34 |
+
try:
|
| 35 |
+
# Read the file
|
| 36 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
| 37 |
+
source_code = f.read()
|
| 38 |
+
|
| 39 |
+
# Count total lines
|
| 40 |
+
total_lines = len(source_code.split('\n'))
|
| 41 |
+
|
| 42 |
+
# Parse the AST
|
| 43 |
+
tree = ast.parse(source_code, filename=file_path)
|
| 44 |
+
|
| 45 |
+
# Extract elements
|
| 46 |
+
result = {
|
| 47 |
+
'file_path': file_path,
|
| 48 |
+
'language': 'python',
|
| 49 |
+
'imports': self._extract_imports(tree),
|
| 50 |
+
'functions': self._extract_functions(tree, source_code),
|
| 51 |
+
'classes': self._extract_classes(tree, source_code),
|
| 52 |
+
'globals': self._extract_globals(tree),
|
| 53 |
+
'total_lines': total_lines,
|
| 54 |
+
'parse_errors': []
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
return result
|
| 58 |
+
|
| 59 |
+
except FileNotFoundError:
|
| 60 |
+
return {
|
| 61 |
+
'file_path': file_path,
|
| 62 |
+
'parse_errors': [f"File not found: '{file_path}'"]
|
| 63 |
+
}
|
| 64 |
+
except SyntaxError as e:
|
| 65 |
+
return {
|
| 66 |
+
'file_path': file_path,
|
| 67 |
+
'parse_errors': [f"Syntax error at line {e.lineno}: {e.msg}"]
|
| 68 |
+
}
|
| 69 |
+
except Exception as e:
|
| 70 |
+
return {
|
| 71 |
+
'file_path': file_path,
|
| 72 |
+
'parse_errors': [f"Parse error: {str(e)}"]
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
def _extract_imports(self, tree: ast.AST) -> List[Dict[str, Any]]:
|
| 76 |
+
"""Extract all import statements"""
|
| 77 |
+
imports = []
|
| 78 |
+
|
| 79 |
+
for node in ast.walk(tree):
|
| 80 |
+
if isinstance(node, ast.Import):
|
| 81 |
+
for alias in node.names:
|
| 82 |
+
imports.append({
|
| 83 |
+
'name': alias.name,
|
| 84 |
+
'alias': alias.asname,
|
| 85 |
+
'line': node.lineno,
|
| 86 |
+
'type': 'import'
|
| 87 |
+
})
|
| 88 |
+
elif isinstance(node, ast.ImportFrom):
|
| 89 |
+
module = node.module or ''
|
| 90 |
+
for alias in node.names:
|
| 91 |
+
imports.append({
|
| 92 |
+
'name': f"{module}.{alias.name}" if module else alias.name,
|
| 93 |
+
'module': module,
|
| 94 |
+
'imported': alias.name,
|
| 95 |
+
'alias': alias.asname,
|
| 96 |
+
'line': node.lineno,
|
| 97 |
+
'type': 'from'
|
| 98 |
+
})
|
| 99 |
+
|
| 100 |
+
return imports
|
| 101 |
+
|
| 102 |
+
def _extract_functions(self, tree: ast.AST, source_code: str) -> List[Dict[str, Any]]:
|
| 103 |
+
"""Extract all function definitions"""
|
| 104 |
+
functions = []
|
| 105 |
+
|
| 106 |
+
for node in ast.walk(tree):
|
| 107 |
+
if isinstance(node, ast.FunctionDef) or isinstance(node, ast.AsyncFunctionDef):
|
| 108 |
+
# Get function parameters
|
| 109 |
+
params = [arg.arg for arg in node.args.args]
|
| 110 |
+
|
| 111 |
+
# Get docstring
|
| 112 |
+
docstring = ast.get_docstring(node)
|
| 113 |
+
|
| 114 |
+
# Check if async
|
| 115 |
+
is_async = isinstance(node, ast.AsyncFunctionDef)
|
| 116 |
+
|
| 117 |
+
# Get decorators
|
| 118 |
+
decorators = [ast.unparse(dec) for dec in node.decorator_list]
|
| 119 |
+
|
| 120 |
+
functions.append({
|
| 121 |
+
'name': node.name,
|
| 122 |
+
'start_line': node.lineno,
|
| 123 |
+
'end_line': node.end_lineno,
|
| 124 |
+
'parameters': params,
|
| 125 |
+
'docstring': docstring,
|
| 126 |
+
'is_async': is_async,
|
| 127 |
+
'decorators': decorators
|
| 128 |
+
})
|
| 129 |
+
|
| 130 |
+
return functions
|
| 131 |
+
|
| 132 |
+
def _extract_classes(self, tree: ast.AST, source_code: str) -> List[Dict[str, Any]]:
|
| 133 |
+
"""Extract all class definitions"""
|
| 134 |
+
classes = []
|
| 135 |
+
|
| 136 |
+
for node in ast.walk(tree):
|
| 137 |
+
if isinstance(node, ast.ClassDef):
|
| 138 |
+
# Get base classes
|
| 139 |
+
bases = [ast.unparse(base) for base in node.bases]
|
| 140 |
+
|
| 141 |
+
# Get docstring
|
| 142 |
+
docstring = ast.get_docstring(node)
|
| 143 |
+
|
| 144 |
+
# Get methods
|
| 145 |
+
methods = []
|
| 146 |
+
for item in node.body:
|
| 147 |
+
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
| 148 |
+
methods.append({
|
| 149 |
+
'name': item.name,
|
| 150 |
+
'is_async': isinstance(item, ast.AsyncFunctionDef),
|
| 151 |
+
'line': item.lineno
|
| 152 |
+
})
|
| 153 |
+
|
| 154 |
+
# Get decorators
|
| 155 |
+
decorators = [ast.unparse(dec) for dec in node.decorator_list]
|
| 156 |
+
|
| 157 |
+
classes.append({
|
| 158 |
+
'name': node.name,
|
| 159 |
+
'start_line': node.lineno,
|
| 160 |
+
'end_line': node.end_lineno,
|
| 161 |
+
'bases': bases,
|
| 162 |
+
'docstring': docstring,
|
| 163 |
+
'methods': methods,
|
| 164 |
+
'decorators': decorators
|
| 165 |
+
})
|
| 166 |
+
|
| 167 |
+
return classes
|
| 168 |
+
|
| 169 |
+
def _extract_globals(self, tree: ast.AST) -> List[Dict[str, Any]]:
|
| 170 |
+
"""Extract global variable assignments"""
|
| 171 |
+
globals_list = []
|
| 172 |
+
|
| 173 |
+
# Only look at module-level assignments
|
| 174 |
+
for node in tree.body if isinstance(tree, ast.Module) else []:
|
| 175 |
+
if isinstance(node, ast.Assign):
|
| 176 |
+
for target in node.targets:
|
| 177 |
+
if isinstance(target, ast.Name):
|
| 178 |
+
# Try to infer type from value
|
| 179 |
+
value_type = self._infer_type(node.value)
|
| 180 |
+
|
| 181 |
+
globals_list.append({
|
| 182 |
+
'name': target.id,
|
| 183 |
+
'line': node.lineno,
|
| 184 |
+
'type': value_type
|
| 185 |
+
})
|
| 186 |
+
|
| 187 |
+
return globals_list
|
| 188 |
+
|
| 189 |
+
def _infer_type(self, node: ast.AST) -> str:
|
| 190 |
+
"""Infer type from AST node"""
|
| 191 |
+
if isinstance(node, ast.Constant):
|
| 192 |
+
return type(node.value).__name__
|
| 193 |
+
elif isinstance(node, ast.List):
|
| 194 |
+
return 'list'
|
| 195 |
+
elif isinstance(node, ast.Dict):
|
| 196 |
+
return 'dict'
|
| 197 |
+
elif isinstance(node, ast.Set):
|
| 198 |
+
return 'set'
|
| 199 |
+
elif isinstance(node, ast.Tuple):
|
| 200 |
+
return 'tuple'
|
| 201 |
+
elif isinstance(node, ast.Call):
|
| 202 |
+
if isinstance(node.func, ast.Name):
|
| 203 |
+
return node.func.id
|
| 204 |
+
return 'object'
|
| 205 |
+
else:
|
| 206 |
+
return 'unknown'
|
| 207 |
+
|
| 208 |
+
def extract_code_chunk(self, file_path: str, element_name: str) -> str:
|
| 209 |
+
"""
|
| 210 |
+
Extract a specific function or class with its dependencies
|
| 211 |
+
|
| 212 |
+
Args:
|
| 213 |
+
file_path: Path to the Python file
|
| 214 |
+
element_name: Name of function or class to extract
|
| 215 |
+
|
| 216 |
+
Returns:
|
| 217 |
+
Complete code chunk including relevant imports and the element itself
|
| 218 |
+
"""
|
| 219 |
+
try:
|
| 220 |
+
# Parse the file
|
| 221 |
+
result = self.parse_file(file_path)
|
| 222 |
+
|
| 223 |
+
if result.get('parse_errors'):
|
| 224 |
+
return f"Error: {result['parse_errors'][0]}"
|
| 225 |
+
|
| 226 |
+
# Read source code
|
| 227 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
| 228 |
+
lines = f.readlines()
|
| 229 |
+
|
| 230 |
+
# Find the element
|
| 231 |
+
element_lines = None
|
| 232 |
+
|
| 233 |
+
# Check functions
|
| 234 |
+
for func in result.get('functions', []):
|
| 235 |
+
if func['name'] == element_name:
|
| 236 |
+
element_lines = (func['start_line'], func['end_line'])
|
| 237 |
+
break
|
| 238 |
+
|
| 239 |
+
# Check classes
|
| 240 |
+
if not element_lines:
|
| 241 |
+
for cls in result.get('classes', []):
|
| 242 |
+
if cls['name'] == element_name:
|
| 243 |
+
element_lines = (cls['start_line'], cls['end_line'])
|
| 244 |
+
break
|
| 245 |
+
|
| 246 |
+
if not element_lines:
|
| 247 |
+
return f"Error: '{element_name}' not found in {file_path}"
|
| 248 |
+
|
| 249 |
+
# Extract the code chunk
|
| 250 |
+
start_line, end_line = element_lines
|
| 251 |
+
chunk_lines = lines[start_line - 1:end_line]
|
| 252 |
+
|
| 253 |
+
# Add relevant imports at the beginning
|
| 254 |
+
import_lines = []
|
| 255 |
+
for imp in result.get('imports', []):
|
| 256 |
+
import_lines.append(lines[imp['line'] - 1])
|
| 257 |
+
|
| 258 |
+
# Combine imports and element code
|
| 259 |
+
if import_lines:
|
| 260 |
+
code_chunk = ''.join(import_lines) + '\n' + ''.join(chunk_lines)
|
| 261 |
+
else:
|
| 262 |
+
code_chunk = ''.join(chunk_lines)
|
| 263 |
+
|
| 264 |
+
return code_chunk.strip()
|
| 265 |
+
|
| 266 |
+
except FileNotFoundError:
|
| 267 |
+
return f"Error: File '{file_path}' not found."
|
| 268 |
+
except Exception as e:
|
| 269 |
+
return f"Error extracting code chunk: {str(e)}"
|
| 270 |
+
|
| 271 |
+
def get_file_summary(self, file_path: str) -> str:
|
| 272 |
+
"""
|
| 273 |
+
Generate a concise summary of file contents
|
| 274 |
+
|
| 275 |
+
Args:
|
| 276 |
+
file_path: Path to the Python file
|
| 277 |
+
|
| 278 |
+
Returns:
|
| 279 |
+
Formatted summary string
|
| 280 |
+
"""
|
| 281 |
+
try:
|
| 282 |
+
result = self.parse_file(file_path)
|
| 283 |
+
|
| 284 |
+
if result.get('parse_errors'):
|
| 285 |
+
return f"Error: {result['parse_errors'][0]}"
|
| 286 |
+
|
| 287 |
+
# Build summary
|
| 288 |
+
summary = []
|
| 289 |
+
summary.append(f"File: {file_path}")
|
| 290 |
+
summary.append(f"Lines: {result.get('total_lines', 0)}")
|
| 291 |
+
|
| 292 |
+
# Functions
|
| 293 |
+
functions = result.get('functions', [])
|
| 294 |
+
if functions:
|
| 295 |
+
func_names = ', '.join(f"{f['name']}()" for f in functions[:5])
|
| 296 |
+
if len(functions) > 5:
|
| 297 |
+
func_names += f", ... ({len(functions) - 5} more)"
|
| 298 |
+
summary.append(f"Functions ({len(functions)}): {func_names}")
|
| 299 |
+
|
| 300 |
+
# Classes
|
| 301 |
+
classes = result.get('classes', [])
|
| 302 |
+
if classes:
|
| 303 |
+
class_names = ', '.join(c['name'] for c in classes[:3])
|
| 304 |
+
if len(classes) > 3:
|
| 305 |
+
class_names += f", ... ({len(classes) - 3} more)"
|
| 306 |
+
summary.append(f"Classes ({len(classes)}): {class_names}")
|
| 307 |
+
|
| 308 |
+
# Imports
|
| 309 |
+
imports = result.get('imports', [])
|
| 310 |
+
if imports:
|
| 311 |
+
# Get unique module names
|
| 312 |
+
modules = set()
|
| 313 |
+
for imp in imports:
|
| 314 |
+
if imp['type'] == 'import':
|
| 315 |
+
modules.add(imp['name'].split('.')[0])
|
| 316 |
+
else:
|
| 317 |
+
modules.add(imp.get('module', '').split('.')[0] if imp.get('module') else imp['name'])
|
| 318 |
+
|
| 319 |
+
import_list = ', '.join(sorted(modules)[:5])
|
| 320 |
+
if len(modules) > 5:
|
| 321 |
+
import_list += f", ... ({len(modules) - 5} more)"
|
| 322 |
+
summary.append(f"Imports: {import_list}")
|
| 323 |
+
|
| 324 |
+
return '\n'.join(summary)
|
| 325 |
+
|
| 326 |
+
except Exception as e:
|
| 327 |
+
return f"Error generating summary: {str(e)}"
|
codepilot/context/selector.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Context Selector
|
| 3 |
+
Builds dependency graph and selects relevant code for LLM context
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import networkx as nx
|
| 7 |
+
from codepilot.context.indexer import CodebaseIndexer
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class ContextSelector:
|
| 11 |
+
"""
|
| 12 |
+
Select relevant code context based on dependencies
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
def __init__(self, indexer: CodebaseIndexer):
|
| 16 |
+
"""
|
| 17 |
+
Initialize with a codebase indexer
|
| 18 |
+
|
| 19 |
+
Args:
|
| 20 |
+
indexer: CodebaseIndexer with already-built index
|
| 21 |
+
"""
|
| 22 |
+
self.indexer = indexer # Store the indexer (has all import data)
|
| 23 |
+
self.graph = nx.DiGraph() # Create empty directed graph
|
| 24 |
+
|
| 25 |
+
def build_dependency_graph(self):
|
| 26 |
+
"""
|
| 27 |
+
Build a directed graph where:
|
| 28 |
+
- Each node is a file
|
| 29 |
+
- Each edge A → B means "A imports from B"
|
| 30 |
+
"""
|
| 31 |
+
# Loop through every file in the index
|
| 32 |
+
for file_path, data in self.indexer.index.items():
|
| 33 |
+
|
| 34 |
+
# Get imports for this file
|
| 35 |
+
imports = data['imports']
|
| 36 |
+
|
| 37 |
+
# Loop through each import
|
| 38 |
+
for imp in imports:
|
| 39 |
+
# Get the module name (e.g., 'codepilot.llm.client')
|
| 40 |
+
module_name = imp.get('module', '')
|
| 41 |
+
|
| 42 |
+
if module_name:
|
| 43 |
+
# Convert to file path: 'codepilot.llm.client' → 'codepilot/llm/client.py'
|
| 44 |
+
target_path = module_name.replace('.', '/') + '.py'
|
| 45 |
+
|
| 46 |
+
# Check if this file exists in our index
|
| 47 |
+
# (we only care about files in our project, not external like 'os' or 'json')
|
| 48 |
+
for indexed_file in self.indexer.index.keys():
|
| 49 |
+
if indexed_file.endswith(target_path):
|
| 50 |
+
# Add edge: file_path depends on indexed_file
|
| 51 |
+
self.graph.add_edge(file_path, indexed_file)
|
| 52 |
+
break
|
| 53 |
+
|
| 54 |
+
print(f"Graph built: {self.graph.number_of_nodes()} files, {self.graph.number_of_edges()} dependencies")
|
| 55 |
+
|
| 56 |
+
def get_dependencies(self, file_path: str) -> list:
|
| 57 |
+
"""
|
| 58 |
+
Get all files that this file imports from
|
| 59 |
+
|
| 60 |
+
Args:
|
| 61 |
+
file_path: The file to check
|
| 62 |
+
|
| 63 |
+
Returns:
|
| 64 |
+
List of file paths that this file depends on
|
| 65 |
+
"""
|
| 66 |
+
if file_path not in self.graph:
|
| 67 |
+
return []
|
| 68 |
+
return list(self.graph.successors(file_path))
|
| 69 |
+
|
| 70 |
+
def get_dependents(self, file_path: str) -> list:
|
| 71 |
+
"""
|
| 72 |
+
Get all files that import from this file
|
| 73 |
+
|
| 74 |
+
Args:
|
| 75 |
+
file_path: The file to check
|
| 76 |
+
|
| 77 |
+
Returns:
|
| 78 |
+
List of file paths that depend on this file
|
| 79 |
+
"""
|
| 80 |
+
if file_path not in self.graph:
|
| 81 |
+
return []
|
| 82 |
+
return list(self.graph.predecessors(file_path))
|
| 83 |
+
|
| 84 |
+
def get_related_files(self, file_path: str) -> list:
|
| 85 |
+
"""
|
| 86 |
+
Get all files related to this file (both directions)
|
| 87 |
+
|
| 88 |
+
Args:
|
| 89 |
+
file_path: The file to check
|
| 90 |
+
|
| 91 |
+
Returns:
|
| 92 |
+
List of all related file paths
|
| 93 |
+
"""
|
| 94 |
+
related = set() # Use set to avoid duplicates
|
| 95 |
+
|
| 96 |
+
# Files this one depends on
|
| 97 |
+
related.update(self.get_dependencies(file_path))
|
| 98 |
+
|
| 99 |
+
# Files that depend on this one
|
| 100 |
+
related.update(self.get_dependents(file_path))
|
| 101 |
+
|
| 102 |
+
return list(related)
|
codepilot/llm/__init__.py
ADDED
|
File without changes
|
codepilot/llm/client.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
OpenAI Client Wrapper
|
| 3 |
+
Handles all communication with OpenAI's API
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
from dotenv import load_dotenv
|
| 8 |
+
import openai
|
| 9 |
+
from typing import List, Dict, Optional
|
| 10 |
+
|
| 11 |
+
load_dotenv()
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class OpenAIClient:
|
| 15 |
+
"""Wrapper for OpenAI API calls"""
|
| 16 |
+
|
| 17 |
+
def __init__(self, model: str = "gpt-3.5-turbo"):
|
| 18 |
+
"""
|
| 19 |
+
Initialize OpenAI client
|
| 20 |
+
|
| 21 |
+
Args:
|
| 22 |
+
model: OpenAI model to use (default: gpt-3.5-turbo)
|
| 23 |
+
"""
|
| 24 |
+
self.api_key = os.getenv('OPENAI_API_KEY')
|
| 25 |
+
|
| 26 |
+
if not self.api_key:
|
| 27 |
+
raise ValueError("OPENAI_API_KEY not found in environment variables")
|
| 28 |
+
|
| 29 |
+
self.client = openai.OpenAI(api_key=self.api_key)
|
| 30 |
+
self.model = model
|
| 31 |
+
|
| 32 |
+
print(f"✅ OpenAI Client initialized with model: {self.model}")
|
| 33 |
+
|
| 34 |
+
def chat(
|
| 35 |
+
self,
|
| 36 |
+
messages: List[Dict[str, str]],
|
| 37 |
+
tools: Optional[List[Dict]] = None,
|
| 38 |
+
temperature: float = 0.7,
|
| 39 |
+
max_tokens: int = 2000
|
| 40 |
+
) -> openai.types.chat.ChatCompletion:
|
| 41 |
+
"""
|
| 42 |
+
Send a chat completion request to OpenAI
|
| 43 |
+
|
| 44 |
+
Args:
|
| 45 |
+
messages: List of message dicts with 'role' and 'content'
|
| 46 |
+
tools: Optional list of tool definitions for function calling
|
| 47 |
+
temperature: Randomness (0-2, lower = more focused)
|
| 48 |
+
max_tokens: Maximum tokens in response
|
| 49 |
+
|
| 50 |
+
Returns:
|
| 51 |
+
OpenAI ChatCompletion response object
|
| 52 |
+
"""
|
| 53 |
+
try:
|
| 54 |
+
# Build request parameters
|
| 55 |
+
request_params = {
|
| 56 |
+
"model": self.model,
|
| 57 |
+
"messages": messages,
|
| 58 |
+
"temperature": temperature,
|
| 59 |
+
"max_tokens": max_tokens
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
# Add tools if provided
|
| 63 |
+
if tools:
|
| 64 |
+
request_params["tools"] = tools
|
| 65 |
+
request_params["tool_choice"] = "auto"
|
| 66 |
+
|
| 67 |
+
# Make API call
|
| 68 |
+
response = self.client.chat.completions.create(**request_params)
|
| 69 |
+
|
| 70 |
+
# Print token usage for cost tracking
|
| 71 |
+
usage = response.usage
|
| 72 |
+
print(f"📊 Tokens: {usage.prompt_tokens} prompt + {usage.completion_tokens} completion = {usage.total_tokens} total")
|
| 73 |
+
|
| 74 |
+
return response
|
| 75 |
+
|
| 76 |
+
except openai.APIError as e:
|
| 77 |
+
print(f"❌ OpenAI API Error: {e}")
|
| 78 |
+
raise
|
| 79 |
+
except openai.RateLimitError as e:
|
| 80 |
+
print(f"❌ Rate Limit Error: {e}")
|
| 81 |
+
raise
|
| 82 |
+
except Exception as e:
|
| 83 |
+
print(f"❌ Unexpected Error: {e}")
|
| 84 |
+
raise
|
codepilot/sandbox/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
E2B Sandbox Integration
|
| 3 |
+
|
| 4 |
+
Provides safe, isolated code execution for AI agents.
|
| 5 |
+
"""
|
codepilot/sandbox/e2b_sandbox.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
E2B Sandbox Manager
|
| 3 |
+
|
| 4 |
+
Manages lifecycle of E2B sandboxes for safe code execution.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from e2b_code_interpreter.code_interpreter_sync import Sandbox
|
| 8 |
+
from typing import Dict, Any, Optional
|
| 9 |
+
import os
|
| 10 |
+
from dotenv import load_dotenv
|
| 11 |
+
|
| 12 |
+
# Load environment variables from .env file
|
| 13 |
+
load_dotenv()
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class E2BSandboxManager:
|
| 17 |
+
"""
|
| 18 |
+
Manages E2B sandbox instances for isolated code execution.
|
| 19 |
+
|
| 20 |
+
The sandbox provides:
|
| 21 |
+
- Isolated filesystem (files don't affect host)
|
| 22 |
+
- Safe execution (code can't access host system)
|
| 23 |
+
- Clean environment (starts fresh each time)
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
def __init__(self):
|
| 27 |
+
"""
|
| 28 |
+
Initialize sandbox manager.
|
| 29 |
+
|
| 30 |
+
E2B API key is read from E2B_API_KEY environment variable.
|
| 31 |
+
"""
|
| 32 |
+
if not os.getenv("E2B_API_KEY"):
|
| 33 |
+
raise ValueError("E2B_API_KEY not found in environment variables")
|
| 34 |
+
|
| 35 |
+
self.sandbox: Optional[Sandbox] = None
|
| 36 |
+
self._is_open = False
|
| 37 |
+
|
| 38 |
+
def create(self) -> str:
|
| 39 |
+
"""
|
| 40 |
+
Create a new sandbox instance.
|
| 41 |
+
|
| 42 |
+
Returns:
|
| 43 |
+
Sandbox ID
|
| 44 |
+
"""
|
| 45 |
+
if self._is_open:
|
| 46 |
+
return f"Sandbox already running (ID: {self.sandbox.sandbox_id})"
|
| 47 |
+
|
| 48 |
+
try:
|
| 49 |
+
api_key = os.getenv("E2B_API_KEY")
|
| 50 |
+
self.sandbox = Sandbox.create(api_key=api_key)
|
| 51 |
+
self._is_open = True
|
| 52 |
+
return f"✅ Sandbox created (ID: {self.sandbox.sandbox_id})"
|
| 53 |
+
except Exception as e:
|
| 54 |
+
return f"❌ Error creating sandbox: {str(e)}"
|
| 55 |
+
|
| 56 |
+
def close(self) -> str:
|
| 57 |
+
"""
|
| 58 |
+
Close and destroy the sandbox.
|
| 59 |
+
|
| 60 |
+
Returns:
|
| 61 |
+
Success message
|
| 62 |
+
"""
|
| 63 |
+
if not self._is_open:
|
| 64 |
+
return "No sandbox to close"
|
| 65 |
+
|
| 66 |
+
try:
|
| 67 |
+
if self.sandbox:
|
| 68 |
+
self.sandbox.kill()
|
| 69 |
+
self._is_open = False
|
| 70 |
+
return "✅ Sandbox closed"
|
| 71 |
+
except Exception as e:
|
| 72 |
+
return f"❌ Error closing sandbox: {str(e)}"
|
| 73 |
+
|
| 74 |
+
def upload_file(self, path: str, content: str) -> str:
|
| 75 |
+
"""
|
| 76 |
+
Upload a file to the sandbox.
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
path: Path in sandbox where file should be written
|
| 80 |
+
content: File content
|
| 81 |
+
|
| 82 |
+
Returns:
|
| 83 |
+
Success or error message
|
| 84 |
+
"""
|
| 85 |
+
if not self._is_open:
|
| 86 |
+
return "❌ No sandbox running. Create one first."
|
| 87 |
+
|
| 88 |
+
try:
|
| 89 |
+
self.sandbox.files.write(path, content)
|
| 90 |
+
return f"✅ Uploaded file to sandbox: {path} ({len(content)} chars)"
|
| 91 |
+
except Exception as e:
|
| 92 |
+
return f"❌ Error uploading file: {str(e)}"
|
| 93 |
+
|
| 94 |
+
def run_code(self, code: str, language: str = "python") -> Dict[str, Any]:
|
| 95 |
+
"""
|
| 96 |
+
Execute code in the sandbox.
|
| 97 |
+
|
| 98 |
+
Args:
|
| 99 |
+
code: Code to execute
|
| 100 |
+
language: Programming language (default: python)
|
| 101 |
+
|
| 102 |
+
Returns:
|
| 103 |
+
Dict with stdout, stderr, exit_code, and error (if any)
|
| 104 |
+
"""
|
| 105 |
+
if not self._is_open:
|
| 106 |
+
return {
|
| 107 |
+
"stdout": "",
|
| 108 |
+
"stderr": "❌ No sandbox running. Create one first.",
|
| 109 |
+
"exit_code": 1,
|
| 110 |
+
"error": "No sandbox"
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
try:
|
| 114 |
+
# Execute code in sandbox
|
| 115 |
+
execution = self.sandbox.run_code(code)
|
| 116 |
+
|
| 117 |
+
return {
|
| 118 |
+
"stdout": execution.text or "",
|
| 119 |
+
"stderr": execution.error or "",
|
| 120 |
+
"exit_code": 0 if not execution.error else 1,
|
| 121 |
+
"error": None
|
| 122 |
+
}
|
| 123 |
+
except Exception as e:
|
| 124 |
+
return {
|
| 125 |
+
"stdout": "",
|
| 126 |
+
"stderr": str(e),
|
| 127 |
+
"exit_code": 1,
|
| 128 |
+
"error": str(e)
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
def run_command(self, command: str) -> Dict[str, Any]:
|
| 132 |
+
"""
|
| 133 |
+
Run a shell command in the sandbox.
|
| 134 |
+
|
| 135 |
+
Args:
|
| 136 |
+
command: Shell command to execute
|
| 137 |
+
|
| 138 |
+
Returns:
|
| 139 |
+
Dict with stdout, stderr, exit_code
|
| 140 |
+
"""
|
| 141 |
+
if not self._is_open:
|
| 142 |
+
return {
|
| 143 |
+
"stdout": "",
|
| 144 |
+
"stderr": "❌ No sandbox running. Create one first.",
|
| 145 |
+
"exit_code": 1
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
try:
|
| 149 |
+
# Run shell command
|
| 150 |
+
process = self.sandbox.commands.run(command)
|
| 151 |
+
|
| 152 |
+
return {
|
| 153 |
+
"stdout": process.stdout,
|
| 154 |
+
"stderr": process.stderr,
|
| 155 |
+
"exit_code": process.exit_code
|
| 156 |
+
}
|
| 157 |
+
except Exception as e:
|
| 158 |
+
return {
|
| 159 |
+
"stdout": "",
|
| 160 |
+
"stderr": str(e),
|
| 161 |
+
"exit_code": 1
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
def list_files(self, path: str = ".") -> str:
|
| 165 |
+
"""
|
| 166 |
+
List files in sandbox directory.
|
| 167 |
+
|
| 168 |
+
Args:
|
| 169 |
+
path: Directory path to list
|
| 170 |
+
|
| 171 |
+
Returns:
|
| 172 |
+
List of files as string
|
| 173 |
+
"""
|
| 174 |
+
if not self._is_open:
|
| 175 |
+
return "❌ No sandbox running. Create one first."
|
| 176 |
+
|
| 177 |
+
try:
|
| 178 |
+
result = self.sandbox.commands.run(f"ls -la {path}")
|
| 179 |
+
return result.stdout
|
| 180 |
+
except Exception as e:
|
| 181 |
+
return f"❌ Error listing files: {str(e)}"
|
| 182 |
+
|
| 183 |
+
def read_file(self, path: str) -> str:
|
| 184 |
+
"""
|
| 185 |
+
Read a file from the sandbox.
|
| 186 |
+
|
| 187 |
+
Args:
|
| 188 |
+
path: File path in sandbox
|
| 189 |
+
|
| 190 |
+
Returns:
|
| 191 |
+
File contents or error message
|
| 192 |
+
"""
|
| 193 |
+
if not self._is_open:
|
| 194 |
+
return "❌ No sandbox running. Create one first."
|
| 195 |
+
|
| 196 |
+
try:
|
| 197 |
+
content = self.sandbox.files.read(path)
|
| 198 |
+
return content
|
| 199 |
+
except Exception as e:
|
| 200 |
+
return f"❌ Error reading file: {str(e)}"
|
| 201 |
+
|
| 202 |
+
def is_running(self) -> bool:
|
| 203 |
+
"""Check if sandbox is currently running."""
|
| 204 |
+
return self._is_open
|
| 205 |
+
|
| 206 |
+
def __enter__(self):
|
| 207 |
+
"""Context manager support: with E2BSandboxManager() as sandbox:"""
|
| 208 |
+
self.create()
|
| 209 |
+
return self
|
| 210 |
+
|
| 211 |
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
| 212 |
+
"""Context manager support: automatically close on exit"""
|
| 213 |
+
self.close()
|
codepilot/sandbox/sandbox_tools.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Sandbox Tools for AI Agents
|
| 3 |
+
|
| 4 |
+
These tools allow agents to safely execute code in isolated environments.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from codepilot.sandbox.e2b_sandbox import E2BSandboxManager
|
| 8 |
+
from typing import Dict, Any
|
| 9 |
+
|
| 10 |
+
# Global sandbox instance (shared across tool calls)
|
| 11 |
+
_sandbox_manager: E2BSandboxManager = None
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def create_sandbox() -> str:
|
| 15 |
+
"""
|
| 16 |
+
Create a new E2B sandbox for code execution.
|
| 17 |
+
|
| 18 |
+
Returns:
|
| 19 |
+
Success message with sandbox ID
|
| 20 |
+
"""
|
| 21 |
+
global _sandbox_manager
|
| 22 |
+
|
| 23 |
+
try:
|
| 24 |
+
_sandbox_manager = E2BSandboxManager()
|
| 25 |
+
return _sandbox_manager.create()
|
| 26 |
+
except Exception as e:
|
| 27 |
+
return f"❌ Failed to create sandbox: {str(e)}"
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def close_sandbox() -> str:
|
| 31 |
+
"""
|
| 32 |
+
Close and destroy the current sandbox.
|
| 33 |
+
|
| 34 |
+
Returns:
|
| 35 |
+
Success message
|
| 36 |
+
"""
|
| 37 |
+
global _sandbox_manager
|
| 38 |
+
|
| 39 |
+
if _sandbox_manager is None:
|
| 40 |
+
return "No sandbox to close"
|
| 41 |
+
|
| 42 |
+
result = _sandbox_manager.close()
|
| 43 |
+
_sandbox_manager = None
|
| 44 |
+
return result
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def upload_to_sandbox(path: str, content: str) -> str:
|
| 48 |
+
"""
|
| 49 |
+
Upload a file to the sandbox.
|
| 50 |
+
|
| 51 |
+
Args:
|
| 52 |
+
path: Path where file should be written in sandbox (e.g., "test.py")
|
| 53 |
+
content: File content to upload
|
| 54 |
+
|
| 55 |
+
Returns:
|
| 56 |
+
Success or error message
|
| 57 |
+
"""
|
| 58 |
+
global _sandbox_manager
|
| 59 |
+
|
| 60 |
+
if _sandbox_manager is None or not _sandbox_manager.is_running():
|
| 61 |
+
# Auto-create sandbox if it doesn't exist
|
| 62 |
+
create_result = create_sandbox()
|
| 63 |
+
if "❌" in create_result:
|
| 64 |
+
return create_result
|
| 65 |
+
|
| 66 |
+
return _sandbox_manager.upload_file(path, content)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def execute_in_sandbox(code: str) -> str:
|
| 70 |
+
"""
|
| 71 |
+
Execute Python code in the sandbox.
|
| 72 |
+
|
| 73 |
+
Args:
|
| 74 |
+
code: Python code to execute
|
| 75 |
+
|
| 76 |
+
Returns:
|
| 77 |
+
Formatted output with stdout and stderr
|
| 78 |
+
"""
|
| 79 |
+
global _sandbox_manager
|
| 80 |
+
|
| 81 |
+
if _sandbox_manager is None or not _sandbox_manager.is_running():
|
| 82 |
+
# Auto-create sandbox if it doesn't exist
|
| 83 |
+
create_result = create_sandbox()
|
| 84 |
+
if "❌" in create_result:
|
| 85 |
+
return create_result
|
| 86 |
+
|
| 87 |
+
result = _sandbox_manager.run_code(code)
|
| 88 |
+
|
| 89 |
+
# Format the output nicely
|
| 90 |
+
output = []
|
| 91 |
+
if result["stdout"]:
|
| 92 |
+
output.append(f"📤 Output:\n{result['stdout']}")
|
| 93 |
+
if result["stderr"]:
|
| 94 |
+
output.append(f"⚠️ Errors:\n{result['stderr']}")
|
| 95 |
+
if result.get("error"):
|
| 96 |
+
output.append(f"❌ Error: {result['error']}")
|
| 97 |
+
|
| 98 |
+
return "\n\n".join(output) if output else "✅ Code executed successfully (no output)"
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def run_command_in_sandbox(command: str) -> str:
|
| 102 |
+
"""
|
| 103 |
+
Run a shell command in the sandbox.
|
| 104 |
+
|
| 105 |
+
Args:
|
| 106 |
+
command: Shell command to execute (e.g., "python test.py", "pytest")
|
| 107 |
+
|
| 108 |
+
Returns:
|
| 109 |
+
Command output
|
| 110 |
+
"""
|
| 111 |
+
global _sandbox_manager
|
| 112 |
+
|
| 113 |
+
if _sandbox_manager is None or not _sandbox_manager.is_running():
|
| 114 |
+
# Auto-create sandbox if it doesn't exist
|
| 115 |
+
create_result = create_sandbox()
|
| 116 |
+
if "❌" in create_result:
|
| 117 |
+
return create_result
|
| 118 |
+
|
| 119 |
+
result = _sandbox_manager.run_command(command)
|
| 120 |
+
|
| 121 |
+
# Format the output
|
| 122 |
+
output = []
|
| 123 |
+
if result["stdout"]:
|
| 124 |
+
output.append(f"📤 Output:\n{result['stdout']}")
|
| 125 |
+
if result["stderr"]:
|
| 126 |
+
output.append(f"⚠️ Errors:\n{result['stderr']}")
|
| 127 |
+
if result["exit_code"] != 0:
|
| 128 |
+
output.append(f"❌ Exit code: {result['exit_code']}")
|
| 129 |
+
|
| 130 |
+
return "\n\n".join(output) if output else "✅ Command executed successfully (no output)"
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def list_sandbox_files(path: str = ".") -> str:
|
| 134 |
+
"""
|
| 135 |
+
List files in the sandbox directory.
|
| 136 |
+
|
| 137 |
+
Args:
|
| 138 |
+
path: Directory path to list (default: current directory)
|
| 139 |
+
|
| 140 |
+
Returns:
|
| 141 |
+
List of files
|
| 142 |
+
"""
|
| 143 |
+
global _sandbox_manager
|
| 144 |
+
|
| 145 |
+
if _sandbox_manager is None or not _sandbox_manager.is_running():
|
| 146 |
+
return "❌ No sandbox running. Create one first."
|
| 147 |
+
|
| 148 |
+
return _sandbox_manager.list_files(path)
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def read_sandbox_file(path: str) -> str:
|
| 152 |
+
"""
|
| 153 |
+
Read a file from the sandbox.
|
| 154 |
+
|
| 155 |
+
Args:
|
| 156 |
+
path: File path in sandbox
|
| 157 |
+
|
| 158 |
+
Returns:
|
| 159 |
+
File contents
|
| 160 |
+
"""
|
| 161 |
+
global _sandbox_manager
|
| 162 |
+
|
| 163 |
+
if _sandbox_manager is None or not _sandbox_manager.is_running():
|
| 164 |
+
return "❌ No sandbox running. Create one first."
|
| 165 |
+
|
| 166 |
+
return _sandbox_manager.read_file(path)
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
# Helper function to get current sandbox status
|
| 170 |
+
def get_sandbox_status() -> str:
|
| 171 |
+
"""
|
| 172 |
+
Get the current sandbox status.
|
| 173 |
+
|
| 174 |
+
Returns:
|
| 175 |
+
Status message
|
| 176 |
+
"""
|
| 177 |
+
global _sandbox_manager
|
| 178 |
+
|
| 179 |
+
if _sandbox_manager is None:
|
| 180 |
+
return "No sandbox created"
|
| 181 |
+
elif _sandbox_manager.is_running():
|
| 182 |
+
return f"✅ Sandbox running (ID: {_sandbox_manager.sandbox.id})"
|
| 183 |
+
else:
|
| 184 |
+
return "Sandbox closed"
|
codepilot/tools/__init__.py
ADDED
|
File without changes
|
codepilot/tools/context_tools.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Context Tools
|
| 3 |
+
Tools that use the codebase index and dependency graph
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from codepilot.context.indexer import CodebaseIndexer
|
| 7 |
+
from codepilot.context.selector import ContextSelector
|
| 8 |
+
from codepilot.context.hybrid_retriever import HybridRetriever
|
| 9 |
+
from typing import List, Dict, Any
|
| 10 |
+
|
| 11 |
+
# Global instances (set when index_codebase is called)
|
| 12 |
+
_indexer = None
|
| 13 |
+
_selector = None
|
| 14 |
+
_hybrid_retriever = None # Will hold our search engine
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def index_codebase(path: str = ".") -> str:
|
| 18 |
+
"""
|
| 19 |
+
Index a codebase to enable context-aware tools.
|
| 20 |
+
|
| 21 |
+
This builds THREE indexes:
|
| 22 |
+
1. CodebaseIndexer - AST-based parsing of all files
|
| 23 |
+
2. ContextSelector - Dependency graph
|
| 24 |
+
3. HybridRetriever - BM25 + Embeddings for search
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
path: Root directory to index (default: current directory)
|
| 28 |
+
|
| 29 |
+
Returns:
|
| 30 |
+
Summary of what was indexed
|
| 31 |
+
"""
|
| 32 |
+
global _indexer, _selector, _hybrid_retriever
|
| 33 |
+
|
| 34 |
+
# Step 1: Create indexer and build AST index
|
| 35 |
+
_indexer = CodebaseIndexer(path)
|
| 36 |
+
stats = _indexer.build_index()
|
| 37 |
+
|
| 38 |
+
# Step 2: Create selector and build dependency graph
|
| 39 |
+
_selector = ContextSelector(_indexer)
|
| 40 |
+
_selector.build_dependency_graph()
|
| 41 |
+
|
| 42 |
+
# Step 3: Build hybrid retriever index
|
| 43 |
+
# Convert indexed data to documents for retrieval
|
| 44 |
+
documents = []
|
| 45 |
+
for file_path, file_data in _indexer.index.items():
|
| 46 |
+
# Read the source file to extract code snippets
|
| 47 |
+
try:
|
| 48 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
| 49 |
+
source_lines = f.readlines()
|
| 50 |
+
except:
|
| 51 |
+
continue # Skip if file can't be read
|
| 52 |
+
|
| 53 |
+
# Add each function as a searchable document
|
| 54 |
+
for func in file_data.get('functions', []):
|
| 55 |
+
start = func.get('start_line', 1) - 1 # Convert to 0-indexed
|
| 56 |
+
end = func.get('end_line', start + 1)
|
| 57 |
+
|
| 58 |
+
# Extract code lines
|
| 59 |
+
code = ''.join(source_lines[start:end])
|
| 60 |
+
|
| 61 |
+
if code.strip(): # Only add if we got code
|
| 62 |
+
documents.append({
|
| 63 |
+
'content': code,
|
| 64 |
+
'file': file_path,
|
| 65 |
+
'name': func['name'],
|
| 66 |
+
'type': 'function',
|
| 67 |
+
'start_line': func.get('start_line', 0),
|
| 68 |
+
'end_line': func.get('end_line', 0)
|
| 69 |
+
})
|
| 70 |
+
|
| 71 |
+
# Add each class as a searchable document
|
| 72 |
+
for cls in file_data.get('classes', []):
|
| 73 |
+
start = cls.get('start_line', 1) - 1
|
| 74 |
+
end = cls.get('end_line', start + 1)
|
| 75 |
+
|
| 76 |
+
code = ''.join(source_lines[start:end])
|
| 77 |
+
|
| 78 |
+
if code.strip():
|
| 79 |
+
documents.append({
|
| 80 |
+
'content': code,
|
| 81 |
+
'file': file_path,
|
| 82 |
+
'name': cls['name'],
|
| 83 |
+
'type': 'class',
|
| 84 |
+
'start_line': cls.get('start_line', 0),
|
| 85 |
+
'end_line': cls.get('end_line', 0)
|
| 86 |
+
})
|
| 87 |
+
|
| 88 |
+
# Create and index hybrid retriever
|
| 89 |
+
_hybrid_retriever = HybridRetriever()
|
| 90 |
+
retrieval_stats = _hybrid_retriever.index_documents(documents)
|
| 91 |
+
|
| 92 |
+
# Return summary
|
| 93 |
+
return (
|
| 94 |
+
f"Indexed {stats['total_files']} files, "
|
| 95 |
+
f"{stats['total_functions']} functions, "
|
| 96 |
+
f"{stats['total_classes']} classes. "
|
| 97 |
+
f"Dependency graph: {_selector.graph.number_of_edges()} connections. "
|
| 98 |
+
f"Hybrid retriever: {retrieval_stats['bm25_indexed']} BM25 docs, "
|
| 99 |
+
f"{retrieval_stats['embedding_indexed']} embedding docs."
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def search_codebase(query: str, top_k: int = 5) -> str:
|
| 104 |
+
"""
|
| 105 |
+
Search the codebase using hybrid retrieval (BM25 + embeddings).
|
| 106 |
+
|
| 107 |
+
Uses both keyword matching and semantic search to find relevant code.
|
| 108 |
+
|
| 109 |
+
Args:
|
| 110 |
+
query: What to search for (e.g., "authentication logic", "error handling")
|
| 111 |
+
top_k: Number of results to return (default: 5)
|
| 112 |
+
|
| 113 |
+
Returns:
|
| 114 |
+
Formatted string with search results including file paths, function names, and code snippets
|
| 115 |
+
"""
|
| 116 |
+
global _hybrid_retriever
|
| 117 |
+
|
| 118 |
+
# Check if index is built
|
| 119 |
+
if _hybrid_retriever is None:
|
| 120 |
+
return "Error: Codebase not indexed. Call index_codebase() first."
|
| 121 |
+
|
| 122 |
+
# Perform hybrid search
|
| 123 |
+
results = _hybrid_retriever.search(query, top_k=top_k)
|
| 124 |
+
|
| 125 |
+
if not results:
|
| 126 |
+
return f"No results found for query: '{query}'"
|
| 127 |
+
|
| 128 |
+
# Format results for the agent
|
| 129 |
+
output = [f"Found {len(results)} results for '{query}':\n"]
|
| 130 |
+
|
| 131 |
+
for result in results:
|
| 132 |
+
output.append(f"\n[{result['rank']}] {result['type']}: {result['name']}")
|
| 133 |
+
output.append(f" File: {result['file']}:{result['start_line']}")
|
| 134 |
+
output.append(f" Score: {result['rrf_score']:.4f}")
|
| 135 |
+
output.append(f" In BM25: {result['in_bm25']}, In Embeddings: {result['in_embeddings']}")
|
| 136 |
+
|
| 137 |
+
# Show code snippet (first 3 lines)
|
| 138 |
+
code_lines = result['content'].split('\n')[:3]
|
| 139 |
+
output.append(f" Code preview:")
|
| 140 |
+
for line in code_lines:
|
| 141 |
+
output.append(f" {line}")
|
| 142 |
+
|
| 143 |
+
return '\n'.join(output)
|
codepilot/tools/file_tools.py
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
File operation tools for the agent
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import subprocess
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def read_file(path):
|
| 10 |
+
"""
|
| 11 |
+
Reads and returns the contents of a file.
|
| 12 |
+
|
| 13 |
+
Args:
|
| 14 |
+
path: File path to read
|
| 15 |
+
|
| 16 |
+
Returns:
|
| 17 |
+
str: File contents or error message
|
| 18 |
+
"""
|
| 19 |
+
try:
|
| 20 |
+
with open(path, 'r') as f:
|
| 21 |
+
content = f.read()
|
| 22 |
+
return f"Successfully read file '{path}':\n\n{content}"
|
| 23 |
+
except FileNotFoundError:
|
| 24 |
+
return f"Error: File '{path}' not found."
|
| 25 |
+
except PermissionError:
|
| 26 |
+
return f"Error: Permission denied to read file '{path}'."
|
| 27 |
+
except Exception as e:
|
| 28 |
+
return f"Error reading file '{path}': {str(e)}"
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def write_file(path, content):
|
| 32 |
+
"""
|
| 33 |
+
Writes content to a file, creating it if it doesn't exist.
|
| 34 |
+
|
| 35 |
+
Args:
|
| 36 |
+
path: File path to write to
|
| 37 |
+
content: Content to write
|
| 38 |
+
|
| 39 |
+
Returns:
|
| 40 |
+
str: Success or error message
|
| 41 |
+
"""
|
| 42 |
+
try:
|
| 43 |
+
# Create directory if it doesn't exist
|
| 44 |
+
directory = os.path.dirname(path)
|
| 45 |
+
if directory and not os.path.exists(directory):
|
| 46 |
+
os.makedirs(directory)
|
| 47 |
+
|
| 48 |
+
with open(path, 'w') as f:
|
| 49 |
+
f.write(content)
|
| 50 |
+
|
| 51 |
+
return f"Successfully wrote {len(content)} characters to '{path}'."
|
| 52 |
+
except PermissionError:
|
| 53 |
+
return f"Error: Permission denied to write to '{path}'."
|
| 54 |
+
except Exception as e:
|
| 55 |
+
return f"Error writing to file '{path}': {str(e)}"
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def run_command(command):
|
| 59 |
+
"""
|
| 60 |
+
Executes a shell command and returns the output.
|
| 61 |
+
|
| 62 |
+
Args:
|
| 63 |
+
command: Shell command to execute
|
| 64 |
+
|
| 65 |
+
Returns:
|
| 66 |
+
str: Command output or error message
|
| 67 |
+
"""
|
| 68 |
+
try:
|
| 69 |
+
result = subprocess.run(
|
| 70 |
+
command,
|
| 71 |
+
shell=True,
|
| 72 |
+
capture_output=True,
|
| 73 |
+
text=True,
|
| 74 |
+
timeout=30
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
output = []
|
| 78 |
+
if result.stdout:
|
| 79 |
+
output.append(f"Output:\n{result.stdout}")
|
| 80 |
+
if result.stderr:
|
| 81 |
+
output.append(f"Errors:\n{result.stderr}")
|
| 82 |
+
|
| 83 |
+
status = "succeeded" if result.returncode == 0 else f"failed (exit code {result.returncode})"
|
| 84 |
+
output.insert(0, f"Command '{command}' {status}.")
|
| 85 |
+
|
| 86 |
+
return "\n\n".join(output)
|
| 87 |
+
|
| 88 |
+
except subprocess.TimeoutExpired:
|
| 89 |
+
return f"Error: Command '{command}' timed out after 30 seconds."
|
| 90 |
+
except Exception as e:
|
| 91 |
+
return f"Error executing command '{command}': {str(e)}"
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def search_code(pattern, path=".", file_extension=None):
|
| 95 |
+
"""
|
| 96 |
+
Search for a pattern in code files (like grep).
|
| 97 |
+
|
| 98 |
+
Args:
|
| 99 |
+
pattern: Text pattern to search for
|
| 100 |
+
path: Directory to search in (default: current directory)
|
| 101 |
+
file_extension: Optional file extension filter (e.g., "py", "js")
|
| 102 |
+
|
| 103 |
+
Returns:
|
| 104 |
+
str: Search results or error message
|
| 105 |
+
"""
|
| 106 |
+
try:
|
| 107 |
+
# Build grep command
|
| 108 |
+
cmd_parts = ["grep", "-r", "-n", "-i", pattern, path]
|
| 109 |
+
|
| 110 |
+
# Add file extension filter if specified
|
| 111 |
+
if file_extension:
|
| 112 |
+
# Remove leading dot if present
|
| 113 |
+
ext = file_extension.lstrip('.')
|
| 114 |
+
cmd_parts.extend(["--include", f"*.{ext}"])
|
| 115 |
+
|
| 116 |
+
# Exclude common directories
|
| 117 |
+
cmd_parts.extend([
|
| 118 |
+
"--exclude-dir=venv",
|
| 119 |
+
"--exclude-dir=node_modules",
|
| 120 |
+
"--exclude-dir=__pycache__",
|
| 121 |
+
"--exclude-dir=.git"
|
| 122 |
+
])
|
| 123 |
+
|
| 124 |
+
result = subprocess.run(
|
| 125 |
+
cmd_parts,
|
| 126 |
+
capture_output=True,
|
| 127 |
+
text=True,
|
| 128 |
+
timeout=10
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
if result.returncode == 0:
|
| 132 |
+
lines = result.stdout.strip().split('\n')
|
| 133 |
+
# Limit results to prevent overwhelming output
|
| 134 |
+
if len(lines) > 50:
|
| 135 |
+
return f"Found {len(lines)} matches (showing first 50):\n\n" + '\n'.join(lines[:50])
|
| 136 |
+
else:
|
| 137 |
+
return f"Found {len(lines)} matches:\n\n{result.stdout}"
|
| 138 |
+
elif result.returncode == 1:
|
| 139 |
+
return f"No matches found for pattern '{pattern}' in {path}"
|
| 140 |
+
else:
|
| 141 |
+
return f"Error searching: {result.stderr}"
|
| 142 |
+
|
| 143 |
+
except subprocess.TimeoutExpired:
|
| 144 |
+
return f"Error: Search timed out after 10 seconds."
|
| 145 |
+
except Exception as e:
|
| 146 |
+
return f"Error searching for pattern '{pattern}': {str(e)}"
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def list_files(path=".", pattern=None, show_hidden=False):
|
| 150 |
+
"""
|
| 151 |
+
List files and directories.
|
| 152 |
+
|
| 153 |
+
Args:
|
| 154 |
+
path: Directory path to list (default: current directory)
|
| 155 |
+
pattern: Optional glob pattern to filter (e.g., "*.py", "test_*")
|
| 156 |
+
show_hidden: Whether to show hidden files (default: False)
|
| 157 |
+
|
| 158 |
+
Returns:
|
| 159 |
+
str: List of files or error message
|
| 160 |
+
"""
|
| 161 |
+
try:
|
| 162 |
+
import glob
|
| 163 |
+
|
| 164 |
+
# Build the search pattern
|
| 165 |
+
if pattern:
|
| 166 |
+
search_path = os.path.join(path, pattern)
|
| 167 |
+
else:
|
| 168 |
+
search_path = os.path.join(path, "*")
|
| 169 |
+
|
| 170 |
+
# Get all matches
|
| 171 |
+
matches = glob.glob(search_path)
|
| 172 |
+
|
| 173 |
+
# Filter hidden files if needed
|
| 174 |
+
if not show_hidden:
|
| 175 |
+
matches = [m for m in matches if not os.path.basename(m).startswith('.')]
|
| 176 |
+
|
| 177 |
+
if not matches:
|
| 178 |
+
return f"No files found in '{path}'" + (f" matching '{pattern}'" if pattern else "")
|
| 179 |
+
|
| 180 |
+
# Separate files and directories
|
| 181 |
+
files = []
|
| 182 |
+
dirs = []
|
| 183 |
+
|
| 184 |
+
for item in sorted(matches):
|
| 185 |
+
rel_path = os.path.relpath(item, path)
|
| 186 |
+
if os.path.isdir(item):
|
| 187 |
+
dirs.append(f"📁 {rel_path}/")
|
| 188 |
+
else:
|
| 189 |
+
size = os.path.getsize(item)
|
| 190 |
+
files.append(f"📄 {rel_path} ({size} bytes)")
|
| 191 |
+
|
| 192 |
+
result = []
|
| 193 |
+
result.append(f"Contents of '{path}':")
|
| 194 |
+
if pattern:
|
| 195 |
+
result.append(f"(filtered by: {pattern})")
|
| 196 |
+
result.append("")
|
| 197 |
+
|
| 198 |
+
if dirs:
|
| 199 |
+
result.append("Directories:")
|
| 200 |
+
result.extend(dirs)
|
| 201 |
+
result.append("")
|
| 202 |
+
|
| 203 |
+
if files:
|
| 204 |
+
result.append("Files:")
|
| 205 |
+
result.extend(files)
|
| 206 |
+
|
| 207 |
+
result.append(f"\nTotal: {len(dirs)} directories, {len(files)} files")
|
| 208 |
+
|
| 209 |
+
return "\n".join(result)
|
| 210 |
+
|
| 211 |
+
except Exception as e:
|
| 212 |
+
return f"Error listing files in '{path}': {str(e)}"
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def git_status():
|
| 216 |
+
"""
|
| 217 |
+
Get git repository status.
|
| 218 |
+
|
| 219 |
+
Returns:
|
| 220 |
+
str: Git status output or error message
|
| 221 |
+
"""
|
| 222 |
+
try:
|
| 223 |
+
# Check if we're in a git repo
|
| 224 |
+
check_result = subprocess.run(
|
| 225 |
+
["git", "rev-parse", "--git-dir"],
|
| 226 |
+
capture_output=True,
|
| 227 |
+
text=True,
|
| 228 |
+
timeout=5
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
if check_result.returncode != 0:
|
| 232 |
+
return "Not a git repository"
|
| 233 |
+
|
| 234 |
+
# Get status
|
| 235 |
+
result = subprocess.run(
|
| 236 |
+
["git", "status", "--short", "--branch"],
|
| 237 |
+
capture_output=True,
|
| 238 |
+
text=True,
|
| 239 |
+
timeout=5
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
if result.returncode == 0:
|
| 243 |
+
if result.stdout.strip():
|
| 244 |
+
return f"Git Status:\n\n{result.stdout}"
|
| 245 |
+
else:
|
| 246 |
+
return "Git Status: Working tree clean (no changes)"
|
| 247 |
+
else:
|
| 248 |
+
return f"Error getting git status: {result.stderr}"
|
| 249 |
+
|
| 250 |
+
except subprocess.TimeoutExpired:
|
| 251 |
+
return "Error: Git command timed out"
|
| 252 |
+
except FileNotFoundError:
|
| 253 |
+
return "Error: Git is not installed"
|
| 254 |
+
except Exception as e:
|
| 255 |
+
return f"Error checking git status: {str(e)}"
|
codepilot/tools/registry.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tool Registry
|
| 3 |
+
Maps tool names to their implementations and schemas
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
from codepilot.tools.file_tools import read_file, write_file, run_command, search_code, list_files, git_status
|
| 8 |
+
from codepilot.sandbox.sandbox_tools import (
|
| 9 |
+
create_sandbox,
|
| 10 |
+
close_sandbox,
|
| 11 |
+
upload_to_sandbox,
|
| 12 |
+
execute_in_sandbox,
|
| 13 |
+
run_command_in_sandbox
|
| 14 |
+
)
|
| 15 |
+
from typing import Callable, List, Dict, Optional
|
| 16 |
+
|
| 17 |
+
# Check if running in production BEFORE importing heavy ML dependencies
|
| 18 |
+
# Detects: Render, HuggingFace Spaces, or any cloud with PORT env var
|
| 19 |
+
_IS_PRODUCTION = os.getenv('RENDER_SERVICE_NAME') or os.getenv('RENDER') or os.getenv('SPACE_ID') or os.getenv('PORT')
|
| 20 |
+
|
| 21 |
+
# Only import heavy context_tools (sentence-transformers, torch) in local development
|
| 22 |
+
if not _IS_PRODUCTION:
|
| 23 |
+
from codepilot.tools.context_tools import search_codebase, index_codebase
|
| 24 |
+
else:
|
| 25 |
+
# Provide stub functions for production to avoid import errors
|
| 26 |
+
def search_codebase(query: str, top_k: int = 5) -> str:
|
| 27 |
+
return "⚠️ Codebase search is disabled in cloud mode (resource constraints)"
|
| 28 |
+
|
| 29 |
+
def index_codebase(root_path: str) -> str:
|
| 30 |
+
return "⚠️ Codebase indexing is disabled in cloud mode (resource constraints)"
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# Tool schemas for OpenAI function calling
|
| 34 |
+
TOOLS = [
|
| 35 |
+
{
|
| 36 |
+
"type": "function",
|
| 37 |
+
"function": {
|
| 38 |
+
"name": "read_file",
|
| 39 |
+
"description": "Reads the contents of a file at the specified path. Use this when you need to view or analyze file contents.",
|
| 40 |
+
"parameters": {
|
| 41 |
+
"type": "object",
|
| 42 |
+
"properties": {
|
| 43 |
+
"path": {
|
| 44 |
+
"type": "string",
|
| 45 |
+
"description": "The file path to read (absolute or relative path)"
|
| 46 |
+
}
|
| 47 |
+
},
|
| 48 |
+
"required": ["path"]
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
},
|
| 52 |
+
{
|
| 53 |
+
"type": "function",
|
| 54 |
+
"function": {
|
| 55 |
+
"name": "write_file",
|
| 56 |
+
"description": "Writes content to a file at the specified path. Creates the file if it doesn't exist, overwrites if it does. Use this when you need to create or modify files.",
|
| 57 |
+
"parameters": {
|
| 58 |
+
"type": "object",
|
| 59 |
+
"properties": {
|
| 60 |
+
"path": {
|
| 61 |
+
"type": "string",
|
| 62 |
+
"description": "The file path to write to (absolute or relative path)"
|
| 63 |
+
},
|
| 64 |
+
"content": {
|
| 65 |
+
"type": "string",
|
| 66 |
+
"description": "The content to write to the file"
|
| 67 |
+
}
|
| 68 |
+
},
|
| 69 |
+
"required": ["path", "content"]
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
},
|
| 73 |
+
{
|
| 74 |
+
"type": "function",
|
| 75 |
+
"function": {
|
| 76 |
+
"name": "run_command",
|
| 77 |
+
"description": "Executes a shell command in the system terminal. Use this for running scripts, installing packages, or executing system commands.",
|
| 78 |
+
"parameters": {
|
| 79 |
+
"type": "object",
|
| 80 |
+
"properties": {
|
| 81 |
+
"command": {
|
| 82 |
+
"type": "string",
|
| 83 |
+
"description": "The shell command to execute"
|
| 84 |
+
}
|
| 85 |
+
},
|
| 86 |
+
"required": ["command"]
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
+
},
|
| 90 |
+
{
|
| 91 |
+
"type": "function",
|
| 92 |
+
"function": {
|
| 93 |
+
"name": "search_code",
|
| 94 |
+
"description": "Search for a text pattern in code files (like grep). Use this to find where functions, classes, or text appears in the codebase.",
|
| 95 |
+
"parameters": {
|
| 96 |
+
"type": "object",
|
| 97 |
+
"properties": {
|
| 98 |
+
"pattern": {
|
| 99 |
+
"type": "string",
|
| 100 |
+
"description": "The text pattern to search for"
|
| 101 |
+
},
|
| 102 |
+
"path": {
|
| 103 |
+
"type": "string",
|
| 104 |
+
"description": "Directory to search in (default: current directory)"
|
| 105 |
+
},
|
| 106 |
+
"file_extension": {
|
| 107 |
+
"type": "string",
|
| 108 |
+
"description": "Optional file extension filter (e.g., 'py', 'js')"
|
| 109 |
+
}
|
| 110 |
+
},
|
| 111 |
+
"required": ["pattern"]
|
| 112 |
+
}
|
| 113 |
+
}
|
| 114 |
+
},
|
| 115 |
+
{
|
| 116 |
+
"type": "function",
|
| 117 |
+
"function": {
|
| 118 |
+
"name": "list_files",
|
| 119 |
+
"description": "List files and directories in a path. Use this to explore the project structure or find files.",
|
| 120 |
+
"parameters": {
|
| 121 |
+
"type": "object",
|
| 122 |
+
"properties": {
|
| 123 |
+
"path": {
|
| 124 |
+
"type": "string",
|
| 125 |
+
"description": "Directory path to list (default: current directory)"
|
| 126 |
+
},
|
| 127 |
+
"pattern": {
|
| 128 |
+
"type": "string",
|
| 129 |
+
"description": "Optional glob pattern to filter files (e.g., '*.py', 'test_*')"
|
| 130 |
+
},
|
| 131 |
+
"show_hidden": {
|
| 132 |
+
"type": "boolean",
|
| 133 |
+
"description": "Whether to show hidden files (default: false)"
|
| 134 |
+
}
|
| 135 |
+
},
|
| 136 |
+
"required": []
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
},
|
| 140 |
+
{
|
| 141 |
+
"type": "function",
|
| 142 |
+
"function": {
|
| 143 |
+
"name": "git_status",
|
| 144 |
+
"description": "Get the git repository status. Use this to see what files have been modified, added, or deleted.",
|
| 145 |
+
"parameters": {
|
| 146 |
+
"type": "object",
|
| 147 |
+
"properties": {},
|
| 148 |
+
"required": []
|
| 149 |
+
}
|
| 150 |
+
}
|
| 151 |
+
},
|
| 152 |
+
{
|
| 153 |
+
"type": "function",
|
| 154 |
+
"function": {
|
| 155 |
+
"name": "search_codebase",
|
| 156 |
+
"description": "Search the codebase using hybrid retrieval (combines keyword matching with semantic search). More powerful than search_code - finds both exact matches AND semantically related code. Use this when looking for specific functionality, patterns, or concepts in the codebase.",
|
| 157 |
+
"parameters": {
|
| 158 |
+
"type": "object",
|
| 159 |
+
"properties": {
|
| 160 |
+
"query": {
|
| 161 |
+
"type": "string",
|
| 162 |
+
"description": "What to search for. Can be natural language (e.g., 'authentication logic', 'error handling') or specific terms (e.g., 'login function', 'database connection')"
|
| 163 |
+
},
|
| 164 |
+
"top_k": {
|
| 165 |
+
"type": "integer",
|
| 166 |
+
"description": "Number of results to return (default: 5, max: 20)",
|
| 167 |
+
"default": 5
|
| 168 |
+
}
|
| 169 |
+
},
|
| 170 |
+
"required": ["query"]
|
| 171 |
+
}
|
| 172 |
+
}
|
| 173 |
+
},
|
| 174 |
+
{
|
| 175 |
+
"type": "function",
|
| 176 |
+
"function": {
|
| 177 |
+
"name": "upload_to_sandbox",
|
| 178 |
+
"description": "Upload a file to the E2B sandbox for safe execution. Use this BEFORE running code to ensure the file exists in the sandbox environment.",
|
| 179 |
+
"parameters": {
|
| 180 |
+
"type": "object",
|
| 181 |
+
"properties": {
|
| 182 |
+
"path": {
|
| 183 |
+
"type": "string",
|
| 184 |
+
"description": "File path in sandbox (e.g., 'test.py', 'utils/helper.py')"
|
| 185 |
+
},
|
| 186 |
+
"content": {
|
| 187 |
+
"type": "string",
|
| 188 |
+
"description": "File content to upload"
|
| 189 |
+
}
|
| 190 |
+
},
|
| 191 |
+
"required": ["path", "content"]
|
| 192 |
+
}
|
| 193 |
+
}
|
| 194 |
+
},
|
| 195 |
+
{
|
| 196 |
+
"type": "function",
|
| 197 |
+
"function": {
|
| 198 |
+
"name": "run_command_in_sandbox",
|
| 199 |
+
"description": "Run a shell command in the isolated E2B sandbox. Use this to safely execute code, run tests, or perform system operations without affecting the host system. Examples: 'python test.py', 'pytest', 'npm test'.",
|
| 200 |
+
"parameters": {
|
| 201 |
+
"type": "object",
|
| 202 |
+
"properties": {
|
| 203 |
+
"command": {
|
| 204 |
+
"type": "string",
|
| 205 |
+
"description": "Shell command to execute in sandbox"
|
| 206 |
+
}
|
| 207 |
+
},
|
| 208 |
+
"required": ["command"]
|
| 209 |
+
}
|
| 210 |
+
}
|
| 211 |
+
},
|
| 212 |
+
{
|
| 213 |
+
"type": "function",
|
| 214 |
+
"function": {
|
| 215 |
+
"name": "execute_in_sandbox",
|
| 216 |
+
"description": "Execute Python code directly in the E2B sandbox. Use for quick code testing or running Python snippets without creating files.",
|
| 217 |
+
"parameters": {
|
| 218 |
+
"type": "object",
|
| 219 |
+
"properties": {
|
| 220 |
+
"code": {
|
| 221 |
+
"type": "string",
|
| 222 |
+
"description": "Python code to execute"
|
| 223 |
+
}
|
| 224 |
+
},
|
| 225 |
+
"required": ["code"]
|
| 226 |
+
}
|
| 227 |
+
}
|
| 228 |
+
}
|
| 229 |
+
]
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
# Map tool names to their implementation functions
|
| 233 |
+
TOOL_FUNCTIONS = {
|
| 234 |
+
"read_file": read_file,
|
| 235 |
+
"write_file": write_file,
|
| 236 |
+
"run_command": run_command,
|
| 237 |
+
"search_code": search_code,
|
| 238 |
+
"list_files": list_files,
|
| 239 |
+
"git_status": git_status,
|
| 240 |
+
"search_codebase": search_codebase,
|
| 241 |
+
"index_codebase": index_codebase,
|
| 242 |
+
"upload_to_sandbox": upload_to_sandbox,
|
| 243 |
+
"execute_in_sandbox": execute_in_sandbox,
|
| 244 |
+
"run_command_in_sandbox": run_command_in_sandbox
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
def get_tools() -> List[Dict]:
|
| 249 |
+
"""
|
| 250 |
+
Get all available tool schemas
|
| 251 |
+
|
| 252 |
+
Returns:
|
| 253 |
+
List of tool schema dictionaries for OpenAI
|
| 254 |
+
"""
|
| 255 |
+
return TOOLS
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
def get_tool_function(tool_name: str) -> Optional[Callable]:
|
| 259 |
+
"""
|
| 260 |
+
Get the implementation function for a tool by name
|
| 261 |
+
|
| 262 |
+
Args:
|
| 263 |
+
tool_name: Name of the tool (e.g., "read_file")
|
| 264 |
+
|
| 265 |
+
Returns:
|
| 266 |
+
The tool function, or None if not found
|
| 267 |
+
"""
|
| 268 |
+
return TOOL_FUNCTIONS.get(tool_name)
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
def list_tool_names() -> List[str]:
|
| 272 |
+
"""
|
| 273 |
+
Get list of all available tool names
|
| 274 |
+
|
| 275 |
+
Returns:
|
| 276 |
+
List of tool name strings
|
| 277 |
+
"""
|
| 278 |
+
return list(TOOL_FUNCTIONS.keys())
|
requirements.txt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Cloud deployment requirements (lightweight - no PyTorch/sentence-transformers)
|
| 2 |
+
# These are only the essential packages needed for HuggingFace Spaces
|
| 3 |
+
|
| 4 |
+
# Core
|
| 5 |
+
openai>=1.0.0
|
| 6 |
+
python-dotenv>=1.2.0
|
| 7 |
+
|
| 8 |
+
# E2B Sandbox
|
| 9 |
+
e2b-code-interpreter>=2.4.0
|
| 10 |
+
|
| 11 |
+
# LangChain (minimal)
|
| 12 |
+
langchain>=0.3.0
|
| 13 |
+
langgraph>=0.2.0
|
| 14 |
+
|
| 15 |
+
# Lightweight search (no embeddings in cloud mode)
|
| 16 |
+
rank-bm25>=0.2.2
|
| 17 |
+
|
| 18 |
+
# Chainlit UI
|
| 19 |
+
chainlit>=1.0.0
|
| 20 |
+
|
| 21 |
+
# For dependency graphs
|
| 22 |
+
networkx>=3.0
|