sachinchandrankallar commited on
Commit
532c782
·
1 Parent(s): 765d77b

changed summary logic

Browse files

Former-commit-id: d509673b3e597bee08e193e8208ae263c3e35a33

' ADDED
@@ -0,0 +1 @@
 
 
1
+ Unable to initialize device PRN
0.41.0 ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Collecting bitsandbytes
2
+ Downloading bitsandbytes-0.47.0-py3-none-win_amd64.whl.metadata (11 kB)
3
+ Requirement already satisfied: torch<3,>=2.2 in c:\users\sachin\appdata\local\programs\python\python311\lib\site-packages (from bitsandbytes) (2.3.0)
4
+ Requirement already satisfied: numpy>=1.17 in c:\users\sachin\appdata\local\programs\python\python311\lib\site-packages (from bitsandbytes) (1.24.3)
5
+ Requirement already satisfied: filelock in c:\users\sachin\appdata\local\programs\python\python311\lib\site-packages (from torch<3,>=2.2->bitsandbytes) (3.18.0)
6
+ Requirement already satisfied: typing-extensions>=4.8.0 in c:\users\sachin\appdata\local\programs\python\python311\lib\site-packages (from torch<3,>=2.2->bitsandbytes) (4.14.1)
7
+ Requirement already satisfied: sympy in c:\users\sachin\appdata\local\programs\python\python311\lib\site-packages (from torch<3,>=2.2->bitsandbytes) (1.14.0)
8
+ Requirement already satisfied: networkx in c:\users\sachin\appdata\local\programs\python\python311\lib\site-packages (from torch<3,>=2.2->bitsandbytes) (3.5)
9
+ Requirement already satisfied: jinja2 in c:\users\sachin\appdata\local\programs\python\python311\lib\site-packages (from torch<3,>=2.2->bitsandbytes) (3.1.6)
10
+ Requirement already satisfied: fsspec in c:\users\sachin\appdata\local\programs\python\python311\lib\site-packages (from torch<3,>=2.2->bitsandbytes) (2025.7.0)
11
+ Requirement already satisfied: mkl<=2021.4.0,>=2021.1.1 in c:\users\sachin\appdata\local\programs\python\python311\lib\site-packages (from torch<3,>=2.2->bitsandbytes) (2021.4.0)
12
+ Requirement already satisfied: intel-openmp==2021.* in c:\users\sachin\appdata\local\programs\python\python311\lib\site-packages (from mkl<=2021.4.0,>=2021.1.1->torch<3,>=2.2->bitsandbytes) (2021.4.0)
13
+ Requirement already satisfied: tbb==2021.* in c:\users\sachin\appdata\local\programs\python\python311\lib\site-packages (from mkl<=2021.4.0,>=2021.1.1->torch<3,>=2.2->bitsandbytes) (2021.13.1)
14
+ Requirement already satisfied: MarkupSafe>=2.0 in c:\users\sachin\appdata\local\programs\python\python311\lib\site-packages (from jinja2->torch<3,>=2.2->bitsandbytes) (2.1.5)
15
+ Requirement already satisfied: mpmath<1.4,>=1.1.0 in c:\users\sachin\appdata\local\programs\python\python311\lib\site-packages (from sympy->torch<3,>=2.2->bitsandbytes) (1.3.0)
16
+ Downloading bitsandbytes-0.47.0-py3-none-win_amd64.whl (60.7 MB)
17
+ ---------------------------------------- 60.7/60.7 MB 12.1 MB/s 0:00:05
18
+ Installing collected packages: bitsandbytes
19
+ Successfully installed bitsandbytes-0.47.0
0.41.0' ADDED
File without changes
ai_med_extract/__pycache__/app.cpython-311.pyc CHANGED
Binary files a/ai_med_extract/__pycache__/app.cpython-311.pyc and b/ai_med_extract/__pycache__/app.cpython-311.pyc differ
 
ai_med_extract/agents/__pycache__/patient_summary_agent.cpython-311.pyc CHANGED
Binary files a/ai_med_extract/agents/__pycache__/patient_summary_agent.cpython-311.pyc and b/ai_med_extract/agents/__pycache__/patient_summary_agent.cpython-311.pyc differ
 
ai_med_extract/api/__pycache__/routes.cpython-311.pyc DELETED
Binary file (45.2 kB)
 
ai_med_extract/api/__pycache__/routes.cpython-311.pyc.REMOVED.git-id ADDED
@@ -0,0 +1 @@
 
 
1
+ 2129106af05cccbba389f4e2c6915fad2060edfd
ai_med_extract/api/routes.py CHANGED
@@ -260,6 +260,79 @@ def get_summarizer_pipeline(summarizer_model_type, summarizer_model_name):
260
 
261
 
262
  def register_routes(app, agents):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
  # Configure upload directory based on environment
264
  import os
265
 
@@ -902,16 +975,25 @@ def register_routes(app, agents):
902
 
903
  @app.route('/generate_patient_summary', methods=['POST'])
