Sandshrew
A lightweight Python library using a decorator-based approach to:
- Prepare LLM Provider Agnostic Tool-Function Descriptions without duplication using Pydantic Field Notation
- Execute tool_calls
Sequentiallyas well asIn Paralleldepending on use case - Internal retry mechanism with a very consistent tool output consisting of Tool-Response and Error Response
- Support for using injected-state inside tools which are not generated as inputs from LLM calls
- Helpers for LLM Provider Agnostic Tool-Call Extractions and Finish Loop
Quick Start
Defining Tools (@sand_tool)
For the function that needs to be converted to a tool, attach @sand_tool to it.
Simple Tool Example
from sandshrew import sand_tool
from pydantic import Field
@sand_tool(
name="custom_name", # Custom name for the tool (defaults to function name)
description="custom description", # Custom description (defaults to function docstring)
tags=["tag1", "tag2"], # Optional List of tags for categorizing the tool
retry_count=3, # Number of retries on failure (default: 0)
timeout=30.0, # Execution timeout in seconds (default: None)
inject_state=False, # Whether to inject state as first parameter (default: False)
)
def add(a: int = Field(description="First number"),
b: int = Field(description="Second number"),
) -> int:
"""Add two numbers together."""
return a + b
Optional Arguments
name- Custom name for the tool (defaults to function name)description- Custom description (defaults to function docstring)tags- List of tags for categorizing the toolretry_count- Number of retries on failure (default: 0)timeout- Execution timeout in seconds (default: None)inject_state- Whether to inject state as first parameter (default: False)
Tool with injected state
💡 For an argument to be injected state, ensure the following:
- The argument is the first argument in the function
- The argument name begins with
_injected_state
from sandshrew import sand_tool
from pydantic import Field
@sand_tool(inject_state=True, tags=["email"])
def send_email(_injected_state: Dict[str, Any], content: str) -> str:
"""
Send an email using injected state.
Expected state:
{
"user_email": "user@example.com"
}
"""
user_email = _injected_state.get("user_email")
if not user_email:
return "no user email found in state."
# Placeholder for actual email logic
message = f"Sent {content} message to {user_email}..."
return message
Getting tool-descriptions (LLM Tool Representations)
helpers.py has provider-agnostic methods that can be used to prepare tool-descriptions for any provider.
from sandshrew.helpers import prepare_tools
from sandshrew.data_types import Provider
tool_list = [add, send_email]
tools = prepare_tools(Provider.OPENAI, tool_list)
Expected arguments:
provider- Name of the provider we want to prepare the tool-function descriptions fortool_list- List of @sand_tool decorated BaseTool(s)
Expected Output format
- List of Dict[str, Any] representing tool-call
One click Tool Execution with Injected State
from sandshrew.helpers import Executor
provider_completion_response = client.chat.completions.create(
model="gpt-4o-mini",
tools=tools,
messages=messages,
)
results = Executor(
tool_list=tool_list,
provider=Provider.OPENAI,
_injected_state=_injected_state,
use_parallel=False,
).execute(provider_completion_response)
Internal Flow
- Based on provider, extracts the tool-calls if available
- Wraps the injected state on arguments of the tool-calls that have
inject_state = Trueand invokes the actual tool-calls - In cases of errors, retries up to
retry_counttimes for the tool and then returns a defined error (SeeExpected Output format) - On success, returns a defined success output (See
Expected Output format)
Expected arguments:
tool_list- List of @sand_tool decorated BaseTool(s)provider- Name of the provider for which to prepare tool-function descriptions_injected_state- Optional object representing the injected state the invoked tools may needuse_parallel- If number of tool_calls > 1, execute them in parallel (Default = False)
Expected Output format
- List of ExecutionResult with the format of
class ExecutionResult:
tool_call: ToolCall
content: Optional[Any] = None
error: Optional[ExecutionError] = None
where ToolCall is the input ToolCall and the optional error is of the format ExecutionError
@dataclass(frozen=True)
class ToolCall:
id: str
name: str
arguments: dict[str, Any]
@dataclass
class ExecutionError:
message: str
raw_error: Optional[Exception] = None
retryable: bool = False
backtrace: Optional[Any] = None
The type of content is Any, making it extensible for other use cases
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ LLM Provider Response │
│ (tool_calls with id, name, arguments from LLM) │
└────────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Executor.execute() │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 1. Extract tool-calls based on provider │ │
│ │ 2. Inject state for tools with inject_state=True │ │
│ │ 3. Execute sequentially or in parallel │ │
│ │ 4. Retry on failure (up to retry_count) │ │
│ └──────────────────────────────────────────────────────────┘ │
└────────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ ExecutionResult[] │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ { │ │
│ │ tool_call: ToolCall, │ │
│ │ content: Any, # Success output │ │
│ │ error: ExecutionError # Error details (if failed) │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Output Format: ExecutionResult
@dataclass
class ExecutionResult:
tool_call: ToolCall # Original tool call
content: Optional[Any] = None # Tool execution result
error: Optional[ExecutionError] = None # Error if execution failed
# Unified Provider Agnostic ToolCall
@dataclass(frozen=True)
class ToolCall:
id: str # Unique identifier from LLM
name: str # Tool function name
arguments: dict[str, Any] # Arguments to pass to tool
@dataclass
class ExecutionError:
message: str # Error description
raw_error: Optional[Exception] = None # Original exception
retryable: bool = False # Whether error is retryable
backtrace: Optional[Any] = None # Stack trace
Examples
See example/example_tools.py and example/main.py for complete working examples including:
- Math operations (add, subtract, multiply, divide)
- String utilities (greet, validate_email)
- Stateful tools (send_email, process_with_contextual_state)
- ReAct-style LLM interactions
Development
Setup
Create virtual environment
python3 -m venv venv
source venv/bin/activate
Install requirements
pip install -r requirements.txt
Running examples
python -B -m example.main
Running Tests
pytest tests/
Code Quality
ruff check .
ruff format .
Contributing
See CONTRIBUTING.md for guidelines.
License
See LICENSE for details.
Release files for sandshrew 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| sandshrew-0.1.0.tar.gz | 12.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sandshrew-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 23.4 kB
Release files / sandshrew-0.1.0.tar.gz
| Download URL | sandshrew-0.1.0.tar.gz |
|---|---|
| Size | 12.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4896ee8d5a69045da419a5df108f21447150a5e4e4a7f1b60770d94ff36d1715
|
|
BLAKE2b-256 checksum How to use checksums |
79f41d54b6e9702cdde950beac3f621b2ebd50b2cd2386d98f021a25f6c67ab7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.5
|
Release files / sandshrew-0.1.0-py3-none-any.whl
| Download URL | sandshrew-0.1.0-py3-none-any.whl |
|---|---|
| Size | 10.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e9ed31e8231d886af554b4722f6688b4004f47a6fb4de0e5dfbb26f04d40be38
|
|
BLAKE2b-256 checksum How to use checksums |
f1e37e2b8da5ac66b093c9f6822ce9d42c1b42a62803176e21ac33636ea5adff
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.5
|