Cardiosense-AG commited on
Commit
02e346e
·
verified ·
1 Parent(s): 2e139f8

Update src/reasoning_panel.py

Browse files
Files changed (1) hide show
  1. src/reasoning_panel.py +46 -197
src/reasoning_panel.py CHANGED
@@ -17,7 +17,7 @@ Iteration 2 note:
17
  """
18
  from __future__ import annotations
19
  from typing import List, Dict, Any, Tuple
20
- import json, html
21
 
22
  import streamlit as st
23
  import streamlit.components.v1 as components
@@ -30,6 +30,9 @@ from .explainability import (
30
  from .rag_index import search_index
31
  from .paths import faiss_index_dir
32
 
 
 
 
33
 
34
  def _normalize_features(pairs: List[Tuple[str, float]], top_k: int = 5) -> List[Dict[str, float]]:
35
  # keep top_k and L1-normalize
@@ -44,13 +47,16 @@ def _best_snippet_for_ref(claim: str, doc: str, page: int, top_k: int = 6) -> st
44
  Heuristic snippet lookup: query FAISS by the claim text and preferentially
45
  return a chunk from the requested doc/page. Falls back gracefully.
46
  """
 
 
 
 
47
  try:
48
- # Use the same embed model that was used to build the FAISS index
49
  results = search_index(
50
  faiss_index_dir(),
51
  claim,
52
  top_k=top_k,
53
- embed_model="intfloat/e5-base-v2",
54
  device="cpu",
55
  )
56
  except TypeError:
@@ -59,8 +65,11 @@ def _best_snippet_for_ref(claim: str, doc: str, page: int, top_k: int = 6) -> st
59
  faiss_index_dir(),
60
  claim,
61
  top_k=top_k,
62
- embed_model="intfloat/e5-base-v2",
63
  )
 
 
 
64
 
65
  # exact doc+page match
66
  for r in results or []:
@@ -81,8 +90,6 @@ def _best_snippet_for_ref(claim: str, doc: str, page: int, top_k: int = 6) -> st
81
  return ""
82
 
83
 
