-
Notifications
You must be signed in to change notification settings - Fork 12.1k
Expand file tree
/
Copy paths01_agent_loop.py
More file actions
165 lines (134 loc) · 4.36 KB
/
Copy paths01_agent_loop.py
File metadata and controls
165 lines (134 loc) · 4.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
#!/usr/bin/env python3
# Harness: the loop -- keep feeding real tool results back into the model.
"""
s01_agent_loop.py - The Agent Loop
This file teaches the smallest useful coding-agent pattern:
user message
-> model reply
-> if tool_use: execute tools
-> write tool_result back to messages
-> continue
It intentionally keeps the loop small, but still makes the loop state explicit
so later chapters can grow from the same structure.
"""
import os
import subprocess
from dataclasses import dataclass
try:
import readline
# #143 UTF-8 backspace fix for macOS libedit
readline.parse_and_bind('set bind-tty-special-chars off')
readline.parse_and_bind('set input-meta on')
readline.parse_and_bind('set output-meta on')
readline.parse_and_bind('set convert-meta off')
readline.parse_and_bind('set enable-meta-keybindings on')
except ImportError:
pass
from anthropic import Anthropic
from dotenv import load_dotenv
load_dotenv(override=True)
if os.getenv("ANTHROPIC_BASE_URL"):
os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
MODEL = os.environ["MODEL_ID"]
SYSTEM = (
f"You are a coding agent at {os.getcwd()}. "
"Use bash to inspect and change the workspace. Act first, then report clearly."
)
TOOLS = [{
"name": "bash",
"description": "Run a shell command in the current workspace.",
"input_schema": {
"type": "object",
"properties": {"command": {"type": "string"}},
"required": ["command"],
},
}]
@dataclass
class LoopState:
# The minimal loop state: history, loop count, and why we continue.
messages: list
turn_count: int = 1
transition_reason: str | None = None
def run_bash(command: str) -> str:
dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
if any(item in command for item in dangerous):
return "Error: Dangerous command blocked"
try:
result = subprocess.run(
command,
shell=True,
cwd=os.getcwd(),
capture_output=True,
text=True,
timeout=120,
)
except subprocess.TimeoutExpired:
return "Error: Timeout (120s)"
except (FileNotFoundError, OSError) as e:
return f"Error: {e}"
output = (result.stdout + result.stderr).strip()
return output[:50000] if output else "(no output)"
def extract_text(content) -> str:
if not isinstance(content, list):
return ""
texts = []
for block in content:
text = getattr(block, "text", None)
if text:
texts.append(text)
return "\n".join(texts).strip()
def execute_tool_calls(response_content) -> list[dict]:
results = []
for block in response_content:
if block.type != "tool_use":
continue
command = block.input["command"]
print(f"\033[33m$ {command}\033[0m")
output = run_bash(command)
print(output[:200])
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output,
})
return results
def run_one_turn(state: LoopState) -> bool:
response = client.messages.create(
model=MODEL,
system=SYSTEM,
messages=state.messages,
tools=TOOLS,
max_tokens=8000,
)
state.messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
state.transition_reason = None
return False
results = execute_tool_calls(response.content)
if not results:
state.transition_reason = None
return False
state.messages.append({"role": "user", "content": results})
state.turn_count += 1
state.transition_reason = "tool_result"
return True
def agent_loop(state: LoopState) -> None:
while run_one_turn(state):
pass
if __name__ == "__main__":
history = []
while True:
try:
query = input("\033[36ms01 >> \033[0m")
except (EOFError, KeyboardInterrupt):
break
if query.strip().lower() in ("q", "exit", ""):
break
history.append({"role": "user", "content": query})
state = LoopState(messages=history)
agent_loop(state)
final_text = extract_text(history[-1]["content"])
if final_text:
print(final_text)
print()