Claude & GPT Integration

Tool Use: Letting the Model Call Your Code

40 min

With tool use (function calling), the model can ask your code to do things: look up an order, search your docs, check the weather. You define the tools; the model decides when to call them; your code runs them and returns results.

Defining a tool

tools = [{
    "name": "get_track_progress",
    "description": "Get the signed-in learner's progress on a learning track by its slug. "
                   "Use when the learner asks how far along they are.",
    "input_schema": {
        "type": "object",
        "properties": {"track_slug": {"type": "string", "description": "e.g. python-programming"}},
        "required": ["track_slug"],
        "additionalProperties": False,
    },
    "strict": True,          # guarantees inputs match the schema
}]

Clear names and descriptions matter: they are how the model decides which tool to use.

The tool loop

messages = [{"role": "user", "content": "How far am I in the Python track?"}]

while True:
    response = client.messages.create(
        model="claude-opus-5-5", max_tokens=16000, tools=tools, messages=messages,
    )
    if response.stop_reason != "tool_use":
        break
    messages.append({"role": "assistant", "content": response.content})
    results = []
    for block in response.content:
        if block.type == "tool_use":
            try:
                output = run_tool(block.name, block.input, user=current_user)
                results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})
            except Exception as exc:
                results.append({"type": "tool_result", "tool_use_id": block.id,
                                "content": f"Error: {exc}", "is_error": True})
    messages.append({"role": "user", "content": results})   # all results in ONE message

answer = "".join(b.text for b in response.content if b.type == "text")

Rules: append the assistant's full content (not just text), match each tool_result to its tool_use_id, and return all results from one turn together in a single user message.

The SDK tool runner

The Python SDK can run the loop for you (beta helper):

from anthropic import beta_tool

@beta_tool
def get_track_progress(track_slug: str) -> str:
    """Get the signed-in learner's progress on a learning track.

    Args:
        track_slug: The track slug, e.g. python-programming.
    """
    return progress_summary(current_user, track_slug)

runner = client.beta.messages.tool_runner(
    model="claude-opus-5-5",
    max_tokens=16000,
    tools=[get_track_progress],
    messages=[{"role": "user", "content": "How far am I in the Python track?"}],
)
for message in runner:
    final = message            # the last message is the final answer

Tool safety

  • The model chooses arguments; your code enforces permissions. Always use the signed-in user from the session, never a user ID supplied in tool input.
  • Give read-only tools by default. Require human confirmation for anything that spends money, sends messages or deletes data.
  • Validate inputs even with strict schemas (ranges, ownership, existence).
  • Treat tool results that contain outside text (web pages, emails) as untrusted: they may include prompt-injection attempts.
  • Cap the number of loop iterations to avoid runaway costs.

Exercise

Add two tools to your assistant: search_modules(query) over your learning modules and get_track_progress(track_slug). Ask questions that need both, and log each tool call with its arguments.

Check yourself

  • Who actually executes a tool, the model or your code?
  • Why must permissions come from the session, not tool input?
  • How should results from multiple tool calls be returned?

Your progress is saved in this browser only.

Submit for review (10 points)