84
-
85
-
86
  def build_panel_data(
87
  assessment: str,
88
  plan: str,
@@ -92,222 +99,64 @@ def build_panel_data(
92
  mode: str = "sim",
93
  ) -> List[Dict[str, Any]]:
94
  text_for_claims = (assessment or "").strip() + (" " if assessment and plan else "") + (plan or "").strip()
 
 
 
95
  claims = segment_claims(text_for_claims)
 
 
 
96
  citation_idx = build_citation_index(citations or [])
97
  exp = explain_claims_sim_only(claims, pcp_summary or "", specialty or "")
98
- # NOTE: attention re-rank is integrated in Iteration 3; here we keep 'sim' mode.
99
 
100
  panel: List[Dict[str, Any]] = []
101
  for e in exp:
102
- claim = e.get("claim", "")
 
 
 
103
  # Merge guideline + pcp token weights (max-pool then normalize)
104
- weights = {}
105
  for tok, w in (e.get("top_tokens_guideline") or []):
106
- tok = tok.strip()
107
- if not tok:
108
  continue
109
- weights[tok] = max(weights.get(tok, 0.0), float(w))
110
  for tok, w in (e.get("top_tokens_pcp") or []):
111
- tok = tok.strip()
112
- if not tok:
113
  continue
114
- weights[tok] = max(weights.get(tok, 0.0), float(w))
 
115
  features = _normalize_features(list(weights.items()), top_k=5)
116
 
117
  # Map refs -> label + snippet
118
  refs_out: List[Dict[str, Any]] = []
119
  for r in (e.get("guideline_refs") or []):
120
  doc = r.get("doc")
121
- page = int(r.get("page", 0))
 
 
 
122
  num = citation_idx.get((doc, page))
123
- label = f"{doc} [{num}]" if num else f"{doc} (p.{page})"
 
 
 
124
  snippet = _best_snippet_for_ref(claim, doc, page)
125
  refs_out.append({"id": f"{doc}-{page}", "label": label, "snippet": snippet})
 
126
  panel.append({"claim": claim, "features": features, "refs": refs_out})
 
127
  return panel
128
 
129
 
130
  def _escape_script_json(d: Any) -> str:
131
  s = json.dumps(d, ensure_ascii=False)
132
- return s.replace("</", "<\\/") # prevent </script> breakage
133
 
 
 
134
 
135
- def render_reasoning_panel(
136
- panel_data: List[Dict[str, Any]],
137
- assessment: str,
138
- plan: str,
139
- citations: List[Dict[str, Any]] = None,
140
- height: int = 720,
141
- key: str = None
142
- ):
143
- """Render dual-pane (SOAP viewer ↔ Reasoning Panel) as an inline component.
144
-
145
- Left pane shows the claims extracted from Assessment+Plan (read-only preview),
146
- which keeps scroll-sync deterministic and fast. The editable textareas remain
147
- on the Streamlit page above this component.
148
- """
149
- citations = citations or []
150
- data_js = _escape_script_json(panel_data)
151
- refs_js = _escape_script_json(citations)
152
-
153
- left_claims_html = "".join(
154
- f'<p class="soap-claim"><span class="marker"></span>{html.escape(d.get("claim",""))}</p>'
155
- for d in panel_data
156
- )
157
-
158
- html_code = f"""
159
- <div id="rpv4-root">
160
- <style>
161
- :root {{
162
- --bg: #ffffff; --fg: #111; --muted: #6b7280; --surface: #f8fafc; --accent: #2563eb;
163
- --accent-weak: rgba(37,99,235,.08); --border: #e5e7eb;
164
- }}
165
- @media (prefers-color-scheme: dark) {{
166
- :root {{
167
- --bg: #0b0f17; --fg: #eaeef5; --muted: #a8b3cf; --surface: #111827; --accent: #60a5fa;
168
- --accent-weak: rgba(96,165,250,.12); --border: #263145;
169
- }}
170
- }}
171
- #rpv4-root {{ font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial; color: var(--fg); }}
172
- .grid {{ display: grid; grid-template-columns: 1fr 0.9fr; gap: 16px; align-items: start; }}
173
- @media (max-width: 980px) {{ .grid {{ grid-template-columns: 1fr; }} }}
174
- .panel {{ background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 12px 14px; overflow: auto; max-height: {int(height)-20}px; }}
175
- .sec {{ margin: 10px 0 8px; font-size: 14px; letter-spacing:.3px; color:var(--muted); text-transform:uppercase; }}
176
- .soap-claim {{ position: relative; margin: 8px 0; padding: 8px 10px 8px 26px; border-radius: 8px; }}
177
- .soap-claim.active {{ background: var(--accent-weak); outline: 1px solid var(--accent); }}
178
- .marker {{ position: absolute; left: 8px; top: 14px; width: 8px; height: 8px; border-radius: 50%; background: var(--muted); }}
179
- .soap-claim.active .marker {{ background: var(--accent); }}
180
- .claim-card {{ border-bottom: 1px dashed var(--border); padding: 10px 6px; }}
181
- .claim-title {{ cursor: pointer; font-weight: 600; }}
182
- .features {{ margin-top: 8px; }}
183
- .feat {{ display:flex; align-items:center; gap:8px; font-size:13px; margin:3px 0; }}
184
- .bar {{ flex:1; height:8px; background:var(--border); border-radius:4px; overflow:hidden; }}
185
- .fill {{ height:100%; background:var(--accent); }}
186
- .refs {{ margin-top: 6px; display:flex; flex-wrap:wrap; gap:6px; }}
187
- .ref {{ padding:3px 6px; background: var(--accent-weak); border:1px solid var(--border); border-radius:6px; cursor:pointer; }}
188
- .ref:hover {{ border-color: var(--accent); }}
189
- dialog#evidence {{ border: 1px solid var(--border); border-radius: 10px; background: var(--bg); color: var(--fg); max-width: 680px; }}
190
- dialog::backdrop {{ background: rgba(0,0,0,.45); }}
191
- .ev-title {{ font-weight:600; margin-bottom:6px; }}
192
- .ev-body {{ white-space: pre-wrap; line-height:1.4; }}
193
- .kbd {{ font: 12px ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono"; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 2px 6px; color: var(--muted); }}
194
- </style>
195
-
196
- <div class="grid">
197
- <div class="panel" id="soap-pane">
198
- <div class="sec" style="margin-top:0;">SOAP — Assessment & Plan</div>
199
- {left_claims_html}
200
- </div>
201
- <div class="panel" id="reason-pane">
202
- <div style="display:flex;justify-content:space-between;align-items:center;">
203
- <div class="sec" style="margin:0;">REASONING PANEL</div>
204
- <div class="kbd" title="Keyboard shortcuts">↑/↓ select • Esc close modal</div>
205
- </div>
206
- <div id="claims"></div>
207
- </div>
208
- </div>
209
-
210
- <dialog id="evidence">
211
- <div class="ev-title"></div>
212
- <div class="ev-body"></div>
213
- <div style="margin-top:10px;display:flex;justify-content:flex-end;">
214
- <button id="closeEv" class="ref">Close (Esc)</button>
215
- </div>
216
- </dialog>
217
-
218
- <script>
219
- const data = {data_js};
220
- const allRefs = {refs_js};
221
- const claimsEl = document.getElementById('claims');
222
- const soap = document.getElementById('soap-pane');
223
- const evidence = document.getElementById('evidence');
224
- const evTitle = evidence.querySelector('.ev-title');
225
- const evBody = evidence.querySelector('.ev-body');
226
- const closeEv = document.getElementById('closeEv');
227
-
228
- function pct(n) {{ return Math.round(n * 100); }}
229
-
230
- function render() {{
231
- claimsEl.innerHTML = '';
232
- data.forEach((d, idx) => {{
233
- const card = document.createElement('div');
234
- card.className = 'claim-card';
235
- const title = document.createElement('div');
236
- title.className = 'claim-title';
237
- title.textContent = d.claim || `Claim ${idx+1}`;
238
- title.addEventListener('click', () => activate(idx, true));
239
- card.appendChild(title);
240
-
241
- const feats = document.createElement('div');
242
- feats.className = 'features';
243
- (d.features || []).forEach(f => {{
244
- const row = document.createElement('div'); row.className = 'feat';
245
- const name = document.createElement('div'); name.textContent = f.token;
246
- const bar = document.createElement('div'); bar.className = 'bar';
247
- const fill = document.createElement('div'); fill.className = 'fill'; fill.style.width = (pct(f.weight)) + '%';
248
- const perc = document.createElement('div'); perc.textContent = (pct(f.weight)) + '%';
249
- bar.appendChild(fill);
250
- row.appendChild(name); row.appendChild(bar); row.appendChild(perc);
251
- feats.appendChild(row);
252
- }});
253
- card.appendChild(feats);
254
-
255
- const refs = document.createElement('div');
256
- refs.className = 'refs';
257
- (d.refs || []).forEach(r => {{
258
- const b = document.createElement('button');
259
- b.className = 'ref';
260
- b.textContent = r.label || 'Reference';
261
- b.title = 'Open evidence snippet';
262
- b.addEventListener('click', () => openEvidence(r));
263
- refs.appendChild(b);
264
- }});
265
- card.appendChild(refs);
266
-
267
- claimsEl.appendChild(card);
268
- }});
269
- }}
270
-
271
- function openEvidence(ref) {{
272
- evTitle.textContent = ref.label || 'Evidence';
273
- evBody.textContent = ref.snippet || 'Snippet not available for this reference.';
274
- evidence.showModal();
275
- }}
276
- closeEv.addEventListener('click', () => evidence.close());
277
- document.addEventListener('keydown', (e) => {{
278
- if (e.key === 'Escape') evidence.close();
279
- if (['ArrowDown','ArrowUp'].includes(e.key)) {{
280
- e.preventDefault();
281
- navigate(e.key === 'ArrowDown' ? 1 : -1);
282
- }}
283
- }});
284
-
285
- function activate(idx, scroll) {{
286
- // Highlight claim in left pane
287
- const claimEls = soap.querySelectorAll('.soap-claim');
288
- claimEls.forEach(el => el.classList.remove('active'));
289
- const el = claimEls[idx];
290
- if (el) {{
291
- el.classList.add('active');
292
- if (scroll) el.scrollIntoView({{behavior:'smooth', block:'center'}});
293
- }}
294
- // Highlight card title (optional)
295
- const cardTitles = document.querySelectorAll('.claim-title');
296
- cardTitles.forEach((t, i) => t.style.color = (i===idx ? 'var(--accent)' : 'inherit'));
297
- window.__rpv4_index = idx;
298
- }}
299
-
300
- function navigate(delta) {{
301
- const max = (data || []).length;
302
- const curr = (window.__rpv4_index || 0);
303
- const next = Math.max(0, Math.min(max-1, curr + delta));
304
- activate(next, true);
305
- }}
306
-
307
- render();
308
- // Initial activation
309
- activate(0, false);
310
- </script>
311
- </div>
312
- """
313
- components.html(html_code, height=height, scrolling=True)
 
17
  """
18
  from __future__ import annotations
19
  from typing import List, Dict, Any, Tuple
20
+ import json, html, os
21
 
22
  import streamlit as st
23
  import streamlit.components.v1 as components
 
30
  from .rag_index import search_index
31
  from .paths import faiss_index_dir
32
 
33
+ # Use the same embed model as the FAISS index. You told me it's e5-base-v2.
34
+ SAFE_EMBED_MODEL = os.getenv("RAG_EMBED_MODEL", "intfloat/e5-base-v2")
35
+
36
 
37
  def _normalize_features(pairs: List[Tuple[str, float]], top_k: int = 5) -> List[Dict[str, float]]:
38
  # keep top_k and L1-normalize
 
47
  Heuristic snippet lookup: query FAISS by the claim text and preferentially
48
  return a chunk from the requested doc/page. Falls back gracefully.
49
  """
50
+ # Guard: if we don't have a doc label or claim text, skip snippet lookup.
51
+ if not claim or not doc:
52
+ return ""
53
+
54
  try:
 
55
  results = search_index(
56
  faiss_index_dir(),
57
  claim,
58
  top_k=top_k,
59
+ embed_model=SAFE_EMBED_MODEL,
60
  device="cpu",
61
  )
62
  except TypeError:
 
65
  faiss_index_dir(),
66
  claim,
67
  top_k=top_k,
68
+ embed_model=SAFE_EMBED_MODEL,
69
  )
