Skip to main content

everruns_core/capabilities/
test_weather.rs

1//! TestWeather Capability - mock weather tools for testing tool calling
2
3use 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
11/// TestWeather capability - mock weather tools for testing tool calling
12pub 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
52// ============================================================================
53// Tool: get_weather
54// ============================================================================
55
56/// Tool that returns mock weather data for a location
57pub 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        // Generate deterministic mock weather based on location hash
111        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; // 5-40°C range
115        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; // 30-80%
130        let wind_speed = (hash % 30) + 5; // 5-35 km/h
131
132        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
144// ============================================================================
145// Tool: get_forecast
146// ============================================================================
147
148/// Tool that returns mock weather forecast for a location
149pub 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        // Generate deterministic mock forecast based on location hash
213        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    // Metadata/tool-list constants covered by builtin_capabilities_satisfy_registry_invariants.
263
264    #[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            // Fahrenheit temps should be higher than Celsius
297            let temp = value.get("temperature").unwrap().as_f64().unwrap();
298            assert!(temp > 30.0); // At least 30°F
299        } 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            // Check first day has expected fields
317            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); // Default is 3
334            let forecast = value.get("forecast").unwrap().as_array().unwrap();
335            assert_eq!(forecast.len(), 3);
336        } else {
337            panic!("Expected success");
338        }
339    }
340}