WebashalarForML commited on
Commit
099031b
·
verified ·
1 Parent(s): aed0c9b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +101 -81
app.py CHANGED
@@ -45,7 +45,6 @@ llm = ChatGroq(
45
  )
46
 
47
  def clean_notes_with_bloatectomy(text: str, style: str = "remov") -> str:
48
- """Helper function to clean up text using the bloatectomy library."""
49
  try:
50
  b = bloatectomy(text, style=style, output="html")
51
  tokens = getattr(b, "tokens", None)
@@ -56,18 +55,18 @@ def clean_notes_with_bloatectomy(text: str, style: str = "remov") -> str:
56
  logger.exception("Bloatectomy cleaning failed; returning original text")
57
  return text
58
 
59
- # --- Agent prompt instructions ---
60
- # Important change: the assistant should NOT insist on PID at the start of conversation.
61
- # It should only ask for patient ID when the user asks for something that requires accessing previous medical records.
62
- # Also, avoid asking for additional identity details (name/DOB) purely for "verification" — accept PID as sufficient for record access in this flow.
63
  PATIENT_ASSISTANT_PROMPT = """
64
- You are a patient assistant helping to analyze medical records and reports. You should be helpful for general medical questions even when no patient ID (PID) is provided.
65
 
66
  Behavior rules (follow these strictly):
67
  - Do NOT ask for the patient ID at the start of every conversation. Only request the PID when the user's question explicitly requires accessing prior medical records (for example: "show my previous lab report", "what does my thyroid test from last month say", "what was my doctor's note for PID 12345", etc.).
68
  - When you do ask for a PID, be concise and ask only for the PID (e.g., "Please provide the patient ID (PID) to retrieve previous records."). Do not request name/DOB/other verification unless the user explicitly asks for an extra verification step.
69
  - If the user supplies a PID in their message (patterns like "pid 5678", "p5678", "patient id: 5678"), accept and use it — do not ask again.
70
- - Avoid repeating unnecessary clarifying questions. If the user has already given the PID, proceed to use it. If you previously asked for the PID and the user didn't provide it, ask once more succinctly and then offer to help with general guidance without records.
 
 
 
 
71
 
72
  STRICT OUTPUT FORMAT (JSON ONLY):
73
  Return a single JSON object with the following keys:
@@ -82,9 +81,8 @@ Rules:
82
  - Do not make up information that is not present in the provided medical reports or conversation history.
83
  """
84
 
85
- # --- Helper utilities ---
86
  PID_PATTERN = re.compile(r"(?:\bpid\b|\bpatient\s*id\b|\bp\b)\s*[:#\-]?\s*(p?\d+)", re.IGNORECASE)
87
- DIGIT_PATTERN = re.compile(r"\b(p?\d{3,})\b") # fallback: any 3+ digit cluster
88
 
