152 lines
5.6 KiB
TypeScript
152 lines
5.6 KiB
TypeScript
import axios from "axios";
|
|
import {toolsLogger} from "./tool-logger";
|
|
|
|
const logger = toolsLogger.child("weather");
|
|
import {Environment} from "../../common/environment";
|
|
import {logError} from "../../util/utils";
|
|
import {AiTool} from "../tool-types";
|
|
import {asNonEmptyString} from "./utils";
|
|
|
|
export const getWeatherTool = {
|
|
type: "function",
|
|
function: {
|
|
name: "get_weather",
|
|
description: "Get the current temperature for a city.",
|
|
parameters: {
|
|
type: "object",
|
|
properties: {
|
|
city: {
|
|
type: "string",
|
|
description: "The name of the city."
|
|
},
|
|
lang: {
|
|
type: "string",
|
|
description: "language code for the response/content. Must be a valid ISO 639-1 two-letter language code, for example: \"en\", \"ru\", \"de\", \"fr\".Determine the value automatically from the language the user is using to communicate with the LLM. If the user explicitly requests a specific language, use that requested language instead. Do not use language names, locales, or regional variants such as \"English\", \"ru-RU\", or \"en_US\"; return only the ISO 639-1 code."
|
|
}
|
|
},
|
|
required: ["city", "lang"],
|
|
}
|
|
}
|
|
} satisfies AiTool;
|
|
|
|
export const weatherToolPrompt = [
|
|
"Weather tool rules:",
|
|
"- Use `get_weather` for current weather, current temperature, conditions, hot/cold/rainy/snowy questions, and weather follow-ups.",
|
|
"- Weather is live/current data. Never answer it from memory.",
|
|
"- A weather tool result is valid only for the exact city used in that tool call.",
|
|
"- If the user changes the city, call `get_weather` again.",
|
|
"- Follow-up questions like `what about Moscow?`, `and for Krasnodar?`, `what about there?`, `what about Berlin?` inherit the previous weather intent and require a new tool call for the new city.",
|
|
"",
|
|
"Arguments:",
|
|
"- `city`: the city from the latest user request or resolved from the follow-up context.",
|
|
"- `lang`: ISO 639-1 two-letter language code only: `ru`, `en`, `de`, etc.",
|
|
"",
|
|
"Do not guess, compare, or reuse weather from another city.",
|
|
"If the city is missing or unclear, ask the user to specify it.",
|
|
].join("\n");
|
|
|
|
export async function getWeather(args?: Record<string, unknown>): Promise<Record<string, unknown> | null> {
|
|
const startedAt = Date.now();
|
|
logger.info("start", {args});
|
|
try {
|
|
const city = asNonEmptyString(args?.city);
|
|
const lang = asNonEmptyString(args?.lang);
|
|
|
|
if (!city) {
|
|
return null;
|
|
}
|
|
|
|
const apiKey = Environment.OPEN_WEATHER_MAP_API_KEY;
|
|
|
|
const geocodeResponse = (await axios.get("https://api.openweathermap.org/geo/1.0/direct", {
|
|
params: {
|
|
q: city,
|
|
limit: 1,
|
|
appid: apiKey,
|
|
},
|
|
})).data[0];
|
|
logger.debug("geocode.done", {city, country: geocodeResponse?.country, hasResult: !!geocodeResponse, geocodeResponse});
|
|
if (!geocodeResponse) {
|
|
return {
|
|
ok: false,
|
|
tool: "get_weather",
|
|
error: `City not found: ${city}`,
|
|
city,
|
|
lang,
|
|
};
|
|
}
|
|
const lat = geocodeResponse.lat;
|
|
const lon = geocodeResponse.lon;
|
|
|
|
const response = (await axios.get("https://api.openweathermap.org/data/2.5/weather", {
|
|
params: {
|
|
lat,
|
|
lon,
|
|
units: "metric",
|
|
appid: apiKey,
|
|
...(lang ? {lang} : {}),
|
|
},
|
|
})).data;
|
|
logger.debug("weather_api.done", {city, country: geocodeResponse.country, lang, units: "metric", hasResponse: !!response});
|
|
|
|
const main = response.main;
|
|
const sys = response.sys;
|
|
const wind = response.wind;
|
|
const weather = response.weather[0];
|
|
|
|
let date = new Date(sys.sunrise * 1000);
|
|
|
|
const sunrise = [
|
|
date.getUTCHours(),
|
|
date.getUTCMinutes(),
|
|
date.getUTCSeconds(),
|
|
]
|
|
.map((v) => String(v).padStart(2, "0"))
|
|
.join(":");
|
|
|
|
date = new Date(sys.sunset * 1000);
|
|
|
|
const sunset = [
|
|
date.getUTCHours(),
|
|
date.getUTCMinutes(),
|
|
date.getUTCSeconds(),
|
|
]
|
|
.map((v) => String(v).padStart(2, "0"))
|
|
.join(":");
|
|
|
|
return {
|
|
ok: true,
|
|
tool: "get_weather",
|
|
scope: {
|
|
city,
|
|
lang,
|
|
validOnlyForExactCity: true,
|
|
liveData: true,
|
|
note: "If the user asks about another city, call get_weather again.",
|
|
},
|
|
weather: {
|
|
main: weather.main,
|
|
description: weather.description,
|
|
temperature: main.temp,
|
|
temperatureMax: main.temp_max,
|
|
temperatureMin: main.temp_min,
|
|
feelsLike: main.feels_like,
|
|
humidity: main.humidity,
|
|
pressure: main.pressure,
|
|
seaLevel: main.sea_level ?? null,
|
|
groundLevel: main.grnd_level ?? null,
|
|
sunriseUtc: sunrise,
|
|
sunsetUtc: sunset,
|
|
windDegree: wind.deg,
|
|
windSpeed: wind.speed,
|
|
},
|
|
};
|
|
} catch (e: unknown) {
|
|
logger.error("failed", {duration: logger.duration(startedAt), error: e});
|
|
logError(e);
|
|
return null;
|
|
} finally {
|
|
logger.debug("done", {duration: logger.duration(startedAt)});
|
|
}
|
|
}
|