Cross-provider LLM tool schema generation from Python type hints. Zero dependencies.
Project description
polytools
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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ea820604e4a3b73775f82120afa69a6f18604a0fd0203ae3fdfcc1db52a4ad4b
|
|
| MD5 |
9a6910a91879ff64584b8c40a93f8fca
|
|
| BLAKE2b-256 |
cad691b8b56bfd5d711c25a3fe38474c79421c602442e2742cc5c7f67e20a3f1
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
55cee6f45e5263d41c772978f81703d1db6c51b89e634f46e571fff771adee5f
|
|
| MD5 |
6349084da05c9423327540e16bc7d6f8
|
|
| BLAKE2b-256 |
3861bb7631e03c33796bf9075c54c32ae443c24975a67ecd519619e813756415
|