nika_engine/runtime/
submit_tool.rs1use std::future::Future;
40use std::pin::Pin;
41
42use rig::completion::ToolDefinition;
43use rig::tool::{ToolDyn, ToolError};
44use serde_json::Value;
45
46type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
48
49#[derive(Debug, Clone)]
59pub struct DynamicSubmitTool {
60 name: String,
62 schema: Value,
64}
65
66impl DynamicSubmitTool {
67 pub fn new(schema: Value) -> Self {
69 Self {
70 name: "submit_result".to_string(),
71 schema,
72 }
73 }
74
75 pub fn with_name(name: impl Into<String>, schema: Value) -> Self {
77 Self {
78 name: name.into(),
79 schema,
80 }
81 }
82
83 pub fn schema(&self) -> &Value {
85 &self.schema
86 }
87}
88
89impl ToolDyn for DynamicSubmitTool {
90 fn name(&self) -> String {
91 self.name.clone()
92 }
93
94 fn definition(&self, _prompt: String) -> BoxFuture<'_, ToolDefinition> {
95 let def = ToolDefinition {
96 name: self.name.clone(),
97 description: "Submit the structured result. You MUST call this tool with your \
98 response formatted as JSON matching the provided schema."
99 .to_string(),
100 parameters: self.schema.clone(),
101 };
102 Box::pin(async move { def })
103 }
104
105 fn call(&self, args: String) -> BoxFuture<'_, Result<String, ToolError>> {
106 Box::pin(async move {
107 let _: Value = serde_json::from_str(&args).map_err(|e| {
110 ToolError::ToolCallError(Box::new(std::io::Error::new(
111 std::io::ErrorKind::InvalidData,
112 format!("submit_result: invalid JSON: {}", e),
113 )))
114 })?;
115 Ok(args)
117 })
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 use rig::tool::ToolDyn;
125 use serde_json::json;
126
127 #[test]
128 fn submit_tool_has_correct_name() {
129 let schema = json!({
130 "type": "object",
131 "properties": {
132 "name": { "type": "string" }
133 },
134 "required": ["name"]
135 });
136 let tool = DynamicSubmitTool::new(schema);
137 assert_eq!(tool.name(), "submit_result");
138 }
139
140 #[tokio::test]
141 async fn submit_tool_definition_has_schema_as_parameters() {
142 let schema = json!({
143 "type": "object",
144 "properties": {
145 "name": { "type": "string" },
146 "age": { "type": "integer" }
147 },
148 "required": ["name"]
149 });
150 let tool = DynamicSubmitTool::new(schema.clone());
151 let def = tool.definition("test prompt".to_string()).await;
152 assert_eq!(def.name, "submit_result");
153 assert_eq!(def.parameters, schema);
154 assert!(def.description.contains("Submit"));
155 }
156
157 #[tokio::test]
158 async fn submit_tool_call_returns_args_as_is() {
159 let schema = json!({"type": "object"});
160 let tool = DynamicSubmitTool::new(schema);
161 let args = r#"{"name": "Alice", "age": 30}"#;
162 let result = tool.call(args.to_string()).await.unwrap();
163 assert_eq!(result, args);
164 }
165
166 #[tokio::test]
167 async fn submit_tool_call_rejects_invalid_json() {
168 let schema = json!({"type": "object"});
169 let tool = DynamicSubmitTool::new(schema);
170 let result = tool.call("not json".to_string()).await;
171 assert!(result.is_err());
172 }
173
174 #[test]
175 fn submit_tool_custom_name() {
176 let schema = json!({"type": "object"});
177 let tool = DynamicSubmitTool::with_name("output_json", schema);
178 assert_eq!(tool.name(), "output_json");
179 }
180
181 #[test]
182 fn submit_tool_schema_accessor() {
183 let schema = json!({
184 "type": "object",
185 "properties": { "x": { "type": "integer" } }
186 });
187 let tool = DynamicSubmitTool::new(schema.clone());
188 assert_eq!(tool.schema(), &schema);
189 }
190
191 #[tokio::test]
192 async fn submit_tool_with_nested_schema() {
193 let schema = json!({
194 "type": "object",
195 "properties": {
196 "user": {
197 "type": "object",
198 "properties": {
199 "name": { "type": "string" },
200 "address": {
201 "type": "object",
202 "properties": {
203 "city": { "type": "string" }
204 }
205 }
206 }
207 }
208 }
209 });
210 let tool = DynamicSubmitTool::new(schema.clone());
211 let def = tool.definition("test".to_string()).await;
212 assert_eq!(def.parameters, schema);
213 }
214
215 #[tokio::test]
216 async fn submit_tool_with_array_schema() {
217 let schema = json!({
218 "type": "object",
219 "properties": {
220 "items": {
221 "type": "array",
222 "items": { "type": "string" }
223 }
224 }
225 });
226 let tool = DynamicSubmitTool::new(schema);
227 let args = r#"{"items": ["a", "b", "c"]}"#;
228 let result = tool.call(args.to_string()).await.unwrap();
229 let parsed: Value = serde_json::from_str(&result).unwrap();
230 assert_eq!(parsed["items"].as_array().unwrap().len(), 3);
231 }
232
233 #[tokio::test]
234 async fn submit_tool_preserves_exact_json() {
235 let schema = json!({"type": "object"});
236 let tool = DynamicSubmitTool::new(schema);
237 let args = r#"{"key":"value","num":42}"#;
239 let result = tool.call(args.to_string()).await.unwrap();
240 assert_eq!(result, args);
241 }
242}