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):
    """Eres la recepción de Clínica Norte. Hablas de usted, con frases cortas."""

    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:
        """Busca al paciente por nombre y teléfono. Pide los dos antes de llamarla."""
        self.patient = agenda.buscar(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]]:
        """Horas libres de un día."""
        self.slots = agenda.libres(day)
        return self.slots

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

{% if stage == "identify" %}
Saluda y pide nombre y teléfono. Nada más hasta identificar al paciente.
{% endif %}
{% if remembers("médico habitual") %}
Ofrece primero las horas de su médico habitual.
{% endif %}
{% if slots %}
## Horas libres, en orden

{% for hueco in slots %}
{{ hueco.cuando }} con {{ hueco.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("médico habitual") 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"Le he oído: {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 "Saluda y pide nombre" 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.6

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.6
File Size Uploaded
pinecall-0.1.6.tar.gz 82.7 kB Details

Built distribution (wheel)

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

Total release size: 183.9 kB

Release files / pinecall-0.1.6.tar.gz

Download URL pinecall-0.1.6.tar.gz
Size 82.7 kB
Tags Source
SHA-256 checksum
How to use checksums
181f9dcb0ac71575a8719302428ed003b95bba0f9b946030db854428341e5af9
BLAKE2b-256 checksum
How to use checksums
72b9dffa48c9ba3c91341cfad1d814b3ca45e29ca703c3570391f6ec583688ff
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

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

Download URL pinecall-0.1.6-py3-none-any.whl
Size 101.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bf9523b3a588dd075e31d25c60378fcf36213a3d2ba2e8dc9c0cc1a0710d3429
BLAKE2b-256 checksum
How to use checksums
3a5036c5e23fca64107990b1b5ba148c52b78035b35450201d8f47d7762e0ad6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

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

0.1.9

2 release files

This release

0.1.6 This release

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