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::{ChatCompletionTool, ChatCompletionToolType, FunctionObject};
7use serde_json::{json, Value};
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        r#type: ChatCompletionToolType::Function,
45        function: FunctionObject {
46            name: name.to_string(),
47            description: Some(description.to_string()),
48            parameters: Some(schema),
49            strict: Some(false),
50        },
51    }
52}
53
54/// Represents a parsed tool call from OpenAI's function calling response
55#[derive(Debug, Clone)]
56pub struct FunctionCall {
57    /// The name of the function/tool to call
58    pub name: String,
59    /// The arguments as a JSON value
60    pub arguments: Value,
61}
62
63/// Parses a tool call from OpenAI's ChatCompletionResponseMessage
64/// Expects the tool call to be in the message's tool_calls array
65pub fn parse_tool_call_from_message(
66    _tool_call_id: &str,
67    function_name: &str,
68    arguments_str: &str,
69) -> Option<(String, Value)> {
70    // Parse the JSON arguments string
71    let args = serde_json::from_str(arguments_str).ok()?;
72    Some((function_name.to_string(), args))
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn test_tool_to_openai_function() {
81        let params = json!({
82            "type": "object",
83            "properties": {
84                "message": {
85                    "type": "string",
86                    "description": "The message to echo"
87                }
88            },
89            "required": ["message"]
90        });
91
92        let tool = tool_to_openai_function("echo", "Echo a message", &params);
93
94        assert_eq!(tool.function.name, "echo");
95        assert_eq!(
96            tool.function.description,
97            Some("Echo a message".to_string())
98        );
99        assert!(tool.function.parameters.is_some());
100    }
101
102    #[test]
103    fn test_parse_tool_call_from_message() {
104        let result = parse_tool_call_from_message("call_123", "echo", r#"{"message": "hello"}"#);
105
106        assert!(result.is_some());
107        let (name, args) = result.unwrap();
108        assert_eq!(name, "echo");
109        assert_eq!(args["message"], "hello");
110    }
111
112    #[test]
113    fn test_parse_tool_call_invalid_json() {
114        let result = parse_tool_call_from_message("call_123", "echo", "invalid json");
115        assert!(result.is_none());
116    }
117}