Skip to main content

🔧 ToolPickr

ToolPickr will make sure your LLM never misses a tool unlike the 'e' missing in its name.

Give your LLM access to hundreds of tools: ToolPickr finds the right ones instantly.

Early Work-In-Progress: ToolPickr is under active development. While the core retrieval mechanics are functional, the project is brand new, and we do not yet claim or guarantee any specific tool-picking accuracy. Formal benchmarking is planned and currently underway.

The Impact • Quick Start • How It Works • Installation • API Reference • Examples


The Problem

LLMs have a limited context window. When you have hundreds of tools, you can't pass all of them in every API call. It's expensive, slow, and degrades tool selection accuracy.

The Solution

ToolPickr is a retrieval-augmented tool picker. It uses an ensemble of semantic search + BM25 (with an optional cross-encoder reranker) to find the most relevant tools for each query. Your LLM only sees the tools it actually needs.

ToolPickr exposes itself as a single meta-tool called toolpickr that your LLM calls in a two-step protocol:

  1. Search: LLM describes what it needs → ToolPickr returns matching tools
  2. Execute: LLM calls a discovered tool → ToolPickr routes (or auto-executes) it

You keep full control of your LLM loop. No wrappers, no proxies, no magic.

Why Use This? (The Impact)

Token Savings

When you have 100+ tools, passing all of them in every prompt consumes a lot of context:

  • Without ToolPickr: 100 tools × ~250 tokens each = ~25,000 tokens per request.
  • With ToolPickr: 1 search meta-tool (~300 tokens) + top 5 retrieved tools (~1,250 tokens) = ~1,550 tokens total. This saves about 90%+ of the tool-related prompt overhead on each turn.

Lower API Bills

Trimming down prompt sizes means fewer tokens are billed. If you run thousands of queries a day, this token saving scales up and keeps your monthly API bill manageable.

Better Tool Call Accuracy

LLMs often struggle to pick the correct tool or hallucinate arguments when overwhelmed with options (the "lost in the middle" effect). By only exposing the most relevant top-k tools for each query, ToolPickr makes tool selection much more reliable.

Quick Start

from toolpickr import ToolPickr, ToolDefinition

# 1. Initialize — zero-config works out of the box (local models, no API key)
pickr = ToolPickr()

# 2. Register your tools
pickr.register_tools([
    ToolDefinition(
        name="send_email",
        description="Send an email to a recipient.",
        parameters={
            "type": "object",
            "properties": {
                "to": {"type": "string", "description": "Recipient email address"},
                "subject": {"type": "string", "description": "Email subject line"},
                "body": {"type": "string", "description": "Email body content"},
            },
            "required": ["to", "subject", "body"]
        }
    ),
    ToolDefinition(
        name="get_weather",
        description="Get the current weather for a location.",
        parameters={
            "type": "object",
            "properties": {
                "location": {"type": "string", "description": "City or location name"},
            },
            "required": ["location"]
        }
    ),
    # ... register 100+ tools
])

# 3. Build the search index
pickr.build()

# 4. Get the toolpickr tool schema for your LLM
tool_schema = pickr.get_tool(format="gemini")  # or format="base" for raw dict
system_prompt = pickr.get_system_prompt("You are a helpful assistant.")

# 5. In your LLM tool-call loop, handle toolpickr calls:
result = pickr.handle_tool_call({"action": "search", "queries": ["send an email"]})
# result.data["available_tools"] → list of matching tool schemas

Installation

# Core (includes sentence-transformers for local embeddings + BM25)
pip install toolpickr

# With Google Gemini support
pip install toolpickr[google]

# With OpenAI support
pip install toolpickr[openai]

# Everything
pip install toolpickr[all]

Requirements

  • Python ≥ 3.9
  • Core dependencies: faiss-cpu, numpy, pydantic, rank-bm25, sentence-transformers

How It Works

Architecture

Your LLM ←→ toolpickr (meta-tool)
                │
                ├── action="search"  → Ensemble Retriever → [Reranker] → tool schemas
                │                       ├── Semantic (FAISS cosine similarity)
                │                       └── BM25 (lexical matching)
                │
                └── action="execute" → Route to handler (or return call info)

Retrieval Pipeline

Stage What It Does Config
Semantic Embeds query → FAISS cosine similarity over tool embeddings embedding_model, semantic_weight
BM25 Lexical keyword matching over tool descriptions bm25_weight
Ensemble Min-max normalizes + weighted fusion of both scores semantic_weight, bm25_weight
Reranker (optional) Cross-encoder re-scores top candidates reranker_model, top_p
Final Returns the best top_k tools top_k

Two-Step Protocol

When your LLM receives a user request, it calls the toolpickr tool:

Step 1: Search:

{
  "action": "search",
  "queries": ["send an email", "get the weather"]
}

ToolPickr returns matching tool schemas with their parameter definitions.

Step 2: Execute:

{
  "action": "execute",
  "tool_name": "send_email",
  "tool_arguments": {"to": "john@example.com", "subject": "Hi", "body": "Hello!"}
}

