1use std::collections::BTreeMap;
2
3use serde_json::{Map, Value};
4use sim_kernel::{CapabilityName, Cx, Error, Expr, Result, Symbol};
5
6use super::tool_schema::{
7 argument_order, arguments_json, canonical_json, default_parameters, expr_text,
8 expr_to_json_string, json_to_expr, json_to_value, validate_schema,
9};
10
11#[derive(Clone, Debug, PartialEq)]
18pub struct OpenAiTool {
19 openai_name: String,
20 symbol: Symbol,
21 description: String,
22 parameters: Value,
23 arg_order: Vec<String>,
24 capabilities: Vec<CapabilityName>,
25 args_shape: Option<Expr>,
26 result_shape: Option<Expr>,
27}
28
29#[derive(Clone, Debug, Default, PartialEq)]
31pub struct OpenAiToolRegistry {
32 tools: BTreeMap<String, OpenAiTool>,
33}
34
35#[derive(Clone, Debug, PartialEq)]
37pub struct OpenAiToolCall {
38 pub id: String,
40 pub name: String,
42 pub arguments: Value,
44}
45
46#[derive(Clone, Debug, PartialEq)]
48pub struct OpenAiToolResult {
49 pub call_id: String,
51 pub name: String,
53 pub status: Symbol,
55 pub output: Expr,
57}
58
59impl OpenAiTool {
60 pub fn from_callable(
63 cx: &mut Cx,
64 openai_name: impl Into<String>,
65 symbol: Symbol,
66 description: impl Into<String>,
67 parameters: Value,
68 capabilities: Vec<CapabilityName>,
69 ) -> Result<Self> {
70 let function = cx.resolve_function(&symbol)?;
71 let Some(callable) = function.object().as_callable() else {
72 return Err(Error::TypeMismatch {
73 expected: "callable",
74 found: "non-callable",
75 });
76 };
77 let args_shape = callable
78 .browse_args_shape(cx)?
79 .map(|shape| shape.object().as_expr(cx))
80 .transpose()?;
81 let result_shape = callable
82 .browse_result_shape(cx)?
83 .map(|shape| shape.object().as_expr(cx))
84 .transpose()?;
85 Ok(Self::new(
86 openai_name,
87 symbol,
88 description,
89 parameters,
90 capabilities,
91 args_shape,
92 result_shape,
93 ))
94 }
95
96 pub fn from_openai_descriptor(value: &Value) -> Result<Self> {
102 let object = value
103 .as_object()
104 .ok_or_else(|| Error::Eval("openai tool descriptor must be an object".to_owned()))?;
105 let kind = object
106 .get("type")
107 .and_then(Value::as_str)
108 .unwrap_or("function");
109 if kind != "function" {
110 return Err(Error::Eval(format!(
111 "unsupported OpenAI tool descriptor type {kind}"
112 )));
113 }
114 let function = object
115 .get("function")
116 .and_then(Value::as_object)
117 .ok_or_else(|| Error::Eval("openai tool missing function object".to_owned()))?;
118 let openai_name = string_member(function, "name")?.to_owned();
119 validate_openai_name(&openai_name)?;
120 let description = function
121 .get("description")
122 .and_then(Value::as_str)
123 .unwrap_or("")
124 .to_owned();
125 let parameters = function
126 .get("parameters")
127 .cloned()
128 .unwrap_or_else(default_parameters);
129 let symbol = function
130 .get("x-sim-symbol")
131 .or_else(|| object.get("x-sim-symbol"))
132 .and_then(Value::as_str)
133 .map(symbol_from_text)
134 .transpose()?
135 .unwrap_or_else(|| openai_name_to_symbol(&openai_name));
136 let capabilities = capabilities_from(function)
137 .or_else(|| capabilities_from(object))
138 .unwrap_or_default();
139 Ok(Self::new(
140 openai_name,
141 symbol,
142 description,
143 parameters,
144 capabilities,
145 None,
146 None,
147 ))
148 }
149
150 fn new(
151 openai_name: impl Into<String>,
152 symbol: Symbol,
153 description: impl Into<String>,
154 parameters: Value,
155 capabilities: Vec<CapabilityName>,
156 args_shape: Option<Expr>,
157 result_shape: Option<Expr>,
158 ) -> Self {
159 let arg_order = argument_order(¶meters);
160 Self {
161 openai_name: openai_name.into(),
162 symbol,
163 description: description.into(),
164 parameters,
165 arg_order,
166 capabilities,
167 args_shape,
168 result_shape,
169 }
170 }
171
172 pub fn openai_name(&self) -> &str {
174 &self.openai_name
175 }
176
177 pub fn symbol(&self) -> &Symbol {
179 &self.symbol
180 }
181
182 pub fn capabilities(&self) -> &[CapabilityName] {
184 &self.capabilities
185 }
186
187 pub fn validate_arguments(&self, arguments: &Value) -> std::result::Result<(), String> {
189 validate_schema(&self.parameters, arguments, "arguments")
190 }
191
192 pub fn argument_values(
194 &self,
195 cx: &mut Cx,
196 arguments: &Value,
197 ) -> std::result::Result<Vec<sim_kernel::Value>, String> {
198 self.validate_arguments(arguments)?;
199 let Some(object) = arguments.as_object() else {
200 return Err("tool arguments must be an object".to_owned());
201 };
202 self.arg_order
203 .iter()
204 .filter_map(|name| object.get(name))
205 .map(|value| json_to_value(cx, value).map_err(|err| err.to_string()))
206 .collect()
207 }
208
209 pub fn descriptor_json(&self) -> Value {
212 let mut function = Map::new();
213 function.insert("name".to_owned(), Value::String(self.openai_name.clone()));
214 function.insert(
215 "description".to_owned(),
216 Value::String(self.description.clone()),
217 );
218 function.insert("parameters".to_owned(), self.parameters.clone());
219 function.insert(
220 "x-sim-symbol".to_owned(),
221 Value::String(self.symbol.as_qualified_str()),
222 );
223 if let Some(shape) = &self.args_shape {
224 function.insert("x-sim-args-shape".to_owned(), expr_to_json_string(shape));
225 }
226 if let Some(shape) = &self.result_shape {
227 function.insert("x-sim-result-shape".to_owned(), expr_to_json_string(shape));
228 }
229 if !self.capabilities.is_empty() {
230 function.insert(
231 "x-sim-capabilities".to_owned(),
232 Value::Array(
233 self.capabilities
234 .iter()
235 .map(|capability| Value::String(capability.as_str().to_owned()))
236 .collect(),
237 ),
238 );
239 }
240 let mut descriptor = Map::new();
241 descriptor.insert("type".to_owned(), Value::String("function".to_owned()));
242 descriptor.insert("function".to_owned(), Value::Object(function));
243 Value::Object(descriptor)
244 }
245
246 pub fn to_expr(&self) -> Expr {
248 Expr::Map(vec![
249 field("kind", Expr::Symbol(Symbol::new("openai-gateway/tool"))),
250 field("openai-name", Expr::String(self.openai_name.clone())),
251 field("symbol", Expr::Symbol(self.symbol.clone())),
252 field("description", Expr::String(self.description.clone())),
253 field("parameters", json_to_expr(&self.parameters)),
254 field(
255 "capabilities",
256 Expr::List(
257 self.capabilities
258 .iter()
259 .map(|capability| Expr::String(capability.as_str().to_owned()))
260 .collect(),
261 ),
262 ),
263 field("args-shape", self.args_shape.clone().unwrap_or(Expr::Nil)),
264 field(
265 "result-shape",
266 self.result_shape.clone().unwrap_or(Expr::Nil),
267 ),
268 ])
269 }
270}
271
272impl OpenAiToolRegistry {
273 pub fn from_request(object: &Map<String, Value>) -> Result<Self> {
276 let Some(tools) = object.get("tools") else {
277 return Ok(Self::default());
278 };
279 if tools.is_null() {
280 return Ok(Self::default());
281 }
282 let tools = tools
283 .as_array()
284 .ok_or_else(|| Error::Eval("openai tools field must be an array".to_owned()))?;
285 let mut registry = Self::default();
286 for tool in tools {
287 registry.insert(OpenAiTool::from_openai_descriptor(tool)?)?;
288 }
289 Ok(registry)
290 }
291
292 pub fn insert(&mut self, tool: OpenAiTool) -> Result<()> {
294 if self.tools.contains_key(tool.openai_name()) {
295 return Err(Error::Eval(format!(
296 "duplicate OpenAI tool name {}",
297 tool.openai_name()
298 )));
299 }
300 self.tools.insert(tool.openai_name.clone(), tool);
301 Ok(())
302 }
303
304 pub fn is_empty(&self) -> bool {
306 self.tools.is_empty()
307 }
308
309 pub fn get(&self, name: &str) -> Option<&OpenAiTool> {
311 self.tools.get(name)
312 }
313
314 pub fn descriptor_json(&self) -> Value {
316 Value::Array(
317 self.tools
318 .values()
319 .map(OpenAiTool::descriptor_json)
320 .collect(),
321 )
322 }
323
324 pub fn to_expr(&self) -> Expr {
326 Expr::List(self.tools.values().map(OpenAiTool::to_expr).collect())
327 }
328}
329
330impl OpenAiToolCall {
331 pub fn from_content_part(part: &Expr) -> Result<Option<Self>> {
334 let Expr::Map(entries) = part else {
335 return Ok(None);
336 };
337 if symbol_field(entries, "type").as_deref() != Some("tool-call") {
338 return Ok(None);
339 }
340 let name = required_string_field(entries, "name")?;
341 let id = string_field(entries, "id").unwrap_or_else(|| format!("call_{name}"));
342 let arguments = entries
343 .iter()
344 .find_map(|(key, value)| match key {
345 Expr::Symbol(symbol)
346 if symbol.namespace.is_none() && symbol.name.as_ref() == "arguments" =>
347 {
348 Some(arguments_json(value))
349 }
350 _ => None,
351 })
352 .transpose()?
353 .unwrap_or_else(|| Value::Object(Map::new()));
354 Ok(Some(Self {
355 id,
356 name,
357 arguments,
358 }))
359 }
360
361 pub fn to_expr(&self) -> Expr {
363 Expr::Map(vec![
364 field("id", Expr::String(self.id.clone())),
365 field("name", Expr::String(self.name.clone())),
366 field("arguments", json_to_expr(&self.arguments)),
367 ])
368 }
369
370 pub fn fingerprint(&self) -> String {
372 format!("{}:{}", self.name, canonical_json(&self.arguments))
373 }
374}
375
376impl OpenAiToolResult {
377 pub fn success(call: &OpenAiToolCall, output: Expr) -> Self {
379 Self {
380 call_id: call.id.clone(),
381 name: call.name.clone(),
382 status: Symbol::new("ok"),
383 output,
384 }
385 }
386
387 pub fn invalid_arguments(call: &OpenAiToolCall, message: impl Into<String>) -> Self {
389 Self::failure(call, "invalid-arguments", message)
390 }
391
392 pub fn capability_denied(call: &OpenAiToolCall, message: impl Into<String>) -> Self {
394 Self::failure(call, "capability-denied", message)
395 }
396
397 pub fn unknown_tool(call: &OpenAiToolCall) -> Self {
399 Self::failure(call, "unknown-tool", format!("unknown tool {}", call.name))
400 }
401
402 fn failure(call: &OpenAiToolCall, status: &'static str, message: impl Into<String>) -> Self {
403 Self {
404 call_id: call.id.clone(),
405 name: call.name.clone(),
406 status: Symbol::new(status),
407 output: Expr::String(message.into()),
408 }
409 }
410
411 pub fn to_expr(&self) -> Expr {
413 Expr::Map(vec![
414 field("tool-call-id", Expr::String(self.call_id.clone())),
415 field("name", Expr::String(self.name.clone())),
416 field("status", Expr::Symbol(self.status.clone())),
417 field("output", self.output.clone()),
418 ])
419 }
420
421 pub fn message_text(&self) -> String {
423 format!(
424 "tool {} {}: {}",
425 self.name,
426 self.status.name,
427 expr_text(&self.output)
428 )
429 }
430}
431
432pub fn openai_name_to_symbol(name: &str) -> Symbol {
435 if let Some((namespace, local)) = name.split_once('_') {
436 Symbol::qualified(namespace.replace('_', "-"), local.replace('_', "-"))
437 } else {
438 Symbol::new(name.replace('_', "-"))
439 }
440}
441
442pub fn symbol_from_text(text: &str) -> Result<Symbol> {
445 if text.trim().is_empty() {
446 return Err(Error::Eval("SIM tool symbol must not be empty".to_owned()));
447 }
448 Ok(if let Some((namespace, name)) = text.split_once('/') {
449 if namespace.is_empty() || name.is_empty() {
450 return Err(Error::Eval(format!("invalid SIM symbol {text}")));
451 }
452 Symbol::qualified(namespace.to_owned(), name.to_owned())
453 } else {
454 Symbol::new(text.to_owned())
455 })
456}
457
458fn validate_openai_name(name: &str) -> Result<()> {
459 if name.is_empty() {
460 return Err(Error::Eval("OpenAI tool name must not be empty".to_owned()));
461 }
462 if name
463 .chars()
464 .all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-')
465 {
466 Ok(())
467 } else {
468 Err(Error::Eval(format!(
469 "OpenAI tool name {name} contains unsupported characters"
470 )))
471 }
472}
473
474fn string_member<'a>(object: &'a Map<String, Value>, name: &str) -> Result<&'a str> {
475 object
476 .get(name)
477 .and_then(Value::as_str)
478 .ok_or_else(|| Error::Eval(format!("openai tool missing string {name}")))
479}
480
481fn capabilities_from(object: &Map<String, Value>) -> Option<Vec<CapabilityName>> {
482 let values = object.get("x-sim-capabilities")?.as_array()?;
483 Some(
484 values
485 .iter()
486 .filter_map(Value::as_str)
487 .map(|value| CapabilityName::new(value.to_owned()))
488 .collect(),
489 )
490}
491
492fn required_string_field(entries: &[(Expr, Expr)], name: &str) -> Result<String> {
493 string_field(entries, name)
494 .ok_or_else(|| Error::Eval(format!("tool-call missing string {name}")))
495}
496
497fn string_field(entries: &[(Expr, Expr)], name: &str) -> Option<String> {
498 sim_value::access::entry_field(entries, name)
499 .and_then(sim_value::access::as_str)
500 .map(str::to_owned)
501}
502
503fn symbol_field(entries: &[(Expr, Expr)], name: &str) -> Option<String> {
504 match sim_value::access::entry_field(entries, name) {
505 Some(Expr::Symbol(value)) => Some(value.name.as_ref().to_owned()),
506 _ => None,
507 }
508}
509
510use sim_value::build::entry as field;