Nagendravarma commited on
Commit Β·
fbd8eda
1
Parent(s): a74c657
Optimize dev console, add benchmark presets, filter knowledge graph, and fix specialist copay retrieval
Browse files- backend/main.py +45 -5
- frontend/dev_console.html +290 -47
- ingestion/graph_ingest.py +24 -1
- ingestion/ingest.py +10 -1
- orchestration/semantic_cache.py +4 -4
- orchestration/tools.py +23 -7
- retrieval/graph_retriever.py +27 -17
- retrieval/retriever.py +46 -2
- storage/chroma_db/chroma.sqlite3 +1 -1
- storage/knowledge_graph.graphml +0 -0
backend/main.py
CHANGED
|
@@ -233,7 +233,8 @@ async def chat_stream(session_id: str, query: str, plan_tier: str = "Unknown"):
|
|
| 233 |
final_query = f"I am on the {plan_tier} plan. {query}"
|
| 234 |
|
| 235 |
# Check Semantic Cache
|
| 236 |
-
|
|
|
|
| 237 |
if cached_result:
|
| 238 |
# Sync orchestrator history & trace
|
| 239 |
if len(orch.chat_history) == 0 or orch.chat_history[-1] != ("ai", cached_result["answer"]):
|
|
@@ -250,6 +251,20 @@ async def chat_stream(session_id: str, query: str, plan_tier: str = "Unknown"):
|
|
| 250 |
await asyncio.sleep(0.15)
|
| 251 |
yield emit({"type": "node_start", "node": "fastapi", "msg": "POST /chat/stream received"})
|
| 252 |
await asyncio.sleep(0.15)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
yield emit({"type": "node_start", "node": "semantic_cache", "msg": "Checking semantic cacheβ¦"})
|
| 254 |
await asyncio.sleep(0.2)
|
| 255 |
|
|
@@ -312,11 +327,36 @@ async def chat_stream(session_id: str, query: str, plan_tier: str = "Unknown"):
|
|
| 312 |
|
| 313 |
# ββ Startup handshake events ββββββββββββββββββββββββββββββ
|
| 314 |
yield emit({"type": "node_start", "node": "user", "msg": query})
|
| 315 |
-
await asyncio.sleep(0.
|
| 316 |
yield emit({"type": "node_start", "node": "fastapi", "msg": "POST /chat/stream received"})
|
| 317 |
-
await asyncio.sleep(0.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
yield emit({"type": "node_start", "node": "orchestrator", "msg": "Building LangGraph stateβ¦"})
|
| 319 |
-
await asyncio.sleep(0.
|
| 320 |
|
| 321 |
prev_steps: list[str] = []
|
| 322 |
final_answer = ""
|
|
@@ -396,7 +436,7 @@ async def chat_stream(session_id: str, query: str, plan_tier: str = "Unknown"):
|
|
| 396 |
"confidence_reason": last_state.get("confidence_reason", ""),
|
| 397 |
"sub_questions": last_state.get("sub_questions", []),
|
| 398 |
}
|
| 399 |
-
cache_manager.store(final_query, cache_payload, plan_tier=plan_tier)
|
| 400 |
|
| 401 |
except Exception as e:
|
| 402 |
logger.error(f"Error in chat_stream: {str(e)}")
|
|
|
|
| 233 |
final_query = f"I am on the {plan_tier} plan. {query}"
|
| 234 |
|
| 235 |
# Check Semantic Cache
|
| 236 |
+
norm_query = cache_manager.normalize_query(final_query)
|
| 237 |
+
cached_result = cache_manager.check(final_query, plan_tier=plan_tier, normalized_query=norm_query)
|
| 238 |
if cached_result:
|
| 239 |
# Sync orchestrator history & trace
|
| 240 |
if len(orch.chat_history) == 0 or orch.chat_history[-1] != ("ai", cached_result["answer"]):
|
|
|
|
| 251 |
await asyncio.sleep(0.15)
|
| 252 |
yield emit({"type": "node_start", "node": "fastapi", "msg": "POST /chat/stream received"})
|
| 253 |
await asyncio.sleep(0.15)
|
| 254 |
+
|
| 255 |
+
# Query Analyzer
|
| 256 |
+
yield emit({"type": "node_start", "node": "query_analyzer", "msg": "Analyzing search intent & phrasing..."})
|
| 257 |
+
await asyncio.sleep(0.2)
|
| 258 |
+
yield emit({
|
| 259 |
+
"type": "substep",
|
| 260 |
+
"node": "query_analyzer",
|
| 261 |
+
"step": f"Normalized query: '{final_query[:40]}...' -> '{norm_query}'"
|
| 262 |
+
})
|
| 263 |
+
await asyncio.sleep(0.2)
|
| 264 |
+
yield emit({"type": "node_done", "node": "query_analyzer", "normalized_query": norm_query})
|
| 265 |
+
await asyncio.sleep(0.15)
|
| 266 |
+
|
| 267 |
+
# Redis Cache Check
|
| 268 |
yield emit({"type": "node_start", "node": "semantic_cache", "msg": "Checking semantic cacheβ¦"})
|
| 269 |
await asyncio.sleep(0.2)
|
| 270 |
|
|
|
|
| 327 |
|
| 328 |
# ββ Startup handshake events ββββββββββββββββββββββββββββββ
|
| 329 |
yield emit({"type": "node_start", "node": "user", "msg": query})
|
| 330 |
+
await asyncio.sleep(0.15)
|
| 331 |
yield emit({"type": "node_start", "node": "fastapi", "msg": "POST /chat/stream received"})
|
| 332 |
+
await asyncio.sleep(0.15)
|
| 333 |
+
|
| 334 |
+
# Query Analyzer
|
| 335 |
+
yield emit({"type": "node_start", "node": "query_analyzer", "msg": "Analyzing search intent & phrasing..."})
|
| 336 |
+
await asyncio.sleep(0.2)
|
| 337 |
+
yield emit({
|
| 338 |
+
"type": "substep",
|
| 339 |
+
"node": "query_analyzer",
|
| 340 |
+
"step": f"Normalized query: '{final_query[:40]}...' -> '{norm_query}'"
|
| 341 |
+
})
|
| 342 |
+
await asyncio.sleep(0.2)
|
| 343 |
+
yield emit({"type": "node_done", "node": "query_analyzer", "normalized_query": norm_query})
|
| 344 |
+
await asyncio.sleep(0.15)
|
| 345 |
+
|
| 346 |
+
# Redis Cache Check
|
| 347 |
+
yield emit({"type": "node_start", "node": "semantic_cache", "msg": "Checking semantic cacheβ¦"})
|
| 348 |
+
await asyncio.sleep(0.2)
|
| 349 |
+
yield emit({
|
| 350 |
+
"type": "substep",
|
| 351 |
+
"node": "semantic_cache",
|
| 352 |
+
"step": "Cache MISS. No matching normalized query found."
|
| 353 |
+
})
|
| 354 |
+
await asyncio.sleep(0.15)
|
| 355 |
+
yield emit({"type": "node_done", "node": "semantic_cache", "msg": "Proceeding to full RAG pipeline"})
|
| 356 |
+
await asyncio.sleep(0.15)
|
| 357 |
+
|
| 358 |
yield emit({"type": "node_start", "node": "orchestrator", "msg": "Building LangGraph stateβ¦"})
|
| 359 |
+
await asyncio.sleep(0.2)
|
| 360 |
|
| 361 |
prev_steps: list[str] = []
|
| 362 |
final_answer = ""
|
|
|
|
| 436 |
"confidence_reason": last_state.get("confidence_reason", ""),
|
| 437 |
"sub_questions": last_state.get("sub_questions", []),
|
| 438 |
}
|
| 439 |
+
cache_manager.store(final_query, cache_payload, plan_tier=plan_tier, normalized_query=norm_query)
|
| 440 |
|
| 441 |
except Exception as e:
|
| 442 |
logger.error(f"Error in chat_stream: {str(e)}")
|
frontend/dev_console.html
CHANGED
|
@@ -157,6 +157,11 @@ body {
|
|
| 157 |
}
|
| 158 |
.preset-item:hover { background: rgba(99,102,241,0.1); border-color: rgba(99,102,241,0.3); color: #a5b4fc; }
|
| 159 |
.preset-icon { flex-shrink: 0; margin-top: 1px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
|
| 161 |
.run-btn {
|
| 162 |
width: 100%; margin-top: 10px; padding: 11px; border: none; border-radius: 9px;
|
|
@@ -351,6 +356,8 @@ svg { width:100%; height:100%; overflow:visible; }
|
|
| 351 |
.n.active rect { fill:rgba(124,58,237,0.25); stroke:#a78bfa; stroke-width:2; filter:drop-shadow(0 0 10px rgba(139,92,246,0.5)); }
|
| 352 |
.n.done rect { fill:rgba(16,185,129,0.13); stroke:#10b981; stroke-width:1.8; }
|
| 353 |
.n.skip rect { fill:#050811; stroke:rgba(255,255,255,0.03); opacity:0.3; }
|
|
|
|
|
|
|
| 354 |
.n .icon { font-size:14px; }
|
| 355 |
.n .title { font-family:'Plus Jakarta Sans',sans-serif; font-size:10px; font-weight:700; fill:#e2e8f0; }
|
| 356 |
.n .sub { font-family:'Plus Jakarta Sans',sans-serif; font-size:8px; fill:#64748b; font-weight:500; }
|
|
@@ -613,24 +620,27 @@ svg { width:100%; height:100%; overflow:visible; }
|
|
| 613 |
π Load demo query <span id="preset-arrow">βΎ</span>
|
| 614 |
</button>
|
| 615 |
<div class="preset-list" id="preset-list">
|
| 616 |
-
<div class="preset-
|
| 617 |
-
<div class="preset-item" onclick="loadPreset(
|
| 618 |
-
<div class="preset-item" onclick="loadPreset(
|
| 619 |
-
<div class="preset-item" onclick="loadPreset(
|
| 620 |
-
<div class="preset-item" onclick="loadPreset(
|
| 621 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 622 |
</div>
|
| 623 |
</div>
|
| 624 |
|
| 625 |
<button class="run-btn" id="runbtn" onclick="run()">π Execute Live Trace</button>
|
| 626 |
</div>
|
| 627 |
|
| 628 |
-
<!-- Intent
|
| 629 |
<div class="meta-row" id="meta-row">
|
| 630 |
<span class="intent-label">Intent</span>
|
| 631 |
<div class="intent-pill" id="intent-pill">β</div>
|
| 632 |
-
<span class="conf-label" style="margin-left:8px">Confidence</span>
|
| 633 |
-
<div class="conf-badge" id="conf-badge">β</div>
|
| 634 |
</div>
|
| 635 |
|
| 636 |
<!-- Memory inspector -->
|
|
@@ -651,8 +661,6 @@ svg { width:100%; height:100%; overflow:visible; }
|
|
| 651 |
<label>β‘ System Event Stream</label>
|
| 652 |
<div class="log-controls">
|
| 653 |
<input class="log-search" id="log-search" placeholder="filterβ¦" oninput="filterLogs()">
|
| 654 |
-
<button class="filter-btn ALL active" id="fb-ALL" onclick="setFilter('ALL')">ALL</button>
|
| 655 |
-
<button class="filter-btn CLASSIFY" id="fb-CLASSIFY" onclick="setFilter('CLASSIFY')">CLS</button>
|
| 656 |
<button class="filter-btn RETRIEVE" id="fb-RETRIEVE" onclick="setFilter('RETRIEVE')">RET</button>
|
| 657 |
<button class="filter-btn SYNTH" id="fb-SYNTH" onclick="setFilter('SYNTH')">SYN</button>
|
| 658 |
<button class="filter-btn ERROR" id="fb-ERROR" onclick="setFilter('ERROR')">ERR</button>
|
|
@@ -734,12 +742,14 @@ document.getElementById('pass-input').addEventListener('keydown', e => { if (e.k
|
|
| 734 |
|
| 735 |
// ββ PRESET QUERIES βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 736 |
const PRESETS = [
|
| 737 |
-
'
|
| 738 |
-
'
|
| 739 |
-
'
|
| 740 |
-
'
|
| 741 |
-
'
|
| 742 |
-
'
|
|
|
|
|
|
|
| 743 |
];
|
| 744 |
function loadPreset(i) {
|
| 745 |
document.getElementById('qinput').value = PRESETS[i];
|
|
@@ -818,7 +828,8 @@ let activeFilter = 'ALL';
|
|
| 818 |
function setFilter(f) {
|
| 819 |
activeFilter = f;
|
| 820 |
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
| 821 |
-
document.getElementById('fb-' + (f === 'SYNTH' ? 'SYNTH' : f))
|
|
|
|
| 822 |
filterLogs();
|
| 823 |
}
|
| 824 |
function filterLogs() {
|
|
@@ -848,6 +859,9 @@ function copyAnswer() {
|
|
| 848 |
|
| 849 |
// ββ DOWNLOAD TRACE ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 850 |
let traceStore = [];
|
|
|
|
|
|
|
|
|
|
| 851 |
function downloadTrace() {
|
| 852 |
if (!traceStore.length) { alert('Run a query first to generate a trace.'); return; }
|
| 853 |
const blob = new Blob([JSON.stringify(traceStore, null, 2)], { type: 'application/json' });
|
|
@@ -895,7 +909,8 @@ const NODES = [
|
|
| 895 |
{id:'confidence', icon:'π', title:'Confidence Scorer', sub:'HIGH/MED/LOW rating', x:920, y:670}, // NEW
|
| 896 |
{id:'mem_add', icon:'πΎ', title:'Memory Commit', sub:'Mem0 fact extractor', x:1100, y:670},
|
| 897 |
{id:'answer', icon:'π¬', title:'Response Output', sub:'Client socket stream', x:1300, y:670},
|
| 898 |
-
{id:'
|
|
|
|
| 899 |
];
|
| 900 |
|
| 901 |
const INTENT_PILLS = [
|
|
@@ -906,7 +921,7 @@ const INTENT_PILLS = [
|
|
| 906 |
];
|
| 907 |
|
| 908 |
const EDGES = [
|
| 909 |
-
['user','fastapi'],['
|
| 910 |
['query_guard','mem_search'],['mem_search','intent_node'],
|
| 911 |
['orchestrator','langsmith'],
|
| 912 |
['intent_node','query_decomposer'],['query_decomposer','retrieve_agent'],
|
|
@@ -921,7 +936,7 @@ const EDGES = [
|
|
| 921 |
['merger','synthesis'],['synthesis','langsmith'],
|
| 922 |
['synthesis','self_critique'],
|
| 923 |
['self_critique','confidence'],['confidence','mem_add'],['mem_add','answer'],
|
| 924 |
-
['fastapi','redis'],['redis','answer'],
|
| 925 |
];
|
| 926 |
|
| 927 |
// Self-critique retry edge (back to retrieve β special styling)
|
|
@@ -971,6 +986,7 @@ const STEP_MAP = [
|
|
| 971 |
];
|
| 972 |
|
| 973 |
const LG_START = {
|
|
|
|
| 974 |
semantic_cache: ['redis'],
|
| 975 |
query_guard: ['query_guard'],
|
| 976 |
memory_search: ['mem_search'],
|
|
@@ -1080,6 +1096,7 @@ const NODE_DESC = {
|
|
| 1080 |
langsmith: 'LangSmith observability: records full traces with latency, token usage, and intermediate states.',
|
| 1081 |
mem_add: 'Mem0 fact extractor: persists key Q&A facts to session memory for future context personalization.',
|
| 1082 |
answer: 'Final answer streamed back to the client via Server-Sent Events (SSE).',
|
|
|
|
| 1083 |
redis: 'Cognitive Semantic Vector Cache: checks incoming queries against previously answered queries using GPT-4o-mini normalization and vector embeddings to bypass the orchestrator and LLM on cache hits.',
|
| 1084 |
};
|
| 1085 |
|
|
@@ -1177,14 +1194,21 @@ function clearAll() {
|
|
| 1177 |
document.getElementById('answer-card').style.display = 'none';
|
| 1178 |
document.getElementById('answer-text').innerHTML = '';
|
| 1179 |
document.getElementById('meta-row').classList.remove('visible');
|
| 1180 |
-
document.getElementById('conf-badge')
|
| 1181 |
-
|
| 1182 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1183 |
Object.keys(nodeTimers).forEach(k => delete nodeTimers[k]);
|
| 1184 |
Object.keys(nodeLatencies).forEach(k => delete nodeLatencies[k]);
|
| 1185 |
multiQueryVariants = [];
|
| 1186 |
nodeData = {};
|
| 1187 |
traceStore = [];
|
|
|
|
| 1188 |
}
|
| 1189 |
|
| 1190 |
function addLog(icon, tag, tagClass, text) {
|
|
@@ -1209,6 +1233,7 @@ function activateFromStep(step, lgNode) {
|
|
| 1209 |
for (const m of STEP_MAP) {
|
| 1210 |
if (m.pat.test(step)) {
|
| 1211 |
m.nodes.forEach(id => {
|
|
|
|
| 1212 |
setNode(id, 'active');
|
| 1213 |
const fromNode = lgNode === 'retrieve' ? 'retrieve_agent' : lgNode;
|
| 1214 |
activateEdge(fromNode, id);
|
|
@@ -1263,8 +1288,9 @@ async function run() {
|
|
| 1263 |
let elapsedTimer = setInterval(() => updateElapsed(performance.now()-startMs), 500);
|
| 1264 |
|
| 1265 |
let activeNodeIds = [];
|
| 1266 |
-
|
| 1267 |
-
|
|
|
|
| 1268 |
|
| 1269 |
try {
|
| 1270 |
const url = `${API}/chat/stream?session_id=${encodeURIComponent(SID)}&query=${encodeURIComponent(q)}`;
|
|
@@ -1290,6 +1316,11 @@ async function run() {
|
|
| 1290 |
const totalMs = Math.round(performance.now() - startMs);
|
| 1291 |
updateElapsed(totalMs);
|
| 1292 |
setStatus(`β
Pipeline completed in ${(totalMs/1000).toFixed(2)}s`, 'done');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1293 |
break;
|
| 1294 |
}
|
| 1295 |
try {
|
|
@@ -1311,6 +1342,9 @@ async function run() {
|
|
| 1311 |
|
| 1312 |
function handleEvent(ev) {
|
| 1313 |
const { type, node, intent, step, msg, answer, confidence, confidence_reason, blocked, sub_questions } = ev;
|
|
|
|
|
|
|
|
|
|
| 1314 |
|
| 1315 |
if (intent && intent !== currentIntent) { currentIntent = intent; setIntent(intent); document.getElementById('meta-row').classList.add('visible'); }
|
| 1316 |
if (confidence) setConfidence(confidence, confidence_reason);
|
|
@@ -1323,8 +1357,14 @@ async function run() {
|
|
| 1323 |
activeNodeIds.forEach(id => setNode(id, 'done'));
|
| 1324 |
|
| 1325 |
if (node === 'user') { setNode('user','active'); activeNodeIds=['user']; activateEdge('user','fastapi'); }
|
| 1326 |
-
else if (node === 'fastapi')
|
| 1327 |
-
else if (node === '
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1328 |
else {
|
| 1329 |
diagramNodes.forEach(id => { setNode(id,'active'); });
|
| 1330 |
activeNodeIds = diagramNodes;
|
|
@@ -1427,6 +1467,28 @@ async function run() {
|
|
| 1427 |
doneEdge('self_critique','confidence');
|
| 1428 |
}
|
| 1429 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1430 |
if (node === 'memory_add') {
|
| 1431 |
doneEdge('synthesis','self_critique'); doneEdge('self_critique','confidence');
|
| 1432 |
doneEdge('confidence','mem_add'); doneEdge('mem_add','answer');
|
|
@@ -1516,10 +1578,10 @@ function collectNodeData(step, node, ev) {
|
|
| 1516 |
if (ev && ev.run_id && !nodeData.langsmith) {
|
| 1517 |
nodeData.langsmith = { run_id: ev.run_id };
|
| 1518 |
}
|
| 1519 |
-
// Retrieved chunks for Vector and
|
| 1520 |
-
const chunkMatch = step.match(/\[(VectorDB|BM25)\] Retrieved: (.+?)(?:\|([-\.\d]+)\|(.+))?$/i);
|
| 1521 |
if (chunkMatch) {
|
| 1522 |
-
const nodeKey = chunkMatch[1].
|
| 1523 |
nodeData[nodeKey] = nodeData[nodeKey] || { retrieved_chunks: [] };
|
| 1524 |
if (chunkMatch[3] !== undefined && chunkMatch[4] !== undefined) {
|
| 1525 |
nodeData[nodeKey].retrieved_chunks.push({
|
|
@@ -1567,6 +1629,10 @@ function collectNodeData(step, node, ev) {
|
|
| 1567 |
nodeData.redis.similarity = m ? parseFloat(m[1]) : 0;
|
| 1568 |
nodeData.redis.hit = true;
|
| 1569 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1570 |
}
|
| 1571 |
|
| 1572 |
// ββ NODE DETAIL MODAL ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -1599,14 +1665,31 @@ function openNodeModal(nodeId, icon, title) {
|
|
| 1599 |
|
| 1600 |
else if (nodeId === 'query_decomposer') {
|
| 1601 |
const sqs = data.sub_questions || [];
|
| 1602 |
-
|
| 1603 |
-
|
| 1604 |
-
|
| 1605 |
-
|
| 1606 |
-
|
| 1607 |
-
if (sqs.length) {
|
| 1608 |
-
body += `
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1609 |
}
|
|
|
|
| 1610 |
}
|
| 1611 |
|
| 1612 |
else if (nodeId === 'query_guard') {
|
|
@@ -1799,27 +1882,187 @@ function openNodeModal(nodeId, icon, title) {
|
|
| 1799 |
body += `</div>`;
|
| 1800 |
}
|
| 1801 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1802 |
else if (nodeId === 'graphdb') {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1803 |
body += `<div class="nm-section"><div class="nm-section-title">πΈοΈ Knowledge Graph Lookups</div>`;
|
| 1804 |
if (data.entities && data.entities.length) {
|
| 1805 |
-
body += `<div class="nm-stat-row"><div class="nm-stat">Entities resolved: <span>${data.entities.length}</span></div></div>`;
|
| 1806 |
|
| 1807 |
-
if (
|
| 1808 |
body += `<div class="nm-section-title" style="margin-top:10px; display:flex; justify-content:space-between; align-items:center;">
|
| 1809 |
-
<span>π Local Graph Explorer</span>
|
| 1810 |
<button onclick="toggleGraphFullscreen()" style="background:none;border:none;color:#38bdf8;cursor:pointer;font-size:0.8rem;padding:0;">βΆ Fullscreen</button>
|
| 1811 |
</div>`;
|
| 1812 |
body += `<svg id="nm-graph-viz" width="100%" height="250" style="background:#1e293b; border-radius:8px; margin-top:5px; border:1px solid #334155; transition: height 0.3s ease;"></svg>`;
|
| 1813 |
}
|
| 1814 |
|
| 1815 |
-
body += `<div class="nm-section-title" style="margin-top:10px">π Fetched
|
| 1816 |
-
body +=
|
| 1817 |
} else if (data.entities && data.entities.length === 0) {
|
| 1818 |
body += `<div class="nm-card info">No matching entities found in the knowledge graph.</div>`;
|
| 1819 |
} else {
|
| 1820 |
body += noData;
|
| 1821 |
}
|
| 1822 |
body += `</div>`;
|
|
|
|
|
|
|
|
|
|
| 1823 |
}
|
| 1824 |
|
| 1825 |
else {
|
|
@@ -1832,7 +2075,7 @@ function openNodeModal(nodeId, icon, title) {
|
|
| 1832 |
document.getElementById('nm-body').innerHTML = body;
|
| 1833 |
document.getElementById('node-modal-backdrop').classList.add('open');
|
| 1834 |
|
| 1835 |
-
if (nodeId === 'graphdb' &&
|
| 1836 |
setTimeout(() => {
|
| 1837 |
const container = document.getElementById('nm-graph-viz');
|
| 1838 |
if (!container) return;
|
|
@@ -1842,12 +2085,12 @@ function openNodeModal(nodeId, icon, title) {
|
|
| 1842 |
svg.selectAll('*').remove();
|
| 1843 |
|
| 1844 |
const nodesMap = {};
|
| 1845 |
-
|
| 1846 |
nodesMap[e.source] = { id: e.source };
|
| 1847 |
nodesMap[e.target] = { id: e.target };
|
| 1848 |
});
|
| 1849 |
const nodes = Object.values(nodesMap);
|
| 1850 |
-
const links =
|
| 1851 |
|
| 1852 |
const simulation = d3.forceSimulation(nodes)
|
| 1853 |
.force("link", d3.forceLink(links).id(d => d.id).distance(120))
|
|
@@ -1932,7 +2175,7 @@ function openNodeModal(nodeId, icon, title) {
|
|
| 1932 |
}, 100);
|
| 1933 |
}
|
| 1934 |
|
| 1935 |
-
if ((nodeId === 'vectordb' || nodeId === 'bm25') && data.retrieved_chunks && data.retrieved_chunks.length > 0) {
|
| 1936 |
setTimeout(() => {
|
| 1937 |
renderScoreGraph(data.retrieved_chunks);
|
| 1938 |
}, 100);
|
|
|
|
| 157 |
}
|
| 158 |
.preset-item:hover { background: rgba(99,102,241,0.1); border-color: rgba(99,102,241,0.3); color: #a5b4fc; }
|
| 159 |
.preset-icon { flex-shrink: 0; margin-top: 1px; }
|
| 160 |
+
.preset-header {
|
| 161 |
+
font-size: 0.68rem; font-weight: 700; text-transform: uppercase; color: #64748b;
|
| 162 |
+
margin-top: 10px; margin-bottom: 2px; padding-left: 4px; letter-spacing: 0.05em;
|
| 163 |
+
}
|
| 164 |
+
.preset-header:first-child { margin-top: 2px; }
|
| 165 |
|
| 166 |
.run-btn {
|
| 167 |
width: 100%; margin-top: 10px; padding: 11px; border: none; border-radius: 9px;
|
|
|
|
| 356 |
.n.active rect { fill:rgba(124,58,237,0.25); stroke:#a78bfa; stroke-width:2; filter:drop-shadow(0 0 10px rgba(139,92,246,0.5)); }
|
| 357 |
.n.done rect { fill:rgba(16,185,129,0.13); stroke:#10b981; stroke-width:1.8; }
|
| 358 |
.n.skip rect { fill:#050811; stroke:rgba(255,255,255,0.03); opacity:0.3; }
|
| 359 |
+
.n.hit rect { fill:rgba(16,185,129,0.22) !important; stroke:#10b981 !important; stroke-width:2.5px !important; filter:drop-shadow(0 0 10px rgba(16,185,129,0.6)) !important; }
|
| 360 |
+
.n.miss rect { fill:rgba(249,115,22,0.18) !important; stroke:#f97316 !important; stroke-width:2.2px !important; filter:drop-shadow(0 0 8px rgba(249,115,22,0.4)) !important; }
|
| 361 |
.n .icon { font-size:14px; }
|
| 362 |
.n .title { font-family:'Plus Jakarta Sans',sans-serif; font-size:10px; font-weight:700; fill:#e2e8f0; }
|
| 363 |
.n .sub { font-family:'Plus Jakarta Sans',sans-serif; font-size:8px; fill:#64748b; font-weight:500; }
|
|
|
|
| 620 |
π Load demo query <span id="preset-arrow">βΎ</span>
|
| 621 |
</button>
|
| 622 |
<div class="preset-list" id="preset-list">
|
| 623 |
+
<div class="preset-header">π RAG Benchmark Queries</div>
|
| 624 |
+
<div class="preset-item" onclick="loadPreset(0)"><span class="preset-icon">π’</span>Easy: What is a deductible?</div>
|
| 625 |
+
<div class="preset-item" onclick="loadPreset(1)"><span class="preset-icon">π‘</span>Medium: What is the copay for Metformin on the Silver plan?</div>
|
| 626 |
+
<div class="preset-item" onclick="loadPreset(2)"><span class="preset-icon">π </span>Hard: Compare the specialist copays and out-of-pocket maximums between the Silver and Gold plans.</div>
|
| 627 |
+
<div class="preset-item" onclick="loadPreset(3)"><span class="preset-icon">π΄</span>Very Hard: Compare the overall deductibles, specialist copays, and drug formulary tier copays between the Bronze, Silver, and Gold plans.</div>
|
| 628 |
+
|
| 629 |
+
<div class="preset-header">β‘ Redis Semantic Cache Testing (Run Base, then Variation)</div>
|
| 630 |
+
<div class="preset-item" onclick="loadPreset(4)"><span class="preset-icon">π</span>Simple Base: Is Metformin covered on the Bronze plan?</div>
|
| 631 |
+
<div class="preset-item" onclick="loadPreset(5)"><span class="preset-icon">π</span>Simple Variation: Does the Bronze plan cover Metformin?</div>
|
| 632 |
+
<div class="preset-item" onclick="loadPreset(6)"><span class="preset-icon">π</span>Complex Base: What is the policy regarding waiting periods for chronic, pre-existing conditions before the plan starts paying for specialist visits and maintenance medications?</div>
|
| 633 |
+
<div class="preset-item" onclick="loadPreset(7)"><span class="preset-icon">π</span>Complex Variation: I was diagnosed with asthma last year and just signed up for this insurance. Do I have to wait a certain number of months before you guys will cover my inhalers and pulmonologist appointments, or am I good to go right now?</div>
|
| 634 |
</div>
|
| 635 |
</div>
|
| 636 |
|
| 637 |
<button class="run-btn" id="runbtn" onclick="run()">π Execute Live Trace</button>
|
| 638 |
</div>
|
| 639 |
|
| 640 |
+
<!-- Intent row -->
|
| 641 |
<div class="meta-row" id="meta-row">
|
| 642 |
<span class="intent-label">Intent</span>
|
| 643 |
<div class="intent-pill" id="intent-pill">β</div>
|
|
|
|
|
|
|
| 644 |
</div>
|
| 645 |
|
| 646 |
<!-- Memory inspector -->
|
|
|
|
| 661 |
<label>β‘ System Event Stream</label>
|
| 662 |
<div class="log-controls">
|
| 663 |
<input class="log-search" id="log-search" placeholder="filterβ¦" oninput="filterLogs()">
|
|
|
|
|
|
|
| 664 |
<button class="filter-btn RETRIEVE" id="fb-RETRIEVE" onclick="setFilter('RETRIEVE')">RET</button>
|
| 665 |
<button class="filter-btn SYNTH" id="fb-SYNTH" onclick="setFilter('SYNTH')">SYN</button>
|
| 666 |
<button class="filter-btn ERROR" id="fb-ERROR" onclick="setFilter('ERROR')">ERR</button>
|
|
|
|
| 742 |
|
| 743 |
// ββ PRESET QUERIES βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 744 |
const PRESETS = [
|
| 745 |
+
'What is a deductible?',
|
| 746 |
+
'What is the copay for Metformin on the Silver plan?',
|
| 747 |
+
'Compare the specialist copays and out-of-pocket maximums between the Silver and Gold plans.',
|
| 748 |
+
'Compare the overall deductibles, specialist copays, and drug formulary tier copays between the Bronze, Silver, and Gold plans.',
|
| 749 |
+
'Is Metformin covered on the Bronze plan?',
|
| 750 |
+
'Does the Bronze plan cover Metformin?',
|
| 751 |
+
'What is the policy regarding waiting periods for chronic, pre-existing conditions before the plan starts paying for specialist visits and maintenance medications?',
|
| 752 |
+
'I was diagnosed with asthma last year and just signed up for this insurance. Do I have to wait a certain number of months before you guys will cover my inhalers and pulmonologist appointments, or am I good to go right now?',
|
| 753 |
];
|
| 754 |
function loadPreset(i) {
|
| 755 |
document.getElementById('qinput').value = PRESETS[i];
|
|
|
|
| 828 |
function setFilter(f) {
|
| 829 |
activeFilter = f;
|
| 830 |
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
| 831 |
+
const btn = document.getElementById('fb-' + (f === 'SYNTH' ? 'SYNTH' : f));
|
| 832 |
+
if (btn) btn.classList.add('active');
|
| 833 |
filterLogs();
|
| 834 |
}
|
| 835 |
function filterLogs() {
|
|
|
|
| 859 |
|
| 860 |
// ββ DOWNLOAD TRACE ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 861 |
let traceStore = [];
|
| 862 |
+
let currentIntent = '';
|
| 863 |
+
let currentConfidence = '';
|
| 864 |
+
let finalResponseText = '';
|
| 865 |
function downloadTrace() {
|
| 866 |
if (!traceStore.length) { alert('Run a query first to generate a trace.'); return; }
|
| 867 |
const blob = new Blob([JSON.stringify(traceStore, null, 2)], { type: 'application/json' });
|
|
|
|
| 909 |
{id:'confidence', icon:'π', title:'Confidence Scorer', sub:'HIGH/MED/LOW rating', x:920, y:670}, // NEW
|
| 910 |
{id:'mem_add', icon:'πΎ', title:'Memory Commit', sub:'Mem0 fact extractor', x:1100, y:670},
|
| 911 |
{id:'answer', icon:'π¬', title:'Response Output', sub:'Client socket stream', x:1300, y:670},
|
| 912 |
+
{id:'query_analyzer', icon:'π', title:'Query Analyzer', sub:'phrasing normalizer', x:200, y:150},
|
| 913 |
+
{id:'redis', icon:'π΄', title:'Redis Cache', sub:'Semantic Vector Cache', x:380, y:150},
|
| 914 |
];
|
| 915 |
|
| 916 |
const INTENT_PILLS = [
|
|
|
|
| 921 |
];
|
| 922 |
|
| 923 |
const EDGES = [
|
| 924 |
+
['user','fastapi'],['redis','orchestrator'],['orchestrator','query_guard'],
|
| 925 |
['query_guard','mem_search'],['mem_search','intent_node'],
|
| 926 |
['orchestrator','langsmith'],
|
| 927 |
['intent_node','query_decomposer'],['query_decomposer','retrieve_agent'],
|
|
|
|
| 936 |
['merger','synthesis'],['synthesis','langsmith'],
|
| 937 |
['synthesis','self_critique'],
|
| 938 |
['self_critique','confidence'],['confidence','mem_add'],['mem_add','answer'],
|
| 939 |
+
['fastapi','query_analyzer'],['query_analyzer','redis'],['redis','answer'],
|
| 940 |
];
|
| 941 |
|
| 942 |
// Self-critique retry edge (back to retrieve β special styling)
|
|
|
|
| 986 |
];
|
| 987 |
|
| 988 |
const LG_START = {
|
| 989 |
+
query_analyzer: ['query_analyzer'],
|
| 990 |
semantic_cache: ['redis'],
|
| 991 |
query_guard: ['query_guard'],
|
| 992 |
memory_search: ['mem_search'],
|
|
|
|
| 1096 |
langsmith: 'LangSmith observability: records full traces with latency, token usage, and intermediate states.',
|
| 1097 |
mem_add: 'Mem0 fact extractor: persists key Q&A facts to session memory for future context personalization.',
|
| 1098 |
answer: 'Final answer streamed back to the client via Server-Sent Events (SSE).',
|
| 1099 |
+
query_analyzer: 'Translates conversational user queries into standardized third-person insurance policy search terms using GPT-4o-mini.',
|
| 1100 |
redis: 'Cognitive Semantic Vector Cache: checks incoming queries against previously answered queries using GPT-4o-mini normalization and vector embeddings to bypass the orchestrator and LLM on cache hits.',
|
| 1101 |
};
|
| 1102 |
|
|
|
|
| 1194 |
document.getElementById('answer-card').style.display = 'none';
|
| 1195 |
document.getElementById('answer-text').innerHTML = '';
|
| 1196 |
document.getElementById('meta-row').classList.remove('visible');
|
| 1197 |
+
const confBadge = document.getElementById('conf-badge');
|
| 1198 |
+
if (confBadge) {
|
| 1199 |
+
confBadge.className = 'conf-badge';
|
| 1200 |
+
confBadge.textContent = 'β';
|
| 1201 |
+
}
|
| 1202 |
+
const intentPill = document.getElementById('intent-pill');
|
| 1203 |
+
if (intentPill) {
|
| 1204 |
+
intentPill.textContent = 'β';
|
| 1205 |
+
}
|
| 1206 |
Object.keys(nodeTimers).forEach(k => delete nodeTimers[k]);
|
| 1207 |
Object.keys(nodeLatencies).forEach(k => delete nodeLatencies[k]);
|
| 1208 |
multiQueryVariants = [];
|
| 1209 |
nodeData = {};
|
| 1210 |
traceStore = [];
|
| 1211 |
+
setFilter('ALL');
|
| 1212 |
}
|
| 1213 |
|
| 1214 |
function addLog(icon, tag, tagClass, text) {
|
|
|
|
| 1233 |
for (const m of STEP_MAP) {
|
| 1234 |
if (m.pat.test(step)) {
|
| 1235 |
m.nodes.forEach(id => {
|
| 1236 |
+
if (id === 'graphdb' && currentIntent === 'POLICY_QUESTION') return;
|
| 1237 |
setNode(id, 'active');
|
| 1238 |
const fromNode = lgNode === 'retrieve' ? 'retrieve_agent' : lgNode;
|
| 1239 |
activateEdge(fromNode, id);
|
|
|
|
| 1288 |
let elapsedTimer = setInterval(() => updateElapsed(performance.now()-startMs), 500);
|
| 1289 |
|
| 1290 |
let activeNodeIds = [];
|
| 1291 |
+
currentIntent = '';
|
| 1292 |
+
currentConfidence = '';
|
| 1293 |
+
finalResponseText = '';
|
| 1294 |
|
| 1295 |
try {
|
| 1296 |
const url = `${API}/chat/stream?session_id=${encodeURIComponent(SID)}&query=${encodeURIComponent(q)}`;
|
|
|
|
| 1316 |
const totalMs = Math.round(performance.now() - startMs);
|
| 1317 |
updateElapsed(totalMs);
|
| 1318 |
setStatus(`β
Pipeline completed in ${(totalMs/1000).toFixed(2)}s`, 'done');
|
| 1319 |
+
const ansEl = document.getElementById('answer-text');
|
| 1320 |
+
if (finalResponseText && (!ansEl.innerHTML || ansEl.innerHTML.trim() === '')) {
|
| 1321 |
+
document.getElementById('answer-card').style.display = 'block';
|
| 1322 |
+
typewriterMarkdown(finalResponseText, ansEl);
|
| 1323 |
+
}
|
| 1324 |
break;
|
| 1325 |
}
|
| 1326 |
try {
|
|
|
|
| 1342 |
|
| 1343 |
function handleEvent(ev) {
|
| 1344 |
const { type, node, intent, step, msg, answer, confidence, confidence_reason, blocked, sub_questions } = ev;
|
| 1345 |
+
if (answer) {
|
| 1346 |
+
finalResponseText = answer;
|
| 1347 |
+
}
|
| 1348 |
|
| 1349 |
if (intent && intent !== currentIntent) { currentIntent = intent; setIntent(intent); document.getElementById('meta-row').classList.add('visible'); }
|
| 1350 |
if (confidence) setConfidence(confidence, confidence_reason);
|
|
|
|
| 1357 |
activeNodeIds.forEach(id => setNode(id, 'done'));
|
| 1358 |
|
| 1359 |
if (node === 'user') { setNode('user','active'); activeNodeIds=['user']; activateEdge('user','fastapi'); }
|
| 1360 |
+
else if (node === 'fastapi') { setNode('fastapi','active'); activeNodeIds=['fastapi']; activateEdge('fastapi','query_analyzer'); }
|
| 1361 |
+
else if (node === 'query_analyzer'){ setNode('query_analyzer','active'); activeNodeIds=['query_analyzer']; activateEdge('fastapi','query_analyzer'); }
|
| 1362 |
+
else if (node === 'semantic_cache') {
|
| 1363 |
+
setNode('redis','active');
|
| 1364 |
+
activeNodeIds=['redis'];
|
| 1365 |
+
activateEdge('query_analyzer','redis');
|
| 1366 |
+
}
|
| 1367 |
+
else if (node === 'orchestrator') { setNode('orchestrator','active'); activeNodeIds=['orchestrator']; activateEdge('redis','orchestrator'); }
|
| 1368 |
else {
|
| 1369 |
diagramNodes.forEach(id => { setNode(id,'active'); });
|
| 1370 |
activeNodeIds = diagramNodes;
|
|
|
|
| 1467 |
doneEdge('self_critique','confidence');
|
| 1468 |
}
|
| 1469 |
|
| 1470 |
+
if (node === 'query_analyzer') {
|
| 1471 |
+
doneEdge('fastapi', 'query_analyzer');
|
| 1472 |
+
setNode('query_analyzer', 'done');
|
| 1473 |
+
nodeData.query_analyzer = nodeData.query_analyzer || {};
|
| 1474 |
+
nodeData.query_analyzer.normalized_query = ev.normalized_query || '';
|
| 1475 |
+
}
|
| 1476 |
+
|
| 1477 |
+
if (node === 'semantic_cache') {
|
| 1478 |
+
doneEdge('query_analyzer', 'redis');
|
| 1479 |
+
if (answer) {
|
| 1480 |
+
setNode('redis', 'hit');
|
| 1481 |
+
doneEdge('redis', 'answer');
|
| 1482 |
+
setNode('answer', 'active');
|
| 1483 |
+
const ansEl = document.getElementById('answer-text');
|
| 1484 |
+
document.getElementById('answer-card').style.display = 'block';
|
| 1485 |
+
typewriterMarkdown(answer, ansEl);
|
| 1486 |
+
} else {
|
| 1487 |
+
setNode('redis', 'miss');
|
| 1488 |
+
doneEdge('redis', 'orchestrator');
|
| 1489 |
+
}
|
| 1490 |
+
}
|
| 1491 |
+
|
| 1492 |
if (node === 'memory_add') {
|
| 1493 |
doneEdge('synthesis','self_critique'); doneEdge('self_critique','confidence');
|
| 1494 |
doneEdge('confidence','mem_add'); doneEdge('mem_add','answer');
|
|
|
|
| 1578 |
if (ev && ev.run_id && !nodeData.langsmith) {
|
| 1579 |
nodeData.langsmith = { run_id: ev.run_id };
|
| 1580 |
}
|
| 1581 |
+
// Retrieved chunks for Vector, BM25, Ensemble, and Reranker
|
| 1582 |
+
const chunkMatch = step.match(/\[(VectorDB|BM25|Ensemble|Reranker)\] Retrieved: (.+?)(?:\|([-\.\d]+)\|(.+))?$/i);
|
| 1583 |
if (chunkMatch) {
|
| 1584 |
+
const nodeKey = chunkMatch[1].toLowerCase();
|
| 1585 |
nodeData[nodeKey] = nodeData[nodeKey] || { retrieved_chunks: [] };
|
| 1586 |
if (chunkMatch[3] !== undefined && chunkMatch[4] !== undefined) {
|
| 1587 |
nodeData[nodeKey].retrieved_chunks.push({
|
|
|
|
| 1629 |
nodeData.redis.similarity = m ? parseFloat(m[1]) : 0;
|
| 1630 |
nodeData.redis.hit = true;
|
| 1631 |
}
|
| 1632 |
+
if (/Cache MISS/i.test(step)) {
|
| 1633 |
+
nodeData.redis = nodeData.redis || {};
|
| 1634 |
+
nodeData.redis.hit = false;
|
| 1635 |
+
}
|
| 1636 |
}
|
| 1637 |
|
| 1638 |
// ββ NODE DETAIL MODAL ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 1665 |
|
| 1666 |
else if (nodeId === 'query_decomposer') {
|
| 1667 |
const sqs = data.sub_questions || [];
|
| 1668 |
+
const orig = data.original_query || document.getElementById('qinput').value || '';
|
| 1669 |
+
const currentIntent = document.getElementById('intent-pill').textContent.trim();
|
| 1670 |
+
|
| 1671 |
+
body += `<div class="nm-section"><div class="nm-section-title">βοΈ Decomposing Complex Intent</div>`;
|
| 1672 |
+
|
| 1673 |
+
if (currentIntent === 'MULTI_HOP' && sqs.length) {
|
| 1674 |
+
body += `
|
| 1675 |
+
<div class="nm-card info" style="margin-bottom:12px; border-left-color:#a78bfa">
|
| 1676 |
+
<strong>Decomposer Logic:</strong> Since the query was classified as <strong>${currentIntent}</strong>, it is split into independent sub-queries to retrieve targeted facts from different documents (e.g. drug formularies, provider databases) which are later synthesized.
|
| 1677 |
+
</div>
|
| 1678 |
+
<div class="nm-card subq" style="background:rgba(255,255,255,0.03); border-left-color:rgba(167,139,250,0.5); margin-bottom:16px;">
|
| 1679 |
+
<strong>Original Input Query:</strong> <em>"${escHtml(orig)}"</em>
|
| 1680 |
+
</div>
|
| 1681 |
+
<div class="nm-section-title" style="font-size:0.85rem; margin-top:8px;">Generated Atomic Sub-Questions:</div>
|
| 1682 |
+
`;
|
| 1683 |
+
body += sqs.map((q, i) => `<div class="nm-card subq" style="border-left-color:#a78bfa; margin-bottom:6px;"><strong>Sub-Q ${i+1}:</strong> ${escHtml(q)}</div>`).join('');
|
| 1684 |
+
body += `<div class="nm-stat-row" style="margin-top:12px;"><div class="nm-stat">Sub-questions generated: <span>${sqs.length}</span></div></div>`;
|
| 1685 |
+
} else {
|
| 1686 |
+
body += `
|
| 1687 |
+
<div class="nm-card warning" style="border-left-color:#fbbf24">
|
| 1688 |
+
<strong>Decomposer Skipped:</strong> The query intent was classified as <strong>${currentIntent || 'non-MULTI_HOP'}</strong>. Only queries with <strong>MULTI_HOP</strong> intent are decomposed. Other intents retrieve results in a single hop.
|
| 1689 |
+
</div>
|
| 1690 |
+
`;
|
| 1691 |
}
|
| 1692 |
+
body += '</div>';
|
| 1693 |
}
|
| 1694 |
|
| 1695 |
else if (nodeId === 'query_guard') {
|
|
|
|
| 1882 |
body += `</div>`;
|
| 1883 |
}
|
| 1884 |
|
| 1885 |
+
else if (nodeId === 'ensemble') {
|
| 1886 |
+
body += `<div class="nm-section"><div class="nm-section-title">βοΈ Reciprocal Rank Fusion (RRF) Formula</div>`;
|
| 1887 |
+
body += `<div class="nm-card info" style="font-family:monospace; font-size: 0.8rem; overflow-x: auto; white-space: nowrap;">
|
| 1888 |
+
RRF_Score(d β D) = β_{m β M} w_m / (r_m(d) + k)
|
| 1889 |
+
</div>`;
|
| 1890 |
+
body += `<div class="nm-card info" style="font-size:0.75rem; line-height:1.4;">
|
| 1891 |
+
Melds Vector search similarity results with BM25 keyword rankings. w_m represents retriever weights, and k is the constant rank smoothing parameter (default c=60).
|
| 1892 |
+
</div>`;
|
| 1893 |
+
if (data.retrieved_chunks && data.retrieved_chunks.length) {
|
| 1894 |
+
body += `<div class="nm-section-title" style="margin-top:10px; display:flex; justify-content:space-between; align-items:center;">
|
| 1895 |
+
<span>π RRF Merged Scores</span>
|
| 1896 |
+
<button onclick="toggleScoreFullscreen()" style="background:none;border:none;color:#38bdf8;cursor:pointer;font-size:0.8rem;padding:0;">βΆ Fullscreen</button>
|
| 1897 |
+
</div>`;
|
| 1898 |
+
body += `<svg id="nm-score-viz" width="100%" height="250" style="background:#1e293b; border-radius:8px; margin-top:5px; border:1px solid #334155; transition: height 0.3s ease;"></svg>`;
|
| 1899 |
+
body += `<div class="nm-section-title" style="margin-top:10px">π Merged Documents (RRF Order)</div>`;
|
| 1900 |
+
body += data.retrieved_chunks.map(c => {
|
| 1901 |
+
const text = typeof c === 'string' ? c : `[Score/RRF: ${c.score.toFixed(4)}] ${c.content}`;
|
| 1902 |
+
return `<div class="nm-card info" style="font-size: 0.8rem;">${escHtml(text)}</div>`;
|
| 1903 |
+
}).join('');
|
| 1904 |
+
} else {
|
| 1905 |
+
body += noData;
|
| 1906 |
+
}
|
| 1907 |
+
body += `</div>`;
|
| 1908 |
+
}
|
| 1909 |
+
|
| 1910 |
+
else if (nodeId === 'reranker') {
|
| 1911 |
+
body += `<div class="nm-section"><div class="nm-section-title">βοΈ Cross-Encoder Relevance Scoring</div>`;
|
| 1912 |
+
body += `<div class="nm-card info" style="font-size:0.75rem; line-height:1.4;">
|
| 1913 |
+
Uses the BGE Cross-Encoder model to score the raw relevance of each retrieved chunk against the query. Chunks scoring below the minimum relevance threshold are filtered out.
|
| 1914 |
+
</div>`;
|
| 1915 |
+
if (data.retrieved_chunks && data.retrieved_chunks.length) {
|
| 1916 |
+
body += `<div class="nm-section-title" style="margin-top:10px; display:flex; justify-content:space-between; align-items:center;">
|
| 1917 |
+
<span>π Reranker Relevance Scores</span>
|
| 1918 |
+
<button onclick="toggleScoreFullscreen()" style="background:none;border:none;color:#38bdf8;cursor:pointer;font-size:0.8rem;padding:0;">βΆ Fullscreen</button>
|
| 1919 |
+
</div>`;
|
| 1920 |
+
body += `<svg id="nm-score-viz" width="100%" height="250" style="background:#1e293b; border-radius:8px; margin-top:5px; border:1px solid #334155; transition: height 0.3s ease;"></svg>`;
|
| 1921 |
+
body += `<div class="nm-section-title" style="margin-top:10px">π Top Reranked Chunks</div>`;
|
| 1922 |
+
body += data.retrieved_chunks.map(c => {
|
| 1923 |
+
const text = typeof c === 'string' ? c : `[Score: ${c.score.toFixed(4)}] ${c.content}`;
|
| 1924 |
+
return `<div class="nm-card info" style="font-size: 0.8rem;">${escHtml(text)}</div>`;
|
| 1925 |
+
}).join('');
|
| 1926 |
+
} else {
|
| 1927 |
+
body += noData;
|
| 1928 |
+
}
|
| 1929 |
+
body += `</div>`;
|
| 1930 |
+
}
|
| 1931 |
+
|
| 1932 |
+
else if (nodeId === 'query_analyzer') {
|
| 1933 |
+
body += `<div class="nm-section"><div class="nm-section-title">π Query Analyzer & Normalizer</div>`;
|
| 1934 |
+
body += `<div class="nm-card info" style="font-size:0.75rem; line-height:1.4;">
|
| 1935 |
+
Translates conversational, user-specific phrasing (first-person details, questions) into standard, formal third-person search terminology using GPT-4o-mini. This bridges the vocabulary gap and enables accurate semantic cache matching.
|
| 1936 |
+
</div>`;
|
| 1937 |
+
if (data.normalized_query) {
|
| 1938 |
+
body += `<div class="nm-section-title" style="margin-top:10px;">π Normalization Output</div>`;
|
| 1939 |
+
body += `<div class="nm-card good" style="font-size:0.85rem; font-weight:700;">
|
| 1940 |
+
"${escHtml(data.normalized_query)}"
|
| 1941 |
+
</div>`;
|
| 1942 |
+
} else {
|
| 1943 |
+
body += noData;
|
| 1944 |
+
}
|
| 1945 |
+
body += `</div>`;
|
| 1946 |
+
}
|
| 1947 |
+
|
| 1948 |
+
else if (nodeId === 'redis') {
|
| 1949 |
+
body += `<div class="nm-section"><div class="nm-section-title">π΄ Redis Semantic Cache Details</div>`;
|
| 1950 |
+
body += `<div class="nm-card info" style="font-size:0.75rem; line-height:1.4;">
|
| 1951 |
+
Checks the Redis vector cache (or fallback local cache) for semantically equivalent queries. Bypasses the LangGraph orchestrator and LLM generation completely on cache hits.
|
| 1952 |
+
</div>`;
|
| 1953 |
+
if (data.matched_query) {
|
| 1954 |
+
body += `<div class="nm-card good" style="font-size: 0.8rem; border-left: 3px solid #10b981;">
|
| 1955 |
+
<strong>β‘ Cache HIT!</strong><br>
|
| 1956 |
+
<strong>Matched Cached Query:</strong> "${escHtml(data.matched_query)}"<br>
|
| 1957 |
+
<strong>Similarity Score:</strong> ${data.similarity}%
|
| 1958 |
+
</div>`;
|
| 1959 |
+
} else if (data.hit === false) {
|
| 1960 |
+
body += `<div class="nm-card bad" style="font-size: 0.8rem; border-left: 3px solid #ef4444;">
|
| 1961 |
+
<strong>β Cache MISS</strong><br>
|
| 1962 |
+
No semantically matching query was found in the cache. Proceeding to standard RAG pipeline.
|
| 1963 |
+
</div>`;
|
| 1964 |
+
} else {
|
| 1965 |
+
body += noData;
|
| 1966 |
+
}
|
| 1967 |
+
body += `</div>`;
|
| 1968 |
+
}
|
| 1969 |
+
|
| 1970 |
else if (nodeId === 'graphdb') {
|
| 1971 |
+
// Edge Filtering Logic for Neater Visualisation
|
| 1972 |
+
let filteredEdges = [];
|
| 1973 |
+
let filteredEntities = data.entities || [];
|
| 1974 |
+
|
| 1975 |
+
if (data.edges && data.edges.length > 0) {
|
| 1976 |
+
const HUBS = new Set(['bronze', 'silver', 'gold', 'all']);
|
| 1977 |
+
const queryTokens = (document.getElementById('qinput').value || '').toLowerCase();
|
| 1978 |
+
|
| 1979 |
+
const directlyRelevant = new Set();
|
| 1980 |
+
data.edges.forEach(e => {
|
| 1981 |
+
[e.source, e.target].forEach(nodeName => {
|
| 1982 |
+
const nLower = nodeName.toLowerCase();
|
| 1983 |
+
if (queryTokens.includes(nLower) && nLower.length > 2) {
|
| 1984 |
+
directlyRelevant.add(nodeName);
|
| 1985 |
+
}
|
| 1986 |
+
if (nLower === 'cardiology' && (queryTokens.includes('cardiologist') || queryTokens.includes('cardio'))) {
|
| 1987 |
+
directlyRelevant.add(nodeName);
|
| 1988 |
+
}
|
| 1989 |
+
if (nLower === 'dermatology' && (queryTokens.includes('dermatologist') || queryTokens.includes('derm'))) {
|
| 1990 |
+
directlyRelevant.add(nodeName);
|
| 1991 |
+
}
|
| 1992 |
+
if (nLower === 'pediatrics' && (queryTokens.includes('pediatrician') || queryTokens.includes('ped'))) {
|
| 1993 |
+
directlyRelevant.add(nodeName);
|
| 1994 |
+
}
|
| 1995 |
+
if (nLower === 'chicago, il' && queryTokens.includes('chicago')) {
|
| 1996 |
+
directlyRelevant.add(nodeName);
|
| 1997 |
+
}
|
| 1998 |
+
});
|
| 1999 |
+
});
|
| 2000 |
+
|
| 2001 |
+
const secondaryRelevant = new Set();
|
| 2002 |
+
data.edges.forEach(e => {
|
| 2003 |
+
const sHub = HUBS.has(e.source.toLowerCase());
|
| 2004 |
+
const tHub = HUBS.has(e.target.toLowerCase());
|
| 2005 |
+
if (directlyRelevant.has(e.source) && !sHub) secondaryRelevant.add(e.target);
|
| 2006 |
+
if (directlyRelevant.has(e.target) && !tHub) secondaryRelevant.add(e.source);
|
| 2007 |
+
});
|
| 2008 |
+
|
| 2009 |
+
filteredEdges = data.edges.filter(e => {
|
| 2010 |
+
const sLower = e.source.toLowerCase();
|
| 2011 |
+
const tLower = e.target.toLowerCase();
|
| 2012 |
+
const sDirect = directlyRelevant.has(e.source);
|
| 2013 |
+
const tDirect = directlyRelevant.has(e.target);
|
| 2014 |
+
const sSec = secondaryRelevant.has(e.source);
|
| 2015 |
+
const tSec = secondaryRelevant.has(e.target);
|
| 2016 |
+
|
| 2017 |
+
if ((sDirect || sSec) && (tDirect || tSec)) {
|
| 2018 |
+
if (HUBS.has(sLower) && !tDirect && !tSec) return false;
|
| 2019 |
+
if (HUBS.has(tLower) && !sDirect && !sSec) return false;
|
| 2020 |
+
return true;
|
| 2021 |
+
}
|
| 2022 |
+
return false;
|
| 2023 |
+
});
|
| 2024 |
+
|
| 2025 |
+
if (filteredEdges.length === 0) {
|
| 2026 |
+
filteredEdges = data.edges.slice(0, 15);
|
| 2027 |
+
}
|
| 2028 |
+
|
| 2029 |
+
const visibleNodes = new Set();
|
| 2030 |
+
filteredEdges.forEach(e => {
|
| 2031 |
+
visibleNodes.add(e.source);
|
| 2032 |
+
visibleNodes.add(e.target);
|
| 2033 |
+
});
|
| 2034 |
+
|
| 2035 |
+
if (data.entities) {
|
| 2036 |
+
filteredEntities = data.entities.filter(ent => visibleNodes.has(ent) || directlyRelevant.has(ent));
|
| 2037 |
+
if (filteredEntities.length === 0) {
|
| 2038 |
+
filteredEntities = data.entities.slice(0, 10);
|
| 2039 |
+
}
|
| 2040 |
+
}
|
| 2041 |
+
}
|
| 2042 |
+
|
| 2043 |
body += `<div class="nm-section"><div class="nm-section-title">πΈοΈ Knowledge Graph Lookups</div>`;
|
| 2044 |
if (data.entities && data.entities.length) {
|
| 2045 |
+
body += `<div class="nm-stat-row"><div class="nm-stat">Entities resolved: <span>${data.entities.length}</span> (showing relevant: ${filteredEntities.length})</div></div>`;
|
| 2046 |
|
| 2047 |
+
if (filteredEdges.length > 0) {
|
| 2048 |
body += `<div class="nm-section-title" style="margin-top:10px; display:flex; justify-content:space-between; align-items:center;">
|
| 2049 |
+
<span>π Local Graph Explorer (Relevant Nodes)</span>
|
| 2050 |
<button onclick="toggleGraphFullscreen()" style="background:none;border:none;color:#38bdf8;cursor:pointer;font-size:0.8rem;padding:0;">βΆ Fullscreen</button>
|
| 2051 |
</div>`;
|
| 2052 |
body += `<svg id="nm-graph-viz" width="100%" height="250" style="background:#1e293b; border-radius:8px; margin-top:5px; border:1px solid #334155; transition: height 0.3s ease;"></svg>`;
|
| 2053 |
}
|
| 2054 |
|
| 2055 |
+
body += `<div class="nm-section-title" style="margin-top:10px">π Fetched Entities</div>`;
|
| 2056 |
+
body += filteredEntities.map(e => `<div class="nm-card info" style="font-size: 0.8rem;">${escHtml(e)}</div>`).join('');
|
| 2057 |
} else if (data.entities && data.entities.length === 0) {
|
| 2058 |
body += `<div class="nm-card info">No matching entities found in the knowledge graph.</div>`;
|
| 2059 |
} else {
|
| 2060 |
body += noData;
|
| 2061 |
}
|
| 2062 |
body += `</div>`;
|
| 2063 |
+
|
| 2064 |
+
// Save filtered edges globally so we can retrieve them in the D3 rendering phase below
|
| 2065 |
+
window.lastFilteredEdges = filteredEdges;
|
| 2066 |
}
|
| 2067 |
|
| 2068 |
else {
|
|
|
|
| 2075 |
document.getElementById('nm-body').innerHTML = body;
|
| 2076 |
document.getElementById('node-modal-backdrop').classList.add('open');
|
| 2077 |
|
| 2078 |
+
if (nodeId === 'graphdb' && window.lastFilteredEdges && window.lastFilteredEdges.length > 0) {
|
| 2079 |
setTimeout(() => {
|
| 2080 |
const container = document.getElementById('nm-graph-viz');
|
| 2081 |
if (!container) return;
|
|
|
|
| 2085 |
svg.selectAll('*').remove();
|
| 2086 |
|
| 2087 |
const nodesMap = {};
|
| 2088 |
+
window.lastFilteredEdges.forEach(e => {
|
| 2089 |
nodesMap[e.source] = { id: e.source };
|
| 2090 |
nodesMap[e.target] = { id: e.target };
|
| 2091 |
});
|
| 2092 |
const nodes = Object.values(nodesMap);
|
| 2093 |
+
const links = window.lastFilteredEdges.map(e => ({ source: e.source, target: e.target, label: e.relation }));
|
| 2094 |
|
| 2095 |
const simulation = d3.forceSimulation(nodes)
|
| 2096 |
.force("link", d3.forceLink(links).id(d => d.id).distance(120))
|
|
|
|
| 2175 |
}, 100);
|
| 2176 |
}
|
| 2177 |
|
| 2178 |
+
if ((nodeId === 'vectordb' || nodeId === 'bm25' || nodeId === 'ensemble' || nodeId === 'reranker') && data.retrieved_chunks && data.retrieved_chunks.length > 0) {
|
| 2179 |
setTimeout(() => {
|
| 2180 |
renderScoreGraph(data.retrieved_chunks);
|
| 2181 |
}, 100);
|
ingestion/graph_ingest.py
CHANGED
|
@@ -83,7 +83,30 @@ def build_knowledge_graph():
|
|
| 83 |
state = str(row['state']).strip()
|
| 84 |
|
| 85 |
# Provider Node
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
# Specialty Node
|
| 89 |
G.add_node(specialty, type="Specialty")
|
|
|
|
| 83 |
state = str(row['state']).strip()
|
| 84 |
|
| 85 |
# Provider Node
|
| 86 |
+
accepting = str(row['accepting_new_patients']).strip()
|
| 87 |
+
telehealth = str(row['telehealth_available']).strip()
|
| 88 |
+
languages = str(row['languages_spoken']).strip()
|
| 89 |
+
group = str(row['group_practice']).strip()
|
| 90 |
+
phone = str(row['phone']).strip()
|
| 91 |
+
address = str(row['street_address']).strip()
|
| 92 |
+
zip_code = str(row['zip']).strip()
|
| 93 |
+
|
| 94 |
+
G.add_node(
|
| 95 |
+
npi,
|
| 96 |
+
type="Provider",
|
| 97 |
+
name=name,
|
| 98 |
+
specialty=specialty,
|
| 99 |
+
city=city,
|
| 100 |
+
state=state,
|
| 101 |
+
accepting_new_patients=accepting,
|
| 102 |
+
telehealth_available=telehealth,
|
| 103 |
+
languages_spoken=languages,
|
| 104 |
+
group_practice=group,
|
| 105 |
+
phone=phone,
|
| 106 |
+
street_address=address,
|
| 107 |
+
zip=zip_code,
|
| 108 |
+
hospital_affiliation=hospital
|
| 109 |
+
)
|
| 110 |
|
| 111 |
# Specialty Node
|
| 112 |
G.add_node(specialty, type="Specialty")
|
ingestion/ingest.py
CHANGED
|
@@ -250,7 +250,16 @@ def load_and_chunk_documents() -> list[Document]:
|
|
| 250 |
|
| 251 |
dl_meta = chunk.metadata.get("dl_meta", {})
|
| 252 |
if dl_meta:
|
| 253 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
chunk.metadata["heading"] = (
|
| 255 |
dl_meta.get("headings", [""])[0]
|
| 256 |
if dl_meta.get("headings") else ""
|
|
|
|
| 250 |
|
| 251 |
dl_meta = chunk.metadata.get("dl_meta", {})
|
| 252 |
if dl_meta:
|
| 253 |
+
page_no = None
|
| 254 |
+
if "doc_items" in dl_meta and dl_meta["doc_items"]:
|
| 255 |
+
prov = dl_meta["doc_items"][0].get("prov", [])
|
| 256 |
+
if prov:
|
| 257 |
+
page_no = prov[0].get("page_no")
|
| 258 |
+
|
| 259 |
+
if page_no is not None:
|
| 260 |
+
chunk.metadata["page"] = page_no - 1 # 0-indexed for internal consistency
|
| 261 |
+
chunk.metadata["page_no"] = page_no
|
| 262 |
+
|
| 263 |
chunk.metadata["heading"] = (
|
| 264 |
dl_meta.get("headings", [""])[0]
|
| 265 |
if dl_meta.get("headings") else ""
|
orchestration/semantic_cache.py
CHANGED
|
@@ -179,7 +179,7 @@ class SemanticCache:
|
|
| 179 |
logger.warning(f"Failed to normalize query: {e}. Using original query.")
|
| 180 |
return query
|
| 181 |
|
| 182 |
-
def check(self, query: str, plan_tier: str = "Unknown") -> Optional[dict]:
|
| 183 |
"""
|
| 184 |
Check the semantic cache for a match.
|
| 185 |
|
|
@@ -192,7 +192,7 @@ class SemanticCache:
|
|
| 192 |
start_time = time.time()
|
| 193 |
try:
|
| 194 |
# 1. Normalize query to standard terminology
|
| 195 |
-
norm_query = self.normalize_query(query)
|
| 196 |
|
| 197 |
# 2. Embed the normalized query
|
| 198 |
query_vector = self.embeddings.embed_query(norm_query)
|
|
@@ -264,7 +264,7 @@ class SemanticCache:
|
|
| 264 |
|
| 265 |
return None
|
| 266 |
|
| 267 |
-
def store(self, query: str, response: dict, plan_tier: str = "Unknown") -> None:
|
| 268 |
"""
|
| 269 |
Store query response in the semantic cache.
|
| 270 |
"""
|
|
@@ -274,7 +274,7 @@ class SemanticCache:
|
|
| 274 |
|
| 275 |
try:
|
| 276 |
# 1. Normalize query to standard terminology
|
| 277 |
-
norm_query = self.normalize_query(query)
|
| 278 |
|
| 279 |
# Generate deterministic cache ID based on normalized query
|
| 280 |
cache_id = hashlib.sha256(norm_query.encode("utf-8")).hexdigest()
|
|
|
|
| 179 |
logger.warning(f"Failed to normalize query: {e}. Using original query.")
|
| 180 |
return query
|
| 181 |
|
| 182 |
+
def check(self, query: str, plan_tier: str = "Unknown", normalized_query: Optional[str] = None) -> Optional[dict]:
|
| 183 |
"""
|
| 184 |
Check the semantic cache for a match.
|
| 185 |
|
|
|
|
| 192 |
start_time = time.time()
|
| 193 |
try:
|
| 194 |
# 1. Normalize query to standard terminology
|
| 195 |
+
norm_query = normalized_query or self.normalize_query(query)
|
| 196 |
|
| 197 |
# 2. Embed the normalized query
|
| 198 |
query_vector = self.embeddings.embed_query(norm_query)
|
|
|
|
| 264 |
|
| 265 |
return None
|
| 266 |
|
| 267 |
+
def store(self, query: str, response: dict, plan_tier: str = "Unknown", normalized_query: Optional[str] = None) -> None:
|
| 268 |
"""
|
| 269 |
Store query response in the semantic cache.
|
| 270 |
"""
|
|
|
|
| 274 |
|
| 275 |
try:
|
| 276 |
# 1. Normalize query to standard terminology
|
| 277 |
+
norm_query = normalized_query or self.normalize_query(query)
|
| 278 |
|
| 279 |
# Generate deterministic cache ID based on normalized query
|
| 280 |
cache_id = hashlib.sha256(norm_query.encode("utf-8")).hexdigest()
|
orchestration/tools.py
CHANGED
|
@@ -44,13 +44,29 @@ def _format_docs(docs: list[Document], max_per_tool: int = 5) -> str:
|
|
| 44 |
formatted = []
|
| 45 |
for d in docs[:max_per_tool]:
|
| 46 |
source = d.metadata.get("source_file", "Unknown")
|
| 47 |
-
page
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
if row: cite += f", Rows: {row}"
|
| 52 |
cite += ")"
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
return "\n\n---\n\n".join(formatted) if formatted else ""
|
| 55 |
|
| 56 |
|
|
@@ -185,7 +201,7 @@ def plan_comparison_search(query: str, tier: str) -> str:
|
|
| 185 |
vs = _load_vectorstore()
|
| 186 |
|
| 187 |
# Pre-filter by both plan_tier AND inferred doc_type for maximum precision
|
| 188 |
-
tier_specific_docs = vs.similarity_search(query, k=
|
| 189 |
docs = tier_specific_docs + docs
|
| 190 |
except Exception:
|
| 191 |
pass
|
|
@@ -200,7 +216,7 @@ def plan_comparison_search(query: str, tier: str) -> str:
|
|
| 200 |
if not tier_docs:
|
| 201 |
tier_docs = docs # Fallback: use all results
|
| 202 |
|
| 203 |
-
result = _format_docs(tier_docs, max_per_tool=
|
| 204 |
return result if result else f"No {tier} plan information found for this query."
|
| 205 |
|
| 206 |
|
|
|
|
| 44 |
formatted = []
|
| 45 |
for d in docs[:max_per_tool]:
|
| 46 |
source = d.metadata.get("source_file", "Unknown")
|
| 47 |
+
# Support both 'page' (0-indexed) and 'page_no' (1-indexed) metadata fields
|
| 48 |
+
page = d.metadata.get("page")
|
| 49 |
+
page_no = d.metadata.get("page_no")
|
| 50 |
+
row = d.metadata.get("row_range", "")
|
| 51 |
+
cite = f"(Source: {source}"
|
| 52 |
+
if page is not None and str(page).strip() != "":
|
| 53 |
+
try:
|
| 54 |
+
# If page is stored, it's 0-indexed, display as 1-indexed
|
| 55 |
+
cite += f", Page: {int(page)+1}"
|
| 56 |
+
except ValueError:
|
| 57 |
+
cite += f", Page: {page}"
|
| 58 |
+
elif page_no is not None and str(page_no).strip() != "":
|
| 59 |
+
# page_no is 1-indexed directly from Docling
|
| 60 |
+
cite += f", Page: {page_no}"
|
| 61 |
+
|
| 62 |
if row: cite += f", Rows: {row}"
|
| 63 |
cite += ")"
|
| 64 |
+
|
| 65 |
+
# Prepend contextual display header (plan tier, document type, source)
|
| 66 |
+
header = d.metadata.get("display_header", "")
|
| 67 |
+
content = f"{header}\n{d.page_content}" if header else d.page_content
|
| 68 |
+
formatted.append(f"{cite}\n{content}")
|
| 69 |
+
|
| 70 |
return "\n\n---\n\n".join(formatted) if formatted else ""
|
| 71 |
|
| 72 |
|
|
|
|
| 201 |
vs = _load_vectorstore()
|
| 202 |
|
| 203 |
# Pre-filter by both plan_tier AND inferred doc_type for maximum precision
|
| 204 |
+
tier_specific_docs = vs.similarity_search(query, k=8, filter={"plan_tier": tier.capitalize()})
|
| 205 |
docs = tier_specific_docs + docs
|
| 206 |
except Exception:
|
| 207 |
pass
|
|
|
|
| 216 |
if not tier_docs:
|
| 217 |
tier_docs = docs # Fallback: use all results
|
| 218 |
|
| 219 |
+
result = _format_docs(tier_docs, max_per_tool=8)
|
| 220 |
return result if result else f"No {tier} plan information found for this query."
|
| 221 |
|
| 222 |
|
retrieval/graph_retriever.py
CHANGED
|
@@ -23,26 +23,26 @@ from orchestration.tracing import log_event
|
|
| 23 |
# Maps colloquial / lay terms β canonical specialty names in the graph
|
| 24 |
SYNONYM_MAP = {
|
| 25 |
"brain doctor": "Neurology", "brain doctors": "Neurology",
|
| 26 |
-
"brain specialist": "Neurology", "neurologist": "Neurology", "neuro": "Neurology",
|
| 27 |
"heart doctor": "Cardiology", "heart doctors": "Cardiology",
|
| 28 |
-
"heart specialist": "Cardiology", "cardiologist": "Cardiology",
|
| 29 |
"eye doctor": "Ophthalmology", "eye doctors": "Ophthalmology",
|
| 30 |
-
"eye specialist": "Ophthalmology", "optometrist": "Ophthalmology",
|
| 31 |
"skin doctor": "Dermatology", "skin doctors": "Dermatology",
|
| 32 |
-
"dermatologist": "Dermatology",
|
| 33 |
-
"bone doctor": "Orthopedics", "joint specialist": "Orthopedics",
|
| 34 |
-
"
|
| 35 |
-
"
|
| 36 |
-
"
|
| 37 |
-
"
|
| 38 |
-
"
|
| 39 |
-
"
|
| 40 |
-
"
|
| 41 |
-
"
|
| 42 |
-
"
|
| 43 |
-
"
|
| 44 |
-
"
|
| 45 |
-
"
|
| 46 |
}
|
| 47 |
|
| 48 |
console = Console()
|
|
@@ -238,6 +238,16 @@ class GraphRetriever:
|
|
| 238 |
if self.G.nodes.get(n, {}).get("type") == "Specialty"
|
| 239 |
]
|
| 240 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
def _find_providers(state_filter=None):
|
| 242 |
return [
|
| 243 |
(n, d) for n, d in self.G.nodes(data=True)
|
|
|
|
| 23 |
# Maps colloquial / lay terms β canonical specialty names in the graph
|
| 24 |
SYNONYM_MAP = {
|
| 25 |
"brain doctor": "Neurology", "brain doctors": "Neurology",
|
| 26 |
+
"brain specialist": "Neurology", "neurologist": "Neurology", "neurologists": "Neurology", "neuro": "Neurology",
|
| 27 |
"heart doctor": "Cardiology", "heart doctors": "Cardiology",
|
| 28 |
+
"heart specialist": "Cardiology", "cardiologist": "Cardiology", "cardiologists": "Cardiology",
|
| 29 |
"eye doctor": "Ophthalmology", "eye doctors": "Ophthalmology",
|
| 30 |
+
"eye specialist": "Ophthalmology", "optometrist": "Ophthalmology", "optometrists": "Ophthalmology", "ophthalmologist": "Ophthalmology", "ophthalmologists": "Ophthalmology",
|
| 31 |
"skin doctor": "Dermatology", "skin doctors": "Dermatology",
|
| 32 |
+
"dermatologist": "Dermatology", "dermatologists": "Dermatology",
|
| 33 |
+
"bone doctor": "Orthopedics", "joint specialist": "Orthopedics", "orthopedist": "Orthopedics", "orthopedists": "Orthopedics",
|
| 34 |
+
"gut doctor": "Gastroenterology", "stomach doctor": "Gastroenterology", "gastroenterologist": "Gastroenterology", "gastroenterologists": "Gastroenterology", "gastro": "Gastroenterology",
|
| 35 |
+
"kidney doctor": "Nephrology", "kidney specialist": "Nephrology", "nephrologist": "Nephrology", "nephrologists": "Nephrology",
|
| 36 |
+
"lung doctor": "Pulmonology", "lung specialist": "Pulmonology", "pulmonologist": "Pulmonology", "pulmonologists": "Pulmonology",
|
| 37 |
+
"kids doctor": "Pediatrics", "child doctor": "Pediatrics", "pediatrician": "Pediatrics", "pediatricians": "Pediatrics",
|
| 38 |
+
"diabetes doctor": "Endocrinology", "endocrinologist": "Endocrinology", "endocrinologists": "Endocrinology",
|
| 39 |
+
"women doctor": "OB/GYN", "gynecologist": "OB/GYN", "gynecologists": "OB/GYN", "obgyn": "OB/GYN",
|
| 40 |
+
"blood doctor": "Hematology", "hematologist": "Hematology", "hematologists": "Hematology",
|
| 41 |
+
"cancer doctor": "Oncology", "oncologist": "Oncology", "oncologists": "Oncology",
|
| 42 |
+
"psychiatrist": "Psychiatry", "psychiatrists": "Psychiatry", "mental health": "Psychiatry",
|
| 43 |
+
"urologist": "Urology", "urologists": "Urology",
|
| 44 |
+
"rheumatologist": "Rheumatology", "rheumatologists": "Rheumatology",
|
| 45 |
+
"plastic surgeon": "Plastic Surgery", "plastic surgeons": "Plastic Surgery",
|
| 46 |
}
|
| 47 |
|
| 48 |
console = Console()
|
|
|
|
| 238 |
if self.G.nodes.get(n, {}).get("type") == "Specialty"
|
| 239 |
]
|
| 240 |
|
| 241 |
+
# Check if we have any providers in the requested city with matching specialty
|
| 242 |
+
in_city_providers = [
|
| 243 |
+
(n, d) for n, d in self.G.nodes(data=True)
|
| 244 |
+
if d.get("type") == "Provider"
|
| 245 |
+
and (not target_specs or d.get("specialty", "") in target_specs)
|
| 246 |
+
and d.get("city", "").lower() == city.lower()
|
| 247 |
+
]
|
| 248 |
+
if in_city_providers:
|
| 249 |
+
return ""
|
| 250 |
+
|
| 251 |
def _find_providers(state_filter=None):
|
| 252 |
return [
|
| 253 |
(n, d) for n, d in self.G.nodes(data=True)
|
retrieval/retriever.py
CHANGED
|
@@ -235,7 +235,37 @@ def get_hybrid_retriever(
|
|
| 235 |
console.print(f" β
BM25 retriever ready (k={k}, {len(all_docs)} documents indexed)")
|
| 236 |
|
| 237 |
# ββ Stage 3: Ensemble Retriever βββββββββββββββββββββββββββββ
|
| 238 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
retrievers=[logging_bm25_retriever, logging_vector_retriever],
|
| 240 |
weights=weights,
|
| 241 |
)
|
|
@@ -281,7 +311,16 @@ def get_hybrid_retriever(
|
|
| 281 |
def compress_documents(self, documents, query, callbacks=None):
|
| 282 |
if not documents:
|
| 283 |
return []
|
| 284 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
for d, s in zip(documents, scores):
|
| 286 |
d.metadata["relevance_score"] = float(s)
|
| 287 |
|
|
@@ -291,6 +330,11 @@ def get_hybrid_retriever(
|
|
| 291 |
if d.metadata.get("doc_type") == "knowledge_graph"
|
| 292 |
or d.metadata.get("relevance_score", 0) >= MIN_RELEVANCE_SCORE
|
| 293 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 294 |
return filtered[:self.top_n]
|
| 295 |
|
| 296 |
compressor = ScoredReranker(model=cross_encoder, top_n=top_n)
|
|
|
|
| 235 |
console.print(f" β
BM25 retriever ready (k={k}, {len(all_docs)} documents indexed)")
|
| 236 |
|
| 237 |
# ββ Stage 3: Ensemble Retriever βββββββββββββββββββββββββββββ
|
| 238 |
+
class LoggingEnsembleRetriever(EnsembleRetriever):
|
| 239 |
+
def weighted_reciprocal_rank(self, doc_lists: list[list[Document]]) -> list[Document]:
|
| 240 |
+
from collections import defaultdict
|
| 241 |
+
rrf_score = defaultdict(float)
|
| 242 |
+
for doc_list, weight in zip(doc_lists, self.weights):
|
| 243 |
+
for rank, doc in enumerate(doc_list, start=1):
|
| 244 |
+
key = (
|
| 245 |
+
doc.page_content
|
| 246 |
+
if self.id_key is None
|
| 247 |
+
else doc.metadata.get(self.id_key)
|
| 248 |
+
)
|
| 249 |
+
rrf_score[key] += weight / (rank + self.c)
|
| 250 |
+
|
| 251 |
+
merged_docs = super().weighted_reciprocal_rank(doc_lists)
|
| 252 |
+
for doc in merged_docs:
|
| 253 |
+
key = (
|
| 254 |
+
doc.page_content
|
| 255 |
+
if self.id_key is None
|
| 256 |
+
else doc.metadata.get(self.id_key)
|
| 257 |
+
)
|
| 258 |
+
doc.metadata["score"] = rrf_score.get(key, 0.0)
|
| 259 |
+
|
| 260 |
+
for i, d in enumerate(merged_docs[:4]):
|
| 261 |
+
source = d.metadata.get("source_file", "unknown")
|
| 262 |
+
page = d.metadata.get("page", "?")
|
| 263 |
+
score = d.metadata.get("score", 0.0)
|
| 264 |
+
log_event(f"[Ensemble] Retrieved: {source} (Page: {page})|{score:.4f}|{d.page_content[:40]}...")
|
| 265 |
+
|
| 266 |
+
return merged_docs
|
| 267 |
+
|
| 268 |
+
ensemble_retriever = LoggingEnsembleRetriever(
|
| 269 |
retrievers=[logging_bm25_retriever, logging_vector_retriever],
|
| 270 |
weights=weights,
|
| 271 |
)
|
|
|
|
| 311 |
def compress_documents(self, documents, query, callbacks=None):
|
| 312 |
if not documents:
|
| 313 |
return []
|
| 314 |
+
|
| 315 |
+
# Prepend display_header (which contains plan tier, source file, etc.)
|
| 316 |
+
# so that the Cross-Encoder is aware of metadata context during scoring.
|
| 317 |
+
texts_to_score = []
|
| 318 |
+
for d in documents:
|
| 319 |
+
header = d.metadata.get("display_header", "")
|
| 320 |
+
content = f"{header}\n{d.page_content}" if header else d.page_content
|
| 321 |
+
texts_to_score.append((query, content))
|
| 322 |
+
|
| 323 |
+
scores = self.model.score(texts_to_score)
|
| 324 |
for d, s in zip(documents, scores):
|
| 325 |
d.metadata["relevance_score"] = float(s)
|
| 326 |
|
|
|
|
| 330 |
if d.metadata.get("doc_type") == "knowledge_graph"
|
| 331 |
or d.metadata.get("relevance_score", 0) >= MIN_RELEVANCE_SCORE
|
| 332 |
]
|
| 333 |
+
for i, d in enumerate(filtered[:4]):
|
| 334 |
+
source = d.metadata.get("source_file", "unknown")
|
| 335 |
+
page = d.metadata.get("page", "?")
|
| 336 |
+
score = d.metadata.get("relevance_score", 0.0)
|
| 337 |
+
log_event(f"[Reranker] Retrieved: {source} (Page: {page})|{score:.4f}|{d.page_content[:40]}...")
|
| 338 |
return filtered[:self.top_n]
|
| 339 |
|
| 340 |
compressor = ScoredReranker(model=cross_encoder, top_n=top_n)
|
storage/chroma_db/chroma.sqlite3
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
size 29683712
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7ef4ea9956fe593a75772e296a52b386783773ed497ba317718629e198503e44
|
| 3 |
size 29683712
|
storage/knowledge_graph.graphml
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|