ToolPickr either auto-executes (if a handler is registered) or returns the call info for you to handle.


Configuration

Constructor Parameters

pickr = ToolPickr(
    # Embedding Model Settings
    embedding_model="sentence_transformers/all-MiniLM-L6-v2",  # "provider/model" format
    embedding_api_key=None,       # Optional (auto-discovered from env vars)

    # Reranker Settings (optional)
    reranker_model=None,          # e.g., "cross_encoder/ms-marco-MiniLM-L-6-v2"
    reranker_api_key=None,        # Optional (auto-discovered from env vars)

    # Retrieval Tuning
    top_k=5,                      # Final number of tools returned per query
    top_p=15,                     # Candidates before reranking (only with reranker)
    semantic_weight=0.5,          # Weight for embedding similarity [0.0 - 1.0]
    bm25_weight=0.5,              # Weight for BM25 lexical score [0.0 - 1.0]

    # Execution Settings
    auto_execute=False,           # True = execute tools with handlers automatically

    # Debugging
    debug=False,                  # True = print detailed pipeline logs
)

Embedding Models

Models use the "provider/model" format. API keys are auto-discovered from environment variables.

Model String Provider Env Var Notes
sentence_transformers/all-MiniLM-L6-v2 Local None needed Default. Fast, 384 dims
sentence_transformers/all-mpnet-base-v2 Local None needed Higher quality, 768 dims
google/gemini-embedding-001 Google GEMINI_API_KEY 3072 dims, best quality
google/text-embedding-004 Google GEMINI_API_KEY 768 dims
openai/text-embedding-3-small OpenAI OPENAI_API_KEY 1536 dims, cheapest
openai/text-embedding-3-large OpenAI OPENAI_API_KEY 3072 dims, best quality

Reranker Models

Model String Provider Notes
cross_encoder/ms-marco-MiniLM-L-6-v2 Local Fast, lightweight
cross_encoder/ms-marco-MiniLM-L-12-v2 Local More accurate

Examples

Gemini Integration (Full Example)

import os
from google import genai
from google.genai import types
from toolpickr import ToolPickr, ToolDefinition

# Initialize with Google embeddings (API key from GEMINI_API_KEY env var)
pickr = ToolPickr(
    embedding_model="google/gemini-embedding-001",
    auto_execute=False,
)

# Register tools
pickr.register_tools(my_tools)  # your list of ToolDefinition objects
pickr.build()

# Get the toolpickr schema formatted for Gemini
toolpickr_tool = pickr.get_tool(format="gemini")
system_prompt = pickr.get_system_prompt("You are a helpful assistant.")

# Standard Gemini API: you keep full control of the loop
client = genai.Client(api_key=os.getenv("GEMINI_API_KEY"))
config = types.GenerateContentConfig(
    system_instruction=system_prompt,
    tools=[toolpickr_tool],
)

history = [types.Content(role="user", parts=[types.Part(text="Send an email to john@example.com")])]

for turn in range(5):
    response = client.models.generate_content(
        model="gemini-2.5-flash",
        contents=history,
        config=config,
    )

    candidate = response.candidates[0]
    history.append(candidate.content)

    # Check for function calls
    function_call = None
    for part in candidate.content.parts:
        if hasattr(part, "function_call") and part.function_call:
            function_call = part.function_call
            break

    if not function_call:
        print(f"Response: {candidate.content.parts[0].text}")
        break

    if function_call.name == "toolpickr":
        result = pickr.handle_tool_call(dict(function_call.args))

        if result.action == "search":
            response_data = result.data
        elif result.action == "execute":
            if result.executed:
                response_data = result.data
            else:
                # Route to your tool implementation
                response_data = my_tool_handlers[result.tool_name](**result.data["tool_arguments"])

        history.append(types.Content(
            role="tool",
            parts=[types.Part(function_response=types.FunctionResponse(
                name="toolpickr", response=response_data,
            ))]
        ))

Zero-Config (Local, No API Key)

from toolpickr import ToolPickr

# Uses sentence-transformers locally (no API key needed)
pickr = ToolPickr()
pickr.register_tools(my_tools)
pickr.build()

# Direct search (no LLM required)
results = pickr.retrieve("send an email")
for r in results:
    print(f"  {r.tool_name}: {r.score:.4f}")

With Reranker

pickr = ToolPickr(
    embedding_model="google/gemini-embedding-001",
    reranker_model="cross_encoder/ms-marco-MiniLM-L-6-v2",
    top_k=3,     # Final 3 tools
    top_p=15,    # Reranker picks from top 15 ensemble candidates
)

Auto-Execute Mode

def send_email(to: str, subject: str, body: str):
    print(f"Sending email to {to}: {subject}")
    return {"status": "sent"}

pickr = ToolPickr(auto_execute=True)

# Register tool with its handler
pickr.register_tool(
    ToolDefinition(name="send_email", description="Send an email", parameters=...),
    handler=send_email,
)

pickr.build()

# When the LLM calls execute, ToolPickr runs the handler automatically
result = pickr.handle_tool_call({
    "action": "execute",
    "tool_name": "send_email",
    "tool_arguments": {"to": "john@example.com", "subject": "Hi", "body": "Hello!"}
})

