Skip to content

Commit 1a3a43e

Browse files
kodjima33claude
andcommitted
feat: implement agentic Claude Code for better code generation
- Add claude_code_agentic.py with tool-based exploration (read_file, list_files, write_file, bash) - Add claude_code_cli.py with GitHub API functions for PR creation/merging - Update main.py to use agentic approach instead of shallow context - Update Dockerfile to remove Claude Code CLI installation (using API instead) - Claude can now explore repos deeply before implementing features Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 303b27e commit 1a3a43e

4 files changed

Lines changed: 718 additions & 41 deletions

File tree

Dockerfile

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
FROM python:3.12-slim
2+
3+
# Install git (needed for git operations in agentic Claude)
4+
RUN apt-get update && apt-get install -y \
5+
git \
6+
&& rm -rf /var/lib/apt/lists/*
7+
8+
# Set working directory
9+
WORKDIR /app
10+
11+
# Copy requirements first for better caching
12+
COPY requirements.txt .
13+
14+
# Install Python dependencies
15+
RUN pip install --no-cache-dir -r requirements.txt
16+
17+
# Copy application code
18+
COPY . .
19+
20+
# Expose port (Railway will set $PORT)
21+
EXPOSE 8000
22+
23+
# Run the application
24+
CMD uvicorn main:app --host 0.0.0.0 --port ${PORT:-8000}

claude_code_agentic.py

Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
"""
2+
Claude Code-like agentic implementation using Anthropic API with tools.
3+
Implements exploration and iteration like Claude Code CLI.
4+
"""
5+
import os
6+
import subprocess
7+
import tempfile
8+
import logging
9+
from typing import Optional, Dict, Any, List
10+
from anthropic import Anthropic
11+
12+
# Set up logging
13+
logging.basicConfig(level=logging.INFO)
14+
logger = logging.getLogger(__name__)
15+
16+
17+
def run_agentic_claude_on_repo(
18+
repo_url: str,
19+
feature_description: str,
20+
branch_name: str,
21+
github_token: str,
22+
anthropic_key: str,
23+
max_iterations: int = 15
24+
) -> Dict[str, Any]:
25+
"""
26+
Clone repo, run agentic Claude to implement feature, return changes.
27+
Claude can explore files and iterate like Claude Code CLI.
28+
29+
Args:
30+
repo_url: GitHub repo URL
31+
feature_description: What to implement
32+
branch_name: Branch name for changes
33+
github_token: GitHub access token
34+
anthropic_key: Anthropic API key
35+
max_iterations: Max tool use iterations
36+
37+
Returns:
38+
Dict with success status and branch info
39+
"""
40+
try:
41+
# Create temp directory for repo
42+
with tempfile.TemporaryDirectory() as tmpdir:
43+
logger.info(f"Cloning {repo_url} to {tmpdir}")
44+
45+
# Clone repo with auth
46+
auth_url = repo_url.replace('https://', f'https://{github_token}@')
47+
clone_result = subprocess.run(
48+
['git', 'clone', auth_url, tmpdir],
49+
capture_output=True,
50+
text=True,
51+
timeout=60
52+
)
53+
54+
if clone_result.returncode != 0:
55+
return {
56+
'success': False,
57+
'message': f'Failed to clone repo: {clone_result.stderr}'
58+
}
59+
60+
logger.info(f"Cloned successfully, creating branch {branch_name}")
61+
62+
# Create new branch
63+
subprocess.run(
64+
['git', 'checkout', '-b', branch_name],
65+
cwd=tmpdir,
66+
check=True
67+
)
68+
69+
# Get default branch
70+
default_branch_result = subprocess.run(
71+
['git', 'remote', 'show', 'origin'],
72+
cwd=tmpdir,
73+
capture_output=True,
74+
text=True
75+
)
76+
77+
default_branch = 'main'
78+
for line in default_branch_result.stdout.split('\n'):
79+
if 'HEAD branch:' in line:
80+
default_branch = line.split(':')[1].strip()
81+
break
82+
83+
# Run agentic Claude with file access
84+
client = Anthropic(api_key=anthropic_key)
85+
86+
# Define tools Claude can use
87+
tools = [
88+
{
89+
"name": "read_file",
90+
"description": "Read contents of a file in the repository. Use this to explore existing code.",
91+
"input_schema": {
92+
"type": "object",
93+
"properties": {
94+
"file_path": {
95+
"type": "string",
96+
"description": "Path to file relative to repo root"
97+
}
98+
},
99+
"required": ["file_path"]
100+
}
101+
},
102+
{
103+
"name": "list_files",
104+
"description": "List files in a directory. Use this to explore repo structure.",
105+
"input_schema": {
106+
"type": "object",
107+
"properties": {
108+
"dir_path": {
109+
"type": "string",
110+
"description": "Directory path relative to repo root (use '.' for root)"
111+
},
112+
"recursive": {
113+
"type": "boolean",
114+
"description": "List files recursively"
115+
}
116+
},
117+
"required": ["dir_path"]
118+
}
119+
},
120+
{
121+
"name": "write_file",
122+
"description": "Write or update a file. Use this to implement the feature.",
123+
"input_schema": {
124+
"type": "object",
125+
"properties": {
126+
"file_path": {
127+
"type": "string",
128+
"description": "Path to file relative to repo root"
129+
},
130+
"content": {
131+
"type": "string",
132+
"description": "Full file contents to write"
133+
}
134+
},
135+
"required": ["file_path", "content"]
136+
}
137+
},
138+
{
139+
"name": "bash",
140+
"description": "Run a bash command in the repo directory. Use for git status, grep, find, etc.",
141+
"input_schema": {
142+
"type": "object",
143+
"properties": {
144+
"command": {
145+
"type": "string",
146+
"description": "Bash command to run"
147+
}
148+
},
149+
"required": ["command"]
150+
}
151+
}
152+
]
153+
154+
# Initial prompt
155+
messages = [{
156+
"role": "user",
157+
"content": f"""You are implementing a feature in a cloned GitHub repository.
158+
159+
**Feature to implement:** {feature_description}
160+
161+
**Your task:**
162+
1. First, explore the repository structure to understand the codebase
163+
2. Find existing relevant files (use list_files, read_file, bash grep/find)
164+
3. Understand the existing patterns and architecture
165+
4. Implement the feature by modifying existing files (prefer editing over creating new files)
166+
5. When done, tell me "IMPLEMENTATION_COMPLETE"
167+
168+
**Available tools:**
169+
- read_file: Read any file to understand code
170+
- list_files: List directory contents
171+
- write_file: Write/update files
172+
- bash: Run commands (git status, grep, find, etc.)
173+
174+
**Guidelines:**
175+
- Explore first, code second
176+
- Modify existing files rather than creating new ones
177+
- Follow existing code patterns
178+
- Be thorough in understanding before implementing
179+
180+
Start by exploring the repository structure."""
181+
}]
182+
183+
iteration = 0
184+
while iteration < max_iterations:
185+
iteration += 1
186+
logger.info(f"Iteration {iteration}/{max_iterations}")
187+
188+
# Call Claude with tools
189+
response = client.messages.create(
190+
model="claude-sonnet-4-20250514",
191+
max_tokens=8000,
192+
tools=tools,
193+
messages=messages
194+
)
195+
196+
logger.info(f"Response stop_reason: {response.stop_reason}")
197+
198+
# Check if Claude is done
199+
if response.stop_reason == "end_turn":
200+
# Check if Claude said implementation is complete
201+
for block in response.content:
202+
if hasattr(block, 'text') and 'IMPLEMENTATION_COMPLETE' in block.text:
203+
logger.info("Claude finished implementation")
204+
break
205+
break
206+
207+
# Process tool calls
208+
if response.stop_reason == "tool_use":
209+
# Add Claude's response to messages
210+
messages.append({
211+
"role": "assistant",
212+
"content": response.content
213+
})
214+
215+
# Execute tools and collect results
216+
tool_results = []
217+
218+
for block in response.content:
219+
if block.type == "tool_use":
220+
tool_name = block.name
221+
tool_input = block.input
222+
tool_id = block.id
223+
224+
logger.info(f"Tool: {tool_name}, Input: {tool_input}")
225+
226+
# Execute tool
227+
result = execute_tool(tool_name, tool_input, tmpdir)
228+
229+
tool_results.append({
230+
"type": "tool_result",
231+
"tool_use_id": tool_id,
232+
"content": result
233+
})
234+
235+
# Add tool results to messages
236+
messages.append({
237+
"role": "user",
238+
"content": tool_results
239+
})
240+
241+
else:
242+
# Unexpected stop reason
243+
break
244+
245+
# Stage and commit all changes
246+
logger.info("Staging changes...")
247+
subprocess.run(['git', 'add', '-A'], cwd=tmpdir, check=True)
248+
249+
# Check if there are changes
250+
status_result = subprocess.run(
251+
['git', 'status', '--porcelain'],
252+
cwd=tmpdir,
253+
capture_output=True,
254+
text=True
255+
)
256+
257+
if not status_result.stdout.strip():
258+
return {
259+
'success': False,
260+
'message': 'No changes were made by Claude'
261+
}
262+
263+
# Commit
264+
subprocess.run(
265+
['git', 'commit', '-m', f'feat: {feature_description}\n\nGenerated by Claude Code (agentic) via Omi'],
266+
cwd=tmpdir,
267+
check=True
268+
)
269+
270+
# Push
271+
logger.info(f"Pushing branch {branch_name}...")
272+
push_result = subprocess.run(
273+
['git', 'push', 'origin', branch_name],
274+
cwd=tmpdir,
275+
capture_output=True,
276+
text=True
277+
)
278+
279+
if push_result.returncode != 0:
280+
return {
281+
'success': False,
282+
'message': f'Failed to push: {push_result.stderr}'
283+
}
284+
285+
return {
286+
'success': True,
287+
'branch': branch_name,
288+
'default_branch': default_branch,
289+
'message': f'Implemented and pushed to {branch_name}'
290+
}
291+
292+
except Exception as e:
293+
logger.error(f"Error: {e}", exc_info=True)
294+
return {
295+
'success': False,
296+
'message': str(e)
297+
}
298+
299+
300+
def execute_tool(tool_name: str, tool_input: Dict, repo_dir: str) -> str:
301+
"""Execute a tool and return the result."""
302+
try:
303+
if tool_name == "read_file":
304+
file_path = os.path.join(repo_dir, tool_input['file_path'])
305+
with open(file_path, 'r') as f:
306+
content = f.read()
307+
return content[:10000] # Limit to 10k chars
308+
309+
elif tool_name == "list_files":
310+
dir_path = os.path.join(repo_dir, tool_input['dir_path'])
311+
recursive = tool_input.get('recursive', False)
312+
313+
if recursive:
314+
result = subprocess.run(
315+
['find', '.', '-type', 'f'],
316+
cwd=dir_path,
317+
capture_output=True,
318+
text=True
319+
)
320+
else:
321+
result = subprocess.run(
322+
['ls', '-la'],
323+
cwd=dir_path,
324+
capture_output=True,
325+
text=True
326+
)
327+
328+
return result.stdout[:5000] # Limit output
329+
330+
elif tool_name == "write_file":
331+
file_path = os.path.join(repo_dir, tool_input['file_path'])
332+
os.makedirs(os.path.dirname(file_path), exist_ok=True)
333+
334+
with open(file_path, 'w') as f:
335+
f.write(tool_input['content'])
336+
337+
return f"Successfully wrote {len(tool_input['content'])} chars to {tool_input['file_path']}"
338+
339+
elif tool_name == "bash":
340+
result = subprocess.run(
341+
tool_input['command'],
342+
shell=True,
343+
cwd=repo_dir,
344+
capture_output=True,
345+
text=True,
346+
timeout=30
347+
)
348+
349+
output = result.stdout + result.stderr
350+
return output[:5000] # Limit output
351+
352+
else:
353+
return f"Unknown tool: {tool_name}"
354+
355+
except Exception as e:
356+
return f"Error executing {tool_name}: {str(e)}"

0 commit comments

Comments
 (0)