Problem
The PII scrubber is only reachable as an explicit /anonymize (and /v1/ai/anonymize) endpoint; the LLM verification path that sends free-text evidence to an external provider never calls it. HumanitarianVerificationService.verify_claim builds prompts from raw inputs and posts them straight to OpenAI or Groq:
# app/ai-service/services/humanitarian_verification.py
primary_prompt = self.prompt_engine.build_primary_prompt(
aid_claim=aid_claim, supporting_evidence=evidence, context_factors=context) # raw text
...
raw_content = self._call_provider(provider=provider, model=model,
system_prompt=prompt["system"], user_prompt=prompt["user"], ...) # sent verbatim
PIIScrubberService (app/ai-service/services/pii_scrubber.py) exists and is instantiated in main.py, but a search for its usage in app/ai-service/services/ finds it only in its own file. Nothing in humanitarian_verification.py, ocr.py, or fraud_detection.py scrubs before the provider call.
Consequence: the README's claim that the platform performs "PII anonymization before external LLM processing" holds only if a caller separately remembers to invoke /anonymize first. On /v1/ai/humanitarian/verify, supporting_evidence — which the schema accepts as free text and which is described as recipient-provided evidence — is transmitted unredacted to a third-party LLM. Recipient names, locations, dates, ID numbers, and contact details can leave the trust boundary with no enforcement. The backend-side issue that scrubbed metadata: Json fields (#213) does not cover this AI-service path, because the evidence reaches the provider from inside the AI service itself.
Root cause
Anonymization is modelled as a separate, opt-in endpoint rather than as a mandatory preprocessing stage of the verification pipeline, so the pipeline and the scrubber are decoupled and the decoupling is invisible in the response.
Why this is architecturally hard
- It must be enforced, not just added. Calling
pii_scrubber.anonymize in verify_claim is the easy part; the hard part is making it impossible to skip — e.g. a pipeline stage that runs before any provider call, with a fail-closed policy when scrubbing itself fails.
- Anonymized vs. raw evidence changes verification semantics. Names/locations are often material to a humanitarian claim. The design must decide whether the LLM sees masked tokens (and whether the verdict degrades), and how the unredacted text is retained for the human reviewer without leaking it to the provider.
- The scrubber's own precision is imperfect.
pii_scrubber.py relies on a small regex set plus a spaCy blank en entity ruler; its NAME_REGEXES includes \b[A-Z][a-z]+\s+[A-Z][a-z]+\b, which matches generic capitalized phrases. Relying on it as a hard gate requires a measurable detection bar and a documented residual-risk posture.
- Retention/audit already exists and must stay correct.
persistence/pii_decisions.py records aggregate scrub metadata (never text); wiring the scrubber into the hot path must feed that store without accidentally persisting raw evidence.
Proposed design
Introduce a single preprocessing step in the verification service that scrubs aid_claim/supporting_evidence/context_factors before prompt construction, with a config flag that fails closed (returns an error) when scrubbing is unavailable rather than silently sending raw text. Emit the scrub summary into pii_decisions and expose the scrubbed/raw distinction in the response envelope so the backend can keep the raw text on the human-review side only.
Acceptance criteria
Service
Tests
Documentation
Out of scope
Improving the scrubber's regex/spaCy precision and the backend metadata scrubbing (#213) are separate concerns.
Getting started
Files: app/ai-service/services/humanitarian_verification.py, app/ai-service/services/pii_scrubber.py, app/ai-service/services/humanitarian_prompt.py, app/ai-service/persistence/pii_decisions.py.
Good first files to read: services/humanitarian_verification.py (the raw-to-provider flow) and services/pii_scrubber.py (the anonymize contract to call).
Problem
The PII scrubber is only reachable as an explicit
/anonymize(and/v1/ai/anonymize) endpoint; the LLM verification path that sends free-text evidence to an external provider never calls it.HumanitarianVerificationService.verify_claimbuilds prompts from raw inputs and posts them straight to OpenAI or Groq:PIIScrubberService(app/ai-service/services/pii_scrubber.py) exists and is instantiated inmain.py, but a search for its usage inapp/ai-service/services/finds it only in its own file. Nothing inhumanitarian_verification.py,ocr.py, orfraud_detection.pyscrubs before the provider call.Consequence: the README's claim that the platform performs "PII anonymization before external LLM processing" holds only if a caller separately remembers to invoke
/anonymizefirst. On/v1/ai/humanitarian/verify,supporting_evidence— which the schema accepts as free text and which is described as recipient-provided evidence — is transmitted unredacted to a third-party LLM. Recipient names, locations, dates, ID numbers, and contact details can leave the trust boundary with no enforcement. The backend-side issue that scrubbedmetadata: Jsonfields (#213) does not cover this AI-service path, because the evidence reaches the provider from inside the AI service itself.Root cause
Anonymization is modelled as a separate, opt-in endpoint rather than as a mandatory preprocessing stage of the verification pipeline, so the pipeline and the scrubber are decoupled and the decoupling is invisible in the response.
Why this is architecturally hard
pii_scrubber.anonymizeinverify_claimis the easy part; the hard part is making it impossible to skip — e.g. a pipeline stage that runs before any provider call, with a fail-closed policy when scrubbing itself fails.pii_scrubber.pyrelies on a small regex set plus a spaCy blankenentity ruler; itsNAME_REGEXESincludes\b[A-Z][a-z]+\s+[A-Z][a-z]+\b, which matches generic capitalized phrases. Relying on it as a hard gate requires a measurable detection bar and a documented residual-risk posture.persistence/pii_decisions.pyrecords aggregate scrub metadata (never text); wiring the scrubber into the hot path must feed that store without accidentally persisting raw evidence.Proposed design
Introduce a single preprocessing step in the verification service that scrubs
aid_claim/supporting_evidence/context_factorsbefore prompt construction, with a config flag that fails closed (returns an error) when scrubbing is unavailable rather than silently sending raw text. Emit the scrub summary intopii_decisionsand expose the scrubbed/raw distinction in the response envelope so the backend can keep the raw text on the human-review side only.Acceptance criteria
Service
/v1/ai/humanitarian/verifyrequest whose evidence contains a name/phone/ID transmits only masked tokens to the provider.Tests
Documentation
Out of scope
Improving the scrubber's regex/spaCy precision and the backend metadata scrubbing (#213) are separate concerns.
Getting started
Files:
app/ai-service/services/humanitarian_verification.py,app/ai-service/services/pii_scrubber.py,app/ai-service/services/humanitarian_prompt.py,app/ai-service/persistence/pii_decisions.py.cd app/ai-service pytestGood first files to read:
services/humanitarian_verification.py(the raw-to-provider flow) andservices/pii_scrubber.py(theanonymizecontract to call).