resberry commited on
Commit
13b7600
·
verified ·
1 Parent(s): e573ab2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +89 -26
app.py CHANGED
@@ -2,7 +2,7 @@ import os
2
  import json
3
  import pickle
4
  import re
5
- from typing import List, Tuple
6
 
7
  import faiss
8
  import gradio as gr
@@ -23,11 +23,13 @@ from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
23
  # FINAL_TOP_N=4
24
  # SCORE_THRESHOLD=0.40
25
  # USE_SCORE_THRESHOLD=true
 
 
26
  #
27
  # In Space Settings -> Secrets:
28
  # HF_TOKEN=hf_xxxxxxxxx
29
  #
30
- # Optional later:
31
  # EMBED_MODEL_NAME=sentence-transformers/all-MiniLM-L6-v2
32
 
33
  BASE_MODEL_PATH = os.getenv("BASE_MODEL_PATH", "meta-llama/Llama-3.1-8B-Instruct")
@@ -93,14 +95,15 @@ SYSTEM_PROMPT = (
93
  " - For devices/procedures questions: explain the indication, principle, and major cardiac or ECG relevance.\n"
94
  " - For special populations questions: adapt the explanation to the relevant group such as pediatrics, pregnancy, older adults, athletes, or patients with congenital/inherited disease.\n"
95
  "7) When relevant, include hallmark ECG features, diagnostic characteristics, mechanism, clinical implications, major differential points, and management relevance.\n"
96
- "8) Keep simple questions concise in 1-3 sentences.\n"
97
- "9) For broader or more complex questions, provide a focused multi-sentence explanation that remains concise but sufficiently complete.\n"
98
- "10) Do NOT fabricate facts, criteria, thresholds, interpretations, or treatment recommendations.\n"
99
- "11) Do NOT include conversational filler, vague generalities, unnecessary repetition, or non-cardiology content.\n"
100
- "12) Do NOT ask follow-up questions unless clarification is absolutely required to avoid an unsafe or invalid answer.\n"
101
- "13) Use a confident clinical tone.\n"
102
- "14) End with a complete declarative sentence.\n"
103
- "15) If uncertain, respond exactly with: I’m not sure about that."
 
104
  )
105
 
