1import Anthropic from "@anthropic-ai/sdk";
2
3const client = new Anthropic(); // reads ANTHROPIC_API_KEY
4
5const tools: Anthropic.Tool[] = [
6 {
7 name: "get_weather",
8 description:
9 "Get the current weather for one city: temperature in °C, the sky and the wind. " +
10 "Use it whenever the user asks about weather, temperature or what to wear outdoors " +
11 "right now, once for each city. It has no forecasts or history, so don't use it " +
12 "for tomorrow or last week.",
13 strict: true,
14 input_schema: {
15 type: "object",
16 properties: {
17 city: {
18 type: "string",
19 description: "City name, with the country if it's ambiguous, e.g. Paris, France",
20 },
21 },
22 required: ["city"],
23 additionalProperties: false,
24 },
25 },
26];
27
28const SYSTEM =
29 "You are a friendly weather assistant. Answer in one or two sentences. " +
30 "Use get_weather for current conditions and never guess the weather.";
31
32// WMO weather codes (Open-Meteo docs), the common ones.
33const SKY: Record<number, string> = {
34 0: "clear", 1: "mainly clear", 2: "partly cloudy", 3: "overcast", 45: "fog",
35 51: "drizzle", 61: "light rain", 63: "rain", 65: "heavy rain",
36 71: "light snow", 73: "snow", 75: "heavy snow", 80: "showers", 95: "thunderstorm",
37};
38
39// The tool: Open-Meteo's free API, no key needed (non-commercial use).
40async function getWeather(city: string) {
41 const geo = await fetch(
42 `https://geocoding-api.open-meteo.com/v1/search?count=1&name=${encodeURIComponent(city)}`,
43 );
44 const place = (await geo.json()).results?.[0];
45 if (!place) {
46 throw new Error(`No city called "${city}". Check the spelling, or try a nearby larger city.`);
47 }
48 const res = await fetch(
49 `https://api.open-meteo.com/v1/forecast?latitude=${place.latitude}` +
50 `&longitude=${place.longitude}¤t=temperature_2m,weather_code,wind_speed_10m`,
51 );
52 if (!res.ok) throw new Error(`The weather service failed (HTTP ${res.status}). Try again later.`);
53 const { current } = await res.json();
54 return {
55 city: `${place.name}, ${place.country}`,
56 temp_c: current.temperature_2m,
57 sky: SKY[current.weather_code] ?? "mixed",
58 wind_kmh: current.wind_speed_10m,
59 };
60}
61
62function printText(reply: Anthropic.Message) {
63 for (const block of reply.content) if (block.type === "text") console.log(block.text);
64}
65
66const question = process.argv[2] ?? "Do I need a coat in Chicago?";
67const messages: Anthropic.MessageParam[] = [{ role: "user", content: question }];
68const ask = () =>
69 client.messages.create({ model: "claude-haiku-5-5", max_tokens: 4096, system: SYSTEM, tools, messages });
70
71// Request one: Claude reads the question and the tool, and decides.
72const first = await ask();
73if (first.stop_reason !== "tool_use") {
74 printText(first); // no tool needed
75 process.exit(0);
76}
77
78// Run every call (there may be several), and answer every call.
79const calls = first.content.filter((block) => block.type === "tool_use");
80const results = await Promise.all(
81 calls.map(async (call): Promise<Anthropic.ToolResultBlockParam> => {
82 const { city } = call.input as { city: string };
83 console.error(`→ get_weather("${city}")`);
84 try {
85 const weather = await getWeather(city);
86 return { type: "tool_result", tool_use_id: call.id, content: JSON.stringify(weather) };
87 } catch (error) {
88 const message = error instanceof Error ? error.message : String(error);
89 return { type: "tool_result", tool_use_id: call.id, content: message, is_error: true };
90 }
91 }),
92);
93
94// Request two: Claude's reply as it came, then every result in one message.
95messages.push({ role: "assistant", content: first.content }, { role: "user", content: results });
96const second = await ask();
97printText(second);
98if (second.stop_reason === "tool_use") {
99 console.error("(Claude wants another call. Looping comes in Your first agent loop.)");
100}