904
  def generate_patient_summary():
 
 
 
 
 
 
 
 
 
 
 
 
905
  try:
906
  data = request.get_json()
907
  patientid = data.get("patientid")
908
  token = data.get("token")
909
  key = data.get("key")
910
- # Be robust to null/empty model names provided by clients
911
- model_name = data.get("patient_summarizer_model_name")
912
- if not model_name or str(model_name).strip().lower() in {"", "none", "null"}:
913
- model_name = "falconsai/medical_summarization"
914
-
915
  if not patientid or not token or not key:
916
  return jsonify({"error": "Missing required fields: patientid, token, or key"}), 400
917
 
@@ -920,9 +1002,8 @@ def register_routes(app, agents):
920
  "Authorization": f"Bearer {token}",
921
  "x-api-key": key,
922
  "Content-Type": "application/json",
923
- }
924
  body = json.dumps({"patientid": patientid})
925
-
926
  response = requests.post(api_url, data=body, headers=headers, timeout=30)
927
  if response.status_code != 200:
928
  return jsonify({
@@ -930,77 +1011,71 @@ def register_routes(app, agents):
930
  "status": response.status_code,
931
  "message": response.text
932
  }), 502
933
-
934
- # Be tolerant to any response format: JSON, text, key-values, etc.
935
  try:
936
  api_data = response.json()
937
  except ValueError:
938
  api_data = response.text
939
-
940
- # If dict, prefer nested 'result'; otherwise pass through
941
  if isinstance(api_data, dict):
942
- raw_data = api_data.get("result") or api_data
943
  else:
944
- raw_data = api_data
945
-
946
- logger.info(f"Fetched data for patient {patientid}")
947
- cleaned_data = clean_patient_data(raw_data)
948
- if not cleaned_data or "result" not in cleaned_data:
949
- return jsonify({"error": "Failed to clean patient data"}), 500
950
-
951
- # Helper: convert plain text sections to Markdown
952
- def _to_markdown_summary(text: str) -> str:
953
- headings = [
954
- "Patient Overview",
955
- "Visit History",
956
- "Trend Analysis",
957
- "Assessment",
958
- "Recommendations",
959
- ]
960
- lines_out = []
961
- for ln in str(text or "").splitlines():
962
- s = ln.strip()
963
- if not s:
964
- lines_out.append("")
965
- continue
966
- matched = next((h for h in headings if s.lower().startswith(h.lower() + ":")), None)
967
- if matched:
968
- after = s[len(matched) + 1:].strip()
969
- lines_out.append(f"### {matched}")
970
- if after:
971
- lines_out.append(after)
972
- continue
973
- if s.startswith("- "):
974
- lines_out.append(s)
975
- elif ":" in s and not s.startswith("###"):
976
- lines_out.append(f"- {s}")
977
- else:
978
- lines_out.append(s)
979
- return "\n".join(lines_out).strip()
980
-
981
- # Pass full cleaned_data (with timeline) to agent
982
- summarizer = PatientSummarizerAgent(model_name=model_name)
983
- raw_summary = summarizer.generate_clinical_summary(cleaned_data)
984
- summary_md = _to_markdown_summary(raw_summary)
985
-
986
- if torch.cuda.is_available():
987
- torch.cuda.empty_cache()
988
-
989
- # Optional: return flattened for debugging
990
- flattened = flatten_to_string_list(cleaned_data)
991
-
992
  return jsonify({
993
- "summary": summary_md,
994
- "flattened": flattened
 
 
995
  }), 200
996
-
997
  except requests.exceptions.Timeout:
998
  return jsonify({"error": "Request to EHR API timed out"}), 504
999
  except requests.exceptions.RequestException as e:
1000
  return jsonify({"error": f"Network error: {str(e)}"}), 503
1001
  except Exception as e:
1002
  logger.error(f"Unexpected error: {str(e)}", exc_info=True)
1003
- return jsonify({"error": f"Internal server error: {str(e)}"}), 500
1004
 
1005
  @app.route("/")
1006
  def home():
 
260
 
261
 
262
  def register_routes(app, agents):
