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, validate_supported_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 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> {
103 let object = value
104 .as_object()
105 .ok_or_else(|| Error::Eval("openai tool descriptor must be an object".to_owned()))?;
106 let kind = object
107 .get("type")
108 .and_then(Value::as_str)
109 .unwrap_or("function");
110 if kind != "function" {
111 return Err(Error::Eval(format!(
112 "unsupported OpenAI tool descriptor type {kind}"
113 )));
114 }
115 let function = object
116 .get("function")
117 .and_then(Value::as_object)
118 .ok_or_else(|| Error::Eval("openai tool missing function object".to_owned()))?;
119 let openai_name = string_member(function, "name")?.to_owned();
120 validate_openai_name(&openai_name)?;
121 let description = function
122 .get("description")
123 .and_then(Value::as_str)
124 .unwrap_or("")
125 .to_owned();
126 let parameters = function
127 .get("parameters")
128 .cloned()
129 .unwrap_or_else(default_parameters);
130 reject_untrusted_authority_fields(object)?;
131 reject_untrusted_authority_fields(function)?;
132 let symbol = openai_name_to_symbol(&openai_name);
133 Self::new(
134 openai_name,
135 symbol,
136 description,
137 parameters,
138 Vec::new(),
139 None,
140 None,
141 )
142 }
143
144 fn new(
145 openai_name: impl Into<String>,
146 symbol: Symbol,
147 description: impl Into<String>,
148 parameters: Value,
149 capabilities: Vec<CapabilityName>,
150 args_shape: Option<Expr>,
151 result_shape: Option<Expr>,
152 ) -> Result<Self> {
153 validate_supported_schema(¶meters, "function.parameters").map_err(Error::Eval)?;
154 let arg_order = argument_order(¶meters);
155 Ok(Self {
156 openai_name: openai_name.into(),
157 symbol,
158 description: description.into(),
159 parameters,
160 arg_order,
161 capabilities,
162 args_shape,
163 result_shape,
164 })
165 }
166
167 pub fn openai_name(&self) -> &str {
169 &self.openai_name
170 }
171
172 pub fn symbol(&self) -> &Symbol {
174 &self.symbol
175 }
176
177 pub fn capabilities(&self) -> &[CapabilityName] {
179 &self.capabilities
180 }
181
182 pub fn validate_arguments(&self, arguments: &Value) -> std::result::Result<(), String> {
184 validate_schema(&self.parameters, arguments, "arguments")
185 }
186
187 pub fn argument_values(
189 &self,
190 cx: &mut Cx,
191 arguments: &Value,
192 ) -> std::result::Result<Vec<sim_kernel::Value>, String> {
193 self.validate_arguments(arguments)?;
194 let Some(object) = arguments.as_object() else {
195 return Err("tool arguments must be an object".to_owned());
196 };
197 self.arg_order
198 .iter()
199 .filter_map(|name| object.get(name))
200 .map(|value| json_to_value(cx, value).map_err(|err| err.to_string()))
201 .collect()
202 }
203
204 pub fn descriptor_json(&self) -> Value {
207 let mut function = Map::new();
208 function.insert("name".to_owned(), Value::String(self.openai_name.clone()));
209 function.insert(
210 "description".to_owned(),
211 Value::String(self.description.clone()),
212 );
213 function.insert("parameters".to_owned(), self.parameters.clone());
214 function.insert(
215 "x-sim-symbol".to_owned(),
216 Value::String(self.symbol.as_qualified_str()),
217 );
218 if let Some(shape) = &self.args_shape {
219 function.insert("x-sim-args-shape".to_owned(), expr_to_json_string(shape));
220 }
221 if let Some(shape) = &self.result_shape {
222 function.insert("x-sim-result-shape".to_owned(), expr_to_json_string(shape));
223 }
224 if !self.capabilities.is_empty() {
225 function.insert(
226 "x-sim-capabilities".to_owned(),
227 Value::Array(
228 self.capabilities
229 .iter()
230 .map(|capability| Value::String(capability.as_str().to_owned()))
231 .collect(),
232 ),
233 );
234 }
235 let mut descriptor = Map::new();
236 descriptor.insert("type".to_owned(), Value::String("function".to_owned()));
237 descriptor.insert("function".to_owned(), Value::Object(function));
238 Value::Object(descriptor)
239 }
240
241 pub fn request_descriptor_json(&self) -> Value {
248 let mut function = Map::new();
249 function.insert("name".to_owned(), Value::String(self.openai_name.clone()));
250 function.insert(
251 "description".to_owned(),
252 Value::String(self.description.clone()),
253 );
254 function.insert("parameters".to_owned(), self.parameters.clone());
255 let mut descriptor = Map::new();
256 descriptor.insert("type".to_owned(), Value::String("function".to_owned()));
257 descriptor.insert("function".to_owned(), Value::Object(function));
258 Value::Object(descriptor)
259 }
260
261 pub fn to_expr(&self) -> Expr {
263 Expr::Map(vec![
264 field("kind", Expr::Symbol(Symbol::new("openai-gateway/tool"))),
265 field("openai-name", Expr::String(self.openai_name.clone())),
266 field("symbol", Expr::Symbol(self.symbol.clone())),
267 field("description", Expr::String(self.description.clone())),
268 field("parameters", json_to_expr(&self.parameters)),
269 field(
270 "capabilities",
271 Expr::List(
272 self.capabilities
273 .iter()
274 .map(|capability| Expr::String(capability.as_str().to_owned()))
275 .collect(),
276 ),
277 ),
278 field("args-shape", self.args_shape.clone().unwrap_or(Expr::Nil)),
279 field(
280 "result-shape",
281 self.result_shape.clone().unwrap_or(Expr::Nil),
282 ),
283 ])
284 }
285}
286
287impl OpenAiToolRegistry {
288 pub fn from_request(cx: &mut Cx, object: &Map<String, Value>) -> Result<Self> {
291 let Some(tools) = object.get("tools") else {
292 return Ok(Self::default());
293 };
294 if tools.is_null() {
295 return Ok(Self::default());
296 }
297 let tools = tools
298 .as_array()
299 .ok_or_else(|| Error::Eval("openai tools field must be an array".to_owned()))?;
300 let mut registry = Self::default();
301 for tool in tools {
302 let tool = OpenAiTool::from_openai_descriptor(tool)?;
303 ensure_server_registered_tool(cx, &tool)?;
304 registry.insert(tool)?;
305 }
306 Ok(registry)
307 }
308
309 pub fn insert(&mut self, tool: OpenAiTool) -> Result<()> {
311 if self.tools.contains_key(tool.openai_name()) {
312 return Err(Error::Eval(format!(
313 "duplicate OpenAI tool name {}",
314 tool.openai_name()
315 )));
316 }
317 self.tools.insert(tool.openai_name.clone(), tool);
318 Ok(())
319 }
320
321 pub fn is_empty(&self) -> bool {
323 self.tools.is_empty()
324 }
325
326 pub fn get(&self, name: &str) -> Option<&OpenAiTool> {
328 self.tools.get(name)
329 }
330
331 pub fn descriptor_json(&self) -> Value {
333 Value::Array(
334 self.tools
335 .values()
336 .map(OpenAiTool::descriptor_json)
337 .collect(),
338 )
339 }
340
341 pub fn to_expr(&self) -> Expr {
343 Expr::List(self.tools.values().map(OpenAiTool::to_expr).collect())
344 }
345}
346
347impl OpenAiToolCall {
348 pub fn from_content_part(part: &Expr) -> Result<Option<Self>> {
351 let Expr::Map(entries) = part else {
352 return Ok(None);
353 };
354 if symbol_field(entries, "type").as_deref() != Some("tool-call") {
355 return Ok(None);
356 }
357 let name = required_string_field(entries, "name")?;
358 let id = string_field(entries, "id").unwrap_or_else(|| format!("call_{name}"));
359 let arguments = entries
360 .iter()
361 .find_map(|(key, value)| match key {
362 Expr::Symbol(symbol)
363 if symbol.namespace.is_none() && symbol.name.as_ref() == "arguments" =>
364 {
365 Some(arguments_json(value))
366 }
367 _ => None,
368 })
369 .transpose()?
370 .unwrap_or_else(|| Value::Object(Map::new()));
371 Ok(Some(Self {
372 id,
373 name,
374 arguments,
375 }))
376 }
377
378 pub fn to_expr(&self) -> Expr {
380 Expr::Map(vec![
381 field("id", Expr::String(self.id.clone())),
382 field("name", Expr::String(self.name.clone())),
383 field("arguments", json_to_expr(&self.arguments)),
384 ])
385 }
386
387 pub fn fingerprint(&self) -> String {
389 format!("{}:{}", self.name, canonical_json(&self.arguments))
390 }
391}
392
393impl OpenAiToolResult {
394 pub fn success(call: &OpenAiToolCall, output: Expr) -> Self {
396 Self {
397 call_id: call.id.clone(),
398 name: call.name.clone(),
399 status: Symbol::new("ok"),
400 output,
401 }
402 }
403
404 pub fn invalid_arguments(call: &OpenAiToolCall, message: impl Into<String>) -> Self {
406 Self::failure(call, "invalid-arguments", message)
407 }
408
409 pub fn capability_denied(call: &OpenAiToolCall, message: impl Into<String>) -> Self {
411 Self::failure(call, "capability-denied", message)
412 }
413
414 pub fn unknown_tool(call: &OpenAiToolCall) -> Self {
416 Self::failure(call, "unknown-tool", format!("unknown tool {}", call.name))
417 }
418
419 fn failure(call: &OpenAiToolCall, status: &'static str, message: impl Into<String>) -> Self {
420 Self {
421 call_id: call.id.clone(),
422 name: call.name.clone(),
423 status: Symbol::new(status),
424 output: Expr::String(message.into()),
425 }
426 }
427
428 pub fn to_expr(&self) -> Expr {
430 Expr::Map(vec![
431 field("tool-call-id", Expr::String(self.call_id.clone())),
432 field("name", Expr::String(self.name.clone())),
433 field("status", Expr::Symbol(self.status.clone())),
434 field("output", self.output.clone()),
435 ])
436 }
437
438 pub fn message_text(&self) -> String {
440 format!(
441 "tool {} {}: {}",
442 self.name,
443 self.status.name,
444 expr_text(&self.output)
445 )
446 }
447}
448
449pub fn openai_name_to_symbol(name: &str) -> Symbol {
452 if let Some((namespace, local)) = name.split_once('_') {
453 Symbol::qualified(namespace.replace('_', "-"), local.replace('_', "-"))
454 } else {
455 Symbol::new(name.replace('_', "-"))
456 }
457}
458
459pub fn symbol_from_text(text: &str) -> Result<Symbol> {
462 if text.trim().is_empty() {
463 return Err(Error::Eval("SIM tool symbol must not be empty".to_owned()));
464 }
465 Ok(if let Some((namespace, name)) = text.split_once('/') {
466 if namespace.is_empty() || name.is_empty() {
467 return Err(Error::Eval(format!("invalid SIM symbol {text}")));
468 }
469 Symbol::qualified(namespace.to_owned(), name.to_owned())
470 } else {
471 Symbol::new(text.to_owned())
472 })
473}
474
475fn validate_openai_name(name: &str) -> Result<()> {
476 if name.is_empty() {
477 return Err(Error::Eval("OpenAI tool name must not be empty".to_owned()));
478 }
479 if name
480 .chars()
481 .all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-')
482 {
483 Ok(())
484 } else {
485 Err(Error::Eval(format!(
486 "OpenAI tool name {name} contains unsupported characters"
487 )))
488 }
489}
490
491fn string_member<'a>(object: &'a Map<String, Value>, name: &str) -> Result<&'a str> {
492 object
493 .get(name)
494 .and_then(Value::as_str)
495 .ok_or_else(|| Error::Eval(format!("openai tool missing string {name}")))
496}
497
498fn ensure_server_registered_tool(cx: &mut Cx, tool: &OpenAiTool) -> Result<()> {
499 let function = cx.resolve_function(tool.symbol())?;
500 if function.object().as_callable().is_some() {
501 Ok(())
502 } else {
503 Err(Error::TypeMismatch {
504 expected: "callable",
505 found: "non-callable",
506 })
507 }
508}
509
510fn reject_untrusted_authority_fields(object: &Map<String, Value>) -> Result<()> {
511 for field in ["x-sim-symbol", "x-sim-capabilities"] {
512 if object.contains_key(field) {
513 return Err(Error::Eval(format!(
514 "untrusted OpenAI tool descriptor cannot set {field}"
515 )));
516 }
517 }
518 Ok(())
519}
520
521fn required_string_field(entries: &[(Expr, Expr)], name: &str) -> Result<String> {
522 string_field(entries, name)
523 .ok_or_else(|| Error::Eval(format!("tool-call missing string {name}")))
524}
525
526fn string_field(entries: &[(Expr, Expr)], name: &str) -> Option<String> {
527 sim_value::access::entry_field(entries, name)
528 .and_then(sim_value::access::as_str)
529 .map(str::to_owned)
530}
531
532fn symbol_field(entries: &[(Expr, Expr)], name: &str) -> Option<String> {
533 match sim_value::access::entry_field(entries, name) {
534 Some(Expr::Symbol(value)) => Some(value.name.as_ref().to_owned()),
535 _ => None,
536 }
537}
538
539use sim_value::build::entry as field;