chatgpt_functions/
function_specification.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use std::fmt;
4
5/// The documentation for a function
6///
7/// # Caveats
8/// The documentation, in July 2023 is not accurate
9/// https://platform.openai.com/docs/api-reference/chat/create#chat/create-parameters
10///
11/// It states that the parameters are optional, but they are not:
12///
13/// curl https://api.openai.com/v1/chat/completions   -H "Content-Type: application/json"   -H "Authorization: Bearer $OPENAI_API_KEY"   -d '{
14///     "model": "gpt-3.5-turbo-0613",
15///     "messages": [{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "What is the weather like in Madrid, Spain?"}],
16///     "functions": [{
17///         "name": "get_current_weather",
18///         "description": "Get the current weather in a given location"
19///     }],
20///     "function_call": "auto"
21/// }'
22/// {
23///   "error": {
24///     "message": "'parameters' is a required property - 'functions.0'",
25///     "type": "invalid_request_error",
26///     "param": null,
27///     "code": null
28///   }
29/// }
30///
31/// The library works around it by actually having the parameters as optional in the struct,
32/// so the configuration can be parsed correctly, but then printing the object with the parameters
33/// and the minimum required fields so the API doesn't complain. This would be by adding the
34/// parameteres, with type and empty properties. Like this:
35///
36/// curl https://api.openai.com/v1/chat/completions   -H "Content-Type: application/json"   -H "Authorization: Bearer $OPENAI_API_KEY"   -d '{
37///     "model": "gpt-3.5-turbo-0613",
38///     "messages": [{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "What is the weather like in Madrid, Spain?"}],
39///     "functions": [{
40///         "name": "get_current_weather",
41///         "description": "Get the current weather in a given location",
42///         "parameters": {
43///             "type": "object",
44///             "properties": {}
45///         }
46///     }]
47/// }'
48///
49#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
50pub struct FunctionSpecification {
51    pub name: String,
52    pub description: Option<String>,
53    pub parameters: Option<Parameters>,
54}
55
56// Struct to deserialize parameters using serde
57// the type_ field is named type because type is a reserved keyword in Rust
58// the anotation will help serde to deserialize the field correctly
59#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
60pub struct Parameters {
61    #[serde(rename = "type")]
62    pub type_: String,
63    pub properties: HashMap<String, Property>,
64    pub required: Vec<String>,
65}
66
67#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
68pub struct Property {
69    #[serde(rename = "type")]
70    pub type_: String,
71    pub description: Option<String>,
72    #[serde(rename = "enum")]
73    pub enum_: Option<Vec<String>>,
74}
75
76impl FunctionSpecification {
77    pub fn new(
78        name: String,
79        description: Option<String>,
80        parameters: Option<Parameters>,
81    ) -> FunctionSpecification {
82        FunctionSpecification {
83            name,
84            description,
85            parameters,
86        }
87    }
88}
89
90// ------------------------------------------------------------------------------
91// Display functions
92// ------------------------------------------------------------------------------
93
94// Print valid JSON for FunctionSpecification, no commas if last field, no field if None
95impl fmt::Display for FunctionSpecification {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        write!(f, "{{\"name\":\"{}\"", self.name)?;
98        if let Some(description) = &self.description {
99            write!(f, ",\"description\":\"{}\"", description)?;
100        }
101        if let Some(parameters) = &self.parameters {
102            write!(f, ",\"parameters\":{}", parameters)?;
103        } else {
104            write!(
105                f,
106                ",\"parameters\":{{\"type\":\"object\",\"properties\":{{}}}}"
107            )?;
108        }
109        write!(f, "}}")
110    }
111}
112
113impl fmt::Display for Parameters {
114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115        write!(f, "{{\"type\":\"{}\"", self.type_)?;
116        if !self.properties.is_empty() {
117            write!(f, ",\"properties\":{{")?;
118            for (i, (key, value)) in self.properties.iter().enumerate() {
119                write!(f, "\"{}\":{}", key, value)?;
120                if i < self.properties.len() - 1 {
121                    write!(f, ",")?;
122                }
123            }
124            write!(f, "}}")?;
125        }
126        if !self.required.is_empty() {
127            write!(f, ",\"required\":[")?;
128            for (i, required) in self.required.iter().enumerate() {
129                write!(f, "\"{}\"", required)?;
130                if i < self.required.len() - 1 {
131                    write!(f, ",")?;
132                }
133            }
134            write!(f, "]")?;
135        }
136        write!(f, "}}")
137    }
138}
139
140impl fmt::Display for Property {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        write!(f, "{{\"type\":\"{}\"", self.type_)?;
143        if let Some(description) = &self.description {
144            write!(f, ",\"description\":\"{}\"", description)?;
145        }
146        if let Some(enum_) = &self.enum_ {
147            write!(f, ",\"enum\":[")?;
148            for (i, enum_value) in enum_.iter().enumerate() {
149                write!(f, "\"{}\"", enum_value)?;
150                if i < enum_.len() - 1 {
151                    write!(f, ",")?;
152                }
153            }
154            write!(f, "]")?;
155        }
156        write!(f, "}}")
157    }
158}
159
160// ------------------------------------------------------------------------------
161// Tests
162// ------------------------------------------------------------------------------
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn test_function_specification_new() {
169        let name = "get_current_weather".to_string();
170        let description = "Get the current weather in a given location".to_string();
171        let parameters = Parameters {
172            type_: "object".to_string(),
173            properties: HashMap::new(),
174            required: vec![],
175        };
176        let function_specification = FunctionSpecification::new(
177            name.clone(),
178            Some(description.clone()),
179            Some(parameters.clone()),
180        );
181        assert_eq!(function_specification.name, name);
182        assert_eq!(function_specification.description, Some(description));
183        assert_eq!(function_specification.parameters, Some(parameters));
184    }
185
186    #[test]
187    fn test_deserialize_function_specification() {
188        let json = r#"
189        {
190            "name": "get_current_weather",
191            "description": "Get the current weather in a given location",
192            "parameters": {
193                "type": "object",
194                "properties": {
195                    "location": {
196                        "type": "string",
197                        "description": "The city and state, e.g. San Francisco, CA"
198                    },
199                    "unit": {
200                        "type": "string",
201                        "enum": ["celsius", "fahrenheit"]
202                    }
203                },
204                "required": ["location"]
205            }
206        }
207        "#;
208        let function_specification: FunctionSpecification = serde_json::from_str(json)
209            .expect("Could not parse correctly the function specification");
210        assert_eq!(function_specification.name, "get_current_weather");
211        assert_eq!(
212            function_specification.description,
213            Some("Get the current weather in a given location".to_string())
214        );
215        let params = function_specification.parameters.expect("No parameters");
216        assert_eq!(params.type_, "object");
217        assert_eq!(params.properties.len(), 2);
218        assert_eq!(params.required.len(), 1);
219
220        let location = params
221            .properties
222            .get("location")
223            .expect("Could not find location property");
224        assert_eq!(location.type_, "string");
225        assert_eq!(
226            location.description,
227            Some("The city and state, e.g. San Francisco, CA".to_string())
228        );
229
230        let unit = params
231            .properties
232            .get("unit")
233            .expect("Could not find unit property");
234        assert_eq!(unit.type_, "string");
235        assert_eq!(unit.description, None);
236        assert_eq!(
237            unit.enum_,
238            Some(vec!["celsius".to_string(), "fahrenheit".to_string()])
239        );
240    }
241
242    #[test]
243    fn test_display_no_parameters() {
244        let function_specification = FunctionSpecification::new(
245            "get_current_weather".to_string(),
246            Some("Get the current weather in a given location".to_string()),
247            None,
248        );
249        assert_eq!(
250            function_specification.to_string(),
251            "{\"name\":\"get_current_weather\",\"description\":\"Get the current weather in a given location\",\"parameters\":{\"type\":\"object\",\"properties\":{}}}"
252        );
253    }
254
255    #[test]
256    fn test_display_parameters_with_properties() {
257        let mut properties = HashMap::new();
258        properties.insert(
259            "unit".to_string(),
260            Property {
261                type_: "string".to_string(),
262                description: None,
263                enum_: Some(vec!["celsius".to_string(), "fahrenheit".to_string()]),
264            },
265        );
266        let parameters = Parameters {
267            type_: "object".to_string(),
268            properties,
269            required: vec!["unit".to_string()],
270        };
271        assert_eq!(
272            parameters.to_string(),
273            "{\"type\":\"object\",\"properties\":{\"unit\":{\"type\":\"string\",\"enum\":[\"celsius\",\"fahrenheit\"]}},\"required\":[\"unit\"]}"
274        );
275    }
276
277    #[test]
278    fn test_display_parameters_without_properties() {
279        let parameters = Parameters {
280            type_: "object".to_string(),
281            properties: HashMap::new(),
282            required: vec!["location".to_string()],
283        };
284        assert_eq!(
285            parameters.to_string(),
286            "{\"type\":\"object\",\"required\":[\"location\"]}"
287        );
288    }
289
290    #[test]
291    fn test_display_property_with_description_and_enum() {
292        let property = Property {
293            type_: "string".to_string(),
294            description: Some("The city and state, e.g. San Francisco, CA".to_string()),
295            enum_: Some(vec!["celsius".to_string(), "fahrenheit".to_string()]),
296        };
297        assert_eq!(
298            property.to_string(),
299            "{\"type\":\"string\",\"description\":\"The city and state, e.g. San Francisco, CA\",\"enum\":[\"celsius\",\"fahrenheit\"]}"
300        );
301    }
302
303    #[test]
304    fn test_display_property_with_description() {
305        let property = Property {
306            type_: "string".to_string(),
307            description: Some("The city and state, e.g. San Francisco, CA".to_string()),
308            enum_: None,
309        };
310        assert_eq!(
311            property.to_string(),
312            "{\"type\":\"string\",\"description\":\"The city and state, e.g. San Francisco, CA\"}"
313        );
314    }
315
316    #[test]
317    fn test_display_property_with_enum() {
318        let property = Property {
319            type_: "string".to_string(),
320            description: None,
321            enum_: Some(vec!["celsius".to_string(), "fahrenheit".to_string()]),
322        };
323        assert_eq!(
324            property.to_string(),
325            "{\"type\":\"string\",\"enum\":[\"celsius\",\"fahrenheit\"]}"
326        );
327    }
328
329    #[test]
330    fn test_display_function_specification() {
331        let mut properties = HashMap::new();
332        properties.insert(
333            "unit".to_string(),
334            Property {
335                type_: "string".to_string(),
336                description: None,
337                enum_: Some(vec!["celsius".to_string(), "fahrenheit".to_string()]),
338            },
339        );
340        let parameters = Parameters {
341            type_: "object".to_string(),
342            properties,
343            required: vec!["unit".to_string()],
344        };
345        let function_specification = FunctionSpecification {
346            name: "get_current_weather".to_string(),
347            description: Some("Get the current weather in a given location".to_string()),
348            parameters: Some(parameters),
349        };
350        assert_eq!(
351            function_specification.to_string(),
352            "{\"name\":\"get_current_weather\",\"description\":\"Get the current weather in a given location\",\"parameters\":{\"type\":\"object\",\"properties\":{\"unit\":{\"type\":\"string\",\"enum\":[\"celsius\",\"fahrenheit\"]}},\"required\":[\"unit\"]}}"
353        );
354    }
355}