A streaming telephony ingestion and AI-assisted supervisor workflow that couples explicit LangGraph processing with asynchronous multi-agent document analysis.
This architecture addresses several integration and decoupling challenges:
- Receiving continuous telephony audio: Ingesting inbound audio as a continuous stream from Twilio Media Streams rather than waiting for call completion.
- Transcribing telephony-compatible audio: Forwarding
mulawaudio directly to streaming Speech-to-Text (Deepgram). - Processing conversational context: Turning final caller transcripts into actionable input for an AI workflow (LangGraph) with model-driven tool routing.
- Synchronizing live events: Bridging Python-generated AI events and transcripts to a React-based human-facing dashboard via Socket.IO.
- Decoupling heavy machine learning: Keeping long-running, sequential multi-agent document analysis outside the latency-sensitive HTTP request path by relying on asynchronous queueing.
| Area | Implementation Evidence |
|---|---|
| Voice ingress | Twilio Media Streams to Python WebSocket server |
| Conversation orchestration | LangGraph with model-driven tool routing |
| Dashboard synchronization | Node.js /internal_emit webhook triggering Socket.IO broadcasts |
| Document processing | CrewAI sequential multi-agent workflow |
| Persistence | MongoDB (Mongoose ODM) |
| Rate limiting | express-rate-limit backed by Redis |
| Task queue | RabbitMQ (durable queues, non-persistent messages) |
| Deployment | Docker Compose with local cloudflared tunneling |
flowchart TD
%% Telephony Ingestion Path
Caller[Phone Caller] -->|PSTN| Twilio[Twilio Webhook]
Twilio -->|Inbound Media Stream| PythonWS[Python Voice WebSocket]
PythonWS -->|Audio Bytes| Deepgram[Deepgram STT]
Deepgram -->|Final Transcript| PythonWS
PythonWS -->|compiled_graph.invoke| LangGraph[LangGraph Engine]
%% Dashboard Synchronization Path
LangGraph -->|AI Event / Suggestion| InternalEmit["Node /internal_emit"]
InternalEmit -->|Socket.IO| React[React Dashboard]
%% Document Processing Path
React -->|Upload| NodeAPI[Node.js Express API]
NodeAPI -->|Publish Task| RabbitMQ[(RabbitMQ)]
RabbitMQ -->|Consume Task| PythonWorker[Python Worker Process]
PythonWorker -->|Process.sequential| CrewAI[CrewAI Workflow]
CrewAI -->|Result Publication| NodeAPI
%% Database and Cache
NodeAPI -->|CRUD| Mongo[(MongoDB)]
PythonWorker -->|Update Claim| Mongo
NodeAPI -->|Rate Limiting| Redis[(Redis)]
Note: Redis is connected only to the Node.js API for rate limiting and is not currently configured as a Socket.IO adapter.
- Node.js Express API (
backend-node):- Responsibility: Manages standard HTTP endpoints, MongoDB reads/writes, JWT validation, RabbitMQ publishing, and Socket.IO broadcasts.
- State Owned: HTTP sessions, Socket.IO connections.
- Why the boundary exists: Isolates network-bound I/O and web server operations from CPU-heavy ML tasks.
- Python Voice/WebSocket Process (
realtime-voice):- Responsibility: Terminates inbound Twilio Media Streams, handles Deepgram STT bridging, executes local HuggingFace sentiment analysis, and traverses the LangGraph state machine.
- State Owned: Process-global
conversation_historyandaudio_queue. - Why the boundary exists: Keeps heavy Python ML dependencies and stateful voice streams separate from the Node.js event loop.
- Python RabbitMQ Worker (
python-worker):- Responsibility: Consumes document verification tasks, executes sequential CrewAI agents, and updates MongoDB.
- Why the boundary exists: Offloads long-running, CPU-bound ML work from both the voice process and the Node API.
- React Frontend: Client-side UI managing dashboard state via Zustand.
- MongoDB: Persistent data store.
- Redis: Rate-limiting backing store.
- RabbitMQ: Asynchronous task decoupling buffer.
The exact executable path for an incoming call:
- Twilio hits the Node
/twilio/voiceHTTP webhook. - Node returns TwiML with
<Connect><Stream url="..."/></Connect>. - Twilio initiates a WebSocket connection to
twilio_ws_handlerin the Python service. - Python receives
"media"events and forwards base64 decodedmulawbytes to the Deepgram WSS URL. receive_from_deepgramyields messages.- The code filters transcripts via
if res.get("is_final"):. - Final transcripts are appended to history and passed to
process_conversation. compiled_graph.invokeevaluates the LangGraph nodes.- An AI response or suggestion is generated.
- Python executes
requests.post('http://127.0.0.1:5000/internal_emit'). - Node.js processes the internal HTTP POST and calls
socketio.emit. - React dashboard receives the event and updates the UI.
sequenceDiagram
participant Twilio
participant PythonWS
participant Deepgram
participant LangGraph
participant NodeAPI
participant React
Twilio->>PythonWS: WSS media events (mulaw)
PythonWS->>Deepgram: WSS audio chunk
Deepgram-->>PythonWS: JSON Transcript
Note over PythonWS: if res.get("is_final"):
PythonWS->>LangGraph: compiled_graph.invoke()
LangGraph-->>PythonWS: Generate suggestion
PythonWS->>NodeAPI: POST /internal_emit (AI event)
NodeAPI->>React: Socket.IO broadcast
- It is not a bidirectional voice agent: There is no Text-to-Speech (TTS) stage wired into the active call path.
- Callers do not hear the AI: No AI-generated audio is sent as
"media"back to Twilio. - Partial transcripts are not processed: The code explicitly waits for Deepgram's end-of-speech detection (
is_final).
The AI conversational workflow uses an explicit LangGraph topology.
stateDiagram-v2
[*] --> generate
generate --> router_after_generate
state router_after_generate <<choice>>
router_after_generate --> tools : if tool_calls
router_after_generate --> reflect : if no tool_calls
tools --> generate
reflect --> [*]
- Explicit Topology: The graph structure is explicitly defined via
StateGraph(AgentState)with concrete nodes (generate,tools,reflect). - Model-Driven Runtime Routing: The execution path is not strictly deterministic. The
router_after_generatefunction examineslast_message.tool_calls. Because the LLM dynamically decides whether to issue a tool call, the runtime route selection is inherently non-deterministic. - Context Truncation: Before invoking the model, the conversation history is aggressively truncated (
list(messages)[-6:]) to control token consumption and manage API rate limits.
The LangGraph tool_node provides schema-described tool calling.
query_claim_status(claim_id): Queries MongoDB directly to retrieve a claim's real-time status, type, and priority.search_policy_vectors(query): Queries a local FAISS index containing pre-computed policy embeddings.schedule_task(title, description, due_date, client_email): Posts a JSON payload to the Node API/api/tasksendpoint, generating a new scheduled task and broadcasting a Socket.IO event.
The tool definitions use standard typed signatures, prompting the LLM to emit compatible JSON arguments. Failure within a tool generally returns an error string directly into the LangGraph state context.
Groq's API strictly enforces rate limits (Tokens Per Minute). The safe_chat_invoke wrapper handles 429 errors.
Execution Trace:
- Try
chat.invoke(messages). - Catch Exception. Check if error string contains "rate_limit" or "429".
- Wait initial delay (
delay = 3seconds). - Exponential backoff (
delay *= 2). - Max retries (5 iterations).
- If retries exhaust,
process_conversationcatches the final exception and falls back to emitting a static error string ("⚠️ BPO Suggestion Draft: [Suggestion currently unavailable...]").
The Python Voice Server cannot emit Socket.IO directly to the frontend because Node.js owns the Socket.IO server.
Trace: Python AI Event -> requests.post('http://.../internal_emit') -> Node.js Express handler -> io.emit('event', data) -> React listener.
- Security Gap: The
/internal_emitwebhook in Node.js currently lacks internal authentication (e.g., mTLS, pre-shared key). - Socket.IO Scaling: Node.js currently uses the default in-process Socket.IO adapter. Multiple Node instances would not automatically share Socket.IO broadcasts without introducing a distributed adapter like Redis.
Long-running CrewAI document analysis is decoupled from the HTTP request path.
sequenceDiagram
participant React
participant NodeAPI
participant RabbitMQ
participant PythonWorker
participant MongoDB
React->>NodeAPI: POST /api/documents/upload
NodeAPI->>NodeAPI: Save file locally
NodeAPI->>RabbitMQ: channel.sendToQueue('document_processing')
NodeAPI-->>React: HTTP 200 (Accepted)
RabbitMQ->>PythonWorker: Consume Message
PythonWorker->>PythonWorker: Process.sequential CrewAI
PythonWorker->>RabbitMQ: ch.basic_ack()
PythonWorker->>RabbitMQ: sendToQueue('verification_results')
NodeAPI->>RabbitMQ: Consume Result
NodeAPI->>MongoDB: Update claim.validation_status
By placing RabbitMQ in between, the Node.js API immediately responds to the client upload. RabbitMQ decouples CrewAI execution from the Node.js HTTP request path and Node process.
The document_crew.py script orchestrates three specialized agents:
- OCR and Data Extraction Specialist: Reads PyTesseract/PyPDF2 text and extracts structured entities.
- Fraud Detection Analyst: Consumes the extracted entities to check for logical inconsistencies.
- Policy Alignment Agent: Assesses the fraud report and entities to make a final Approved/Rejected decision.
Execution Mode: Process.sequential. The agents execute one after another in a linear pipeline. The application does not parallelize their execution.
The system utilizes RabbitMQ primarily for asynchronous workload decoupling. The system does not provide exactly-once or uniform at-least-once processing semantics. A worker crash before ACK can cause redelivery, while handled application exceptions are ACKed in the finally path and discarded. Non-persistent queued messages are also not guaranteed to survive broker restart.
- queue durability:
true. The queues themselves survive a broker restart. - message persistence: Messages are not explicitly published with
delivery_mode: 2orpersistent: true. - auto_ack:
false(via library defaults). Manual acknowledgment (basic_ack) is used. - prefetch_count:
1(ensures the Python worker only accepts one active CrewAI job at a time). - requeue behavior: The Python worker acknowledges messages (
basic_ack) inside afinallyblock even if an exception occurs, meaning failed tasks are discarded rather than requeued.
Failure Matrix:
| Scenario | Current Behavior | Risk |
|---|---|---|
| Worker crashes (OOM/Segfault) before ACK | TCP connection drops. RabbitMQ re-delivers message to another worker. | Task is processed twice. |
| Handled CrewAI Python exception | Worker catches exception, logs it, and basic_acks the message. |
Document is never processed; task is lost. |
| RabbitMQ broker restarts | Durable queue survives. | Non-persistent messages are not guaranteed to survive a RabbitMQ broker restart. |
The system uses Mongoose (ODM) to interact with MongoDB.
erDiagram
AGENT ||--o{ CLAIM : manages
CLIENTAUTH ||--o{ CLAIM : owns
CLAIM ||--o{ TASK : has
CLAIM {
String claimID
String clientEmail
String claimType
Number priority
String status
String validation_status
String documentAnalysis
}
CrewAI eventually mutates the validation_status and documentAnalysis fields on the Claim document based on its findings.
- JWT Handling:
bcryptjsfor password hashing, JWTs issued and verified inhttpOnlycookies. - Role Separation: Enforced via distinct middleware:
protectRoute(for BPO agents) andprotectClientRoute(for clients). - Onboarding: A plaintext generated password is created via
crypto.randomBytes()when a client submits their first claim, and is emailed directly to them.
Security Gaps:
- The
/internal_emitAPI route lacks authentication. - Twilio webhook signature validation is absent.
- Auto-generated plaintext passwords sent via email should ideally mandate a password rotation upon first login.
Redis is actively used as the backing store for express-rate-limit.
Execution: connectRedis() -> new RedisStore() -> app.use("/api/", limiter). The limiter enforces a strict 500 requests per 15-minute window per IP.
Redis is not currently configured as the Socket.IO adapter. Because Socket.IO uses the default in-process adapter, horizontally scaling the Node.js API to multiple instances would break real-time frontend event broadcasting for users connected to different nodes.
The backend/src/index.js file registers a scheduled callback using node-cron (*/1 * * * *).
This callback executes inside the main Node.js process and initiates an asynchronous HTTP POST to /api/claims/assign. It does not spawn a separate OS thread, worker_thread, or child process.
The auto_tunnel.py script is developer tooling designed to bypass local NAT/Firewall restrictions. It detects/downloads the Cloudflare cloudflared binary, launches it as a subprocess, regex-extracts the dynamic HTTPS URL from stdout, mutates the .env file, and issues a REST call to the Twilio API to update the phone number's webhook routing.
| State | Owner | Persistence | Scope | Concurrency Risk |
|---|---|---|---|---|
| Claim State | MongoDB | Disk | Global | Application-dependent; document-level persistence does not provide task idempotency or workflow-level stale-update protection. |
| LangGraph Context | Python LLM.py |
Memory | Process-Global | High |
| Socket.IO Clients | Node.js | Memory | Process-Local | High (Breaks on multi-node) |
| RabbitMQ Tasks | RabbitMQ | Memory | Broker-Global | Medium (Non-persistent messages) |
The Process-Global Conversation History Flaw:
In backend/python/LLM.py, conversation_history is declared as a module-global list. It is not keyed by a Twilio CallSid or Stream ID.
If Call A and Call B occur simultaneously, Call A's Deepgram final transcript will append to conversation_history, and Call B's final transcript will append to the exact same list. LangGraph will evaluate an interleaved context containing utterances from completely distinct phone calls. The Python WebSocket server allows concurrent connections to access the same process-global conversation history.
| Scenario | Detection | Current Response | Persistent State Impact |
|---|---|---|---|
| Deepgram Disconnect | try/except in WS task |
Logs error, stops streaming audio | None |
| Groq 429 Limit | safe_chat_invoke catch |
Exponential backoff up to 5 times | AI event delayed |
| Groq Retries Exhaust | safe_chat_invoke catch |
Returns static error string to frontend | Fallback text emitted |
| RabbitMQ Restart | Connection failure | Non-persistent messages are dropped | In-flight tasks lost |
| Worker Handled Exception | Python try/except |
Logs error, ACKs message to queue | Task discarded |
| Simultaneous Phone Calls | None (Architectural Flaw) | Mutates global history array | Cross-call context corruption |
- Network-Bound Work: Twilio WSS ingestion, Deepgram STT bridging, Groq API invocations, MongoDB CRUD, internal HTTP emissions.
- Local Process-Isolated Work: HuggingFace sentiment analysis pipeline, LangGraph state traversal, PyTesseract OCR.
- Asynchronous Queue Work: CrewAI sequential execution.
Verified execution shape: the repository contains manual single-call E2E tooling; no formal concurrent-call capacity test or load test is present.
Known Code-Derived Bottlenecks:
- Code-derived concurrency limitation: process-global
conversation_historydoes not provide session isolation across simultaneous calls. - Groq TPM (Tokens Per Minute) limits constrain the conversational cadence.
prefetch_count=1limits the Python worker to a single active CrewAI document verification at a time.- The default Socket.IO adapter limits the Node API to a single process.
Potential Scale-Out Directions (Proposed, not implemented):
- Keying voice state by
StreamSid. - Adding
@socket.io/redis-adapterfor multi-node Node API deployments. - Publishing RabbitMQ messages with
persistent: truefor true durability. - Implementing DLQ (Dead Letter Queue) topologies for failed CrewAI jobs.
The repository relies on a manual E2E test script (trigger_call.py) to verify Twilio connectivity. There is currently no verified automated test suite (no unit tests, integration tests, or latency benchmark instrumentation).
| Current Choice | Behavioral Benefit | Cost / Limitation |
|---|---|---|
| Twilio Media Streams over TwiML | Streams raw audio immediately instead of waiting for a completed recording. | Requires maintaining a complex, stateful WebSocket connection for raw mulaw audio. |
| LangGraph over Direct LLM API | Explicit nodes provide structured tool execution paths. | Runtime routing is strictly model-driven, making testing complex. |
| RabbitMQ for Document AI | Decouples long-running CrewAI execution from the HTTP request path. | Introduces broker infrastructure overhead and requires careful ACK management. |
| Process-Global Conversation State | Simple to implement for single-caller demonstrations. | Does not provide conversation-state isolation across concurrent calls. |
| Non-Persistent RMQ Messages | Faster broker memory throughput. | Non-persistent messages are not guaranteed to survive a RabbitMQ broker restart. |
Based on executable evidence, the current vulnerabilities are prioritized as:
- P0: The
/internal_emitNode API lacks internal authentication, allowing arbitrary internal/external network callers (if exposed) to trigger Socket.IO events. - P0: Process-global
conversation_historyallows distinct phone calls to leak PII into each other's LangGraph contexts. - P1: Auto-generated plaintext passwords sent via email do not mandate a forced reset upon first login.
- P1: Twilio webhook signatures are not validated, meaning anyone can simulate a call to the webhook.
P0 (Immediate Architecture Fixes):
- Secure
/internal_emitvia pre-shared key or mTLS. - Isolate voice state per
StreamSid/ connection inLLM.py.
P1 (Resilience & Semantics):
- Explicitly mark RabbitMQ task messages as persistent.
- Change Python worker error handling to NACK failed CrewAI tasks into a DLQ instead of ACKing them.
- Validate Twilio webhook signatures.
P2 (Scalability & Validation):
- Configure Redis as a distributed Socket.IO adapter to allow scaling the Node.js API horizontally.
- Implement formal test suites (Jest/PyTest).
- Docker & Docker Compose
- Node.js (v18+)
- Python 3.12+
- Twilio Account
- API Keys (Deepgram, Groq, Pinecone, Google)
git clone <repository>
cd EyHacks
npm install
cd backend && npm installCreate .env in the root directory:
TWILIO_ACCOUNT_SID=
TWILIO_AUTH_TOKEN=
TWILIO_PHONE_NUMBER=
DEEPGRAM_API_KEY=
API_KEY=
JWT_SECRET=
SMTP_USER=
SMTP_PASS=docker compose up -dNote: Leave running in a separate terminal.
python3.12 -m venv .venv
source .venv/bin/activate
pip install -r backend/python/requirements.txt
python3 auto_tunnel.pynpm run dev