use std::sync::Arc;
use async_trait::async_trait;
use smooth_operator_core::Tool;
use crate::access_control::AccessContext;
#[derive(Debug, Clone, Default)]
pub struct ToolProviderContext {
pub org_id: Option<String>,
pub access: AccessContext,
pub conversation_id: Option<String>,
pub gateway_key: Option<String>,
}
impl ToolProviderContext {
#[must_use]
pub fn new(org_id: Option<String>, access: AccessContext) -> Self {
Self {
org_id,
access,
conversation_id: None,
gateway_key: None,
}
}
#[must_use]
pub fn with_conversation_id(mut self, conversation_id: impl Into<String>) -> Self {
self.conversation_id = Some(conversation_id.into());
self
}
#[must_use]
pub fn with_gateway_key(mut self, gateway_key: impl Into<String>) -> Self {
self.gateway_key = Some(gateway_key.into());
self
}
}
#[async_trait]
pub trait ToolProvider: Send + Sync {
async fn tools_for(&self, ctx: &ToolProviderContext) -> Vec<Arc<dyn Tool>>;
}
#[cfg(test)]
mod tests {
use super::*;
use smooth_operator_core::{ToolRegistry, ToolSchema};
struct StubTool {
name: String,
}
#[async_trait]
impl Tool for StubTool {
fn schema(&self) -> ToolSchema {
ToolSchema {
name: self.name.clone(),
description: "stub".into(),
parameters: serde_json::json!({"type": "object"}),
}
}
async fn execute(&self, _arguments: serde_json::Value) -> anyhow::Result<String> {
Ok("ok".into())
}
}
struct StubProvider {
names: Vec<String>,
}
#[async_trait]
impl ToolProvider for StubProvider {
async fn tools_for(&self, _ctx: &ToolProviderContext) -> Vec<Arc<dyn Tool>> {
self.names
.iter()
.map(|n| Arc::new(StubTool { name: n.clone() }) as Arc<dyn Tool>)
.collect()
}
}
#[tokio::test]
async fn provider_tools_register_into_registry() {
let provider = StubProvider {
names: vec!["crm_lookup".into(), "open_ticket".into()],
};
let ctx = ToolProviderContext::new(Some("org-a".into()), AccessContext::anonymous());
let mut registry = ToolRegistry::new();
for tool in provider.tools_for(&ctx).await {
registry.register_arc(tool);
}
assert!(registry.has_tool("crm_lookup"));
assert!(registry.has_tool("open_ticket"));
}
#[test]
fn new_defaults_per_turn_handles_to_none() {
let ctx = ToolProviderContext::new(Some("org-a".into()), AccessContext::anonymous());
assert_eq!(ctx.conversation_id, None);
assert_eq!(ctx.gateway_key, None);
}
#[test]
fn builder_sets_conversation_id_and_gateway_key() {
let ctx = ToolProviderContext::new(Some("org-a".into()), AccessContext::anonymous())
.with_conversation_id("conv-123")
.with_gateway_key("sk-org-a");
assert_eq!(ctx.conversation_id.as_deref(), Some("conv-123"));
assert_eq!(ctx.gateway_key.as_deref(), Some("sk-org-a"));
}
#[tokio::test]
async fn empty_provider_leaves_registry_unchanged() {
let provider = StubProvider { names: vec![] };
let ctx = ToolProviderContext::default();
let mut registry = ToolRegistry::new();
let before = registry.schemas().len();
for tool in provider.tools_for(&ctx).await {
registry.register_arc(tool);
}
assert_eq!(registry.schemas().len(), before);
}
}