descry-tool-core 0.3.1

Core traits and types for descry-tool framework
Documentation
//! Anthropic (Claude) API adapter
//!
//! Converts tools to Anthropic tool use format.

use serde::{Deserialize, Serialize};

use crate::adapters::ToolAdapter;
use crate::ToolMeta;

/// Anthropic tool specification
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnthropicTool {
    /// Tool name
    pub name: String,
    /// Tool description
    pub description: String,
    /// Input schema (JSON Schema)
    pub input_schema: serde_json::Value,
}

/// Anthropic tool use (in request)
#[derive(Debug, Clone, Deserialize)]
pub struct AnthropicToolUse {
    /// Type (always "tool_use")
    #[serde(rename = "type")]
    pub use_type: String,
    /// Tool use ID
    pub id: String,
    /// Tool name
    pub name: String,
    /// Tool input
    pub input: serde_json::Value,
}

/// Anthropic call request (wrapper for tool use)
#[derive(Debug, Clone, Deserialize)]
pub struct AnthropicCallRequest {
    /// Tool use
    pub tool_use: AnthropicToolUse,
}

/// Anthropic tool result (in response)
#[derive(Debug, Clone, Serialize)]
pub struct AnthropicToolResult {
    /// Type (always "tool_result")
    #[serde(rename = "type")]
    pub result_type: String,
    /// Tool use ID
    pub tool_use_id: String,
    /// Tool output content
    pub content: String,
    /// Whether the tool call resulted in an error
    #[serde(skip_serializing_if = "Option::is_none")]
    pub is_error: Option<bool>,
}

/// Anthropic adapter
pub struct AnthropicAdapter;

impl ToolAdapter for AnthropicAdapter {
    type ToolSpec = AnthropicTool;
    type CallRequest = AnthropicCallRequest;
    type CallResponse = AnthropicToolResult;

    fn to_spec(meta: &ToolMeta) -> AnthropicTool {
        AnthropicTool {
            name: meta.name.to_string(),
            description: meta.description.to_string(),
            input_schema: (meta.schema)().clone(),
        }
    }

    fn from_request(req: AnthropicCallRequest) -> Result<(String, serde_json::Value), crate::ToolError> {
        Ok((req.tool_use.name, req.tool_use.input))
    }

    fn to_response(output: serde_json::Value) -> AnthropicToolResult {
        AnthropicToolResult {
            result_type: "tool_result".to_string(),
            tool_use_id: String::new(), // Should be set by caller
            content: serde_json::to_string(&output).unwrap_or_else(|_| output.to_string()),
            is_error: None,
        }
    }
}

impl AnthropicAdapter {
    /// Create response with tool use ID
    pub fn to_response_with_id(output: serde_json::Value, tool_use_id: String) -> AnthropicToolResult {
        AnthropicToolResult {
            result_type: "tool_result".to_string(),
            tool_use_id,
            content: serde_json::to_string(&output).unwrap_or_else(|_| output.to_string()),
            is_error: None,
        }
    }

    /// Create error response
    pub fn error_response(error_message: String, tool_use_id: String) -> AnthropicToolResult {
        AnthropicToolResult {
            result_type: "tool_result".to_string(),
            tool_use_id,
            content: error_message,
            is_error: Some(true),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{Tool, ToolContext, ToolError};
    use schemars::JsonSchema;
    use serde::{Deserialize, Serialize};
    use std::sync::Arc;

    #[derive(Deserialize, JsonSchema)]
    struct TestParams {
        value: i32,
    }

    #[derive(Serialize, JsonSchema)]
    struct TestOutput {
        result: i32,
    }

    struct TestTool;

    impl Tool for TestTool {
        type Params = TestParams;
        type Output = TestOutput;
        const NAME: &'static str = "test_anthropic";
        const DESCRIPTION: &'static str = "Test tool for Anthropic";

        async fn call(
            _ctx: Arc<ToolContext>,
            params: Self::Params,
        ) -> Result<Self::Output, ToolError> {
            Ok(TestOutput {
                result: params.value * 3,
            })
        }
    }

    inventory::submit! {
        crate::ToolMeta {
            name: TestTool::NAME,
            description: TestTool::DESCRIPTION,
            call: |ctx, params| {
                Box::pin(async move {
                    let params: TestParams = serde_json::from_value(params)?;
                    let result = <TestTool as Tool>::call(ctx, params).await?;
                    Ok(serde_json::to_value(result)?)
                })
            },
            schema: || <TestTool as Tool>::schema(),
            examples: || <TestTool as Tool>::EXAMPLES,
        }
    }

    #[test]
    fn test_to_spec() {
        let meta = crate::find_tool("test_anthropic").unwrap();
        let spec = AnthropicAdapter::to_spec(meta);
        
        assert_eq!(spec.name, "test_anthropic");
        assert_eq!(spec.description, "Test tool for Anthropic");
        assert!(spec.input_schema.is_object());
    }

    #[test]
    fn test_from_request() {
        let req = AnthropicCallRequest {
            tool_use: AnthropicToolUse {
                use_type: "tool_use".to_string(),
                id: "toolu_123".to_string(),
                name: "test_anthropic".to_string(),
                input: serde_json::json!({"value": 5}),
            },
        };
        
        let (name, params) = AnthropicAdapter::from_request(req).unwrap();
        assert_eq!(name, "test_anthropic");
        assert_eq!(params["value"], 5);
    }

    #[test]
    fn test_to_response() {
        let output = serde_json::json!({"result": 15});
        let response = AnthropicAdapter::to_response(output);
        
        assert_eq!(response.result_type, "tool_result");
        assert!(response.content.contains("result"));
        assert!(response.is_error.is_none());
    }

    #[test]
    fn test_to_response_with_id() {
        let output = serde_json::json!({"result": 15});
        let response = AnthropicAdapter::to_response_with_id(output, "toolu_456".to_string());
        
        assert_eq!(response.tool_use_id, "toolu_456");
        assert_eq!(response.result_type, "tool_result");
    }

    #[test]
    fn test_error_response() {
        let response = AnthropicAdapter::error_response("Test error".to_string(), "toolu_789".to_string());
        
        assert_eq!(response.tool_use_id, "toolu_789");
        assert_eq!(response.result_type, "tool_result");
        assert_eq!(response.is_error, Some(true));
    }
}