| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112 |
- #!/usr/bin/env python3
- """Minimal, correct tool-use agentic loop on the Anthropic Messages API.
- The canonical pattern: define a tool, call messages.create, and keep looping
- while stop_reason == "tool_use" — execute each requested tool, append a
- tool_result, and re-request until stop_reason == "end_turn".
- Run: pip install anthropic (then: export ANTHROPIC_API_KEY=sk-...)
- python agentic-loop.py
- Copy this file and adapt the >>> ADAPT marks for your own tools.
- Reflects the current API (model claude-opus-4-8, typed content blocks).
- """
- # The Anthropic SDK accepts plain dict literals for tools/messages at runtime
- # (as the official docs show), but its strict TypedDict stubs over-narrow them.
- # Silence those false positives so this starter stays readable; real apps may
- # prefer the SDK's typed params (anthropic.types.ToolParam, MessageParam).
- # pyright: reportArgumentType=false
- import anthropic
- client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from the environment
- MODEL = "claude-opus-4-8" # >>> ADAPT: pick a tier (see the skill's model table)
- # --- 1. Define your tool(s) ------------------------------------------------
- # The input_schema is JSON Schema. Write a description that says WHEN to call.
- TOOLS = [
- {
- "name": "get_weather", # >>> ADAPT
- "description": "Get the current weather for a city. "
- "Call this whenever the user asks about weather conditions.",
- "input_schema": {
- "type": "object",
- "properties": {
- "location": {"type": "string", "description": "City, e.g. 'Paris'"},
- },
- "required": ["location"],
- },
- }
- ]
- # --- 2. Implement each tool ------------------------------------------------
- # Map tool name -> a Python callable. Never trust the arguments blindly: they
- # come from the model. Validate before doing anything with side effects.
- def get_weather(location: str) -> str: # >>> ADAPT: real implementation
- return f"It is 21°C and sunny in {location}."
- TOOL_IMPLS = {"get_weather": get_weather}
- def run_tool(name: str, tool_input: dict) -> str:
- """Dispatch a tool call, returning a string result for the model."""
- impl = TOOL_IMPLS.get(name)
- if impl is None:
- return f"ERROR: unknown tool {name!r}"
- try:
- return impl(**tool_input)
- except Exception as exc: # surface failures back to the model, don't crash
- return f"ERROR running {name}: {exc}"
- # --- 3. The loop -----------------------------------------------------------
- def agent(user_prompt: str, max_turns: int = 10) -> str:
- # The conversation is a growing list of message dicts we own and replay.
- messages = [{"role": "user", "content": user_prompt}]
- for _ in range(max_turns):
- response = client.messages.create(
- model=MODEL,
- max_tokens=4096,
- tools=TOOLS,
- messages=messages,
- )
- # Append the assistant turn VERBATIM — content is a list of typed
- # blocks (text and/or tool_use). It must go back as-is next request.
- messages.append({"role": "assistant", "content": response.content})
- # If the model didn't ask for a tool, we're done — return its text.
- if response.stop_reason != "tool_use":
- return "".join(
- block.text for block in response.content if block.type == "text"
- )
- # Otherwise: execute EVERY tool_use block and collect tool_result
- # blocks (the model may request several tools in parallel).
- tool_results = []
- for block in response.content:
- if block.type != "tool_use":
- continue # skip text/thinking blocks
- result_text = run_tool(block.name, block.input)
- tool_results.append({
- "type": "tool_result",
- "tool_use_id": block.id, # MUST echo the matching id
- "content": result_text,
- # "is_error": True, # set when the tool failed
- })
- # Feed results back as a single user turn, then loop to re-request.
- messages.append({"role": "user", "content": tool_results})
- return "Stopped: hit max_turns without an end_turn."
- if __name__ == "__main__":
- answer = agent("What's the weather in Tokyo right now?")
- print(answer)
- # For debugging, inspect the assembled transcript:
- # print(json.dumps(..., indent=2, default=str))
|