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:
- Hallucinated precision. A figure like "$8,432" reads as a quote. It is not. We have no provider contract behind it.
- 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_enabled — config/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:
$0is sanctioned, not a price — "no coordination fee" / "$0" is skipped deliberately.k-suffixed figures are normalised ($8k→8000) 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
falsein both envs — no redeploy. - See Feature Flags for the full flag row.
Related¶
- Feature Flags —
price_guard_enabledrow - Guardrails architecture — output-validation layers
config/prompts/knowledge/financial_options.yaml— source of truth + HARD RULES