Skip to main content

Documentation Index

Fetch the complete documentation index at: https://patter-06b046ce-feat-observability-otel-attrs-0-6-1.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

Tools & Function Calling

Tools let your voice agent perform actions during a conversation — check a database, call an API, transfer a call, or anything else you can expose via a webhook or an in-process handler.

Defining Tools

Each tool is a dictionary with the following fields:
FieldTypeRequiredDescription
namestrYesUnique identifier for the tool.
descriptionstrYesNatural language description of what the tool does. The AI uses this to decide when to call it.
parametersdictYesJSON Schema defining the tool’s input parameters.
webhook_urlstrOne of webhook_url or handlerURL that Patter POSTs to when the AI invokes this tool.
handlerCallableOne of webhook_url or handlerAsync or sync callable `(arguments, context) -> strdict`. Runs in-process instead of making an HTTP call.
Every tool must have either a webhook_url or a handler. Providing neither raises a ValueError.
agent = phone.agent(
    system_prompt="You are a restaurant reservation assistant.",
    tools=[
        {
            "name": "check_availability",
            "description": "Check if a table is available for a given date, time, and party size.",
            "parameters": {
                "type": "object",
                "properties": {
                    "date": {
                        "type": "string",
                        "description": "Reservation date in YYYY-MM-DD format",
                    },
                    "time": {
                        "type": "string",
                        "description": "Reservation time in HH:MM format",
                    },
                    "party_size": {
                        "type": "integer",
                        "description": "Number of guests",
                    },
                },
                "required": ["date", "time", "party_size"],
            },
            "webhook_url": "https://api.example.com/reservations/check",
        },
        {
            "name": "make_reservation",
            "description": "Book a table reservation.",
            "parameters": {
                "type": "object",
                "properties": {
                    "date": {"type": "string"},
                    "time": {"type": "string"},
                    "party_size": {"type": "integer"},
                    "customer_name": {"type": "string"},
                    "phone": {"type": "string"},
                },
                "required": ["date", "time", "party_size", "customer_name"],
            },
            "webhook_url": "https://api.example.com/reservations/create",
        },
    ],
)

Webhook Request Format

When the AI decides to call a tool, Patter sends an HTTP POST to the webhook_url with the following JSON body:
{
  "tool": "check_availability",
  "arguments": {
    "date": "2025-03-15",
    "time": "19:00",
    "party_size": 4
  },
  "call_id": "call_abc123",
  "caller": "+15550001234",
  "callee": "+15550009876",
  "attempt": 1
}
FieldTypeDescription
toolstrThe name of the tool being invoked.
argumentsdictThe arguments extracted by the AI, matching the JSON Schema.
call_idstrUnique identifier for the current call.
callerstrThe caller’s phone number.
calleestrThe callee’s phone number.
attemptintAttempt number (1, 2, or 3).

Webhook Response

Your webhook must return valid JSON. The response is passed back to the AI as the tool result:
{
  "available": true,
  "tables": [
    {"id": "T5", "seats": 4, "location": "patio"},
    {"id": "T12", "seats": 6, "location": "main"}
  ]
}

Response Requirements

ConstraintValue
Content typeapplication/json
Max response size1 MB
Timeout10 seconds

Retry Behavior

If your webhook fails (non-2xx status code or network error), Patter retries automatically. Same exponential-backoff policy applies to in-process handlers — see Retries & circuit breaker below.

System Tools

Patter automatically injects two system tools into every agent. You do not need to define these — they are always available.

transfer_call

Transfers the current call to another phone number. The AI decides when to trigger this based on the conversation context.
ParameterTypeDescription
numberstrPhone number to transfer to (E.164 format).
The AI might say: “Let me transfer you to our billing department.” and then invoke transfer_call with the appropriate number.

end_call

Ends the current call programmatically.
ParameterTypeDescription
reasonstrReason for ending the call (logged in the transcript).

Using tool()

The tool() static method provides a convenient way to create tool definitions:
agent = phone.agent(
    system_prompt="You are a helpful assistant.",
    tools=[
        tool(
            name="get_order_status",
            description="Look up an order by ID.",
            parameters={
                "type": "object",
                "properties": {
                    "order_id": {"type": "string", "description": "Order ID"},
                },
                "required": ["order_id"],
            },
            webhook_url="https://api.example.com/orders/status",
        ),
    ],
)

In-Process Handlers

Instead of webhook URLs, you can pass a Python callable that runs in-process. This is useful for tools that query local databases, call internal APIs, or perform any logic without an external HTTP endpoint.
async def check_inventory(arguments, context):
    product_id = arguments["product_id"]
    # Your custom logic here
    stock = await db.get_stock(product_id)
    return {"product_id": product_id, "in_stock": stock > 0, "quantity": stock}

agent = phone.agent(
    system_prompt="You are a product specialist.",
    tools=[
        tool(
            name="check_inventory",
            description="Check if a product is in stock.",
            parameters={
                "type": "object",
                "properties": {
                    "product_id": {"type": "string", "description": "Product ID"},
                },
                "required": ["product_id"],
            },
            handler=check_inventory,
        ),
    ],
)
The handler receives two arguments:
  • arguments — A dict of the arguments extracted by the AI
  • context — A dict with call metadata (call_id, caller, callee)
