Skip to main content

descry_tool_core/adapters/
openai.rs

1//! OpenAI API adapter
2//!
3//! Converts tools to OpenAI function Calling format.
4
5use serde::{Deserialize, Serialize};
6
7use crate::adapters::ToolAdapter;
8use crate::ToolMeta;
9
10/// OpenAI function definition
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct OpenAiFunction {
13    /// Function name
14    pub name: String,
15    /// Function description
16    pub description: String,
17    /// Function parameters (JSON Schema)
18    pub parameters: serde_json::Value,
19}
20
21/// OpenAI tool specification
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct OpenAiTool {
24    /// Tool type (always "function")
25    #[serde(rename = "type")]
26    pub tool_type: String,
27    /// Function definition
28    pub function: OpenAiFunction,
29}
30
31/// OpenAI function call
32#[derive(Debug, Clone, Deserialize)]
33pub struct OpenAiFunctionCall {
34    /// Function name
35    pub name: String,
36    /// Function arguments (JSON string)
37    pub arguments: String,
38}
39
40/// OpenAI tool call
41#[derive(Debug, Clone, Deserialize)]
42pub struct OpenAiToolCall {
43    /// Tool call ID
44    pub id: String,
45    /// Tool type (always "function")
46    #[serde(rename = "type")]
47    pub tool_type: String,
48    /// Function call
49    pub function: OpenAiFunctionCall,
50}
51
52/// OpenAI call request (wrapper for tool call)
53#[derive(Debug, Clone, Deserialize)]
54pub struct OpenAiCallRequest {
55    /// Tool call
56    pub tool_call: OpenAiToolCall,
57}
58
59/// OpenAI tool response
60#[derive(Debug, Clone, Serialize)]
61pub struct OpenAiToolResponse {
62    /// Tool call ID
63    pub tool_call_id: String,
64    /// Role (always "tool")
65    pub role: String,
66    /// Tool output
67    pub content: String,
68}
69
70/// OpenAI adapter
71pub struct OpenAiAdapter;
72
73impl ToolAdapter for OpenAiAdapter {
74    type ToolSpec = OpenAiTool;
75    type CallRequest = OpenAiCallRequest;
76    type CallResponse = OpenAiToolResponse;
77
78    fn to_spec(meta: &ToolMeta) -> OpenAiTool {
79        OpenAiTool {
80            tool_type: "function".to_string(),
81            function: OpenAiFunction {
82                name: meta.name.to_string(),
83                description: meta.description.to_string(),
84                parameters: (meta.schema)().clone(),
85            },
86        }
87    }
88
89    fn from_request(req: OpenAiCallRequest) -> Result<(String, serde_json::Value), crate::ToolError> {
90        let params: serde_json::Value = serde_json::from_str(&req.tool_call.function.arguments)
91            .map_err(|e| crate::ToolError::invalid_params_with_source(
92                "Failed to parse function arguments",
93                e
94            ))?;
95        
96        Ok((req.tool_call.function.name, params))
97    }
98
99    fn to_response(output: serde_json::Value) -> OpenAiToolResponse {
100        OpenAiToolResponse {
101            tool_call_id: String::new(), // Should be set by caller
102            role: "tool".to_string(),
103            content: serde_json::to_string(&output).unwrap_or_else(|_| output.to_string()),
104        }
105    }
106}
107
108impl OpenAiAdapter {
109    /// Create response with tool call ID
110    pub fn to_response_with_id(output: serde_json::Value, tool_call_id: String) -> OpenAiToolResponse {
111        OpenAiToolResponse {
112            tool_call_id,
113            role: "tool".to_string(),
114            content: serde_json::to_string(&output).unwrap_or_else(|_| output.to_string()),
115        }
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use crate::{Tool, ToolContext, ToolError};
123    use schemars::JsonSchema;
124    use serde::{Deserialize, Serialize};
125    use std::sync::Arc;
126
127    #[derive(Deserialize, JsonSchema)]
128    struct TestParams {
129        value: i32,
130    }
131
132    #[derive(Serialize, JsonSchema)]
133    struct TestOutput {
134        result: i32,
135    }
136
137    struct TestTool;
138
139    impl Tool for TestTool {
140        type Params = TestParams;
141        type Output = TestOutput;
142        const NAME: &'static str = "test_openai";
143        const DESCRIPTION: &'static str = "Test tool for OpenAI";
144
145        async fn call(
146            _ctx: Arc<ToolContext>,
147            params: Self::Params,
148        ) -> Result<Self::Output, ToolError> {
149            Ok(TestOutput {
150                result: params.value * 2,
151            })
152        }
153    }
154
155    inventory::submit! {
156        crate::ToolMeta {
157            name: TestTool::NAME,
158            description: TestTool::DESCRIPTION,
159            call: |ctx, params| {
160                Box::pin(async move {
161                    let params: TestParams = serde_json::from_value(params)?;
162                    let result = <TestTool as Tool>::call(ctx, params).await?;
163                    Ok(serde_json::to_value(result)?)
164                })
165            },
166            schema: || <TestTool as Tool>::schema(),
167            examples: || <TestTool as Tool>::EXAMPLES,
168        }
169    }
170
171    #[test]
172    fn test_to_spec() {
173        let meta = crate::find_tool("test_openai").unwrap();
174        let spec = OpenAiAdapter::to_spec(meta);
175        
176        assert_eq!(spec.tool_type, "function");
177        assert_eq!(spec.function.name, "test_openai");
178        assert!(spec.function.parameters.is_object());
179    }
180
181    #[test]
182    fn test_from_request() {
183        let req = OpenAiCallRequest {
184            tool_call: OpenAiToolCall {
185                id: "call_123".to_string(),
186                tool_type: "function".to_string(),
187                function: OpenAiFunctionCall {
188                    name: "test_openai".to_string(),
189                    arguments: r#"{"value": 5}"#.to_string(),
190                },
191            },
192        };
193        
194        let (name, params) = OpenAiAdapter::from_request(req).unwrap();
195        assert_eq!(name, "test_openai");
196        assert_eq!(params["value"], 5);
197    }
198
199    #[test]
200    fn test_to_response() {
201        let output = serde_json::json!({"result": 10});
202        let response = OpenAiAdapter::to_response(output);
203        
204        assert_eq!(response.role, "tool");
205        assert!(response.content.contains("result"));
206    }
207
208    #[test]
209    fn test_to_response_with_id() {
210        let output = serde_json::json!({"result": 10});
211        let response = OpenAiAdapter::to_response_with_id(output, "call_456".to_string());
212        
213        assert_eq!(response.tool_call_id, "call_456");
214        assert_eq!(response.role, "tool");
215    }
216}