Skip to content

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:

  1. Regex-based emotional state detector (app/services/emotional_state.py) — zero LLM cost, runs in ~1ms on every turn
  2. 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:

  1. System promptNEVER list built dynamically from YAML at runtime via _get_forbidden_phrases_block() in llm_conversation.py
  2. response_policy.py — runtime validator with check_response() and check_response_strict()
  3. tests/test_voice_compliance.py — CI gate that scans all source files in app/ and config/, fails the build on any error-severity violation
  4. voice-auditor Claude 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

  1. Add a new label to EmotionalState literal in emotional_state.py
  2. Define a _NEW_PATTERNS list of regex patterns
  3. Add a check in detect_emotional_state() at the correct priority position (higher priority = checked earlier)
  4. Add a row to the EMOTIONAL CONTEXT block in llm_conversation.py with the tone rule
  5. Add at least 3 tests:
  6. One that the new state is detected
  7. One that a higher-priority state still wins (anti-trampling)
  8. One that the new state doesn't fire on neutral text (no false positive)
  9. 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.