mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-04-11 18:08:16 +00:00
211 lines
11 KiB
Markdown
211 lines
11 KiB
Markdown
# Helix Engage Server — Sidecar Backend
|
|
|
|
NestJS sidecar that bridges Ozonetel telephony APIs with the FortyTwo platform. Handles agent auth, call control, disposition, missed call queue, worklist aggregation, AI enrichment, and live call assist.
|
|
|
|
**Owner: Kartik**
|
|
|
|
## Architecture
|
|
|
|
```
|
|
┌─────────────────────┐ ┌──────────────────────┐ ┌─────────────────────┐
|
|
│ helix-engage │ │ helix-engage-server │ │ FortyTwo Platform │
|
|
│ React frontend │────▶│ (this repo) │────▶│ GraphQL API │
|
|
│ │ │ Port 4100 │ │ Port 4000 │
|
|
└─────────────────────┘ └──────────────────────┘ └─────────────────────┘
|
|
│
|
|
│ Ozonetel CloudAgent APIs
|
|
▼
|
|
┌──────────────┐
|
|
│ Ozonetel │
|
|
│ in1-ccaas-api│
|
|
└──────────────┘
|
|
```
|
|
|
|
This server has **no database**. All persistent data flows to/from the FortyTwo platform via GraphQL. Ozonetel is the telephony provider (CloudAgent APIs).
|
|
|
|
**Three repos:**
|
|
| Repo | Purpose | Owner |
|
|
|------|---------|-------|
|
|
| `helix-engage` | React frontend | Mouli |
|
|
| `helix-engage-server` (this) | NestJS sidecar | Kartik |
|
|
| `helix-engage-app` | FortyTwo SDK app — entity schemas | Shared |
|
|
|
|
## Getting Started
|
|
|
|
```bash
|
|
npm install
|
|
npm run start:dev # http://localhost:4100 (watch mode)
|
|
npm run build # Production build
|
|
npm run start:prod # Run production build
|
|
```
|
|
|
|
### Environment Variables
|
|
|
|
| Variable | Purpose | Default |
|
|
|----------|---------|---------|
|
|
| `PORT` | Server port | `4100` |
|
|
| `CORS_ORIGIN` | Allowed frontend origin | `http://localhost:5173` |
|
|
| `PLATFORM_GRAPHQL_URL` | FortyTwo GraphQL endpoint | `http://localhost:4000/graphql` |
|
|
| `PLATFORM_API_KEY` | FortyTwo API key (server-to-server) | — |
|
|
| `EXOTEL_API_KEY` | Ozonetel API key | — |
|
|
| `EXOTEL_API_TOKEN` | Ozonetel API token | — |
|
|
| `EXOTEL_ACCOUNT_SID` | Ozonetel account SID | — |
|
|
| `OZONETEL_AGENT_ID` | Default agent ID | `agent3` |
|
|
| `OZONETEL_AGENT_PASSWORD` | Default agent password | — |
|
|
| `OZONETEL_SIP_ID` | Default SIP extension | `521814` |
|
|
| `OZONETEL_DID` | Inbound DID number | `918041763265` |
|
|
| `OZONETEL_CAMPAIGN_NAME` | Default campaign | `Inbound_918041763265` |
|
|
| `MISSED_QUEUE_POLL_INTERVAL_MS` | Missed call ingestion interval | `30000` |
|
|
| `OPENAI_API_KEY` | For AI enrichment / call assist | — |
|
|
| `ANTHROPIC_API_KEY` | Alternative AI provider | — |
|
|
| `DEEPGRAM_API_KEY` | Live transcription (STT) | — |
|
|
|
|
## Module Structure
|
|
|
|
```
|
|
src/
|
|
├── ozonetel/ # ⚡ Ozonetel telephony — WHERE MOST WORK HAPPENS
|
|
│ ├── ozonetel-agent.controller.ts # REST endpoints for agent operations
|
|
│ ├── ozonetel-agent.service.ts # Ozonetel API wrapper (token, CDR, abandon calls)
|
|
│ ├── ozonetel-agent.module.ts # Module wiring
|
|
│ └── kookoo-ivr.controller.ts # IVR callback handler (XML responses)
|
|
│
|
|
├── worklist/ # Agent task queue + missed call queue
|
|
│ ├── worklist.controller.ts # GET /api/worklist, missed queue endpoints
|
|
│ ├── worklist.service.ts # Aggregates leads + missed calls + follow-ups
|
|
│ ├── missed-queue.service.ts # Ingestion, dedup, auto-assignment
|
|
│ ├── missed-call-webhook.controller.ts # Webhook receiver
|
|
│ └── kookoo-callback.controller.ts # Kookoo webhook
|
|
│
|
|
├── call-events/ # Real-time call event processing
|
|
│ ├── call-events.service.ts # Incoming call handling, AI enrichment, disposition logging
|
|
│ ├── call-events.gateway.ts # WebSocket push to frontend (Socket.IO)
|
|
│ └── call-lookup.controller.ts # Reverse phone lookup + AI enrichment
|
|
│
|
|
├── platform/ # FortyTwo platform GraphQL client
|
|
│ ├── platform-graphql.service.ts # query() for server-to-server, queryWithAuth() for user JWT
|
|
│ ├── platform.types.ts # Lead, Call, Activity types
|
|
│ └── platform.module.ts
|
|
│
|
|
├── search/ # Cross-entity search
|
|
│ └── search.controller.ts # GET /api/search — leads + patients + appointments
|
|
│
|
|
├── call-assist/ # Live call assistance
|
|
│ └── (Socket.IO namespace /call-assist, Deepgram STT, AI suggestions)
|
|
│
|
|
├── ai/ # AI enrichment (lead summaries, suggested actions)
|
|
├── auth/ # User auth proxy
|
|
├── graphql-proxy/ # GraphQL passthrough to platform
|
|
├── health/ # Health check endpoint
|
|
├── config/
|
|
│ └── configuration.ts # All env var loading
|
|
├── app.module.ts # Root module — imports all feature modules
|
|
└── main.ts # NestJS bootstrap (port 4100, CORS)
|
|
```
|
|
|
|
## API Endpoints
|
|
|
|
### Ozonetel Agent (`/api/ozonetel/`)
|
|
|
|
| Method | Path | Purpose |
|
|
|--------|------|---------|
|
|
| POST | `/agent-login` | Agent login to Ozonetel |
|
|
| POST | `/agent-logout` | Agent logout |
|
|
| POST | `/agent-state` | Change state (Ready/Pause) + auto-assign missed call on Ready |
|
|
| POST | `/agent-ready` | Force ready (logout + login) |
|
|
| POST | `/dispose` | Submit call disposition + update missed call status + auto-assign next |
|
|
| POST | `/dial` | Manual outbound dial |
|
|
| POST | `/call-control` | CONFERENCE, HOLD, UNHOLD, MUTE, UNMUTE, KICK_CALL |
|
|
| POST | `/recording` | Pause/unpause recording |
|
|
| GET | `/missed-calls` | Raw Ozonetel abandon calls |
|
|
| GET | `/call-history?date=` | CDR for a date |
|
|
| GET | `/performance?date=` | Aggregated agent metrics |
|
|
|
|
### Worklist (`/api/worklist/`)
|
|
|
|
| Method | Path | Purpose |
|
|
|--------|------|---------|
|
|
| GET | `/` | Agent's worklist (missed calls + follow-ups + leads) |
|
|
| GET | `/missed-queue` | Missed calls grouped by callback status |
|
|
| PATCH | `/missed-queue/:id/status` | Update callback status on a missed call |
|
|
|
|
### Other
|
|
|
|
| Method | Path | Purpose |
|
|
|--------|------|---------|
|
|
| GET | `/api/search?q=` | Cross-entity search (leads, patients, appointments) |
|
|
| POST | `/api/call/lookup` | Reverse phone lookup + AI enrichment |
|
|
| GET | `/api/health` | Health check |
|
|
| POST | `/graphql` | GraphQL proxy to platform |
|
|
|
|
## Troubleshooting Guide — Where to Look
|
|
|
|
### "Agent can't log in to Ozonetel"
|
|
**File:** `src/ozonetel/ozonetel-agent.controller.ts` → `agentLogin()`
|
|
**Service:** `src/ozonetel/ozonetel-agent.service.ts` → `loginAgent()`
|
|
Uses HTTP Basic auth to Ozonetel's `AgentAuthenticationV2` endpoint. "Already logged in" responses have `status: "error"` but are not real errors. Check `OZONETEL_AGENT_ID` and `OZONETEL_AGENT_PASSWORD` env vars.
|
|
|
|
### "Disposition failing / ACW not releasing"
|
|
**File:** `src/ozonetel/ozonetel-agent.controller.ts` → `dispose()`
|
|
**Service:** `src/ozonetel/ozonetel-agent.service.ts` → `setDisposition()`
|
|
All dispositions currently map to `'General Enquiry'` (campaign limitation). Uses `autoRelease: 'true'` to end ACW. If agent stays in ACW, the Ozonetel campaign's wrapup time (8s) may not have elapsed.
|
|
|
|
### "Missed calls not being ingested"
|
|
**File:** `src/worklist/missed-queue.service.ts` → `ingest()`
|
|
Runs on a 30s interval (`onModuleInit`). Polls Ozonetel `abandonCalls` API for the last 5 minutes. Look for log lines with `[MissedQueueService]`. Common issues: Ozonetel token expired (55-min cache), platform API key missing, phone number format mismatch.
|
|
|
|
### "Auto-assignment not working"
|
|
**File:** `src/worklist/missed-queue.service.ts` → `assignNext()`
|
|
Triggered from two places: `dispose()` and `agent-state()` in `ozonetel-agent.controller.ts`. Queries platform for oldest `PENDING_CALLBACK` call with empty `agentName`. Uses a mutex to prevent race conditions. If no calls are assigned, check that `callbackstatus` field exists on the Call entity (custom field, all-lowercase in GraphQL).
|
|
|
|
### "Worklist returning empty"
|
|
**File:** `src/worklist/worklist.service.ts`
|
|
Three parallel queries: `getMissedCalls()`, `getPendingFollowUps()`, `getAssignedLeads()`. All filter by `agentName`. If the agent name from the JWT doesn't match what's stored in lead/call records, results will be empty. Check `resolveAgentName()` in `worklist.controller.ts`.
|
|
|
|
### "Call events / webhooks not arriving"
|
|
**File:** `src/call-events/call-events.service.ts`
|
|
Ozonetel sends webhooks to the sidecar. Check that the webhook URL is configured in the Ozonetel dashboard and that the sidecar is reachable from the internet (Caddy reverse proxy on the VPS).
|
|
|
|
### "AI enrichment / call assist broken"
|
|
**Files:** `src/ai/`, `src/call-assist/`
|
|
Live transcription uses Deepgram Nova STT via raw WebSocket. AI suggestions use OpenAI gpt-4o-mini. Check `DEEPGRAM_API_KEY` and `OPENAI_API_KEY` env vars. The call-assist gateway uses Socket.IO namespace `/call-assist`.
|
|
|
|
### "Search not finding records"
|
|
**File:** `src/search/search.controller.ts`
|
|
Runs three parallel GraphQL queries (leads, patients, appointments), filters client-side. Requires minimum 2 characters. Uses the user's JWT (passed from frontend auth header).
|
|
|
|
## Key Technical Patterns
|
|
|
|
### Two Auth Models
|
|
1. **User JWT passthrough** — `platform.queryWithAuth(query, vars, authHeader)` — for user-facing endpoints (worklist, search). The frontend sends its JWT and the sidecar forwards it.
|
|
2. **Server API key** — `platform.query(query, vars)` — for server-to-server operations (missed call ingestion, auto-assignment). Uses `PLATFORM_API_KEY`.
|
|
|
|
### Ozonetel Token Caching
|
|
`ozonetel-agent.service.ts` → `getToken()` caches the bearer token for 55 minutes (tokens expire at 60 min). All CloudAgent API calls use this cached token.
|
|
|
|
### Custom Field Naming
|
|
Fields added via the FortyTwo admin portal use **all-lowercase** GraphQL names:
|
|
- `callbackstatus` (not `callbackStatus`)
|
|
- `callsourcenumber` (not `callSourceNumber`)
|
|
- `missedcallcount` (not `missedCallCount`)
|
|
- `callbackattemptedat` (not `callbackAttemptedAt`)
|
|
|
|
App-defined (managed) fields keep camelCase: `callStatus`, `agentName`, etc.
|
|
|
|
### Error Handling Pattern
|
|
Ozonetel endpoints return `{ status: 'error', message }` instead of throwing — this prevents UI from blocking on telephony failures. The frontend catches errors silently on disposition and recording.
|
|
|
|
## Deployment
|
|
|
|
```bash
|
|
npm run build
|
|
# Then tar + scp + docker cp + restart (see deploy script in project docs)
|
|
```
|
|
|
|
The sidecar runs inside a Docker container (`fortytwo-staging-sidecar-1`) on the staging VPS.
|
|
|
|
## Git Workflow
|
|
|
|
- `dev` — active development
|
|
- `master` — stable baseline
|