Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/bashkit-python/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ doc = false # Python extension, no Rust docs needed

[dependencies]
# Bashkit core
bashkit = { path = "../bashkit" }
bashkit = { path = "../bashkit", features = ["scripted_tool"] }

# PyO3 for Python bindings
pyo3 = { workspace = true }
Expand Down
13 changes: 9 additions & 4 deletions crates/bashkit-python/bashkit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,33 @@
>>> print(result.stdout)
Hello, World!

For scripted multi-tool orchestration:
>>> from bashkit import ScriptedTool
>>> tool = ScriptedTool("api")
>>> tool.add_tool("greet", "Greet user", callback=lambda p, s=None: f"hello {p.get('name', 'world')}")
>>> result = tool.execute_sync("greet --name Alice")

For LangChain integration:
>>> from bashkit.langchain import create_bash_tool
>>> tool = create_bash_tool()
>>> from bashkit.langchain import create_bash_tool, create_scripted_tool

For Deep Agents integration:
>>> from bashkit.deepagents import create_bash_middleware
>>> middleware = create_bash_middleware()

For PydanticAI integration:
>>> from bashkit.pydantic_ai import create_bash_tool
>>> tool = create_bash_tool()
"""

from bashkit._bashkit import (
BashTool,
ExecResult,
ScriptedTool,
create_langchain_tool_spec,
)

__version__ = "0.1.2"
__all__ = [
"BashTool",
"ExecResult",
"ScriptedTool",
"create_langchain_tool_spec",
]
104 changes: 52 additions & 52 deletions crates/bashkit-python/bashkit/_bashkit.pyi
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Type stubs for bashkit_py native module."""

from typing import Any, Callable

class ExecResult:
"""Result from executing bash commands."""

Expand All @@ -9,7 +11,7 @@ class ExecResult:
error: str | None
success: bool

def to_dict(self) -> dict[str, any]: ...
def to_dict(self) -> dict[str, Any]: ...

class BashTool:
"""Sandboxed bash interpreter for AI agents.
Expand All @@ -35,62 +37,60 @@ class BashTool:
hostname: str | None = None,
max_commands: int | None = None,
max_loop_iterations: int | None = None,
) -> None:
"""Create a new BashTool instance.

Args:
username: Custom username for sandbox (default: "user")
hostname: Custom hostname for sandbox (default: "sandbox")
max_commands: Maximum commands to execute (default: 10000)
max_loop_iterations: Maximum loop iterations (default: 100000)
"""
...

async def execute(self, commands: str) -> ExecResult:
"""Execute bash commands asynchronously.

Args:
commands: Bash commands to execute (like `bash -c "commands"`)

Returns:
ExecResult with stdout, stderr, exit_code
"""
...

def execute_sync(self, commands: str) -> ExecResult:
"""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
"""
...

def description(self) -> str:
"""Get the full description."""
...
) -> None: ...
async def execute(self, commands: str) -> ExecResult: ...
def execute_sync(self, commands: str) -> ExecResult: ...
def description(self) -> str: ...
def help(self) -> str: ...
def system_prompt(self) -> str: ...
def input_schema(self) -> str: ...
def output_schema(self) -> str: ...

def help(self) -> str:
"""Get LLM documentation."""
...
class ScriptedTool:
"""Compose Python callbacks as bash builtins for multi-tool orchestration.

def system_prompt(self) -> str:
"""Get system prompt for LLMs."""
...
Each registered tool becomes a bash builtin command. An LLM (or user)
writes a single bash script that pipes, loops, and branches across tools.

def input_schema(self) -> str:
"""Get JSON schema for input validation."""
...
Example:
>>> tool = ScriptedTool("api")
>>> tool.add_tool("greet", "Greet user",
... callback=lambda p, s=None: f"hello {p.get('name', 'world')}\\n",
... schema={"type": "object", "properties": {"name": {"type": "string"}}})
>>> result = tool.execute_sync("greet --name Alice")
>>> print(result.stdout.strip())
hello Alice
"""

def output_schema(self) -> str:
"""Get JSON schema for output."""
...
name: str
short_description: str
version: str

def create_langchain_tool_spec() -> dict[str, any]:
def __init__(
self,
name: str,
short_description: str | None = None,
max_commands: int | None = None,
max_loop_iterations: int | None = None,
) -> None: ...
def add_tool(
self,
name: str,
description: str,
callback: Callable[[dict[str, Any], str | None], str],
schema: dict[str, Any] | None = None,
) -> None: ...
def env(self, key: str, value: str) -> None: ...
async def execute(self, commands: str) -> ExecResult: ...
def execute_sync(self, commands: str) -> ExecResult: ...
def tool_count(self) -> int: ...
def description(self) -> str: ...
def help(self) -> str: ...
def system_prompt(self) -> str: ...
def input_schema(self) -> str: ...
def output_schema(self) -> str: ...

def create_langchain_tool_spec() -> dict[str, Any]:
"""Create a LangChain-compatible tool specification.

Returns:
Expand Down
131 changes: 110 additions & 21 deletions crates/bashkit-python/bashkit/langchain.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
"""
LangChain integration for Bashkit.

Provides a LangChain-compatible tool that wraps BashTool for use with
LangChain agents and chains.
Provides LangChain-compatible tools wrapping BashTool and ScriptedTool
for use with LangChain agents and chains.

Example:
Example (BashTool):
>>> from bashkit.langchain import create_bash_tool
>>> from langchain.agents import create_agent
>>>
>>> tool = create_bash_tool()
>>> agent = create_agent(model="claude-sonnet-4-20250514", tools=[tool])
>>> result = tool.invoke({"commands": "echo hello"})

Example (ScriptedTool):
>>> from bashkit import ScriptedTool
>>> from bashkit.langchain import create_scripted_tool
>>>
>>> st = ScriptedTool("api")
>>> st.add_tool("greet", "Greet user", callback=lambda p, s=None: f"hello {p.get('name')}\\n")
>>> tool = create_scripted_tool(st)
>>> result = tool.invoke({"commands": "greet --name Alice"})
"""

