11#!/usr/bin/env python3
22"""
3- s02: Tool Use — 工具分发,循环不变 。
3+ s02: Tool Use — 在 s01 基础上新增 4 个工具 + 分发映射 + 并发分区 。
44
5- 运行方式:
6- python s02_tool_use/code.py
5+ 运行: python s02_tool_use/code.py
6+ 需要: pip install anthropic python-dotenv + .env 中配置 ANTHROPIC_API_KEY
77
8- 需要:
9- - pip install anthropic python-dotenv
10- - .env 文件中配置 ANTHROPIC_API_KEY 或 ANTHROPIC_BASE_URL + MODEL_ID
8+ 本文件 = s01 的全部代码 + 以下新增:
9+ + run_read / run_write / run_edit / run_glob 四个工具实现
10+ + TOOL_HANDLERS 分发映射(替代 s01 中硬编码的 run_bash 调用)
11+ + partition_tool_calls 并发分区
12+ + safe_path 路径安全校验
1113
12- 代码对应 README 章节:
13- TOOLS / TOOL_HANDLERS → 工具定义与分发
14- partition_tool_calls() → 并发 vs 顺序执行
15- agent_loop() → 循环本身(与 s01 完全相同)
14+ 循环本身(agent_loop)与 s01 完全一致。
1615"""
1716
18- import os
19- import subprocess
17+ import os , subprocess
2018from pathlib import Path
2119
2220try :
3230from dotenv import load_dotenv
3331
3432load_dotenv (override = True )
35-
3633if os .getenv ("ANTHROPIC_BASE_URL" ):
3734 os .environ .pop ("ANTHROPIC_AUTH_TOKEN" , None )
3835
4340SYSTEM = f"You are a coding agent at { WORKDIR } . Use tools to solve tasks. Act, don't explain."
4441
4542
46- def safe_path (p : str ) -> Path :
47- path = (WORKDIR / p ).resolve ()
48- if not path .is_relative_to (WORKDIR ):
49- raise ValueError (f"Path escapes workspace: { p } " )
50- return path
51-
52-
53- # ── 工具实现 ──────────────────────────────────────────────
43+ # ═══════════════════════════════════════════════════════════
44+ # FROM s01 (unchanged)
45+ # ═══════════════════════════════════════════════════════════
5446
5547def run_bash (command : str ) -> str :
5648 dangerous = ["rm -rf /" , "sudo" , "shutdown" , "reboot" , "> /dev/" ]
@@ -63,6 +55,19 @@ def run_bash(command: str) -> str:
6355 return out [:50000 ] if out else "(no output)"
6456 except subprocess .TimeoutExpired :
6557 return "Error: Timeout (120s)"
58+ except (FileNotFoundError , OSError ) as e :
59+ return f"Error: { e } "
60+
61+
62+ # ═══════════════════════════════════════════════════════════
63+ # NEW in s02: 4 个新工具
64+ # ═══════════════════════════════════════════════════════════
65+
66+ def safe_path (p : str ) -> Path :
67+ path = (WORKDIR / p ).resolve ()
68+ if not path .is_relative_to (WORKDIR ):
69+ raise ValueError (f"Path escapes workspace: { p } " )
70+ return path
6671
6772
6873def run_read (path : str , limit : int | None = None ) -> str :
@@ -106,79 +111,38 @@ def run_glob(pattern: str) -> str:
106111 return f"Error: { e } "
107112
108113
109- # ── 工具定义 ──────────────────────────────────────────────
114+ # ═══════════════════════════════════════════════════════════
115+ # NEW in s02: 工具定义(s01 只有一个 bash,现在扩展到 5 个)
116+ # ═══════════════════════════════════════════════════════════
110117
111118TOOLS = [
112- {
113- "name" : "bash" ,
114- "description" : "Run a shell command." ,
115- "input_schema" : {
116- "type" : "object" ,
117- "properties" : {"command" : {"type" : "string" }},
118- "required" : ["command" ],
119- },
120- },
121- {
122- "name" : "read_file" ,
123- "description" : "Read file contents." ,
124- "input_schema" : {
125- "type" : "object" ,
126- "properties" : {
127- "path" : {"type" : "string" },
128- "limit" : {"type" : "integer" },
129- },
130- "required" : ["path" ],
131- },
132- },
133- {
134- "name" : "write_file" ,
135- "description" : "Write content to a file." ,
136- "input_schema" : {
137- "type" : "object" ,
138- "properties" : {
139- "path" : {"type" : "string" },
140- "content" : {"type" : "string" },
141- },
142- "required" : ["path" , "content" ],
143- },
144- },
145- {
146- "name" : "edit_file" ,
147- "description" : "Replace exact text in a file once." ,
148- "input_schema" : {
149- "type" : "object" ,
150- "properties" : {
151- "path" : {"type" : "string" },
152- "old_text" : {"type" : "string" },
153- "new_text" : {"type" : "string" },
154- },
155- "required" : ["path" , "old_text" , "new_text" ],
156- },
157- },
158- {
159- "name" : "glob" ,
160- "description" : "Find files matching a glob pattern." ,
161- "input_schema" : {
162- "type" : "object" ,
163- "properties" : {"pattern" : {"type" : "string" }},
164- "required" : ["pattern" ],
165- },
166- },
119+ {"name" : "bash" , "description" : "Run a shell command." ,
120+ "input_schema" : {"type" : "object" , "properties" : {"command" : {"type" : "string" }}, "required" : ["command" ]}},
121+ {"name" : "read_file" , "description" : "Read file contents." ,
122+ "input_schema" : {"type" : "object" , "properties" : {"path" : {"type" : "string" }, "limit" : {"type" : "integer" }}, "required" : ["path" ]}},
123+ {"name" : "write_file" , "description" : "Write content to a file." ,
124+ "input_schema" : {"type" : "object" , "properties" : {"path" : {"type" : "string" }, "content" : {"type" : "string" }}, "required" : ["path" , "content" ]}},
125+ {"name" : "edit_file" , "description" : "Replace exact text in a file once." ,
126+ "input_schema" : {"type" : "object" , "properties" : {"path" : {"type" : "string" }, "old_text" : {"type" : "string" }, "new_text" : {"type" : "string" }}, "required" : ["path" , "old_text" , "new_text" ]}},
127+ {"name" : "glob" , "description" : "Find files matching a glob pattern." ,
128+ "input_schema" : {"type" : "object" , "properties" : {"pattern" : {"type" : "string" }}, "required" : ["pattern" ]}},
167129]
168130
131+ # ═══════════════════════════════════════════════════════════
132+ # NEW in s02: 工具分发映射(s01 是硬编码 run_bash,现在改为查表)
133+ # ═══════════════════════════════════════════════════════════
134+
169135TOOL_HANDLERS = {
170- "bash" : run_bash ,
171- "read_file" : run_read ,
172- "write_file" : run_write ,
173- "edit_file" : run_edit ,
174- "glob" : run_glob ,
136+ "bash" : run_bash , "read_file" : run_read , "write_file" : run_write ,
137+ "edit_file" : run_edit , "glob" : run_glob ,
175138}
176139
177140
178- # ── 工具分区 ──────────────────────────────────────────────
141+ # ═══════════════════════════════════════════════════════════
142+ # NEW in s02: 并发分区(s01 只有 bash 单个工具,不需要分区)
143+ # ═══════════════════════════════════════════════════════════
179144
180145def partition_tool_calls (blocks ):
181- """Split tool calls into sequential (may modify filesystem) and concurrent (read-only)."""
182146 concurrent , sequential = [], []
183147 for block in blocks :
184148 if block .type != "tool_use" :
@@ -190,58 +154,46 @@ def partition_tool_calls(blocks):
190154 return concurrent , sequential
191155
192156
193- # ── 核心:Agent Loop(与 s01 完全相同)────────────────────
194-
195- def execute_tool (block ) -> str :
196- handler = TOOL_HANDLERS .get (block .name )
197- if not handler :
198- return f"Unknown tool: { block .name } "
199- return handler (** block .input )
200-
157+ # ═══════════════════════════════════════════════════════════
158+ # agent_loop — 与 s01 结构完全一致,只改了工具执行那一行
159+ # s01: output = run_bash(block.input["command"])
160+ # s02: output = TOOL_HANDLERS[block.name](**block.input)
161+ # ═══════════════════════════════════════════════════════════
201162
202163def agent_loop (messages : list ):
203164 while True :
204165 response = client .messages .create (
205166 model = MODEL , system = SYSTEM , messages = messages ,
206167 tools = TOOLS , max_tokens = 8000 ,
207168 )
208-
209169 messages .append ({"role" : "assistant" , "content" : response .content })
210170
211171 if response .stop_reason != "tool_use" :
212172 return
213173
214- # 分区执行:先跑可能改文件系统的,再并发跑只读的
174+ # s02 改动: 分区执行(s01 是直接循环执行)
215175 concurrent , sequential = partition_tool_calls (response .content )
216176 results = []
217177
218178 for block in sequential :
219- print (f"\033 [33m> { block .name } ({ list (block .input .values ())[:1 ]} )\033 [0m" )
220- output = execute_tool (block )
221- print (output [:200 ])
222- results .append ({
223- "type" : "tool_result" ,
224- "tool_use_id" : block .id ,
225- "content" : output ,
226- })
179+ print (f"\033 [33m> { block .name } \033 [0m" )
180+ handler = TOOL_HANDLERS .get (block .name ) # s02: 查表替代硬编码
181+ output = handler (** block .input ) if handler else f"Unknown: { block .name } "
182+ print (str (output )[:200 ])
183+ results .append ({"type" : "tool_result" , "tool_use_id" : block .id , "content" : output })
227184
228185 for block in concurrent :
229- print (f"\033 [36m> { block .name } ({ list (block .input .values ())[:1 ]} )\033 [0m" )
230- output = execute_tool (block )
231- print (output [:200 ])
232- results .append ({
233- "type" : "tool_result" ,
234- "tool_use_id" : block .id ,
235- "content" : output ,
236- })
186+ print (f"\033 [36m> { block .name } \033 [0m" )
187+ handler = TOOL_HANDLERS .get (block .name )
188+ output = handler (** block .input ) if handler else f"Unknown: { block .name } "
189+ print (str (output )[:200 ])
190+ results .append ({"type" : "tool_result" , "tool_use_id" : block .id , "content" : output })
237191
238192 messages .append ({"role" : "user" , "content" : results })
239193
240194
241- # ── 入口 ─────────────────────────────────────────────────
242-
243195if __name__ == "__main__" :
244- print ("s02: Tool Use" )
196+ print ("s02: Tool Use — 在 s01 基础上加了 4 个工具 " )
245197 print ("输入问题,回车发送。输入 q 退出。\n " )
246198
247199 history = []
@@ -254,9 +206,7 @@ def agent_loop(messages: list):
254206 break
255207 history .append ({"role" : "user" , "content" : query })
256208 agent_loop (history )
257- response_content = history [- 1 ]["content" ]
258- if isinstance (response_content , list ):
259- for block in response_content :
260- if getattr (block , "type" , None ) == "text" :
261- print (block .text )
209+ for block in history [- 1 ]["content" ]:
210+ if getattr (block , "type" , None ) == "text" :
211+ print (block .text )
262212 print ()
0 commit comments