agentic-loop.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. #!/usr/bin/env python3
  2. """Minimal, correct tool-use agentic loop on the Anthropic Messages API.
  3. The canonical pattern: define a tool, call messages.create, and keep looping
  4. while stop_reason == "tool_use" — execute each requested tool, append a
  5. tool_result, and re-request until stop_reason == "end_turn".
  6. Run: pip install anthropic (then: export ANTHROPIC_API_KEY=sk-...)
  7. python agentic-loop.py
  8. Copy this file and adapt the >>> ADAPT marks for your own tools.
  9. Reflects the current API (model claude-opus-4-8, typed content blocks).
  10. """
  11. # The Anthropic SDK accepts plain dict literals for tools/messages at runtime
  12. # (as the official docs show), but its strict TypedDict stubs over-narrow them.
  13. # Silence those false positives so this starter stays readable; real apps may
  14. # prefer the SDK's typed params (anthropic.types.ToolParam, MessageParam).
  15. # pyright: reportArgumentType=false
  16. import anthropic
  17. client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from the environment
  18. MODEL = "claude-opus-4-8" # >>> ADAPT: pick a tier (see the skill's model table)
  19. # --- 1. Define your tool(s) ------------------------------------------------
  20. # The input_schema is JSON Schema. Write a description that says WHEN to call.
  21. TOOLS = [
  22. {
  23. "name": "get_weather", # >>> ADAPT
  24. "description": "Get the current weather for a city. "
  25. "Call this whenever the user asks about weather conditions.",
  26. "input_schema": {
  27. "type": "object",
  28. "properties": {
  29. "location": {"type": "string", "description": "City, e.g. 'Paris'"},
  30. },
  31. "required": ["location"],
  32. },
  33. }
  34. ]
  35. # --- 2. Implement each tool ------------------------------------------------
  36. # Map tool name -> a Python callable. Never trust the arguments blindly: they
  37. # come from the model. Validate before doing anything with side effects.
  38. def get_weather(location: str) -> str: # >>> ADAPT: real implementation
  39. return f"It is 21°C and sunny in {location}."
  40. TOOL_IMPLS = {"get_weather": get_weather}
  41. def run_tool(name: str, tool_input: dict) -> str:
  42. """Dispatch a tool call, returning a string result for the model."""
  43. impl = TOOL_IMPLS.get(name)
  44. if impl is None:
  45. return f"ERROR: unknown tool {name!r}"
  46. try:
  47. return impl(**tool_input)
  48. except Exception as exc: # surface failures back to the model, don't crash
  49. return f"ERROR running {name}: {exc}"
  50. # --- 3. The loop -----------------------------------------------------------
  51. def agent(user_prompt: str, max_turns: int = 10) -> str:
  52. # The conversation is a growing list of message dicts we own and replay.
  53. messages = [{"role": "user", "content": user_prompt}]
  54. for _ in range(max_turns):
  55. response = client.messages.create(
  56. model=MODEL,
  57. max_tokens=4096,
  58. tools=TOOLS,
  59. messages=messages,
  60. )
  61. # Append the assistant turn VERBATIM — content is a list of typed
  62. # blocks (text and/or tool_use). It must go back as-is next request.
  63. messages.append({"role": "assistant", "content": response.content})
  64. # If the model didn't ask for a tool, we're done — return its text.
  65. if response.stop_reason != "tool_use":
  66. return "".join(
  67. block.text for block in response.content if block.type == "text"
  68. )
  69. # Otherwise: execute EVERY tool_use block and collect tool_result
  70. # blocks (the model may request several tools in parallel).
  71. tool_results = []
  72. for block in response.content:
  73. if block.type != "tool_use":
  74. continue # skip text/thinking blocks
  75. result_text = run_tool(block.name, block.input)
  76. tool_results.append({
  77. "type": "tool_result",
  78. "tool_use_id": block.id, # MUST echo the matching id
  79. "content": result_text,
  80. # "is_error": True, # set when the tool failed
  81. })
  82. # Feed results back as a single user turn, then loop to re-request.
  83. messages.append({"role": "user", "content": tool_results})
  84. return "Stopped: hit max_turns without an end_turn."
  85. if __name__ == "__main__":
  86. answer = agent("What's the weather in Tokyo right now?")
  87. print(answer)
  88. # For debugging, inspect the assembled transcript:
  89. # print(json.dumps(..., indent=2, default=str))