rohitsar567 Claude Opus 4.7 (1M context) commited on
Commit
1a7161d
Β·
1 Parent(s): 545bd83

#47: UIN-based net-new dedup for user-uploaded policy PDFs

Browse files

When a user uploads a policy PDF whose IRDAI UIN already belongs to a
catalogue policy, it is NOT net-new β€” the route now short-circuits and
returns the existing card (UploadResponse.already_in_catalogue=True +
existing_policy_id/name) instead of indexing a duplicate. Adds
_catalogue_uin_index() (cached, modern-format UINs only) and
_match_catalogue_uin() (case-insensitive extraction). Hash-dedup (Gate 8)
still handles identical re-uploads; this adds same-policy-different-PDF.

Other gaps from the #47 spec were already covered by the existing #52
work (persisted JSON, global graded marketplace card, dynamic insurer
logo) β€” not rebuilt; durable cross-restart persistence intentionally
left session-scoped (HF FS is ephemeral; durability would leak unvetted
uploads into the shared catalogue).

Also fixes a pre-existing test-hygiene leak: the #45 auto-persist wrote
the recall test's throwaway name to disk; fixture teardown now cleans it.

Regression test tests/test_upload_uin_dedup.py; full suite green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

backend/main.py CHANGED
@@ -237,6 +237,88 @@ class UploadResponse(BaseModel):
237
  chunks_added: int
238
  pages_indexed: int
239
  elapsed_ms: int
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
 
241
 
242
  # ---------------------------------------------------------------------------
