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.0.tar.gz (24.8 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.0-py3-none-any.whl (20.1 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: polytools-0.1.0.tar.gz
  • Upload date:
  • Size: 24.8 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.0.tar.gz
Algorithm Hash digest
SHA256 ea820604e4a3b73775f82120afa69a6f18604a0fd0203ae3fdfcc1db52a4ad4b
MD5 9a6910a91879ff64584b8c40a93f8fca
BLAKE2b-256 cad691b8b56bfd5d711c25a3fe38474c79421c602442e2742cc5c7f67e20a3f1

See more details on using hashes here.

File details

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

File metadata

  • Download URL: polytools-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 20.1 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.0-py3-none-any.whl
Algorithm Hash digest
SHA256 55cee6f45e5263d41c772978f81703d1db6c51b89e634f46e571fff771adee5f
MD5 6349084da05c9423327540e16bc7d6f8
BLAKE2b-256 3861bb7631e03c33796bf9075c54c32ae443c24975a67ecd519619e813756415

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