Skip to main content

spec_ai/spec_ai_core/agent/
function_calling.rs

1//! OpenAI Function Calling Integration
2//!
3//! This module provides functionality to convert tool definitions into OpenAI's
4//! ChatCompletionTool format and parse function call responses from the SDK.
5
6use async_openai::types::chat::{ChatCompletionTool, FunctionObject};
7use serde_json::{Value, json};
8
9/// Converts parameters to OpenAI function schema format
10fn parameters_to_openai_schema(params: &Value) -> Value {
11    // Extract properties and required fields from the input
12    let properties = params
13        .get("properties")
14        .and_then(|p| p.as_object())
15        .cloned()
16        .unwrap_or_default();
17
18    let required: Vec<String> = params
19        .get("required")
20        .and_then(|r| r.as_array())
21        .map(|arr| {
22            arr.iter()
23                .filter_map(|v| v.as_str().map(String::from))
24                .collect()
25        })
26        .unwrap_or_default();
27
28    json!({
29        "type": "object",
30        "properties": properties,
31        "required": required
32    })
33}
34
35/// Converts a tool definition into OpenAI ChatCompletionTool format
36pub fn tool_to_openai_function(
37    name: &str,
38    description: &str,
39    parameters: &Value,
40) -> ChatCompletionTool {
41    let schema = parameters_to_openai_schema(parameters);
42
43    ChatCompletionTool {
44        function: FunctionObject {
45            name: name.to_string(),
46            description: Some(description.to_string()),
47            parameters: Some(schema),
48            strict: Some(false),
49        },
50    }
51}
52
53/// Represents a parsed tool call from OpenAI's function calling response
54#[derive(Debug, Clone)]
55pub struct FunctionCall {
56    /// The name of the function/tool to call
57    pub name: String,
58    /// The arguments as a JSON value
59    pub arguments: Value,
60}
61
62/// Parses a tool call from OpenAI's ChatCompletionResponseMessage
63/// Expects the tool call to be in the message's tool_calls array
64pub fn parse_tool_call_from_message(
65    _tool_call_id: &str,
66    function_name: &str,
67    arguments_str: &str,
68) -> Option<(String, Value)> {
69    // Parse the JSON arguments string
70    let args = serde_json::from_str(arguments_str).ok()?;
71    Some((function_name.to_string(), args))
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn test_tool_to_openai_function() {
80        let params = json!({
81            "type": "object",
82            "properties": {
83                "message": {
84                    "type": "string",
85                    "description": "The message to echo"
86                }
87            },
88            "required": ["message"]
89        });
90
91        let tool = tool_to_openai_function("echo", "Echo a message", &params);
92
93        assert_eq!(tool.function.name, "echo");
94        assert_eq!(
95            tool.function.description,
96            Some("Echo a message".to_string())
97        );
98        assert!(tool.function.parameters.is_some());
99    }
100
101    #[test]
102    fn test_parse_tool_call_from_message() {
103        let result = parse_tool_call_from_message("call_123", "echo", r#"{"message": "hello"}"#);
104
105        assert!(result.is_some());
106        let (name, args) = result.unwrap();
107        assert_eq!(name, "echo");
108        assert_eq!(args["message"], "hello");
109    }
110
111    #[test]
112    fn test_parse_tool_call_invalid_json() {
113        let result = parse_tool_call_from_message("call_123", "echo", "invalid json");
114        assert!(result.is_none());
115    }
116}