Tool loop in PHP
The model can call two functions, find_book and availability, as many times as it needs; the loop ends when it answers in text. It uses plain HTTP, so there is nothing to install beyond the standard library unless noted. It needs PHP 8.1+ with the curl extension.
<?php
const LIBRARY = [
'978-0141439518' => ['title' => 'Pride and Prejudice', 'copies' => 3, 'on_loan' => 3, 'due_back' => '2026-10-19'],
'978-0007119318' => ['title' => 'The Hobbit', 'copies' => 2, 'on_loan' => 1, 'due_back' => null],
];
$tools = json_decode(<<<'JSON'
[
{"type": "function", "function": {"name": "find_book", "description": "Search the catalogue by title words. Returns ISBNs.",
"parameters": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}}},
{"type": "function", "function": {"name": "availability", "description": "Copies available now for an ISBN, and when the next one is due back.",
"parameters": {"type": "object", "properties": {"isbn": {"type": "string"}}, "required": ["isbn"]}}}
]
JSON, true);
function run_tool(string $name, string $rawArgs): array {
$args = json_decode($rawArgs ?: '{}', true);
if (!is_array($args)) return ['error' => 'arguments were not valid JSON'];
switch ($name) {
case 'find_book':
$q = mb_strtolower($args['query'] ?? '');
$hits = [];
foreach (LIBRARY as $isbn => $b) {
if (str_contains(mb_strtolower($b['title']), $q)) $hits[] = ['isbn' => $isbn, 'title' => $b['title']];
}
return ['results' => $hits];
case 'availability':
$b = LIBRARY[$args['isbn'] ?? ''] ?? null;
if (!$b) return ['error' => 'no book with ISBN ' . ($args['isbn'] ?? '')];
$free = $b['copies'] - $b['on_loan'];
return ['available' => $free, 'next_due_back' => $free === 0 ? $b['due_back'] : null];
default:
return ['error' => "unknown tool $name"];
}
}
function complete(array $messages, array $tools): array {
$ch = curl_init('https://app.syntic.ai/v1/chat/completions');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('SYNTIC_API_KEY'), 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode(['model' => 'Amara S1.2', 'stream' => false, 'max_tokens' => 800, 'tools' => $tools, 'messages' => $messages]),
]);
$data = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 300) throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
return $data['choices'][0]['message'];
}
$messages = [
['role' => 'system', 'content' => 'You help members of a town library. Use the tools; never guess availability.'],
['role' => 'user', 'content' => 'Can I borrow Pride and Prejudice today? If not, when?'],
];
for ($round = 0; $round < 8; $round++) {
$msg = complete($messages, $tools);
if (empty($msg['tool_calls'])) { echo $msg['content'], PHP_EOL; exit; }
$messages[] = $msg;
foreach ($msg['tool_calls'] as $call) {
$result = run_tool($call['function']['name'], $call['function']['arguments'] ?? '{}');
$messages[] = ['role' => 'tool', 'tool_call_id' => $call['id'], 'content' => json_encode($result)];
}
}
echo "Sorry, I couldn't finish that request.", PHP_EOL;The three rules
- Append the assistant message with its
tool_callsexactly as received. - Add one
toolmessage per call, with the matchingtool_call_id. - Send the same
toolslist on every round.
Errors from your functions are returned to the model as results so it can recover, and the round limit stops a runaway loop.
Related
Was this page helpful?