···
1
1
+
ANTHROPIC_API_KEY=""
2
2
+
3
3
+
MODEL=claude-3-5-sonnet-20240620
···
1
1
+
output.txt
2
2
+
node_modules
3
3
+
dist
4
4
+
.mastra
5
5
+
.env.development
6
6
+
.env
7
7
+
*.db
8
8
+
*.db-*
···
1
1
+
# Weather Agent Template
2
2
+
3
3
+
This is a template project that demonstrates how to create a weather agent using the Mastra framework. The agent can provide weather information and forecasts based on user queries.
4
4
+
5
5
+
## Overview
6
6
+
7
7
+
The Weather Agent template showcases how to:
8
8
+
9
9
+
- Create an AI-powered agent using Mastra framework
10
10
+
- Implement weather-related workflows
11
11
+
- Handle user queries about weather conditions
12
12
+
- Integrate with Anthropic's API for natural language processing
13
13
+
14
14
+
## Setup
15
15
+
16
16
+
1. Copy `.env.example` to `.env` and fill in your API keys.
17
17
+
2. Install dependencies: `pnpm install`
18
18
+
3. Run the project: `pnpm dev`.
19
19
+
20
20
+
## Environment Variables
21
21
+
22
22
+
- `ANTHROPIC_API_KEY`: Your Anthropic API key. [https://console.anthropic.com/settings/keys](https://console.anthropic.com/settings/keys)
···
1
1
+
{
2
2
+
"name": "weather-agent",
3
3
+
"version": "1.0.0",
4
4
+
"main": "index.js",
5
5
+
"private": true,
6
6
+
"scripts": {
7
7
+
"test": "echo \"Error: no test specified\" && exit 1",
8
8
+
"dev": "mastra dev"
9
9
+
},
10
10
+
"keywords": [],
11
11
+
"author": "",
12
12
+
"license": "ISC",
13
13
+
"description": "One Agent, one Workflow and one Tool to bring you the weather in your city.",
14
14
+
"type": "module",
15
15
+
"dependencies": {
16
16
+
"@mastra/core": "latest",
17
17
+
"@mastra/loggers": "latest",
18
18
+
"zod": "^3.25.67",
19
19
+
"@ai-sdk/anthropic": "^2.0.0"
20
20
+
},
21
21
+
"devDependencies": {
22
22
+
"@types/node": "^22.15.29",
23
23
+
"mastra": "latest",
24
24
+
"typescript": "^5.8.3"
25
25
+
}
26
26
+
}
···
1
1
+
import { anthropic } from '@ai-sdk/anthropic';
2
2
+
import { Agent } from '@mastra/core/agent';
3
3
+
import { weatherTool } from '../tools';
4
4
+
5
5
+
export const weatherAgent = new Agent({
6
6
+
name: 'Weather Agent',
7
7
+
instructions: `
8
8
+
You are a helpful weather assistant that provides accurate weather information.
9
9
+
10
10
+
Your primary function is to help users get weather details for specific locations. When responding:
11
11
+
- Always ask for a location if none is provided
12
12
+
- If the location name isn’t in English, please translate it
13
13
+
- If giving a location with multiple parts (e.g. "New York, NY"), use the most relevant part (e.g. "New York")
14
14
+
- Include relevant details like humidity, wind conditions, and precipitation
15
15
+
- Keep responses concise but informative
16
16
+
17
17
+
Use the weatherTool to fetch current weather data.
18
18
+
`,
19
19
+
model: anthropic(process.env.MODEL ?? "claude-3-5-sonnet-20240620"),
20
20
+
tools: { weatherTool },
21
21
+
});
···
1
1
+
import { Mastra } from '@mastra/core/mastra';
2
2
+
import { PinoLogger } from '@mastra/loggers';
3
3
+
import { weatherWorkflow } from './workflows';
4
4
+
import { weatherAgent } from './agents';
5
5
+
6
6
+
export const mastra = new Mastra({
7
7
+
workflows: { weatherWorkflow },
8
8
+
agents: { weatherAgent },
9
9
+
logger: new PinoLogger({
10
10
+
name: 'Mastra',
11
11
+
level: 'info',
12
12
+
}),
13
13
+
});
···
1
1
+
import { createTool } from '@mastra/core/tools';
2
2
+
import { z } from 'zod';
3
3
+
4
4
+
interface GeocodingResponse {
5
5
+
results: {
6
6
+
latitude: number;
7
7
+
longitude: number;
8
8
+
name: string;
9
9
+
}[];
10
10
+
}
11
11
+
interface WeatherResponse {
12
12
+
current: {
13
13
+
time: string;
14
14
+
temperature_2m: number;
15
15
+
apparent_temperature: number;
16
16
+
relative_humidity_2m: number;
17
17
+
wind_speed_10m: number;
18
18
+
wind_gusts_10m: number;
19
19
+
weather_code: number;
20
20
+
};
21
21
+
}
22
22
+
23
23
+
export const weatherTool = createTool({
24
24
+
id: 'get-weather',
25
25
+
description: 'Get current weather for a location',
26
26
+
inputSchema: z.object({
27
27
+
location: z.string().describe('City name'),
28
28
+
}),
29
29
+
outputSchema: z.object({
30
30
+
temperature: z.number(),
31
31
+
feelsLike: z.number(),
32
32
+
humidity: z.number(),
33
33
+
windSpeed: z.number(),
34
34
+
windGust: z.number(),
35
35
+
conditions: z.string(),
36
36
+
location: z.string(),
37
37
+
}),
38
38
+
execute: async ({ context }) => {
39
39
+
return await getWeather(context.location);
40
40
+
},
41
41
+
});
42
42
+
43
43
+
const getWeather = async (location: string) => {
44
44
+
const geocodingUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(location)}&count=1`;
45
45
+
const geocodingResponse = await fetch(geocodingUrl);
46
46
+
const geocodingData = (await geocodingResponse.json()) as GeocodingResponse;
47
47
+
48
48
+
if (!geocodingData.results?.[0]) {
49
49
+
throw new Error(`Location '${location}' not found`);
50
50
+
}
51
51
+
52
52
+
const { latitude, longitude, name } = geocodingData.results[0];
53
53
+
54
54
+
const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,wind_gusts_10m,weather_code`;
55
55
+
56
56
+
const response = await fetch(weatherUrl);
57
57
+
const data = (await response.json()) as WeatherResponse;
58
58
+
59
59
+
return {
60
60
+
temperature: data.current.temperature_2m,
61
61
+
feelsLike: data.current.apparent_temperature,
62
62
+
humidity: data.current.relative_humidity_2m,
63
63
+
windSpeed: data.current.wind_speed_10m,
64
64
+
windGust: data.current.wind_gusts_10m,
65
65
+
conditions: getWeatherCondition(data.current.weather_code),
66
66
+
location: name,
67
67
+
};
68
68
+
};
69
69
+
70
70
+
function getWeatherCondition(code: number): string {
71
71
+
const conditions: Record<number, string> = {
72
72
+
0: 'Clear sky',
73
73
+
1: 'Mainly clear',
74
74
+
2: 'Partly cloudy',
75
75
+
3: 'Overcast',
76
76
+
45: 'Foggy',
77
77
+
48: 'Depositing rime fog',
78
78
+
51: 'Light drizzle',
79
79
+
53: 'Moderate drizzle',
80
80
+
55: 'Dense drizzle',
81
81
+
56: 'Light freezing drizzle',
82
82
+
57: 'Dense freezing drizzle',
83
83
+
61: 'Slight rain',
84
84
+
63: 'Moderate rain',
85
85
+
65: 'Heavy rain',
86
86
+
66: 'Light freezing rain',
87
87
+
67: 'Heavy freezing rain',
88
88
+
71: 'Slight snow fall',
89
89
+
73: 'Moderate snow fall',
90
90
+
75: 'Heavy snow fall',
91
91
+
77: 'Snow grains',
92
92
+
80: 'Slight rain showers',
93
93
+
81: 'Moderate rain showers',
94
94
+
82: 'Violent rain showers',
95
95
+
85: 'Slight snow showers',
96
96
+
86: 'Heavy snow showers',
97
97
+
95: 'Thunderstorm',
98
98
+
96: 'Thunderstorm with slight hail',
99
99
+
99: 'Thunderstorm with heavy hail',
100
100
+
};
101
101
+
return conditions[code] || 'Unknown';
102
102
+
}
···
1
1
+
import { anthropic } from '@ai-sdk/anthropic';
2
2
+
import { Agent } from '@mastra/core/agent';
3
3
+
import { createStep, createWorkflow } from '@mastra/core/workflows';
4
4
+
import { z } from 'zod';
5
5
+
6
6
+
const llm = anthropic(process.env.MODEL ?? "claude-3-5-sonnet-20240620");
7
7
+
8
8
+
const agent = new Agent({
9
9
+
name: 'Weather Agent',
10
10
+
model: llm,
11
11
+
instructions: `
12
12
+
You are a local activities and travel expert who excels at weather-based planning. Analyze the weather data and provide practical activity recommendations.
13
13
+
14
14
+
For each day in the forecast, structure your response exactly as follows:
15
15
+
16
16
+
📅 [Day, Month Date, Year]
17
17
+
═══════════════════════════
18
18
+
19
19
+
🌡️ WEATHER SUMMARY
20
20
+
• Conditions: [brief description]
21
21
+
• Temperature: [X°C/Y°F to A°C/B°F]
22
22
+
• Precipitation: [X% chance]
23
23
+
24
24
+
🌅 MORNING ACTIVITIES
25
25
+
Outdoor:
26
26
+
• [Activity Name] - [Brief description including specific location/route]
27
27
+
Best timing: [specific time range]
28
28
+
Note: [relevant weather consideration]
29
29
+
30
30
+
🌞 AFTERNOON ACTIVITIES
31
31
+
Outdoor:
32
32
+
• [Activity Name] - [Brief description including specific location/route]
33
33
+
Best timing: [specific time range]
34
34
+
Note: [relevant weather consideration]
35
35
+
36
36
+
🏠 INDOOR ALTERNATIVES
37
37
+
• [Activity Name] - [Brief description including specific venue]
38
38
+
Ideal for: [weather condition that would trigger this alternative]
39
39
+
40
40
+
⚠️ SPECIAL CONSIDERATIONS
41
41
+
• [Any relevant weather warnings, UV index, wind conditions, etc.]
42
42
+
43
43
+
Guidelines:
44
44
+
- Suggest 2-3 time-specific outdoor activities per day
45
45
+
- Include 1-2 indoor backup options
46
46
+
- For precipitation >50%, lead with indoor activities
47
47
+
- All activities must be specific to the location
48
48
+
- Include specific venues, trails, or locations
49
49
+
- Consider activity intensity based on temperature
50
50
+
- Keep descriptions concise but informative
51
51
+
52
52
+
Maintain this exact formatting for consistency, using the emoji and section headers as shown.
53
53
+
`,
54
54
+
});
55
55
+
56
56
+
const forecastSchema = z.object({
57
57
+
date: z.string(),
58
58
+
maxTemp: z.number(),
59
59
+
minTemp: z.number(),
60
60
+
precipitationChance: z.number(),
61
61
+
condition: z.string(),
62
62
+
location: z.string(),
63
63
+
});
64
64
+
65
65
+
function getWeatherCondition(code: number): string {
66
66
+
const conditions: Record<number, string> = {
67
67
+
0: 'Clear sky',
68
68
+
1: 'Mainly clear',
69
69
+
2: 'Partly cloudy',
70
70
+
3: 'Overcast',
71
71
+
45: 'Foggy',
72
72
+
48: 'Depositing rime fog',
73
73
+
51: 'Light drizzle',
74
74
+
53: 'Moderate drizzle',
75
75
+
55: 'Dense drizzle',
76
76
+
61: 'Slight rain',
77
77
+
63: 'Moderate rain',
78
78
+
65: 'Heavy rain',
79
79
+
71: 'Slight snow fall',
80
80
+
73: 'Moderate snow fall',
81
81
+
75: 'Heavy snow fall',
82
82
+
95: 'Thunderstorm',
83
83
+
};
84
84
+
return conditions[code] || 'Unknown';
85
85
+
}
86
86
+
87
87
+
const fetchWeather = createStep({
88
88
+
id: 'fetch-weather',
89
89
+
description: 'Fetches weather forecast for a given city',
90
90
+
inputSchema: z.object({
91
91
+
city: z.string().describe('The city to get the weather for'),
92
92
+
}),
93
93
+
outputSchema: forecastSchema,
94
94
+
execute: async ({ inputData }) => {
95
95
+
if (!inputData) {
96
96
+
throw new Error('Input data not found');
97
97
+
}
98
98
+
99
99
+
const geocodingUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(inputData.city)}&count=1`;
100
100
+
const geocodingResponse = await fetch(geocodingUrl);
101
101
+
const geocodingData = (await geocodingResponse.json()) as {
102
102
+
results: { latitude: number; longitude: number; name: string }[];
103
103
+
};
104
104
+
105
105
+
if (!geocodingData.results?.[0]) {
106
106
+
throw new Error(`Location '${inputData.city}' not found`);
107
107
+
}
108
108
+
109
109
+
const { latitude, longitude, name } = geocodingData.results[0];
110
110
+
111
111
+
const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=precipitation,weathercode&timezone=auto,&hourly=precipitation_probability,temperature_2m`;
112
112
+
const response = await fetch(weatherUrl);
113
113
+
const data = (await response.json()) as {
114
114
+
current: {
115
115
+
time: string;
116
116
+
precipitation: number;
117
117
+
weathercode: number;
118
118
+
};
119
119
+
hourly: {
120
120
+
precipitation_probability: number[];
121
121
+
temperature_2m: number[];
122
122
+
};
123
123
+
};
124
124
+
125
125
+
const forecast = {
126
126
+
date: new Date().toISOString(),
127
127
+
maxTemp: Math.max(...data.hourly.temperature_2m),
128
128
+
minTemp: Math.min(...data.hourly.temperature_2m),
129
129
+
condition: getWeatherCondition(data.current.weathercode),
130
130
+
precipitationChance: data.hourly.precipitation_probability.reduce((acc, curr) => Math.max(acc, curr), 0),
131
131
+
location: inputData.city,
132
132
+
};
133
133
+
134
134
+
return forecast;
135
135
+
},
136
136
+
});
137
137
+
138
138
+
const planActivities = createStep({
139
139
+
id: 'plan-activities',
140
140
+
description: 'Suggests activities based on weather conditions',
141
141
+
inputSchema: forecastSchema,
142
142
+
outputSchema: z.object({
143
143
+
activities: z.string(),
144
144
+
}),
145
145
+
execute: async ({ inputData }) => {
146
146
+
const forecast = inputData;
147
147
+
148
148
+
if (!forecast) {
149
149
+
throw new Error('Forecast data not found');
150
150
+
}
151
151
+
152
152
+
const prompt = `Based on the following weather forecast for ${forecast.location}, suggest appropriate activities:
153
153
+
${JSON.stringify(forecast, null, 2)}
154
154
+
`;
155
155
+
156
156
+
const response = await agent.stream([
157
157
+
{
158
158
+
role: 'user',
159
159
+
content: prompt,
160
160
+
},
161
161
+
]);
162
162
+
163
163
+
let activitiesText = '';
164
164
+
165
165
+
for await (const chunk of response.textStream) {
166
166
+
process.stdout.write(chunk);
167
167
+
activitiesText += chunk;
168
168
+
}
169
169
+
170
170
+
return {
171
171
+
activities: activitiesText,
172
172
+
};
173
173
+
},
174
174
+
});
175
175
+
176
176
+
const weatherWorkflow = createWorkflow({
177
177
+
id: 'weather-workflow',
178
178
+
inputSchema: z.object({
179
179
+
city: z.string().describe('The city to get the weather for'),
180
180
+
}),
181
181
+
outputSchema: z.object({
182
182
+
activities: z.string(),
183
183
+
}),
184
184
+
})
185
185
+
.then(fetchWeather)
186
186
+
.then(planActivities);
187
187
+
188
188
+
weatherWorkflow.commit();
189
189
+
190
190
+
export { weatherWorkflow };
···
1
1
+
{
2
2
+
"compilerOptions": {
3
3
+
"target": "ES2022",
4
4
+
"module": "ES2022",
5
5
+
"moduleResolution": "bundler",
6
6
+
"esModuleInterop": true,
7
7
+
"forceConsistentCasingInFileNames": true,
8
8
+
"strict": true,
9
9
+
"skipLibCheck": true,
10
10
+
"outDir": "dist"
11
11
+
},
12
12
+
"include": ["src/**/*"],
13
13
+
"exclude": ["node_modules", "dist", ".mastra"]
14
14
+
}