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.1.9

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.1.9
File Size Uploaded
pinecall-0.1.9.tar.gz 82.8 kB Details

Built distribution (wheel)

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

Total release size: 184.1 kB

Release files / pinecall-0.1.9.tar.gz

Download URL pinecall-0.1.9.tar.gz
Size 82.8 kB
Tags Source
SHA-256 checksum
How to use checksums
ac9327de242ad67d0033e343457585189651db00f7497007327fa0e0ba5645c9
BLAKE2b-256 checksum
How to use checksums
43c2a35127ead39d1c90bae9166b1c674a751f9100af1f20160fd2e56fa927b5
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 9, 2026.

Transparency log

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

Download URL pinecall-0.1.9-py3-none-any.whl
Size 101.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3022b0bdc26b871bc6673aa99bf81a5ed5b251e0bc02e184dfea75f0ee7da9eb
BLAKE2b-256 checksum
How to use checksums
036e7aed8d441d1e76cad7a56a26b156f234a651a6e94d8a73557175bdc5bf57
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 9, 2026.

Transparency log

Release history Release notifications | RSS feed

0.10.0

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

This release

0.1.9 This release

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