Spaces:
Sleeping
Sleeping
Download data.py from harsh-d/claim-agent: direct link, hf CLI and curl.
- Browser
- Download file 10.6 kB
-
https://huggingface.co/spaces/harsh-d/claim-agent/resolve/main/data.py
- Command line
-
hf download hf://spaces/harsh-d/claim-agent/data.py
-
curl -L -o data.py https://huggingface.co/spaces/harsh-d/claim-agent/resolve/main/data.py
10.6 kB
| from __future__ import annotations | |
| import json | |
| import re | |
| from pathlib import Path | |
| FIXTURES_DIR = Path(__file__).parent / "fixtures" | |
| def load_fixtures() -> dict: | |
| return { | |
| "policyholders": _load_json("policyholders.json"), | |
| "claims": _load_json("claims.json"), | |
| "claim_schema": _load_json("claim_schema.json"), | |
| "guidelines": _load_json("required_document_guideline.json"), | |
| "representatives": _load_json("representatives.json"), | |
| "consent_scenarios": _load_json("consent_scenarios.json"), | |
| } | |
| def _load_json(filename: str): | |
| with open(FIXTURES_DIR / filename) as f: | |
| return json.load(f) | |
| # --------------------------------------------------------------------------- | |
| # PII matching | |
| # --------------------------------------------------------------------------- | |
| def _normalize_phone(raw: str) -> str: | |
| digits = re.sub(r"[^0-9]", "", raw) | |
| if len(digits) == 11 and digits.startswith("1"): | |
| digits = digits[1:] | |
| return digits | |
| def _normalize_policy(raw: str) -> str: | |
| return re.sub(r"^pol[-\s]?", "", raw.strip(), flags=re.IGNORECASE).strip().upper() | |
| def _normalize_date(raw: str) -> str | None: | |
| raw = raw.strip() | |
| iso_match = re.match(r"^(\d{4})-(\d{1,2})-(\d{1,2})$", raw) | |
| if iso_match: | |
| return f"{iso_match.group(1)}-{int(iso_match.group(2)):02d}-{int(iso_match.group(3)):02d}" | |
| us_match = re.match(r"^(\d{1,2})/(\d{1,2})/(\d{2,4})$", raw) | |
| if us_match: | |
| year = us_match.group(3) | |
| if len(year) == 2: | |
| century = "20" if int(year) <= 30 else "19" | |
| year = century + year | |
| return f"{year}-{int(us_match.group(1)):02d}-{int(us_match.group(2)):02d}" | |
| # "March 15, 1985", "15 March 1985", "Mar 15 1985", "March 15th, 1985" — | |
| # the model passes dates through in whatever form the caller spoke them. | |
| # "3 december 1989" used to return None, so a caller with correct details | |
| # failed verification until the model happened to re-record it as ISO. | |
| day, month_str, year = None, None, None | |
| verbal = re.match(r"^([a-zA-Z]+)\.?\s+(\d{1,2})(?:st|nd|rd|th)?,?\s+(\d{4})$", raw) | |
| if verbal: | |
| month_str, day, year = verbal.group(1), verbal.group(2), verbal.group(3) | |
| else: | |
| verbal = re.match(r"^(\d{1,2})(?:st|nd|rd|th)?\s+(?:of\s+)?([a-zA-Z]+)\.?,?\s+(\d{4})$", raw) | |
| if verbal: | |
| day, month_str, year = verbal.group(1), verbal.group(2), verbal.group(3) | |
| if month_str: | |
| month = _MONTHS.get(month_str.lower()) or _MONTHS.get(month_str.lower()[:3]) | |
| if month: | |
| return f"{year}-{month}-{int(day):02d}" | |
| return None | |
| _MONTHS = { | |
| "january": "01", "february": "02", "march": "03", "april": "04", | |
| "may": "05", "june": "06", "july": "07", "august": "08", | |
| "september": "09", "october": "10", "november": "11", "december": "12", | |
| "jan": "01", "feb": "02", "mar": "03", "apr": "04", "jun": "06", "jul": "07", | |
| "aug": "08", "sep": "09", "sept": "09", "oct": "10", "nov": "11", "dec": "12", | |
| } | |
| # The five identity factors the SOP accepts (SPEC.txt). A policy number is | |
| # recorded because callers volunteer it and it is useful later, but it does not | |
| # count toward the three — it is printed on every piece of mail. | |
| VERIFICATION_FIELDS = ("full_name", "dob", "phone", "email", "id_last4") | |
| PII_FIELDS = VERIFICATION_FIELDS + ("policy_number",) | |
| def verification_field_count(collected_pii: dict[str, str]) -> int: | |
| return sum(1 for k in collected_pii if k in VERIFICATION_FIELDS) | |
| def pii_value_problem(field_name: str, value: str) -> str | None: | |
| """Why `value` cannot be a `field_name`, or None if it is plausible. | |
| The model sometimes records a field from a message that contains no PII | |
| at all ("I'm calling about my denied claim from January" produced a | |
| record_pii call), and small models pass the caller's phrasing through | |
| verbatim. Junk in collected_pii inflates the count that gates | |
| verify_identity and can shadow a real value given later, so it is | |
| refused at the door with a reason the model can act on. | |
| """ | |
| v = (value or "").strip() | |
| if field_name not in PII_FIELDS: | |
| return f"unknown field; use one of {', '.join(PII_FIELDS)}" | |
| if not v: | |
| return "empty value" | |
| if field_name == "full_name" and not re.search(r"[A-Za-z]", v): | |
| return "not a name" | |
| if field_name == "dob" and _normalize_date(v) is None: | |
| return "not a recognizable date — re-record it as YYYY-MM-DD" | |
| if field_name == "phone" and len(re.sub(r"\D", "", v)) < 7: | |
| return "not a phone number" | |
| if field_name == "email" and "@" not in v: | |
| return "not an email address" | |
| if field_name == "id_last4" and len(re.sub(r"\D", "", v)) != 4: | |
| return "must be exactly the last 4 digits" | |
| if field_name == "policy_number" and not re.search(r"\d", v): | |
| return "not a policy number" | |
| return None | |
| def match_policyholder( | |
| collected_pii: dict[str, str], | |
| policyholders: list[dict], | |
| ) -> tuple[dict | None, int]: | |
| best_match = None | |
| best_count = 0 | |
| for ph in policyholders: | |
| count = 0 | |
| if "full_name" in collected_pii: | |
| names = [ph["name"].lower()] | |
| names.extend(a.lower() for a in ph.get("name_aliases", [])) | |
| if collected_pii["full_name"].strip().lower() in names: | |
| count += 1 | |
| if "dob" in collected_pii: | |
| normalized = _normalize_date(collected_pii["dob"]) | |
| if normalized and normalized == ph.get("dob"): | |
| count += 1 | |
| if "phone" in collected_pii: | |
| phones = [_normalize_phone(ph["phone"])] | |
| phones.extend(_normalize_phone(p) for p in ph.get("phone_aliases", [])) | |
| if _normalize_phone(collected_pii["phone"]) in phones: | |
| count += 1 | |
| if "email" in collected_pii: | |
| emails = [ph["email"].lower()] | |
| emails.extend(e.lower() for e in ph.get("email_aliases", [])) | |
| if collected_pii["email"].strip().lower() in emails: | |
| count += 1 | |
| if "id_last4" in collected_pii: | |
| if re.sub(r"\D", "", collected_pii["id_last4"]) == ph.get("id_last4", ""): | |
| count += 1 | |
| # Deliberately not counted — see VERIFICATION_FIELDS. | |
| if count > best_count: | |
| best_count = count | |
| best_match = ph | |
| if best_count >= 3: | |
| return best_match, best_count | |
| return None, best_count | |
| # --------------------------------------------------------------------------- | |
| # Claim lookups | |
| # --------------------------------------------------------------------------- | |
| def get_claims_for_party(party_id: str, claims: list[dict]) -> list[dict]: | |
| return [c for c in claims if c["party_id"] == party_id] | |
| def get_claim_by_id(case_id: str, claims: list[dict]) -> dict | None: | |
| for c in claims: | |
| if c["case_id"] == case_id: | |
| return c | |
| return None | |
| # --------------------------------------------------------------------------- | |
| # Document and follow-up guidance | |
| # --------------------------------------------------------------------------- | |
| def get_document_guidance(document_name: str, guidelines: dict, case_type: str | None = None) -> str: | |
| doc_guidance = guidelines.get("document_guidance", {}) | |
| alt_guidance = guidelines.get("document_alternative_guidance", {}) | |
| doc_name_lower = document_name.lower().strip() | |
| matched_key = None | |
| for key in doc_guidance: | |
| if doc_name_lower in key.lower() or key.lower() in doc_name_lower: | |
| matched_key = key | |
| break | |
| parts = [] | |
| if matched_key: | |
| parts.append(f"Submission guidance for '{matched_key}':\n{doc_guidance[matched_key].get('en', '')}") | |
| if matched_key in alt_guidance: | |
| parts.append(f"\nIf not available:\n{alt_guidance[matched_key].get('en', '')}") | |
| else: | |
| default = guidelines.get("default_guidance", {}).get("en", "") | |
| parts.append(f"General document submission guidance:\n{default}") | |
| default_alt = alt_guidance.get("default", {}).get("en", "") | |
| if default_alt: | |
| parts.append(f"\nIf not available:\n{default_alt}") | |
| # Only the note for this claim's own type — emitting every case type gave | |
| # healthcare callers auto-claim advice. | |
| case_type_guidance = guidelines.get("case_type_guidance", {}) | |
| for ct, g in case_type_guidance.items(): | |
| if case_type and ct.lower() != case_type.lower(): | |
| continue | |
| parts.append(f"\n{ct.title()} note: {g.get('en', '')}") | |
| return "\n".join(parts) | |
| def match_followup_guidance( | |
| question: str, | |
| intent: str | None, | |
| active_claim: dict | None, | |
| guidelines: dict, | |
| ) -> str: | |
| question_lower = question.lower() | |
| settings = guidelines.get("claim_followup_settings", {}) | |
| # Preference order: | |
| # 1. keyword hit on an entry whose intent_hints include this intent | |
| # 2. keyword hit on any entry — if the caller literally asked about the | |
| # topic, that beats the classified intent (a denial_question caller | |
| # still deserves submission_method guidance when they ask how to send) | |
| # 3. an intent-matching catch-all (entries with no `match_any`) | |
| # Without this, the catch-all listed first shadows every specific rule. | |
| by_intent_and_keyword = None | |
| by_keyword = None | |
| by_intent = None | |
| for entry in guidelines.get("claim_followup_guidance", []): | |
| if entry.get("requires_documents") and not (active_claim and active_claim.get("documents_needed")): | |
| continue | |
| intent_ok = not intent or intent in entry.get("intent_hints", []) | |
| match_any = entry.get("match_any", []) | |
| if match_any and any(kw.lower() in question_lower for kw in match_any): | |
| if intent_ok: | |
| by_intent_and_keyword = by_intent_and_keyword or entry | |
| else: | |
| by_keyword = by_keyword or entry | |
| elif not match_any and intent_ok: | |
| by_intent = by_intent or entry | |
| entry = by_intent_and_keyword or by_keyword or by_intent | |
| if entry is None: | |
| return guidelines.get("claim_followup_fallback", {}).get("en", "No specific guidance available.") | |
| text = entry["en"] | |
| if active_claim: | |
| text = text.replace("{case_id}", active_claim.get("case_id", "")) | |
| docs = ", ".join(active_claim.get("documents_needed", [])) | |
| text = text.replace("{documents}", docs) | |
| avg_time = settings.get("average_processing_time_after_submission", {}).get("en", "") | |
| return text.replace("{average_processing_time_after_submission}", avg_time) | |