263
+ from ai_med_extract.utils.openvino_summarizer_utils import (
264
+ parse_ehr_chartsummarydtl, visits_sorted, compute_deltas, build_compact_baseline, delta_to_text, build_main_prompt, validate_and_compare_summaries
265
+ )
266
+ import threading
267
+ patient_summary_state = {}
268
+ state_lock = threading.Lock()
269
+
270
+ @app.route('/api/patient_summary_openvino', methods=['POST'])
271
+ def patient_summary_openvino():
272
+ """
273
+ Generate a patient summary using OpenVINO-style prompt, delta, and validation logic.
274
+ Accepts EHR API response JSON (or just chartsummarydtl) and returns summary and validation.
275
+ """
276
+ try:
277
+ data = request.get_json()
278
+ ehr_result = data.get("result") or data
279
+ chartsummarydtl = ehr_result.get("chartsummarydtl") if isinstance(ehr_result, dict) else None
280
+ if not chartsummarydtl:
281
+ return jsonify({"error": "Missing chartsummarydtl in input"}), 400
282
+
283
+ # Normalize visits
284
+ visits = parse_ehr_chartsummarydtl(chartsummarydtl)
285
+ patient_id = ehr_result.get("patientid") or ehr_result.get("patientnumber") or "default"
286
+
287
+ # Track state per patient (in-memory, thread-safe)
288
+ with state_lock:
289
+ state = patient_summary_state.setdefault(patient_id, {"visits": [], "last_summary": ""})
290
+ old_visits = state["visits"]
291
+ old_summary = state["last_summary"]
292
+
293
+ # Compute deltas and prompt
294
+ delta = compute_deltas(old_visits, visits)
295
+ all_visits = visits_sorted(old_visits + visits)
296
+ baseline = build_compact_baseline(all_visits)
297
+ delta_text = delta_to_text(delta)
298
+ prompt = build_main_prompt(old_summary, baseline, delta_text)
299
+
300
+ # Model selection logic (model_name, model_type)
301
+ model_name = data.get("model_name") or "microsoft/Phi-3-mini-4k-instruct"
302
+ model_type = data.get("model_type") or "text-generation"
303
+ # Use existing model loader abstraction
304
+ if model_type == "text-generation":
305
+ loader = agents.get("medical_data_extractor")
306
+ else:
307
+ loader = agents.get("summarizer")
308
+ pipeline = loader.model_loader.load() if hasattr(loader, "model_loader") else None
309
+ if not pipeline:
310
+ return jsonify({"error": "Model pipeline not available"}), 500
311
+
312
+ # Run inference
313
+ import torch
314
+ torch.set_num_threads(2)
315
+ inputs = pipeline.tokenizer([prompt], return_tensors="pt")
316
+ outputs = pipeline.model.generate(**inputs, max_new_tokens=400, do_sample=False, pad_token_id=pipeline.tokenizer.eos_token_id or 32000)
317
+ text = pipeline.tokenizer.decode(outputs[0], skip_special_tokens=True)
318
+ new_summary = text.split("Now generate the complete, updated clinical summary with all four sections:")[-1].strip()
319
+
320
+ # Update state
321
+ with state_lock:
322
+ state["visits"] = all_visits
323
+ state["last_summary"] = new_summary
324
+
325
+ # Validation
326
+ validation_report = validate_and_compare_summaries(old_summary, new_summary, "Update")
327
+
328
+ return jsonify({
329
+ "summary": new_summary,
330
+ "validation": validation_report,
331
+ "baseline": baseline,
332
+ "delta": delta_text
333
+ }), 200
334
+ except Exception as e:
335
+ return jsonify({"error": f"Failed to generate summary: {str(e)}"}), 500
336
  # Configure upload directory based on environment
337
  import os
338
 
 
975
 
976
  @app.route('/generate_patient_summary', methods=['POST'])
977
  def generate_patient_summary():
978
+ """
979
+ Enhanced: Uses OpenVINO-style prompt, delta, and validation logic for patient summary generation.
980
+ """
981
+ from ai_med_extract.utils.openvino_summarizer_utils import (
982
+ parse_ehr_chartsummarydtl, visits_sorted, compute_deltas, build_compact_baseline, delta_to_text, build_main_prompt, validate_and_compare_summaries
983
+ )
984
+ import threading
985
+ if not hasattr(generate_patient_summary, "state"):
986
+ generate_patient_summary.state = {}
987
+ generate_patient_summary.lock = threading.Lock()
988
+ state = generate_patient_summary.state
989
+ state_lock = generate_patient_summary.lock
990
  try:
991
  data = request.get_json()
992
  patientid = data.get("patientid")
993
  token = data.get("token")
994
  key = data.get("key")
995
+ model_name = data.get("patient_summarizer_model_name") or "falconsai/medical_summarization"
996
+ model_type = data.get("patient_summarizer_model_type") or data.get("model_type") or "summarization"
 
 
 
997
  if not patientid or not token or not key:
998
  return jsonify({"error": "Missing required fields: patientid, token, or key"}), 400
999
 
 
1002
  "Authorization": f"Bearer {token}",
1003
  "x-api-key": key,
1004
  "Content-Type": "application/json",
1005
+ }
1006
  body = json.dumps({"patientid": patientid})
 
1007
  response = requests.post(api_url, data=body, headers=headers, timeout=30)
1008
  if response.status_code != 200:
1009
  return jsonify({
 
1011
  "status": response.status_code,
1012
  "message": response.text
1013
  }), 502
 
 
1014
  try:
1015
  api_data = response.json()
1016
  except ValueError:
1017
  api_data = response.text
 
 
1018
  if isinstance(api_data, dict):
1019
+ ehr_result = api_data.get("result") or api_data
1020
  else:
1021
+ ehr_result = api_data
1022
+ chartsummarydtl = ehr_result.get("chartsummarydtl") if isinstance(ehr_result, dict) else None
1023
+ if not chartsummarydtl:
1024
+ return jsonify({"error": "Missing chartsummarydtl in EHR response"}), 500
1025
+ visits = parse_ehr_chartsummarydtl(chartsummarydtl)
1026
+ # Per-patient state (in-memory)
1027
+ with state_lock:
1028
+ patient_state = state.setdefault(patientid, {"visits": [], "last_summary": ""})
1029
+ old_visits = patient_state["visits"]
1030
+ old_summary = patient_state["last_summary"]
1031
+ delta = compute_deltas(old_visits, visits)
1032
+ all_visits = visits_sorted(old_visits + visits)
1033
+ baseline = build_compact_baseline(all_visits)
1034
+ delta_text = delta_to_text(delta)
1035
+ prompt = build_main_prompt(old_summary, baseline, delta_text)
1036
+ # Model selection logic (supporting OpenVINO and HuggingFace)
1037
+ pipeline = None
1038
+ loader = None
1039
+ import torch
1040
+ torch.set_num_threads(2)
1041
+ if model_type in {"text-generation", "causal-openvino"}:
1042
+ # Try to use an existing loader if available
1043
+ loader = agents.get("medical_data_extractor")
1044
+ if not loader or getattr(loader, 'model_name', None) != model_name:
1045
+ # Dynamically create OpenVINO loader if needed
1046
+ from ai_med_extract.utils.model_loader_spaces import get_openvino_pipeline
1047
+ try:
1048
+ pipeline = get_openvino_pipeline(model_name)
1049
+ except Exception as e:
1050
+ return jsonify({"error": f"Failed to load OpenVINO pipeline: {str(e)}"}), 500
1051
+ elif model_type == "summarization":
1052
+ loader = agents.get("summarizer")
1053
+ # Use loader if available
1054
+ if not pipeline and loader and hasattr(loader, "model_loader"):
1055
+ pipeline = loader.model_loader.load()
1056
+ if not pipeline:
1057
+ return jsonify({"error": "Model pipeline not available"}), 500
1058
+ inputs = pipeline.tokenizer([prompt], return_tensors="pt")
1059
+ outputs = pipeline.model.generate(**inputs, max_new_tokens=400, do_sample=False, pad_token_id=pipeline.tokenizer.eos_token_id or 32000)
1060
+ text = pipeline.tokenizer.decode(outputs[0], skip_special_tokens=True)
1061
+ new_summary = text.split("Now generate the complete, updated clinical summary with all four sections:")[-1].strip()
1062
+ with state_lock:
1063
+ patient_state["visits"] = all_visits
1064
+ patient_state["last_summary"] = new_summary
1065
+ validation_report = validate_and_compare_summaries(old_summary, new_summary, "Update")
 
 
 
1066
  return jsonify({
1067
+ "summary": new_summary,
1068
+ "validation": validation_report,
1069
+ "baseline": baseline,
1070
+ "delta": delta_text
1071
  }), 200
 
1072
  except requests.exceptions.Timeout:
1073
  return jsonify({"error": "Request to EHR API timed out"}), 504
1074
  except requests.exceptions.RequestException as e:
1075
  return jsonify({"error": f"Network error: {str(e)}"}), 503
1076
  except Exception as e:
1077
  logger.error(f"Unexpected error: {str(e)}", exc_info=True)
1078
+ return jsonify({"error": f"Internal server error: {str(e)}"}), 500
1079
 
1080
  @app.route("/")
1081
  def home():
ai_med_extract/utils/__pycache__/model_loader_spaces.cpython-311.pyc ADDED
Binary file (1.58 kB). View file
 
ai_med_extract/utils/__pycache__/openvino_summarizer_utils.cpython-311.pyc ADDED
Binary file (12.8 kB). View file
 
ai_med_extract/utils/__pycache__/patient_summary_utils.cpython-311.pyc CHANGED
Binary files a/ai_med_extract/utils/__pycache__/patient_summary_utils.cpython-311.pyc and b/ai_med_extract/utils/__pycache__/patient_summary_utils.cpython-311.pyc differ
 