70
+ except Exception:
71
+ # Index missing / incompatible — fail closed without breaking UI
72
+ return ""
73
 
74
  # exact doc+page match
75
  for r in results or []:
 
90
  return ""
91
 
92
 
 
 
93
  def build_panel_data(
94
  assessment: str,
95
  plan: str,
 
99
  mode: str = "sim",
100
  ) -> List[Dict[str, Any]]:
101
  text_for_claims = (assessment or "").strip() + (" " if assessment and plan else "") + (plan or "").strip()
102
+ if not text_for_claims:
103
+ return []
104
+
105
  claims = segment_claims(text_for_claims)
106
+ if not claims:
107
+ return []
108
+
109
  citation_idx = build_citation_index(citations or [])
110
  exp = explain_claims_sim_only(claims, pcp_summary or "", specialty or "")
111
+ # NOTE: attention re-rank lands in Iteration 3; here we keep 'sim' mode.
112
 
113
  panel: List[Dict[str, Any]] = []
114
  for e in exp:
115
+ claim = (e.get("claim") or "").strip()
116
+ if not claim:
117
+ continue
118
+
119
  # Merge guideline + pcp token weights (max-pool then normalize)
120
+ weights: Dict[str, float] = {}
121
  for tok, w in (e.get("top_tokens_guideline") or []):
