Chapter 7 · AI-Curious Developer

Detecting a tool call

A model can reply in two different ways, and your code has to tell them apart. Sometimes it answers in plain text. Sometimes it asks to use a tool — a structured signal that means "run this function with these arguments and give me the result".

In the structured form, the first content block's "type" is "tool_use" (instead of "text"), and it carries a "name" (which tool) and an "input" dict (the arguments):

  • text reply → response["content"][0]["type"] == "text"
  • tool call → response["content"][0]["type"] == "tool_use", plus "name" and "input"

Detecting the tool call is this step's whole job: inspect the response, decide which branch you're in, and pull out the intent (the tool name + arguments) when it's a tool call. That's intent parsing — mapping the model's output to "what does it want to happen". Actually *running* the tool is the next entry.

The technique is plain structured inspection: index into the dict, compare a string with ==, return a tuple. The same approach works if the model signals intent as a prefix in plain text (e.g. a line starting with get_weather) — you'd use string methods like .startswith() and .split() instead.

Syntax

# Two response shapes — branch on the first block's type:
text_resp = {"content": [{"type": "text", "text": "hello"}]}
tool_resp = {"content": [{"type": "tool_use",
                          "name": "get_weather",
                          "input": {"city": "London"}}]}

is_tool = response["content"][0]["type"] == "tool_use"

Worked examples

Detect which kind of response arrived

def is_tool_call(response):
    return response["content"][0]["type"] == "tool_use"

tool_resp = {"content": [{"type": "tool_use", "name": "get_weather",
                          "input": {"city": "London"}}]}
text_resp = {"content": [{"type": "text", "text": "hello"}]}

print(is_tool_call(tool_resp))
print(is_tool_call(text_resp))
# Output:
# True
# False

Extract the intent: tool name + arguments

def extract_tool_call(response):
    if not is_tool_call(response):
        raise ValueError("Not a tool call")
    tool = response["content"][0]
    return (tool["name"], tool["input"])

tool_resp = {"content": [{"type": "tool_use", "name": "get_weather",
                          "input": {"city": "London"}}]}
print(extract_tool_call(tool_resp))
# Output:
# ('get_weather', {'city': 'London'})

Route on the detected type

def route_response(response):
    if is_tool_call(response):
        name, _ = extract_tool_call(response)
        return f"TOOL:{name}"
    return f"TEXT:{response['content'][0]['text']}"

print(route_response({"content": [{"type": "tool_use", "name": "get_weather",
                                   "input": {"city": "London"}}]}))
print(route_response({"content": [{"type": "text", "text": "hello"}]}))
# Output:
# TOOL:get_weather
# TEXT:hello

Common mistakes & gotchas

Assuming every response is text

If you always reach for response["content"][0]["text"], a tool-call response has no "text" key and you get a KeyError. Check the "type" *first* and branch — never assume which kind arrived.

Detecting is not running

extract_tool_call only tells you *what* the model wants — ("get_weather", {"city": "London"}). It does not fetch any weather. Pulling out the intent and actually dispatching it are two separate steps (dispatch is the next entry); conflating them hides bugs.

Fragile string-prefix detection

When intent comes as a text prefix instead of a structured block, prompt.startswith("get_weather") is brittle — a user message that happens to start with those words triggers it falsely. Structured tool_use blocks exist precisely to avoid that ambiguity; prefer them when available.

Why it matters

Tool use is what turns a chatbot into an agent that can *do* things — look up data, run a calculation, hit an API. It all starts with this detection step: reliably telling "the model answered" from "the model wants me to act". Get the branch wrong and your agent either crashes or silently ignores its own tools.