1#![allow(clippy::too_many_arguments)]
10
11use std::collections::HashMap;
12use std::sync::Arc;
13use tokio::sync::RwLock;
14
15use crate::error::{Error, Result};
16use crate::parsers::{Deserializable, ParsedTool, ToolConstructor};
17use crate::schema::ToolParametersSchema;
18use crate::types::ReasoningSegment;
19use crate::utils::ConversationMessage;
20use futures::future::BoxFuture;
21
22pub mod decorators;
23pub mod inference;
24
25#[derive(Debug, Default, Clone)]
27pub struct Accumulators {
28 raw_response: Vec<String>,
29 reasoning: Vec<String>,
30 reasoning_segments: Vec<ReasoningSegment>,
31 current_reasoning_segment: Option<ReasoningSegment>,
32}
33
34#[derive(Debug, Clone)]
36pub struct ToolSchemaInfo {
37 pub name: String,
38 pub description: String,
39 pub fields: Vec<crate::parsers::FieldDescription>,
40 pub parameters: ToolParametersSchema,
41}
42
43pub struct Runtime {
45 pub model: Option<String>,
47 pub api_key: Option<String>,
48 pub used: bool,
49
50 tools: Arc<RwLock<HashMap<String, ToolFunction>>>,
52 tool_types: Arc<RwLock<HashMap<String, String>>>, tool_schemas: Arc<RwLock<HashMap<String, ToolSchemaInfo>>>, tool_constructors: Arc<RwLock<HashMap<String, Arc<ToolConstructor>>>>, default_prompt: Arc<RwLock<String>>,
56 return_type: Arc<RwLock<Option<String>>>, accumulators: Arc<RwLock<Accumulators>>,
58 #[allow(dead_code)]
59 messages: Arc<RwLock<Vec<ConversationMessage>>>,
60 current_call_args: Arc<RwLock<Option<HashMap<String, serde_json::Value>>>>,
61}
62
63pub enum ToolFunction {
69 Sync(Box<dyn Fn(serde_json::Value) -> Result<String> + Send + Sync>),
70 Async(
71 Box<
72 dyn Fn(serde_json::Value) -> futures::future::BoxFuture<'static, Result<String>>
73 + Send
74 + Sync,
75 >,
76 ),
77}
78
79#[derive(Debug, Default, Clone)]
81pub struct RunParams {
82 pub prompt: Option<String>,
83 pub system: Option<String>,
84 pub history: Option<Vec<ConversationMessage>>,
85 pub output_type: Option<String>,
86 pub output_schema: Option<serde_json::Value>,
87 pub temperature: Option<f32>,
88 pub top_p: Option<f32>,
89 pub max_tokens: Option<u32>,
90 pub model: Option<String>,
91 pub api_key: Option<String>,
92 pub timeout: Option<std::time::Duration>,
93}
94
95#[derive(Debug, Clone)]
97pub struct ToolCallContext {
98 pub tool_name: String,
99 pub tool_use_id: String,
100}
101
102impl Runtime {
103 pub fn new() -> Self {
105 Self {
106 model: None,
107 api_key: None,
108 used: false,
109 tools: Arc::new(RwLock::new(HashMap::new())),
110 tool_types: Arc::new(RwLock::new(HashMap::new())),
111 tool_schemas: Arc::new(RwLock::new(HashMap::new())),
112 tool_constructors: Arc::new(RwLock::new(HashMap::new())),
113 default_prompt: Arc::new(RwLock::new(String::new())),
114 return_type: Arc::new(RwLock::new(None)),
115 accumulators: Arc::new(RwLock::new(Accumulators::default())),
116 messages: Arc::new(RwLock::new(Vec::new())),
117 current_call_args: Arc::new(RwLock::new(None)),
118 }
119 }
120
121 pub fn with_config(model: Option<String>, api_key: Option<String>) -> Self {
123 Self {
124 model,
125 api_key,
126 used: false,
127 tools: Arc::new(RwLock::new(HashMap::new())),
128 tool_types: Arc::new(RwLock::new(HashMap::new())),
129 tool_schemas: Arc::new(RwLock::new(HashMap::new())),
130 tool_constructors: Arc::new(RwLock::new(HashMap::new())),
131 default_prompt: Arc::new(RwLock::new(String::new())),
132 return_type: Arc::new(RwLock::new(None)),
133 accumulators: Arc::new(RwLock::new(Accumulators::default())),
134 messages: Arc::new(RwLock::new(Vec::new())),
135 current_call_args: Arc::new(RwLock::new(None)),
136 }
137 }
138
139 pub async fn register_tool(
146 &self,
147 name: impl Into<String>,
148 tool_fn: ToolFunction,
149 tool_type: Option<String>,
150 ) -> Result<()> {
151 let name = name.into();
152
153 let mut tools = self.tools.write().await;
155 if tools.contains_key(&name) {
156 return Err(Error::NonRetryable(format!(
157 "Tool '{}' is already registered",
158 name
159 )));
160 }
161 tools.insert(name.clone(), tool_fn);
162 drop(tools);
163
164 if let Some(type_name) = tool_type {
166 let mut tool_types = self.tool_types.write().await;
167 tool_types.insert(name, type_name);
168 }
169
170 Ok(())
171 }
172
173 pub async fn register_tool_with_schema(
197 &self,
198 name: impl Into<String>,
199 description: impl Into<String>,
200 schema: serde_json::Value,
201 tool_fn: ToolFunction,
202 ) -> Result<()> {
203 let name = name.into();
204 let description = description.into();
205
206 let mut tools = self.tools.write().await;
208 if tools.contains_key(&name) {
209 return Err(Error::NonRetryable(format!(
210 "Tool '{}' is already registered",
211 name
212 )));
213 }
214 tools.insert(name.clone(), tool_fn);
215 drop(tools);
216
217 let parameters = ToolParametersSchema::from_json_schema(&schema)?;
220 let fields = parameters.top_level_field_descriptions();
221 let schema_info = ToolSchemaInfo {
222 name: name.clone(),
223 description,
224 fields,
225 parameters,
226 };
227 let mut schemas = self.tool_schemas.write().await;
228 schemas.insert(name, schema_info);
229
230 Ok(())
231 }
232
233 pub async fn tool<T, F>(&self, handler: F, name: Option<&str>) -> Result<()>
258 where
259 T: Deserializable + serde::Serialize + 'static,
260 F: Fn(T) -> BoxFuture<'static, Result<String>> + Send + Sync + 'static,
261 {
262 let type_name = std::any::type_name::<T>();
264 let tool_name_str =
265 name.unwrap_or_else(|| type_name.split("::").last().unwrap_or(type_name));
266 let tool_name = tool_name_str.to_string();
267
268 let mut tools = self.tools.write().await;
270 if tools.contains_key(&tool_name) {
271 return Err(Error::NonRetryable(format!(
272 "Tool '{}' is already registered",
273 tool_name
274 )));
275 }
276
277 let handler = Arc::new(handler);
279 let wrapped_handler = Box::new(move |json_value: serde_json::Value| {
280 let handler = handler.clone();
281 Box::pin(async move {
282 let typed_args: T = T::from_partial(json_value)?;
284 handler(typed_args).await
286 }) as BoxFuture<'static, Result<String>>
287 });
288
289 tools.insert(tool_name.clone(), ToolFunction::Async(wrapped_handler));
290 drop(tools);
291
292 let mut tool_types = self.tool_types.write().await;
294 tool_types.insert(tool_name.clone(), type_name.to_string());
295 drop(tool_types);
296
297 let field_descriptions = T::field_descriptions();
299 let parameters = ToolParametersSchema::from_field_descriptions(&field_descriptions);
300 let schema_info = ToolSchemaInfo {
301 name: tool_name.clone(),
302 description: format!("Tool: {}", tool_name), fields: field_descriptions,
304 parameters,
305 };
306 let mut schemas = self.tool_schemas.write().await;
307 schemas.insert(tool_name.clone(), schema_info);
308 drop(schemas);
309
310 let tool_name_clone = tool_name.clone();
312 let constructor: ToolConstructor = Box::new(move |json: serde_json::Value| {
313 T::from_partial(json.clone()).map(|tool| {
314 ParsedTool {
315 tool_name: tool_name_clone.clone(),
316 tool_use_id: String::new(), value: serde_json::to_value(&tool).unwrap(),
318 }
319 })
320 });
321
322 let mut constructors = self.tool_constructors.write().await;
323 constructors.insert(tool_name.clone(), Arc::new(constructor));
324
325 Ok(())
326 }
327
328 pub async fn get_parser(&self) -> crate::parsers::NativeToolParser {
333 let constructors = self.tool_constructors.read().await;
334 crate::parsers::NativeToolParser::new(constructors.clone())
335 }
336
337 pub async fn get_tool_schemas(&self) -> HashMap<String, ToolSchemaInfo> {
339 let schemas = self.tool_schemas.read().await;
340 schemas.clone()
341 }
342
343 pub async fn run(&mut self, params: RunParams) -> Result<serde_json::Value> {
345 self.used = true;
347
348 self.clear_raw_response().await;
350 self.clear_reasoning().await;
351
352 let prompt_text = match params.prompt {
354 Some(p) => p,
355 None => self.default_prompt.read().await.clone(),
356 };
357
358 let effective_model = params
359 .model
360 .or_else(|| self.model.clone())
361 .ok_or_else(|| Error::NonRetryable("No model specified".to_string()))?;
362
363 let effective_api_key = params.api_key.or_else(|| self.api_key.clone());
364
365 let result = inference::call_llm(
367 Some(&prompt_text),
368 &effective_model,
369 self.tools.clone(),
370 self.tool_schemas.clone(),
371 params.output_type,
372 params.output_schema,
373 effective_api_key.as_deref(),
374 params.system.as_deref(),
375 params.history,
376 params.temperature,
377 params.top_p,
378 params.max_tokens,
379 params.timeout,
380 self.current_call_args.clone(),
381 )
382 .await?;
383
384 if let Some(raw) = &result.raw_response {
386 let mut acc = self.accumulators.write().await;
387 acc.raw_response.push(raw.clone());
388 }
389
390 if let Some(reasoning) = &result.reasoning {
391 let mut acc = self.accumulators.write().await;
392 acc.reasoning.push(reasoning.clone());
393 }
394
395 Ok(result.parsed_value)
396 }
397
398 pub async fn run_stream(
402 &mut self,
403 params: RunParams,
404 ) -> Result<impl futures::stream::Stream<Item = Result<(String, serde_json::Value)>>> {
405 self.used = true;
407
408 self.clear_raw_response().await;
410 self.clear_reasoning().await;
411 self.clear_reasoning_segments().await;
412
413 let prompt_text = match params.prompt {
415 Some(p) => p,
416 None => self.default_prompt.read().await.clone(),
417 };
418
419 let effective_model = params
420 .model
421 .or_else(|| self.model.clone())
422 .ok_or_else(|| Error::NonRetryable("No model specified".to_string()))?;
423
424 let effective_api_key = params.api_key.or_else(|| self.api_key.clone());
425
426 inference::call_llm_stream(
428 Some(&prompt_text),
429 &effective_model,
430 self.tools.clone(),
431 self.tool_schemas.clone(),
432 params.output_type,
433 params.output_schema,
434 effective_api_key.as_deref(),
435 params.system.as_deref(),
436 params.history,
437 params.temperature,
438 params.top_p,
439 params.max_tokens,
440 params.timeout,
441 self.current_call_args.clone(),
442 self.accumulators.clone(),
443 )
444 .await
445 }
446
447 pub fn is_tool_call(&self, result: &serde_json::Value) -> bool {
449 result.get("_tool_name").and_then(|v| v.as_str()).is_some()
451 }
452
453 pub fn get_tool_name(&self, result: &serde_json::Value) -> Option<String> {
455 result
456 .get("_tool_name")
457 .and_then(|v| v.as_str())
458 .map(|s| s.to_string())
459 }
460
461 pub async fn execute_tool(&self, tool_result: &serde_json::Value) -> Result<String> {
463 let tool_name = self
464 .get_tool_name(tool_result)
465 .ok_or_else(|| Error::NonRetryable("No tool name in result".to_string()))?;
466
467 let tools = self.tools.read().await;
468 let tool_fn = tools
469 .get(&tool_name)
470 .ok_or_else(|| Error::NonRetryable(format!("Tool '{}' not found", tool_name)))?;
471
472 let args = tool_result.clone();
474
475 match tool_fn {
477 ToolFunction::Sync(f) => f(args),
478 ToolFunction::Async(f) => f(args).await,
479 }
480 }
481
482 pub async fn raw_response(&self) -> String {
484 let acc = self.accumulators.read().await;
485 acc.raw_response.join("")
486 }
487
488 pub async fn reasoning(&self) -> String {
490 let acc = self.accumulators.read().await;
491 acc.reasoning.join("")
492 }
493
494 pub async fn reasoning_segments(&self) -> Vec<ReasoningSegment> {
496 let acc = self.accumulators.read().await;
497 acc.reasoning_segments.clone()
498 }
499
500 pub async fn clear_raw_response(&self) {
502 let mut acc = self.accumulators.write().await;
503 acc.raw_response.clear();
504 }
505
506 pub async fn clear_reasoning(&self) {
508 let mut acc = self.accumulators.write().await;
509 acc.reasoning.clear();
510 }
511
512 pub async fn clear_reasoning_segments(&self) {
514 let mut acc = self.accumulators.write().await;
515 acc.reasoning_segments.clear();
516 acc.current_reasoning_segment = None;
517 }
518
519 pub async fn set_default_prompt(&self, prompt: impl Into<String>) {
521 let mut default_prompt = self.default_prompt.write().await;
522 *default_prompt = prompt.into();
523 }
524
525 pub async fn set_return_type(&self, type_name: Option<String>) {
527 let mut return_type = self.return_type.write().await;
528 *return_type = type_name;
529 }
530
531 pub async fn set_current_call_args(&self, args: Option<HashMap<String, serde_json::Value>>) {
533 let mut current_args = self.current_call_args.write().await;
534 *current_args = args;
535 }
536
537 #[cfg(feature = "mcp")]
557 pub async fn mcp(&self, uri: impl Into<String>) -> Result<()> {
558 let _ = crate::mcp::connect_and_register(self, &uri.into(), None).await?;
559 Ok(())
560 }
561
562 #[cfg(feature = "mcp")]
580 pub async fn mcp_as(&self, uri: impl Into<String>, label: &str) -> Result<()> {
581 let _ = crate::mcp::connect_and_register(self, &uri.into(), Some(label)).await?;
582 Ok(())
583 }
584}
585
586impl Default for Runtime {
587 fn default() -> Self {
588 Self::new()
589 }
590}
591
592#[cfg(test)]
593mod tests {
594 use super::*;
595
596 #[tokio::test]
597 async fn test_runtime_new() {
598 let runtime = Runtime::new();
599 assert!(!runtime.used);
600 assert!(runtime.model.is_none());
601 }
602
603 #[tokio::test]
604 async fn test_runtime_with_config() {
605 let runtime =
606 Runtime::with_config(Some("test-model".to_string()), Some("test-key".to_string()));
607
608 assert_eq!(runtime.model, Some("test-model".to_string()));
609 assert_eq!(runtime.api_key, Some("test-key".to_string()));
610 }
611
612 #[tokio::test]
613 async fn test_register_tool() {
614 let runtime = Runtime::new();
615
616 let tool_fn = ToolFunction::Sync(Box::new(|_args| Ok("result".to_string())));
617
618 runtime
619 .register_tool("test_tool", tool_fn, None)
620 .await
621 .unwrap();
622
623 let tools = runtime.tools.read().await;
624 assert!(tools.contains_key("test_tool"));
625 }
626
627 #[tokio::test]
628 async fn test_register_duplicate_tool() {
629 let runtime = Runtime::new();
630
631 let tool_fn1 = ToolFunction::Sync(Box::new(|_args| Ok("result1".to_string())));
632 let tool_fn2 = ToolFunction::Sync(Box::new(|_args| Ok("result2".to_string())));
633
634 runtime
635 .register_tool("test_tool", tool_fn1, None)
636 .await
637 .unwrap();
638
639 let result = runtime.register_tool("test_tool", tool_fn2, None).await;
640 assert!(result.is_err());
641 }
642
643 #[tokio::test]
644 async fn test_is_tool_call() {
645 let runtime = Runtime::new();
646
647 let tool_call = serde_json::json!({
648 "_tool_name": "test_tool",
649 "arg1": "value1"
650 });
651
652 assert!(runtime.is_tool_call(&tool_call));
653
654 let not_tool_call = serde_json::json!({
655 "result": "value"
656 });
657
658 assert!(!runtime.is_tool_call(¬_tool_call));
659 }
660
661 #[tokio::test]
662 async fn test_get_tool_name() {
663 let runtime = Runtime::new();
664
665 let tool_call = serde_json::json!({
666 "_tool_name": "my_tool",
667 "arg1": "value1"
668 });
669
670 assert_eq!(
671 runtime.get_tool_name(&tool_call),
672 Some("my_tool".to_string())
673 );
674 }
675
676 #[tokio::test]
677 async fn test_execute_tool_sync() {
678 let runtime = Runtime::new();
679
680 let tool_fn = ToolFunction::Sync(Box::new(|args| {
681 let name = args.get("name").and_then(|v| v.as_str()).unwrap_or("world");
682 Ok(format!("Hello, {}!", name))
683 }));
684
685 runtime.register_tool("greet", tool_fn, None).await.unwrap();
686
687 let tool_call = serde_json::json!({
688 "_tool_name": "greet",
689 "name": "Alice"
690 });
691
692 let result = runtime.execute_tool(&tool_call).await.unwrap();
693 assert_eq!(result, "Hello, Alice!");
694 }
695
696 #[tokio::test]
697 async fn test_accumulators() {
698 let runtime = Runtime::new();
699
700 assert_eq!(runtime.raw_response().await, "");
702 assert_eq!(runtime.reasoning().await, "");
703
704 {
706 let mut acc = runtime.accumulators.write().await;
707 acc.raw_response.push("Hello ".to_string());
708 acc.raw_response.push("World".to_string());
709 acc.reasoning.push("Think: ".to_string());
710 acc.reasoning.push("Answer".to_string());
711 }
712
713 assert_eq!(runtime.raw_response().await, "Hello World");
714 assert_eq!(runtime.reasoning().await, "Think: Answer");
715
716 runtime.clear_raw_response().await;
718 runtime.clear_reasoning().await;
719
720 assert_eq!(runtime.raw_response().await, "");
721 assert_eq!(runtime.reasoning().await, "");
722 }
723
724 #[tokio::test]
725 #[ignore] async fn test_context() {
727 }
729
730 #[derive(Debug, serde::Serialize, serde::Deserialize)]
732 struct WeatherQuery {
733 #[serde(default)]
734 location: String,
735 #[serde(default)]
736 unit: Option<String>,
737 }
738
739 impl crate::parsers::Deserializable for WeatherQuery {
740 fn from_partial(partial: serde_json::Value) -> crate::error::Result<Self> {
741 serde_json::from_value(partial)
742 .map_err(|e| crate::error::Error::NonRetryable(format!("Parse error: {}", e)))
743 }
744
745 fn validate_complete(&self) -> crate::error::Result<()> {
746 if self.location.is_empty() {
747 return Err(crate::error::Error::NonRetryable(
748 "location is required".to_string(),
749 ));
750 }
751 Ok(())
752 }
753
754 fn field_descriptions() -> Vec<crate::parsers::FieldDescription> {
755 vec![
756 crate::parsers::FieldDescription {
757 name: "location".to_string(),
758 field_type: "string".to_string(),
759 description: "The city to get weather for".to_string(),
760 required: true,
761 },
762 crate::parsers::FieldDescription {
763 name: "unit".to_string(),
764 field_type: "string".to_string(),
765 description: "Temperature unit (celsius or fahrenheit)".to_string(),
766 required: false,
767 },
768 ]
769 }
770 }
771
772 #[tokio::test]
773 async fn test_tool_registration_with_schema() {
774 use futures::future::BoxFuture;
775
776 let runtime = Runtime::new();
777
778 runtime
780 .tool::<WeatherQuery, _>(
781 |query| -> BoxFuture<'static, crate::error::Result<String>> {
782 Box::pin(async move { Ok(format!("Weather in {}: Sunny", query.location)) })
783 },
784 Some("get_weather"),
785 )
786 .await
787 .unwrap();
788
789 let tools = runtime.tools.read().await;
791 assert!(tools.contains_key("get_weather"));
792 drop(tools);
793
794 let schemas = runtime.tool_schemas.read().await;
796 let schema_info = schemas.get("get_weather").unwrap();
797 assert_eq!(schema_info.name, "get_weather");
798 assert_eq!(schema_info.fields.len(), 2);
799 assert_eq!(schema_info.fields[0].name, "location");
800 assert!(schema_info.fields[0].required);
801 assert_eq!(schema_info.fields[1].name, "unit");
802 assert!(!schema_info.fields[1].required);
803 }
804
805 #[tokio::test]
806 async fn test_tool_schema_generation() {
807 use futures::future::BoxFuture;
808
809 let runtime = Runtime::new();
810
811 runtime
813 .tool::<WeatherQuery, _>(
814 |_query| -> BoxFuture<'static, crate::error::Result<String>> {
815 Box::pin(async move { Ok("Sunny".to_string()) })
816 },
817 Some("get_weather"),
818 )
819 .await
820 .unwrap();
821
822 let schemas = runtime.get_tool_schemas().await;
824 assert_eq!(schemas.len(), 1);
825
826 let weather_schema = schemas.get("get_weather").unwrap();
827 assert!(weather_schema
828 .fields
829 .iter()
830 .any(|f| f.name == "location" && f.required));
831 assert!(weather_schema
832 .fields
833 .iter()
834 .any(|f| f.name == "unit" && !f.required));
835 assert_eq!(
836 weather_schema.parameters.to_json_schema()["properties"]["location"]["type"],
837 "string"
838 );
839 }
840
841 #[tokio::test]
842 async fn test_register_tool_with_schema_preserves_nested_structure() {
843 let runtime = Runtime::new();
844
845 runtime
846 .register_tool_with_schema(
847 "write_thread",
848 "Write a thread",
849 serde_json::json!({
850 "type": "object",
851 "properties": {
852 "items": {
853 "type": "array",
854 "items": {
855 "$ref": "#/$defs/ThreadItem"
856 }
857 }
858 },
859 "required": ["items"],
860 "$defs": {
861 "ThreadItem": {
862 "type": "object",
863 "properties": {
864 "text": { "type": "string" },
865 "image_ids": {
866 "type": "array",
867 "items": { "type": "integer" }
868 }
869 },
870 "required": ["text"]
871 }
872 }
873 }),
874 ToolFunction::Sync(Box::new(|args| Ok(args.to_string()))),
875 )
876 .await
877 .unwrap();
878
879 let schemas = runtime.get_tool_schemas().await;
880 let write_thread = schemas.get("write_thread").unwrap();
881 let json = write_thread.parameters.to_json_schema();
882 assert_eq!(
883 json["properties"]["items"]["items"]["properties"]["text"]["type"],
884 "string"
885 );
886 assert_eq!(
887 json["properties"]["items"]["items"]["properties"]["image_ids"]["items"]["type"],
888 "integer"
889 );
890 }
891
892 #[tokio::test]
893 async fn test_tool_hydration_and_execution() {
894 use futures::future::BoxFuture;
895
896 let runtime = Runtime::new();
897
898 runtime
900 .tool::<WeatherQuery, _>(
901 |query| -> BoxFuture<'static, crate::error::Result<String>> {
902 Box::pin(async move {
903 let unit = query.unit.unwrap_or_else(|| "celsius".to_string());
905 Ok(format!(
906 "Weather in {} ({}): Sunny, 22°",
907 query.location, unit
908 ))
909 })
910 },
911 Some("get_weather"),
912 )
913 .await
914 .unwrap();
915
916 let tool_call = serde_json::json!({
918 "_tool_name": "get_weather",
919 "location": "Paris",
920 "unit": "fahrenheit"
921 });
922
923 let result = runtime.execute_tool(&tool_call).await.unwrap();
925 assert_eq!(result, "Weather in Paris (fahrenheit): Sunny, 22°");
926 }
927
928 #[tokio::test]
929 async fn test_tool_hydration_with_defaults() {
930 use futures::future::BoxFuture;
931
932 let runtime = Runtime::new();
933
934 runtime
936 .tool::<WeatherQuery, _>(
937 |query| -> BoxFuture<'static, crate::error::Result<String>> {
938 Box::pin(async move {
939 let unit = query.unit.unwrap_or_else(|| "celsius".to_string());
940 Ok(format!("{} in {}", unit, query.location))
941 })
942 },
943 Some("get_weather"),
944 )
945 .await
946 .unwrap();
947
948 let tool_call = serde_json::json!({
950 "_tool_name": "get_weather",
951 "location": "Tokyo"
952 });
954
955 let result = runtime.execute_tool(&tool_call).await.unwrap();
956 assert_eq!(result, "celsius in Tokyo");
957 }
958}