Skip to main content

Cross-provider LLM tool schema generation from Python type hints. Zero dependencies.

Project description

polytools

PyPI Python CI License: MIT Zero dependencies

Cross-provider LLM tool schema generation from Python type hints.

Write your tool once. Export to OpenAI, Anthropic, Gemini, and MCP — no framework lock-in, no third-party dependencies.

from polytools import tool

@tool
def search_web(query: str, max_results: int = 5) -> list[str]:
    """Search the web and return relevant URLs.

    Args:
        query: The search query string.
        max_results: Maximum number of results to return.
    """
    ...

search_web.to_openai()     # → OpenAI function calling format
search_web.to_anthropic()  # → Anthropic tools format
search_web.to_gemini()     # → Gemini FunctionDeclaration format
search_web.to_mcp()        # → MCP JSON-RPC tool definition
search_web.to_all()        # → all four at once

Why polytools?

Every LLM provider uses a different JSON format for tool/function definitions. Today you either hand-write four separate schemas, lock into a framework that weighs megabytes, or wrestle Pydantic into every project.

polytools is a single decorator — pure Python stdlib, zero external dependencies — that reads your type hints and docstring once and outputs whatever format you need.

Installation

pip install polytools

Requires Python 3.9+. No other dependencies, ever.

Quick Start

from polytools import tool
from typing import Optional, Literal

@tool
def send_email(
    recipient: str,
    subject: str,
    body: str,
    cc: Optional[str] = None,
    priority: Literal["low", "normal", "high"] = "normal",
) -> bool:
    """Send an email message.

    Args:
        recipient: Email address of the primary recipient.
        subject: Subject line of the email.
        body: Full text body of the email.
        cc: Optional CC email address.
        priority: Message priority level.
    """
    ...

OpenAI

import openai

