Skip to content

Instantly share code, notes, and snippets.

@leerob
Created July 30, 2025 23:14
Show Gist options
  • Save leerob/f25f99a4f298680762b117cedd5ecc4c to your computer and use it in GitHub Desktop.
Save leerob/f25f99a4f298680762b117cedd5ecc4c to your computer and use it in GitHub Desktop.
agent.py
import os
import json
import subprocess
from anthropic import Anthropic
# Tool definitions
TOOLS = [
{
"name": "list_files",
"description": "List files and directories at a given path",
"input_schema": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to list (defaults to current directory)"
}
}
}
},
{
"name": "read_file",
"description": "Read the contents of a file",
"input_schema": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the file to read"
}
},
"required": ["path"]
}
},
{
"name": "run_bash",
"description": "Run a bash command and return the output",
"input_schema": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The bash command to run"
}
},
"required": ["command"]
}
},
{
"name": "edit_file",
"description": "Edit a file by replacing old text with new text",
"input_schema": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the file to edit"
},
"old_text": {
"type": "string",
"description": "Text to search for and replace"
},
"new_text": {
"type": "string",
"description": "Text to replace with"
}
},
"required": ["path", "old_text", "new_text"]
}
}
]
def execute_tool(name, arguments):
"""Execute a tool and return the result"""
if name == "list_files":
path = arguments.get("path", ".")
files = os.listdir(path)
return json.dumps(files, indent=2)
elif name == "read_file":
with open(arguments["path"], 'r') as f:
return f.read()
elif name == "run_bash":
result = subprocess.run(
arguments["command"],
shell=True,
capture_output=True,
text=True
)
return f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}"
elif name == "edit_file":
path = arguments["path"]
old_text = arguments["old_text"]
new_text = arguments["new_text"]
# Create new file if old_text is empty
if old_text == "":
with open(path, 'w') as f:
f.write(new_text)
return "File created"
# Read existing file
with open(path, 'r') as f:
content = f.read()
# Replace text
new_content = content.replace(old_text, new_text)
# Write back
with open(path, 'w') as f:
f.write(new_content)
return "File edited"
def run_agent(prompt):
"""Run the agent with a given prompt"""
client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
messages = [{"role": "user", "content": prompt}]
print(f"πŸ€– Working on: {prompt}\n")
while True:
# Call the AI with tools
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
messages=messages,
tools=TOOLS
)
# Add AI response to conversation
messages.append({
"role": "assistant",
"content": response.content
})
# Check if we have tool calls
tool_calls = [c for c in response.content if c.type == "tool_use"]
if tool_calls:
# Execute each tool
tool_results = []
for call in tool_calls:
print(f"πŸ”§ Using tool: {call.name}")
result = execute_tool(call.name, call.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": call.id,
"content": result
})
# Add results to conversation
messages.append({
"role": "user",
"content": tool_results
})
else:
# No more tools, we're done
text_blocks = [c for c in response.content if c.type == "text"]
if text_blocks:
print(f"\nβœ… Done: {text_blocks[0].text}")
break
# Interactive CLI
if __name__ == "__main__":
while True:
try:
prompt = input("\nπŸ’¬ What would you like me to do? (or 'quit' to exit)\n> ")
if prompt.lower() == 'quit':
break
run_agent(prompt)
except KeyboardInterrupt:
print("\nπŸ‘‹ Goodbye!")
break
@leerob
Copy link
Author

leerob commented Jul 30, 2025

You might want this as well:

  # Install uv to manage Python dependencies
  curl -LsSf https://astral.sh/uv/install.sh | sh

  # Create a virtual environment
  uv venv

  # Activate the virtual environment
  source .venv/bin/activate

  # Install anthropic
  uv pip install anthropic
  
  # Add API key
  ANTHROPIC_API_KEY="paste"

  # Run the agent
  uv run python agent.py

@Thomas-TyTech
Copy link

Thomas-TyTech commented Jul 31, 2025

