Roadmap / Agents & Multi-Agent Systems

✓

Parsing and Validating Tool Calls

easyagentstoolsstructured-output

The problem

A model asks for a tool by writing a tool call: a JSON (JavaScript Object Notation) object between tags, in the middle of its text.

I will look it up.
<tool_call>
{"name": "search",
 "arguments": {"query": "OLMo 3"}}
</tool_call>

The program around the model must not trust what it finds there: the JSON can be broken, the tool can be made up, the arguments can be wrong. You will write the function that finds the calls and checks them, so that only valid calls are run and every problem becomes an error message.

The registry and the checks

registry describes the available tools: {name: {"required": [...], "optional": [...]}}.

Find every <tool_call>...</tool_call> block, in order, and check each, in this order:

  1. The content must be valid JSON, and an object with a string "name" and an object "arguments": otherwise the error is "bad json: <i>", where <i> is the index of the block, from 0.
  2. The tool must exist in registry: otherwise "unknown tool: <name>".
  3. Every required argument must be present: otherwise "missing argument: <name>.<arg>" for the first missing one (in registry order).
  4. Every argument must be required or optional: otherwise "unexpected argument: <name>.<arg>" for the first unexpected one (in the call's order).

A valid block becomes a (name, arguments) pair in calls. An invalid block adds one error string to errors and is skipped. Return (calls, errors).

Example. registry = {"search": {"required": ["query"], "optional": ["limit"]}} and a text with five blocks:

block content result
0 search with query "OLMo 3" valid: ("search", {"query": "OLMo 3"})
1 weather with no arguments "unknown tool: weather"
2 {"name": "search", "arguments": {"query": "x" (cut off) "bad json: 2"
3 search with no arguments "missing argument: search.query"
4 search with query and depth "unexpected argument: search.depth"

The result is calls = [("search", {"query": "OLMo 3"})] and the four errors, in block order.

What to implement

parse_tool_calls(text, registry): return (calls, errors).

Requirements

  • Standard library only (re, json). Use a non-greedy pattern with re.DOTALL: a block can span lines.

Hint

re.findall(r"<tool_call>(.*?)</tool_call>", text, re.DOTALL) returns the contents of all blocks. The ? makes .* stop at the first closing tag, and re.DOTALL lets . match newlines.

solution.py
Python idle

Press Run (⌘/Ctrl + Enter) to test your code.