Skip to content

Price Governance

How Curaway controls the cost figures the conversational agent is allowed to state, and the runtime guard that enforces it.

TL;DR

Cost numbers come from one governed source — config/prompts/knowledge/financial_options.yaml. A runtime guard (price_guard_enabled) scans every agent response and flags any dollar figure that falls outside the sanctioned indicative bands, routing the turn back through the agent for a revision. The agent never promises a procedure is "affordable" without a real provider quote.


Why this exists

The agent discusses money. Patients ask "how much will my knee replacement cost?" and the model — left ungoverned — will happily invent a precise-sounding number. Two failure modes follow:

  1. Hallucinated precision. A figure like "$8,432" reads as a quote. It is not. We have no provider contract behind it.
  2. Hardcoded drift. Cost numbers copied into prompts, few-shot examples, and code comments drift apart over time and contradict each other.

The price-policy decision resolved this: Curaway publishes governed indicative ranges (broad bands, clearly framed as estimates) rather than per-case quotes, and a single source of truth feeds both the prompt and a runtime guard. It shipped as two PRs — the runtime guard (#1374) and the single-sourcing of the previously duplicated cost numbers (#1375).


The single source of truth

All sanctioned cost numbers live in config/prompts/knowledge/financial_options.yaml.

That file carries two coupled representations:

  • A human-readable content: block (the prose the model actually sees as knowledge), which states the indicative ranges as $X–$Y.
  • A machine-readable governed_cost_ranges: block (metadata only — not serialized into the prompt) that the runtime guard parses.

The two are kept in lockstep by a CI drift test (see Drift guards). Editing one without the other fails CI.

Governed indicative ranges

Procedure band Min (USD) Max (USD)
Total Knee / Hip Replacement (TKR / THR) 6,000 12,000
Coronary Artery Bypass (CABG) 7,000 15,000
Bariatric surgery 5,000 10,000
Transplants (kidney, liver) 25,000 50,000
MSO / specialist teleconsultation 200 500

The currency: USD marker is informational (ISO 4217). The guard compares bare magnitudes — it does not parse currency symbols, so a figure is judged purely on its numeric value against every band.

These are indicative bands, not quotes

The ranges exist so the agent can give a patient a sense of scale without fabricating a precise number. The financial_options.yaml HARD RULES still apply: never tell a patient a procedure is "affordable", "cheap", or within their budget without a real provider quote.


The runtime guard

When price_guard_enabled is ON, every assembled agent response is scanned for cost figures before it reaches the patient.

Flow

agent response
  └─ output_validator (Layer 3)
       └─ response_policy.check_price_claims(response_text)
            ├─ clean  → response sent
            └─ violations → agent_revising retry (existing retry loop)

The guard rides the existing agent_revising retry path — it does not introduce a new failure mode. A flagged response is regenerated, not hard-blocked, so the patient still gets an answer.

Implementation

Concern Location
Flag price_guard_enabledconfig/feature_flags.yaml:177 (default false)
Range loader _load_governed_cost_ranges()app/services/response_policy.py:522
Claim scanner check_price_claims(response_text) -> list[str]app/services/response_policy.py:553
Wiring app/services/output_validator.py Layer 3 → called from app/routers/chat.py

Scanner behaviour

check_price_claims walks every currency figure in the response and returns a violation string for any value that falls outside all governed bands. Notable rules:

  • $0 is sanctioned, not a price — "no coordination fee" / "$0" is skipped deliberately.
  • k-suffixed figures are normalised ($8k8000) before the band check.
  • A value is a violation only if it sits outside every band (a number valid for any procedure passes).

Fail-safe: the guard degrades OFF

_load_governed_cost_ranges() returns None on any load/parse failure, which makes check_price_claims a no-op. This is deliberate: a guard that can't read its config must not flag every cost figure and send every cost response into a retry/fallback storm. Degrading the guard off is far safer than a false-positive storm. A malformed config is instead caught in CI (below), not at runtime.


Drift guards

Two CI checks keep the governed ranges trustworthy:

Guard What it asserts
tests/test_price_guard.py The structured governed_cost_ranges bands match the $X–$Y prose in the content: block — the two representations can't drift apart.
v6_artifact_validator Each band is well-formed: ranges non-empty; min/max numeric and non-negative; min <= max.

Because validation lives in CI, a broken financial_options.yaml fails the pipeline rather than silently disabling the guard in production.


Operating the guard

price_guard_enabled is OFF by default. It is flipped ON as part of the v6.2-flexible canary rollout.

  • Flip applies to both Flagsmith Production and Development environments together (per the dual-env rule).
  • Rollback is a flag flip to false in both envs — no redeploy.
  • See Feature Flags for the full flag row.