was able to get it down to 125 lines, the schema for the text-editor tool is built into Claude models and is not something you need to pass to the model. You only have to create the functions.

import os
from anthropic import Anthropic

def handle_text_editor(tool_call):
"""Handle text editor commands for Claude"""
params = tool_call.input
command = params.get('command')
path = params.get('path')

try:
    if command == 'view':
        if os.path.isdir(path):
            return '\n'.join(os.listdir(path))
        
        with open(path, 'r') as f:
            lines = f.readlines()
        
        # Handle view_range if specified
        view_range = params.get('view_range')
        if view_range:
            start, end = view_range
            if end == -1:
                end = len(lines)
            lines = lines[start-1:end]
            start_line = start
        else:
            start_line = 1
        
        return ''.join(f"{i+start_line}: {line}" for i, line in enumerate(lines))
            
    elif command == 'str_replace':
        with open(path, 'r') as f:
            content = f.read()
        
        old_str = params.get('old_str')
        new_str = params.get('new_str')
        
        if content.count(old_str) != 1:
            return f"Error: Found {content.count(old_str)} matches. Need exactly 1."
        
        with open(path, 'w') as f:
            f.write(content.replace(old_str, new_str))
        
        return "Successfully replaced text at exactly one location."
        
    elif command == 'create':
        with open(path, 'w') as f:
            f.write(params.get('file_text'))
        return "File created"  
        
    elif command == 'insert':
        with open(path, 'r') as f:
            lines = f.readlines()
        
        insert_line = params.get('insert_line')
        lines.insert(insert_line, params.get('new_str') + '\n')
        
        with open(path, 'w') as f:
            f.writelines(lines)
        
        return f"Text inserted at line {insert_line}."
        
except Exception as e:
    return f"Error: {str(e)}"

def run_agent(prompt):
"""Run the agent"""
client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
messages = [{"role": "user", "content": prompt}]

print(f"πŸ€– Working on: {prompt}\n")

while True:
    
    response = client.messages.create(
        model="claude-3-5-sonnet-20241022",
        max_tokens=1024,
        tools=[{"type": "text_editor_20250124", "name": "str_replace_editor"}],
        messages=messages
    )
    
    
    messages.append({
        "role": "assistant",
        "content": response.content
    })
    
    
    tool_calls = [c for c in response.content if c.type == "tool_use"]
    
    if tool_calls:
        
        tool_results = []
        for call in tool_calls:
            print(f"πŸ”§ Using tool: {call.name}")
            result = handle_text_editor(call)
            tool_results.append({
                "type": "tool_result",
                "tool_use_id": call.id,
                "content": result
            })
        
        
        messages.append({
            "role": "user",
            "content": tool_results
        })
    else:
        
        text_blocks = [c for c in response.content if c.type == "text"]
        if text_blocks:
            print(f"\nβœ… Done: {text_blocks[0].text}")
        break

if name == "main":
while True:
try:
prompt = input("\nπŸ’¬ What would you like me to do? (or 'quit' to exit)\n> ")
if prompt.lower() == 'quit':
break
run_agent(prompt)
except KeyboardInterrupt:
print("\nπŸ‘‹ Goodbye!")
break

@Thomas-TyTech
Copy link

Would love your thoughts on this approach that forgoes passing the schema to make it even shorter @leerob

@leerob
Copy link
Author

leerob commented Jul 31, 2025

Nice, yeah the built in tools can probably help make things more robust. I fed in all of the Anthropic docs to use their built-in tools and got a similar sized one that is likely more robust.

import os
import json
import subprocess
from anthropic import Anthropic

# Tool definitions for built-in Anthropic tools
TOOLS = [
    {
        "type": "bash_20250124",
        "name": "bash",
    },
    {
        "type": "text_editor_20250728",
        "name": "str_replace_based_edit_tool",
    }
]

