Skip to main content

pinecall

Build voice and chat agents as Python classes. Annotated fields are the agent's state, @tool methods are what the model can call, docstrings are what it reads about them, and a Jinja view renders the part of the prompt that changes during the call.

from typing import Literal

from pinecall import Agent, state, tool

from clinica import agenda  # your own system: a CRM, an API, a database


class ClinicaNorte(Agent):
    """You are the front desk of Clínica Norte. Formal, short sentences."""

    stage: Literal["identify", "book"] = "identify"
    patient: dict[str, str] | None = state(None, pii=True)
    slots: list[dict[str, str]] = []

    @tool(stage="identify", pii=("name", "phone"))
    def find_patient(self, name: str, phone: str) -> dict[str, str] | None:
        """Finds the patient by name and phone. Ask for both before calling it."""
        self.patient = agenda.find(name, phone)
        if self.patient:
            self.stage = "book"
        return self.patient

    @tool(stage="book", preview=2)
    def free_slots(self, day: str) -> list[dict[str, str]]:
        """Free slots on one day."""
        self.slots = agenda.free(day)
        return self.slots

views/clinica-norte.jinja, beside the class, renders the prompt from that state:

{% if stage == "identify" %}
Greet the caller and ask for their name and phone. Nothing else until the patient is identified.
{% endif %}
{% if remembers("usual doctor") %}
Offer their usual doctor's slots first.
{% endif %}
{% if slots %}
## Free slots, in order

{% for slot in slots %}
{{ slot.when }} with {{ slot.doctor }}
{% endfor %}
{% endif %}

The prompt has four named blocks: identity, knowledge and tools are static and cached before the history; view comes after it and is replaced every turn. A block is re-sent only when its text changes.

Memory and knowledge-base results never enter the prompt. The platform runs recall and search and returns their results to the model as tool results, so retrieved text is treated as data, not instructions. A view can ask remembers("usual doctor") and write its own sentence about the answer.

Five minutes

uv add pinecall, or pip install pinecall. PyPI pinecall 0.1.0 to 0.1.5 are the runtime's releases, before it moved to pinecall-runtime: a project pins pinecall>=0.1.6.

This package is a library: the verbs are the one pinecall CLI's, the same for every language. In a project laid out as agents/<name>/agent.py, with this package in its .venv:

npm i -g pinecall                     # the CLI: Node, whatever the agent is written in
pinecall link                         # this folder tied to your org: its key, in ./.env
pinecall prompt --state test/<name>/goldens/a.json   # the exact prompt a state produces. No gateway.
pinecall chat                         # the agent served from this terminal, and a caller against it
pinecall test                         # ring 1: the goldens, scored by the gateway
pinecall start                        # registered and answering: the process you deploy

The CLI never loads your class: for prompt, chat, test and start it runs this package's serve entry, .venv/bin/python -m pinecall.serve, and talks to it through the gateway. The console, the knowledge bases, memory, keys and every other verb are the CLI's. How a server runs the agent, with the CLI or inside your own Python process, is docs/production.md.

Architecture

This package is the application side. It never talks to a model vendor or handles audio: it sends commands and reads log entries over one WebSocket, in the runtime's own wire (pinecall.wire), held to the runtime's golden call log. Tools and hooks are the only code that may change state.

you want you use
the class and the view pinecall.Agent, pinecall.bridge.mount
the agent held inside your own process await pinecall.hold(ClinicaNorte, url=…, api_key=…)
the socket alone, your own way of deciding what to answer pinecall.Client
a suite with no network, no key and no model pinecall.testing
import asyncio
import os

from pinecall import Client
from pinecall.wire.events import UserTurnEnded


async def main() -> None:
    pc = Client("https://cloud.pinecall.io", os.environ["PINECALL_KEY"])
    agent = pc.agent("clinica-norte")

    def heard(event, call):  # every turn.user of every call this socket serves
        if isinstance(event, UserTurnEnded):
            call.say(f"I heard: {event.text}")

    agent.on("turn.user", heard)
    await pc.connect()
    await asyncio.Event().wait()


asyncio.run(main())

Testing it

Agent tests run with no network, key, model or gateway, in a plain pytest:

from pinecall.testing import Gateway, load

ClinicaNorte = load("agents/clinica-norte/agent.py")

with Gateway() as pc:
    pc.mount(ClinicaNorte)
    call = pc.call_started(from_="+34600123456")
    call.tool("find_patient", name="Marta Ruiz", phone="600123456")

    assert "ask for their name" not in call.prompt
    assert call.tools == ["free_slots"]

Where the rest is

what where
tutorial: an agent with knowledge and memory, from an empty directory docs/tutorial.md
every file, entity and rule ARCHITECTURE.md
how to write an agent, step by step docs/writing-an-agent.md
the view, and the blocks of a prompt docs/the-view.md
how to test one docs/testing-an-agent.md
running it in production docs/production.md
the CLI, verb by verb the one CLI's reference, at docs.pinecall.io
a complete example agent examples/clinica_norte
the wire itself src/pinecall/wire/, the runtime's shapes this package speaks

Developing

uv sync
make check        # ruff, pyright strict, deptry, the suite and the example's own

Requirements

Python 3.11+. Runtime dependencies: pydantic, Jinja2, websockets and httpx. The pinecall CLI is a Node program (Node 24+): the verbs need it, the library does not.

License

Apache-2.0.

Metadata

Release files for pinecall 0.10.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pinecall 0.10.0
File Size Uploaded
pinecall-0.10.0.tar.gz 87.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pinecall 0.10.0
File Interpreter ABI Platform
pinecall-0.10.0-py3-none-any.whl Python 3 none any Details

Total release size: 191.8 kB

Release files / pinecall-0.10.0.tar.gz

Download URL pinecall-0.10.0.tar.gz
Size 87.1 kB
Tags Source
SHA-256 checksum
How to use checksums
79f6849a40d821898f8cee5fa2b1bc5eb3a1b4847c0968256f83b0646b500ab1
BLAKE2b-256 checksum
How to use checksums
fd1248a072edabb295508bc1c315ecc60c6eb243563535bd8ab9d9e1b6bb1403
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.

Transparency log

Release files / pinecall-0.10.0-py3-none-any.whl

Download URL pinecall-0.10.0-py3-none-any.whl
Size 104.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3e288d4562141f53337a0ba529289bc4d3622684ce0bdbc5307b4136d361cd74
BLAKE2b-256 checksum
How to use checksums
5931a22cae0f8e4b0698279729b0e2001785aeadc2d6345853b1fd8dea7c6cf8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.10.0 This release

2 release files

0.1.13

2 release files

0.1.12

2 release files

0.1.11

2 release files

0.1.10

2 release files

0.1.9

2 release files

0.1.6

2 release files

0.1.5

1 release file

0.1.4

1 release file

0.1.3

1 release file

0.1.2

1 release file

0.1.1

1 release file

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page