everruns_core/capabilities/
test_weather.rs1use super::{Capability, CapabilityLocalization, CapabilityStatus};
4use crate::tool_types::ToolHints;
5use crate::tools::{Tool, ToolExecutionResult};
6use async_trait::async_trait;
7use serde_json::Value;
8
9pub const TEST_WEATHER_CAPABILITY_ID: &str = "test_weather";
10
11pub struct TestWeatherCapability;
13
14impl Capability for TestWeatherCapability {
15 fn id(&self) -> &str {
16 TEST_WEATHER_CAPABILITY_ID
17 }
18
19 fn name(&self) -> &str {
20 "Test Weather"
21 }
22
23 fn description(&self) -> &str {
24 "Testing capability: adds mock weather tools (get_weather, get_forecast) for tool calling tests."
25 }
26
27 fn localizations(&self) -> Vec<CapabilityLocalization> {
28 vec![CapabilityLocalization::text(
29 "uk",
30 "Тестова погода",
31 "Тестова можливість: додає імітаційні інструменти погоди (get_weather, get_forecast) для тестів виклику інструментів.",
32 )]
33 }
34
35 fn status(&self) -> CapabilityStatus {
36 CapabilityStatus::Available
37 }
38
39 fn icon(&self) -> Option<&str> {
40 Some("cloud-sun")
41 }
42
43 fn category(&self) -> Option<&str> {
44 Some("Testing")
45 }
46
47 fn tools(&self) -> Vec<Box<dyn Tool>> {
48 vec![Box::new(GetWeatherTool), Box::new(GetForecastTool)]
49 }
50}
51
52pub struct GetWeatherTool;
58
59#[async_trait]
60impl Tool for GetWeatherTool {
61 fn name(&self) -> &str {
62 "get_weather"
63 }
64
65 fn display_name(&self) -> Option<&str> {
66 Some("Get Weather")
67 }
68
69 fn description(&self) -> &str {
70 "Get the current weather for a location. Returns temperature, conditions, humidity, and wind speed."
71 }
72
73 fn parameters_schema(&self) -> Value {
74 serde_json::json!({
75 "type": "object",
76 "properties": {
77 "location": {
78 "type": "string",
79 "description": "The city or location name (e.g., 'New York', 'London', 'Tokyo')"
80 },
81 "units": {
82 "type": "string",
83 "enum": ["celsius", "fahrenheit"],
84 "description": "Temperature units. Defaults to 'celsius'."
85 }
86 },
87 "required": ["location"],
88 "additionalProperties": false
89 })
90 }
91
92 fn hints(&self) -> ToolHints {
93 ToolHints::default()
94 .with_readonly(true)
95 .with_idempotent(true)
96 .with_open_world(true)
97 }
98
99 async fn execute(&self, arguments: Value) -> ToolExecutionResult {
100 let location = arguments
101 .get("location")
102 .and_then(|v| v.as_str())
103 .unwrap_or("Unknown");
104
105 let units = arguments
106 .get("units")
107 .and_then(|v| v.as_str())
108 .unwrap_or("celsius");
109
110 let hash = location
112 .bytes()
113 .fold(0u32, |acc, b| acc.wrapping_add(b as u32));
114 let temp_c = ((hash % 35) as i32) + 5; let temp = if units == "fahrenheit" {
116 (temp_c as f64 * 9.0 / 5.0) + 32.0
117 } else {
118 temp_c as f64
119 };
120
121 let conditions = match hash % 5 {
122 0 => "sunny",
123 1 => "partly cloudy",
124 2 => "cloudy",
125 3 => "rainy",
126 _ => "windy",
127 };
128
129 let humidity = (hash % 50) + 30; let wind_speed = (hash % 30) + 5; ToolExecutionResult::success(serde_json::json!({
133 "location": location,
134 "temperature": temp,
135 "units": units,
136 "conditions": conditions,
137 "humidity": humidity,
138 "wind_speed_kmh": wind_speed,
139 "timestamp": chrono::Utc::now().to_rfc3339()
140 }))
141 }
142}
143
144pub struct GetForecastTool;
150
151#[async_trait]
152impl Tool for GetForecastTool {
153 fn name(&self) -> &str {
154 "get_forecast"
155 }
156
157 fn display_name(&self) -> Option<&str> {
158 Some("Get Forecast")
159 }
160
161 fn description(&self) -> &str {
162 "Get the weather forecast for a location for the next several days."
163 }
164
165 fn parameters_schema(&self) -> Value {
166 serde_json::json!({
167 "type": "object",
168 "properties": {
169 "location": {
170 "type": "string",
171 "description": "The city or location name (e.g., 'New York', 'London', 'Tokyo')"
172 },
173 "days": {
174 "type": "integer",
175 "description": "Number of days to forecast (1-7). Defaults to 3."
176 },
177 "units": {
178 "type": "string",
179 "enum": ["celsius", "fahrenheit"],
180 "description": "Temperature units. Defaults to 'celsius'."
181 }
182 },
183 "required": ["location"],
184 "additionalProperties": false
185 })
186 }
187
188 fn hints(&self) -> ToolHints {
189 ToolHints::default()
190 .with_readonly(true)
191 .with_idempotent(true)
192 .with_open_world(true)
193 }
194
195 async fn execute(&self, arguments: Value) -> ToolExecutionResult {
196 let location = arguments
197 .get("location")
198 .and_then(|v| v.as_str())
199 .unwrap_or("Unknown");
200
201 let days = arguments
202 .get("days")
203 .and_then(|v| v.as_u64())
204 .unwrap_or(3)
205 .min(7) as usize;
206
207 let units = arguments
208 .get("units")
209 .and_then(|v| v.as_str())
210 .unwrap_or("celsius");
211
212 let hash = location
214 .bytes()
215 .fold(0u32, |acc, b| acc.wrapping_add(b as u32));
216
217 let today = chrono::Utc::now().date_naive();
218 let mut forecast_days = Vec::new();
219
220 for day_offset in 0..days {
221 let day_hash = hash.wrapping_add(day_offset as u32 * 7);
222 let temp_c = ((day_hash % 35) as i32) + 5;
223 let temp_high = if units == "fahrenheit" {
224 (temp_c as f64 * 9.0 / 5.0) + 32.0
225 } else {
226 temp_c as f64
227 };
228 let temp_low = temp_high - 8.0 - ((day_hash % 5) as f64);
229
230 let conditions = match day_hash % 5 {
231 0 => "sunny",
232 1 => "partly cloudy",
233 2 => "cloudy",
234 3 => "rainy",
235 _ => "windy",
236 };
237
238 let date = today + chrono::Duration::days(day_offset as i64);
239
240 forecast_days.push(serde_json::json!({
241 "date": date.to_string(),
242 "high": temp_high,
243 "low": temp_low,
244 "conditions": conditions,
245 "precipitation_chance": (day_hash % 100) as i32
246 }));
247 }
248
249 ToolExecutionResult::success(serde_json::json!({
250 "location": location,
251 "units": units,
252 "days": days,
253 "forecast": forecast_days
254 }))
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
265 fn test_capability_no_system_prompt() {
266 let cap = TestWeatherCapability;
267 assert!(cap.system_prompt_addition().is_none());
268 }
269
270 #[tokio::test]
271 async fn test_get_weather_tool() {
272 let tool = GetWeatherTool;
273 let result = tool
274 .execute(serde_json::json!({"location": "New York"}))
275 .await;
276
277 if let ToolExecutionResult::Success(value) = result {
278 assert_eq!(value.get("location").unwrap().as_str().unwrap(), "New York");
279 assert!(value.get("temperature").is_some());
280 assert!(value.get("conditions").is_some());
281 assert!(value.get("humidity").is_some());
282 } else {
283 panic!("Expected success");
284 }
285 }
286
287 #[tokio::test]
288 async fn test_get_weather_fahrenheit() {
289 let tool = GetWeatherTool;
290 let result = tool
291 .execute(serde_json::json!({"location": "London", "units": "fahrenheit"}))
292 .await;
293
294 if let ToolExecutionResult::Success(value) = result {
295 assert_eq!(value.get("units").unwrap().as_str().unwrap(), "fahrenheit");
296 let temp = value.get("temperature").unwrap().as_f64().unwrap();
298 assert!(temp > 30.0); } else {
300 panic!("Expected success");
301 }
302 }
303
304 #[tokio::test]
305 async fn test_get_forecast_tool() {
306 let tool = GetForecastTool;
307 let result = tool
308 .execute(serde_json::json!({"location": "Tokyo", "days": 5}))
309 .await;
310
311 if let ToolExecutionResult::Success(value) = result {
312 assert_eq!(value.get("location").unwrap().as_str().unwrap(), "Tokyo");
313 assert_eq!(value.get("days").unwrap().as_u64().unwrap(), 5);
314 let forecast = value.get("forecast").unwrap().as_array().unwrap();
315 assert_eq!(forecast.len(), 5);
316 let first_day = &forecast[0];
318 assert!(first_day.get("date").is_some());
319 assert!(first_day.get("high").is_some());
320 assert!(first_day.get("low").is_some());
321 assert!(first_day.get("conditions").is_some());
322 } else {
323 panic!("Expected success");
324 }
325 }
326
327 #[tokio::test]
328 async fn test_get_forecast_default_days() {
329 let tool = GetForecastTool;
330 let result = tool.execute(serde_json::json!({"location": "Paris"})).await;
331
332 if let ToolExecutionResult::Success(value) = result {
333 assert_eq!(value.get("days").unwrap().as_u64().unwrap(), 3); let forecast = value.get("forecast").unwrap().as_array().unwrap();
335 assert_eq!(forecast.len(), 3);
336 } else {
337 panic!("Expected success");
338 }
339 }
340}