-
-
Save leerob/f25f99a4f298680762b117cedd5ecc4c to your computer and use it in GitHub Desktop.
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 |
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
Would love your thoughts on this approach that forgoes passing the schema to make it even shorter @leerob
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}")
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!
You might want this as well: