Skip to main content

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 Sequentially as well as In Parallel depending 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
  1. name - Custom name for the tool (defaults to function name)
  2. description - Custom description (defaults to function docstring)
  3. tags - List of tags for categorizing the tool
  4. retry_count - Number of retries on failure (default: 0)
  5. timeout - Execution timeout in seconds (default: None)
  6. 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:

  1. provider - Name of the provider we want to prepare the tool-function descriptions for
  2. tool_list - List of @sand_tool decorated BaseTool(s)

Expected Output format

  1. 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

  1. Based on provider, extracts the tool-calls if available
  2. Wraps the injected state on arguments of the tool-calls that have inject_state = True and invokes the actual tool-calls
  3. In cases of errors, retries up to retry_count times for the tool and then returns a defined error (See Expected Output format)
  4. On success, returns a defined success output (See Expected Output format)

Expected arguments:

  1. tool_list - List of @sand_tool decorated BaseTool(s)
  2. provider - Name of the provider for which to prepare tool-function descriptions
  3. _injected_state - Optional object representing the injected state the invoked tools may need
  4. use_parallel - If number of tool_calls > 1, execute them in parallel (Default = False)

Expected Output format

  1. 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

Architecture diagram

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)

Source distribution for sandshrew 0.1.0
File Size Uploaded
sandshrew-0.1.0.tar.gz 12.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for sandshrew 0.1.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page