-
Notifications
You must be signed in to change notification settings - Fork 12
Expand file tree
/
Copy pathlib.rs
More file actions
320 lines (289 loc) · 9.74 KB
/
lib.rs
File metadata and controls
320 lines (289 loc) · 9.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
//! Python bindings for Bashkit
//!
//! Exposes the Bash interpreter as a Python class for use in AI agent frameworks.
//! Uses stateful execution - filesystem and variables persist between calls.
use bashkit::{Bash, BashTool as RustBashTool, ExecutionLimits, Tool};
use pyo3::exceptions::{PyRuntimeError, PyValueError};
use pyo3::prelude::*;
use pyo3_async_runtimes::tokio::future_into_py;
use std::sync::Arc;
use tokio::sync::Mutex;
/// Result from executing bash commands
#[pyclass]
#[derive(Clone)]
pub struct ExecResult {
#[pyo3(get)]
pub stdout: String,
#[pyo3(get)]
pub stderr: String,
#[pyo3(get)]
pub exit_code: i32,
#[pyo3(get)]
pub error: Option<String>,
}
#[pymethods]
impl ExecResult {
fn __repr__(&self) -> String {
format!(
"ExecResult(stdout={:?}, stderr={:?}, exit_code={}, error={:?})",
self.stdout, self.stderr, self.exit_code, self.error
)
}
fn __str__(&self) -> String {
if self.exit_code == 0 {
self.stdout.clone()
} else {
format!("Error ({}): {}", self.exit_code, self.stderr)
}
}
/// Check if command succeeded
#[getter]
fn success(&self) -> bool {
self.exit_code == 0
}
/// Return output as dict
fn to_dict(&self) -> pyo3::PyResult<pyo3::Py<pyo3::types::PyDict>> {
Python::with_gil(|py| {
let dict = pyo3::types::PyDict::new(py);
dict.set_item("stdout", &self.stdout)?;
dict.set_item("stderr", &self.stderr)?;
dict.set_item("exit_code", self.exit_code)?;
dict.set_item("error", &self.error)?;
Ok(dict.into())
})
}
}
/// Virtual bash interpreter for AI agents
///
/// BashTool provides a safe execution environment for running bash commands
/// with a virtual filesystem. State persists between calls - files created
/// in one call are available in subsequent calls.
///
/// Example:
/// ```python
/// from bashkit_py import BashTool
///
/// tool = BashTool()
/// result = await tool.execute("echo 'Hello, World!'")
/// print(result.stdout) # Hello, World!
/// ```
#[pyclass]
#[allow(dead_code)]
pub struct BashTool {
/// Stateful bash interpreter - persists filesystem and variables
inner: Arc<Mutex<Bash>>,
username: Option<String>,
hostname: Option<String>,
max_commands: Option<u64>,
max_loop_iterations: Option<u64>,
}
#[pymethods]
impl BashTool {
/// Create a new BashTool instance
///
/// Args:
/// username: Custom username for virtual environment (default: "user")
/// hostname: Custom hostname for virtual environment (default: "sandbox")
/// max_commands: Maximum commands to execute (default: 10000)
/// max_loop_iterations: Maximum loop iterations (default: 100000)
#[new]
#[pyo3(signature = (username=None, hostname=None, max_commands=None, max_loop_iterations=None))]
fn new(
username: Option<String>,
hostname: Option<String>,
max_commands: Option<u64>,
max_loop_iterations: Option<u64>,
) -> PyResult<Self> {
let mut builder = Bash::builder();
if let Some(ref u) = username {
builder = builder.username(u);
}
if let Some(ref h) = hostname {
builder = builder.hostname(h);
}
let mut limits = ExecutionLimits::new();
if let Some(mc) = max_commands {
limits = limits.max_commands(mc as usize);
}
if let Some(mli) = max_loop_iterations {
limits = limits.max_loop_iterations(mli as usize);
}
builder = builder.limits(limits);
let bash = builder.build();
Ok(Self {
inner: Arc::new(Mutex::new(bash)),
username,
hostname,
max_commands,
max_loop_iterations,
})
}
/// Execute bash commands asynchronously
///
/// State persists between calls - files, variables, and functions
/// created in one call are available in subsequent calls.
///
/// Args:
/// commands: Bash commands to execute (like `bash -c "commands"`)
///
/// Returns:
/// ExecResult with stdout, stderr, exit_code
///
/// Example:
/// ```python
/// result = await tool.execute("echo hello && echo world")
/// print(result.stdout) # hello\nworld\n
/// ```
fn execute<'py>(&self, py: Python<'py>, commands: String) -> PyResult<Bound<'py, PyAny>> {
let inner = self.inner.clone();
future_into_py(py, async move {
let mut bash = inner.lock().await;
match bash.exec(&commands).await {
Ok(result) => Ok(ExecResult {
stdout: result.stdout,
stderr: result.stderr,
exit_code: result.exit_code,
error: None,
}),
Err(e) => Ok(ExecResult {
stdout: String::new(),
stderr: String::new(),
exit_code: 1,
error: Some(e.to_string()),
}),
}
})
}
/// Execute bash commands synchronously (blocking)
///
/// Note: Prefer `execute()` for async contexts. This method blocks.
///
/// Args:
/// commands: Bash commands to execute
///
/// Returns:
/// ExecResult with stdout, stderr, exit_code
fn execute_sync(&self, commands: String) -> PyResult<ExecResult> {
let inner = self.inner.clone();
let rt = tokio::runtime::Runtime::new()
.map_err(|e| PyRuntimeError::new_err(format!("Failed to create runtime: {}", e)))?;
rt.block_on(async move {
let mut bash = inner.lock().await;
match bash.exec(&commands).await {
Ok(result) => Ok(ExecResult {
stdout: result.stdout,
stderr: result.stderr,
exit_code: result.exit_code,
error: None,
}),
Err(e) => Ok(ExecResult {
stdout: String::new(),
stderr: String::new(),
exit_code: 1,
error: Some(e.to_string()),
}),
}
})
}
/// Reset the interpreter state (clear filesystem, variables, functions)
fn reset(&self) -> PyResult<()> {
let inner = self.inner.clone();
let rt = tokio::runtime::Runtime::new()
.map_err(|e| PyRuntimeError::new_err(format!("Failed to create runtime: {}", e)))?;
rt.block_on(async move {
let mut bash = inner.lock().await;
// Create fresh Bash with same settings
let builder = Bash::builder();
// Note: We lose settings on reset, could store them
*bash = builder.build();
Ok(())
})
}
/// Get the tool name
#[getter]
fn name(&self) -> &str {
"bashkit"
}
/// Get short description
#[getter]
fn short_description(&self) -> &str {
"Virtual bash interpreter with virtual filesystem"
}
/// Get the full description
fn description(&self) -> PyResult<String> {
let tool = RustBashTool::default();
Ok(tool.description())
}
/// Get LLM documentation
fn help(&self) -> PyResult<String> {
let tool = RustBashTool::default();
Ok(tool.help())
}
/// Get system prompt for LLMs
fn system_prompt(&self) -> PyResult<String> {
let tool = RustBashTool::default();
Ok(tool.system_prompt())
}
/// Get JSON schema for input validation
fn input_schema(&self) -> PyResult<String> {
let tool = RustBashTool::default();
let schema = tool.input_schema();
serde_json::to_string_pretty(&schema)
.map_err(|e| PyValueError::new_err(format!("Schema serialization failed: {}", e)))
}
/// Get JSON schema for output
fn output_schema(&self) -> PyResult<String> {
let tool = RustBashTool::default();
let schema = tool.output_schema();
serde_json::to_string_pretty(&schema)
.map_err(|e| PyValueError::new_err(format!("Schema serialization failed: {}", e)))
}
/// Get tool version
#[getter]
fn version(&self) -> &str {
bashkit::tool::VERSION
}
fn __repr__(&self) -> String {
format!(
"BashTool(username={:?}, hostname={:?})",
self.username.as_deref().unwrap_or("user"),
self.hostname.as_deref().unwrap_or("sandbox")
)
}
}
/// Create a LangChain-compatible tool from BashTool
///
/// Returns a dict with:
/// - name: Tool name
/// - description: Tool description
/// - args_schema: JSON schema for arguments
///
/// Example:
/// ```python
/// from bashkit_py import create_langchain_tool_spec
///
/// spec = create_langchain_tool_spec()
/// # Use with langchain's StructuredTool.from_function()
/// ```
#[pyfunction]
fn create_langchain_tool_spec() -> PyResult<pyo3::Py<pyo3::types::PyDict>> {
let tool = RustBashTool::default();
Python::with_gil(|py| {
let dict = pyo3::types::PyDict::new(py);
dict.set_item("name", tool.name())?;
dict.set_item("description", tool.description())?;
let schema = tool.input_schema();
let schema_str = serde_json::to_string(&schema)
.map_err(|e| PyValueError::new_err(format!("Schema error: {}", e)))?;
dict.set_item("args_schema", schema_str)?;
Ok(dict.into())
})
}
/// Python module definition
#[pymodule]
fn _bashkit(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<BashTool>()?;
m.add_class::<ExecResult>()?;
m.add_function(wrap_pyfunction!(create_langchain_tool_spec, m)?)?;
Ok(())
}