An agent that only talks is a toy. The moment you give it a tool that hits a real external API, it becomes useful - it can answer with current, real data instead of whatever it memorized in training.
This example gives the agent a FunctionTool that calls wttr.in (a free, no-key weather API) and returns live conditions.
The tool is just a PHP function that does HTTP
Start with the tool's shape: a name, a description the model reads to decide when to use it, and a single required city argument.
use NanoAgent\Agent;
use NanoAgent\Tools\FunctionTool;
$weatherTool = new FunctionTool(
name: 'fetch_live_weather',
description: 'Retrieves current weather for any city using a real weather API.',
parameters: [
'type' => 'object',
'properties' => [
'city' => ['type' => 'string', 'description' => 'The city name']
],
'required' => ['city']
],
The callable is where the real HTTP call happens - a plain cURL request to wttr.in's JSON endpoint, with a timeout and a user agent so the request doesn't hang or get rejected outright.
callable: function (array $args) {
$url = "https://wttr.in/" . urlencode($args['city']) . "?format=j1";
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_USERAGENT => 'NanoAgent/1.0',
]);
$response = curl_exec($ch);
if (curl_errno($ch)) {
$err = curl_error($ch);
curl_close($ch);
return "Network error while reaching weather API: $err";
}
curl_close($ch);
Then decode the response and hand back a plain sentence - not raw JSON - so the model has something natural to fold into its answer.
$data = json_decode($response, true);
$current = $data['current_condition'][0] ?? null;
if (!$current) return "Could not find weather data for '{$args['city']}'.";
return "Current weather in {$args['city']}: "
. $current['weatherDesc'][0]['value']
. ", {$current['temp_C']}°C.";
}
);
The agent decides when to call it
$city = $_GET['city'] ?? 'London';
$agent = new Agent(
llm: $llmConfig,
systemPrompt: "You are a meteorological assistant. You can retrieve real-time "
. "weather information for any city.",
tools: [$weatherTool]
);
$response = $agent->chat("What is the real-time weather in $city right now?");
echo $response;
The user never mentions fetch_live_weather. The model reads the tool's description, decides it's the right tool, fills in city, and the agent runs the function and folds the result into the answer. You wrote the HTTP; the model wrote the decision to use it.
Make the tool robust - the details that matter
A real-world tool lives or dies on error handling. Notice the patterns above:
-
Timeout (
CURLOPT_TIMEOUT) so a slow API can't hang your request. - User-Agent - some APIs reject the default cURL UA.
- Graceful failure string, not an exception. Return a human-readable error as the tool result. The model can then say "I couldn't reach the weather service" instead of your app crashing.
-
Validate the JSON before you trust
$data['current_condition'][0].
Generalize it
Swap wttr.in for anything that speaks HTTP:
- a payments API,
- your own REST backend,
- a search engine,
- a CRM.
Rule of thumb: the tool's description is what the model reads, so write it like you're telling a smart intern what the tool does and when to use it. The parameters schema is the contract; the callable is your normal PHP (cURL, PDO, anything).
Part of the NanoAgent examples series. Landing + demos.
Top comments (0)