Skip to content

Live — voice calls (Voice AI + Realtime + Inkbox TTS/STT) #2

Live — voice calls (Voice AI + Realtime + Inkbox TTS/STT)

Live — voice calls (Voice AI + Realtime + Inkbox TTS/STT) #2

Workflow file for this run

name: Live — voice calls (Inkbox TTS/STT + realtime)
# Boots the agent-under-test (AUT) gateway plus a driver process that bridges the
# other side of a real phone call over its own Inkbox tunnel. Two matrix legs:
# inbound_inkbox — driver calls the agent; agent answers with Inkbox STT/TTS.
# outbound_realtime — driver texts "call me"; the agent calls back powered by the
# OpenAI Realtime API.
# Each leg verifies the stored call transcript shows the agent spoke to the caller.
# Real model + real calls, so this runs only on ready (non-draft) PRs + dispatch, and
# shares the AUT tunnel lock with the other live suites.
on:
pull_request:
branches: [main]
types: [opened, synchronize, reopened, ready_for_review]
workflow_dispatch:
inputs:
timeout_s:
description: "Seconds to wait for the call/transcript"
default: "220"
permissions:
contents: read
concurrency:
# Same group as the other live suites: only one holder of the AUT tunnel at a time.
group: inkbox-live-aut-tunnel
cancel-in-progress: false
jobs:
voice:
runs-on: ubuntu-latest
timeout-minutes: 45
# Skip fork PRs (no secrets) and draft PRs (expensive). Pushes + dispatch always run.
if: >-
(github.event_name != 'pull_request' || (github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.draft == false))
strategy:
fail-fast: false
max-parallel: 1 # legs share the AUT identity → one at a time
matrix:
scenario: [inbound_inkbox, outbound_realtime]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: actions/setup-node@v4
with:
node-version: "22"
- name: Set up env paths
run: |
echo "CODEX_HOME=$RUNNER_TEMP/codex-home" >> "$GITHUB_ENV"
echo "CODEX_PROJECT_DIR=$RUNNER_TEMP/project" >> "$GITHUB_ENV"
echo "GATEWAY_LOG=$RUNNER_TEMP/gateway.log" >> "$GITHUB_ENV"
echo "DRIVER_LOG=$RUNNER_TEMP/driver.log" >> "$GITHUB_ENV"
echo "DRIVER_STATE=$RUNNER_TEMP/driver_state.json" >> "$GITHUB_ENV"
mkdir -p "$RUNNER_TEMP/codex-home" "$RUNNER_TEMP/project"
# uvicorn[standard] matters: the bare install can't accept WebSocket upgrades,
# and the driver's call-media endpoint is a WebSocket.
- name: Install bridge + test deps
run: pip install -e . pytest fastapi 'uvicorn[standard]'
# @alpha is the prerelease channel cut from codex main near-daily — the
# freshest main build available without compiling the host from source.
- name: Install Codex (freshest main prerelease)
run: |
npm install -g @openai/codex@alpha
codex --version
- name: Configure AUT identity + model + speech path (${{ matrix.scenario }})
env:
CODEX_INKBOX_API_KEY: ${{ secrets.CODEX_INKBOX_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
HANDLE="$(python3 - <<'PYEOF'
import os
from inkbox import Inkbox
c = Inkbox(api_key=os.environ["CODEX_INKBOX_API_KEY"], base_url=os.environ.get("INKBOX_BASE_URL", "https://inkbox.ai"))
print(c.mailboxes.list()[0].email_address.split("@", 1)[0])
PYEOF
)"
echo "AUT handle: $HANDLE"
# The agent's reasoning (deciding to place a call, composing replies)
# uses the chat model; authenticate the codex CLI with the API key.
printenv OPENAI_API_KEY | codex login --with-api-key
{
echo "INKBOX_IDENTITY=$HANDLE"
echo "INKBOX_ALLOW_ALL_USERS=true"
echo "CODEX_MODEL=gpt-5.5"
# Unattended runner: nobody answers approval texts → never escalate,
# and keep the sandbox read-only so stray commands stay harmless.
echo "CODEX_SANDBOX=read-only"
echo "CODEX_APPROVAL_POLICY=never"
} >> "$GITHUB_ENV"
if [ "${{ matrix.scenario }}" = "outbound_realtime" ]; then
# Realtime key falls back to OPENAI_API_KEY in the gateway env.
echo "INKBOX_REALTIME_ENABLED=true" >> "$GITHUB_ENV"
else
echo "INKBOX_REALTIME_ENABLED=false" >> "$GITHUB_ENV"
fi
- name: Start gateway and wait for readiness
env:
INKBOX_API_KEY: ${{ secrets.CODEX_INKBOX_API_KEY }}
INKBOX_SIGNING_KEY: ${{ secrets.CODEX_INKBOX_SIGNING_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
inkbox-codex run > "$GATEWAY_LOG" 2>&1 &
echo $! > "$RUNNER_TEMP/gateway.pid"
echo "Waiting for the gateway (tunnel + webhooks + call channel)…"
for i in $(seq 1 36); do # up to ~180s
if grep -q "tunnel ready" "$GATEWAY_LOG" && grep -q "\[bridge\] phone" "$GATEWAY_LOG"; then
echo "Gateway ready."; exit 0
fi
sleep 5
done
echo "::error::gateway did not become ready"; cat "$GATEWAY_LOG"; exit 1
- name: Start voice driver and wait for its tunnel
env:
REMOTE_INKBOX_API_KEY: ${{ secrets.REMOTE_INKBOX_API_KEY }}
run: |
VOICE_DRIVER_STATE="$DRIVER_STATE" VOICE_DRIVER_PORT=8090 \
python3 "$GITHUB_WORKSPACE/tests/live/voice_driver.py" > "$DRIVER_LOG" 2>&1 &
echo $! > "$RUNNER_TEMP/driver.pid"
for i in $(seq 1 30); do # up to ~90s
if [ -s "$DRIVER_STATE" ]; then echo "driver ready:"; cat "$DRIVER_STATE"; exit 0; fi
sleep 3
done
echo "::error::driver did not become ready"; cat "$DRIVER_LOG"; exit 1
- name: Run voice test (${{ matrix.scenario }})
env:
REMOTE_INKBOX_API_KEY: ${{ secrets.REMOTE_INKBOX_API_KEY }}
CODEX_INKBOX_API_KEY: ${{ secrets.CODEX_INKBOX_API_KEY }}
VOICE_SCENARIO: ${{ matrix.scenario }}
LIVE_VOICE_TIMEOUT: ${{ github.event.inputs.timeout_s || '220' }}
run: |
LIVE_REAL_MODEL=1 VOICE_DRIVER_STATE="$DRIVER_STATE" \
python3 -m pytest tests/live/test_voice.py -v
# Failure-only: these logs carry live call content and this repo is public.
- name: Dump logs (on failure only)
if: failure()
run: |
echo "=== gateway.log ==="; cat "$GATEWAY_LOG" || true
echo "=== driver.log ==="; cat "$DRIVER_LOG" || true
- name: Tear down (always)
if: always()
run: |
kill "$(cat "$RUNNER_TEMP/driver.pid" 2>/dev/null)" 2>/dev/null || true
kill "$(cat "$RUNNER_TEMP/gateway.pid" 2>/dev/null)" 2>/dev/null || true
sleep 3 # let the driver revert its number on exit
notify:
needs: [voice]
if: always() && needs.voice.result == 'failure' && github.event_name == 'workflow_run'
runs-on: ubuntu-latest
steps:
- name: Notify Google Chat on scheduled failure
run: |
curl -sS --max-time 10 --retry 3 -X POST "${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }}" \
-H 'Content-Type: application/json' \
-d '{"text": "⚠️ *FAILED* — Live voice-call suite\n\nRun: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}' || true