from __future__ import annotations
Expand All @@ -32,6 +39,7 @@ def PrivateAttr(*args, **kwargs):


from bashkit import BashTool as NativeBashTool
from bashkit import ScriptedTool as NativeScriptedTool


class BashToolInput(BaseModel):
Expand All @@ -51,12 +59,11 @@ class BashkitTool(BaseTool):
>>> print(result) # Hello!
"""

name: str = "" # Set in __init__ from bashkit
description: str = "" # Set in __init__ from bashkit
name: str = ""
description: str = ""
args_schema: type[BaseModel] = BashToolInput
handle_tool_error: bool = True

# Internal state - use PrivateAttr for pydantic v2 compatibility
_bash_tool: NativeBashTool = PrivateAttr()

def __init__(
Expand All @@ -67,21 +74,12 @@ def __init__(
max_loop_iterations: int | None = None,
**kwargs,
):
"""Initialize BashkitTool.

Args:
username: Custom username for sandbox
hostname: Custom hostname for sandbox
max_commands: Max commands to execute
max_loop_iterations: Max loop iterations
"""
bash_tool = NativeBashTool(
username=username,
hostname=hostname,
max_commands=max_commands,
max_loop_iterations=max_loop_iterations,
)
# Use name and description from bashkit lib
kwargs["name"] = bash_tool.name
kwargs["description"] = bash_tool.description()
super().__init__(**kwargs)
Expand All @@ -94,7 +92,6 @@ def _run(self, commands: str) -> str:
if result.error:
raise ToolException(f"Execution error: {result.error}")

# Return combined output for the agent
output = result.stdout
if result.stderr:
output += f"\nSTDERR: {result.stderr}"
Expand All @@ -110,7 +107,64 @@ async def _arun(self, commands: str) -> str:
if result.error:
raise ToolException(f"Execution error: {result.error}")

# Return combined output for the agent
output = result.stdout
if result.stderr:
output += f"\nSTDERR: {result.stderr}"
if result.exit_code != 0:
output += f"\n[Exit code: {result.exit_code}]"

return output

class ScriptedToolLangChain(BaseTool):
"""LangChain tool wrapper for Bashkit ScriptedTool.

Wraps a pre-configured ScriptedTool (with registered Python callbacks)
as a LangChain tool. The LLM sends bash scripts that orchestrate all
registered sub-tools in one call.

Example:
>>> from bashkit import ScriptedTool
>>> st = ScriptedTool("k8s")
>>> st.add_tool("get_pods", "List pods", callback=my_callback)
>>> tool = ScriptedToolLangChain(st)
>>> result = tool.invoke({"commands": "get_pods --namespace default | jq '.items | length'"})
"""

name: str = ""
description: str = ""
args_schema: type[BaseModel] = BashToolInput
handle_tool_error: bool = True

_scripted_tool: NativeScriptedTool = PrivateAttr()

def __init__(self, scripted_tool: NativeScriptedTool, **kwargs):
kwargs["name"] = scripted_tool.name
kwargs["description"] = scripted_tool.system_prompt()
super().__init__(**kwargs)
object.__setattr__(self, "_scripted_tool", scripted_tool)

def _run(self, commands: str) -> str:
"""Execute scripted tool commands synchronously."""
result = self._scripted_tool.execute_sync(commands)

if result.error:
raise ToolException(f"Execution error: {result.error}")

output = result.stdout
if result.stderr:
output += f"\nSTDERR: {result.stderr}"
if result.exit_code != 0:
output += f"\n[Exit code: {result.exit_code}]"

return output

async def _arun(self, commands: str) -> str:
"""Execute scripted tool commands asynchronously."""
result = await self._scripted_tool.execute(commands)

if result.error:
raise ToolException(f"Execution error: {result.error}")

output = result.stdout
if result.stderr:
output += f"\nSTDERR: {result.stderr}"
Expand Down Expand Up @@ -158,4 +212,39 @@ def create_bash_tool(
)


__all__ = ["BashkitTool", "BashToolInput", "create_bash_tool"]
def create_scripted_tool(scripted_tool: NativeScriptedTool) -> ScriptedToolLangChain:
"""Create a LangChain-compatible tool from a configured ScriptedTool.

Args:
scripted_tool: A ScriptedTool with registered tool callbacks

Returns:
ScriptedToolLangChain instance for use with LangChain agents

Raises:
ImportError: If langchain-core is not installed

Example:
>>> from bashkit import ScriptedTool
>>> from bashkit.langchain import create_scripted_tool
>>>
>>> st = ScriptedTool("api")
>>> st.add_tool("get_data", "Fetch data", callback=my_fn)
>>> tool = create_scripted_tool(st)
>>> # Use with: create_react_agent(model, [tool])
"""
if not LANGCHAIN_AVAILABLE:
raise ImportError(
"langchain-core is required for LangChain integration. Install with: pip install 'bashkit[langchain]'"
)

return ScriptedToolLangChain(scripted_tool)


__all__ = [
"BashkitTool",
"BashToolInput",
"ScriptedToolLangChain",
"create_bash_tool",
"create_scripted_tool",
]
Loading
Loading