Skip to main content

everruns_core/capabilities/
test_weather.rs

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