106
  RAG_INSTRUCTION = (
@@ -112,7 +115,7 @@ RAG_INSTRUCTION = (
112
  "5) If different CONTEXT passages conflict, prefer the most directly relevant and specific passage.\n"
113
  "6) If the CONTEXT is relevant, generate a clinically structured answer based on the CONTEXT.\n"
114
  "7) If the CONTEXT is partially relevant or incomplete, you may provide a careful general cardiology/ECG answer that remains consistent with the CONTEXT and the question.\n"
115
- "8) If the CONTEXT is empty or clearly irrelevant, say exactly: \"I couldn't find this in my sources.\" Then provide only a brief, careful general cardiology/ECG reply.\n"
116
  "9) Do not introduce specific claims, criteria, thresholds, localization statements, pharmacologic details, management recommendations, or prognostic claims unless they are supported by the CONTEXT or are high-confidence core domain knowledge.\n"
117
  "10) Internally organize the answer according to the question type, using the most relevant of the following areas when appropriate:\n"
118
  " - Definition or core concept\n"
@@ -126,21 +129,24 @@ RAG_INSTRUCTION = (
126
  " - Preventive implications\n"
127
  " - Device/procedural relevance\n"
128
  " - Population-specific considerations\n"
129
- "11) If citations are enabled and CONTEXT is used, support each sentence with citations such as [S1] or [S1][S2] when multiple sources support the sentence.\n"
130
  "12) Output ONLY the answer text. No headings, bullet points, URLs, reference sections, or source labels.\n"
131
  "13) Do NOT paste, quote, or closely copy the CONTEXT.\n"
132
  "14) Do NOT repeat the same fact in different wording.\n"
133
- "15) Keep the answer concise but sufficiently complete for a clinically useful response.\n"
134
  "16) Stop immediately after the final sentence.\n"
135
  )
136
 
137
  ANSWER_STYLE_HINT = (
138
- "Prefer answers that naturally follow this order when relevant: "
139
- "definition -> key diagnostic or ECG features -> mechanism -> differential -> management or emergency relevance -> prognosis or prevention note."
 
 
 
140
  )
141
 
142
  MAX_LEN = 2048
143
- MAX_NEW_TOKENS = 320
144
  DO_SAMPLE = True
145
  TEMPERATURE = 0.2
146
  TOP_P = 0.9
@@ -167,8 +173,8 @@ def strip_bad_sections(txt: str) -> str:
167
  "Please let me know",
168
  "Is there anything else",
169
  ]
170
- for m in cut_markers:
171
- pos = t.lower().find(m.lower())
172
  if pos != -1:
173
  t = t[:pos].strip()
174
 
@@ -204,6 +210,31 @@ def extract_fallback_answer_from_doc(doc_text: str) -> str:
204
  return doc_text.split("A:", 1)[1].strip()
205
  return doc_text.strip()
206
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  # ============================================================
208
  # LOAD EMBEDDER
209
  # ============================================================
@@ -234,7 +265,6 @@ def build_rag_from_csv(csv_path: str):
234
  for i, row in df.iterrows():
235
  q = row["instruction"]
236
  a = row["response"]
237
-
238
  doc_text = f"Topic Question: {q}\nClinical Content: {a}"
239
  docs.append(doc_text)
240
  meta.append({"id": int(i), "question": q})
@@ -264,6 +294,7 @@ def build_rag_from_csv(csv_path: str):
264
  "retrieval_k": RETRIEVAL_K,
265
  "final_top_n": FINAL_TOP_N,
266
  "evidence_synthesis": ENABLE_EVIDENCE_SYNTHESIS,
 
267
  },
268
  f,
269
  indent=2
@@ -445,8 +476,7 @@ def _generate(messages):
445
  gen_ids = out[0, inputs["input_ids"].shape[1]:]
446
  answer = tokenizer.decode(gen_ids, skip_special_tokens=True).strip()
447
  answer = strip_bad_sections(answer)
448
- answer = answer.strip()
449
- return answer
450
 
451
  @torch.no_grad()
452
  def extract_relevant_evidence(user_query: str, hits):
@@ -485,6 +515,28 @@ def is_invalid_answer(answer: str, used_context: bool, show_citations: bool) ->
485
  return True
486
  return False
487
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
488
  @torch.no_grad()
489
  def generate_with_rag(user_query: str, show_citations: bool = True):
490
  hits = retrieve(user_query, k=RETRIEVAL_K)
@@ -516,16 +568,16 @@ def generate_with_rag(user_query: str, show_citations: bool = True):
516
 
517
  if is_invalid_answer(answer, used_context, show_citations):
