1use crate::completion::{Completion, Usage};
14use crate::message::Message;
15use crate::provider::{BoxFuture, Provider};
16use crate::request::Request;
17use rustlavel_core::{Error, Json, Result};
18use std::collections::BTreeMap;
19use std::future::Future;
20use std::sync::Arc;
21
22#[derive(Debug, Clone, Default, PartialEq)]
28pub struct Schema {
29 properties: Vec<(String, Json)>,
30 required: Vec<String>,
31 description: Option<String>,
32 raw: Option<Json>,
34}
35
36impl Schema {
37 pub fn object() -> Schema {
38 Schema::default()
39 }
40
41 pub fn raw(schema: Json) -> Schema {
43 Schema { raw: Some(schema), ..Schema::default() }
44 }
45
46 pub fn describe(mut self, description: impl Into<String>) -> Schema {
48 self.description = Some(description.into());
49 self
50 }
51
52 pub fn string(self, name: &str, description: &str) -> Schema {
53 self.field(name, "string", description)
54 }
55
56 pub fn integer(self, name: &str, description: &str) -> Schema {
57 self.field(name, "integer", description)
58 }
59
60 pub fn number(self, name: &str, description: &str) -> Schema {
61 self.field(name, "number", description)
62 }
63
64 pub fn boolean(self, name: &str, description: &str) -> Schema {
65 self.field(name, "boolean", description)
66 }
67
68 pub fn one_of(mut self, name: &str, description: &str, values: &[&str]) -> Schema {
70 let values: Vec<Json> = values.iter().map(|v| Json::from(*v)).collect();
71 self.put(
72 name,
73 Json::object([
74 ("type", Json::from("string")),
75 ("description", Json::from(description)),
76 ("enum", Json::Array(values)),
77 ]),
78 );
79 self
80 }
81
82 pub fn array_of(mut self, name: &str, description: &str, item_type: &str) -> Schema {
84 self.put(
85 name,
86 Json::object([
87 ("type", Json::from("array")),
88 ("description", Json::from(description)),
89 ("items", Json::object([("type", Json::from(item_type))])),
90 ]),
91 );
92 self
93 }
94
95 pub fn nested(mut self, name: &str, description: &str, schema: Schema) -> Schema {
97 let mut inner = schema.to_json();
98 if let Json::Object(map) = &mut inner {
99 map.insert("description".into(), Json::from(description));
100 }
101 self.put(name, inner);
102 self
103 }
104
105 pub fn optional(mut self) -> Schema {
111 self.required.pop();
112 self
113 }
114
115 fn field(mut self, name: &str, kind: &str, description: &str) -> Schema {
116 self.put(
117 name,
118 Json::object([
119 ("type", Json::from(kind)),
120 ("description", Json::from(description)),
121 ]),
122 );
123 self
124 }
125
126 fn put(&mut self, name: &str, definition: Json) {
127 self.properties.push((name.to_string(), definition));
128 self.required.push(name.to_string());
129 }
130
131 pub fn to_json(&self) -> Json {
133 if let Some(raw) = &self.raw {
134 return raw.clone();
135 }
136
137 let mut object = BTreeMap::new();
138 object.insert("type".to_string(), Json::from("object"));
139 if let Some(description) = &self.description {
140 object.insert("description".to_string(), Json::from(description.as_str()));
141 }
142 object.insert(
143 "properties".to_string(),
144 Json::object(self.properties.iter().map(|(k, v)| (k.as_str(), v.clone()))),
145 );
146 object.insert(
147 "required".to_string(),
148 Json::Array(self.required.iter().map(|name| Json::from(name.as_str())).collect()),
149 );
150 Json::Object(object)
151 }
152
153 pub fn is_empty(&self) -> bool {
154 self.raw.is_none() && self.properties.is_empty()
155 }
156}
157
158#[derive(Debug, Clone, PartialEq)]
160pub struct Tool {
161 pub name: String,
162 pub description: String,
165 pub parameters: Schema,
166}
167
168impl Tool {
169 pub fn new(name: impl Into<String>, description: impl Into<String>) -> Tool {
170 Tool {
171 name: name.into(),
172 description: description.into(),
173 parameters: Schema::object(),
174 }
175 }
176
177 pub fn parameters(mut self, schema: Schema) -> Tool {
178 self.parameters = schema;
179 self
180 }
181
182 pub fn string(mut self, name: &str, description: &str) -> Tool {
183 self.parameters = self.parameters.string(name, description);
184 self
185 }
186
187 pub fn integer(mut self, name: &str, description: &str) -> Tool {
188 self.parameters = self.parameters.integer(name, description);
189 self
190 }
191
192 pub fn number(mut self, name: &str, description: &str) -> Tool {
193 self.parameters = self.parameters.number(name, description);
194 self
195 }
196
197 pub fn boolean(mut self, name: &str, description: &str) -> Tool {
198 self.parameters = self.parameters.boolean(name, description);
199 self
200 }
201
202 pub fn one_of(mut self, name: &str, description: &str, values: &[&str]) -> Tool {
203 self.parameters = self.parameters.one_of(name, description, values);
204 self
205 }
206
207 pub fn array_of(mut self, name: &str, description: &str, item_type: &str) -> Tool {
208 self.parameters = self.parameters.array_of(name, description, item_type);
209 self
210 }
211
212 pub fn optional(mut self) -> Tool {
214 self.parameters = self.parameters.optional();
215 self
216 }
217
218 pub fn schema_json(&self) -> Json {
219 self.parameters.to_json()
220 }
221}
222
223type Handler = Arc<dyn Fn(Json) -> BoxFuture<'static, Result<Json>> + Send + Sync>;
225
226#[derive(Clone, Default)]
231pub struct Toolbox {
232 tools: Vec<Tool>,
233 handlers: BTreeMap<String, Handler>,
234}
235
236impl Toolbox {
237 pub fn new() -> Toolbox {
238 Toolbox::default()
239 }
240
241 pub fn add<F, Fut>(mut self, tool: Tool, handler: F) -> Toolbox
243 where
244 F: Fn(Json) -> Fut + Send + Sync + 'static,
245 Fut: Future<Output = Result<Json>> + Send + 'static,
246 {
247 let name = tool.name.clone();
248 self.tools.retain(|existing| existing.name != name);
249 self.tools.push(tool);
250 self.handlers.insert(name, Arc::new(move |input| Box::pin(handler(input))));
251 self
252 }
253
254 pub fn tools(&self) -> &[Tool] {
255 &self.tools
256 }
257
258 pub fn has(&self, name: &str) -> bool {
259 self.handlers.contains_key(name)
260 }
261
262 pub fn len(&self) -> usize {
263 self.tools.len()
264 }
265
266 pub fn is_empty(&self) -> bool {
267 self.tools.is_empty()
268 }
269
270 pub async fn call(&self, name: &str, arguments: Json) -> Result<Json> {
272 let handler = self.handlers.get(name).ok_or_else(|| {
273 Error::msg(format!(
274 "the model asked for a tool named `{name}`, which is not registered. \
275 Registered tools: {}",
276 self.tools.iter().map(|t| t.name.as_str()).collect::<Vec<_>>().join(", ")
277 ))
278 })?;
279 handler(arguments).await
280 }
281}
282
283impl std::fmt::Debug for Toolbox {
284 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
285 f.debug_struct("Toolbox")
286 .field("tools", &self.tools.iter().map(|t| &t.name).collect::<Vec<_>>())
287 .finish()
288 }
289}
290
291pub const DEFAULT_MAX_ROUNDS: usize = 8;
296
297pub async fn run_tools(
303 provider: &dyn Provider,
304 request: &Request,
305 toolbox: &Toolbox,
306 max_rounds: usize,
307) -> Result<Completion> {
308 let mut request = request.clone();
309 if request.tools.is_empty() {
310 request.tools = toolbox.tools().to_vec();
311 }
312
313 let mut total = Usage::default();
314
315 for round in 0..max_rounds.max(1) {
316 let mut completion = provider.complete(&request).await?;
317 total.add(completion.usage);
318
319 if !completion.wants_tools() {
320 completion.usage = total;
321 return Ok(completion);
322 }
323
324 request.messages.push(completion.to_message());
325 for call in &completion.tool_calls {
326 let message = match toolbox.call(&call.name, call.arguments.clone()).await {
327 Ok(output) => Message::tool_result(&call.id, &call.name, output),
328 Err(error) => Message::tool_error(&call.id, &call.name, error.to_string()),
332 };
333 request.messages.push(message);
334 }
335
336 if round + 1 == max_rounds.max(1) {
337 return Err(Error::msg(format!(
338 "the model still wanted tools after {max_rounds} rounds; \
339 raise the limit or check that the tools answer usefully"
340 )));
341 }
342 }
343
344 unreachable!("the loop returns or errors on its last round")
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350
351 fn weather_tool() -> Tool {
352 Tool::new("get_weather", "Look up today's weather")
353 .string("city", "The city to look up")
354 .one_of("unit", "Temperature unit", &["celsius", "fahrenheit"])
355 .optional()
356 }
357
358 #[test]
359 fn a_schema_is_built_without_hand_writing_json() {
360 let schema = weather_tool().schema_json();
361
362 assert_eq!(schema.get("type").unwrap().as_str(), Some("object"));
363 assert_eq!(schema.get("properties.city.type").unwrap().as_str(), Some("string"));
364 assert_eq!(
365 schema.get("properties.city.description").unwrap().as_str(),
366 Some("The city to look up")
367 );
368 assert_eq!(schema.get("properties.unit.enum.0").unwrap().as_str(), Some("celsius"));
369
370 let required = schema.get("required").unwrap().as_array().unwrap();
372 assert_eq!(required.len(), 1);
373 assert_eq!(required[0].as_str(), Some("city"));
374 }
375
376 #[test]
377 fn schemas_describe_arrays_and_nested_objects() {
378 let schema = Schema::object()
379 .describe("A search")
380 .array_of("terms", "Words to look for", "string")
381 .nested("page", "Paging", Schema::object().integer("size", "How many"))
382 .to_json();
383
384 assert_eq!(schema.get("description").unwrap().as_str(), Some("A search"));
385 assert_eq!(schema.get("properties.terms.items.type").unwrap().as_str(), Some("string"));
386 assert_eq!(schema.get("properties.page.type").unwrap().as_str(), Some("object"));
387 assert_eq!(schema.get("properties.page.description").unwrap().as_str(), Some("Paging"));
388 assert_eq!(
389 schema.get("properties.page.properties.size.type").unwrap().as_str(),
390 Some("integer")
391 );
392 }
393
394 #[test]
395 fn a_hand_written_schema_is_passed_through_untouched() {
396 let raw = Json::object([("type", "string".into())]);
397 assert_eq!(Schema::raw(raw.clone()).to_json(), raw);
398 }
399
400 #[test]
401 fn marking_optional_with_no_fields_is_harmless() {
402 assert!(Schema::object().optional().to_json().get("required").is_some());
403 }
404
405 #[tokio::test]
406 async fn a_toolbox_runs_the_handler_it_was_given() {
407 let toolbox = Toolbox::new().add(weather_tool(), |input: Json| async move {
408 let city = input.get("city").and_then(Json::as_str).unwrap_or("nowhere").to_string();
409 Ok(Json::object([("city", Json::from(city)), ("degrees", 7.into())]))
410 });
411
412 assert_eq!(toolbox.len(), 1);
413 assert!(toolbox.has("get_weather"));
414
415 let answer = toolbox
416 .call("get_weather", Json::object([("city", "Oslo".into())]))
417 .await
418 .unwrap();
419
420 assert_eq!(answer.get("city").unwrap().as_str(), Some("Oslo"));
421 assert_eq!(answer.get("degrees").unwrap().as_i64(), Some(7));
422 }
423
424 #[tokio::test]
425 async fn an_unknown_tool_names_the_ones_that_do_exist() {
426 let toolbox = Toolbox::new().add(weather_tool(), |_| async { Ok(Json::Null) });
427
428 let error = toolbox.call("get_tides", Json::Null).await.unwrap_err().to_string();
429
430 assert!(error.contains("get_tides"), "{error}");
431 assert!(error.contains("get_weather"), "{error}");
432 }
433
434 #[test]
435 fn registering_a_tool_twice_replaces_it() {
436 let toolbox = Toolbox::new()
437 .add(Tool::new("t", "first"), |_| async { Ok(Json::Null) })
438 .add(Tool::new("t", "second"), |_| async { Ok(Json::Null) });
439
440 assert_eq!(toolbox.len(), 1);
441 assert_eq!(toolbox.tools()[0].description, "second");
442 assert!(format!("{toolbox:?}").contains('t'));
443 }
444}