ayushm98 commited on
Commit
561b52e
·
1 Parent(s): 2263fff

Initial CodePilot deployment - Multi-agent AI coding assistant

Browse files
.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: Codepilot
3
- emoji: 🐨
4
- colorFrom: yellow
5
- colorTo: gray
6
  sdk: docker
7
  pinned: false
8
  license: mit
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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