> ## Documentation Index
> Fetch the complete documentation index at: https://assemblyai.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Test your agent with Bluejay simulations

> Run Bluejay's simulated callers against a stored voice agent through the open-source CHIRP bridge, then review the results on both sides.

[Bluejay](https://getbluejay.ai) runs simulated callers, Digital Humans, against a voice agent and scores what happens: happy paths, interruptions, accents, the edge cases you would never sit through by hand. It reaches an agent over [CHIRP](https://docs.getbluejay.ai/simulation-integrations/websockets), its own WebSocket protocol.

The [bluejay-aai-bridge](https://github.com/AssemblyAI/bluejay-aai-bridge) repo is the piece in between. It translates CHIRP to the Voice Agent API so simulations run against the agent you actually ship:

```
Bluejay ──CHIRP/WebSocket──▶ bridge.py ──Voice Agent API──▶ your agent
        16 kHz PCM + events            24 kHz PCM + JSON events
```

The bridge is transport only. It doesn't define an agent: you give it the id of a [stored agent](/docs/voice-agents/voice-agent-api/create-agent) you already run, and the prompt, voice, tools, and turn detection all come from that agent.

**You need:**

* An [AssemblyAI API key](https://www.assemblyai.com/dashboard/api-keys)
* An `agent_id`. Don't have one? [Create an agent](/docs/voice-agents/voice-agent-api/create-agent) first.
* A Bluejay account. Don't have one? Sign up at [getbluejay.ai](https://getbluejay.ai).
* Python 3.12+

## 1. Clone and configure

```bash theme={null}
git clone https://github.com/AssemblyAI/bluejay-aai-bridge
cd bluejay-aai-bridge
pip install -r requirements.txt
cp .env.example .env
```

Add your key and your agent to `.env`. The agent id is in the agent's URL in the [dashboard](https://www.assemblyai.com/dashboard):

```bash .env icon=gear theme={null}
ASSEMBLYAI_API_KEY=your_key_here
AGENT_ID=7ad24396-b822-4dca-871a-be9cc4781cf9
```

That is the whole setup. If you'd rather keep the agent in the repo as a file you can edit between simulation runs, pull it in and publish it back when you change it:

```bash theme={null}
python import_agent.py <agent-id>     # writes agents/<its-name>.jsonc
AGENT=<name> python bridge.py
```

## 2. Call it yourself first

`call.py` speaks CHIRP the way Bluejay does, so a call that sounds right through it will sound right in a simulation. It records the agent to `out/agent.wav`.

```bash theme={null}
python bridge.py
python call.py --seconds 12    # in another terminal
```

Give it something to say and it will hold a conversation:

```bash theme={null}
say -o q.aiff "Hi, I have a question about my order" \
  && afconvert -f WAVE -d LEI16@16000 -c 1 q.aiff question.wav
python call.py --wav question.wav --seconds 30
```

Lower `--delay` so the question starts while the greeting is still playing, and you have tested barge-in.

## 3. Host the bridge

Bluejay dials in, so the bridge needs a public `wss://` address. Any host that runs Python, terminates TLS, and passes WebSockets through will do. Before hosting, set the Basic-auth credentials Bluejay will send:

```bash .env icon=gear theme={null}
CHIRP_USER=bluejay
CHIRP_PASS=a-long-random-string
```

<Tabs>
  <Tab title="Railway">
    Use the [Railway template](https://railway.com/new/template?template=https%3A%2F%2Fgithub.com%2FAssemblyAI%2Fbluejay-aai-bridge), or from an existing project: **New** → **GitHub Repo** → the bridge repo. Railway reads `.python-version` and `requirements.txt` to build, and the `Procfile` for the start command.

    Under **Variables** set `ASSEMBLYAI_API_KEY`, `AGENT_ID`, `CHIRP_USER`, and `CHIRP_PASS`; `PORT` arrives on its own. Then under **Settings** → **Networking** generate a domain, and give Bluejay `wss://<that domain>/voice`.
  </Tab>

  <Tab title="Render">
    Use the [Deploy to Render](https://render.com/deploy?repo=https://github.com/AssemblyAI/bluejay-aai-bridge) button. Render reads `render.yaml`, prompts for `ASSEMBLYAI_API_KEY` and `AGENT_ID`, generates a `CHIRP_PASS` for you to copy into Bluejay, and sets `PORT` itself.
  </Tab>

  <Tab title="Anywhere else">
    ```bash theme={null}
    pip install -r requirements.txt
    python -u bridge.py
    ```

    One stateless process, no database, nothing shared between calls, so it scales by running more of them. `GET /health` returns the agent id it resolved, which is what to point a health check at.
  </Tab>
</Tabs>

<Tip>
  To try it before deploying anything, put a tunnel in front of it. Bluejay only needs a reachable `wss://` URL:

  ```bash theme={null}
  python bridge.py
  cloudflared tunnel --url http://localhost:8767    # or: ngrok http 8767
  ```
</Tip>

Two things to watch, both of which cost you failed simulations rather than errors you can see:

* **Don't run it on an instance that sleeps when idle.** Simulations arrive in bursts after a long quiet period, which is exactly when a scaled-to-zero instance is cold. The first calls of the run time out on the WebSocket upgrade, and Bluejay records them as `INCOMPLETED`.
* **Concurrent simulations need concurrent sessions.** Each call is one Voice Agent session on your account. Past the limit AssemblyAI returns `concurrency_exceeded`, which the bridge passes to Bluejay as a failed call.

## 4. Point Bluejay at it

In Bluejay, create an Agent with connection type **Websocket**, URL `wss://<your-host>/voice`, and the same `CHIRP_USER` and `CHIRP_PASS` you set on the host. Run a simulation.

## After a simulation

Bluejay has the transcript, the recording, and the evaluations. AssemblyAI has its own record of the same call, with a stereo recording, per-turn timings, and which replies were cut off:

```bash theme={null}
python sessions.py                 # recent sessions
python sessions.py sess_abc123     # download one and print its transcript
```

The bridge prints the session id when each call ends. With `BLUEJAY_API_KEY` set, that id is also written onto the Bluejay simulation result, so the two records point at each other. See [Recordings and transcripts](/docs/voice-agents/voice-agent-api/session-history).

## Configuration

| Variable                   | Default        | Description                                                                                                                                                                                 |
| -------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ASSEMBLYAI_API_KEY`       | required       | Sessions are billed to it. Stays in this process.                                                                                                                                           |
| `AGENT_ID`                 | one of these   | The agent to test. Connected to as it is.                                                                                                                                                   |
| `AGENT`                    |                | Which file in `agents/` to publish and test instead.                                                                                                                                        |
| `AGENT_ID_<NAME>`          |                | The id `python publish.py` saved for that file. Set for you.                                                                                                                                |
| `CHIRP_USER`, `CHIRP_PASS` | before hosting | The Basic-auth pair Bluejay sends. Unset means anyone who can reach the port can start a call on your key.                                                                                  |
| `BLUEJAY_API_KEY`          | optional       | Writes each call's AssemblyAI session id back onto the Bluejay simulation result.                                                                                                           |
| `LOG_LEVEL`                | `INFO`         | `DEBUG` logs every event and the level of the incoming audio.                                                                                                                               |
| `LOG_TRANSCRIPTS`          | off            | Print what was said. `.env.example` sets it to `1` so local runs show the conversation. Leave it out when hosting, since those logs go to a third party; turn counts are logged either way. |
| `PORT`                     | `8767`         | Set for you by Railway and Render.                                                                                                                                                          |

A `.env` next to `bridge.py` is loaded on startup, and real environment variables win over it.

## How the two protocols meet

| Bluejay (CHIRP)                                                  |     | AssemblyAI Voice Agent API                                                                                                                     |
| ---------------------------------------------------------------- | :-: | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| upgrade with `Authorization: Basic` and `X-Simulation-Result-Id` |  ▶  | connect to `/v1/ws` with the API key, then [`session.update`](/docs/voice-agents/voice-agent-api/events-reference#session-update) with `{agent_id}` |
| binary frame, 16 kHz `pcm_s16le`                                 |  ▶  | `input.audio`, base64 at 24 kHz. Held until `session.ready`, which the API needs before it will accept audio.                                  |
| `speech.started` / `speech.completed`                            |  ▶  | logged. The agent's own [turn detection](/docs/voice-agents/voice-agent-api/turn-detection-and-interruptions) works from the audio.                 |
| `speech.started {utterance_id}`                                  |  ◀  | the first `reply.audio` of a reply                                                                                                             |
| binary frames, 20 ms, at real-time pace                          |  ◀  | `reply.audio` chunks                                                                                                                           |
| `speech.completed`, then `mark`                                  |  ◀  | `reply.done` with `status: "completed"`, once the last frame is out                                                                            |
| `speech.completed` at once, queued audio dropped                 |  ◀  | `reply.done` with `status: "interrupted"`                                                                                                      |
| `session.error`, then close `1011`                               |  ◀  | a connection-level `error`, a `session.error` before the session is up, or an unexpected close                                                 |
| close `1000`                                                     |  ▶  | `session.end`, then wait for `session.ended`                                                                                                   |

Two details are worth knowing, because both are the difference between a simulation that measures your agent and one that measures the bridge:

* **Reply audio is paced.** The API sends a reply faster than real time. Forwarding it straight through would put seconds of speech in Bluejay's playback buffer, and an interruption would arrive to find the agent already committed to talking. The bridge stays at most 200 ms ahead, so `reply.done` with `status: "interrupted"` actually stops the voice.
* **Hanging up ends the session.** When Bluejay closes the call the bridge sends `session.end` and waits for `session.ended`. Dropping the socket instead leaves the session resumable, and billable, for another 30 seconds, which over a suite of simulations is real money.

## Cost

Every simulated call is a Voice Agent session billed to the API key in your `.env`, and Bluejay bills its own side. Running a suite costs real money, so keep `CHIRP_USER` and `CHIRP_PASS` set on anything hosted: without them, anyone who finds the URL can start sessions on your key.
