use std::collections::{BTreeSet, HashMap, HashSet};
use dynamo_protocols::types::responses::{
InputItem, InputParam, Item, NamespaceToolParamTool, Tool,
};
use super::ResponsesConversionError;
#[derive(Clone, Debug, Default)]
pub struct ToolNameMap {
by_identity: HashMap<(Option<String>, String), String>,
by_alias: HashMap<String, (Option<String>, String)>,
available: BTreeSet<(Option<String>, String)>,
}
impl ToolNameMap {
pub fn new(tools: &[Tool], input: Option<&InputParam>) -> Self {
let mut identities = BTreeSet::new();
for tool in tools {
match tool {
Tool::Function(function) => {
identities.insert((None, function.name.clone()));
}
Tool::Namespace(namespace) => {
for tool in &namespace.tools {
if let NamespaceToolParamTool::Function(function) = tool {
identities
.insert((Some(namespace.name.clone()), function.name.clone()));
}
}
}
_ => {}
}
}
let available = identities.clone();
if let Some(InputParam::Items(items)) = input {
for item in items {
if let InputItem::Item(Item::FunctionCall(call)) = item {
identities.insert((call.namespace.clone(), call.name.clone()));
}
}
}
let mut counts = HashMap::new();
for (_, name) in &identities {
*counts.entry(name.as_str()).or_insert(0) += 1;
}
let mut reserved: HashSet<String> = counts.keys().map(|name| (*name).to_owned()).collect();
let mut result = Self {
available,
..Self::default()
};
let mut next_alias = 0;
for identity in &identities {
let name = &identity.1;
let alias = if counts[name.as_str()] == 1 {
name.clone()
} else {
let qualified = format!("{}__{name}", identity.0.as_deref().unwrap_or("function"));
let stem: String = qualified
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '_' || c == '-' {
c
} else {
'_'
}
})
.take(48)
.collect();
let mut candidate = stem.clone();
while !reserved.insert(candidate.clone()) {
candidate = format!("{stem}_{next_alias}");
next_alias += 1;
}
candidate
};
result.by_identity.insert(identity.clone(), alias.clone());
result.by_alias.insert(alias, identity.clone());
}
result
}
pub(super) fn encode(&self, namespace: Option<&str>, name: &str) -> String {
self.by_identity
.get(&(namespace.map(str::to_owned), name.to_owned()))
.cloned()
.unwrap_or_else(|| name.to_owned())
}
pub(super) fn decode<'a>(&'a self, alias: &'a str) -> (Option<&'a str>, &'a str) {
self.by_alias
.get(alias)
.map(|(namespace, name)| (namespace.as_deref(), name.as_str()))
.unwrap_or((None, alias))
}
pub(super) fn resolve_choice(
&self,
namespace: Option<&str>,
name: &str,
) -> anyhow::Result<String> {
let identity = (namespace.map(str::to_owned), name.to_owned());
if self.available.contains(&identity) {
return Ok(self.encode(namespace, name));
}
if namespace.is_none() {
let mut matches = self
.available
.iter()
.filter(|(_, candidate)| candidate == name);
if let Some((namespace, name)) = matches.next() {
if matches.next().is_none() {
return Ok(self.encode(namespace.as_deref(), name));
}
return Err(ResponsesConversionError::InvalidArgument(format!(
"Responses tool_choice function '{name}' is ambiguous without a namespace"
))
.into());
}
}
if namespace.is_none() && !self.by_alias.contains_key(name) {
return Ok(name.to_owned());
}
Err(ResponsesConversionError::InvalidArgument(format!(
"Responses tool_choice references unknown function '{name}'"
))
.into())
}
}