use std::collections::HashMap;
use indexmap::IndexMap;
use crate::{
error::{EvalError, InterpreterError},
eval::functions::resolve_proxy,
state::InterpreterState,
tools::{Tools, lazy_proxy::LazyProxy},
value::Value,
};
const BUILTIN_NAMES: &[&str] = &[
"print",
"len",
"range",
"str",
"int",
"float",
"bool",
"type",
"isinstance",
"issubclass",
"super",
"hasattr",
"callable",
"abs",
"round",
"min",
"max",
"sum",
"all",
"any",
"sorted",
"enumerate",
"zip",
"reversed",
"chr",
"ord",
"list",
"tuple",
"dict",
"set",
"iter",
"next",
"filter",
"map",
"repr",
"hash",
"id",
"input",
"object",
];
#[must_use]
pub fn is_builtin_name(name: &str) -> bool {
BUILTIN_NAMES.contains(&name)
}
pub struct ToolCallDescriptor<'a> {
pub name: &'a str,
pub args: &'a [Value],
pub kwargs: &'a IndexMap<String, Value>,
}
pub async fn resolve_and_dispatch(
state: &InterpreterState,
call: ToolCallDescriptor<'_>,
tools: &Tools,
) -> Result<Option<Value>, EvalError> {
let ToolCallDescriptor { name, args, kwargs } = call;
if is_builtin_name(name) {
return Ok(None);
}
let Some(tool_config) = tools.get(name) else { return Ok(None) };
let mut resolved_args: Vec<Value> = Vec::with_capacity(args.len());
for arg in args {
resolved_args.push(resolve_proxy(arg).await?);
}
let mut resolved_kwargs: HashMap<String, Value> = HashMap::new();
for (k, v) in kwargs {
resolved_kwargs.insert(k.clone(), resolve_proxy(v).await?);
}
let mut tool_kwargs: HashMap<String, Value> = HashMap::new();
for (i, arg) in resolved_args.iter().enumerate() {
tool_kwargs.insert(format!("arg{i}"), arg.clone());
}
for (k, v) in &resolved_kwargs {
tool_kwargs.insert(k.clone(), v.clone());
}
let tool_timeout = remaining_tool_timeout(state);
if tool_config.parallelizable {
let semaphore = state.tool_semaphore.clone();
let handler = tool_config.handler.clone();
let tool_name = name.to_string();
let handle = tokio::spawn(async move {
let _permit = semaphore.acquire_owned().await;
call_handler(handler.as_ref(), tool_kwargs, tool_timeout).await
});
return Ok(Some(Value::LazyProxy(LazyProxy::new(handle, tool_name))));
}
match call_handler(tool_config.handler.as_ref(), tool_kwargs, tool_timeout).await {
Ok(val) => Ok(Some(val)),
Err(e) => {
Err(InterpreterError::Tool { tool_name: name.to_string(), message: e.message }.into())
}
}
}
fn remaining_tool_timeout(state: &InterpreterState) -> Option<std::time::Duration> {
let max = state.config.max_execution_time?;
Some(max.saturating_sub(state.execution_start.elapsed()))
}
async fn call_handler(
handler: &dyn crate::tools::ToolHandler,
tool_kwargs: HashMap<String, Value>,
timeout: Option<std::time::Duration>,
) -> Result<Value, crate::tools::ToolError> {
use crate::tools::ToolError;
match timeout {
Some(d) if d.is_zero() => {
Err(ToolError::new("tool timed out (no remaining execution budget)"))
}
Some(d) => match tokio::time::timeout(d, handler.call(tool_kwargs)).await {
Ok(r) => r,
Err(_) => Err(ToolError::new(format!(
"tool timed out after {d:?} (max_execution_time budget)"
))),
},
None => handler.call(tool_kwargs).await,
}
}