89
  RECORD_KEYWORDS = [
90
  "report", "lab", "result", "results", "previous", "history", "record", "records",
@@ -92,58 +90,39 @@ RECORD_KEYWORDS = [
92
  "prescription", "doctor", "referral", "visit", "consultation",
93
  ]
94
 
95
-
96
  def extract_pid_from_text(text: str) -> str | None:
97
- """Try to extract PID from a free-form text string.
98
- Accepts patterns like: 'pid 5678', 'p5678', 'patient id: 5678', or a bare number if clearly intended as an id.
99
- """
100
  if not text:
101
  return None
102
  m = PID_PATTERN.search(text)
103
  if m:
104
  return m.group(1).lstrip('pP')
105
- # fallback: look for explicit mention like 'pid p5678' with different casing
106
- # final fallback: any 3+ digit cluster but only if message also contains a record keyword
107
  if any(k in text.lower() for k in RECORD_KEYWORDS):
108
  m2 = DIGIT_PATTERN.search(text)
109
  if m2:
110
  return m2.group(1).lstrip('pP')
111
  return None
112
 
113
-
114
  def needs_pid_for_query(text: str) -> bool:
115
- """Decide whether the user's message requires looking up prior records (thus needs a PID).
116
- Simple heuristic: if message contains any keyword from RECORD_KEYWORDS or explicit phrases asking for previous tests/records.
117
- """
118
  if not text:
119
  return False
120
  lower = text.lower()
121
- # direct phrases that clearly require historical records
122
  phrases = ["previous report", "previous lab", "my report", "my records", "past report", "last report", "previous test", "previous results"]
123
  if any(p in lower for p in phrases):
124
  return True
125
- # if message contains any record-related keyword, treat as needing PID
126
  if any(k in lower for k in RECORD_KEYWORDS):
127
  return True
128
  return False
129
 
130
- # --- JSON extraction helper (unchanged) ---
131
  def extract_json_from_llm_response(raw_response: str) -> dict:
132
- """Safely extracts a JSON object from a string that might contain extra text or markdown."""
133
  default = {
134
  "assistant_reply": "I'm sorry — I couldn't understand that. Could you please rephrase?",
135
  "patientDetails": {},
136
  "conversationSummary": "",
137
  }
138
-
139
  if not raw_response or not isinstance(raw_response, str):
140
  return default
141
-
142
- # Find the JSON object, ignoring any markdown code fences
143
  m = re.search(r"```(?:json)?\s*([\s\S]*?)\s*```", raw_response)
144
  json_string = m.group(1).strip() if m else raw_response
145
-
146
- # Find the first opening brace and the last closing brace
147
  first = json_string.find('{')
148
  last = json_string.rfind('}')
149
  if first == -1 or last == -1 or first >= last:
@@ -152,18 +131,13 @@ def extract_json_from_llm_response(raw_response: str) -> dict:
152
  except Exception:
153
  logger.warning("Could not locate JSON braces in LLM output. Falling back to default.")
154
  return default
155
-
156
  candidate = json_string[first:last+1]
157
- # Remove trailing commas that might cause parsing issues
158
  candidate = re.sub(r',\s*(?=[}\]])', '', candidate)
159
-
160
  try:
161
  parsed = json.loads(candidate)
162
  except Exception as e:
163
  logger.warning("Failed to parse JSON from LLM output: %s", e)
164
  return default
165
-
166
- # Basic validation of the parsed JSON
167
  if isinstance(parsed, dict) and "assistant_reply" in parsed and isinstance(parsed["assistant_reply"], str) and parsed["assistant_reply"].strip():
168
  parsed.setdefault("patientDetails", {})
169
  parsed.setdefault("conversationSummary", "")
@@ -172,27 +146,47 @@ def extract_json_from_llm_response(raw_response: str) -> dict:
172
  logger.warning("Parsed JSON missing 'assistant_reply' or invalid format. Returning default.")
173
  return default
174
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  # --- Flask routes ---
176
  @app.route("/", methods=["GET"])
177
  def serve_frontend():
178
- """Serves the frontend HTML file."""
179
  try:
180
- return app.send_static_file("frontend.html")
181
  except Exception:
182
  return "<h3>frontend.html not found in static/ — please add your frontend.html there.</h3>", 404
183
 
184
  @app.route("/upload_report", methods=["POST"])
185
  def upload_report():
186
- """Handles the upload of a new PDF report for a specific patient."""
187
  if 'report' not in request.files:
188
  return jsonify({"error": "No file part in the request"}), 400
189
-
190
  file = request.files['report']
191
  patient_id = request.form.get("patient_id")
192
-
193
  if file.filename == '' or not patient_id:
194
  return jsonify({"error": "No selected file or patient ID"}), 400
195
-
196
  if file:
197
  filename = secure_filename(file.filename)
198
  patient_folder = REPORTS_ROOT / f"{patient_id}"
@@ -203,41 +197,57 @@ def upload_report():
203
 
204
  @app.route("/chat", methods=["POST"])
205
  def chat():
206
- """Handles the chat conversation with the assistant."""
207
  data = request.get_json(force=True)
208
  if not isinstance(data, dict):
209
  return jsonify({"error": "invalid request body"}), 400
210
 
211
  chat_history = data.get("chat_history") or []
212
  patient_state = data.get("patient_state") or {}
213
- patient_id = patient_state.get("patientDetails", {}).get("pid")
 
214
 
215
- # --- Prepare the state for the LLM ---
216
  state = patient_state.copy()
217
  state.setdefault("asked_for_pid", False)
218
  state.setdefault("conversationSummary", state.get("conversationSummary", ""))
219
  state["lastUserMessage"] = ""
220
  if chat_history:
221
- # Find the last user message
222
  for msg in reversed(chat_history):
223
  if msg.get("role") == "user" and msg.get("content"):
224
  state["lastUserMessage"] = msg["content"]
225
  break
226
 
227
- # Try to infer PID from the last message (user might include it inline)
228
  inferred_pid = extract_pid_from_text(state.get("lastUserMessage", "") or "")
229
- if not patient_id and inferred_pid:
 
230
  logger.info("Inferred PID from user message: %s", inferred_pid)
231
  state.setdefault("patientDetails", {})["pid"] = inferred_pid
232
  patient_id = inferred_pid
233
 
234
  combined_text_parts = []
235
 
236
- # Decide whether this query actually needs patient records
237
  wants_records = needs_pid_for_query(state.get("lastUserMessage", "") or "")
238
 
239
- # If a PID is known, load the patient reports.
240
- if patient_id:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  patient_folder = REPORTS_ROOT / f"{patient_id}"
242
  if patient_folder.exists() and patient_folder.is_dir():
243
  for fname in sorted(os.listdir(patient_folder)):
@@ -260,26 +270,17 @@ def chat():
260
  if cleaned:
261
  combined_text_parts.append(cleaned)
262
 
263
- # Update the conversation summary with the parsed documents
264
- base_summary = state.get("conversationSummary", "") or ""
265
- docs_summary = "\n\n".join(combined_text_parts)
266
- if docs_summary:
267
- state["conversationSummary"] = (base_summary + "\n\n" + docs_summary).strip()
268
- else:
269
- state["conversationSummary"] = base_summary
270
 
271
- # Build the user prompt for the LLM. We explicitly tell the LLM whether a PID is available and whether the user's message requires records.
272
- if patient_id:
273
  action_hint = f"Use the patient ID {patient_id} to retrieve and summarize any relevant reports."
274
  else:
275
- if wants_records and not state.get("asked_for_pid", False):
276
- action_hint = "The user is asking for information that requires prior medical records. Ask succinctly for the Patient ID (PID) if needed."
277
- # mark that we asked for PID so we don't repeatedly ask
278
- state["asked_for_pid"] = True
279
- elif wants_records and state.get("asked_for_pid", False):
280
- action_hint = "The user previously was asked for a PID but has not supplied one. Ask once more concisely for the PID; otherwise offer to help with general guidance without accessing records."
281
- else:
282
- action_hint = "No PID provided and the user's request does not need prior records. Provide helpful, general medical guidance and offer to retrieve records if the user later supplies a PID."
283
 
284
  user_prompt = f"""
285
  Current patientDetails: {json.dumps(state.get("patientDetails", {}))}
@@ -318,12 +319,45 @@ Return ONLY valid JSON with keys: assistant_reply, patientDetails, conversationS
318
  state.setdefault("patientDetails", {}).update(updated_state.get("patientDetails", {}))
319
  state["conversationSummary"] = updated_state.get("conversationSummary", state.get("conversationSummary", ""))
320
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  assistant_reply = updated_state.get("assistant_reply")
322
  if not assistant_reply or not isinstance(assistant_reply, str) or not assistant_reply.strip():
323
- # Fallback to a polite message if the LLM response is invalid or empty
324
  assistant_reply = "I'm here to help — could you tell me more about your symptoms or provide a Patient ID (PID) if you want me to fetch past reports?"
325
 
326
- # Return the new assistant reply and the updated state so the frontend can persist it.
327
  response_payload = {
328
  "assistant_reply": assistant_reply,
329
  "updated_state": state,
@@ -333,23 +367,13 @@ Return ONLY valid JSON with keys: assistant_reply, patientDetails, conversationS
333
 
334
  @app.route("/upload_reports", methods=["POST"])
335
  def upload_reports():
336
- """
337
- Upload one or more files for a patient.
338
-
339
- Expects multipart/form-data with:
340
- - patient_id (form field)
341
- - files (one or multiple files; use the same field name 'files' for each file)
342
- """
343
  try:
344
- # patient id can be in form or args (for convenience)
345
  patient_id = request.form.get("patient_id") or request.args.get("patient_id")
346
  if not patient_id:
347
  return jsonify({"error": "patient_id form field required"}), 400
348
 
349
- # get uploaded files (support both files and files[] naming)
350
  uploaded_files = request.files.getlist("files")
351
  if not uploaded_files:
352
- # fallback: single file under name 'file'
353
  single = request.files.get("file")
354
  if single:
355
  uploaded_files = [single]
@@ -357,7 +381,6 @@ def upload_reports():
357
  if not uploaded_files:
358
  return jsonify({"error": "no files uploaded (use form field 'files')"}), 400
359
 
360
- # create patient folder under REPORTS_ROOT/<patient_id>
361
  patient_folder = REPORTS_ROOT / str(patient_id)
362
  patient_folder.mkdir(parents=True, exist_ok=True)
363
 
@@ -371,17 +394,14 @@ def upload_reports():
371
  skipped.append({"filename": orig_name, "reason": "invalid filename"})
372
  continue
373
 
374
- # extension check
375
  ext = filename.rsplit(".", 1)[1].lower() if "." in filename else ""
376
  if ext not in ALLOWED_EXTENSIONS:
377
  skipped.append({"filename": filename, "reason": f"extension '{ext}' not allowed"})
378
  continue
379
 
380
- # avoid overwriting: if collision, add numeric suffix
381
  dest = patient_folder / filename
382
  if dest.exists():
383
  base, dot, extension = filename.rpartition(".")
384
- # if no base (e.g. ".bashrc") fallback
385
  base = base or filename
386
  i = 1
387
  while True:
 
45
  )
46
 
47
  def clean_notes_with_bloatectomy(text: str, style: str = "remov") -> str:
 
48
  try:
49
  b = bloatectomy(text, style=style, output="html")
50
  tokens = getattr(b, "tokens", None)
 
55
  logger.exception("Bloatectomy cleaning failed; returning original text")
56
  return text
57
 
 
 
 
 
58
  PATIENT_ASSISTANT_PROMPT = """
59
+ You are a helpful medical assistant acting as a doctor. You respond naturally to greetings and general medical questions without asking for patient ID unless the user requests information about prior medical records
60
 
61
  Behavior rules (follow these strictly):
62
  - Do NOT ask for the patient ID at the start of every conversation. Only request the PID when the user's question explicitly requires accessing prior medical records (for example: "show my previous lab report", "what does my thyroid test from last month say", "what was my doctor's note for PID 12345", etc.).
63
  - When you do ask for a PID, be concise and ask only for the PID (e.g., "Please provide the patient ID (PID) to retrieve previous records."). Do not request name/DOB/other verification unless the user explicitly asks for an extra verification step.
64
  - If the user supplies a PID in their message (patterns like "pid 5678", "p5678", "patient id: 5678"), accept and use it — do not ask again.
65
+ - Never ask for the PID if it is already known. If the user provides a different PID later, update it and proceed accordingly.
66
+ - Avoid repeating unnecessary clarifying questions. If you previously asked for the PID and the user didn't provide it, ask once more succinctly and then offer to help with general guidance without records.
67
+ - When analyzing medical reports, trust the patient ID from the folder or query context as the source of truth.
68
+ - **If the report text mentions a different patient ID or name, do not refuse to answer but mention the discrepancy politely and proceed to answer based on the available data.**
69
+ - **Always protect patient privacy and avoid sharing information from reports not matching the current PID unless explicitly requested and with a clear disclaimer.**
70
 
71
  STRICT OUTPUT FORMAT (JSON ONLY):
72
  Return a single JSON object with the following keys:
 
81
  - Do not make up information that is not present in the provided medical reports or conversation history.
82
  """
83
 
 
84
  PID_PATTERN = re.compile(r"(?:\bpid\b|\bpatient\s*id\b|\bp\b)\s*[:#\-]?\s*(p?\d+)", re.IGNORECASE)
85
+ DIGIT_PATTERN = re.compile(r"\b(p?\d{3,})\b")
86
 
87
  RECORD_KEYWORDS = [
88
  "report", "lab", "result", "results", "previous", "history", "record", "records",
 
90
  "prescription", "doctor", "referral", "visit", "consultation",
91
  ]
92
 
 
93
  def extract_pid_from_text(text: str) -> str | None:
 
 
 
94
  if not text:
95
  return None
96
  m = PID_PATTERN.search(text)
97
  if m:
98
  return m.group(1).lstrip('pP')
 
 
99
  if any(k in text.lower() for k in RECORD_KEYWORDS):
100
  m2 = DIGIT_PATTERN.search(text)
101
  if m2:
102
  return m2.group(1).lstrip('pP')
103
  return None
104
 
 
105
  def needs_pid_for_query(text: str) -> bool:
 
 
 
106
  if not text:
107
  return False
108
  lower = text.lower()
 
109
  phrases = ["previous report", "previous lab", "my report", "my records", "past report", "last report", "previous test", "previous results"]
110
  if any(p in lower for p in phrases):
111
  return True
 
112
  if any(k in lower for k in RECORD_KEYWORDS):
113
  return True
114
  return False
115
 
 
116
  def extract_json_from_llm_response(raw_response: str) -> dict:
 
117
  default = {
118
  "assistant_reply": "I'm sorry — I couldn't understand that. Could you please rephrase?",
119
  "patientDetails": {},
120
  "conversationSummary": "",
121
  }
 
122
  if not raw_response or not isinstance(raw_response, str):
123
  return default
 
 
124
  m = re.search(r"```(?:json)?\s*([\s\S]*?)\s*```", raw_response)
125
  json_string = m.group(1).strip() if m else raw_response
 
 
126
  first = json_string.find('{')
127
  last = json_string.rfind('}')
128
  if first == -1 or last == -1 or first >= last:
 
131
  except Exception:
132
  logger.warning("Could not locate JSON braces in LLM output. Falling back to default.")
133
  return default
 
134
  candidate = json_string[first:last+1]
 
135
  candidate = re.sub(r',\s*(?=[}\]])', '', candidate)
 
136
  try:
137
  parsed = json.loads(candidate)
138
  except Exception as e:
139
  logger.warning("Failed to parse JSON from LLM output: %s", e)
140
  return default
 
 
141
  if isinstance(parsed, dict) and "assistant_reply" in parsed and isinstance(parsed["assistant_reply"], str) and parsed["assistant_reply"].strip():
142
  parsed.setdefault("patientDetails", {})
143
  parsed.setdefault("conversationSummary", "")
 
146
  logger.warning("Parsed JSON missing 'assistant_reply' or invalid format. Returning default.")
147
  return default
148
 
149
+ def extract_details_from_user_message(user_message: str) -> dict:
150
+ """
151
+ Use the LLM to extract patient details (name, contact, city, problem) from the user's last message.
152
+ Returns a dict with any found fields.
153
+ """
154
+ extraction_prompt = f"""
155
+ Extract any patient details from the following user message. Return a JSON object with keys name, contact, city, problem.
156
+ If a field is not present, omit it.
157
+
158
+ User message:
159
+ \"\"\"{user_message}\"\"\"
160
+ """
161
+ messages = [
162
+ {"role": "system", "content": "You are a helpful assistant that extracts patient details from user messages."},
163
+ {"role": "user", "content": extraction_prompt}
164
+ ]
165
+ try:
166
+ response = llm.invoke(messages)
167
+ content = response.content if hasattr(response, "content") else str(response)
168
+ extracted = extract_json_from_llm_response(content)
169
+ return extracted.get("patientDetails", extracted) # support both keys
170
+ except Exception as e:
171
+ logger.warning(f"Detail extraction failed: {e}")
172
+ return {}
173
+
174
  # --- Flask routes ---
175
  @app.route("/", methods=["GET"])
176
  def serve_frontend():
 
177
  try:
178
+ return app.send_static_file("front1.html")
179
  except Exception:
180
  return "<h3>frontend.html not found in static/ — please add your frontend.html there.</h3>", 404
181
 
182
  @app.route("/upload_report", methods=["POST"])
183
  def upload_report():
 
184
  if 'report' not in request.files:
185
  return jsonify({"error": "No file part in the request"}), 400
 
186
  file = request.files['report']
187
  patient_id = request.form.get("patient_id")
 
188
  if file.filename == '' or not patient_id:
189
  return jsonify({"error": "No selected file or patient ID"}), 400
 
190
  if file:
191
  filename = secure_filename(file.filename)
192
  patient_folder = REPORTS_ROOT / f"{patient_id}"
 
197
 
198
  @app.route("/chat", methods=["POST"])
199
  def chat():
 
200
  data = request.get_json(force=True)
201
  if not isinstance(data, dict):
202
  return jsonify({"error": "invalid request body"}), 400
203
 
204
  chat_history = data.get("chat_history") or []
205
  patient_state = data.get("patient_state") or {}
206
+ patient_details = patient_state.get("patientDetails", {})
207
+ patient_id = patient_details.get("pid")
208
 
 
209
  state = patient_state.copy()
210
  state.setdefault("asked_for_pid", False)
211
  state.setdefault("conversationSummary", state.get("conversationSummary", ""))
212
  state["lastUserMessage"] = ""
213
  if chat_history:
 
214
  for msg in reversed(chat_history):
215
  if msg.get("role") == "user" and msg.get("content"):
216
  state["lastUserMessage"] = msg["content"]
217
  break
218
 
 
219
  inferred_pid = extract_pid_from_text(state.get("lastUserMessage", "") or "")
220
+ patient_id_str = str(patient_id) if patient_id is not None else ""
221
+ if (not patient_id_str or patient_id_str.strip() == "") and inferred_pid:
222
  logger.info("Inferred PID from user message: %s", inferred_pid)
223
  state.setdefault("patientDetails", {})["pid"] = inferred_pid
224
  patient_id = inferred_pid
225
 
226
  combined_text_parts = []
227
 
 
228
  wants_records = needs_pid_for_query(state.get("lastUserMessage", "") or "")
229
 
230
+ if wants_records and (not patient_id or patient_id_str.strip() == ""):
231
+ if not state.get("asked_for_pid", False):
232
+ assistant_reply = "Please provide the patient ID (PID) to retrieve previous records."
233
+ state["asked_for_pid"] = True
234
+ response_payload = {
235
+ "assistant_reply": assistant_reply,
236
+ "updated_state": state,
237
+ }
238
+ return jsonify(response_payload)
239
+ else:
240
+ assistant_reply = (
241
+ "I still need your Patient ID (PID) to access your records. "
242
+ "If you prefer, I can help with general medical questions instead."
243
+ )
244
+ response_payload = {
245
+ "assistant_reply": assistant_reply,
246
+ "updated_state": state,
247
+ }
248
+ return jsonify(response_payload)
249
+
250
+ if patient_id and patient_id_str.strip() != "":
251
  patient_folder = REPORTS_ROOT / f"{patient_id}"
252
  if patient_folder.exists() and patient_folder.is_dir():
253
  for fname in sorted(os.listdir(patient_folder)):
 
270
  if cleaned:
271
  combined_text_parts.append(cleaned)
272
 
273
+ base_summary = state.get("conversationSummary", "") or ""
274
+ docs_summary = "\n\n".join(combined_text_parts)
275
+ if docs_summary:
276
+ state["conversationSummary"] = (base_summary + "\n\n" + docs_summary).strip()
277
+ else:
278
+ state["conversationSummary"] = base_summary
 
279
 
280
+ if patient_id and patient_id_str.strip() != "":
 
281
  action_hint = f"Use the patient ID {patient_id} to retrieve and summarize any relevant reports."
282
  else:
283
+ action_hint = "No PID provided and the user's request does not need prior records. Provide helpful, general medical guidance and offer to retrieve records if the user later supplies a PID."
 
 
 
 
 
 
 
284
 
285
  user_prompt = f"""
286
  Current patientDetails: {json.dumps(state.get("patientDetails", {}))}
 
319
  state.setdefault("patientDetails", {}).update(updated_state.get("patientDetails", {}))
320
  state["conversationSummary"] = updated_state.get("conversationSummary", state.get("conversationSummary", ""))
321
 
322
+ # --- New: Extract details from last user message to update patientDetails ---
323
+ REQUIRED_DETAILS = ["name", "contact", "city", "problem"]
324
+ booking_intent_keywords = ["book appointment", "schedule appointment", "make appointment", "appointment"]
325
+
326
+ last_msg_lower = state.get("lastUserMessage", "").lower()
327
+ conversation_summary_lower = state.get("conversationSummary", "").lower()
328
+
329
+ wants_to_book = any(kw in last_msg_lower for kw in booking_intent_keywords) or \
330
+ any(kw in conversation_summary_lower for kw in booking_intent_keywords)
331
+
332
+ if wants_to_book:
333
+ # Extract details from last user message
334
+ extracted_details = extract_details_from_user_message(state.get("lastUserMessage", ""))
335
+ patient_details = state.setdefault("patientDetails", {})
336
+ # Update patientDetails with any newly extracted info
337
+ for key in REQUIRED_DETAILS:
338
+ if key in extracted_details and extracted_details[key]:
339
+ patient_details[key] = extracted_details[key]
340
+
341
+ missing_fields = [field for field in REQUIRED_DETAILS if not patient_details.get(field)]
342
+ if missing_fields:
343
+ missing_field = missing_fields[0]
344
+ field_prompts = {
345
+ "name": "Could you please provide your full name?",
346
+ "contact": "May I have your contact number?",
347
+ "city": "What city are you located in?",
348
+ "problem": "Please briefly describe your medical problem or reason for the appointment.",
349
+ }
350
+ assistant_reply = field_prompts.get(missing_field, f"Please provide your {missing_field}.")
351
+ response_payload = {
352
+ "assistant_reply": assistant_reply,
353
+ "updated_state": state,
354
+ }
355
+ return jsonify(response_payload)
356
+
357
  assistant_reply = updated_state.get("assistant_reply")
358
  if not assistant_reply or not isinstance(assistant_reply, str) or not assistant_reply.strip():
 
359
  assistant_reply = "I'm here to help — could you tell me more about your symptoms or provide a Patient ID (PID) if you want me to fetch past reports?"
360
 
 
361
  response_payload = {
362
  "assistant_reply": assistant_reply,
363
  "updated_state": state,
 
367
 
368
  @app.route("/upload_reports", methods=["POST"])
369
  def upload_reports():
 
 
 
 
 
 
 
370
  try:
 
371
  patient_id = request.form.get("patient_id") or request.args.get("patient_id")
372
  if not patient_id:
373
  return jsonify({"error": "patient_id form field required"}), 400
374
 
 
375
  uploaded_files = request.files.getlist("files")
376
  if not uploaded_files:
 
377
  single = request.files.get("file")
378
  if single:
379
  uploaded_files = [single]
 
381
  if not uploaded_files:
382
  return jsonify({"error": "no files uploaded (use form field 'files')"}), 400
383
 
 
384
  patient_folder = REPORTS_ROOT / str(patient_id)
385
  patient_folder.mkdir(parents=True, exist_ok=True)
386
 
 
394
  skipped.append({"filename": orig_name, "reason": "invalid filename"})
395
  continue
396
 
 
397
  ext = filename.rsplit(".", 1)[1].lower() if "." in filename else ""
398
  if ext not in ALLOWED_EXTENSIONS:
399
  skipped.append({"filename": filename, "reason": f"extension '{ext}' not allowed"})
400
  continue
401
 
 
402
  dest = patient_folder / filename
403
  if dest.exists():
404
  base, dot, extension = filename.rpartition(".")
 
405
  base = base or filename
406
  i = 1
407
  while True: