use std::collections::HashMap;
use std::sync::Arc;
use oxillama_runtime::tool_dispatch::{ToolDispatcher, ToolResult};
use serde_json::Value;
pub struct ServerToolDispatcher {
pub schema_map: HashMap<String, Value>,
}
impl ServerToolDispatcher {
pub fn new(schema_map: HashMap<String, Value>) -> Self {
Self { schema_map }
}
}
impl ToolDispatcher for ServerToolDispatcher {
fn invoke(&self, name: &str, args: &Value) -> ToolResult {
if self.schema_map.contains_key(name) {
tracing::info!(
tool_name = name,
args = %args,
"tool call received (placeholder dispatcher — no external invocation in v0.1.3)"
);
ToolResult::Ok(serde_json::json!({ "result": "tool_call_logged" }))
} else {
tracing::warn!(tool_name = name, "unknown tool called");
ToolResult::Err(format!("tool '{name}' is not registered on this server"))
}
}
}
pub fn build_tool_dispatcher(
tools: &[crate::routes::tools::Tool],
) -> Option<Arc<dyn ToolDispatcher>> {
if tools.is_empty() {
return None;
}
let schema_map: HashMap<String, Value> = tools
.iter()
.map(|t| (t.function.name.clone(), t.function.parameters.clone()))
.collect();
Some(Arc::new(ServerToolDispatcher::new(schema_map)))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn make_dispatcher() -> ServerToolDispatcher {
let mut map = HashMap::new();
map.insert(
"get_weather".to_string(),
json!({ "type": "object", "properties": { "location": { "type": "string" } } }),
);
ServerToolDispatcher::new(map)
}
#[test]
fn dispatcher_returns_placeholder_for_known_tool() {
let d = make_dispatcher();
let result = d.invoke("get_weather", &json!({"location": "Tokyo"}));
match result {
ToolResult::Ok(v) => {
assert_eq!(
v["result"].as_str(),
Some("tool_call_logged"),
"placeholder result mismatch: {v}"
);
}
ToolResult::Err(e) => panic!("expected Ok, got Err: {e}"),
}
}
#[test]
fn dispatcher_returns_error_for_unknown_tool() {
let d = make_dispatcher();
let result = d.invoke("unknown_tool", &json!({}));
match result {
ToolResult::Ok(v) => panic!("expected Err, got Ok: {v}"),
ToolResult::Err(msg) => assert!(
msg.contains("not registered"),
"error message should mention 'not registered': {msg}"
),
}
}
#[test]
fn build_tool_dispatcher_returns_none_for_empty_tools() {
let result = build_tool_dispatcher(&[]);
assert!(result.is_none(), "empty tools should yield None dispatcher");
}
#[test]
fn build_tool_dispatcher_returns_some_for_tools() {
use crate::routes::tools::{FunctionDef, Tool};
let tools = vec![Tool {
tool_type: "function".to_string(),
function: FunctionDef {
name: "get_weather".to_string(),
description: None,
parameters: json!({"type": "object"}),
},
}];
let result = build_tool_dispatcher(&tools);
assert!(
result.is_some(),
"non-empty tools should yield a dispatcher"
);
}
}