php-llm-agent/lib/actions/weather.action.php
Frederico @ VilaRosa02 436e0e57c5 new version
2025-09-10 11:40:03 +00:00

304 lines
10 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* MODULE: Weather (Open-Meteo, no API key)
* ---------------------------------------
* Provides three actions:
* @weather_today Lisbon
* @weather_tomorrow Lisbon
* @weather_this_week Lisbon
*
* Uses Open-Meteo Geocoding + Forecast APIs (free, public).
* Returns human-readable summaries (temps °C, precip, wind + weather emoji).
*/
// ───────────────────────────────────────────────────────────────────────────────
// 1) Register actions
// ───────────────────────────────────────────────────────────────────────────────
registerAction(
(new Action())
->setFunctionName("weather_today")
->setArgumentsTypes(["string"]) // location
);
registerAction(
(new Action())
->setFunctionName("weather_tomorrow")
->setArgumentsTypes(["string"]) // location
);
registerAction(
(new Action())
->setFunctionName("weather_this_week")
->setArgumentsTypes(["string"]) // location
);
// ───────────────────────────────────────────────────────────────────────────────
// 2) Public action functions
// ───────────────────────────────────────────────────────────────────────────────
/**
* @param string $place e.g. "Lisbon"
* @return string
*/
function weather_today(string $place): string {
$ctx = wm_build_context($place);
if (isset($ctx['error'])) return $ctx['error'];
// index 0 = today
$i = 0;
$d = $ctx['daily'];
if (!isset($d['time'][$i])) return "No forecast found for today in {$ctx['name']}.";
$dateLabel = wm_pretty_date($d['time'][$i], $ctx['tz']);
return wm_format_day_summary("Today in {$ctx['name']}", $dateLabel, $d, $i);
}
/**
* @param string $place
* @return string
*/
function weather_tomorrow(string $place): string {
$ctx = wm_build_context($place);
if (isset($ctx['error'])) return $ctx['error'];
// index 1 = tomorrow
$i = 1;
$d = $ctx['daily'];
if (!isset($d['time'][$i])) return "No forecast found for tomorrow in {$ctx['name']}.";
$dateLabel = wm_pretty_date($d['time'][$i], $ctx['tz']);
return wm_format_day_summary("Tomorrow in {$ctx['name']}", $dateLabel, $d, $i);
}
/**
* @param string $place
* @return string
*/
function weather_this_week(string $place): string {
$ctx = wm_build_context($place);
if (isset($ctx['error'])) return $ctx['error'];
$d = $ctx['daily'];
if (empty($d['time'])) return "No weekly forecast found for {$ctx['name']}.";
$lines = [];
$lines[] = "This week in {$ctx['name']} ({$ctx['tz']}):";
$n = min(count($d['time']), 7);
for ($i = 0; $i < $n; $i++) {
$date = $d['time'][$i];
$label = wm_pretty_weekday($date, $ctx['tz']); // e.g., Mon 2025-09-15
$lines[] = "" . wm_format_compact_line($label, $d, $i);
}
return implode("\n", $lines);
}
// ───────────────────────────────────────────────────────────────────────────────
// 3) Internals
// ───────────────────────────────────────────────────────────────────────────────
/**
* Resolve place -> lat/lon via Open-Meteo Geocoding and fetch a 7-day daily forecast.
* @param string $place
* @return array context with:
* name (City, Country), tz, daily => [keys matching Open-Meteo daily params]
* or ['error' => 'message']
*/
function wm_build_context(string $place): array {
$place = trim($place);
if ($place === '') return ['error' => "Please provide a location (e.g., 'Lisbon')."];
$geo = wm_http_json("https://geocoding-api.open-meteo.com/v1/search?name=" . urlencode($place) . "&count=1&language=en&format=json");
if (!$geo || empty($geo['results'][0])) {
return ['error' => "Could not find location for '$place'."];
}
$g = $geo['results'][0];
$lat = $g['latitude'];
$lon = $g['longitude'];
$displayName = $g['name'] . (isset($g['country']) ? ", " . $g['country'] : "");
// Daily parameters
$dailyParams = [
'weathercode',
'temperature_2m_max',
'temperature_2m_min',
'precipitation_sum',
'precipitation_probability_max',
'windspeed_10m_max'
];
$forecastUrl = "https://api.open-meteo.com/v1/forecast?"
. http_build_query([
'latitude' => $lat,
'longitude' => $lon,
'daily' => implode(',', $dailyParams),
'forecast_days' => 7,
'timezone' => 'auto'
]);
$fc = wm_http_json($forecastUrl);
if (!$fc || empty($fc['daily'])) {
return ['error' => "Could not fetch forecast for '$displayName'."];
}
$tz = $fc['timezone'] ?? 'local';
return [
'name' => $displayName,
'tz' => $tz,
'daily' => $fc['daily'],
];
}
/**
* Simple GET JSON helper with cURL
* @param string $url
* @return array|null
*/
function wm_http_json(string $url): ?array {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_USERAGENT => "WeatherModule/1.0 (+github.com/you)"
]);
$res = curl_exec($ch);
$err = curl_error($ch);
curl_close($ch);
if ($res === false || !$res) return null;
$data = json_decode($res, true);
return is_array($data) ? $data : null;
}
/**
* Format a single-day verbose summary.
*/
function wm_format_day_summary(string $title, string $dateLabel, array $d, int $i): string {
$code = $d['weathercode'][$i] ?? null;
$desc = wm_weathercode_text($code);
$icon = wm_weathercode_emoji($code);
$tmax = wm_num($d['temperature_2m_max'][$i] ?? null);
$tmin = wm_num($d['temperature_2m_min'][$i] ?? null);
$pp = wm_num($d['precipitation_probability_max'][$i] ?? null);
$pr = wm_num($d['precipitation_sum'][$i] ?? null);
$wmax = wm_num($d['windspeed_10m_max'][$i] ?? null);
$parts = [];
$parts[] = "$title$dateLabel";
$parts[] = "$icon $desc";
if ($tmax !== null && $tmin !== null) $parts[] = "Temp: min {$tmin}°C / max {$tmax}°C";
if ($pp !== null) $parts[] = "Precip prob: {$pp}%";
if ($pr !== null) $parts[] = "Precip total: {$pr} mm";
if ($wmax !== null) $parts[] = "Wind up to: {$wmax} m/s";
return implode("\n", $parts);
}
/**
* Format a compact weekly line for a given day.
*/
function wm_format_compact_line(string $label, array $d, int $i): string {
$code = $d['weathercode'][$i] ?? null;
$icon = wm_weathercode_emoji($code);
$tmax = wm_num($d['temperature_2m_max'][$i] ?? null);
$tmin = wm_num($d['temperature_2m_min'][$i] ?? null);
$pp = wm_num($d['precipitation_probability_max'][$i] ?? null);
$temp = ($tmin !== null && $tmax !== null) ? "{$tmin}{$tmax}°C" : "n/a";
$prob = ($pp !== null) ? "{$pp}%" : "n/a";
return sprintf("%s %s %s (rain prob %s)", $label, $icon, $temp, $prob);
}
/**
* Weather code -> short text
* Ref: https://open-meteo.com/en/docs
*/
function wm_weathercode_text(?int $code): string {
$map = [
0 => "Clear sky",
1 => "Mainly clear",
2 => "Partly cloudy",
3 => "Overcast",
45 => "Fog",
48 => "Depositing rime fog",
51 => "Light drizzle",
53 => "Moderate drizzle",
55 => "Dense drizzle",
56 => "Freezing drizzle (light)",
57 => "Freezing drizzle (dense)",
61 => "Slight rain",
63 => "Moderate rain",
65 => "Heavy rain",
66 => "Freezing rain (light)",
67 => "Freezing rain (heavy)",
71 => "Slight snow fall",
73 => "Moderate snow fall",
75 => "Heavy snow fall",
77 => "Snow grains",
80 => "Rain showers (slight)",
81 => "Rain showers (moderate)",
82 => "Rain showers (violent)",
85 => "Snow showers (slight)",
86 => "Snow showers (heavy)",
95 => "Thunderstorm (slight/moderate)",
96 => "Thunderstorm with slight hail",
99 => "Thunderstorm with heavy hail",
];
return $map[$code] ?? "Unknown conditions";
}
/**
* Weather code -> emoji
*/
function wm_weathercode_emoji(?int $code): string {
if ($code === null) return "🌡️";
if ($code === 0) return "☀️";
if (in_array($code, [1,2])) return "";
if ($code === 3) return "☁️";
if (in_array($code, [45,48])) return "🌫️";
if (in_array($code, [51,53,55,61,63,65,80,81,82])) return "🌧️";
if (in_array($code, [66,67])) return "🌨️🧊";
if (in_array($code, [71,73,75,77,85,86])) return "❄️";
if (in_array($code, [95,96,99])) return "⛈️";
return "🌡️";
}
/**
* Pretty date label (e.g., 2025-09-09 → Tue 9 Sep)
*/
function wm_pretty_date(string $isoDate, string $tz): string {
try {
$dt = new DateTime($isoDate, new DateTimeZone($tz ?: 'UTC'));
return $dt->format('D j M Y');
} catch (Throwable $e) {
return $isoDate;
}
}
/**
* Weekday + date (e.g., Tue 2025-09-09)
*/
function wm_pretty_weekday(string $isoDate, string $tz): string {
try {
$dt = new DateTime($isoDate, new DateTimeZone($tz ?: 'UTC'));
return $dt->format('D Y-m-d');
} catch (Throwable $e) {
return $isoDate;
}
}
/**
* Round number nicely or return null
*/
function wm_num($v): ?string {
if ($v === null) return null;
if ($v === '' || !is_numeric($v)) return null;
return (string)round((float)$v, 1);
}