ai_med_extract/utils/model_loader_spaces.py CHANGED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from transformers import AutoTokenizer
3
+ from optimum.intel.openvino import OVModelForCausalLM
4
+
5
+ class OpenVinoPipeline:
6
+ def __init__(self, model, tokenizer):
7
+ self.model = model
8
+ self.tokenizer = tokenizer
9
+
10
+ def get_openvino_pipeline(model_name: str):
11
+ """
12
+ Loads an OpenVINO CausalLM pipeline for the given model name or IR directory.
13
+ """
14
+ # If model_name is a directory, try to load IR from there; else, download and export
15
+ import os
16
+ if os.path.isdir(model_name):
17
+ model = OVModelForCausalLM.from_pretrained(model_name, compile=True, device="CPU")
18
+ tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=True)
19
+ else:
20
+ model = OVModelForCausalLM.from_pretrained(model_name, export=True, compile=True, device="CPU")
21
+ tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=True)
22
+ return OpenVinoPipeline(model, tokenizer)
ai_med_extract/utils/openvino_summarizer_utils.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import re
4
+ import textwrap
5
+ import difflib
6
+ import logging
7
+ from copy import deepcopy
8
+
9
+ def parse_ehr_chartsummarydtl(chartsummarydtl):
10
+ """
11
+ Converts EHR API chartsummarydtl list to the internal visit format expected by the summarizer.
12
+ """
13
+ visits = []
14
+ for entry in chartsummarydtl:
15
+ visit = {}
16
+ # Parse chartdate
17
+ visit["chartdate"] = entry.get("chartdate", "")[:10] # YYYY-MM-DD
18
+ # Parse vitals
19
+ vitals_dict = {}
20
+ weight = None
21
+ if "vitals" in entry:
22
+ for v in entry["vitals"]:
23
+ if ":" in v:
24
+ k, val = v.split(":", 1)
25
+ k = k.strip()
26
+ val = val.strip()
27
+ if k.lower().startswith("weight"):
28
+ weight = val
29
+ else:
30
+ vitals_dict[k] = val
31
+ visit["vitals"] = vitals_dict
32
+ if weight:
33
+ visit["weight"] = weight
34
+ # Allergies
35
+ if "allergies" in entry:
36
+ visit["allergies"] = entry["allergies"]
37
+ # Diagnosis
38
+ if "diagnosis" in entry:
39
+ visit["diagnosis"] = entry["diagnosis"]
40
+ # Medications
41
+ if "medications" in entry:
42
+ visit["medications"] = entry["medications"]
43
+ # Labtests
44
+ labtests = []
45
+ if "labtests" in entry:
46
+ for l in entry["labtests"]:
47
+ name = l.get("name", "")
48
+ value = l.get("value", "")
49
+ if name or value:
50
+ labtests.append({"name": name, "value": value})
51
+ visit["labtests"] = labtests
52
+ # Radiology orders
53
+ if "radiologyorders" in entry:
54
+ visit["radiologyorders"] = [r.get("name", "") for r in entry["radiologyorders"] if r.get("name")]
55
+ visits.append(visit)
56
+ return visits
57
+
58
+ # ========== PROMPT, DELTA, VALIDATION LOGIC (adapted from your script) =============
59
+ ALIASES = {("vitals","Bp(sys)(mmHg)"): [("vitals","Bp(sys)(mmHg)"), ("vitals","Bp_sys"), ("vitals","SBP")],
60
+ ("vitals","Bp(dia)(mmHg)"): [("vitals","Bp(dia)(mmHg)"), ("vitals","Bp_dia"), ("vitals","DBP")],
61
+ ("labtests","HbA1c (%)"): [("labtests","HbA1c (%)"), ("labtests","HbA1c")],
62
+ ("labtests","Creatinine Ratio"): [("labtests","Creatinine Ratio"), ("labtests","Creatinine")],
63
+ }
64
+ def visits_sorted(v):
65
+ return sorted(v, key=lambda v: v.get("chartdate", ""))
66
+ def to_float(val):
67
+ try:
68
+ s = str(val); m = re.findall(r"-?\d+\.?\d*", s)
69
+ return float(m[0]) if m else None
70
+ except: return None
71
+ def _latest_value_exact(visits, key_path):
72
+ v_sorted = visits_sorted(visits)
73
+ if not v_sorted: return None
74
+ if key_path[0] == "labtests":
75
+ for v in reversed(v_sorted):
76
+ for lab in v.get("labtests", []):
77
+ if lab.get("name") == key_path[1]: return lab.get("value")
78
+ return None
79
+ for v in reversed(v_sorted):
80
+ cur = v; ok = True
81
+ for k in key_path:
82
+ if isinstance(cur, dict) and k in cur: cur = cur[k]
83
+ else: ok=False; break
84
+ if ok: return cur
85
+ return None
86
+ def latest_value(visits, key_path):
87
+ for kp in ALIASES.get(key_path, [key_path]):
88
+ val = _latest_value_exact(visits, kp)
89
+ if val is not None: return val
90
+ return None
91
+ def active_set(visits, field):
92
+ s = set()
93
+ for v in visits: s.update(v.get(field, []))
94
+ return s
95
+ def _fmt(x, spec=None):
96
+ if x is None: return "N/A"
97
+ try: return format(x, spec) if spec else str(x)
98
+ except Exception: return str(x)
99
+ def compute_deltas(old_visits, new_visits):
100
+ prev_all = old_visits
101
+ curr_all = old_visits + new_visits
102
+ def get_val(visits, path): return to_float(latest_value(visits, path))
103
+ w_p, w_c = get_val(prev_all, ("weight",)), get_val(curr_all, ("weight",))
104
+ s_p, s_c = get_val(prev_all, ("vitals","Bp(sys)(mmHg)")), get_val(curr_all, ("vitals","Bp(sys)(mmHg)"))
105
+ d_p, d_c = get_val(prev_all, ("vitals","Bp(dia)(mmHg)")), get_val(curr_all, ("vitals","Bp(dia)(mmHg)"))
106
+ h_p, h_c = get_val(prev_all, ("labtests","HbA1c (%)")), get_val(curr_all, ("labtests","HbA1c (%)"))
107
+ c_p, c_c = get_val(prev_all, ("labtests","Creatinine Ratio")), get_val(curr_all, ("labtests","Creatinine Ratio"))
108
+ return {
109
+ "added_dx": sorted(list(active_set(curr_all,"diagnosis") - active_set(prev_all,"diagnosis"))),
110
+ "started_meds": sorted(list(active_set(curr_all,"medications") - active_set(prev_all,"medications"))),
111
+ "stopped_meds": sorted(list(active_set(prev_all,"medications") - active_set(curr_all,"medications"))),
112
+ "weight": {"prev": w_p, "curr": w_c, "delta": (w_c - w_p) if w_p and w_c else None},
113
+ "bp_sys": {"prev": s_p, "curr": s_c, "delta": (s_c - s_p) if s_p and s_c else None},
114
+ "bp_dia": {"prev": d_p, "curr": d_c, "delta": (d_c - d_p) if d_p and d_c else None},
115
+ "hba1c": {"prev": h_p, "curr": h_c, "delta": (h_c - h_p) if h_p and h_c else None},
116
+ "cratio": {"prev": c_p, "curr": c_c, "delta": (c_c - c_p) if c_p and c_c else None},
117
+ }
118
+ def build_compact_baseline(all_visits):
119
+ return f"Latest date: {latest_value(all_visits,('chartdate',)) or 'N/A'}\n" \
120
+ f"Active Diagnoses: {', '.join(sorted(active_set(all_visits,'diagnosis'))) or 'N/A'}\n" \
121
+ f"Active Medications: {', '.join(sorted(active_set(all_visits,'medications'))) or 'N/A'}\n" \
122
+ f"Latest Vitals: Bp: {latest_value(all_visits,('vitals','Bp(sys)(mmHg)'))}/{latest_value(all_visits,('vitals','Bp(dia)(mmHg)'))} mmHg, Weight: {latest_value(all_visits,('weight',))}\n" \
123
+ f"Latest Labs: HbA1c: {latest_value(all_visits,('labtests','HbA1c (%)'))}%, Creatinine: {latest_value(all_visits,('labtests','Creatinine Ratio'))}"
124
+ def delta_to_text(delta):
125
+ L = []
126
+ if delta["added_dx"]: L.append("New Diagnoses: " + ", ".join(delta["added_dx"]))
127
+ if delta["started_meds"]: L.append("Medications Started: " + ", ".join(delta["started_meds"]))
128
+ if delta["stopped_meds"]: L.append("Medications Stopped: " + ", ".join(delta["stopped_meds"]))
129
+ w = delta["weight"]; L.append(f"Weight: {_fmt(w['prev'])} -> {_fmt(w['curr'])} (Δ {_fmt(w['delta'], '+.1f')})")
130
+ s, d = delta["bp_sys"], delta["bp_dia"]; L.append(f"BP: {_fmt(s['curr'])}/{_fmt(d['curr'])} (Δs {_fmt(s['delta'], '+.0f')}, Δd {_fmt(d['delta'], '+.0f')})")
131
+ h, c = delta["hba1c"], delta["cratio"]; L.append(f"HbA1c: {_fmt(h['prev'])} -> {_fmt(h['curr'])} (Δ {_fmt(h['delta'], '.1f')})"); L.append(f"Creatinine: {_fmt(c['prev'])} -> {_fmt(c['curr'])} (Δ {_fmt(c['delta'], '.1f')})")
132
+ return "\n".join(L)
133
+ def build_main_prompt(prev_summary, baseline, delta_text):
134
+ prev_excerpt = textwrap.shorten(prev_summary, width=700, placeholder="...") if prev_summary else "None"
135
+ return (
136
+ "You are an expert clinical AI assistant. Your task is to update a patient summary.\n"
137
+ "Use the PRIOR SUMMARY for context. The STRUCTURED BASELINE and DELTAS are the absolute ground truth.\n"
138
+ "Produce a concise, physician-ready update. Never omit critical new information from the deltas.\n\n"
139
+ "The summary MUST have four sections:\n"
140
+ "1) Clinical Assessment\n"
141
+ "2) Key Trends & Changes\n"
142
+ "3) Plan & Suggested Actions\n"
143
+ "4) Direct Guidance for Physician\n\n"
144
+ f"PRIOR SUMMARY (context):\n{prev_excerpt}\n\n"
145
+ f"STRUCTURED BASELINE (authoritative):\n{baseline}\n\n"
146
+ f"STRUCTURED DELTAS (authoritative):\n{delta_text}\n\n"
147
+ "Now generate the complete, updated clinical summary with all four sections:"
148
+ )
149
+ def validate_and_compare_summaries(old_summary, new_summary, update_name=""):
150
+ report = f"### Validation Report for {update_name}\n"
151
+ report += "This report validates that the updated summary incorporates new information correctly.\n"
152
+ report += "\n**Unified Diff (Line-by-Line Changes):**\n"
153
+ diff = difflib.unified_diff(
154
+ old_summary.splitlines(), new_summary.splitlines(),
155
+ fromfile='Previous Summary', tofile='Current Summary', lineterm=''
156
+ )
157
+ diff_text = "\n".join(list(diff))
158
+ if not diff_text:
159
+ report += "No textual differences found between summaries.\n"
160
+ else:
161
+ report += "```diff\n" + diff_text + "\n```\n"
162
+ return report
export_phi3_openvino.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from optimum.intel.openvino import OVModelForCausalLM
2
+ from transformers import AutoTokenizer
3
+
4
+ model_id = "microsoft/Phi-3-mini-4k-instruct"
5
+ export_dir = "ov_models/microsoft_Phi-3-mini-4k-instruct_ir"
6
+
7
+ tokenizer = AutoTokenizer.from_pretrained(model_id, library_name="transformers")
8
+ model = OVModelForCausalLM.from_pretrained(model_id, export=True, save_dir=export_dir)
9
+ tokenizer.save_pretrained(export_dir)
old_requirements.txt ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ about-time==4.2.1
2
+ accelerate==0.25.0
3
+ aiofiles==23.2.1
4
+ aiohappyeyeballs==2.6.1
5
+ aiohttp==3.12.15
6
+ aiosignal==1.4.0
7
+ alive-progress==3.3.0
8
+ altair==5.5.0
9
+ annotated-types==0.7.0
10
+ anyio==4.10.0
11
+ attrs==25.3.0
12
+ autograd==1.8.0
13
+ bitsandbytes==0.47.0
14
+ blinker==1.9.0
15
+ blis==0.7.11
16
+ catalogue==2.0.10
17
+ certifi==2025.8.3
18
+ cffi==1.17.1
19
+ charset-normalizer==3.4.3
20
+ click==8.2.1
21
+ cloudpathlib==0.16.0
22
+ cma==4.3.0
23
+ colorama==0.4.6
24
+ confection==0.1.5
25
+ contourpy==1.3.2
26
+ cryptography==45.0.6
27
+ cycler==0.12.1
28
+ cymem==2.0.11
29
+ datasets==4.0.0
30
+ Deprecated==1.2.18
31
+ dill==0.3.8
32
+ einops==0.7.0
33
+ en-core-web-sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.7.1/en_core_web_sm-3.7.1-py3-none-any.whl#sha256=86cc141f63942d4b2c5fcee06630fd6f904788d2f0ab005cce45aadb8fb73889
34
+ fastapi==0.116.1
35
+ ffmpy==0.6.1
36
+ filelock==3.18.0
37
+ Flask==3.1.0
38
+ flask-cors==5.0.1
39
+ fonttools==4.59.0
40
+ frozenlist==1.7.0
41
+ fsspec==2025.3.0
42
+ gradio==4.13.0
43
+ gradio_client==0.8.0
44
+ graphemeu==0.7.2
45
+ gunicorn==21.2.0
46
+ h11==0.16.0
47
+ httpcore==1.0.9
48
+ httpx==0.28.1
49
+ huggingface-hub==0.34.4
50
+ idna==3.10
51
+ importlib_resources==6.5.2
52
+ intel-openmp==2021.4.0
53
+ itsdangerous==2.2.0
54
+ Jinja2==3.1.6
55
+ joblib==1.5.1
56
+ jsonschema==4.25.0
57
+ jsonschema-specifications==2025.4.1
58
+ kiwisolver==1.4.9
59
+ langcodes==3.5.0
60
+ language_data==1.3.0
61
+ llvmlite==0.44.0
62
+ lxml==6.0.0
63
+ marisa-trie==1.2.1
64
+ markdown-it-py==4.0.0
65
+ MarkupSafe==2.1.5
66
+ matplotlib==3.10.5
67
+ mdurl==0.1.2
68
+ mkl==2021.4.0
69
+ more-itertools==10.7.0
70
+ mpmath==1.3.0
71
+ multidict==6.6.4
72
+ multiprocess==0.70.16
73
+ murmurhash==1.0.13
74
+ narwhals==2.1.1
75
+ natsort==8.4.0
76
+ networkx==3.4.2
77
+ ninja==1.11.1.4
78
+ nltk==3.8.1
79
+ nncf==2.17.0
80
+ numba==0.61.2
81
+ numpy==1.24.3
82
+ onnx==1.18.0
83
+ openai-whisper==20231117
84
+ opencv-python-headless==4.8.1.78
85
+ openvino==2025.2.0
86
+ openvino-telemetry==2025.2.0
87
+ openvino-tokenizers==2025.2.0.1
88
+ optimum==1.27.0
89
+ optimum-intel==1.25.2
90
+ orjson==3.11.2
91
+ packaging==25.0
92
+ pandas==2.1.4
93
+ pdf2image==1.16.3
94
+ pdfminer.six==20221105
95
+ pdfplumber==0.10.3
96
+ Pillow==10.1.0
97
+ preshed==3.0.10
98
+ propcache==0.3.2
99
+ protobuf==4.25.1
100
+ psutil==7.0.0
101
+ pyarrow==21.0.0
102
+ pycparser==2.22
103
+ pydantic==2.11.7
104
+ pydantic_core==2.33.2
105
+ pydot==3.0.4
106
+ pydub==0.25.1
107
+ Pygments==2.19.2
108
+ pymoo==0.6.1.5
109
+ pyparsing==3.2.3
110
+ PyPDF2==3.0.1
111
+ pypdfium2==4.30.0
112
+ pytesseract==0.3.10
113
+ python-dateutil==2.9.0.post0
114
+ python-docx==1.0.1
115
+ python-dotenv==1.0.1
116
+ python-multipart==0.0.20
117
+ pytz==2025.2
118
+ PyYAML==6.0.2
119
+ referencing==0.36.2
120
+ regex==2025.7.34
121
+ requests==2.32.5
122
+ rich==13.9.4
123
+ rpds-py==0.27.0
124
+ safetensors==0.6.2
125
+ scikit-learn==1.3.2
126
+ scipy==1.11.4
127
+ semantic-version==2.10.0
128
+ sentence-transformers==5.1.0
129
+ sentencepiece==0.1.99
130
+ shellingham==1.5.4
131
+ six==1.17.0
132
+ smart-open==6.4.0
133
+ sniffio==1.3.1
134
+ spacy==3.7.2
135
+ spacy-legacy==3.0.12
136
+ spacy-loggers==1.0.5
137
+ srsly==2.5.1
138
+ starlette==0.47.2
139
+ sympy==1.14.0
140
+ tabulate==0.9.0
141
+ tbb==2021.13.1
142
+ termcolor==3.1.0
143
+ thinc==8.2.5
144
+ threadpoolctl==3.6.0
145
+ tiktoken==0.5.2
146
+ tokenizers==0.21.4
147
+ tomlkit==0.12.0
148
+ torch==2.3.0
149
+ torchaudio==2.3.0
150
+ torchvision==0.18.0
151
+ tqdm==4.67.1
152
+ transformers==4.53.3
153
+ typer==0.9.4
154
+ typing-inspection==0.4.1
155
+ typing_extensions==4.14.1
156
+ tzdata==2025.2
157
+ urllib3==2.5.0
158
+ uvicorn==0.35.0
159
+ wasabi==1.1.3
160
+ weasel==0.3.4
161
+ websockets==11.0.3
162
+ Werkzeug==3.1.3
163
+ wrapt==1.17.3
164
+ xxhash==3.5.0
165
+ yarl==1.20.1
ov_models/microsoft_Phi-3-mini-4k-instruct_ir/chat_template.jinja ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {% for message in messages %}{% if message['role'] == 'system' %}{{'<|system|>
2
+ ' + message['content'] + '<|end|>
3
+ '}}{% elif message['role'] == 'user' %}{{'<|user|>
4
+ ' + message['content'] + '<|end|>
5
+ '}}{% elif message['role'] == 'assistant' %}{{'<|assistant|>
6
+ ' + message['content'] + '<|end|>
7
+ '}}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|assistant|>
8
+ ' }}{% else %}{{ eos_token }}{% endif %}
ov_models/microsoft_Phi-3-mini-4k-instruct_ir/special_tokens_map.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "bos_token": {
3
+ "content": "<s>",
4
+ "lstrip": false,
5
+ "normalized": false,
6
+ "rstrip": false,
7
+ "single_word": false
8
+ },
9
+ "eos_token": {
10
+ "content": "<|endoftext|>",
11
+ "lstrip": false,
12
+ "normalized": false,
13
+ "rstrip": false,
14
+ "single_word": false
15
+ },
16
+ "pad_token": {
17
+ "content": "<|endoftext|>",
18
+ "lstrip": false,
19
+ "normalized": false,
20
+ "rstrip": false,
21
+ "single_word": false
22
+ },
23
+ "unk_token": {
24
+ "content": "<unk>",
25
+ "lstrip": false,
26
+ "normalized": false,
27
+ "rstrip": false,
28
+ "single_word": false
29
+ }
30
+ }
ov_models/microsoft_Phi-3-mini-4k-instruct_ir/tokenizer.json.REMOVED.git-id ADDED
@@ -0,0 +1 @@
 
 
1
+ 759de6dd15d187c9ececdea11d3287d4cb4b604a