The handler must return a str or dict. Dict values are serialized to JSON before being passed to the AI.

Tool Design Tips

The AI uses the description field to decide when to call a tool. Be specific:
# Good
"description": "Look up the customer's order status by order ID. Returns tracking number and estimated delivery date."

# Bad
"description": "Check order."
Parameter names and descriptions help the AI extract the right values from the conversation:
"properties": {
    "order_id": {
        "type": "string",
        "description": "The order ID, usually starts with 'ORD-' followed by 6 digits",
    },
}
The AI processes the full JSON response. Large responses add latency. Return only what the AI needs to continue the conversation.

Schema validation at build time

Patter structurally validates every tool’s parameters schema the moment you call phone.agent(tools=[...]). Typos that previously failed silently mid-call (required: "name" instead of required: ["name"]) now raise ToolSchemaError immediately, naming the offending tool. The validator checks:
  • The root must be type: "object".
  • properties must be a dict mapping field name to JSON Schema.
  • required must be a list of strings.
  • Every entry in required must exist in properties.
from getpatter import Patter, Tool
from getpatter.tools.schema_validation import ToolSchemaError

try:
    agent = phone.agent(
        system_prompt="...",
        tools=[
            Tool(
                name="lookup_order",
                description="Find an order.",
                parameters={
                    "type": "object",
                    "properties": {"order_id": {"type": "string"}},
                    "required": "order_id",  # bug: should be a list
                },
                webhook_url="https://api.example.com/orders",
            ),
        ],
    )
except ToolSchemaError as e:
    print(e)
    # tool 'lookup_order': `parameters['required']` must be a list of field names.
Validation lives in getpatter.tools.schema_validation and runs once per tool at agent build time. There is no runtime overhead per call.

Streaming progress from long-running tools

Realtime mode only. When a tool takes more than a moment to run — a database query, a multi-step API workflow, a file generation — you can write the handler as an async generator and yield {"progress": "..."} updates while it works. Each progress message is spoken inline by the agent so the caller hears live status instead of dead air. The generator’s final {"result": "..."} yield becomes the function-call result the model sees.
from getpatter import Patter, Tool

async def search_database(arguments, context):
    yield {"progress": "Searching the database..."}
    rows = await db.search(arguments["query"])

    yield {"progress": f"Found {len(rows)} matches, ranking now..."}
    ranked = await rank(rows)

    yield {"result": json.dumps({"top_results": ranked[:5]})}

agent = phone.agent(
    system_prompt="You help customers find products.",
    tools=[
        Tool(
            name="search_products",
            description="Search the product catalogue.",
            parameters={
                "type": "object",
                "properties": {"query": {"type": "string"}},
                "required": ["query"],
            },
            handler=search_database,
        ),
    ],
)
Plain async def handlers continue to work unchanged — streaming is purely opt-in by switching to a generator. Pipeline mode silently discards progress yields for now and uses only the final result; Realtime mode is fully supported.
Async generators in Python don’t surface return values cleanly across the iterator boundary. The agreed protocol is to yield {"result": "..."} as the final yield. Anything yielded after a result is ignored.

Reassurance during long tool calls

Realtime mode only. Even with progress streaming, some tools take a beat before they have anything useful to say. The reassurance field on Tool lets you set a single filler line the agent will speak if the tool hasn’t returned within a grace window (default 1.5 s). If the tool returns earlier, the timer is cancelled and the line is never spoken.
from getpatter import Patter, Tool

agent = phone.agent(
    system_prompt="You are a booking assistant.",
    tools=[
        # String shorthand — fires after the default 1500 ms.
        Tool(
            name="check_availability",
            description="Check open appointment slots.",
            parameters={
                "type": "object",
                "properties": {"date": {"type": "string"}},
                "required": ["date"],
            },
            webhook_url="https://api.example.com/availability",
            reassurance="Let me check the calendar for you...",
        ),

        # Dict form — explicit grace window.
        Tool(
            name="run_credit_check",
            description="Run a credit check.",
            parameters={
                "type": "object",
                "properties": {"ssn": {"type": "string"}},
                "required": ["ssn"],
            },
            handler=credit_check_handler,
            reassurance={
                "message": "One moment while I pull that up — this can take a few seconds.",
                "after_ms": 800,
            },
        ),
    ],
)
Pipeline mode silently skips reassurance for now — there is no clean injection point mid-turn. If you need it for a Pipeline agent, prefer an async-generator handler with a first {"progress": ...} yield.

Retries & circuit breaker

Both handler and webhook tool calls go through the same execution policy:
  • Retries: up to 3 total attempts (default MAX_RETRIES = 2).
  • Backoff: exponential — 500 ms × 2^attempt, jittered up to ~60 ms, capped at 5 s.
  • Failure response: after the last attempt the executor returns a structured JSON error so the model can recover gracefully.
    {"error": "Tool failed after 3 attempts: ...", "fallback": true}
    