client = openai.OpenAI()
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Send an email to alice@example.com"}],
    tools=[send_email.to_openai()],
)
Schema output
{
  "type": "function",
  "function": {
    "name": "send_email",
    "description": "Send an email message.",
    "parameters": {
      "type": "object",
      "properties": {
        "recipient": {"type": "string", "description": "Email address of the primary recipient."},
        "subject":   {"type": "string", "description": "Subject line of the email."},
        "body":      {"type": "string", "description": "Full text body of the email."},
        "cc":        {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Optional CC email address."},
        "priority":  {"type": "string", "enum": ["low", "normal", "high"], "description": "Message priority level."}
      },
      "required": ["recipient", "subject", "body"]
    }
  }
}

Anthropic

import anthropic

client = anthropic.Anthropic()
response = client.messages.create(
    model="claude-sonnet-4-6",
    messages=[{"role": "user", "content": "Send an email to alice@example.com"}],
    tools=[send_email.to_anthropic()],
)
Schema output
{
  "name": "send_email",
  "description": "Send an email message.",
  "input_schema": {
    "type": "object",
    "properties": {
      "recipient": {"type": "string", "description": "Email address of the primary recipient."},
      "subject":   {"type": "string", "description": "Subject line of the email."},
      "body":      {"type": "string", "description": "Full text body of the email."},
      "cc":        {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Optional CC email address."},
      "priority":  {"type": "string", "enum": ["low", "normal", "high"], "description": "Message priority level."}
    },
    "required": ["recipient", "subject", "body"]
  }
}

Gemini

import google.generativeai as genai

model = genai.GenerativeModel(
    model_name="gemini-1.5-pro",
    tools=[{"function_declarations": [send_email.to_gemini()]}],
)
Schema output
{
  "name": "send_email",
  "description": "Send an email message.",
  "parameters": {
    "type": "OBJECT",
    "properties": {
      "recipient": {"type": "STRING", "description": "Email address of the primary recipient."},
      "subject":   {"type": "STRING", "description": "Subject line of the email."},
      "body":      {"type": "STRING", "description": "Full text body of the email."},
      "cc":        {"type": "STRING", "description": "Optional CC email address."},
      "priority":  {"type": "STRING", "enum": ["low", "normal", "high"], "description": "Message priority level."}
    },
    "required": ["recipient", "subject", "body"]
  }
}

MCP

# In your MCP server's tools/list handler:
tools = [send_email.to_mcp()]
Schema output
{
  "name": "send_email",
  "description": "Send an email message.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "recipient": {"type": "string", "description": "Email address of the primary recipient."},
      "subject":   {"type": "string", "description": "Subject line of the email."},
      "body":      {"type": "string", "description": "Full text body of the email."},
      "cc":        {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Optional CC email address."},
      "priority":  {"type": "string", "enum": ["low", "normal", "high"], "description": "Message priority level."}
    },
    "required": ["recipient", "subject", "body"]
  }
}

Invoking from an LLM response

import json

# OpenAI
tool_call = response.choices[0].message.tool_calls[0]
result = send_email.call(json.loads(tool_call.function.arguments))

# Anthropic
block = response.content[0]  # ToolUseBlock
result = send_email.call(block.input)

Supported Type Annotations

Python type JSON Schema output
str {"type": "string"}
int {"type": "integer"}
float {"type": "number"}
bool {"type": "boolean"}
bytes {"type": "string", "format": "byte"}
None / type(None) {"type": "null"}
list / List[T] / list[T] {"type": "array", "items": {...}}
dict / Dict[K, V] / dict[K, V] {"type": "object", "additionalProperties": {...}}
tuple[T, ...] {"type": "array", "items": {...}}
tuple[T1, T2, T3] {"type": "array", "prefixItems": [...]}
set[T] / frozenset[T] {"type": "array", "uniqueItems": true, "items": {...}}
Optional[T] {"anyOf": [{...}, {"type": "null"}]}
Union[T1, T2] {"anyOf": [{...}, {...}]}
Literal["a", "b"] {"type": "string", "enum": ["a", "b"]}
Any {} (no constraints)
Unannotated {} (no constraints)

Nested types are fully supported: list[dict[str, Optional[int]]] works as expected.

Docstring Styles

Parameter descriptions are parsed automatically from Google, NumPy, and reStructuredText docstrings:

Google (recommended)

def f(x: int, y: str = "hello") -> bool:
    """Summary line.

    Args:
        x: Description of x.
        y: Description of y.
    """

NumPy

def f(x: int, y: str = "hello") -> bool:
    """Summary line.

    Parameters
    ----------
    x : int
        Description of x.
    y : str, optional
        Description of y.
    """

reStructuredText

def f(x: int, y: str = "hello") -> bool:
    """Summary line.

    :param x: Description of x.
    :param y: Description of y.
    """

API Reference

@tool

Decorator that wraps a Python function and exposes provider schema methods. Can be used with or without parentheses:

@tool
def my_func(...): ...

@tool()
def my_func(...): ...

Tool methods

Method Returns Description
.to_openai() dict OpenAI Chat Completions tools list entry
.to_anthropic() dict Anthropic Messages API tools list entry
.to_gemini() dict Gemini function_declarations list entry
.to_mcp() dict MCP tools/list response entry
.to_all() dict[str, dict] All four, keyed by provider name
.call(args: dict) Any Invoke the function with an LLM argument dict

Tool attributes

Attribute Type Description
__wrapped__ Callable The original unwrapped function
__name__ str Preserved from the original function
__doc__ str Preserved from the original function

Notes on Gemini

Gemini's API does not support anyOf in tool schemas. Optional[T] parameters are handled by omitting them from the required list rather than encoding nullability in the schema type. Union types are resolved to the first non-null type.

Contributing

See CONTRIBUTING.md.

License

MIT — see LICENSE.


Part of the aenealabs AI agent toolkit.

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

polytools-0.1.1.tar.gz (25.5 kB view details)

Uploaded Source

Built Distribution

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

polytools-0.1.1-py3-none-any.whl (20.4 kB view details)

Uploaded Python 3

File details

Details for the file polytools-0.1.1.tar.gz.

File metadata

  • Download URL: polytools-0.1.1.tar.gz
  • Upload date:
  • Size: 25.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for polytools-0.1.1.tar.gz
Algorithm Hash digest
SHA256 3c236b3488bd5eccc4a363ac79cd32b4bc1192c61145ada188294f2826988dd1
MD5 32ec253d56c95f325a6b49f9b55f7fdb
BLAKE2b-256 f1e007cd35edc830774b9299e7e4bb377167501e6de271c2e5c10d02e7d1be40

See more details on using hashes here.

File details

Details for the file polytools-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: polytools-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 20.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for polytools-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 8f7bcb44a7f5873b7b418307c8f7de214e022679fbc03ddc1a38622ae308caa8
MD5 94d2c65b59f69514954cd3713bfe88e1
BLAKE2b-256 e70feac1ce97a5971ff75bf3a0f171144b9d3d5e84704ca01caf9d946fdfa64d

See more details on using hashes here.

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