An agent sends tool schemas to a model and gets tool calls back. The arguments in a reply can have the wrong types, missing fields or extra keys. Before the agent runs a tool, it must parse each call and validate its arguments. Errors with exact paths let the model fix the call in one more turn.
Task
Write three functions. Use the standard library json module only.
parse_tool_calls(reply_text)parses the raw JSON body of a chat completion reply. The calls are inbody["choices"][0]["message"]["tool_calls"]. Each call has anidand afunctionwith anameand anargumentsfield. Theargumentsfield is a JSON string, not an object. Return one dict{"id": ..., "name": ..., "arguments": ...}for each call, in reply order, whereargumentsis the decoded dict.- If
tool_callsis missing ornull, return[]. - If
argumentsis an empty or blank string, the arguments are{}. - If
argumentsis not valid JSON, or does not decode to a JSON object, raiseValueError.
- If
validate_args(schema, args)checks a value against a JSON Schema subset and returns a list of error strings. An empty list means the value is valid.check_reply(reply_text, schemas)parses the reply and returns one pair(id, errors)for each call, in reply order.schemasmaps a tool name to its schema. For a name that is not inschemas, the errors are["/: unknown tool <name>"].
The schema subset has these keywords: type, properties, required, enum, additionalProperties and items. A type is one of object, array, string, integer, number, boolean and null. The JSON type of a Python value is:
| Python value | JSON type |
|---|---|
None |
null |
True, False |
boolean |
int |
integer |
float |
number |
str |
string |
list |
array |
dict |
object |
The type integer matches the JSON type integer only, so 1.0 does not match. The type number matches integer and number. A boolean never matches integer or number.
Check one value at a path with these steps, in this order:
- If the schema has
typeand the value does not match it, add<path>: expected <type>, got <json type>. Stop. Do not check the children. - If the schema has
enumand the value is not in the list (Python==), add<path>: value <json.dumps(value)> is not in <json.dumps(enum)>. Stop. - If the type is
object:- For each name in
required, in list order, that is not a key of the value, add<path>/<name>: missing required property. - Then for each key of the value, in its input order: if the key is in
properties, check the child value at<path>/<key>. Else, ifadditionalPropertiesisfalse, add<path>/<key>: unexpected property. WithoutadditionalProperties: false, extra keys are allowed.
- For each name in
- If the type is
arrayand the schema hasitems, check each element at<path>/<index>, in index order.
A path is a JSON pointer. The root value has the path /. A child path is the parent path, a slash and the segment. The child of the root / with key city is /city. In a segment, replace ~ with ~0 first, then replace / with ~1.
Input and output
| Name | Type | Meaning |
|---|---|---|
reply_text |
str |
the raw JSON body of one reply |
schema |
dict |
a schema in the subset above |
args |
any JSON value | the value to check, usually a dict |
schemas |
dict |
a map from tool name to schema |
parse_tool_callsreturns alistofdict.validate_argsreturns alistofstr.check_replyreturns alistof(str, list of str)tuples.
Constraints
- Schemas nest at most 5 levels deep.
- Do not change the schema or the arguments.
- Use the standard library only.
Examples
REPLY = '{"choices": [{"message": {"role": "assistant", "content": null, "tool_calls": [{"id": "call_abc123", "type": "function", "function": {"name": "get_weather", "arguments": "{\\"city\\":\\"Bengaluru\\",\\"units\\":\\"celsius\\"}"}}, {"id": "call_def456", "type": "function", "function": {"name": "get_weather", "arguments": "{\\"city\\":42,\\"units\\":\\"kelvin\\",\\"days\\":3}"}}]}, "finish_reason": "tool_calls"}]}'
WEATHER = {
"type": "object",
"properties": {
"city": {"type": "string"},
"units": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["city", "units"],
"additionalProperties": False,
}
parse_tool_calls(REPLY)[{'id': 'call_abc123', 'name': 'get_weather', 'arguments': {'city': 'Bengaluru', 'units': 'celsius'}}, {'id': 'call_def456', 'name': 'get_weather', 'arguments': {'city': 42, 'units': 'kelvin', 'days': 3}}]check_reply(REPLY, {"get_weather": WEATHER})[('call_abc123', []), ('call_def456', ['/city: expected string, got integer', '/units: value "kelvin" is not in ["celsius", "fahrenheit"]', '/days: unexpected property'])]USER = {
"type": "object",
"required": ["id", "tags"],
"properties": {
"id": {"type": "integer"},
"tags": {"type": "array", "items": {"type": "string", "enum": ["a", "b"]}},
"score": {"type": "number"},
"active": {"type": "boolean"},
},
"additionalProperties": False,
}
validate_args(USER, {"id": True, "tags": ["a", "c", 3], "score": 1, "extra/x": 1, "active": 1})['/id: expected integer, got boolean', '/tags/1: value "c" is not in ["a", "b"]', '/tags/2: expected string, got integer', '/extra~1x: unexpected property', '/active: expected boolean, got integer']