| name | hume-evi-langgraph |
| description | Integrate Hume EVI voice AI with LangGraph using interrupt/resume patterns. Use when building voice-based AI agents that need Twilio call handling, Hume EVI persona creation, transcript fetching with emotion extraction, and LangGraph state management across the call lifecycle. Covers dynamic Hume config creation, TwiML generation, webhook handling, chat_group event fetching, and emotion timeline extraction. |
Hume EVI + LangGraph Integration
Architecture
Single LangGraph StateGraph with interrupt/resume:
receive_call → verify_pin → select_persona → create_hume_config → generate_twiml
→ await_call_end [INTERRUPT] → fetch_transcript → analyze → coach → store → END
The interrupt boundary separates pre-call (synchronous) from post-call (webhook-triggered).
Critical Patterns
1. Interrupt/Resume for Async Calls
from langgraph.types import interrupt, Command
def await_call_end(state):
resume_data = interrupt({"reason": "waiting_for_webhook"})
return {**state, "chat_id": resume_data["chat_id"]}
graph.invoke(Command(resume={"chat_id": "xxx"}), config)
2. Hume Config Creation
Create dynamic EVI configs per call. Set temperature low (0.6) to prevent default enthusiasm:
request_body = {
"evi_version": "3",
"name": f"Session-{persona_name}-{timestamp}",
"prompt": {"text": voice_prompt},
"voice": {"provider": "HUME_AI", "name": "KORA"},
"language_model": {
"model_provider": "OPEN_AI",
"model_resource": "gpt-4o-mini",
"temperature": 0.6,
},
"event_messages": {"on_new_chat": {"enabled": True, "text": first_message}},
"webhooks": [{"events": ["chat_ended"], "url": webhook_url}],
}
resp = httpx.post("https://api.hume.ai/v0/evi/configs", json=request_body, headers=headers)
3. TwiML Redirect (not Stream)
twiml = f'''<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say voice="Polly.Matthew">Connecting now.</Say>
<Redirect>https://api.hume.ai/v0/evi/twilio?config_id={config_id}&api_key={api_key}</Redirect>
</Response>'''
Use & not & — this is inside XML.
4. Transcript Fetching (⚠️ Known Bug Zone)
Hume's /chats/{id}/events returns 404. Must use chat_groups:
chat_resp = httpx.get(f"https://api.hume.ai/v0/evi/chats/{chat_id}", headers=headers)
chat_group_id = chat_resp.json().get("chat_group_id")
events_resp = httpx.get(
f"https://api.hume.ai/v0/evi/chat_groups/{chat_group_id}/events",
headers=headers, params={"page_size": 100}
)
events = events_resp.json().get("events_page", [])
Field names are snake_case: message_text, emotion_features (not messageText).
5. Emotion Extraction
for msg in messages:
ef = msg.get("emotion_features")
if ef and msg.get("role") == "USER":
top = sorted(ef.items(), key=lambda x: x[1], reverse=True)[:5]
emotion_timeline.append({"turn": n, "text": text, "top_emotions": dict(top)})
6. Webhook Session Resolution
Hume chat_ended webhook does NOT include call_sid. Use config_id mapping:
config_to_thread: dict[str, str] = {}
config_to_thread[config_id] = thread_id
thread_id = config_to_thread.pop(body["config_id"])
Prevention Rules
See references/bug-prevention.md for the full bug registry and prevention checklist.