Skip to main content

Professional-grade ensemble-based RAG retrieval system (semantic + BM25) with cross-encoder reranking

Project description

๐Ÿ”ง 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).

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

toolpickr-1.0.0.tar.gz (37.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

toolpickr-1.0.0-py3-none-any.whl (43.8 kB view details)

Uploaded Python 3

File details

Details for the file toolpickr-1.0.0.tar.gz.

File metadata

  • Download URL: toolpickr-1.0.0.tar.gz
  • Upload date:
  • Size: 37.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for toolpickr-1.0.0.tar.gz
Algorithm Hash digest
SHA256 3ca919ccc10bb30142d192f73b8adfc99e43ac6e4f79dbe051f7549edb078208
MD5 c9d45a23be4f196a83307494c827726c
BLAKE2b-256 c2bf6460a840a860e8fc2214b2f8e8bad440d3503c57ce9abc50628eca216106

See more details on using hashes here.

Provenance

The following attestation bundles were made for toolpickr-1.0.0.tar.gz:

Publisher: publish.yml on smoke-2k3/toolpickr

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file toolpickr-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: toolpickr-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 43.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for toolpickr-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a3fbc7fd21eddf6776cc80468ad51ba633b0543c6b812dca05e46963d4e2edb9
MD5 87a1af127ab20fe08e09bd28ff8f39d2
BLAKE2b-256 34ca0a33f8f6e939d4a586722f5ed0da136d320c92cbab9eeadbdf3e45e62be4

See more details on using hashes here.

Provenance

The following attestation bundles were made for toolpickr-1.0.0-py3-none-any.whl:

Publisher: publish.yml on smoke-2k3/toolpickr

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page