use sema_core::{Env, SemaError, Value};
use sema_otel::AttrValue;
fn map_entries(v: &Value) -> Vec<(String, Value)> {
match v.as_map_rc() {
Some(m) => m
.iter()
.filter_map(|(k, val)| {
let key = k
.as_keyword()
.or_else(|| k.as_str().map(|s| s.to_string()))?;
Some((key, val.clone()))
})
.collect(),
None => Vec::new(),
}
}
fn attr_value(v: &Value) -> AttrValue {
if let Some(b) = v.as_bool() {
AttrValue::Bool(b)
} else if let Some(i) = v.as_int() {
AttrValue::Int(i)
} else if let Some(f) = v.as_float() {
AttrValue::Float(f)
} else if let Some(s) = v.as_str() {
AttrValue::Str(s.to_string())
} else {
AttrValue::Str(v.to_string())
}
}
fn parse_attrs(v: Option<&Value>) -> Vec<(String, AttrValue)> {
match v {
Some(m) => map_entries(m)
.into_iter()
.map(|(k, val)| (k, attr_value(&val)))
.collect(),
None => Vec::new(),
}
}
fn as_name(v: &Value) -> Option<String> {
v.as_keyword().or_else(|| v.as_str().map(|s| s.to_string()))
}
fn run_in_span(span: sema_otel::VmSpan, thunk: &Value) -> Result<Value, SemaError> {
let result = crate::list::call_function(thunk, &[]);
if let Err(e) = &result {
sema_otel::set_current_status(Some(&e.to_string()));
}
drop(span);
result
}
pub fn register(env: &Env) {
crate::register_fn(env, "otel/span", |args| {
if args.len() < 2 || args.len() > 3 {
return Err(SemaError::arity("otel/span", "2-3", args.len()));
}
let name = args[0]
.as_str()
.ok_or_else(|| SemaError::type_error("string", args[0].type_name()))?;
let span = sema_otel::user_span(
name,
sema_otel::SemaSpanKind::Internal,
parse_attrs(args.get(2)),
);
run_in_span(span, &args[1])
});
crate::register_fn(env, "otel/llm-span", |args| {
if args.len() != 2 {
return Err(SemaError::arity("otel/llm-span", "2", args.len()));
}
let (mut model, mut provider, mut operation) =
(String::new(), String::new(), String::new());
let mut attrs = Vec::new();
for (k, val) in map_entries(&args[0]) {
match k.as_str() {
"model" => model = val.as_str().map(|s| s.to_string()).unwrap_or_default(),
"provider" => provider = val.as_str().map(|s| s.to_string()).unwrap_or_default(),
"operation" => operation = val.as_str().map(|s| s.to_string()).unwrap_or_default(),
_ => attrs.push((k, attr_value(&val))),
}
}
let span = sema_otel::user_llm_span(&model, &provider, &operation, attrs);
run_in_span(span, &args[1])
});
crate::register_fn(env, "otel/tool-span", |args| {
if args.len() < 2 || args.len() > 3 {
return Err(SemaError::arity("otel/tool-span", "2-3", args.len()));
}
let name = args[0]
.as_str()
.ok_or_else(|| SemaError::type_error("string", args[0].type_name()))?;
let span = sema_otel::user_span(
name,
sema_otel::SemaSpanKind::Tool,
parse_attrs(args.get(2)),
);
run_in_span(span, &args[1])
});
crate::register_fn(env, "otel/retrieval-span", |args| {
if args.len() < 2 || args.len() > 3 {
return Err(SemaError::arity("otel/retrieval-span", "2-3", args.len()));
}
let name = args[0]
.as_str()
.ok_or_else(|| SemaError::type_error("string", args[0].type_name()))?;
let span = sema_otel::user_span(
name,
sema_otel::SemaSpanKind::Retrieval,
parse_attrs(args.get(2)),
);
run_in_span(span, &args[1])
});
crate::register_fn(env, "otel/set-attribute", |args| {
if args.len() != 2 {
return Err(SemaError::arity("otel/set-attribute", "2", args.len()));
}
let key = as_name(&args[0])
.ok_or_else(|| SemaError::type_error("keyword or string", args[0].type_name()))?;
sema_otel::set_current_attr(&key, attr_value(&args[1]));
Ok(Value::nil())
});
crate::register_fn(env, "otel/set-attributes", |args| {
if args.len() != 1 {
return Err(SemaError::arity("otel/set-attributes", "1", args.len()));
}
sema_otel::set_current_attrs(parse_attrs(Some(&args[0])));
Ok(Value::nil())
});
crate::register_fn(env, "otel/set-status", |args| {
if args.is_empty() || args.len() > 2 {
return Err(SemaError::arity("otel/set-status", "1-2", args.len()));
}
let status = as_name(&args[0])
.ok_or_else(|| SemaError::type_error("keyword or string", args[0].type_name()))?;
if status == "error" {
let msg = args.get(1).and_then(|v| v.as_str()).unwrap_or("error");
sema_otel::set_current_status(Some(msg));
} else {
sema_otel::set_current_status(None);
}
Ok(Value::nil())
});
crate::register_fn(env, "otel/llm-usage", |args| {
if args.len() != 1 {
return Err(SemaError::arity("otel/llm-usage", "1", args.len()));
}
let (mut input, mut output, mut cost) = (0u32, 0u32, None);
for (k, val) in map_entries(&args[0]) {
match k.as_str() {
"input-tokens" => input = val.as_int().unwrap_or(0).max(0) as u32,
"output-tokens" => output = val.as_int().unwrap_or(0).max(0) as u32,
"cost-usd" => cost = val.as_float(),
_ => {}
}
}
sema_otel::set_current_llm_usage(input, output, cost);
Ok(Value::nil())
});
crate::register_fn(env, "otel/with-session", |args| {
if args.len() < 2 || args.len() > 3 {
return Err(SemaError::arity("otel/with-session", "2-3", args.len()));
}
let session = args[0]
.as_str()
.ok_or_else(|| SemaError::type_error("string", args[0].type_name()))?;
let (user, thunk) = if args.len() == 3 {
let user = map_entries(&args[1])
.into_iter()
.find(|(k, _)| k == "user")
.and_then(|(_, v)| v.as_str().map(|s| s.to_string()));
(user, &args[2])
} else {
(None, &args[1])
};
let guard = sema_otel::set_conversation_scope(session, Some(session), user.as_deref());
let result = crate::list::call_function(thunk, &[]);
drop(guard);
result
});
crate::register_fn(env, "otel/event", |args| {
if args.is_empty() || args.len() > 2 {
return Err(SemaError::arity("otel/event", "1-2", args.len()));
}
let name = args[0]
.as_str()
.ok_or_else(|| SemaError::type_error("string", args[0].type_name()))?;
let attrs: Vec<(String, String)> = match args.get(1).and_then(|v| v.as_map_rc()) {
Some(m) => m
.iter()
.filter_map(|(k, v)| {
let key = k
.as_keyword()
.or_else(|| k.as_str().map(|s| s.to_string()))?;
let val = v
.as_str()
.map(|s| s.to_string())
.unwrap_or_else(|| v.to_string());
Some((key, val))
})
.collect(),
None => Vec::new(),
};
sema_otel::add_event(name, attrs);
Ok(Value::nil())
});
}