rohitsar567 commited on
Commit
2438253
Β·
verified Β·
1 Parent(s): 81703e5

Deploy: Stack A (NIM brain + Maverick judge + Sarvam voice). D-019.

Browse files
40-data/llm_health.json CHANGED
@@ -1,22 +1,16 @@
1
  {
2
- "updated_at": "2026-05-15T02:03:36Z",
3
  "models": {
4
  "qwen/qwen3-next-80b-a3b-instruct": {
5
  "model": "qwen/qwen3-next-80b-a3b-instruct",
6
  "status": "healthy",
7
- "last_success_at": "2026-05-15T02:03:36Z",
8
  "last_failure_at": null,
9
  "last_error": null,
10
- "latency_ms": 772,
11
  "consecutive_failures": 0,
12
- "tested_at": "2026-05-15T02:03:36Z",
13
  "probe_history": [
14
- {
15
- "ok": true,
16
- "latency_ms": 1208,
17
- "ts": "2026-05-15T01:52:30Z",
18
- "src": "probe"
19
- },
20
  {
21
  "ok": true,
22
  "latency_ms": 771,
@@ -40,6 +34,12 @@
40
  "latency_ms": 772,
41
  "ts": "2026-05-15T02:03:36Z",
42
  "src": "probe"
 
 
 
 
 
 
43
  }
44
  ],
45
  "degraded_until_monotonic": 0.0,
@@ -53,18 +53,12 @@
53
  "model": "qwen/qwen3.5-122b-a10b",
54
  "status": "down",
55
  "last_success_at": null,
56
- "last_failure_at": "2026-05-15T02:03:36Z",
57
  "last_error": "timeout",
58
  "latency_ms": null,
59
- "consecutive_failures": 21,
60
- "tested_at": "2026-05-15T02:03:36Z",
61
  "probe_history": [
62
- {
63
- "ok": false,
64
- "latency_ms": 8001,
65
- "ts": "2026-05-15T01:52:30Z",
66
- "src": "probe"
67
- },
68
  {
69
  "ok": false,
70
  "latency_ms": 8002,
@@ -88,6 +82,12 @@
88
  "latency_ms": 8002,
89
  "ts": "2026-05-15T02:03:36Z",
90
  "src": "probe"
 
 
 
 
 
 
91
  }
92
  ],
93
  "degraded_until_monotonic": 0.0,
@@ -101,18 +101,12 @@
101
  "model": "openai/gpt-oss-120b",
102
  "status": "down",
103
  "last_success_at": null,
104
- "last_failure_at": "2026-05-15T02:03:36Z",
105
  "last_error": "empty_content",
106
  "latency_ms": null,
107
- "consecutive_failures": 21,
108
- "tested_at": "2026-05-15T02:03:36Z",
109
  "probe_history": [
110
- {
111
- "ok": false,
112
- "latency_ms": 253,
113
- "ts": "2026-05-15T01:52:30Z",
114
- "src": "probe"
115
- },
116
  {
117
  "ok": false,
118
  "latency_ms": 311,
@@ -136,6 +130,12 @@
136
  "latency_ms": 271,
137
  "ts": "2026-05-15T02:03:36Z",
138
  "src": "probe"
 
 
 
 
 
 
139
  }
140
  ],
141
  "degraded_until_monotonic": 0.0,
@@ -148,19 +148,13 @@
148
  "mistralai/mistral-large-3-675b-instruct-2512": {
149
  "model": "mistralai/mistral-large-3-675b-instruct-2512",
150
  "status": "healthy",
151
- "last_success_at": "2026-05-15T02:03:36Z",
152
  "last_failure_at": "2026-05-15T02:00:06Z",
153
  "last_error": null,
154
- "latency_ms": 514,
155
  "consecutive_failures": 0,
156
- "tested_at": "2026-05-15T02:03:36Z",
157
  "probe_history": [
158
- {
159
- "ok": false,
160
- "latency_ms": 8002,
161
- "ts": "2026-05-15T01:52:30Z",
162
- "src": "probe"
163
- },
164
  {
165
  "ok": true,
166
  "latency_ms": 617,
@@ -184,6 +178,12 @@
184
  "latency_ms": 514,
185
  "ts": "2026-05-15T02:03:36Z",
186
  "src": "probe"
 
 
 
 
 
 
187
  }
188
  ],
189
  "degraded_until_monotonic": 0.0,
@@ -196,19 +196,13 @@
196
  "nvidia/llama-3.3-nemotron-super-49b-v1.5": {
197
  "model": "nvidia/llama-3.3-nemotron-super-49b-v1.5",
198
  "status": "healthy",
199
- "last_success_at": "2026-05-15T02:03:36Z",
200
  "last_failure_at": "2026-05-15T01:24:45Z",
201
  "last_error": null,
202
- "latency_ms": 315,
203
  "consecutive_failures": 0,
204
- "tested_at": "2026-05-15T02:03:36Z",
205
  "probe_history": [
206
- {
207
- "ok": true,
208
- "latency_ms": 349,
209
- "ts": "2026-05-15T01:52:30Z",
210
- "src": "probe"
211
- },
212
  {
213
  "ok": true,
214
  "latency_ms": 5838,
@@ -232,6 +226,12 @@
232
  "latency_ms": 315,
233
  "ts": "2026-05-15T02:03:36Z",
234
  "src": "probe"
 
 
 
 
 
 
235
  }
236
  ],
237
  "degraded_until_monotonic": 0.0,
@@ -243,20 +243,14 @@
243
  },
244
  "meta/llama-3.3-70b-instruct": {
245
  "model": "meta/llama-3.3-70b-instruct",
246
- "status": "healthy",
247
  "last_success_at": "2026-05-15T02:03:36Z",
248
- "last_failure_at": "2026-05-15T01:41:07Z",
249
- "last_error": null,
250
  "latency_ms": 698,
251
- "consecutive_failures": 0,
252
- "tested_at": "2026-05-15T02:03:36Z",
253
  "probe_history": [
254
- {
255
- "ok": true,
256
- "latency_ms": 212,
257
- "ts": "2026-05-15T01:52:30Z",
258
- "src": "probe"
259
- },
260
  {
261
  "ok": true,
262
  "latency_ms": 3890,
@@ -280,6 +274,12 @@
280
  "latency_ms": 698,
281
  "ts": "2026-05-15T02:03:36Z",
282
  "src": "probe"
 
 
 
 
 
 
283
  }
284
  ],
285
  "degraded_until_monotonic": 0.0,
@@ -293,18 +293,12 @@
293
  "model": "deepseek-ai/deepseek-v4-pro",
294
  "status": "down",
295
  "last_success_at": null,
296
- "last_failure_at": "2026-05-15T02:03:36Z",
297
  "last_error": "timeout",
298
  "latency_ms": null,
299
- "consecutive_failures": 21,
300
- "tested_at": "2026-05-15T02:03:36Z",
301
  "probe_history": [
302
- {
303
- "ok": false,
304
- "latency_ms": 8003,
305
- "ts": "2026-05-15T01:52:30Z",
306
- "src": "probe"
307
- },
308
  {
309
  "ok": false,
310
  "latency_ms": 8002,
@@ -328,6 +322,12 @@
328
  "latency_ms": 8003,
329
  "ts": "2026-05-15T02:03:36Z",
330
  "src": "probe"
 
 
 
 
 
 
331
  }
332
  ],
333
  "degraded_until_monotonic": 0.0,
@@ -341,18 +341,12 @@
341
  "model": "nvidia/nemotron-3-nano-30b-a3b",
342
  "status": "down",
343
  "last_success_at": null,
344
- "last_failure_at": "2026-05-15T02:03:36Z",
345
  "last_error": "empty_content",
346
  "latency_ms": null,
347
- "consecutive_failures": 21,
348
- "tested_at": "2026-05-15T02:03:36Z",
349
  "probe_history": [
350
- {
351
- "ok": false,
352
- "latency_ms": 263,
353
- "ts": "2026-05-15T01:52:30Z",
354
- "src": "probe"
355
- },
356
  {
357
  "ok": false,
358
  "latency_ms": 276,
@@ -376,6 +370,12 @@
376
  "latency_ms": 283,
377
  "ts": "2026-05-15T02:03:36Z",
378
  "src": "probe"
 
 
 
 
 
 
379
  }
380
  ],
381
  "degraded_until_monotonic": 0.0,
@@ -389,18 +389,12 @@
389
  "model": "deepseek-ai/deepseek-v4-flash",
390
  "status": "down",
391
  "last_success_at": null,
392
- "last_failure_at": "2026-05-15T02:03:36Z",
393
  "last_error": "timeout",
394
  "latency_ms": null,
395
- "consecutive_failures": 21,
396
- "tested_at": "2026-05-15T02:03:36Z",
397
  "probe_history": [
398
- {
399
- "ok": false,
400
- "latency_ms": 8004,
401
- "ts": "2026-05-15T01:52:30Z",
402
- "src": "probe"
403
- },
404
  {
405
  "ok": false,
406
  "latency_ms": 8003,
@@ -424,6 +418,12 @@
424
  "latency_ms": 8001,
425
  "ts": "2026-05-15T02:03:36Z",
426
  "src": "probe"
 
 
 
 
 
 
427
  }
428
  ],
429
  "degraded_until_monotonic": 0.0,
@@ -437,18 +437,12 @@
437
  "model": "moonshotai/kimi-k2-instruct-0905",
438
  "status": "down",
439
  "last_success_at": null,
440
- "last_failure_at": "2026-05-15T02:03:36Z",
441
  "last_error": "http_404",
442
  "latency_ms": null,
443
- "consecutive_failures": 21,
444
- "tested_at": "2026-05-15T02:03:36Z",
445
  "probe_history": [
446
- {
447
- "ok": false,
448
- "latency_ms": 84,
449
- "ts": "2026-05-15T01:52:30Z",
450
- "src": "probe"
451
- },
452
  {
453
  "ok": false,
454
  "latency_ms": 81,
@@ -472,6 +466,12 @@
472
  "latency_ms": 91,
473
  "ts": "2026-05-15T02:03:36Z",
474
  "src": "probe"
 
 
 
 
 
 
475
  }
476
  ],
477
  "degraded_until_monotonic": 0.0,
@@ -485,18 +485,12 @@
485
  "model": "minimaxai/minimax-m2.5",
486
  "status": "down",
487
  "last_success_at": null,
488
- "last_failure_at": "2026-05-15T02:03:36Z",
489
  "last_error": "http_410",
490
  "latency_ms": null,
491
- "consecutive_failures": 21,
492
- "tested_at": "2026-05-15T02:03:36Z",
493
  "probe_history": [
494
- {
495
- "ok": false,
496
- "latency_ms": 29,
497
- "ts": "2026-05-15T01:52:30Z",
498
- "src": "probe"
499
- },
500
  {
501
  "ok": false,
502
  "latency_ms": 31,
@@ -520,6 +514,12 @@
520
  "latency_ms": 31,
521
  "ts": "2026-05-15T02:03:36Z",
522
  "src": "probe"
 
 
 
 
 
 
523
  }
524
  ],
525
  "degraded_until_monotonic": 0.0,
@@ -532,19 +532,13 @@
532
  "meta/llama-4-maverick-17b-128e-instruct": {
533
  "model": "meta/llama-4-maverick-17b-128e-instruct",
534
  "status": "healthy",
535
- "last_success_at": "2026-05-15T02:03:36Z",
536
  "last_failure_at": "2026-05-14T01:03:07Z",
537
  "last_error": null,
538
- "latency_ms": 214,
539
  "consecutive_failures": 0,
540
- "tested_at": "2026-05-15T02:03:36Z",
541
  "probe_history": [
542
- {
543
- "ok": true,
544
- "latency_ms": 205,
545
- "ts": "2026-05-15T01:52:30Z",
546
- "src": "probe"
547
- },
548
  {
549
  "ok": true,
550
  "latency_ms": 247,
@@ -568,6 +562,12 @@
568
  "latency_ms": 214,
569
  "ts": "2026-05-15T02:03:36Z",
570
  "src": "probe"
 
 
 
 
 
 
571
  }
572
  ],
573
  "degraded_until_monotonic": 0.0,
@@ -581,18 +581,12 @@
581
  "model": "openrouter:openai/gpt-oss-120b",
582
  "status": "down",
583
  "last_success_at": "2026-05-15T01:46:37Z",
584
- "last_failure_at": "2026-05-15T02:03:36Z",
585
  "last_error": "empty_content",
586
  "latency_ms": 1349,
587
- "consecutive_failures": 5,
588
- "tested_at": "2026-05-15T02:03:36Z",
589
  "probe_history": [
590
- {
591
- "ok": false,
592
- "latency_ms": 1635,
593
- "ts": "2026-05-15T01:52:30Z",
594
- "src": "probe"
595
- },
596
  {
597
  "ok": false,
598
  "latency_ms": 856,
@@ -616,6 +610,12 @@
616
  "latency_ms": 497,
617
  "ts": "2026-05-15T02:03:36Z",
618
  "src": "probe"
 
 
 
 
 
 
619
  }
620
  ],
621
  "degraded_until_monotonic": 0.0,
@@ -628,19 +628,13 @@
628
  "groq:llama-3.3-70b-versatile": {
629
  "model": "groq:llama-3.3-70b-versatile",
630
  "status": "healthy",
631
- "last_success_at": "2026-05-15T02:03:36Z",
632
  "last_failure_at": null,
633
  "last_error": null,
634
- "latency_ms": 252,
635
  "consecutive_failures": 0,
636
- "tested_at": "2026-05-15T02:03:36Z",
637
  "probe_history": [
638
- {
639
- "ok": true,
640
- "latency_ms": 247,
641
- "ts": "2026-05-15T01:52:30Z",
642
- "src": "probe"
643
- },
644
  {
645
  "ok": true,
646
  "latency_ms": 261,
@@ -664,6 +658,12 @@
664
  "latency_ms": 252,
665
  "ts": "2026-05-15T02:03:36Z",
666
  "src": "probe"
 
 
 
 
 
 
667
  }
668
  ],
669
  "degraded_until_monotonic": 0.0,
 
1
  {
2
+ "updated_at": "2026-05-15T02:09:13Z",
3
  "models": {
4
  "qwen/qwen3-next-80b-a3b-instruct": {
5
  "model": "qwen/qwen3-next-80b-a3b-instruct",
6
  "status": "healthy",
7
+ "last_success_at": "2026-05-15T02:09:13Z",
8
  "last_failure_at": null,
9
  "last_error": null,
10
+ "latency_ms": 869,
11
  "consecutive_failures": 0,
12
+ "tested_at": "2026-05-15T02:09:13Z",
13
  "probe_history": [
 
 
 
 
 
 
14
  {
15
  "ok": true,
16
  "latency_ms": 771,
 
34
  "latency_ms": 772,
35
  "ts": "2026-05-15T02:03:36Z",
36
  "src": "probe"
37
+ },
38
+ {
39
+ "ok": true,
40
+ "latency_ms": 869,
41
+ "ts": "2026-05-15T02:09:13Z",
42
+ "src": "probe"
43
  }
44
  ],
45
  "degraded_until_monotonic": 0.0,
 
53
  "model": "qwen/qwen3.5-122b-a10b",
54
  "status": "down",
55
  "last_success_at": null,
56
+ "last_failure_at": "2026-05-15T02:09:13Z",
57
  "last_error": "timeout",
58
  "latency_ms": null,
59
+ "consecutive_failures": 22,
60
+ "tested_at": "2026-05-15T02:09:13Z",
61
  "probe_history": [
 
 
 
 
 
 
62
  {
63
  "ok": false,
64
  "latency_ms": 8002,
 
82
  "latency_ms": 8002,
83
  "ts": "2026-05-15T02:03:36Z",
84
  "src": "probe"
85
+ },
86
+ {
87
+ "ok": false,
88
+ "latency_ms": 8002,
89
+ "ts": "2026-05-15T02:09:13Z",
90
+ "src": "probe"
91
  }
92
  ],
93
  "degraded_until_monotonic": 0.0,
 
101
  "model": "openai/gpt-oss-120b",
102
  "status": "down",
103
  "last_success_at": null,
104
+ "last_failure_at": "2026-05-15T02:09:13Z",
105
  "last_error": "empty_content",
106
  "latency_ms": null,
107
+ "consecutive_failures": 22,
108
+ "tested_at": "2026-05-15T02:09:13Z",
109
  "probe_history": [
 
 
 
 
 
 
110
  {
111
  "ok": false,
112
  "latency_ms": 311,
 
130
  "latency_ms": 271,
131
  "ts": "2026-05-15T02:03:36Z",
132
  "src": "probe"
133
+ },
134
+ {
135
+ "ok": false,
136
+ "latency_ms": 340,
137
+ "ts": "2026-05-15T02:09:13Z",
138
+ "src": "probe"
139
  }
140
  ],
141
  "degraded_until_monotonic": 0.0,
 
148
  "mistralai/mistral-large-3-675b-instruct-2512": {
149
  "model": "mistralai/mistral-large-3-675b-instruct-2512",
150
  "status": "healthy",
151
+ "last_success_at": "2026-05-15T02:09:13Z",
152
  "last_failure_at": "2026-05-15T02:00:06Z",
153
  "last_error": null,
154
+ "latency_ms": 726,
155
  "consecutive_failures": 0,
156
+ "tested_at": "2026-05-15T02:09:13Z",
157
  "probe_history": [
 
 
 
 
 
 
158
  {
159
  "ok": true,
160
  "latency_ms": 617,
 
178
  "latency_ms": 514,
179
  "ts": "2026-05-15T02:03:36Z",
180
  "src": "probe"
181
+ },
182
+ {
183
+ "ok": true,
184
+ "latency_ms": 726,
185
+ "ts": "2026-05-15T02:09:13Z",
186
+ "src": "probe"
187
  }
188
  ],
189
  "degraded_until_monotonic": 0.0,
 
196
  "nvidia/llama-3.3-nemotron-super-49b-v1.5": {
197
  "model": "nvidia/llama-3.3-nemotron-super-49b-v1.5",
198
  "status": "healthy",
199
+ "last_success_at": "2026-05-15T02:09:13Z",
200
  "last_failure_at": "2026-05-15T01:24:45Z",
201
  "last_error": null,
202
+ "latency_ms": 364,
203
  "consecutive_failures": 0,
204
+ "tested_at": "2026-05-15T02:09:13Z",
205
  "probe_history": [
 
 
 
 
 
 
206
  {
207
  "ok": true,
208
  "latency_ms": 5838,
 
226
  "latency_ms": 315,
227
  "ts": "2026-05-15T02:03:36Z",
228
  "src": "probe"
229
+ },
230
+ {
231
+ "ok": true,
232
+ "latency_ms": 364,
233
+ "ts": "2026-05-15T02:09:13Z",
234
+ "src": "probe"
235
  }
236
  ],
237
  "degraded_until_monotonic": 0.0,
 
243
  },
244
  "meta/llama-3.3-70b-instruct": {
245
  "model": "meta/llama-3.3-70b-instruct",
246
+ "status": "degraded",
247
  "last_success_at": "2026-05-15T02:03:36Z",
248
+ "last_failure_at": "2026-05-15T02:09:13Z",
249
+ "last_error": "timeout",
250
  "latency_ms": 698,
251
+ "consecutive_failures": 1,
252
+ "tested_at": "2026-05-15T02:09:13Z",
253
  "probe_history": [
 
 
 
 
 
 
254
  {
255
  "ok": true,
256
  "latency_ms": 3890,
 
274
  "latency_ms": 698,
275
  "ts": "2026-05-15T02:03:36Z",
276
  "src": "probe"
277
+ },
278
+ {
279
+ "ok": false,
280
+ "latency_ms": 8001,
281
+ "ts": "2026-05-15T02:09:13Z",
282
+ "src": "probe"
283
  }
284
  ],
285
  "degraded_until_monotonic": 0.0,
 
293
  "model": "deepseek-ai/deepseek-v4-pro",
294
  "status": "down",
295
  "last_success_at": null,
296
+ "last_failure_at": "2026-05-15T02:09:13Z",
297
  "last_error": "timeout",
298
  "latency_ms": null,
299
+ "consecutive_failures": 22,
300
+ "tested_at": "2026-05-15T02:09:13Z",
301
  "probe_history": [
 
 
 
 
 
 
302
  {
303
  "ok": false,
304
  "latency_ms": 8002,
 
322
  "latency_ms": 8003,
323
  "ts": "2026-05-15T02:03:36Z",
324
  "src": "probe"
325
+ },
326
+ {
327
+ "ok": false,
328
+ "latency_ms": 8055,
329
+ "ts": "2026-05-15T02:09:13Z",
330
+ "src": "probe"
331
  }
332
  ],
333
  "degraded_until_monotonic": 0.0,
 
341
  "model": "nvidia/nemotron-3-nano-30b-a3b",
342
  "status": "down",
343
  "last_success_at": null,
344
+ "last_failure_at": "2026-05-15T02:09:13Z",
345
  "last_error": "empty_content",
346
  "latency_ms": null,
347
+ "consecutive_failures": 22,
348
+ "tested_at": "2026-05-15T02:09:13Z",
349
  "probe_history": [
 
 
 
 
 
 
350
  {
351
  "ok": false,
352
  "latency_ms": 276,
 
370
  "latency_ms": 283,
371
  "ts": "2026-05-15T02:03:36Z",
372
  "src": "probe"
373
+ },
374
+ {
375
+ "ok": false,
376
+ "latency_ms": 263,
377
+ "ts": "2026-05-15T02:09:13Z",
378
+ "src": "probe"
379
  }
380
  ],
381
  "degraded_until_monotonic": 0.0,
 
389
  "model": "deepseek-ai/deepseek-v4-flash",
390
  "status": "down",
391
  "last_success_at": null,
392
+ "last_failure_at": "2026-05-15T02:09:13Z",
393
  "last_error": "timeout",
394
  "latency_ms": null,
395
+ "consecutive_failures": 22,
396
+ "tested_at": "2026-05-15T02:09:13Z",
397
  "probe_history": [
 
 
 
 
 
 
398
  {
399
  "ok": false,
400
  "latency_ms": 8003,
 
418
  "latency_ms": 8001,
419
  "ts": "2026-05-15T02:03:36Z",
420
  "src": "probe"
421
+ },
422
+ {
423
+ "ok": false,
424
+ "latency_ms": 8002,
425
+ "ts": "2026-05-15T02:09:13Z",
426
+ "src": "probe"
427
  }
428
  ],
429
  "degraded_until_monotonic": 0.0,
 
437
  "model": "moonshotai/kimi-k2-instruct-0905",
438
  "status": "down",
439
  "last_success_at": null,
440
+ "last_failure_at": "2026-05-15T02:09:13Z",
441
  "last_error": "http_404",
442
  "latency_ms": null,
443
+ "consecutive_failures": 22,
444
+ "tested_at": "2026-05-15T02:09:13Z",
445
  "probe_history": [
 
 
 
 
 
 
446
  {
447
  "ok": false,
448
  "latency_ms": 81,
 
466
  "latency_ms": 91,
467
  "ts": "2026-05-15T02:03:36Z",
468
  "src": "probe"
469
+ },
470
+ {
471
+ "ok": false,
472
+ "latency_ms": 86,
473
+ "ts": "2026-05-15T02:09:13Z",
474
+ "src": "probe"
475
  }
476
  ],
477
  "degraded_until_monotonic": 0.0,
 
485
  "model": "minimaxai/minimax-m2.5",
486
  "status": "down",
487
  "last_success_at": null,
488
+ "last_failure_at": "2026-05-15T02:09:13Z",
489
  "last_error": "http_410",
490
  "latency_ms": null,
491
+ "consecutive_failures": 22,
492
+ "tested_at": "2026-05-15T02:09:13Z",
493
  "probe_history": [
 
 
 
 
 
 
494
  {
495
  "ok": false,
496
  "latency_ms": 31,
 
514
  "latency_ms": 31,
515
  "ts": "2026-05-15T02:03:36Z",
516
  "src": "probe"
517
+ },
518
+ {
519
+ "ok": false,
520
+ "latency_ms": 30,
521
+ "ts": "2026-05-15T02:09:13Z",
522
+ "src": "probe"
523
  }
524
  ],
525
  "degraded_until_monotonic": 0.0,
 
532
  "meta/llama-4-maverick-17b-128e-instruct": {
533
  "model": "meta/llama-4-maverick-17b-128e-instruct",
534
  "status": "healthy",
535
+ "last_success_at": "2026-05-15T02:09:13Z",
536
  "last_failure_at": "2026-05-14T01:03:07Z",
537
  "last_error": null,
538
+ "latency_ms": 268,
539
  "consecutive_failures": 0,
540
+ "tested_at": "2026-05-15T02:09:13Z",
541
  "probe_history": [
 
 
 
 
 
 
542
  {
543
  "ok": true,
544
  "latency_ms": 247,
 
562
  "latency_ms": 214,
563
  "ts": "2026-05-15T02:03:36Z",
564
  "src": "probe"
565
+ },
566
+ {
567
+ "ok": true,
568
+ "latency_ms": 268,
569
+ "ts": "2026-05-15T02:09:13Z",
570
+ "src": "probe"
571
  }
572
  ],
573
  "degraded_until_monotonic": 0.0,
 
581
  "model": "openrouter:openai/gpt-oss-120b",
582
  "status": "down",
583
  "last_success_at": "2026-05-15T01:46:37Z",
584
+ "last_failure_at": "2026-05-15T02:09:13Z",
585
  "last_error": "empty_content",
586
  "latency_ms": 1349,
587
+ "consecutive_failures": 6,
588
+ "tested_at": "2026-05-15T02:09:13Z",
589
  "probe_history": [
 
 
 
 
 
 
590
  {
591
  "ok": false,
592
  "latency_ms": 856,
 
610
  "latency_ms": 497,
611
  "ts": "2026-05-15T02:03:36Z",
612
  "src": "probe"
613
+ },
614
+ {
615
+ "ok": false,
616
+ "latency_ms": 1535,
617
+ "ts": "2026-05-15T02:09:13Z",
618
+ "src": "probe"
619
  }
620
  ],
621
  "degraded_until_monotonic": 0.0,
 
628
  "groq:llama-3.3-70b-versatile": {
629
  "model": "groq:llama-3.3-70b-versatile",
630
  "status": "healthy",
631
+ "last_success_at": "2026-05-15T02:09:13Z",
632
  "last_failure_at": null,
633
  "last_error": null,
634
+ "latency_ms": 260,
635
  "consecutive_failures": 0,
636
+ "tested_at": "2026-05-15T02:09:13Z",
637
  "probe_history": [
 
 
 
 
 
 
638
  {
639
  "ok": true,
640
  "latency_ms": 261,
 
658
  "latency_ms": 252,
659
  "ts": "2026-05-15T02:03:36Z",
660
  "src": "probe"
661
+ },
662
+ {
663
+ "ok": true,
664
+ "latency_ms": 260,
665
+ "ts": "2026-05-15T02:09:13Z",
666
+ "src": "probe"
667
  }
668
  ],
669
  "degraded_until_monotonic": 0.0,
Dockerfile CHANGED
@@ -70,7 +70,7 @@ COPY 40-data ./40-data
70
  # (7356 chunks from a prior ingest) instead of the freshly-uploaded
71
  # cleaned one (3799 chunks). Bump CACHE_BUST manually each time the
72
  # dataset is re-uploaded; the value just needs to change.
73
- ARG DATASET_CACHE_BUST=2026-05-15-ki126-v1
74
  RUN echo "Dataset cache bust: ${DATASET_CACHE_BUST}" && python -c "\
75
  from huggingface_hub import snapshot_download; \
76
  snapshot_download(\
 
70
  # (7356 chunks from a prior ingest) instead of the freshly-uploaded
71
  # cleaned one (3799 chunks). Bump CACHE_BUST manually each time the
72
  # dataset is re-uploaded; the value just needs to change.
73
+ ARG DATASET_CACHE_BUST=2026-05-15-ki137-v1
74
  RUN echo "Dataset cache bust: ${DATASET_CACHE_BUST}" && python -c "\
75
  from huggingface_hub import snapshot_download; \
76
  snapshot_download(\
frontend/src/lib/useLiveConversation.ts CHANGED
@@ -87,10 +87,12 @@ export type LiveConversationState = {
87
  };
88
 
89
  const DEFAULTS = {
90
- // KI-113 (2026-05-15) β€” raised from 18 β†’ 26 to reject ambient background
91
- // noise (HVAC, traffic, distant chatter). Effective threshold is
92
- // max(this, adaptive noise_floor * 2.5 + 6) β€” see KI-114 in the VAD loop.
93
- rmsThreshold: 26,
 
 
94
  // KI-113 β€” raised 3 β†’ 5 (~80 ms sustained). Single clicks / cutlery /
95
  // typing transients no longer flip the gate. Preroll buffer (KI-044)
96
  // still captures the first phoneme via the 300 ms look-back.
@@ -119,12 +121,15 @@ const DEFAULTS = {
119
  // segment. Avoids bot's TTS attack transient bleeding through even
120
  // with echoCancellation on.
121
  postUtteranceCooldownMs: 700,
122
- // KI-113 raised 0.35 β†’ 0.50 to gate out broadband HVAC / fan / traffic.
123
- // KI-134 (2026-05-15) β€” backed off to 0.35 because some laptop built-in
124
- // mics with aggressive noiseSuppression flatten the voice-band proportion
125
- // to 0.35-0.45, so the VAD never opens and the green pill renders but
126
- // no audio is ever posted β€” matches the live-test symptom exactly.
127
- voiceBandMinProp: 0.35,
 
 
 
128
  };
129
 
130
  // AudioWorklet processor source β€” inlined as a Blob URL so we don't need
@@ -329,9 +334,14 @@ export function useLiveConversation(opts: LiveConversationOptions): LiveConversa
329
  // (which sits just above ambient because high frequencies attenuate
330
  // with distance) is rejected. Close-in speech still clears the gate
331
  // because the speaker's directly-radiated energy is ~10-20Γ— ambient.
 
 
 
 
 
332
  const effectiveThreshold = Math.max(
333
  cfg.rmsThreshold,
334
- noiseFloorRef.current * 2.5 + 6,
335
  );
336
 
337
  // KI-057 β€” suppress new triggers right after we closed a segment
@@ -507,6 +517,17 @@ export function useLiveConversation(opts: LiveConversationOptions): LiveConversa
507
  return;
508
  }
509
  }
 
 
 
 
 
 
 
 
 
 
 
510
  sampleRateRef.current = ctx.sampleRate;
511
 
512
  const source = ctx.createMediaStreamSource(stream);
 
87
  };
88
 
89
  const DEFAULTS = {
90
+ // KI-113 raised from 18 β†’ 26 to reject ambient noise.
91
+ // KI-139 (2026-05-15) β€” backed off to 18 because voice-forensics agent
92
+ // proved 26 sat ABOVE typical speech avg on consumer mics (especially
93
+ // built-ins with active noise gate that pin noiseFloor to 0). VAD never
94
+ // opened β†’ green pill rendered β†’ zero audio posted.
95
+ rmsThreshold: 18,
96
  // KI-113 β€” raised 3 β†’ 5 (~80 ms sustained). Single clicks / cutlery /
97
  // typing transients no longer flip the gate. Preroll buffer (KI-044)
98
  // still captures the first phoneme via the 300 ms look-back.
 
121
  // segment. Avoids bot's TTS attack transient bleeding through even
122
  // with echoCancellation on.
123
  postUtteranceCooldownMs: 700,
124
+ // KI-113 raised to 0.50, KI-134 backed off to 0.35.
125
+ // KI-139 (2026-05-15) β€” voice-forensics agent measured live bundle on
126
+ // user's actual hardware: voiceProp sits at 0.25–0.33 on quiet laptop
127
+ // mics with NS enabled. 0.35 still gates the user out. 0.20 puts the
128
+ // floor well below voiced-speech minimum and only rejects pure tones
129
+ // (constant whine of HVAC, traffic). This is the deepest pushback
130
+ // β€” if HVAC noise creeps in, KI-140 will add a /api/transcribe round
131
+ // trip that detects empty responses and surfaces "couldn't hear you".
132
+ voiceBandMinProp: 0.20,
133
  };
134
 
135
  // AudioWorklet processor source β€” inlined as a Blob URL so we don't need
 
334
  // (which sits just above ambient because high frequencies attenuate
335
  // with distance) is rejected. Close-in speech still clears the gate
336
  // because the speaker's directly-radiated energy is ~10-20Γ— ambient.
337
+ // KI-139 (2026-05-15) β€” noise-floor multiplier 2.5 β†’ 1.8. On built-in
338
+ // mics with active noise gate, noiseFloor EMAs to 0 β†’ effectiveThreshold
339
+ // pinned at max(rmsThreshold, 6). User's voice avg sits 18-22 then
340
+ // never crosses. 1.8 keeps headroom for HVAC (which pins at 5-7) but
341
+ // lets quiet voice through.
342
  const effectiveThreshold = Math.max(
343
  cfg.rmsThreshold,
344
+ noiseFloorRef.current * 1.8 + 6,
345
  );
346
 
347
  // KI-057 β€” suppress new triggers right after we closed a segment
 
517
  return;
518
  }
519
  }
520
+ // KI-139 (2026-05-15) β€” Safari iOS resume() can return without
521
+ // throwing yet leave state at "suspended" β€” silent rejection of the
522
+ // autoplay-policy unlock. Treat anything other than "running" as a
523
+ // failure and surface to the user.
524
+ if (ctx.state !== "running") {
525
+ // eslint-disable-next-line no-console
526
+ console.error("[live-mode] AudioContext state stuck at", ctx.state, "β€” giving up");
527
+ setMicPermissionDenied(true);
528
+ setLive(false);
529
+ return;
530
+ }
531
  sampleRateRef.current = ctx.sampleRate;
532
 
533
  const source = ctx.createMediaStreamSource(stream);
frontend/tsconfig.tsbuildinfo CHANGED
The diff for this file is too large to render. See raw diff
 
tools/canonicalize_policy_names.py ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """KI-138: Canonicalize policy_name across extracted JSONs and Chroma metadata.
3
+
4
+ Strategy:
5
+ - For each `policy_id_base` (policy_id stripped of trailing `__<doctype>`),
6
+ join extracted policy_name(s) against Chroma chunk policy_name(s).
7
+ - When they disagree, treat the Chroma-majority name as the canonical label
8
+ (Chroma names are short clean labels; extracted names often contain
9
+ parenthetical descriptions or doctype suffixes).
10
+ - Overwrite the extracted JSON `policy_name` and update every Chroma chunk's
11
+ `policy_name` metadata in place.
12
+
13
+ Skip list (require human review β€” left untouched):
14
+ - bajaj-allianz__group-health-guard-gold (Silver/Gold conflict)
15
+ - reliance-general__hospi-care, __health-gain, __group-mediclaim (IndusInd co-brand)
16
+ - regulatory__irda-grievance-redressal-handbook (irda→irdai slug rename)
17
+ - regulatory__protection-of-policyholders-interests-2024 (slug rename)
18
+
19
+ Backup of Chroma sqlite was taken before running:
20
+ rag/vectors/chroma.sqlite3.pre-ki138.bak
21
+
22
+ Run:
23
+ /Users/rohitsar/.cache/uv-venvs/insurance-sales-bot/bin/python3 \
24
+ tools/canonicalize_policy_names.py
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import glob
30
+ import json
31
+ import os
32
+ import sys
33
+ from collections import Counter, defaultdict
34
+ from typing import Dict, List, Tuple
35
+
36
+ import chromadb
37
+
38
+ REPO = '/Users/rohitsar/Developer/Insurance Sales Bot'
39
+ EXTRACTED_DIR = os.path.join(REPO, 'rag/_hf_dataset_backup/rag/extracted')
40
+ CHROMA_PATH = os.path.join(REPO, 'rag/vectors/')
41
+ MISMATCH_REPORT = '/tmp/policy_name_mismatches.json'
42
+
43
+ SKIP_BASES = {
44
+ 'bajaj-allianz__group-health-guard-gold',
45
+ 'reliance-general__hospi-care',
46
+ 'reliance-general__health-gain',
47
+ 'reliance-general__group-mediclaim',
48
+ 'regulatory__irda-grievance-redressal-handbook',
49
+ 'regulatory__protection-of-policyholders-interests-2024',
50
+ }
51
+
52
+
53
+ def policy_id_base(pid: str) -> str:
54
+ parts = pid.split('__')
55
+ return '__'.join(parts[:-1]) if len(parts) >= 3 else pid
56
+
57
+
58
+ def load_extracted() -> Dict[str, List[Tuple[str, str, str]]]:
59
+ """base -> [(path, policy_name, policy_id), ...]"""
60
+ out: Dict[str, List[Tuple[str, str, str]]] = defaultdict(list)
61
+ for path in glob.glob(os.path.join(EXTRACTED_DIR, '*.json')):
62
+ fname = os.path.basename(path)
63
+ if fname.startswith('_'):
64
+ continue
65
+ try:
66
+ with open(path, 'r', encoding='utf-8') as fh:
67
+ d = json.load(fh)
68
+ except Exception as exc:
69
+ print(f'WARN unreadable extracted JSON {path}: {exc}', file=sys.stderr)
70
+ continue
71
+ pid = d.get('policy_id', fname.replace('.json', ''))
72
+ pname = d.get('policy_name', '')
73
+ out[policy_id_base(pid)].append((path, pname, pid))
74
+ return out
75
+
76
+
77
+ def load_chroma(coll) -> Dict[str, List[Tuple[str, str, str]]]:
78
+ """base -> [(chunk_id, policy_name, policy_id), ...]"""
79
+ got = coll.get(include=['metadatas'])
80
+ out: Dict[str, List[Tuple[str, str, str]]] = defaultdict(list)
81
+ for cid, md in zip(got['ids'], got['metadatas']):
82
+ pid = md.get('policy_id', '')
83
+ pname = md.get('policy_name', '')
84
+ out[policy_id_base(pid)].append((cid, pname, pid))
85
+ return out
86
+
87
+
88
+ def build_mismatch_table(extracted, chroma) -> List[dict]:
89
+ rows: List[dict] = []
90
+ bases = set(extracted) | set(chroma)
91
+ for base in sorted(bases):
92
+ ext = extracted.get(base, [])
93
+ chr_ = chroma.get(base, [])
94
+ if not ext or not chr_:
95
+ continue
96
+ ext_names = [n for _, n, _ in ext]
97
+ chr_names = [n for _, n, _ in chr_]
98
+ if set(ext_names) == set(chr_names):
99
+ continue
100
+ chr_majority = Counter(chr_names).most_common(1)[0][0]
101
+ rows.append({
102
+ 'policy_id_base': base,
103
+ 'extracted_name': Counter(ext_names).most_common(1)[0][0],
104
+ 'extracted_names_all': sorted(set(ext_names)),
105
+ 'chroma_name_majority': chr_majority,
106
+ 'chroma_names_all': sorted(set(chr_names)),
107
+ 'proposed_canonical': chr_majority,
108
+ 'extracted_files': [p for p, _, _ in ext],
109
+ 'chroma_doc_ids': [c for c, _, _ in chr_],
110
+ 'chroma_chunk_count': len(chr_),
111
+ })
112
+ return rows
113
+
114
+
115
+ def main() -> int:
116
+ client = chromadb.PersistentClient(path=CHROMA_PATH)
117
+ coll = client.get_collection('policies')
118
+
119
+ extracted = load_extracted()
120
+ chroma = load_chroma(coll)
121
+
122
+ rows = build_mismatch_table(extracted, chroma)
123
+ with open(MISMATCH_REPORT, 'w', encoding='utf-8') as fh:
124
+ json.dump(rows, fh, indent=2, ensure_ascii=False)
125
+ print(f'mismatch table: {len(rows)} rows -> {MISMATCH_REPORT}')
126
+
127
+ actionable = [r for r in rows if r['policy_id_base'] not in SKIP_BASES]
128
+ skipped = [r for r in rows if r['policy_id_base'] in SKIP_BASES]
129
+ print(f'actionable: {len(actionable)} skipped (human review): {len(skipped)}')
130
+
131
+ json_updates = 0
132
+ json_missing = 0
133
+ chroma_updates = 0
134
+ samples = []
135
+
136
+ for row in actionable:
137
+ canonical = row['proposed_canonical']
138
+
139
+ # 1) Update extracted JSON files
140
+ for fpath in row['extracted_files']:
141
+ if not os.path.exists(fpath):
142
+ print(f'WARN missing extracted JSON: {fpath}', file=sys.stderr)
143
+ json_missing += 1
144
+ continue
145
+ try:
146
+ with open(fpath, 'r', encoding='utf-8') as fh:
147
+ d = json.load(fh)
148
+ except Exception as exc:
149
+ print(f'WARN unreadable {fpath}: {exc}', file=sys.stderr)
150
+ continue
151
+ before = d.get('policy_name', '')
152
+ if before != canonical:
153
+ d['policy_name'] = canonical
154
+ with open(fpath, 'w', encoding='utf-8') as fh:
155
+ json.dump(d, fh, indent=2, ensure_ascii=False)
156
+ json_updates += 1
157
+
158
+ # 2) Update Chroma metadata for every chunk in this base
159
+ ids_to_update: List[str] = []
160
+ metas_to_update: List[dict] = []
161
+ existing = coll.get(ids=row['chroma_doc_ids'], include=['metadatas'])
162
+ for cid, md in zip(existing['ids'], existing['metadatas']):
163
+ if md.get('policy_name') == canonical:
164
+ continue
165
+ new_md = dict(md)
166
+ new_md['policy_name'] = canonical
167
+ ids_to_update.append(cid)
168
+ metas_to_update.append(new_md)
169
+
170
+ if ids_to_update:
171
+ # batch update for speed
172
+ BATCH = 500
173
+ for i in range(0, len(ids_to_update), BATCH):
174
+ coll.update(
175
+ ids=ids_to_update[i:i + BATCH],
176
+ metadatas=metas_to_update[i:i + BATCH],
177
+ )
178
+ chroma_updates += len(ids_to_update)
179
+
180
+ if len(samples) < 5:
181
+ samples.append({
182
+ 'policy_id_base': row['policy_id_base'],
183
+ 'before_extracted': row['extracted_name'],
184
+ 'before_chroma_majority': row['chroma_name_majority'],
185
+ 'after': canonical,
186
+ })
187
+
188
+ print(f'JSON files updated: {json_updates} (missing: {json_missing})')
189
+ print(f'Chroma chunks updated: {chroma_updates}')
190
+
191
+ # 3) Verify by re-joining
192
+ extracted2 = load_extracted()
193
+ chroma2 = load_chroma(coll)
194
+ remaining = build_mismatch_table(extracted2, chroma2)
195
+ remaining_bases = [r['policy_id_base'] for r in remaining]
196
+ print(f'mismatches_remaining: {len(remaining)}')
197
+ for r in remaining:
198
+ marker = 'SKIPPED' if r['policy_id_base'] in SKIP_BASES else 'UNEXPECTED'
199
+ print(f" [{marker}] {r['policy_id_base']}: ext={r['extracted_name']!r} chr={r['chroma_name_majority']!r}")
200
+
201
+ print('\nsample before/after:')
202
+ for s in samples:
203
+ print(f" {s['policy_id_base']}")
204
+ print(f" extracted-before: {s['before_extracted']}")
205
+ print(f" chroma-before: {s['before_chroma_majority']}")
206
+ print(f" after (canonical):{s['after']}")
207
+
208
+ return 0
209
+
210
+
211
+ if __name__ == '__main__':
212
+ sys.exit(main())
tools/ingest_curated_into_chroma.py ADDED
@@ -0,0 +1,318 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Ingest curated-fact JSONs into the Chroma `policies` collection.
2
+
3
+ Problem: ~21 policies appear in the marketplace (/api/policies/all) but have
4
+ ZERO Chroma chunks, because their canonical PDFs are image-only / not in the
5
+ corpus / or the curated JSON is the only data source we have. The chat bot
6
+ cannot retrieve or cite these policies β€” RAG returns no hits.
7
+
8
+ Fix: render each curated policy_facts JSON into a flat text representation
9
+ that mimics what a wordings PDF chunk would look like, embed it with the
10
+ SAME LocalEmbeddings (BGE-small, 384-dim) used by rag/ingest.py, and add it
11
+ to the SAME `policies` collection with metadata.doc_type='curated' so the
12
+ retriever can find these the same way it finds PDF chunks.
13
+
14
+ Run:
15
+ /Users/rohitsar/.cache/uv-venvs/insurance-sales-bot/bin/python3 \
16
+ tools/ingest_curated_into_chroma.py
17
+
18
+ Idempotent β€” re-running skips any policy_id already present in Chroma.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import asyncio
24
+ import json
25
+ import re
26
+ import sys
27
+ from pathlib import Path
28
+
29
+ import chromadb
30
+ from chromadb.config import Settings as ChromaSettings
31
+
32
+ # Make backend importable
33
+ ROOT = Path(__file__).resolve().parent.parent
34
+ sys.path.insert(0, str(ROOT))
35
+
36
+ from backend.config import settings # noqa: E402
37
+ from backend.main import _load_curated_facts # noqa: E402
38
+ from backend.providers.local_embeddings import LocalEmbeddings # noqa: E402
39
+ from rag.ingest import ( # noqa: E402
40
+ CHUNK_CHARS,
41
+ OVERLAP_CHARS,
42
+ chunk_pages,
43
+ get_chroma_collection,
44
+ )
45
+
46
+ CURATED_DIR = ROOT / "40-data" / "policy_facts"
47
+ CORPUS_URLS_MD = ROOT / "40-data" / "corpus_urls.md"
48
+
49
+
50
+ # ---------- corpus_urls.md β†’ policy_id β†’ source_url ----------
51
+
52
+ def _load_corpus_urls() -> dict[str, str]:
53
+ """Parse 40-data/corpus_urls.md table rows into {insurer_slug+policy_name_slug: url}.
54
+
55
+ Used as a best-effort source_url backfill for curated policies that have
56
+ no PDF in rag/corpus/. The slug match is fuzzy β€” we slugify the
57
+ policy_name and check if it overlaps with the curated policy_id stem.
58
+ """
59
+ if not CORPUS_URLS_MD.exists():
60
+ return {}
61
+ text = CORPUS_URLS_MD.read_text()
62
+ rows: dict[str, str] = {}
63
+ for line in text.splitlines():
64
+ if not line.startswith("| ") or "---" in line or "insurer_slug" in line:
65
+ continue
66
+ cells = [c.strip() for c in line.strip("|").split("|")]
67
+ if len(cells) < 5:
68
+ continue
69
+ insurer_slug, _insurer_name, policy_name, _doc_type, url = cells[:5]
70
+ if not url.startswith("http"):
71
+ continue
72
+ name_slug = re.sub(r"[^a-z0-9]+", "-", policy_name.lower()).strip("-")
73
+ rows.setdefault(f"{insurer_slug}__{name_slug}", url)
74
+ # Also index a shorter token-overlap key for fuzzier matching
75
+ rows.setdefault(name_slug, url)
76
+ return rows
77
+
78
+
79
+ def _best_source_url(policy_id: str, curated_data: dict, url_index: dict[str, str]) -> str:
80
+ """Pick a source URL for a curated policy. Preference order:
81
+ 1. Any non-null `source_url` field in the curated JSON
82
+ 2. corpus_urls.md row whose insurer_slug__name_slug matches policy_id
83
+ 3. Empty string (downstream code accepts "" gracefully)
84
+ """
85
+ # 1. Look inside the curated JSON for any source_url
86
+ for k, v in curated_data.items():
87
+ if isinstance(v, dict) and v.get("source_url"):
88
+ return v["source_url"]
89
+ # 2. corpus_urls.md fuzzy match
90
+ if policy_id in url_index:
91
+ return url_index[policy_id]
92
+ # Try the stem (everything after `insurer__`)
93
+ parts = policy_id.split("__", 1)
94
+ if len(parts) == 2:
95
+ stem = parts[1]
96
+ if stem in url_index:
97
+ return url_index[stem]
98
+ # Loose match: check any url-index key that contains the stem
99
+ for key, url in url_index.items():
100
+ if stem in key or key in stem:
101
+ return url
102
+ return ""
103
+
104
+
105
+ # ---------- curated JSON β†’ flat text representation ----------
106
+
107
+ _HUMAN_LABEL = {
108
+ "uin_code": "UIN Code",
109
+ "min_entry_age": "Minimum Entry Age",
110
+ "max_entry_age": "Maximum Entry Age",
111
+ "max_renewal_age": "Maximum Renewal Age",
112
+ "sum_insured_options": "Sum Insured Options",
113
+ "initial_waiting_period_days": "Initial Waiting Period (days)",
114
+ "pre_existing_disease_waiting_months": "Pre-Existing Disease Waiting Period (months)",
115
+ "specific_disease_waiting_months": "Specific Disease Waiting Period (months)",
116
+ "maternity_waiting_months": "Maternity Waiting Period (months)",
117
+ "pre_hospitalization_days": "Pre-Hospitalization Coverage (days)",
118
+ "post_hospitalization_days": "Post-Hospitalization Coverage (days)",
119
+ "day_care_treatments_count": "Number of Day-Care Treatments Covered",
120
+ "ayush_coverage": "AYUSH Coverage",
121
+ "maternity_coverage": "Maternity Coverage",
122
+ "newborn_coverage": "Newborn Baby Coverage",
123
+ "organ_donor_expenses": "Organ Donor Expenses",
124
+ "no_claim_bonus_pct": "No Claim Bonus (%)",
125
+ "restoration_benefit": "Restoration / Reload Benefit",
126
+ "room_rent_capping": "Room Rent Capping",
127
+ "copayment_pct": "Co-Payment (%)",
128
+ "deductible_amount": "Deductible Amount",
129
+ "network_hospital_count": "Network Hospital Count",
130
+ "cashless_treatment_supported": "Cashless Treatment Supported",
131
+ "claim_settlement_ratio": "Claim Settlement Ratio (%)",
132
+ "tat_cashless_authorization_hours": "Cashless Authorization Turnaround Time (hours)",
133
+ "policy_type": "Policy Type",
134
+ }
135
+
136
+
137
+ def render_curated_to_text(curated: dict) -> str:
138
+ """Render a curated_facts dict into a flat text document the embedder
139
+ can ingest. Includes both value and source_quote so the LLM sees the
140
+ full evidence the curator used.
141
+ """
142
+ policy_name = curated.get("policy_name") or curated.get("policy_id", "")
143
+ insurer_slug = curated.get("insurer_slug", "")
144
+ out: list[str] = []
145
+ out.append(f"Policy Name: {policy_name}")
146
+ out.append(f"Insurer: {insurer_slug}")
147
+ out.append(f"Policy ID: {curated.get('policy_id', '')}")
148
+ out.append("")
149
+ out.append("Structured Policy Facts (curated from official wordings/CIS/brochure):")
150
+ out.append("")
151
+ for key, val in curated.items():
152
+ if key.startswith("_") or key in ("policy_id", "policy_name", "insurer_slug"):
153
+ continue
154
+ label = _HUMAN_LABEL.get(key, key.replace("_", " ").title())
155
+ if isinstance(val, dict):
156
+ value = val.get("value")
157
+ quote = val.get("source_quote") or ""
158
+ unit = val.get("unit", "")
159
+ if value is None or value == "":
160
+ # Skip null fields but keep the source_quote if it gives context
161
+ if quote:
162
+ out.append(f"- {label}: not specified. Source note: {quote}")
163
+ continue
164
+ display = f"{value} {unit}".strip() if unit else str(value)
165
+ line = f"- {label}: {display}."
166
+ if quote:
167
+ line += f" Source quote: \"{quote}\""
168
+ out.append(line)
169
+ else:
170
+ if val is None or val == "" or val == []:
171
+ continue
172
+ out.append(f"- {label}: {val}")
173
+
174
+ # Meta block β€” curation context + primary source PDF
175
+ meta = curated.get("_meta") or {}
176
+ if meta:
177
+ out.append("")
178
+ out.append("Curation metadata:")
179
+ if meta.get("curated_at"):
180
+ out.append(f"- Curated on: {meta['curated_at']}")
181
+ if meta.get("primary_source_pdf"):
182
+ out.append(f"- Primary source PDF: {meta['primary_source_pdf']}")
183
+ if meta.get("completeness_pct") is not None:
184
+ out.append(f"- Curation completeness: {meta['completeness_pct']}%")
185
+ if meta.get("notes"):
186
+ out.append(f"- Notes: {meta['notes']}")
187
+
188
+ return "\n".join(out)
189
+
190
+
191
+ # ---------- main pipeline ----------
192
+
193
+ async def main():
194
+ # 1. Identify missing curated policies
195
+ curated_all = _load_curated_facts()
196
+ # _load_curated_facts adds __wordings/__brochure/__cis duplicate keys;
197
+ # collapse to the actual policy_id from inside each entry.
198
+ base_curated: dict[str, dict] = {}
199
+ for _key, data in curated_all.items():
200
+ real_pid = data.get("policy_id")
201
+ if real_pid and real_pid not in base_curated:
202
+ base_curated[real_pid] = data
203
+ print(f"Distinct curated policy_ids: {len(base_curated)}")
204
+
205
+ coll = get_chroma_collection()
206
+ print(f"Chroma `policies` chunks before ingest: {coll.count()}")
207
+
208
+ existing = coll.get(include=["metadatas"])
209
+ chroma_pids = set(md.get("policy_id") for md in existing["metadatas"])
210
+ print(f"Chroma unique policy_ids before ingest: {len(chroma_pids)}")
211
+
212
+ # Missing = curated pid AND no chroma pid that exactly matches or matches
213
+ # `<curated_pid>__<docvariant>` (Chroma stores e.g. `...__wordings`).
214
+ missing: list[str] = []
215
+ for pid in sorted(base_curated):
216
+ if pid in chroma_pids:
217
+ continue
218
+ if any(c.startswith(pid + "__") for c in chroma_pids):
219
+ continue
220
+ missing.append(pid)
221
+ print(f"\nMissing curated policies: {len(missing)}")
222
+ for m in missing:
223
+ print(f" - {m}")
224
+
225
+ if not missing:
226
+ print("\nNothing to ingest. Exiting.")
227
+ return
228
+
229
+ # 2. Render + embed + add
230
+ url_index = _load_corpus_urls()
231
+ embedder = LocalEmbeddings()
232
+ print(f"\nEmbedder: {embedder.name} ({embedder.model_name}, dim={embedder.dimension}, device={embedder.device})")
233
+
234
+ total_chunks = 0
235
+ per_policy: list[tuple[str, int]] = []
236
+
237
+ for pid in missing:
238
+ data = base_curated[pid]
239
+ text = render_curated_to_text(data)
240
+ n_chars = len(text)
241
+
242
+ # Single chunk if under target, else slide window via rag.ingest.chunk_pages
243
+ # (chunk_pages expects [(page_no, text)]; we feed one virtual page).
244
+ if n_chars <= CHUNK_CHARS:
245
+ chunks = [{
246
+ "chunk_idx": 0,
247
+ "text": text,
248
+ "page_start": 0,
249
+ "page_end": 0,
250
+ "char_start": 0,
251
+ "char_end": n_chars,
252
+ }]
253
+ else:
254
+ chunks = list(chunk_pages(
255
+ pages=[(0, text)],
256
+ target_chars=CHUNK_CHARS,
257
+ overlap_chars=OVERLAP_CHARS,
258
+ ))
259
+
260
+ if not chunks:
261
+ print(f" WARN empty render for {pid}, skipping")
262
+ continue
263
+
264
+ texts = [c["text"] for c in chunks]
265
+ vectors = await embedder.embed(texts, input_type="document")
266
+
267
+ insurer_slug = data.get("insurer_slug") or pid.split("__", 1)[0]
268
+ policy_name = data.get("policy_name") or pid
269
+ source_url = _best_source_url(pid, data, url_index)
270
+
271
+ ids = [f"{pid}::curated::chunk{c['chunk_idx']}" for c in chunks]
272
+ metadatas = [
273
+ {
274
+ "policy_id": pid,
275
+ "insurer_slug": insurer_slug,
276
+ "policy_name": policy_name,
277
+ "doc_type": "curated",
278
+ "source_url": source_url,
279
+ "page_start": 0,
280
+ "page_end": 0,
281
+ "chunk_idx": c["chunk_idx"],
282
+ "local_path": f"curated-facts:{pid}",
283
+ }
284
+ for c in chunks
285
+ ]
286
+
287
+ coll.add(ids=ids, documents=texts, embeddings=vectors, metadatas=metadatas)
288
+
289
+ per_policy.append((pid, len(chunks)))
290
+ total_chunks += len(chunks)
291
+ print(f" + {pid}: {len(chunks)} chunk(s), {n_chars} chars, url={'yes' if source_url else 'no'}")
292
+
293
+ # 3. Verify by re-querying
294
+ print("\n--- Verification ---")
295
+ final_count = coll.count()
296
+ print(f"Chroma `policies` chunks after ingest: {final_count}")
297
+ for pid, n in per_policy:
298
+ got = coll.get(where={"policy_id": pid})
299
+ print(f" verify {pid}: chunks_now={len(got.get('ids') or [])}")
300
+
301
+ # 4. Sample retrieval test β€” embed a query and pull top-5
302
+ print("\n--- Sample retrieval test ---")
303
+ sample_query = "What are the waiting periods and AYUSH coverage for Aditya Birla Activ One?"
304
+ qv = await embedder.embed([sample_query], input_type="query")
305
+ res = coll.query(query_embeddings=qv, n_results=5)
306
+ for i, (doc_id, md, dist) in enumerate(zip(res["ids"][0], res["metadatas"][0], res["distances"][0])):
307
+ snippet = (res["documents"][0][i] or "")[:120].replace("\n", " ")
308
+ print(f" rank {i+1}: {md.get('policy_id')} (dist={dist:.4f}) doc_type={md.get('doc_type')}")
309
+ print(f" {snippet}...")
310
+
311
+ # 5. Summary
312
+ print("\n--- Summary ---")
313
+ print(f"Policies ingested: {len(per_policy)}")
314
+ print(f"Total chunks added: {total_chunks}")
315
+
316
+
317
+ if __name__ == "__main__":
318
+ asyncio.run(main())
tools/upload_extracted_to_dataset.py CHANGED
@@ -20,7 +20,7 @@ def main():
20
  path_in_repo="rag/extracted",
21
  repo_id="rohitsar567/insurance-bot-data",
22
  repo_type="dataset",
23
- commit_message="sync rag/extracted JSONs (post-NIM-extraction)",
24
  ignore_patterns=["*._raw.txt"],
25
  )
26
  n = len(list((ROOT / "rag" / "extracted").glob("*.json")))
 
20
  path_in_repo="rag/extracted",
21
  repo_id="rohitsar567/insurance-bot-data",
22
  repo_type="dataset",
23
+ commit_message="feat(extracted): KI-138 β€” canonicalize policy_name across 84 mismatches",
24
  ignore_patterns=["*._raw.txt"],
25
  )
26
  n = len(list((ROOT / "rag" / "extracted").glob("*.json")))