518
  strict_format = (
519
- "Rewrite the answer so it is clinically precise, concise, and well structured.\n"
520
  "Use the provided CONTEXT and EXTRACTED_RELEVANT_EVIDENCE as the primary evidence base.\n"
521
  "If the CONTEXT supports the answer, base the response on the CONTEXT.\n"
522
  "When multiple relevant sources are present, combine the consistent relevant information into one coherent answer.\n"
523
- "If multiple sources support the same sentence, citations such as [S1][S2] are allowed.\n"
524
  "If the CONTEXT is insufficient, say exactly: \"I couldn't find this in my sources.\" "
525
- "Then provide only a brief, careful general cardiology/ECG reply.\n"
526
  "Include only information relevant to the question.\n"
527
  "Do not add unsupported facts, criteria, thresholds, or interpretations.\n"
528
- "If citations are enabled and CONTEXT is used, every sentence must end with [S#] or [S#][S#].\n"
529
  "Output only the answer text.\n"
530
  "No headings, no bullet points, no references section, no URLs.\n"
531
  "Stop immediately after the final sentence."
@@ -541,6 +593,14 @@ def generate_with_rag(user_query: str, show_citations: bool = True):
541
  fallback = extract_fallback_answer_from_doc(hits[0][2]) if hits else ""
542
  answer = f"{fallback} [S1]" if fallback else "I couldn't find this in my sources."
543
 
 
 
 
 
 
 
 
 
544
  if not show_citations:
545
  answer = remove_citations(answer)
546
 
@@ -614,7 +674,10 @@ def reset_chat():
614
 
615
  with gr.Blocks(title="MrCardio") as demo:
616
  gr.Markdown("# MrCardio")
617
- gr.Markdown("Fine-tuned cardiology / ECG LoRA model with FAISS-based RAG, lightweight reranking, and evidence synthesis.")
 
 
 
618
 
619
  show_citations = gr.Checkbox(value=True, label="Show citations in answers")
620
  chatbot = gr.Chatbot(type="messages", height=520)
 
2
  import json
3
  import pickle
4
  import re
5
+ from typing import List
6
 
7
  import faiss
8
  import gradio as gr
 
23
  # FINAL_TOP_N=4
24
  # SCORE_THRESHOLD=0.40
25
  # USE_SCORE_THRESHOLD=true
26
+ # ENABLE_EVIDENCE_SYNTHESIS=true
27
+ # MAX_NEW_TOKENS=500
28
  #
29
  # In Space Settings -> Secrets:
30
  # HF_TOKEN=hf_xxxxxxxxx
31
  #
32
+ # Optional:
33
  # EMBED_MODEL_NAME=sentence-transformers/all-MiniLM-L6-v2
34
 
35
  BASE_MODEL_PATH = os.getenv("BASE_MODEL_PATH", "meta-llama/Llama-3.1-8B-Instruct")
 
95
  " - For devices/procedures questions: explain the indication, principle, and major cardiac or ECG relevance.\n"
96
  " - For special populations questions: adapt the explanation to the relevant group such as pediatrics, pregnancy, older adults, athletes, or patients with congenital/inherited disease.\n"
97
  "7) When relevant, include hallmark ECG features, diagnostic characteristics, mechanism, clinical implications, major differential points, and management relevance.\n"
98
+ "8) For simple questions, provide a concise but clinically informative answer.\n"
99
+ "9) For broader or more complex questions, provide a fuller explanation with clinically relevant detail when supported by the CONTEXT.\n"
100
+ "10) When the question is general, explanatory, or educational, expand the answer enough to cover the key clinical points.\n"
101
+ "11) Do NOT fabricate facts, criteria, thresholds, interpretations, or treatment recommendations.\n"
102
+ "12) Do NOT include conversational filler, vague generalities, unnecessary repetition, or non-cardiology content.\n"
103
+ "13) Do NOT ask follow-up questions unless clarification is absolutely required to avoid an unsafe or invalid answer.\n"
104
+ "14) Use a confident clinical tone.\n"
105
+ "15) End with a complete declarative sentence.\n"
106
+ "16) If uncertain, respond exactly with: I’m not sure about that."
107
  )
108
 
