langchain_rust/tools/command_executor/
command_executor.rs

1use std::error::Error;
2
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5use serde_json::{json, Value};
6
7use crate::tools::Tool;
8
9pub struct CommandExecutor {
10    platform: String,
11}
12
13impl CommandExecutor {
14    /// Create a new CommandExecutor instance
15    /// # Example
16    /// ```rust,ignore
17    /// let tool = CommandExecutor::new("linux");
18    /// ```
19    pub fn new<S: Into<String>>(platform: S) -> Self {
20        Self {
21            platform: platform.into(),
22        }
23    }
24}
25
26impl Default for CommandExecutor {
27    fn default() -> Self {
28        Self::new("linux")
29    }
30}
31
32#[derive(Deserialize, Serialize, Debug)]
33struct CommandInput {
34    cmd: String,
35    #[serde(default)]
36    args: Vec<String>,
37}
38#[derive(Serialize, Deserialize, Debug)]
39struct CommandsWrapper {
40    commands: Vec<CommandInput>,
41}
42
43#[async_trait]
44impl Tool for CommandExecutor {
45    fn name(&self) -> String {
46        String::from("Command_Executor")
47    }
48    fn description(&self) -> String {
49        format!(
50            r#""This tool let you run command on the terminal"
51            "The input should be an array with commands for the following platform: {}"
52            "examle of input: [{{ "cmd": "ls", "args": [] }},{{"cmd":"mkdir","args":["test"]}}]"
53            "Should be a comma separated commands"
54            "#,
55            self.platform
56        )
57    }
58
59    fn parameters(&self) -> Value {
60        let prompt = format!(
61            "This tool let you run command on the terminal.
62        The input should be an array with commands for the following platform: {}",
63            self.platform
64        );
65        json!(
66
67        {
68          "description": prompt,
69          "type": "object",
70          "properties": {
71            "commands": {
72              "description": "An array of command objects to be executed",
73              "type": "array",
74              "items": {
75                "type": "object",
76                "properties": {
77                  "cmd": {
78                    "type": "string",
79                    "description": "The command to execute"
80                  },
81                  "args": {
82                    "type": "array",
83                    "items": {
84                      "type": "string"
85                    },
86                    "default": [],
87                    "description": "List of arguments for the command"
88                  }
89                },
90                "required": ["cmd"],
91                "additionalProperties": false,
92                "description": "Object representing a command and its optional arguments"
93              }
94            }
95          },
96          "required": ["commands"],
97          "additionalProperties": false
98        }
99                )
100    }
101
102    async fn parse_input(&self, input: &str) -> Value {
103        log::info!("Parsing input: {}", input);
104
105        // Attempt to parse input string into CommandsWrapper struct first
106        let wrapper_result = serde_json::from_str::<CommandsWrapper>(input);
107
108        if let Ok(wrapper) = wrapper_result {
109            // If successful, serialize the `commands` back into a serde_json::Value
110            // this is for llm like open ai tools
111            serde_json::to_value(wrapper.commands).unwrap_or_else(|err| {
112                log::error!("Serialization error: {}", err);
113                Value::Null
114            })
115        } else {
116            // If the first attempt fails, try parsing it as Vec<CommandInput> directly
117            // This works on any llm
118            let commands_result = serde_json::from_str::<Vec<CommandInput>>(input);
119
120            commands_result.map_or_else(
121                |err| {
122                    log::error!("Failed to parse input: {}", err);
123                    Value::Null
124                },
125                |commands| serde_json::to_value(commands).unwrap_or(Value::Null),
126            )
127        }
128    }
129
130    async fn run(&self, input: Value) -> Result<String, Box<dyn Error>> {
131        let commands: Vec<CommandInput> = serde_json::from_value(input)?;
132        let mut result = String::new();
133
134        for command in commands {
135            let mut command_to_execute = std::process::Command::new(&command.cmd);
136            command_to_execute.args(&command.args);
137
138            let output = command_to_execute.output()?;
139
140            result.push_str(&format!(
141                "Command: {}\nOutput: {}",
142                command.cmd,
143                String::from_utf8_lossy(&output.stdout),
144            ));
145
146            if !output.status.success() {
147                return Err(Box::new(std::io::Error::new(
148                    std::io::ErrorKind::Other,
149                    format!(
150                        "Command {} failed with status: {}",
151                        command.cmd, output.status
152                    ),
153                )));
154            }
155        }
156
157        Ok(result)
158    }
159}
160
161#[cfg(test)]
162mod test {
163    use super::*;
164    use serde_json::json;
165    #[tokio::test]
166    async fn test_with_string_executor() {
167        let tool = CommandExecutor::new("linux");
168        let input = json!({
169            "commands": [
170                {
171                    "cmd": "ls",
172                    "args": []
173                }
174            ]
175        });
176        println!("{}", &input.to_string());
177        let result = tool.call(&input.to_string()).await.unwrap();
178        println!("Res: {}", result);
179    }
180}