use serde_json::{Map, Value as JsonValue};
use sim_codec_json::{JsonProjectionMode, project_json_to_expr};
use sim_kernel::{Args, Cx, Error, Expr, Result, Shape, Symbol, Value};
use crate::registry::card_from_value;
use crate::{SkillCard, SkillRole};
pub(crate) fn openai_tool(cx: &mut Cx, args: Args) -> Result<Value> {
let card = card_arg(cx, args)?;
let descriptor = skill_openai_tool_descriptor(cx, &card)?;
cx.factory().expr(project_json_to_expr(
&descriptor,
JsonProjectionMode::UntaggedInterop,
))
}
pub(crate) fn openai_tools(cx: &mut Cx, args: Args) -> Result<Value> {
let cards = selected_cards(cx, args)?;
let descriptors = cards
.iter()
.map(|card| skill_openai_tool_descriptor(cx, card))
.collect::<Result<Vec<_>>>()?;
let values = descriptors
.iter()
.map(|descriptor| {
cx.factory().expr(project_json_to_expr(
descriptor,
JsonProjectionMode::UntaggedInterop,
))
})
.collect::<Result<Vec<_>>>()?;
cx.factory().list(values)
}
pub fn skill_openai_tool_descriptor(cx: &mut Cx, card: &SkillCard) -> Result<JsonValue> {
ensure_tool_role(card)?;
let name = {
let mangled = sim_lib_surface_card::external_name(
&card.symbol,
sim_lib_surface_card::ExternalNamePolicy::OpenAiTool,
);
if mangled.is_empty() {
"skill".to_owned()
} else {
mangled
}
};
let parameters = parameters_for_shape(cx, &card.input_shape)?;
let tool = sim_lib_openai_server::OpenAiTool::from_callable(
cx,
name,
card.symbol.clone(),
card.description.clone(),
parameters,
card.capabilities.clone(),
)?;
let descriptor = tool.descriptor_json();
sim_lib_openai_server::OpenAiTool::from_openai_descriptor(&descriptor)?;
Ok(descriptor)
}
pub fn insert_skill_openai_tools(
cx: &mut Cx,
registry: &mut sim_lib_openai_server::OpenAiToolRegistry,
cards: &[SkillCard],
) -> Result<()> {
for card in cards {
let descriptor = skill_openai_tool_descriptor(cx, card)?;
registry.insert(sim_lib_openai_server::OpenAiTool::from_openai_descriptor(
&descriptor,
)?)?;
}
Ok(())
}
pub fn skill_openai_tool_symbol() -> Symbol {
Symbol::qualified("skill", "openai-tool")
}
pub fn skill_openai_tools_symbol() -> Symbol {
Symbol::qualified("skill", "openai-tools")
}
fn selected_cards(cx: &mut Cx, args: Args) -> Result<Vec<SkillCard>> {
let values = args.into_vec();
if values.is_empty() {
let cards = crate::skill_registry(cx)?
.cards()?
.into_iter()
.filter(|card| card.roles.contains(&SkillRole::Tool))
.collect::<Vec<_>>();
return Ok(cards);
}
values
.into_iter()
.map(|value| card_from_target(cx, value))
.collect()
}
fn card_arg(cx: &mut Cx, args: Args) -> Result<SkillCard> {
let mut values = args.into_vec();
if values.len() != 1 {
return Err(Error::Eval(
"skill/openai-tool expects one skill id, symbol, or SkillCard value".to_owned(),
));
}
card_from_target(cx, values.remove(0))
}
fn card_from_target(cx: &mut Cx, value: Value) -> Result<SkillCard> {
if value.object().downcast_ref::<SkillCard>().is_some() {
return card_from_value(cx, &value);
}
match value.object().as_expr(cx)? {
Expr::Map(_) => card_from_value(cx, &value),
Expr::String(id) => crate::skill_registry(cx)?
.card_by_id(&id)?
.ok_or_else(|| Error::Eval(format!("unknown skill {id}"))),
Expr::Symbol(symbol) => crate::skill_registry(cx)?
.card_by_symbol(&symbol)?
.ok_or_else(|| Error::Eval(format!("unknown skill {symbol}"))),
_ => Err(Error::TypeMismatch {
expected: "skill id, symbol, or SkillCard",
found: "invalid target",
}),
}
}
fn ensure_tool_role(card: &SkillCard) -> Result<()> {
if card.roles.contains(&SkillRole::Tool) {
Ok(())
} else {
Err(Error::Eval(format!(
"skill {} is not marked with tool role",
card.id
)))
}
}
fn parameters_for_shape(cx: &mut Cx, shape: &Value) -> Result<JsonValue> {
let Some(shape) = shape.object().as_shape() else {
return Ok(empty_parameters());
};
let doc = shape.describe(cx)?;
if doc.name == "list shape" {
return Ok(list_parameters_from_doc(&doc.details));
}
let mut properties = Map::new();
properties.insert("value".to_owned(), schema_for_shape(cx, shape)?);
Ok(parameters_object(properties, vec!["value".to_owned()]))
}
fn list_parameters_from_doc(details: &[String]) -> JsonValue {
let mut properties = Map::new();
let mut required = Vec::new();
for (index, detail) in details
.iter()
.filter(|detail| *detail != "rest")
.enumerate()
{
let name = format!("arg{index}");
properties.insert(name.clone(), schema_for_doc_name(detail));
required.push(name);
}
parameters_object(properties, required)
}
fn parameters_object(properties: Map<String, JsonValue>, required: Vec<String>) -> JsonValue {
let mut object = Map::new();
object.insert("type".to_owned(), JsonValue::String("object".to_owned()));
object.insert("properties".to_owned(), JsonValue::Object(properties));
object.insert(
"required".to_owned(),
JsonValue::Array(required.into_iter().map(JsonValue::String).collect()),
);
JsonValue::Object(object)
}
fn empty_parameters() -> JsonValue {
parameters_object(Map::new(), Vec::new())
}
fn schema_for_shape(cx: &mut Cx, shape: &dyn Shape) -> Result<JsonValue> {
let kind = shape
.symbol()
.and_then(|symbol| json_type_for_symbol(&symbol))
.or_else(|| json_type_for_doc_name(&shape.describe(cx).ok()?.name));
Ok(match kind {
Some(kind) => serde_json::json!({ "type": kind }),
None => JsonValue::Object(Map::new()),
})
}
fn json_type_for_symbol(symbol: &Symbol) -> Option<&'static str> {
match (symbol.namespace.as_deref(), symbol.name.as_ref()) {
(Some("core"), "Number") => Some("number"),
(Some("core"), "String") => Some("string"),
(Some("core"), "Bool") => Some("boolean"),
_ => None,
}
}
fn json_type_for_doc_name(name: &str) -> Option<&'static str> {
if name.contains("number") {
Some("number")
} else if name.contains("string") {
Some("string")
} else if name.contains("bool") {
Some("boolean")
} else {
None
}
}
fn schema_for_doc_name(name: &str) -> JsonValue {
match json_type_for_doc_name(name) {
Some(kind) => serde_json::json!({ "type": kind }),
None => JsonValue::Object(Map::new()),
}
}
#[cfg(test)]
mod tests;