Skip to main content

langchain_rust/schemas/
tools_openai_like.rs

1use crate::schemas::convert::{OpenAIFromLangchain, TryOpenAiFromLangchain};
2use crate::tools::Tool;
3use async_openai::types::chat::{
4    ChatCompletionNamedToolChoice, ChatCompletionTool, ChatCompletionToolChoiceOption,
5    ChatCompletionTools, FunctionName, FunctionObjectArgs, ToolChoiceOptions,
6};
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::ops::Deref;
10
11#[derive(Clone, Debug)]
12pub enum FunctionCallBehavior {
13    None,
14    Auto,
15    Named(String),
16}
17
18impl OpenAIFromLangchain<FunctionCallBehavior> for ChatCompletionToolChoiceOption {
19    fn from_langchain(langchain: FunctionCallBehavior) -> Self {
20        match langchain {
21            FunctionCallBehavior::Auto => {
22                ChatCompletionToolChoiceOption::Mode(ToolChoiceOptions::Auto)
23            }
24            FunctionCallBehavior::None => {
25                ChatCompletionToolChoiceOption::Mode(ToolChoiceOptions::None)
26            }
27            FunctionCallBehavior::Named(name) => {
28                ChatCompletionToolChoiceOption::Function(ChatCompletionNamedToolChoice {
29                    function: FunctionName { name },
30                })
31            }
32        }
33    }
34}
35
36#[derive(Clone, Debug)]
37pub struct FunctionDefinition {
38    pub name: String,
39    pub description: String,
40    pub parameters: Value,
41}
42
43impl FunctionDefinition {
44    pub fn new(name: &str, description: &str, parameters: Value) -> Self {
45        FunctionDefinition {
46            name: name.trim().replace(" ", "_"),
47            description: description.to_string(),
48            parameters,
49        }
50    }
51
52    /// Generic function that can be used with both Arc<Tool>, Box<Tool>, and direct references
53    pub fn from_langchain_tool<T>(tool: &T) -> FunctionDefinition
54    where
55        T: Deref<Target = dyn Tool> + ?Sized,
56    {
57        FunctionDefinition {
58            name: tool.name().trim().replace(" ", "_"),
59            description: tool.description(),
60            parameters: tool.parameters(),
61        }
62    }
63}
64
65impl TryOpenAiFromLangchain<FunctionDefinition> for ChatCompletionTool {
66    type Error = async_openai::error::OpenAIError;
67    fn try_from_langchain(langchain: FunctionDefinition) -> Result<Self, Self::Error> {
68        let function = FunctionObjectArgs::default()
69            .name(langchain.name)
70            .description(langchain.description)
71            .parameters(langchain.parameters)
72            .build()?;
73
74        Ok(ChatCompletionTool { function })
75    }
76}
77
78impl TryOpenAiFromLangchain<FunctionDefinition> for ChatCompletionTools {
79    type Error = async_openai::error::OpenAIError;
80    fn try_from_langchain(langchain: FunctionDefinition) -> Result<Self, Self::Error> {
81        ChatCompletionTool::try_from_langchain(langchain).map(ChatCompletionTools::Function)
82    }
83}
84
85#[derive(Serialize, Deserialize, Debug)]
86pub struct FunctionCallResponse {
87    pub id: String,
88    #[serde(rename = "type")]
89    pub type_field: String,
90    pub function: FunctionDetail,
91}
92
93#[derive(Serialize, Deserialize, Debug)]
94pub struct FunctionDetail {
95    pub name: String,
96    ///this should be an string, and this should be passed to the tool, to
97    ///then be deserilised inside the tool, becuase just the tools knows the names of the arguments.
98    pub arguments: String,
99}
100
101impl FunctionDetail {
102    /// The arguments as a JSON object, for providers that take tool input as an object
103    /// (Anthropic, Ollama, Bedrock). Non-object JSON or plain text is wrapped as
104    /// `{"input": ...}`, and empty arguments become `{}`.
105    pub fn arguments_object(&self) -> Value {
106        if self.arguments.trim().is_empty() {
107            return Value::Object(Default::default());
108        }
109        match serde_json::from_str::<Value>(&self.arguments) {
110            Ok(v @ Value::Object(_)) => v,
111            Ok(other) => serde_json::json!({ "input": other }),
112            Err(_) => serde_json::json!({ "input": self.arguments }),
113        }
114    }
115}
116
117impl FunctionCallResponse {
118    pub fn from_str(s: &str) -> Result<Self, serde_json::Error> {
119        serde_json::from_str(s)
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use async_trait::async_trait;
127    use serde_json::json;
128    use std::{error::Error, sync::Arc};
129
130    struct MyTool;
131
132    #[async_trait]
133    impl Tool for MyTool {
134        fn name(&self) -> String {
135            " Web Search ".into()
136        }
137        fn description(&self) -> String {
138            "searches".into()
139        }
140        async fn run(&self, _input: Value) -> Result<String, Box<dyn Error>> {
141            Ok(String::new())
142        }
143    }
144
145    #[test]
146    fn function_definition_normalizes_name() {
147        let f = FunctionDefinition::new(" my cool tool ", "desc", json!({}));
148        assert_eq!(f.name, "my_cool_tool");
149    }
150
151    #[test]
152    fn function_definition_from_tool_uses_default_parameters() {
153        let tool: Arc<dyn Tool> = Arc::new(MyTool);
154        let f = FunctionDefinition::from_langchain_tool(&tool);
155        assert_eq!(f.name, "Web_Search");
156        assert_eq!(f.description, "searches");
157        assert_eq!(f.parameters["required"], json!(["input"]));
158        assert_eq!(
159            f.parameters["properties"]["input"]["description"],
160            "searches"
161        );
162    }
163
164    #[test]
165    fn function_call_response_parses() {
166        let r = FunctionCallResponse::from_str(
167            r#"{"id":"c1","type":"function","function":{"name":"calc","arguments":"{\"a\":1}"}}"#,
168        )
169        .unwrap();
170        assert_eq!(r.id, "c1");
171        assert_eq!(r.type_field, "function");
172        assert_eq!(r.function.name, "calc");
173        assert_eq!(r.function.arguments, "{\"a\":1}");
174        assert!(FunctionCallResponse::from_str("{}").is_err());
175    }
176}