122
+ t = (tok or "").strip()
123
+ if not t:
124
  continue
125
+ weights[t] = max(weights.get(t, 0.0), float(w or 0.0))
126
  for tok, w in (e.get("top_tokens_pcp") or []):
127
+ t = (tok or "").strip()
128
+ if not t:
129
  continue
130
+ weights[t] = max(weights.get(t, 0.0), float(w or 0.0))
131
+
132
  features = _normalize_features(list(weights.items()), top_k=5)
133
 
134
  # Map refs -> label + snippet
135
  refs_out: List[Dict[str, Any]] = []
136
  for r in (e.get("guideline_refs") or []):
137
  doc = r.get("doc")
138
+ try:
139
+ page = int(r.get("page", 0))
140
+ except Exception:
141
+ page = 0
142
  num = citation_idx.get((doc, page))
143
+ if doc:
144
+ label = f"{doc} [{num}]" if num else f"{doc} (p.{page})"
145
+ else:
146
+ label = f"Reference (p.{page})"
147
  snippet = _best_snippet_for_ref(claim, doc, page)
148
  refs_out.append({"id": f"{doc}-{page}", "label": label, "snippet": snippet})
149
+
150
  panel.append({"claim": claim, "features": features, "refs": refs_out})
151
+
152
  return panel
153
 
154
 
155
  def _escape_script_json(d: Any) -> str:
156
  s = json.dumps(d, ensure_ascii=False)
157
+ return s.replace("</",
158
 
159
+
160
+
161
 
162
+