descry-tool-core 0.3.1

Core traits and types for descry-tool framework
Documentation
//! OpenAI API adapter
//!
//! Converts tools to OpenAI function Calling format.

use serde::{Deserialize, Serialize};

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

/// OpenAI function definition
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenAiFunction {
    /// Function name
    pub name: String,
    /// Function description
    pub description: String,
    /// Function parameters (JSON Schema)
    pub parameters: serde_json::Value,
}

/// OpenAI tool specification
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenAiTool {
    /// Tool type (always "function")
    #[serde(rename = "type")]
    pub tool_type: String,
    /// Function definition
    pub function: OpenAiFunction,
}

/// OpenAI function call
#[derive(Debug, Clone, Deserialize)]
pub struct OpenAiFunctionCall {
    /// Function name
    pub name: String,
    /// Function arguments (JSON string)
    pub arguments: String,
}

/// OpenAI tool call
#[derive(Debug, Clone, Deserialize)]
pub struct OpenAiToolCall {
    /// Tool call ID
    pub id: String,
    /// Tool type (always "function")
    #[serde(rename = "type")]
    pub tool_type: String,
    /// Function call
    pub function: OpenAiFunctionCall,
}

/// OpenAI call request (wrapper for tool call)
#[derive(Debug, Clone, Deserialize)]
pub struct OpenAiCallRequest {
    /// Tool call
    pub tool_call: OpenAiToolCall,
}

/// OpenAI tool response
#[derive(Debug, Clone, Serialize)]
pub struct OpenAiToolResponse {
    /// Tool call ID
    pub tool_call_id: String,
    /// Role (always "tool")
    pub role: String,
    /// Tool output
    pub content: String,
}

/// OpenAI adapter
pub struct OpenAiAdapter;

impl ToolAdapter for OpenAiAdapter {
    type ToolSpec = OpenAiTool;
    type CallRequest = OpenAiCallRequest;
    type CallResponse = OpenAiToolResponse;

    fn to_spec(meta: &ToolMeta) -> OpenAiTool {
        OpenAiTool {
            tool_type: "function".to_string(),
            function: OpenAiFunction {
                name: meta.name.to_string(),
                description: meta.description.to_string(),
                parameters: (meta.schema)().clone(),
            },
        }
    }

    fn from_request(req: OpenAiCallRequest) -> Result<(String, serde_json::Value), crate::ToolError> {
        let params: serde_json::Value = serde_json::from_str(&req.tool_call.function.arguments)
            .map_err(|e| crate::ToolError::invalid_params_with_source(
                "Failed to parse function arguments",
                e
            ))?;
        
        Ok((req.tool_call.function.name, params))
    }

    fn to_response(output: serde_json::Value) -> OpenAiToolResponse {
        OpenAiToolResponse {
            tool_call_id: String::new(), // Should be set by caller
            role: "tool".to_string(),
            content: serde_json::to_string(&output).unwrap_or_else(|_| output.to_string()),
        }
    }
}

impl OpenAiAdapter {
    /// Create response with tool call ID
    pub fn to_response_with_id(output: serde_json::Value, tool_call_id: String) -> OpenAiToolResponse {
        OpenAiToolResponse {
            tool_call_id,
            role: "tool".to_string(),
            content: serde_json::to_string(&output).unwrap_or_else(|_| output.to_string()),
        }
    }
}

#[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_openai";
        const DESCRIPTION: &'static str = "Test tool for OpenAI";

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

    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_openai").unwrap();
        let spec = OpenAiAdapter::to_spec(meta);
        
        assert_eq!(spec.tool_type, "function");
        assert_eq!(spec.function.name, "test_openai");
        assert!(spec.function.parameters.is_object());
    }

    #[test]
    fn test_from_request() {
        let req = OpenAiCallRequest {
            tool_call: OpenAiToolCall {
                id: "call_123".to_string(),
                tool_type: "function".to_string(),
                function: OpenAiFunctionCall {
                    name: "test_openai".to_string(),
                    arguments: r#"{"value": 5}"#.to_string(),
                },
            },
        };
        
        let (name, params) = OpenAiAdapter::from_request(req).unwrap();
        assert_eq!(name, "test_openai");
        assert_eq!(params["value"], 5);
    }

    #[test]
    fn test_to_response() {
        let output = serde_json::json!({"result": 10});
        let response = OpenAiAdapter::to_response(output);
        
        assert_eq!(response.role, "tool");
        assert!(response.content.contains("result"));
    }

    #[test]
    fn test_to_response_with_id() {
        let output = serde_json::json!({"result": 10});
        let response = OpenAiAdapter::to_response_with_id(output, "call_456".to_string());
        
        assert_eq!(response.tool_call_id, "call_456");
        assert_eq!(response.role, "tool");
    }
}