In addition, ToolExecutor keeps a per-tool circuit breaker so a flaky downstream doesn’t burn LLM tokens on calls that will keep failing.

State machine

StateWhenBehaviour
CLOSEDDefault.Calls run normally; failures count toward the threshold.
OPENAfter 5 consecutive failures (default).Calls short-circuit immediately for 30 s. The model receives {"error": ..., "fallback": true, "circuit_state": "open", "retry_after_ms": ...}.
HALF_OPENFirst call after the cooldown elapses.One probe call is allowed. Success transitions to CLOSED; failure trips back to OPEN for another cooldown.
When the breaker is OPEN the model can recover with a graceful response such as: “I couldn’t reach the booking system right now — can I take your number and call you back?”

Tunables

from getpatter.tools.circuit_breaker import CircuitBreakerOptions
from getpatter.tools.tool_executor import ToolExecutor

# Disable the breaker entirely (legacy behaviour).
executor = ToolExecutor(
    circuit_breaker=CircuitBreakerOptions(failure_threshold=0),
)

# Tighter -- trip after 3 failures, cool down for 60 s.
executor = ToolExecutor(
    circuit_breaker=CircuitBreakerOptions(
        failure_threshold=3,
        cooldown_s=60.0,
    ),
)
Defaults match the TypeScript SDK byte-for-byte.

OpenAI strict mode (opt-in)

Set strict=True on a tool to constrain the model to emit arguments that exactly match the declared schema — no missing required fields, no extra properties, no type coercion. Recommended for any tool whose handler can’t tolerate malformed arguments (DB writes, payments, transfers).
from getpatter import Patter, Tool

agent = phone.agent(
    system_prompt="You are a payment assistant.",
    tools=[
        Tool(
            name="charge_card",
            description="Charge the customer's saved card.",
            parameters={
                "type": "object",
                "additionalProperties": False,
                "properties": {
                    "amount_cents": {"type": "integer"},
                    "currency": {"type": "string"},
                    "memo": {"type": ["string", "null"]},  # nullable, NOT optional
                },
                "required": ["amount_cents", "currency", "memo"],
            },
            handler=charge_card_handler,
            strict=True,
        ),
    ],
)
When strict=True, Patter:
  1. Validates the schema satisfies OpenAI’s strict-mode requirements at agent build time — raising ToolSchemaError with the offending path on any violation.
  2. Propagates strict: true in the OpenAI Realtime session.update wire payload so the model honours it.

Strict-mode schema rules

Strict mode does not allow truly optional fields. Every property in properties must also appear in required. To express “this field may be absent,” use a nullable union type: {"type": ["string", "null"]}. The model can then pass null instead of omitting the field.
RuleWhy
Root must be type: "object".OpenAI function tools require object roots.
Every nested object must set additionalProperties: False.Prevents the model from inventing extra keys.
Every property in properties must also be in required.Strict mode has no concept of “optional” — use nullable types instead.
Arrays’ items schema is recursively validated under the same rules.Same guarantees inside lists.
Default is strict=False — existing tools keep working with no changes.

Adding third-party tools via MCP

If you need to plug in tools from external services — Google Workspace, GitHub, Postgres, PayPal, and so on — Patter ships an MCP (Model Context Protocol) client that auto-discovers and wires up remote tool servers without writing wrapper handlers per tool. See MCP integration for the full guide.

Complete Example

import os
import asyncio
from dotenv import load_dotenv
from getpatter import Patter, Twilio, OpenAIRealtime

load_dotenv()

phone = Patter(
    carrier=Twilio(),                               # TWILIO_* from env
    phone_number=os.environ["PHONE_NUMBER"],
    webhook_url=os.environ["WEBHOOK_URL"],
)

agent = phone.agent(
    engine=OpenAIRealtime(),                        # OPENAI_API_KEY from env
    system_prompt="""You are a customer service agent for an e-commerce store.
Help customers check order status and process returns.
Always ask for the order ID before looking anything up.""",
    tools=[
        {
            "name": "get_order_status",
            "description": "Look up an order by ID. Returns order status, items, and tracking info.",
            "parameters": {
                "type": "object",
                "properties": {
                    "order_id": {
                        "type": "string",
                        "description": "Order ID (e.g., ORD-123456)",
                    },
                },
                "required": ["order_id"],
            },
            "webhook_url": "https://api.example.com/orders/status",
        },
        {
            "name": "initiate_return",
            "description": "Start a return process for a specific order item.",
            "parameters": {
                "type": "object",
                "properties": {
                    "order_id": {"type": "string"},
                    "item_id": {"type": "string"},
                    "reason": {
                        "type": "string",
                        "enum": ["defective", "wrong_item", "changed_mind", "other"],
                    },
                },
                "required": ["order_id", "item_id", "reason"],
            },
            "webhook_url": "https://api.example.com/returns/initiate",
        },
    ],
    first_message="Hi! I'm here to help with your order. Do you have an order ID I can look up?",
)

async def main():
    await phone.serve(agent, port=8000)

asyncio.run(main())