Metadata-Version: 2.4
Name: piopiy-agent
Version: 1.0.0
Summary: Connect a voice agent to the TeleCMI telephony platform
License: MIT
Project-URL: Homepage, https://piopiy.com
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: grpcio>=1.60
Requires-Dist: protobuf>=4.25

# piopiy-agent

Connect a voice agent to the TeleCMI telephony platform.

```bash
pip install piopiy-agent
```

No `protoc`. No `grpcio-tools`. No protobuf version clash. The stubs ship
pre-generated, built against an old runtime, so they load under whatever
protobuf your agent framework pins.

## Check your credentials

```bash
export PIOPIY_AGENT_ID=agent_42
export PIOPIY_TOKEN=eyJhbGciOi...

python -m piopiy_agent
```

A `.env` in the working directory is picked up too, if `python-dotenv` is
installed.

```
OK    registered in 41ms
      accept_deadline_ms 400
```

Do this before anything else. It separates "my credentials are wrong" from
"my agent code is wrong", which otherwise look identical.

## Take calls

```python
import asyncio
from piopiy_agent import PiopiyWorker

worker = PiopiyWorker(max_sessions=10)

@worker.on_job
async def handle(job):
    # Everything needed to join is on the job. The token is pre-minted and
    # scoped to this one room - you never hold LiveKit admin credentials.
    await my_agent.join(job.livekit_url, job.access_token, job.room_name)

    # ONLY once you are actually in the room. The caller is bridged in on
    # the strength of this call.
    await job.accept()

    await my_agent.run()   # returns when the call ends

asyncio.run(worker.run())
```

`job.call` carries `from_number`, `to_number`, `direction`, `call_uuid` and any
whitelisted SIP headers.

## The one rule

**Accept only after you are in the room.**

TeleCMI bridges the caller the moment you accept. Accept on *receiving* the
job and the caller arrives in an empty room and hears silence — which is the
single failure this whole design exists to prevent.

You have `job.deadline_ms` (400 by default) to join. `job.remaining_ms` tells
you what is left.

If you join late, `job.accept()` returns **False** and sends nothing: the call
has gone to another instance, and a late accept would put two agents on one
call. Leave the room when you see False.

## What the SDK handles so you do not have to

- **Registration and reconnects.** A TeleCMI restart ends your stream with
  `CANCELLED`, not `UNAVAILABLE`. `CANCELLED` is non-retryable by gRPC
  convention, so a client reconnecting only on `UNAVAILABLE` silently stays
  down after every deploy. This reconnects on any end of stream, with backoff.
- **Serving late.** Reports `NOT_SERVING` until one call has actually been
  accepted. A gRPC channel opens long before a Python process can reach a
  media server, and a fresh instance reports zero active sessions — so it
  looks like the *best* candidate exactly when it is least able to answer.
- **Capacity.** Stops accepting at `max_sessions` and rejects with
  `AT_CAPACITY`. Be honest with this number: a worker that takes more than it
  can serve produces dead air, where a fast rejection costs TeleCMI 2ms and it
  moves on.
- **Heartbeats and status.**
- **A handler that returns without accepting** is rejected rather than left to
  time out — the caller is listening to silence for every millisecond of it.

## Configuration

Constructor arguments, or environment:

| | |
|---|---|
| `TELECMI_AGENT_ID` | from the dashboard |
| `TELECMI_TOKEN` | from the dashboard |
| `PIOPIY_REGISTER` | default `grpc.piopiy.com:50051` |
| `PIOPIY_TLS` | `false` only against a local register |
| `TELECMI_INSTANCE_ID` | defaults to hostname-pid |

## Why the stubs are vendored

Generated protobuf code carries the version it was built with, and the runtime
refuses to load gencode **newer** than itself. `pip install grpcio-tools`
resolves protobuf 7.x, while agent frameworks commonly pin 5.x — so generating
locally produces stubs your own environment cannot import:

```
VersionError: gencode 7.35.1 runtime 5.29.6
```

The error names protobuf, not gRPC and not TeleCMI, which is what makes it
expensive to diagnose. Shipping stubs built against an old runtime removes the
problem: protobuf accepts a runtime newer than the gencode, never older.

Verified against protobuf 4.25, 5.29 and 7.36.
