use std::collections::BTreeMap;
use serde_json::{Map, Value};
use sim_kernel::{CapabilityName, Cx, Error, Expr, Result, Symbol};
use super::tool_schema::{
argument_order, arguments_json, canonical_json, default_parameters, expr_text,
expr_to_json_string, json_to_expr, json_to_value, validate_schema,
};
#[derive(Clone, Debug, PartialEq)]
pub struct OpenAiTool {
openai_name: String,
symbol: Symbol,
description: String,
parameters: Value,
arg_order: Vec<String>,
capabilities: Vec<CapabilityName>,
args_shape: Option<Expr>,
result_shape: Option<Expr>,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct OpenAiToolRegistry {
tools: BTreeMap<String, OpenAiTool>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct OpenAiToolCall {
pub id: String,
pub name: String,
pub arguments: Value,
}
#[derive(Clone, Debug, PartialEq)]
pub struct OpenAiToolResult {
pub call_id: String,
pub name: String,
pub status: Symbol,
pub output: Expr,
}
impl OpenAiTool {
pub fn from_callable(
cx: &mut Cx,
openai_name: impl Into<String>,
symbol: Symbol,
description: impl Into<String>,
parameters: Value,
capabilities: Vec<CapabilityName>,
) -> Result<Self> {
let function = cx.resolve_function(&symbol)?;
let Some(callable) = function.object().as_callable() else {
return Err(Error::TypeMismatch {
expected: "callable",
found: "non-callable",
});
};
let args_shape = callable
.browse_args_shape(cx)?
.map(|shape| shape.object().as_expr(cx))
.transpose()?;
let result_shape = callable
.browse_result_shape(cx)?
.map(|shape| shape.object().as_expr(cx))
.transpose()?;
Ok(Self::new(
openai_name,
symbol,
description,
parameters,
capabilities,
args_shape,
result_shape,
))
}
pub fn from_openai_descriptor(value: &Value) -> Result<Self> {
let object = value
.as_object()
.ok_or_else(|| Error::Eval("openai tool descriptor must be an object".to_owned()))?;
let kind = object
.get("type")
.and_then(Value::as_str)
.unwrap_or("function");
if kind != "function" {
return Err(Error::Eval(format!(
"unsupported OpenAI tool descriptor type {kind}"
)));
}
let function = object
.get("function")
.and_then(Value::as_object)
.ok_or_else(|| Error::Eval("openai tool missing function object".to_owned()))?;
let openai_name = string_member(function, "name")?.to_owned();
validate_openai_name(&openai_name)?;
let description = function
.get("description")
.and_then(Value::as_str)
.unwrap_or("")
.to_owned();
let parameters = function
.get("parameters")
.cloned()
.unwrap_or_else(default_parameters);
let symbol = function
.get("x-sim-symbol")
.or_else(|| object.get("x-sim-symbol"))
.and_then(Value::as_str)
.map(symbol_from_text)
.transpose()?
.unwrap_or_else(|| openai_name_to_symbol(&openai_name));
let capabilities = capabilities_from(function)
.or_else(|| capabilities_from(object))
.unwrap_or_default();
Ok(Self::new(
openai_name,
symbol,
description,
parameters,
capabilities,
None,
None,
))
}
fn new(
openai_name: impl Into<String>,
symbol: Symbol,
description: impl Into<String>,
parameters: Value,
capabilities: Vec<CapabilityName>,
args_shape: Option<Expr>,
result_shape: Option<Expr>,
) -> Self {
let arg_order = argument_order(¶meters);
Self {
openai_name: openai_name.into(),
symbol,
description: description.into(),
parameters,
arg_order,
capabilities,
args_shape,
result_shape,
}
}
pub fn openai_name(&self) -> &str {
&self.openai_name
}
pub fn symbol(&self) -> &Symbol {
&self.symbol
}
pub fn capabilities(&self) -> &[CapabilityName] {
&self.capabilities
}
pub fn validate_arguments(&self, arguments: &Value) -> std::result::Result<(), String> {
validate_schema(&self.parameters, arguments, "arguments")
}
pub fn argument_values(
&self,
cx: &mut Cx,
arguments: &Value,
) -> std::result::Result<Vec<sim_kernel::Value>, String> {
self.validate_arguments(arguments)?;
let Some(object) = arguments.as_object() else {
return Err("tool arguments must be an object".to_owned());
};
self.arg_order
.iter()
.filter_map(|name| object.get(name))
.map(|value| json_to_value(cx, value).map_err(|err| err.to_string()))
.collect()
}
pub fn descriptor_json(&self) -> Value {
let mut function = Map::new();
function.insert("name".to_owned(), Value::String(self.openai_name.clone()));
function.insert(
"description".to_owned(),
Value::String(self.description.clone()),
);
function.insert("parameters".to_owned(), self.parameters.clone());
function.insert(
"x-sim-symbol".to_owned(),
Value::String(self.symbol.as_qualified_str()),
);
if let Some(shape) = &self.args_shape {
function.insert("x-sim-args-shape".to_owned(), expr_to_json_string(shape));
}
if let Some(shape) = &self.result_shape {
function.insert("x-sim-result-shape".to_owned(), expr_to_json_string(shape));
}
if !self.capabilities.is_empty() {
function.insert(
"x-sim-capabilities".to_owned(),
Value::Array(
self.capabilities
.iter()
.map(|capability| Value::String(capability.as_str().to_owned()))
.collect(),
),
);
}
let mut descriptor = Map::new();
descriptor.insert("type".to_owned(), Value::String("function".to_owned()));
descriptor.insert("function".to_owned(), Value::Object(function));
Value::Object(descriptor)
}
pub fn to_expr(&self) -> Expr {
Expr::Map(vec![
field("kind", Expr::Symbol(Symbol::new("openai-gateway/tool"))),
field("openai-name", Expr::String(self.openai_name.clone())),
field("symbol", Expr::Symbol(self.symbol.clone())),
field("description", Expr::String(self.description.clone())),
field("parameters", json_to_expr(&self.parameters)),
field(
"capabilities",
Expr::List(
self.capabilities
.iter()
.map(|capability| Expr::String(capability.as_str().to_owned()))
.collect(),
),
),
field("args-shape", self.args_shape.clone().unwrap_or(Expr::Nil)),
field(
"result-shape",
self.result_shape.clone().unwrap_or(Expr::Nil),
),
])
}
}
impl OpenAiToolRegistry {
pub fn from_request(object: &Map<String, Value>) -> Result<Self> {
let Some(tools) = object.get("tools") else {
return Ok(Self::default());
};
if tools.is_null() {
return Ok(Self::default());
}
let tools = tools
.as_array()
.ok_or_else(|| Error::Eval("openai tools field must be an array".to_owned()))?;
let mut registry = Self::default();
for tool in tools {
registry.insert(OpenAiTool::from_openai_descriptor(tool)?)?;
}
Ok(registry)
}
pub fn insert(&mut self, tool: OpenAiTool) -> Result<()> {
if self.tools.contains_key(tool.openai_name()) {
return Err(Error::Eval(format!(
"duplicate OpenAI tool name {}",
tool.openai_name()
)));
}
self.tools.insert(tool.openai_name.clone(), tool);
Ok(())
}
pub fn is_empty(&self) -> bool {
self.tools.is_empty()
}
pub fn get(&self, name: &str) -> Option<&OpenAiTool> {
self.tools.get(name)
}
pub fn descriptor_json(&self) -> Value {
Value::Array(
self.tools
.values()
.map(OpenAiTool::descriptor_json)
.collect(),
)
}
pub fn to_expr(&self) -> Expr {
Expr::List(self.tools.values().map(OpenAiTool::to_expr).collect())
}
}
impl OpenAiToolCall {
pub fn from_content_part(part: &Expr) -> Result<Option<Self>> {
let Expr::Map(entries) = part else {
return Ok(None);
};
if symbol_field(entries, "type").as_deref() != Some("tool-call") {
return Ok(None);
}
let name = required_string_field(entries, "name")?;
let id = string_field(entries, "id").unwrap_or_else(|| format!("call_{name}"));
let arguments = entries
.iter()
.find_map(|(key, value)| match key {
Expr::Symbol(symbol)
if symbol.namespace.is_none() && symbol.name.as_ref() == "arguments" =>
{
Some(arguments_json(value))
}
_ => None,
})
.transpose()?
.unwrap_or_else(|| Value::Object(Map::new()));
Ok(Some(Self {
id,
name,
arguments,
}))
}
pub fn to_expr(&self) -> Expr {
Expr::Map(vec![
field("id", Expr::String(self.id.clone())),
field("name", Expr::String(self.name.clone())),
field("arguments", json_to_expr(&self.arguments)),
])
}
pub fn fingerprint(&self) -> String {
format!("{}:{}", self.name, canonical_json(&self.arguments))
}
}
impl OpenAiToolResult {
pub fn success(call: &OpenAiToolCall, output: Expr) -> Self {
Self {
call_id: call.id.clone(),
name: call.name.clone(),
status: Symbol::new("ok"),
output,
}
}
pub fn invalid_arguments(call: &OpenAiToolCall, message: impl Into<String>) -> Self {
Self::failure(call, "invalid-arguments", message)
}
pub fn capability_denied(call: &OpenAiToolCall, message: impl Into<String>) -> Self {
Self::failure(call, "capability-denied", message)
}
pub fn unknown_tool(call: &OpenAiToolCall) -> Self {
Self::failure(call, "unknown-tool", format!("unknown tool {}", call.name))
}
fn failure(call: &OpenAiToolCall, status: &'static str, message: impl Into<String>) -> Self {
Self {
call_id: call.id.clone(),
name: call.name.clone(),
status: Symbol::new(status),
output: Expr::String(message.into()),
}
}
pub fn to_expr(&self) -> Expr {
Expr::Map(vec![
field("tool-call-id", Expr::String(self.call_id.clone())),
field("name", Expr::String(self.name.clone())),
field("status", Expr::Symbol(self.status.clone())),
field("output", self.output.clone()),
])
}
pub fn message_text(&self) -> String {
format!(
"tool {} {}: {}",
self.name,
self.status.name,
expr_text(&self.output)
)
}
}
pub fn openai_name_to_symbol(name: &str) -> Symbol {
if let Some((namespace, local)) = name.split_once('_') {
Symbol::qualified(namespace.replace('_', "-"), local.replace('_', "-"))
} else {
Symbol::new(name.replace('_', "-"))
}
}
pub fn symbol_from_text(text: &str) -> Result<Symbol> {
if text.trim().is_empty() {
return Err(Error::Eval("SIM tool symbol must not be empty".to_owned()));
}
Ok(if let Some((namespace, name)) = text.split_once('/') {
if namespace.is_empty() || name.is_empty() {
return Err(Error::Eval(format!("invalid SIM symbol {text}")));
}
Symbol::qualified(namespace.to_owned(), name.to_owned())
} else {
Symbol::new(text.to_owned())
})
}
fn validate_openai_name(name: &str) -> Result<()> {
if name.is_empty() {
return Err(Error::Eval("OpenAI tool name must not be empty".to_owned()));
}
if name
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-')
{
Ok(())
} else {
Err(Error::Eval(format!(
"OpenAI tool name {name} contains unsupported characters"
)))
}
}
fn string_member<'a>(object: &'a Map<String, Value>, name: &str) -> Result<&'a str> {
object
.get(name)
.and_then(Value::as_str)
.ok_or_else(|| Error::Eval(format!("openai tool missing string {name}")))
}
fn capabilities_from(object: &Map<String, Value>) -> Option<Vec<CapabilityName>> {
let values = object.get("x-sim-capabilities")?.as_array()?;
Some(
values
.iter()
.filter_map(Value::as_str)
.map(|value| CapabilityName::new(value.to_owned()))
.collect(),
)
}
fn required_string_field(entries: &[(Expr, Expr)], name: &str) -> Result<String> {
string_field(entries, name)
.ok_or_else(|| Error::Eval(format!("tool-call missing string {name}")))
}
fn string_field(entries: &[(Expr, Expr)], name: &str) -> Option<String> {
sim_value::access::entry_field(entries, name)
.and_then(sim_value::access::as_str)
.map(str::to_owned)
}
fn symbol_field(entries: &[(Expr, Expr)], name: &str) -> Option<String> {
match sim_value::access::entry_field(entries, name) {
Some(Expr::Symbol(value)) => Some(value.name.as_ref().to_owned()),
_ => None,
}
}
use sim_value::build::entry as field;