CookbookTool loop in PHP

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

  1. Append the assistant message with its tool_calls exactly as received.
  2. Add one tool message per call, with the matching tool_call_id.
  3. Send the same tools list 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.

Was this page helpful?