109
  RAG_INSTRUCTION = (
 
115
  "5) If different CONTEXT passages conflict, prefer the most directly relevant and specific passage.\n"
116
  "6) If the CONTEXT is relevant, generate a clinically structured answer based on the CONTEXT.\n"
117
  "7) If the CONTEXT is partially relevant or incomplete, you may provide a careful general cardiology/ECG answer that remains consistent with the CONTEXT and the question.\n"
118
+ "8) If the CONTEXT is empty or clearly irrelevant, say exactly: \"I couldn't find this in my sources.\" Then provide a careful general cardiology/ECG reply with enough detail to be clinically useful.\n"
119
  "9) Do not introduce specific claims, criteria, thresholds, localization statements, pharmacologic details, management recommendations, or prognostic claims unless they are supported by the CONTEXT or are high-confidence core domain knowledge.\n"
120
  "10) Internally organize the answer according to the question type, using the most relevant of the following areas when appropriate:\n"
121
  " - Definition or core concept\n"
 
129
  " - Preventive implications\n"
130
  " - Device/procedural relevance\n"
131
  " - Population-specific considerations\n"
132
+ "11) If citations are enabled and CONTEXT is used, support each sentence with one or more citations such as [S1], [S1][S2], or [S1][S2][S3] when supported by multiple sources.\n"
133
  "12) Output ONLY the answer text. No headings, bullet points, URLs, reference sections, or source labels.\n"
134
  "13) Do NOT paste, quote, or closely copy the CONTEXT.\n"
135
  "14) Do NOT repeat the same fact in different wording.\n"
136
+ "15) Keep the answer sufficiently complete for a clinically useful response.\n"
137
  "16) Stop immediately after the final sentence.\n"
138
  )
139
 
140
  ANSWER_STYLE_HINT = (
141
+ "Prefer clinically rich, well-structured answers. "
142
+ "When relevant, naturally follow this order: definition -> key diagnostic or ECG features -> mechanism -> differential -> management or emergency relevance -> prognosis or prevention note. "
143
+ "For general or educational questions, expand enough to provide a useful cardiology/ECG explanation. "
144
+ "For definition questions, provide a precise definition followed by the main clinical purpose or significance when relevant. "
145
+ "Use specific cardiology terminology rather than generic phrases."
146
  )
147
 
148
  MAX_LEN = 2048
149
+ MAX_NEW_TOKENS = int(os.getenv("MAX_NEW_TOKENS", "500"))
150
  DO_SAMPLE = True
151
  TEMPERATURE = 0.2
152
  TOP_P = 0.9
 
173
  "Please let me know",
174
  "Is there anything else",
175
  ]
176
+ for marker in cut_markers:
177
+ pos = t.lower().find(marker.lower())
178
  if pos != -1:
179
  t = t[:pos].strip()
180
 
 
210
  return doc_text.split("A:", 1)[1].strip()
211
  return doc_text.strip()
212
 
213
+ def is_definition_question(question: str) -> bool:
214
+ q = (question or "").strip().lower()
215
+ return (
216
+ q.startswith("what is ")
217
+ or q.startswith("what are ")
218
+ or q.startswith("define ")
219
+ or q.startswith("what does ")
220
+ )
221
+
222
+ def is_too_generic_definition(question: str, answer: str) -> bool:
223
+ if not is_definition_question(question):
224
+ return False
225
+
226
+ a = (answer or "").strip().lower()
227
+ generic_phrases = [
228
+ "is a test that records",
229
+ "records the electrical activity of the heart over time",
230
+ "providing insight into",
231
+ "shown as waves",
232
+ "paper or digital display",
233
+ "gives insight into the heart's rhythm and function",
234
+ "rhythm and function",
235
+ ]
236
+ return any(phrase in a for phrase in generic_phrases)
237
+
238
  # ============================================================
239
  # LOAD EMBEDDER
240
  # ============================================================
 
265
  for i, row in df.iterrows():
266
  q = row["instruction"]
267
  a = row["response"]
 
268
  doc_text = f"Topic Question: {q}\nClinical Content: {a}"
269
  docs.append(doc_text)
270
  meta.append({"id": int(i), "question": q})
 
294
  "retrieval_k": RETRIEVAL_K,
295
  "final_top_n": FINAL_TOP_N,
296
  "evidence_synthesis": ENABLE_EVIDENCE_SYNTHESIS,
297
+ "max_new_tokens": MAX_NEW_TOKENS,
298
  },
299
  f,
300
  indent=2
 
476
  gen_ids = out[0, inputs["input_ids"].shape[1]:]
