Spaces:
Sleeping
Sleeping
Deploy: Stack A (NIM brain + Maverick judge + Sarvam voice). D-019.
Browse files- 40-data/llm_health.json +129 -129
- Dockerfile +1 -1
- frontend/src/lib/useLiveConversation.ts +32 -11
- frontend/tsconfig.tsbuildinfo +0 -0
- tools/canonicalize_policy_names.py +212 -0
- tools/ingest_curated_into_chroma.py +318 -0
- tools/upload_extracted_to_dataset.py +1 -1
40-data/llm_health.json
CHANGED
|
@@ -1,22 +1,16 @@
|
|
| 1 |
{
|
| 2 |
-
"updated_at": "2026-05-15T02:
|
| 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:
|
| 8 |
"last_failure_at": null,
|
| 9 |
"last_error": null,
|
| 10 |
-
"latency_ms":
|
| 11 |
"consecutive_failures": 0,
|
| 12 |
-
"tested_at": "2026-05-15T02:
|
| 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:
|
| 57 |
"last_error": "timeout",
|
| 58 |
"latency_ms": null,
|
| 59 |
-
"consecutive_failures":
|
| 60 |
-
"tested_at": "2026-05-15T02:
|
| 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:
|
| 105 |
"last_error": "empty_content",
|
| 106 |
"latency_ms": null,
|
| 107 |
-
"consecutive_failures":
|
| 108 |
-
"tested_at": "2026-05-15T02:
|
| 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:
|
| 152 |
"last_failure_at": "2026-05-15T02:00:06Z",
|
| 153 |
"last_error": null,
|
| 154 |
-
"latency_ms":
|
| 155 |
"consecutive_failures": 0,
|
| 156 |
-
"tested_at": "2026-05-15T02:
|
| 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:
|
| 200 |
"last_failure_at": "2026-05-15T01:24:45Z",
|
| 201 |
"last_error": null,
|
| 202 |
-
"latency_ms":
|
| 203 |
"consecutive_failures": 0,
|
| 204 |
-
"tested_at": "2026-05-15T02:
|
| 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": "
|
| 247 |
"last_success_at": "2026-05-15T02:03:36Z",
|
| 248 |
-
"last_failure_at": "2026-05-
|
| 249 |
-
"last_error":
|
| 250 |
"latency_ms": 698,
|
| 251 |
-
"consecutive_failures":
|
| 252 |
-
"tested_at": "2026-05-15T02:
|
| 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:
|
| 297 |
"last_error": "timeout",
|
| 298 |
"latency_ms": null,
|
| 299 |
-
"consecutive_failures":
|
| 300 |
-
"tested_at": "2026-05-15T02:
|
| 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:
|
| 345 |
"last_error": "empty_content",
|
| 346 |
"latency_ms": null,
|
| 347 |
-
"consecutive_failures":
|
| 348 |
-
"tested_at": "2026-05-15T02:
|
| 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:
|
| 393 |
"last_error": "timeout",
|
| 394 |
"latency_ms": null,
|
| 395 |
-
"consecutive_failures":
|
| 396 |
-
"tested_at": "2026-05-15T02:
|
| 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:
|
| 441 |
"last_error": "http_404",
|
| 442 |
"latency_ms": null,
|
| 443 |
-
"consecutive_failures":
|
| 444 |
-
"tested_at": "2026-05-15T02:
|
| 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:
|
| 489 |
"last_error": "http_410",
|
| 490 |
"latency_ms": null,
|
| 491 |
-
"consecutive_failures":
|
| 492 |
-
"tested_at": "2026-05-15T02:
|
| 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:
|
| 536 |
"last_failure_at": "2026-05-14T01:03:07Z",
|
| 537 |
"last_error": null,
|
| 538 |
-
"latency_ms":
|
| 539 |
"consecutive_failures": 0,
|
| 540 |
-
"tested_at": "2026-05-15T02:
|
| 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:
|
| 585 |
"last_error": "empty_content",
|
| 586 |
"latency_ms": 1349,
|
| 587 |
-
"consecutive_failures":
|
| 588 |
-
"tested_at": "2026-05-15T02:
|
| 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:
|
| 632 |
"last_failure_at": null,
|
| 633 |
"last_error": null,
|
| 634 |
-
"latency_ms":
|
| 635 |
"consecutive_failures": 0,
|
| 636 |
-
"tested_at": "2026-05-15T02:
|
| 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-
|
| 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
|
| 91 |
-
//
|
| 92 |
-
//
|
| 93 |
-
|
|
|
|
|
|
|
| 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
|
| 123 |
-
// KI-
|
| 124 |
-
//
|
| 125 |
-
//
|
| 126 |
-
//
|
| 127 |
-
|
|
|
|
|
|
|
|
|
|
| 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 *
|
| 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="
|
| 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")))
|