print(result.executed)  # True
print(result.data)      # {"result": {"status": "sent"}}

Custom Retrieval Weights

# Favor semantic similarity (good for descriptive queries)
pickr = ToolPickr(semantic_weight=0.7, bm25_weight=0.3)

# Favor keyword matching (good for exact tool name queries)
pickr = ToolPickr(semantic_weight=0.3, bm25_weight=0.7)

API Reference

ToolPickr

Method Description
register_tool(tool, handler=None) Register a single tool with optional callable handler
register_tools(tools) Bulk-register a list of tools
register_handler(tool_name, handler) Add a handler to an already-registered tool
build() Build the FAISS + BM25 search indexes
get_tool(format="base") Get the toolpickr meta-tool schema ("base" or "gemini")
get_system_prompt(existing="") Get the system prompt with ToolPickr instructions
handle_tool_call(args) Process a toolpickr function call from the LLM, returning a ToolCallResult
retrieve(query, top_k=None) Direct retrieval (returns a list of SearchResult objects)

ToolDefinition

Pydantic model for defining tools:

ToolDefinition(
    name="tool_name",              # Required: unique tool name
    description="What it does",     # Required: natural language description
    parameters=Parameters(          # Optional: JSON Schema for arguments
        type="object",
        properties={
            "param_name": Property(type="string", description="..."),
        },
        required=["param_name"]
    ),
    category="optional_category",   # Optional: used in embeddings
)

ToolCallResult

Returned by handle_tool_call():

Field Type Description
action str "search" or "execute"
success bool Whether the operation succeeded
data dict Response payload (tool schemas or execution result)
tool_name str | None Tool name (for execute actions)
error str | None Error message if success=False
executed bool True if tool was auto-executed, False if routed

Project Structure

toolpickr/
├── __init__.py             # Public API exports
├── _version.py             # Package version
├── pickr.py                # Main facade — ToolPickr class
├── core/
│   ├── tool.py             # ToolDefinition, Parameters, Property (Pydantic)
│   ├── registry.py         # ToolRegistry — stores tools + optional handlers
│   └── results.py          # ToolCallResult dataclass
├── embeddings/
│   ├── base.py             # EmbeddingProvider ABC
│   ├── factory.py          # create_embedding_provider("provider/model")
│   ├── google.py           # Google Gemini embeddings
│   ├── openai.py           # OpenAI embeddings
│   ├── sentence_transformers.py  # Local sentence-transformers
│   └── renderer.py         # Renders ToolDefinition → text for embedding
├── vectorstores/
│   ├── base.py             # VectorStore ABC + SearchResult
│   └── faiss.py            # FAISS vector store with disk persistence
├── retrieval/
│   ├── flat.py             # Semantic retriever (embed query → FAISS search)
│   ├── bm25.py             # BM25 lexical retriever
│   ├── ensemble.py         # Weighted fusion of semantic + BM25
│   └── reranker.py         # Cross-encoder reranker
└── integrations/
    └── gemini.py           # Tool schema converter for Gemini

Environment Variables

ToolPickr auto-discovers API keys from environment variables:

Variable Used By
GEMINI_API_KEY Google embeddings (google/...)
GOOGLE_API_KEY Google embeddings (fallback)
OPENAI_API_KEY OpenAI embeddings (openai/...)

You can always override with explicit embedding_api_key or reranker_api_key parameters.


LLM Integration Support

LLM Provider Tool Schema Format Status
Google Gemini get_tool(format="gemini") ✅ Supported
OpenAI / GPT get_tool(format="base") 🔜 Coming soon
Any LLM get_tool(format="base") ✅ Use raw dict schema

Benchmarks & Testing

Formal benchmark tests and accuracy evaluations are currently pending.


License

Apache License 2.0 (see LICENSE for details).

Metadata

Release files for toolpickr 1.0.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 toolpickr 1.0.0
File Size Uploaded
toolpickr-1.0.0.tar.gz 37.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for toolpickr 1.0.0
File Interpreter ABI Platform
toolpickr-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 81.2 kB

Release files / toolpickr-1.0.0.tar.gz

Download URL toolpickr-1.0.0.tar.gz
Size 37.4 kB
Tags Source
SHA-256 checksum
How to use checksums
3ca919ccc10bb30142d192f73b8adfc99e43ac6e4f79dbe051f7549edb078208
BLAKE2b-256 checksum
How to use checksums
c2bf6460a840a860e8fc2214b2f8e8bad440d3503c57ce9abc50628eca216106
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on May 29, 2026.

Transparency log

Release files / toolpickr-1.0.0-py3-none-any.whl

Download URL toolpickr-1.0.0-py3-none-any.whl
Size 43.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a3fbc7fd21eddf6776cc80468ad51ba633b0543c6b812dca05e46963d4e2edb9
BLAKE2b-256 checksum
How to use checksums
34ca0a33f8f6e939d4a586722f5ed0da136d320c92cbab9eeadbdf3e45e62be4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on May 29, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

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