|
| 1 | +# pytest: unit, qualitative |
| 2 | +"""Example usage patterns for bash_executor and local_bash_executor tools. |
| 3 | +
|
| 4 | +Demonstrates three ways to use Mellea's bash execution capabilities: |
| 5 | +1. Direct execution for non-LLM tasks |
| 6 | +2. Wrapping as a MelleaTool for agent use |
| 7 | +3. Integration with requirements framework for rejection sampling |
| 8 | +
|
| 9 | +Safety note: bash_executor uses Docker isolation via llm-sandbox (recommended |
| 10 | +for production). local_bash_executor runs commands directly (for dev/testing only). |
| 11 | +Both enforce a conservative safety denylist: no sudo, no rm -rf, no destructive |
| 12 | +git operations, no writes to /etc, /sys, /proc, etc. |
| 13 | +""" |
| 14 | + |
| 15 | +from mellea.backends.tools import MelleaTool |
| 16 | +from mellea.stdlib.tools.shell import bash_executor, local_bash_executor |
| 17 | + |
| 18 | + |
| 19 | +def example_1_direct_execution() -> None: |
| 20 | + """Example 1: Execute bash commands directly.""" |
| 21 | + print("=== Example 1: Direct Execution ===") |
| 22 | + |
| 23 | + # Execute a simple command |
| 24 | + result = local_bash_executor("echo 'Hello from Bash'") |
| 25 | + print("Command: echo 'Hello from Bash'") |
| 26 | + print(f"Success: {result.success}") |
| 27 | + print(f"Output: {result.stdout}") |
| 28 | + print() |
| 29 | + |
| 30 | + # Execute a command with pipes and redirects |
| 31 | + result = local_bash_executor("ls -la | wc -l") |
| 32 | + print("Command: ls -la | wc -l") |
| 33 | + print(f"Success: {result.success}") |
| 34 | + print(f"Output: {result.stdout}") |
| 35 | + print() |
| 36 | + |
| 37 | + # Attempt a dangerous command (will be rejected) |
| 38 | + result = local_bash_executor("sudo echo unsafe") |
| 39 | + print("Command: sudo echo unsafe") |
| 40 | + print(f"Skipped: {result.skipped}") |
| 41 | + print(f"Reason: {result.skip_message}") |
| 42 | + print() |
| 43 | + |
| 44 | + |
| 45 | +def example_2_wrapped_as_tool() -> None: |
| 46 | + """Example 2: Wrap bash executor as a MelleaTool for LLM use.""" |
| 47 | + print("=== Example 2: Wrapped as MelleaTool ===") |
| 48 | + |
| 49 | + # Create tool from bash executor |
| 50 | + bash_tool = MelleaTool.from_callable(local_bash_executor) |
| 51 | + print(f"Tool name: {bash_tool.name}") |
| 52 | + print(f"Tool schema keys: {bash_tool.as_json_tool.keys()}") |
| 53 | + print() |
| 54 | + |
| 55 | + # Invoke the tool directly (normally LLM would call this) |
| 56 | + result = bash_tool.run("pwd") |
| 57 | + print("Tool invocation result:") |
| 58 | + print(f" Success: {result.success}") |
| 59 | + print(f" Output: {result.stdout}") |
| 60 | + print() |
| 61 | + |
| 62 | + |
| 63 | +def example_3_with_working_dir() -> None: |
| 64 | + """Example 3: Restrict command execution to a specific directory.""" |
| 65 | + print("=== Example 3: Working Directory Restriction ===") |
| 66 | + |
| 67 | + import tempfile |
| 68 | + |
| 69 | + with tempfile.TemporaryDirectory() as tmpdir: |
| 70 | + print(f"Working directory: {tmpdir}") |
| 71 | + |
| 72 | + # Create a file in the working directory |
| 73 | + result = local_bash_executor( |
| 74 | + f"echo 'project content' > {tmpdir}/myfile.txt", working_dir=tmpdir |
| 75 | + ) |
| 76 | + print(f"Command: echo 'project content' > {tmpdir}/myfile.txt") |
| 77 | + print(f"Success: {result.success}") |
| 78 | + print() |
| 79 | + |
| 80 | + # Read it back |
| 81 | + result = local_bash_executor(f"cat {tmpdir}/myfile.txt", working_dir=tmpdir) |
| 82 | + print(f"Command: cat {tmpdir}/myfile.txt") |
| 83 | + print(f"Output: {result.stdout}") |
| 84 | + print() |
| 85 | + |
| 86 | + # Attempt to write outside working directory (will be rejected) |
| 87 | + result = local_bash_executor( |
| 88 | + "echo 'bad' > /tmp/outside.txt", working_dir=tmpdir |
| 89 | + ) |
| 90 | + print(f"Command: echo 'bad' > /tmp/outside.txt (with working_dir={tmpdir})") |
| 91 | + print(f"Skipped: {result.skipped}") |
| 92 | + print(f"Reason: {result.skip_message}") |
| 93 | + print() |
| 94 | + |
| 95 | + |
| 96 | +def example_4_safety_features() -> None: |
| 97 | + """Example 4: Demonstrate safety features.""" |
| 98 | + print("=== Example 4: Safety Features ===") |
| 99 | + |
| 100 | + dangerous_commands = [ |
| 101 | + ("rm -rf /home", "Recursive force delete"), |
| 102 | + ("git push --force", "Force git push"), |
| 103 | + ("sudo whoami", "Privilege escalation"), |
| 104 | + ("bash -i", "Interactive shell"), |
| 105 | + ("touch /etc/config", "Write to system path"), |
| 106 | + ] |
| 107 | + |
| 108 | + for cmd, description in dangerous_commands: |
| 109 | + result = local_bash_executor(cmd) |
| 110 | + print(f"{description}: {cmd}") |
| 111 | + print(f" Rejected: {result.skipped}") |
| 112 | + print(f" Reason: {result.skip_message}") |
| 113 | + print() |
| 114 | + |
| 115 | + |
| 116 | +def example_5_error_handling() -> None: |
| 117 | + """Example 5: Handle execution errors gracefully.""" |
| 118 | + print("=== Example 5: Error Handling ===") |
| 119 | + |
| 120 | + # Command that fails (returns non-zero exit code) |
| 121 | + result = local_bash_executor("exit 1") |
| 122 | + print("Command: exit 1") |
| 123 | + print(f"Success: {result.success}") |
| 124 | + print(f"Stderr: {result.stderr}") |
| 125 | + print() |
| 126 | + |
| 127 | + # Command that doesn't exist |
| 128 | + result = local_bash_executor("nonexistent_command_xyz") |
| 129 | + print("Command: nonexistent_command_xyz") |
| 130 | + print(f"Success: {result.success}") |
| 131 | + if not result.success and result.stderr is not None: |
| 132 | + print(f"Error output: {result.stderr[:100]}") |
| 133 | + print() |
| 134 | + |
| 135 | + |
| 136 | +if __name__ == "__main__": |
| 137 | + example_1_direct_execution() |
| 138 | + example_2_wrapped_as_tool() |
| 139 | + example_3_with_working_dir() |
| 140 | + example_4_safety_features() |
| 141 | + example_5_error_handling() |
0 commit comments