@@ -1815,6 +1897,22 @@ async def upload_policy(
1815
  # Run 8-gate security check (dedupe + mechanics + encrypted + content +
1816
  # page ceiling + injection + per-session + per-IP rate limit + LLM judge)
1817
  full_text = "\n".join(t for _, t in pages)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1818
  client_ip = (request.client.host if request and request.client else "") or request.headers.get("X-Forwarded-For", "").split(",")[0].strip()
1819
  verdict = await check_upload(
1820
  content=contents,
 
237
  chunks_added: int
238
  pages_indexed: int
239
  elapsed_ms: int
240
+ # #47 (2026-05-21) β€” UIN net-new dedup. When the uploaded PDF's IRDAI
241
+ # UIN already belongs to a catalogue policy, the upload is NOT indexed
242
+ # as a new card; these fields point the caller at the existing policy.
243
+ already_in_catalogue: bool = False
244
+ existing_policy_id: Optional[str] = None
245
+ existing_policy_name: Optional[str] = None
246
+
247
+
248
+ # ---------------------------------------------------------------------------
249
+ # #47 (2026-05-21) β€” UIN net-new dedup for user uploads. Before a freshly
250
+ # uploaded PDF is indexed as a brand-new marketplace card, check whether its
251
+ # IRDAI UIN already belongs to a catalogued policy; if so it is NOT net-new
252
+ # and the caller is pointed at the existing card. All imports are lazy β€” the
253
+ # upload route imports `re` locally, so `re` is not module-level here.
254
+ _UIN_PATTERN = r"\b[A-Z]{5,9}\d{5}V\d{6}\b"
255
+ _catalogue_uin_cache = None # type: Optional[dict]
256
+
257
+
258
+ def _catalogue_uin_index() -> dict:
259
+ """Map every catalogue policy's IRDAI UIN -> (policy_id, policy_name).
260
+ Built once from 40-data/policy_facts/*.json, then cached."""
261
+ global _catalogue_uin_cache
262
+ if _catalogue_uin_cache is not None:
263
+ return _catalogue_uin_cache
264
+ import json as _json
265
+ import pathlib as _pl
266
+ import re as _re
267
+
268
+ def _find_uin(o):
269
+ if isinstance(o, dict):
270
+ if "uin_code" in o:
271
+ v = o["uin_code"]
272
+ return v.get("value") if isinstance(v, dict) else v
273
+ for x in o.values():
274
+ r = _find_uin(x)
275
+ if r:
276
+ return r
277
+ elif isinstance(o, list):
278
+ for x in o:
279
+ r = _find_uin(x)
280
+ if r:
281
+ return r
282
+ return None
283
+
284
+ idx: dict = {}
285
+ pf_dir = _pl.Path(__file__).resolve().parent.parent / "40-data" / "policy_facts"
286
+ for fp in sorted(pf_dir.glob("*.json")):
287
+ try:
288
+ uin = _find_uin(_json.loads(fp.read_text()))
289
+ except Exception:
290
+ continue
291
+ if not uin or not isinstance(uin, str):
292
+ continue
293
+ uin = uin.strip().upper()
294
+ # Only index modern-format IRDAI UINs β€” those are the only ones the
295
+ # uploaded-text matcher (_UIN_PATTERN) can ever extract. Legacy
296
+ # registration codes (e.g. "IRDAI/HLT/CTTK/...") are unmatchable,
297
+ # so indexing them would be dead weight.
298
+ if not _re.fullmatch(r"[A-Z]{5,9}\d{5}V\d{6}", uin):
299
+ continue
300
+ stem = fp.stem
301
+ for suf in ("__wordings", "__cis", "__brochure", "__prospectus"):
302
+ if stem.endswith(suf):
303
+ stem = stem[: -len(suf)]
304
+ nm = stem.split("__")[-1].replace("-", " ").title()
305
+ idx.setdefault(uin, (stem, nm))
306
+ _catalogue_uin_cache = idx
307
+ return idx
308
+
309
+
310
+ def _match_catalogue_uin(text: str):
311
+ """Return (policy_id, policy_name) if `text` carries the IRDAI UIN of an
312
+ already-catalogued policy; else None."""
313
+ import re as _re
314
+
315
+ idx = _catalogue_uin_index()
316
+ # Case-insensitive extraction β€” a UIN may appear in any case in the
317
+ # uploaded text / after PDF extraction; normalise to upper for lookup.
318
+ for u in {m.upper() for m in _re.findall(_UIN_PATTERN, text or "", _re.IGNORECASE)}:
319
+ if u in idx:
320
+ return idx[u]
321
+ return None
322
 
323
 
324
  # ---------------------------------------------------------------------------
 
1897
  # Run 8-gate security check (dedupe + mechanics + encrypted + content +
1898
  # page ceiling + injection + per-session + per-IP rate limit + LLM judge)
1899
  full_text = "\n".join(t for _, t in pages)
1900
+ # #47 β€” UIN net-new dedup: if the uploaded PDF's IRDAI UIN already
1901
+ # belongs to a catalogue policy it is NOT net-new β€” return the
1902
+ # existing card instead of indexing a duplicate. `indexed_ok` stays
1903
+ # False so the finally block deletes the freshly-written temp file.
1904
+ _uin_hit = _match_catalogue_uin(full_text)
1905
+ if _uin_hit:
1906
+ return UploadResponse(
1907
+ policy_id=_uin_hit[0],
1908
+ policy_name=_uin_hit[1],
1909
+ chunks_added=0,
1910
+ pages_indexed=len(pages),
1911
+ elapsed_ms=int((_time.time() - t0) * 1000),
1912
+ already_in_catalogue=True,
1913
+ existing_policy_id=_uin_hit[0],
1914
+ existing_policy_name=_uin_hit[1],
1915
+ )
1916
  client_ip = (request.client.host if request and request.client else "") or request.headers.get("X-Forwarded-For", "").split(",")[0].strip()
1917
  verdict = await check_upload(
1918
  content=contents,
tests/test_bug2526_recall_and_reconstruct.py CHANGED
@@ -101,6 +101,14 @@ class _FirstNameStoredFixture(unittest.TestCase):
101
  try:
102
  import json
103
  for fp in _PROFILES_DIR.glob("*.json"):
 
 
 
 
 
 
 
 
104
  try:
105
  d = json.loads(fp.read_text())
106
  except Exception:
 
101
  try:
102
  import json
103
  for fp in _PROFILES_DIR.glob("*.json"):
104
+ # Bug-#45 test hygiene: handle_turn now auto-persists ANY
105
+ # captured name, so the hard-coded "unknown name" used by
106
+ # test_unknown_name_no_false_recall_later_turn leaks a
107
+ # zzqxnobody*.json. Clean it (and this fixture's own files)
108
+ # so a re-run starts from a true no-stored-profile state.
109
+ if fp.stem.startswith("zzqxnobody"):
110
+ fp.unlink(missing_ok=True)
111
+ continue
112
  try:
113
  d = json.loads(fp.read_text())
114
  except Exception:
tests/test_upload_uin_dedup.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Regression test for #47 β€” UIN net-new dedup on user uploads (2026-05-21).
2
+
3
+ When a user uploads a policy PDF whose IRDAI UIN already belongs to a
4
+ catalogue policy, it is NOT net-new: the upload route must short-circuit
5
+ and return the existing card (UploadResponse.already_in_catalogue=True)
6
+ instead of indexing a duplicate. These pin the helper logic + the
7
+ response-model contract; both symbols did not exist pre-#47.
8
+ """
9
+ import unittest
10
+
11
+ from backend.main import (
12
+ UploadResponse,
13
+ _catalogue_uin_index,
14
+ _match_catalogue_uin,
15
+ )
16
+
17
+
18
+ class TestUploadUinDedup(unittest.TestCase):
19
+ def test_catalogue_uin_index_is_built(self):
20
+ idx = _catalogue_uin_index()
21
+ self.assertGreater(len(idx), 50, "catalogue UIN index looks empty")
22
+ for uin, val in idx.items():
23
+ self.assertRegex(uin, r"^[A-Z]{5,9}\d{5}V\d{6}$")
24
+ self.assertEqual(len(val), 2, "value must be (policy_id, name)")
25
+
26
+ def test_known_catalogue_uin_matches(self):
27
+ # Every indexed (modern-format) UIN must round-trip through the
28
+ # matcher when embedded in wording text. Order-independent β€” not
29
+ # fragile to dict-iteration / module-cache state.
30
+ idx = _catalogue_uin_index()
31
+ self.assertTrue(idx, "catalogue UIN index is empty")
32
+ misses = []
33
+ for uin, val in idx.items():
34
+ if _match_catalogue_uin(f"policy wording UIN {uin} terms") != val:
35
+ misses.append(uin)
36
+ if _match_catalogue_uin(uin.lower()) != val: # case-insensitive
37
+ misses.append(uin + " (lowercase)")
38
+ self.assertEqual(misses, [], f"UINs that did not round-trip: {misses[:8]}")
39
+
40
+ def test_unknown_or_fake_uin_is_not_matched(self):
41
+ self.assertIsNone(_match_catalogue_uin("plain text, no identifier"))
42
+ self.assertIsNone(_match_catalogue_uin(""))
43
+ # valid UIN *shape* but not a real catalogue policy
44
+ self.assertIsNone(_match_catalogue_uin("ZZZHLIP00000V000000"))
45
+
46
+ def test_uploadresponse_dedup_fields(self):
47
+ hit = UploadResponse(
48
+ policy_id="acko__acko-health-ii", policy_name="Acko Health Ii",
49
+ chunks_added=0, pages_indexed=10, elapsed_ms=5,
50
+ already_in_catalogue=True,
51
+ existing_policy_id="acko__acko-health-ii",
52
+ existing_policy_name="Acko Health Ii")
53
+ self.assertTrue(hit.already_in_catalogue)
54
+ self.assertEqual(hit.existing_policy_id, "acko__acko-health-ii")
55
+ # net-new upload β€” fields default to the not-a-dedup-hit state
56
+ fresh = UploadResponse(
57
+ policy_id="user-upload__x__y", policy_name="Y",
58
+ chunks_added=42, pages_indexed=45, elapsed_ms=9)
59
+ self.assertFalse(fresh.already_in_catalogue)
60
+ self.assertIsNone(fresh.existing_policy_id)
61
+
62
+
63
+ if __name__ == "__main__":
64
+ unittest.main()