(Made GPT to write me the ReadmeFile for the POC - reviewed )
A production-inspired real-time contact center AI pipeline built on Google Cloud Platform, simulating the core middleware layer of an enterprise contact center architecture.
POC Focus: Layer 3 — Cloud Middleware & Event Pipeline Simulates what happens after a phone call's audio hits the SIP Gateway — transcript streaming, sentiment analysis, real-time storage, and live dashboard updates.
- What We Wanted to Build
- What We Actually Built
- What We Achieved
- Architecture
- Tech Stack
- Project Structure
- Setup Guide
- Running the Project
- Edge Cases & Known Behaviours
- What's Next
The goal was to understand and implement the Cloud Middleware layer of a real enterprise contact center AI system — specifically the event-driven pipeline that processes live call data.
The reference architecture (enterprise contact center on Google Cloud) processes calls like this:
Phone Call
→ SBC (assigns UCID per caller)
→ SIP Gateway (GCP)
→ Google STT (speech to text)
→ Agent Assist (AI suggestions)
→ Cloud Pub/Sub (event bus)
→ Cloud Run (custom processing)
→ Memorystore + BigQuery (storage)
→ Agent Desktop (Salesforce UI)
We wanted to understand and simulate this pipeline — specifically everything after the SIP Gateway — without needing real telephony hardware.
A fully working simulation of the middleware layer with:
mock_call.py → GCP Pub/Sub → processor/main.py
(fake STT output) (3 topics/subs) (subscriber + WS server)
│
┌──────────┴──────────┐
▼ ▼
Supabase WebSocket
(PostgreSQL) (broadcast)
│ │
└──────────┬──────────┘
▼
dashboard/
(live UI + call history)
| Component | File | What it does |
|---|---|---|
| Config loader | config.py |
Loads all .env variables centrally |
| Data models | models.py |
Pydantic schemas for all event types |
| DB client | database/client.py |
Async Supabase connection (singleton) |
| DB writer | database/writer.py |
Async functions to save each event type |
| Mock publisher | publisher/mock_call.py |
Simulates a full phone call with STT output |
| Processor | processor/main.py |
Pub/Sub subscriber + FastAPI + WebSocket server |
| Dashboard | dashboard/ |
Live UI showing transcript, sentiment, call history |
- ✅ Full event-driven pipeline — publisher → Pub/Sub → processor → DB → dashboard
- ✅ 3 GCP Pub/Sub topics with default subscriptions (
call-events,transcript-events,sentiment-events) - ✅ Async Python architecture — built async from day one for DB swappability
- ✅ Decoupled layers — publisher and processor only communicate via Pub/Sub topics, never directly
- ✅ Real-time dashboard — WebSocket broadcasts every event to connected browser clients instantly
- ✅ Persistent storage — all calls, transcripts, sentiments stored in Supabase PostgreSQL
- ✅ Call history — dashboard fetches past calls from Supabase REST API
- ✅ Config endpoint — dashboard fetches credentials from FastAPI, never hardcoded
- ✅ Message durability — unprocessed Pub/Sub messages survive processor restarts (up to 7 days)
- ✅ Foreign key relationships — transcripts and sentiments are always linked to their parent call via
ucid - ✅ Realtime enabled on all 3 Supabase tables
- ✅ Understood GCP project setup — billing, APIs, IAM, service accounts
- ✅ Understood Pub/Sub — topics, subscriptions, publish/consume, ack/nack
- ✅ Understood async Python — event loops, threading,
run_coroutine_threadsafe - ✅ Understood WebSocket — connection manager, broadcast pattern
- ✅ Understood Repository pattern — DB layer is swappable without touching pipeline code
- ✅ Understood why decoupling matters — each layer only knows about topics, not other layers
┌─────────────────────────────────────────────────────────────────┐
│ YOUR MACHINE │
│ │
│ publisher/mock_call.py │
│ ┌─────────────────────┐ │
│ │ simulate_call() │ │
│ │ - generate UCID │ │
│ │ - fake conversation │ │
│ │ - analyse sentiment │ │
│ └──────────┬──────────┘ │
│ │ publish(bytes) │
└──────────────┼──────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ GOOGLE CLOUD ☁️ │
│ │
│ ┌─────────────────┐ ┌──────────────────┐ ┌───────────────┐ │
│ │ call-events │ │transcript-events │ │sentiment- │ │
│ │ topic │ │topic │ │events topic │ │
│ │ └─ call-events │ │└─transcript- │ │└─sentiment- │ │
│ │ -sub │ │ events-sub │ │ events-sub │ │
│ └────────┬────────┘ └────────┬─────────┘ └──────┬────────┘ │
│ │ │ │ │
└───────────┼────────────────────┼────────────────────┼───────────┘
│ │ │
└────────────────────┼────────────────────┘
│ subscribe (streaming pull)
▼
┌─────────────────────────────────────────────────────────────────┐
│ YOUR MACHINE │
│ │
│ processor/main.py │
│ ┌──────────────────────────────────────────┐ │
│ │ Thread 1 (main): FastAPI + uvicorn │ │
│ │ GET /health │ │
│ │ GET /config │ │
│ │ WS /ws ←── dashboard connects here │ │
│ │ │ │
│ │ Thread 2: async event loop │ │
│ │ save_call() → Supabase │ │
│ │ save_transcript() → Supabase │ │
│ │ save_sentiment() → Supabase │ │
│ │ broadcast() → all WS clients │ │
│ │ │ │
│ │ Thread 3: Pub/Sub subscribers │ │
│ │ handle_call_event() │ │
│ │ handle_transcript_event() │ │
│ │ handle_sentiment_event() │ │
│ └──────────────────────────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
└─────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────────────────────┐
│ SUPABASE ☁️ │ │ YOUR BROWSER │
│ │ │ │
│ calls table │ │ dashboard/index.html │
│ transcripts table │ │ ┌────────────────────────────────┐ │
│ sentiments table │ │ │ 📞 Active Call Info │ │
│ │ │ │ 💬 Live Transcript Feed │ │
│ realtime enabled ✅ │ │ │ 🎭 Sentiment Indicator │ │
└──────────────────────┘ │ │ 📋 Call History Table │ │
│ └────────────────────────────────┘ │
└──────────────────────────────────────┘
calls
├── id UUID (primary key, auto-generated)
├── ucid TEXT UNIQUE (business key, e.g. "CALL-A1B2C3D4")
├── agent_id TEXT
├── caller_id TEXT
├── started_at TIMESTAMPTZ
├── ended_at TIMESTAMPTZ (null until call ends)
└── status TEXT ("active" | "completed")
transcripts
├── id UUID (primary key)
├── ucid TEXT → references calls(ucid)
├── speaker TEXT ("agent" | "caller")
├── text TEXT
├── confidence FLOAT (0.0 - 1.0)
└── created_at TIMESTAMPTZ
sentiments
├── id UUID (primary key)
├── ucid TEXT → references calls(ucid)
├── sentiment TEXT ("positive" | "neutral" | "negative")
├── score FLOAT (-1.0 to 1.0)
└── created_at TIMESTAMPTZ| Layer | Technology | Why |
|---|---|---|
| Event Bus | Google Cloud Pub/Sub | Decoupled, durable, auto-scales |
| Backend | Python 3.11 + FastAPI | Async-first, clean API, WebSocket support |
| Web Server | Uvicorn | ASGI server for FastAPI |
| Data Validation | Pydantic v2 | Runtime type validation (like TypeScript at runtime) |
| Database | Supabase (PostgreSQL) | Free tier, built-in realtime, REST API |
| GCP Auth | Service Account + JSON key | Credentials for GCP APIs |
| Frontend | Vanilla JS + CSS | No framework overhead for a POC |
realtime-call-intelligence/
│
├── .env # Environment variables (never commit!)
├── .gitignore # Ignores credentials.json, .env, venv/
├── credentials.json # GCP service account key (never commit!)
├── requirements.txt # Python dependencies
│
├── config.py # Centralised env loader (like constants.ts)
├── models.py # Pydantic event schemas (like TypeScript interfaces)
│
├── publisher/
│ ├── __init__.py
│ └── mock_call.py # Simulates STT + sentiment events
│
├── processor/
│ ├── __init__.py
│ └── main.py # Pub/Sub subscriber + FastAPI + WebSocket server
│
├── database/
│ ├── __init__.py
│ ├── client.py # Async Supabase singleton client
│ └── writer.py # Async DB write functions
│
└── dashboard/
├── index.html # Dashboard UI
├── style.css # Dark theme styling
└── app.js # WebSocket client + Supabase REST calls
| Tool | Version | Install |
|---|---|---|
| Python | 3.11 | brew install python@3.11 |
| gcloud CLI | latest | cloud.google.com/sdk |
| Git | any | pre-installed on Mac |
- Go to console.cloud.google.com
- Create new project → name it
contact-center-poc - Note the Project ID (e.g.
contact-center-poc-494117)
gcloud billing projects link YOUR_PROJECT_ID \
--billing-account=YOUR_BILLING_ACCOUNT_IDgcloud services enable \
pubsub.googleapis.com \
run.googleapis.com \
bigquery.googleapis.com \
storage.googleapis.com \
secretmanager.googleapis.com \
--project=YOUR_PROJECT_IDOr via UI: console.cloud.google.com/apis/library
Search and enable: Cloud Pub/Sub, Cloud Run, BigQuery, Cloud Storage, Secret Manager
Via UI: console.cloud.google.com/iam-admin/serviceaccounts
- Click + Create Service Account
- Name:
contact-center-sa - Assign roles:
Pub/Sub Admin,BigQuery Admin,Cloud Run Admin - Go to Keys tab → Add Key → JSON → download
- Rename downloaded file to
credentials.json - Place in project root
Via UI: console.cloud.google.com/cloudpubsub/topic/list
Create these 3 topics (check "Add a default subscription" for each):
| Topic ID | Subscription (auto-created) |
|---|---|
call-events |
call-events-sub |
transcript-events |
transcript-events-sub |
sentiment-events |
sentiment-events-sub |
-
Go to supabase.com → create new project
-
Name:
contact-center-poc, Region:South Asia (Mumbai) -
Go to Settings → API and note down:
- Project URL
anonpublic keyservice_rolesecret key
-
Go to SQL Editor → run this:
CREATE TABLE calls (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
ucid TEXT NOT NULL UNIQUE,
agent_id TEXT NOT NULL,
caller_id TEXT NOT NULL,
started_at TIMESTAMPTZ DEFAULT NOW(),
ended_at TIMESTAMPTZ,
status TEXT DEFAULT 'active'
);
CREATE TABLE transcripts (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
ucid TEXT NOT NULL REFERENCES calls(ucid),
speaker TEXT NOT NULL,
text TEXT NOT NULL,
confidence FLOAT DEFAULT 1.0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE sentiments (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
ucid TEXT NOT NULL REFERENCES calls(ucid),
sentiment TEXT NOT NULL,
score FLOAT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
ALTER PUBLICATION supabase_realtime ADD TABLE calls;
ALTER PUBLICATION supabase_realtime ADD TABLE transcripts;
ALTER PUBLICATION supabase_realtime ADD TABLE sentiments;# Clone repo
git clone https://github.com/yourusername/realtime-call-intelligence
cd realtime-call-intelligence
# Create virtual environment with Python 3.11
python3.11 -m venv venv
source venv/bin/activate # Mac/Linux
# venv\Scripts\activate # Windows
# Install dependencies
pip install -r requirements.txtCreate .env in the project root:
# ── Google Cloud ──────────────────────────────
GOOGLE_APPLICATION_CREDENTIALS=./credentials.json
GCP_PROJECT_ID=your-project-id
# ── Pub/Sub Topics ────────────────────────────
PUBSUB_CALL_TOPIC=call-events
PUBSUB_TRANSCRIPT_TOPIC=transcript-events
PUBSUB_SENTIMENT_TOPIC=sentiment-events
# ── Supabase ──────────────────────────────────
SUPABASE_URL=https://xxxxxxxxxxxx.supabase.co
SUPABASE_SERVICE_KEY=your-service-role-key
SUPABASE_ANON_KEY=your-anon-key# Test config loads correctly
python config.py
# Expected: no output (all vars found)
# Test Supabase connection
python -m database.client
# Expected: ✅ Supabase async connected successfully!
# Test DB writes
python -m database.writer
# Expected: all 4 operations succeed, check Supabase table editor
⚠️ Always start the processor BEFORE the publisher to avoid message ordering issues.
source venv/bin/activate
python -m processor.mainExpected output:
📡 Listening on 3 Pub/Sub subscriptions
🔌 WebSocket server: ws://localhost:8000/ws
🏥 Health check: http://localhost:8000/health
Verify endpoints:
- Health: http://localhost:8000/health
- Config: http://localhost:8000/config
Open dashboard/index.html in your browser via Live Server or:
cd dashboard && python -m http.server 3000Then visit: http://localhost:3000
You should see the green connected dot in the header.
source venv/bin/activate
python -m publisher.mock_callWatch the dashboard update live — transcript lines appear one by one, sentiment updates in real time, and call history loads from Supabase.
Problem: If publisher runs before processor starts, Pub/Sub queues messages. When processor starts, it consumes them out of order — transcripts arrive before call_started, causing foreign key violations.
Why: Pub/Sub doesn't guarantee strict ordering across topics.
Fix: Always start processor first. If ordering issues occur, purge subscriptions via: console.cloud.google.com/cloudpubsub/subscription/list → select subscription → Purge messages
Problem: When processor restarts, it consumes ALL unacknowledged messages from previous sessions (Pub/Sub stores them for 7 days).
Why: This is actually a feature — messages aren't lost if processor goes down.
Fix: This is expected behaviour. If you want a clean start, purge subscriptions manually.
Problem: The sentiment analyser uses keyword matching — context-free. "I need help" incorrectly scores as negative because of the word "help".
Why: We deliberately kept sentiment simple to focus on the pipeline architecture.
Fix (future): Replace analyse_sentiment() in mock_call.py with Google Natural Language API:
from google.cloud import language_v1
client = language_v1.LanguageServiceClient()Problem: Any client can connect to ws://localhost:8000/ws and receive all call data.
Why: POC — auth adds complexity without educational value here.
Fix (future): Add JWT token validation in websocket_endpoint():
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket, token: str):
if not verify_token(token):
await websocket.close(code=1008)
returnProblem: Every connected dashboard sees every call, regardless of which agent is handling it.
Why: broadcast() sends to all clients — no filtering by agent.
Fix (future): Track agent ID per WebSocket connection and use send_to_agent() instead of broadcast().
Problem: ModuleNotFoundError: No module named 'google' when running scripts.
Fix: Always activate venv first:
source venv/bin/activate # Mac/LinuxOr set VS Code interpreter to ./venv/bin/python via Cmd+Shift+P → Python: Select Interpreter.
Problem: ModuleNotFoundError: No module named 'pip._vendor.packaging' when using Python 3.14.
Why: Python 3.14 has a broken pip at time of writing.
Fix: Use Python 3.11 explicitly:
brew install python@3.11
python3.11 -m venv venv- Real Google STT — feed actual
.wavaudio files throughgoogle-cloud-speechinstead of hardcoded text - Real sentiment — replace keyword matching with Google Natural Language API
- Per-agent WebSocket — filter events by
agent_idso each dashboard only sees its own call - JWT auth on WebSocket endpoint
- Deploy processor to Cloud Run — containerise with Docker, deploy to GCP
- Retry logic — if
call_startedhasn't arrived yet, queue transcripts and retry
- Asterisk/FreeSWITCH — simulate SBC and SIPREC streaming
- Real SIP Gateway — route audio to GCP's telephony stack
- UCID generation — replicate how SBC assigns UCIDs in real calls
- Rebuild dashboard in React — component-based, TypeScript
- Knowledge Assist panel — surface relevant articles based on transcript
- AI Coach suggestions — use Gemini to suggest responses to agent in real time
- Call summarization — generate summary when call ends
- Salesforce integration — embed dashboard in Salesforce Agent Desktop
- GitHub Actions CI/CD — auto-deploy processor to Cloud Run on push
- Terraform — infrastructure as code for all GCP resources
- Monitoring — Cloud Monitoring + alerting for processor health
- Google Cloud Pub/Sub Docs
- Pub/Sub Python Client
- FastAPI Docs
- Supabase Python Client
- Pydantic v2 Docs
Avinash Sharma — Senior Frontend Engineer
- Currently exploring agentic AI development and GCP infrastructure
- This POC is part of a larger learning roadmap toward production-grade agentic systems
Built as a personal learning POC.