everruns_core/capabilities/
test_weather.rs1use super::{Capability, CapabilityStatus};
4use crate::tool_types::ToolHints;
5use crate::tools::{Tool, ToolExecutionResult};
6use async_trait::async_trait;
7use serde_json::Value;
8
9pub 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
42pub 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 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; 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; let wind_speed = (hash % 30) + 5; 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
134pub 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 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 let temp = value.get("temperature").unwrap().as_f64().unwrap();
318 assert!(temp > 30.0); } 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 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); let forecast = value.get("forecast").unwrap().as_array().unwrap();
355 assert_eq!(forecast.len(), 3);
356 } else {
357 panic!("Expected success");
358 }
359 }
360}