477
  answer = tokenizer.decode(gen_ids, skip_special_tokens=True).strip()
478
  answer = strip_bad_sections(answer)
479
+ return answer.strip()
 
480
 
481
  @torch.no_grad()
482
  def extract_relevant_evidence(user_query: str, hits):
 
515
  return True
516
  return False
517
 
518
+ @torch.no_grad()
519
+ def refine_definition_answer(user_query: str, answer: str, show_citations: bool, messages):
520
+ if not is_too_generic_definition(user_query, answer):
521
+ return answer
522
+
523
+ refine_prompt = (
524
+ "Rewrite the answer in more precise clinical cardiology language.\n"
525
+ "Keep it concise but clinically informative.\n"
526
+ "For a definition question, provide a precise definition and the main clinical purpose or significance when relevant.\n"
527
+ "Prefer specific cardiology terminology over generic phrases.\n"
528
+ "Do not add unsupported facts.\n"
529
+ "If citations are enabled, preserve sentence-ending citations.\n"
530
+ "Output only the answer text."
531
+ )
532
+ messages2 = messages + [{"role": "user", "content": refine_prompt}]
533
+ refined = _generate(messages2)
534
+
535
+ if show_citations:
536
+ cited = keep_only_cited_sentences(refined)
537
+ return cited if cited.strip() else answer
538
+ return refined.strip() or answer
539
+
540
  @torch.no_grad()
541
  def generate_with_rag(user_query: str, show_citations: bool = True):
542
  hits = retrieve(user_query, k=RETRIEVAL_K)
 
568
 
569
  if is_invalid_answer(answer, used_context, show_citations):
570
  strict_format = (
571
+ "Rewrite the answer so it is clinically precise, well structured, and sufficiently detailed for a useful cardiology/ECG explanation.\n"
572
  "Use the provided CONTEXT and EXTRACTED_RELEVANT_EVIDENCE as the primary evidence base.\n"
573
  "If the CONTEXT supports the answer, base the response on the CONTEXT.\n"
574
  "When multiple relevant sources are present, combine the consistent relevant information into one coherent answer.\n"
575
+ "If multiple sources support the same sentence, citations such as [S1][S2] or [S1][S2][S3] are allowed.\n"
576
  "If the CONTEXT is insufficient, say exactly: \"I couldn't find this in my sources.\" "
577
+ "Then provide a careful general cardiology/ECG reply with enough detail to be clinically useful.\n"
578
  "Include only information relevant to the question.\n"
579
  "Do not add unsupported facts, criteria, thresholds, or interpretations.\n"
580
+ "If citations are enabled and CONTEXT is used, every sentence must end with one or more citations such as [S#], [S#][S#], or [S#][S#][S#].\n"
581
  "Output only the answer text.\n"
582
  "No headings, no bullet points, no references section, no URLs.\n"
583
  "Stop immediately after the final sentence."
 
593
  fallback = extract_fallback_answer_from_doc(hits[0][2]) if hits else ""
594
  answer = f"{fallback} [S1]" if fallback else "I couldn't find this in my sources."
595
 
596
+ if used_context:
597
+ answer = refine_definition_answer(
598
+ user_query=user_query,
599
+ answer=answer,
600
+ show_citations=show_citations,
601
+ messages=messages,
602
+ )
603
+
604
  if not show_citations:
605
  answer = remove_citations(answer)
606
 
 
674
 
675
  with gr.Blocks(title="MrCardio") as demo:
676
  gr.Markdown("# MrCardio")
677
+ gr.Markdown(
678
+ "Fine-tuned cardiology / ECG LoRA model with FAISS-based RAG, "
679
+ "lightweight reranking, evidence synthesis, and richer answer generation."
680
+ )
681
 
682
  show_citations = gr.Checkbox(value=True, label="Show citations in answers")
683
  chatbot = gr.Chatbot(type="messages", height=520)