Emotional Intelligence Layer¶
The Curaway agent has a coordinated EQ layer that detects the patient's emotional state from each message and adapts its tone accordingly. This prevents the agent from sounding like a customer-service rep when patients share something difficult, and prevents saccharine over-empathy when they just want answers.
Architecture¶
Two layers work together:
- Regex-based emotional state detector (
app/services/emotional_state.py) — zero LLM cost, runs in ~1ms on every turn - System prompt with state-aware tone rules and few-shot examples
(
app/agents/llm_conversation.py) — the LLM reads the detected state and adapts its response shape
Patient message
↓
detect_emotional_state(message) ← regex, ~1ms
↓
emotional_context_for_prompt(state) ← maps to label string
↓
System prompt {emotional_context} ← injected into prompt
↓
LLM generates state-aware response
Cost: zero additional API calls. The state label is passed as a placeholder substitution into the existing system prompt, not as a separate LLM round-trip.
Six Classifications¶
The detector checks states in this order — first match wins, so stronger signals never get suppressed by weaker ones.
1. anxious (highest priority)¶
Triggers (regex \b...\b):
- Direct:
scared,afraid,nervous,worried,terrified,frightened,petrified - Phrases: "is it/this safe", "what if", "can I trust"
- Surgery anxiety: "never had/done surgery", "first time/surgery"
- Mortality language: "dying", "death"
- "complications" near "scared/worried/afraid"
Tone rule: Grounding language. Slower pace. ONE question only, never 2-3. Emphasize the patient is in control.
Patient: "I'm scared. Is this safe to do abroad?"
Agent: "Fair question — going abroad for surgery is a real decision. Every hospital we work with is internationally accredited (JCI or equivalent), and you'll see their outcomes data, surgeon credentials, and patient reviews before making any choice. Would you like to start by seeing a few hospitals, or would you rather understand the process first?"
2. grieving — loss of mobility or independence¶
Triggers:
- "can't walk/move/sleep/work/drive/play/run"
- "used to ... now (I/can't)"
- "miss [walking/running/playing/working/gardening/hiking]"
- "lost/losing my/the ability"
- "dependent on", "depending on"
- "gave up", "had to stop"
- "wheelchair", "bedridden"
Tone rule: Name the loss specifically — use the patient's actual words ("walking your dog", not "your hobbies"). Then offer agency, never pity.
Patient: "I can't even walk my dog anymore. I'm tired of waiting 18 months for surgery."
Agent: "Eighteen months waiting while you can't even walk your dog — that's a long time to lose the small things that matter. Let's see what we can do to get you moving again. Has your doctor confirmed it's a Total Knee Replacement?"
3. frustrated¶
Triggers:
- "tired of", "fed up", "sick of", "had enough"
- "been waiting", "wait(ed/ing) X (months/years/forever)"
- "no one/nobody listens", "(third/second) opinion"
- "NHS wait/waiting/backlog"
- "can't (afford/wait/take it)"
- "so frustrating", "ridiculous"
Tone rule: Validate the wait briefly (one sentence), then pivot fast to action and progress. Frustrated patients want momentum, not commiseration.
Patient: "I've been waiting 18 months on the NHS and nothing is happening."
Agent: "Eighteen months is a long time, and waiting in pain only makes it worse. Here's what we can do quickly: I'll match you with hospitals that have availability in the next 2-4 weeks, not 18 months. Has your doctor confirmed the procedure you need?"
4. hopeful¶
Triggers:
- "finally", "hopeful", "excited"
- "ready to (do this/move forward/proceed/start)"
- "looking forward", "can't wait"
- "grateful", "thank you ... help"
Tone rule: Match their energy without overdoing it. Channel hope into next concrete steps. Don't dampen, don't oversell.
Patient: "Finally found something that might actually help. I'm ready to move forward."
Agent: "Good — let's keep that momentum. To get you matched with the right hospital quickly, I just need a few details. First: which procedure has your doctor recommended, and do you have any recent records (X-ray, blood work) on hand?"
5. discomfort (lowest priority)¶
Triggers:
- Pain words:
hurts,painful,sore,aching,stiff,throbbing,swollen,inflamed,uncomfortable,discomfort - Body parts: "my (knee/hip/back/shoulder/neck/joint/chest/stomach/head)"
- Procedure mentions:
surgery,operation,procedure,replacement
Tone rule: ONE sentence acknowledgment of the pain or difficulty before pivoting to logistics. Name the experience, don't mirror the words. This catches patients who mention pain without explicit emotional language — prevents the agent from sounding like a customer-service rep.
Patient: "My knee hurts and I need a replacement."
Agent: "Knee pain bad enough to need a replacement is exhausting — both the pain and the uncertainty around it. Let's get you a real plan. Before we look at hospitals, do you have any recent X-rays, blood work, or your doctor's notes on hand?"
6. neutral¶
Anything else — factual questions, payment/process queries, generic statements.
Tone rule: Default voice rules apply. No emotional override. Standard acknowledge-then-act flow.
Patient: "What payment options do you accept?"
Agent: "Most hospitals accept self-pay (credit card, wire transfer) and many accept international insurance with pre-authorization. Specific quotes come from each hospital after they review your case — you'll see the cost breakdown side-by-side before committing. Are you working with insurance, or planning self-pay?"
Anti-Trampling Guarantees¶
The priority order is enforced in code AND in tests. Stronger signals always win over weaker ones.
| Combination | Wins | Why |
|---|---|---|
| "knee hurts and scared" | anxious |
scared > pain mention |
| "knee hurts and can't walk my dog" | grieving |
loss > pain mention |
| "back hurts and tired of waiting" | frustrated |
wait > pain mention |
| "knee hurts" alone | discomfort |
only weak signal present |
| "I need TKR" | discomfort |
replacement keyword |
| "What's your refund policy?" | neutral |
no medical signal |
8 priority-order tests in tests/test_emotional_state.py enforce this. They
fail if anyone adds a higher-priority pattern that gets shadowed by a weaker
state.
Forbidden Phrases (the NEVER list)¶
The prompt also has a NEVER block that applies to ALL classifications, loaded
from config/voice_rules.yaml (see Voice Compliance).
Hollow / reflexive empathy:
- "I'm so sorry to hear" (reflexive)
- "completely natural to feel" (generic)
- "That must be so hard" (presumptuous)
- "I completely understand" (you don't)
- "I'm here for you" / "you're not alone" (saccharine)
Outcome promises (medical safety risk):
- "don't worry" / "everything will be fine"
- "you'll be fine"
- "guaranteed"
Therapy-speak:
- "I'm hearing that..." / "What I'm hearing is"
- "It sounds like..." (warning level)
Sales-y / corporate copy:
- "exactly why Curaway exists"
- "take the complexity out"
- "exploring treatment options abroad"
- "in good hands" / "rest assured" / "stress-free" / "peace of mind"
- "world-class" / "state-of-the-art" / "cutting-edge" (warning level)
- "I'd love to help"
Enforced in four places:
- System prompt —
NEVERlist built dynamically from YAML at runtime via_get_forbidden_phrases_block()inllm_conversation.py response_policy.py— runtime validator withcheck_response()andcheck_response_strict()tests/test_voice_compliance.py— CI gate that scans all source files inapp/andconfig/, fails the build on any error-severity violationvoice-auditorClaude Code subagent — manual review for sophisticated patterns regex can't catch (long preambles, generic deflection, outcome promises)
Files¶
| File | Purpose |
|---|---|
app/services/emotional_state.py |
Regex detector, priority logic |
app/agents/llm_conversation.py |
System prompt with {emotional_context} placeholder |
app/services/response_policy.py |
Runtime forbidden-phrase validator |
app/services/output_validator.py |
Wired into agent response pipeline |
config/voice_rules.yaml |
Single source of truth for forbidden phrases |
tests/test_emotional_state.py |
27 unit tests covering each state + priority |
tests/test_voice_compliance.py |
7 CI tests scanning code for violations |
.claude/agents/voice-auditor.md |
Subagent for manual PR review |
Adding new emotional states¶
- Add a new label to
EmotionalStateliteral inemotional_state.py - Define a
_NEW_PATTERNSlist of regex patterns - Add a check in
detect_emotional_state()at the correct priority position (higher priority = checked earlier) - Add a row to the
EMOTIONAL CONTEXTblock inllm_conversation.pywith the tone rule - Add at least 3 tests:
- One that the new state is detected
- One that a higher-priority state still wins (anti-trampling)
- One that the new state doesn't fire on neutral text (no false positive)
- Run
pytest tests/test_emotional_state.py tests/test_voice_compliance.py
Cost & Latency¶
The detector adds zero LLM calls and ~1ms of CPU per turn. The tone rules
are part of the existing system prompt — no additional tokens beyond the
small {emotional_context} substitution (typically 5-15 tokens).
The 4-layer enforcement of forbidden phrases catches issues in the prompt itself (validator), in CI (linter), in source code (scanner), and in PR review (subagent). This eliminated the class of bug where a canned guardrail template short-circuited the LLM and reached real users with hollow empathy.