"""Tool-call parsing, validation, and repetition checks.
Pure helpers for complete argument parsing, schema normalization, explicit tool
markup, and repetition checks. Callers own execution, history, and persistence.
"""
import ast
import copy
import json
import math
import re
from uuid import uuid4
FINAL_ANSWER_PROMPT = (
'Tool use has stopped for this response. Use the results already available to answer the user. '
'Explain any unfinished work briefly. Do not request or describe additional tool calls.'
)
INCOMPLETE_CALL_ERROR = (
'Error: the model response was incomplete; its tool calls were not executed. Retry a complete call.'
)
def add_tool_call_metadata(tools, *, labels=False, approval=False, language='English'):
"""Extend request-local schemas; markers identify metadata owned by the application."""
for name, tool in list(tools.items()):
spec = tool.get('spec') or {}
schema = spec.get('parameters') or {}
if (
tool.get('tool_id') == 'builtin:call_tool'
or schema.get('type', 'object') != 'object'
or any(key in schema for key in ('$ref', 'oneOf', 'anyOf', 'allOf', 'if'))
):
continue
properties = schema.get('properties', {})
injected, markers = {}, {}
if labels and not tool.get('hidden') and not tool.get('display_labels') and '__labels__' not in properties:
injected['__labels__'] = {
'type': 'object',
'description': (
f'Short UI labels in {language}. Describe the action and its subject, not technical tool names. '
'Use at most 8 words and 120 characters each, without a trailing period. '
'The completed label describes the action taken, not an unverified outcome.'
),
'properties': {
'running': {'type': 'string', 'description': 'While running, e.g. "Searching for flights".'},
'completed': {'type': 'string', 'description': 'After success, e.g. "Searched for flights".'},
},
'required': ['running', 'completed'],
'additionalProperties': False,
}
markers['display_labels'] = True
if (
approval
and not tool.get('approval_review')
and '__approval__' not in properties
and tool.get('tool_id') != 'builtin:search_tools'
and not (name == 'ask_user' and tool.get('type') == 'builtin')
):
injected['__approval__'] = {
'type': 'object',
'properties': {
'required': {
'type': 'boolean',
'description': 'Whether this call needs human approval under the approval policy. '
'When uncertain, use true.',
},
},
'required': ['required'],
'additionalProperties': False,
}
markers['approval_review'] = True
if injected:
tools[name] = {
**tool,
**markers,
'spec': {
**spec,
'parameters': {
**schema,
'type': 'object',
'properties': {**injected, **properties},
'required': [*injected, *schema.get('required', [])],
},
},
}
_TOOL_APPROVAL_PROMPT_RE = re.compile(r'\n?\n.*?', re.DOTALL)
def strip_tool_approval_prompt(prompt):
"""Remove request-specific approval instructions before rebuilding or reusing a prompt."""
return _TOOL_APPROVAL_PROMPT_RE.sub('', prompt) if prompt else prompt
def set_tool_call_metadata(item, tools):
"""Copy application-owned display metadata, never trusting provider annotations."""
if item.get('type') != 'function_call':
return
tool = tools.get(item.get('name'), {})
for field, source in (('tool_id', 'tool_id'), ('tool_type', 'type')):
item.pop(field, None)
if tool.get(source):
item[field] = tool[source]
item.pop('tool_targets', None)
if tool.get('type') == 'builtin' and tool.get('tool_id') == 'builtin:call_tool':
item['tool_targets'] = {
name: {'tool_id': target['tool_id'], 'tool_type': target['type']}
for name, target in tools.items()
if target.get('type') == 'builtin' and target.get('tool_id') and not target.get('hidden')
}
if tool.get('hidden') is True:
item['hidden'] = True
def parse_arguments(raw):
"""Accept complete JSON or JSON-compatible Python literals, never invent missing values."""
if isinstance(raw, str):
text = raw.strip()
fence = re.fullmatch(r'```(?:json)?\s*\n(.*?)\n```', text, re.DOTALL)
if fence:
text = fence[1]
try:
raw = json.loads(text or '{}')
except (ValueError, RecursionError):
try:
# Literal recovery is bounded independently of valid JSON/code arguments.
if len(text) > 65_536:
raise ValueError('Arguments are too large for literal recovery.')
raw = ast.literal_eval(text)
except (ValueError, SyntaxError, RecursionError, MemoryError) as exc:
raise ValueError('Tool arguments must be a complete JSON object.') from exc
if isinstance(raw, str):
# Some providers encode the entire argument object twice. Unwrap only one extra layer.
try:
raw = json.loads(raw)
except (ValueError, RecursionError) as exc:
raise ValueError('Tool arguments must be a complete JSON object.') from exc
if not isinstance(raw, dict):
raise ValueError('Tool arguments must be a JSON object.')
_check_json_value(raw)
return raw
def _check_json_value(value, depth=0):
if depth > 32:
raise ValueError('Tool arguments are nested too deeply.')
if isinstance(value, dict):
for key, child in value.items():
if not isinstance(key, str):
raise ValueError('Tool argument keys must be strings.')
_check_json_value(child, depth + 1)
elif isinstance(value, list):
for child in value:
_check_json_value(child, depth + 1)
elif isinstance(value, float) and not math.isfinite(value):
raise ValueError('Tool arguments must contain finite numbers.')
elif value is not None and not isinstance(value, (str, int, float, bool)):
raise ValueError('Tool arguments must contain JSON values only.')
def _coerce_scalar(value, kind):
if kind == 'string' and isinstance(value, (int, float)) and not isinstance(value, bool):
return str(value)
if not isinstance(value, str):
return value
text = value.strip()
if kind == 'boolean' and text.lower() in {'true', 'false'}:
return text.lower() == 'true'
if kind not in {'integer', 'number', 'object', 'array'}:
return value
try:
parsed = json.loads(text)
except (ValueError, RecursionError):
return value
if kind in {'object', 'array'}:
return parsed if isinstance(parsed, dict if kind == 'object' else list) else value
if type(parsed) is int or (kind == 'number' and type(parsed) is float and math.isfinite(parsed)):
return parsed
return value
def _coerce(value, schema, depth=0):
if not isinstance(schema, dict) or depth > 32:
return value
# Ambiguous unions/references must be validated without guessing which branch was intended.
if any(key in schema for key in ('$ref', 'oneOf', 'anyOf', 'allOf', 'if')):
return value
kind = schema.get('type')
if isinstance(kind, list):
kinds = [item for item in kind if item != 'null']
kind = kinds[0] if len(kinds) == 1 else None
value = _coerce_scalar(value, kind)
if isinstance(value, dict):
properties = schema.get('properties', {})
return {key: _coerce(child, properties.get(key, {}), depth + 1) for key, child in value.items()}
if isinstance(value, list) and isinstance(schema.get('items'), dict):
return [_coerce(child, schema['items'], depth + 1) for child in value]
return value
def normalize_arguments(raw, schema=None):
params = parse_arguments(raw)
if schema is not None:
# Provider schemas strip nullability; use them for coercion, leaving validation to the tool.
params = _coerce(params, schema)
_check_json_value(params)
return params
def split_tool_calls(calls):
"""Split complete concatenated objects, preserving each requested call."""
expanded = []
decoder = json.JSONDecoder()
for call in calls:
call = copy.deepcopy(call)
function = call.setdefault('function', {})
raw = function.get('arguments')
raw = raw if isinstance(raw, str) else json.dumps(raw) if raw is not None else ''
function['arguments'] = raw
objects, position = [], 0
try:
while position < len(raw):
if raw[position].isspace():
position += 1
continue
value, position = decoder.raw_decode(raw, position)
if not isinstance(value, dict):
raise ValueError('Expected an object')
objects.append(value)
except (ValueError, RecursionError):
objects = []
if len(objects) <= 1:
expanded.append(call)
continue
for index, value in enumerate(objects):
cloned = copy.deepcopy(call)
if index:
cloned['id'] = f'call_{uuid4().hex[:24]}'
cloned['function']['arguments'] = json.dumps(value, ensure_ascii=False)
expanded.append(cloned)
return expanded
_CALL_BLOCK = re.compile(r'(.*?)|(.*?)', re.DOTALL)
_PARAMETER = re.compile(r'(.*?)', re.DOTALL)
def recover_tool_calls(text, allowed_names):
"""Recover a message consisting ONLY of complete call wrappers; examples and prose stay text."""
if not isinstance(text, str) or len(text) > 65_536:
return []
calls, cursor = [], 0
for match in _CALL_BLOCK.finditer(text):
if text[cursor : match.start()].strip():
return []
try:
if match[1] is not None:
data = parse_arguments(match[1])
function = data.get('function', data)
name = function['name']
arguments = function.get('arguments', function.get('parameters', function.get('args', {})))
else:
name, body = match[2], match[3]
arguments, end = {}, 0
for parameter in _PARAMETER.finditer(body):
if body[end : parameter.start()].strip() or parameter[1] in arguments:
return []
arguments[parameter[1]] = parameter[2]
end = parameter.end()
if body[end:].strip():
return []
if name not in allowed_names:
return []
arguments = parse_arguments(arguments)
except (ValueError, TypeError, KeyError, AttributeError, RecursionError):
return []
calls.append(
{
'id': f'call_{uuid4().hex[:24]}',
'type': 'function',
'function': {'name': name, 'arguments': json.dumps(arguments, ensure_ascii=False)},
}
)
cursor = match.end()
return calls if not text[cursor:].strip() else []
def tool_call_key(call):
"""Compare calls by name and arguments, independently of provider IDs."""
arguments = call.get('arguments', '{}')
try:
arguments = parse_arguments(arguments)
except ValueError:
arguments = arguments if isinstance(arguments, str) else repr(arguments)
return call.get('name', ''), json.dumps(arguments, sort_keys=True, ensure_ascii=False)
def tool_repetition_error(call, previous_calls):
"""Stop unchanged retries after two consecutive failures; successful polls can repeat."""
key = tool_call_key(call)
recent = previous_calls[-2:]
if len(recent) == 2 and all(tool_call_key(item) == key and item.get('status') == 'failed' for item in recent):
return 'Error: the same tool call failed twice. Finish using the available results.'
return None
def prepare_tool_call(call, schema=None, *, display_labels=False, approval_review=False):
"""Normalize a call before approval; return an actionable error for the model."""
original_arguments = call.get('arguments', '{}')
try:
arguments = dict(parse_arguments(original_arguments))
if display_labels and '__labels__' in arguments:
labels = arguments.pop('__labels__')
call.pop('display_labels', None)
if isinstance(labels, dict) and all(
isinstance(labels.get(key), str) and 0 < len(labels[key].strip()) <= 120
for key in ('running', 'completed')
):
call['display_labels'] = {key: labels[key].strip() for key in ('running', 'completed')}
if approval_review and '__approval__' in arguments:
approval = arguments.pop('__approval__')
valid = isinstance(approval, dict) and set(approval) == {'required'} and type(approval['required']) is bool
call['permission_review'] = {'engine': '', 'required': approval['required'] if valid else True}
arguments = normalize_arguments(arguments, schema)
except ValueError as exc:
return f'Error: {exc} Correct the arguments before retrying.'
normalized = json.dumps(arguments, ensure_ascii=False)
if normalized != original_arguments:
call.setdefault('original_arguments', original_arguments)
call['arguments'] = normalized
return None
def final_answer_payload(payload):
"""Make one OpenAI-compatible continuation request with tool calls turned off."""
return {
**payload,
'tool_choice': 'none',
'messages': [*payload.get('messages', []), {'role': 'system', 'content': FINAL_ANSWER_PROMPT}],
}