def execute_tool(name, arguments):
    """Execute a built-in tool and return the result"""
    if name == "bash":
        command = arguments.get("command")
        if not command:
            return "Error: missing command for bash tool"
        
        result = subprocess.run(
            command,
            shell=True,
            capture_output=True,
            text=True
        )
        return f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}"

    elif name == "str_replace_based_edit_tool":
        command = arguments.get("command")
        path = arguments.get("path")

        if not command or not path:
            return "Error: missing command or path for text editor tool"

        try:
            if command == "view":
                if os.path.isdir(path):
                    return json.dumps(os.listdir(path), indent=2)
                with open(path, 'r') as f:
                    lines = f.readlines()
                    return "".join([f"{i+1}: {line}" for i, line in enumerate(lines)])
            
            elif command == "str_replace":
                old_str = arguments.get("old_str")
                new_str = arguments.get("new_str")
                if old_str is None or new_str is None:
                    return "Error: old_str or new_str not provided for str_replace"
                
                with open(path, 'r') as f:
                    content = f.read()
                
                match_count = content.count(old_str)
                if match_count == 0:
                    return "Error: No match found for replacement."
                if match_count > 1:
                    return f"Error: Found {match_count} matches. Please provide more context for a unique match."
                
                new_content = content.replace(old_str, new_str, 1)
                with open(path, 'w') as f:
                    f.write(new_content)
                return "Successfully replaced text at exactly one location."

            elif command == "create":
                file_text = arguments.get("file_text", "")
                with open(path, 'w') as f:
                    f.write(file_text)
                return f"File created at {path}"

            elif command == "insert":
                insert_line = arguments.get("insert_line")
                new_str = arguments.get("new_str")
                if insert_line is None or new_str is None:
                    return "Error: insert_line or new_str not provided for insert"

                with open(path, 'r') as f:
                    lines = f.readlines()
                
                lines.insert(insert_line, new_str + '\n')

                with open(path, 'w') as f:
                    f.writelines(lines)
                return "Successfully inserted text."

        except FileNotFoundError:
            return f"Error: File not found at {path}"
        except Exception as e:
            return f"An error occurred: {e}"
            
    return f"Unknown tool: {name}"


def run_agent(prompt):
    """Run the agent with a given prompt"""
    client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
    if not client.api_key:
        print("Error: ANTHROPIC_API_KEY environment variable not set.")
        return

    messages = [{"role": "user", "content": prompt}]
    
    print(f"πŸ€– Working on: {prompt}\n")
    
    while True:
        response = client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=4096,
            messages=messages,
            tools=TOOLS
        )
        
        messages.append({
            "role": "assistant",
            "content": response.content
        })
        
        tool_calls = [c for c in response.content if c.type == "tool_use"]
        
        if tool_calls:
            tool_results = []
            for call in tool_calls:
                print(f"πŸ”§ Using tool: {call.name} with input {call.input}")
                result = execute_tool(call.name, call.input)
                print(f"   Result: {result[:200]}...")
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": call.id,
                    "content": result
                })
            
            messages.append({
                "role": "user",
                "content": tool_results
            })
        else:
            text_blocks = [c for c in response.content if c.type == "text"]
            if text_blocks:
                print(f"\nβœ… Done: {text_blocks[0].text}")
            break

# Interactive CLI
if __name__ == "__main__":
    while True:
        try:
            prompt = input("\nπŸ’¬ What would you like me to do? (or 'quit' to exit)\n> ")
            if prompt.lower() == 'quit':
                break
            run_agent(prompt)
        except KeyboardInterrupt:
            print("\nπŸ‘‹ Goodbye!")
            break
        except Exception as e:
            print(f"An error occurred: {e}")

@Thomas-TyTech
Copy link

Thomas-TyTech commented Jul 31, 2025

That one is only for Claude 4, the text edit tool I used was the one that is specifically for 3.5 just to keep consistency with your original implementation that used Claude 3.5 sonnet. Agreed on the new one being more robust! That was what I played around with initially before realizing you used 3.5 in the original!

Thanks for your time and feedback!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment