rig_core/tool/
portable.rs1use std::sync::Arc;
8
9use serde::{Deserialize, Serialize};
10
11use crate::{
12 completion::ToolDefinition,
13 wasm_compat::{WasmBoxedFuture, WasmCompatSend, WasmCompatSync},
14};
15
16use super::{IntoToolOutput, ToolExecutionError, ToolOutput};
17
18pub trait PortableTool: Sized + WasmCompatSend + WasmCompatSync {
20 const NAME: &'static str;
22 type Args: for<'de> Deserialize<'de> + WasmCompatSend + WasmCompatSync;
24 type Output: IntoToolOutput + WasmCompatSend;
26 type Error: std::error::Error + WasmCompatSend + WasmCompatSync + 'static;
28
29 fn description(&self) -> String;
31
32 fn parameters(&self) -> serde_json::Value;
34
35 fn map_error(&self, error: Self::Error) -> ToolExecutionError {
37 ToolExecutionError::from_error(error)
38 }
39
40 fn call(
42 &self,
43 arguments: Self::Args,
44 ) -> impl Future<Output = Result<Self::Output, Self::Error>> + WasmCompatSend;
45}
46
47pub trait PortableToolEmbedding: PortableTool {
49 type InitError: std::error::Error + WasmCompatSend + WasmCompatSync + 'static;
51 type Context: for<'de> Deserialize<'de> + Serialize;
53 type State: WasmCompatSend;
55
56 fn embedding_docs(&self) -> Vec<String>;
58 fn context(&self) -> Self::Context;
60 fn init(state: Self::State, context: Self::Context) -> Result<Self, Self::InitError>;
62}
63
64trait PortableDynamicCallback:
65 Fn(serde_json::Value) -> WasmBoxedFuture<'static, Result<ToolOutput, ToolExecutionError>>
66 + WasmCompatSend
67 + WasmCompatSync
68{
69}
70
71impl<F> PortableDynamicCallback for F where
72 F: Fn(serde_json::Value) -> WasmBoxedFuture<'static, Result<ToolOutput, ToolExecutionError>>
73 + WasmCompatSend
74 + WasmCompatSync
75{
76}
77
78#[derive(Clone)]
80pub struct PortableDynamicTool {
81 name: String,
82 description: String,
83 parameters: serde_json::Value,
84 callback: Arc<dyn PortableDynamicCallback>,
85}
86
87impl std::fmt::Debug for PortableDynamicTool {
88 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 formatter
90 .debug_struct("PortableDynamicTool")
91 .field("name", &self.name)
92 .field("description", &self.description)
93 .field("parameters", &self.parameters)
94 .finish_non_exhaustive()
95 }
96}
97
98impl PortableDynamicTool {
99 pub fn new<F>(
101 name: impl Into<String>,
102 description: impl Into<String>,
103 parameters: serde_json::Value,
104 callback: F,
105 ) -> Self
106 where
107 F: Fn(
108 serde_json::Value,
109 ) -> WasmBoxedFuture<'static, Result<ToolOutput, ToolExecutionError>>
110 + WasmCompatSend
111 + WasmCompatSync
112 + 'static,
113 {
114 Self {
115 name: name.into(),
116 description: description.into(),
117 parameters,
118 callback: Arc::new(callback),
119 }
120 }
121
122 pub fn name(&self) -> &str {
124 &self.name
125 }
126
127 pub fn definition(&self) -> ToolDefinition {
129 ToolDefinition {
130 name: self.name.clone(),
131 description: self.description.clone(),
132 parameters: self.parameters.clone(),
133 }
134 }
135
136 pub async fn execute(
138 &self,
139 arguments: serde_json::Value,
140 ) -> Result<ToolOutput, ToolExecutionError> {
141 (self.callback)(arguments).await
142 }
143}
144
145pub fn portable_tool_definition<T>(tool: &T) -> ToolDefinition
147where
148 T: PortableTool,
149{
150 ToolDefinition {
151 name: T::NAME.to_owned(),
152 description: tool.description(),
153 parameters: tool.parameters(),
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use std::convert::Infallible;
160
161 use serde::{Deserialize, Serialize};
162
163 use super::*;
164
165 #[derive(Deserialize)]
166 struct AddArgs {
167 left: i64,
168 right: i64,
169 }
170
171 #[derive(Serialize)]
172 struct Sum {
173 value: i64,
174 }
175
176 struct Add;
177
178 impl PortableTool for Add {
179 const NAME: &'static str = "add";
180 type Args = AddArgs;
181 type Output = Sum;
182 type Error = Infallible;
183
184 fn description(&self) -> String {
185 "Add two integers".to_string()
186 }
187
188 fn parameters(&self) -> serde_json::Value {
189 serde_json::json!({"type": "object"})
190 }
191
192 async fn call(&self, arguments: Self::Args) -> Result<Self::Output, Self::Error> {
193 Ok(Sum {
194 value: arguments.left + arguments.right,
195 })
196 }
197 }
198
199 #[tokio::test]
200 async fn portable_tools_execute_without_runtime_context() {
201 let output = Add.call(AddArgs { left: 2, right: 3 }).await;
202 let Ok(output) = output;
203 assert_eq!(output.value, 5);
204 assert_eq!(portable_tool_definition(&Add).name, "add");
205 }
206
207 #[tokio::test]
208 async fn portable_dynamic_tools_receive_owned_arguments() {
209 let tool = PortableDynamicTool::new(
210 "echo",
211 "Echo a JSON value",
212 serde_json::json!({"type": "object"}),
213 |arguments| Box::pin(async move { Ok(ToolOutput::json(arguments)) }),
214 );
215
216 let arguments = serde_json::json!({"value": "hello"});
217 let output = tool.execute(arguments.clone()).await;
218 assert!(output.is_ok());
219 let Ok(output) = output else {
220 return;
221 };
222 assert_eq!(output.as_json(), Some(&arguments));
223 }
224}