twissamodi commited on
Commit
db33ebc
Β·
0 Parent(s):

add code for mediquery-assist

Browse files
Files changed (10) hide show
  1. .gitignore +6 -0
  2. audio_handler.py +20 -0
  3. chat_handler.py +67 -0
  4. experimentation.ipynb +601 -0
  5. graph_setup.py +57 -0
  6. main.py +83 -0
  7. prompts.py +42 -0
  8. rag_setup.py +88 -0
  9. readme.md +163 -0
  10. tools.py +29 -0
.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ .venv
2
+ .gradio
3
+ .env
4
+ data/
5
+ __pycache__
6
+ .DS_Store
audio_handler.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import pipeline
2
+
3
+
4
+ class AudioHandler:
5
+ def __init__(self):
6
+ self.transcriber = pipeline("automatic-speech-recognition", model="openai/whisper-small")
7
+
8
+ def transcribe_audio(self, audio, current_text, file_input, message_history, chat_func):
9
+ if audio is None:
10
+ return message_history, current_text, None, file_input
11
+
12
+ transcript = self.transcriber(audio)["text"].strip()
13
+
14
+ updated_history, cleared_text, cleared_file = chat_func(
15
+ transcript,
16
+ file_input,
17
+ message_history
18
+ )
19
+
20
+ return updated_history, current_text, None, cleared_file
chat_handler.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import uuid
3
+ from langgraph.errors import GraphRecursionError
4
+ from prompts import REACT_SYSTEM_PROMPT
5
+
6
+
7
+ class ChatHandler:
8
+ def __init__(self, graph, rag_setup):
9
+ self.graph = graph
10
+ self.rag = rag_setup
11
+ self.session_id = str(uuid.uuid4())
12
+ print(self.session_id)
13
+
14
+ def chat(self, user_message, uploaded_file, message_history):
15
+ user_query_parts = []
16
+ try:
17
+ if user_message and user_message.strip():
18
+ user_query_parts.append(user_message)
19
+
20
+ if uploaded_file is not None:
21
+ result = self.rag.store_data(uploaded_file)
22
+ result_str = json.dumps(result, indent=2)
23
+ user_query_parts.append(f"""A medical document was uploaded. Here are the upload details: {result_str} Please inform the user about the upload status in a friendly, professional way.""")
24
+
25
+ if not user_query_parts:
26
+ return message_history, "", None, None
27
+
28
+ user_query = (' ').join(user_query_parts)
29
+
30
+ config = {"configurable": {"thread_id": self.session_id}, "recursion_limit" : 25}
31
+ current_state = self.graph.get_state(config)
32
+
33
+ if not current_state.values.get("messages"):
34
+ messages = {
35
+ "messages": [
36
+ {"role": "system", "content": REACT_SYSTEM_PROMPT},
37
+ {"role": "user", "content": user_query}
38
+ ]
39
+ }
40
+ else:
41
+ messages = {"messages": [{"role": "user", "content": user_query}]}
42
+
43
+ result = self.graph.invoke(
44
+ messages,
45
+ config=config
46
+ )
47
+
48
+ last_message = result["messages"][-1].content
49
+
50
+ updated_history = message_history + [
51
+ {"role": "user", "content": user_message},
52
+ {"role": "assistant", "content": last_message}
53
+ ]
54
+
55
+ return updated_history, "", None
56
+
57
+ except GraphRecursionError:
58
+ error_message = "This query is too complex and exceeded the reasoning limit. Please simplify or break it into smaller questions."
59
+ return message_history + [
60
+ {"role": "assistant", "content": error_message}
61
+ ], "", None
62
+
63
+ except Exception as e:
64
+ error_message = f"Error: {str(e)}"
65
+ return message_history + [
66
+ {"role": "assistant", "content": error_message}
67
+ ], "", None
experimentation.ipynb ADDED
@@ -0,0 +1,601 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": 11,
6
+ "id": "f9c151c2",
7
+ "metadata": {},
8
+ "outputs": [],
9
+ "source": [
10
+ "from langgraph.graph import START, END, StateGraph\n",
11
+ "import sqlite3\n",
12
+ "from langgraph.checkpoint.sqlite import SqliteSaver\n",
13
+ "from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint\n",
14
+ "from langchain_huggingface.embeddings import HuggingFaceEmbeddings\n",
15
+ "from langchain.tools import tool\n",
16
+ "from langchain_community.utilities import GoogleSerperAPIWrapper\n",
17
+ "from langchain_community.document_loaders import PyPDFLoader\n",
18
+ "from typing_extensions import TypedDict, Annotated\n",
19
+ "from langgraph.graph.message import add_messages\n",
20
+ "from langgraph.prebuilt import ToolNode, tools_condition\n",
21
+ "from dotenv import load_dotenv\n",
22
+ "from IPython.display import display, Image\n",
23
+ "import gradio as gr\n",
24
+ "from langchain_text_splitters import RecursiveCharacterTextSplitter\n",
25
+ "from langchain_chroma import Chroma\n",
26
+ "import uuid\n",
27
+ "from langgraph.errors import GraphRecursionError"
28
+ ]
29
+ },
30
+ {
31
+ "cell_type": "code",
32
+ "execution_count": 12,
33
+ "id": "919b6be4",
34
+ "metadata": {},
35
+ "outputs": [
36
+ {
37
+ "data": {
38
+ "text/plain": [
39
+ "True"
40
+ ]
41
+ },
42
+ "execution_count": 12,
43
+ "metadata": {},
44
+ "output_type": "execute_result"
45
+ }
46
+ ],
47
+ "source": [
48
+ "load_dotenv(override=True)"
49
+ ]
50
+ },
51
+ {
52
+ "cell_type": "code",
53
+ "execution_count": 13,
54
+ "id": "21a63f2c",
55
+ "metadata": {},
56
+ "outputs": [],
57
+ "source": [
58
+ "class RAG_Setup:\n",
59
+ " def __init__(self):\n",
60
+ " self.embeddings = HuggingFaceEmbeddings(model_name=\"sentence-transformers/all-mpnet-base-v2\")\n",
61
+ " self.vector_store = Chroma(\n",
62
+ " collection_name=\"medical_history_collection\",\n",
63
+ " embedding_function=self.embeddings,\n",
64
+ " persist_directory=\"data/patient_record_db\", \n",
65
+ " )\n",
66
+ "\n",
67
+ " def _calculate_file_hash(self, file_path):\n",
68
+ " import hashlib\n",
69
+ " sha256 = hashlib.sha256()\n",
70
+ " with open(file_path, 'rb') as f:\n",
71
+ " while chunk := f.read(8192):\n",
72
+ " sha256.update(chunk)\n",
73
+ " return sha256.hexdigest()\n",
74
+ "\n",
75
+ " def _is_file_uploaded(self, file_hash):\n",
76
+ " results = self.vector_store.get(\n",
77
+ " where={\"file_hash\": file_hash},\n",
78
+ " limit=1\n",
79
+ " )\n",
80
+ " return len(results['ids']) > 0\n",
81
+ " \n",
82
+ " def _extract_content(self, file_path):\n",
83
+ " pdf_loader = PyPDFLoader(file_path)\n",
84
+ " content = pdf_loader.load()\n",
85
+ " return content\n",
86
+ "\n",
87
+ " def _split_content(self, content):\n",
88
+ " text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, add_start_index=True)\n",
89
+ " chunks = text_splitter.split_documents(content)\n",
90
+ " return chunks\n",
91
+ "\n",
92
+ " def _embed_content(self, chunks):\n",
93
+ " self.vector_store.add_documents(chunks)\n",
94
+ "\n",
95
+ " def store_data(self, file_path):\n",
96
+ "\n",
97
+ " file_hash = self._calculate_file_hash(file_path)\n",
98
+ " \n",
99
+ " if self._is_file_uploaded(file_hash):\n",
100
+ " return {\n",
101
+ " \"status\": \"skipped\",\n",
102
+ " \"message\": f\"File already exists in database\"\n",
103
+ " }\n",
104
+ " \n",
105
+ " try:\n",
106
+ " content = self._extract_content(file_path)\n",
107
+ " chunks = self._split_content(content)\n",
108
+ " \n",
109
+ " for chunk in chunks:\n",
110
+ " chunk.metadata.update({\n",
111
+ " 'file_hash': file_hash\n",
112
+ " })\n",
113
+ " \n",
114
+ " self._embed_content(chunks)\n",
115
+ " \n",
116
+ " return {\n",
117
+ " \"status\": \"success\",\n",
118
+ " \"message\": f\"File successfully uploaded\",\n",
119
+ " \"chunks\": len(chunks)\n",
120
+ " }\n",
121
+ " except Exception as e:\n",
122
+ " return {\n",
123
+ " \"status\": \"error\",\n",
124
+ " \"message\": f\"Failed to upload file: {str(e)}\"\n",
125
+ " }\n",
126
+ "\n",
127
+ " def retrieve_info(self, query: str):\n",
128
+ " try:\n",
129
+ " results = self.vector_store.similarity_search(query, k=5)\n",
130
+ " print(\"printing tool results\", results)\n",
131
+ " \n",
132
+ " if not results:\n",
133
+ " return \"No medical history found for this query.\"\n",
134
+ " \n",
135
+ " content = \"\\n\\n---DOCUMENT---\\n\\n\".join([doc.page_content for doc in results])\n",
136
+ " \n",
137
+ " return content\n",
138
+ " \n",
139
+ " except Exception as e:\n",
140
+ " return \"Failed to retrieve medical record\"\n",
141
+ " "
142
+ ]
143
+ },
144
+ {
145
+ "cell_type": "code",
146
+ "execution_count": 14,
147
+ "id": "796b25c1",
148
+ "metadata": {},
149
+ "outputs": [],
150
+ "source": [
151
+ "rag = RAG_Setup()"
152
+ ]
153
+ },
154
+ {
155
+ "cell_type": "code",
156
+ "execution_count": 15,
157
+ "id": "795f7bce",
158
+ "metadata": {},
159
+ "outputs": [],
160
+ "source": [
161
+ "@tool\n",
162
+ "def check_medical_history(query: str):\n",
163
+ " '''Retrieves relevent medical history of the user\n",
164
+ "\n",
165
+ " Args:\n",
166
+ " query: medical history to be searched for\n",
167
+ " '''\n",
168
+ " return rag.retrieve_info(query)"
169
+ ]
170
+ },
171
+ {
172
+ "cell_type": "code",
173
+ "execution_count": 16,
174
+ "id": "a9efe8fa",
175
+ "metadata": {},
176
+ "outputs": [],
177
+ "source": [
178
+ "serper = GoogleSerperAPIWrapper()\n",
179
+ "@tool\n",
180
+ "def web_search(query: str):\n",
181
+ " ''' Search web for answering queries with latest information\n",
182
+ " Args:\n",
183
+ " query: query to be searched on the web\n",
184
+ " '''\n",
185
+ " print(\"Websearch tool calling\")\n",
186
+ " return serper.run(query)"
187
+ ]
188
+ },
189
+ {
190
+ "cell_type": "code",
191
+ "execution_count": 17,
192
+ "id": "9bc930eb",
193
+ "metadata": {},
194
+ "outputs": [],
195
+ "source": [
196
+ "tools = [web_search, check_medical_history]"
197
+ ]
198
+ },
199
+ {
200
+ "cell_type": "code",
201
+ "execution_count": 18,
202
+ "id": "96d52fc2",
203
+ "metadata": {},
204
+ "outputs": [],
205
+ "source": [
206
+ "llm = HuggingFaceEndpoint(\n",
207
+ " repo_id=\"deepseek-ai/DeepSeek-V3\",\n",
208
+ " task=\"text-generation\",\n",
209
+ " max_new_tokens=1024,\n",
210
+ " do_sample=False,\n",
211
+ " repetition_penalty=1.03,\n",
212
+ " provider=\"auto\", \n",
213
+ ")\n",
214
+ "llm = ChatHuggingFace(llm=llm)\n",
215
+ "llm_with_tools = llm.bind_tools(tools)"
216
+ ]
217
+ },
218
+ {
219
+ "cell_type": "code",
220
+ "execution_count": 19,
221
+ "id": "4bff6b8c",
222
+ "metadata": {},
223
+ "outputs": [],
224
+ "source": [
225
+ "class State(TypedDict):\n",
226
+ " messages: Annotated[list, add_messages]"
227
+ ]
228
+ },
229
+ {
230
+ "cell_type": "code",
231
+ "execution_count": 20,
232
+ "id": "c4ea1e72",
233
+ "metadata": {},
234
+ "outputs": [],
235
+ "source": [
236
+ "REACT_SYSTEM_PROMPT = '''You are a helpful medical assistant with access to patient records and web search.\n",
237
+ "\n",
238
+ "You solve problems using the ReAct (Reasoning and Acting) framework:\n",
239
+ "1. Thought: Reason about what information you need\n",
240
+ "2. Action: Call the appropriate tool\n",
241
+ "3. Observation: Receive the tool result\n",
242
+ "4. Repeat until you can answer confidently\n",
243
+ "\n",
244
+ "AVAILABLE TOOLS:\n",
245
+ "- check_medical_history: Search patient's personal medical records (medications, appointments, conditions, lab results)\n",
246
+ "- web_search: Search the web for general medical information, drug interactions, side effects, treatment guidelines\n",
247
+ "\n",
248
+ "MULTI-STEP REASONING EXAMPLES:\n",
249
+ "\n",
250
+ "Example 1: Drug Interaction Query\n",
251
+ "User: \"Can I take ibuprofen with my medicines?\"\n",
252
+ "Thought: I need to first check what medications the patient is currently taking.\n",
253
+ "Action: check_medical_history(query=\"current medications\")\n",
254
+ "Observation: Patient takes Metformin, Lisinopril, Atorvastatin, Levothyroxine, Omeprazole, Aspirin, Vitamin D3\n",
255
+ "Thought: Now I need to check if ibuprofen interacts with these specific medications, especially Aspirin (both are NSAIDs).\n",
256
+ "Action: web_search(query=\"ibuprofen interactions with aspirin metformin lisinopril atorvastatin\")\n",
257
+ "Observation: Ibuprofen + Aspirin can reduce aspirin's cardioprotective effect. Risk of bleeding increases. Should avoid concurrent use.\n",
258
+ "Answer: Based on your current medications, taking ibuprofen with aspirin is not recommended...\n",
259
+ "\n",
260
+ "Example 2: Simple Patient Query\n",
261
+ "User: \"What medications am I taking?\"\n",
262
+ "Thought: This is a straightforward question about the patient's records.\n",
263
+ "Action: check_medical_history(query=\"current medications\")\n",
264
+ "Observation: [Patient medication list]\n",
265
+ "Answer: You are currently taking...\n",
266
+ "\n",
267
+ "Example 3: General Medical Question\n",
268
+ "User: \"What are the side effects of Metformin?\"\n",
269
+ "Thought: This is a general medical question, not specific to the patient's records.\n",
270
+ "Action: web_search(query=\"Metformin side effects\")\n",
271
+ "Observation: [Web search results]\n",
272
+ "Answer: Common side effects of Metformin include...\n",
273
+ "\n",
274
+ "CRITICAL RULES:\n",
275
+ "- Use multiple tools when needed - don't stop after one tool if more information is required\n",
276
+ "- Think step-by-step and be thorough\n",
277
+ "'''"
278
+ ]
279
+ },
280
+ {
281
+ "cell_type": "code",
282
+ "execution_count": 21,
283
+ "id": "57c7bd31",
284
+ "metadata": {},
285
+ "outputs": [],
286
+ "source": [
287
+ "def personal_assistant(state: State):\n",
288
+ " print(\"assistant responses:\")\n",
289
+ " print(state[\"messages\"])\n",
290
+ " messages = state[\"messages\"]\n",
291
+ " return {\n",
292
+ " \"messages\": llm_with_tools.invoke(messages)\n",
293
+ " }"
294
+ ]
295
+ },
296
+ {
297
+ "cell_type": "code",
298
+ "execution_count": 22,
299
+ "id": "c27a0d7c",
300
+ "metadata": {},
301
+ "outputs": [],
302
+ "source": [
303
+ "db_path = 'data/long_term_memory.db'\n",
304
+ "conn = sqlite3.connect(db_path, check_same_thread=False)\n",
305
+ "memory = SqliteSaver(conn)"
306
+ ]
307
+ },
308
+ {
309
+ "cell_type": "code",
310
+ "execution_count": 23,
311
+ "id": "6a6029e6",
312
+ "metadata": {},
313
+ "outputs": [
314
+ {
315
+ "data": {
316
+ "image/png": "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",
317
+ "text/plain": [
318
+ "<IPython.core.display.Image object>"
319
+ ]
320
+ },
321
+ "metadata": {},
322
+ "output_type": "display_data"
323
+ }
324
+ ],
325
+ "source": [
326
+ "graph_builder = StateGraph(State)\n",
327
+ "graph_builder.add_node(\"personal_assistant\", personal_assistant)\n",
328
+ "graph_builder.add_node(\"tools\", ToolNode(tools))\n",
329
+ "graph_builder.add_conditional_edges(\"personal_assistant\", tools_condition, {\"tools\": \"tools\", \"__end__\": END})\n",
330
+ "graph_builder.add_edge(START, \"personal_assistant\")\n",
331
+ "graph_builder.add_edge(\"tools\", \"personal_assistant\")\n",
332
+ "\n",
333
+ "graph = graph_builder.compile(checkpointer=memory)\n",
334
+ "display(Image(graph.get_graph().draw_mermaid_png()))"
335
+ ]
336
+ },
337
+ {
338
+ "cell_type": "code",
339
+ "execution_count": 24,
340
+ "id": "368db378",
341
+ "metadata": {},
342
+ "outputs": [
343
+ {
344
+ "name": "stderr",
345
+ "output_type": "stream",
346
+ "text": [
347
+ "Device set to use mps:0\n"
348
+ ]
349
+ }
350
+ ],
351
+ "source": [
352
+ "from transformers import pipeline\n",
353
+ "\n",
354
+ "transcriber = pipeline(\"automatic-speech-recognition\", model=\"openai/whisper-small\")"
355
+ ]
356
+ },
357
+ {
358
+ "cell_type": "code",
359
+ "execution_count": 25,
360
+ "id": "1733601d",
361
+ "metadata": {},
362
+ "outputs": [
363
+ {
364
+ "name": "stdout",
365
+ "output_type": "stream",
366
+ "text": [
367
+ "b0196b9a-8b29-407c-b736-e2d6d6abeeb7\n"
368
+ ]
369
+ }
370
+ ],
371
+ "source": [
372
+ "import json\n",
373
+ "session_id = str(uuid.uuid4())\n",
374
+ "print(session_id)\n",
375
+ "\n",
376
+ "def chat(user_message, uploaded_file, message_history):\n",
377
+ " \"\"\"\n",
378
+ " Handle chat with text, audio, or file upload.\n",
379
+ " The LLM decides what to do with uploaded files.\n",
380
+ " \"\"\"\n",
381
+ " user_query_parts = []\n",
382
+ " try: \n",
383
+ " if user_message and user_message.strip():\n",
384
+ " user_query_parts.append(user_message)\n",
385
+ " \n",
386
+ " if uploaded_file is not None:\n",
387
+ " result = rag.store_data(uploaded_file)\n",
388
+ " result_str = json.dumps(result, indent=2)\n",
389
+ " user_query_parts.append(f\"\"\"A medical document was uploaded. Here are the upload details: {result_str} Please inform the user about the upload status in a friendly, professional way.\"\"\")\n",
390
+ "\n",
391
+ " if not user_query_parts:\n",
392
+ " return message_history, \"\", None, None\n",
393
+ " \n",
394
+ " user_query = (' ').join(user_query_parts)\n",
395
+ " \n",
396
+ " config = {\"configurable\": {\"thread_id\": session_id}, \"recursion_limit\" : 25}\n",
397
+ " current_state = graph.get_state(config)\n",
398
+ " \n",
399
+ " if not current_state.values.get(\"messages\"):\n",
400
+ " messages = {\n",
401
+ " \"messages\": [\n",
402
+ " {\"role\": \"system\", \"content\": REACT_SYSTEM_PROMPT},\n",
403
+ " {\"role\": \"user\", \"content\": user_query}\n",
404
+ " ]\n",
405
+ " }\n",
406
+ " else:\n",
407
+ " messages = {\"messages\": [{\"role\": \"user\", \"content\": user_query}]}\n",
408
+ "\n",
409
+ " result = graph.invoke(\n",
410
+ " messages,\n",
411
+ " config=config\n",
412
+ " )\n",
413
+ " \n",
414
+ " last_message = result[\"messages\"][-1].content\n",
415
+ " \n",
416
+ " updated_history = message_history + [\n",
417
+ " {\"role\": \"user\", \"content\": user_message},\n",
418
+ " {\"role\": \"assistant\", \"content\": last_message}\n",
419
+ " ]\n",
420
+ " \n",
421
+ " return updated_history, \"\", None\n",
422
+ " \n",
423
+ " except GraphRecursionError:\n",
424
+ " error_message = \"This query is too complex and exceeded the reasoning limit. Please simplify or break it into smaller questions.\"\n",
425
+ " return message_history + [\n",
426
+ " {\"role\": \"assistant\", \"content\": error_message}\n",
427
+ " ], \"\", None\n",
428
+ " \n",
429
+ " except Exception as e:\n",
430
+ " error_message = f\"Error: {str(e)}\"\n",
431
+ " return message_history + [\n",
432
+ " {\"role\": \"assistant\", \"content\": error_message}\n",
433
+ " ], \"\", None"
434
+ ]
435
+ },
436
+ {
437
+ "cell_type": "code",
438
+ "execution_count": 26,
439
+ "id": "2cd6085c",
440
+ "metadata": {},
441
+ "outputs": [
442
+ {
443
+ "name": "stdout",
444
+ "output_type": "stream",
445
+ "text": [
446
+ "* Running on local URL: http://127.0.0.1:7860\n",
447
+ "* Running on public URL: https://a92b3a19656a2e6316.gradio.live\n",
448
+ "\n",
449
+ "This share link expires in 1 week. For free permanent hosting and GPU upgrades, run `gradio deploy` from the terminal in the working directory to deploy to Hugging Face Spaces (https://huggingface.co/spaces)\n"
450
+ ]
451
+ },
452
+ {
453
+ "data": {
454
+ "text/html": [
455
+ "<div><iframe src=\"https://a92b3a19656a2e6316.gradio.live\" width=\"100%\" height=\"500\" allow=\"autoplay; camera; microphone; clipboard-read; clipboard-write;\" frameborder=\"0\" allowfullscreen></iframe></div>"
456
+ ],
457
+ "text/plain": [
458
+ "<IPython.core.display.HTML object>"
459
+ ]
460
+ },
461
+ "metadata": {},
462
+ "output_type": "display_data"
463
+ },
464
+ {
465
+ "data": {
466
+ "text/plain": []
467
+ },
468
+ "execution_count": 26,
469
+ "metadata": {},
470
+ "output_type": "execute_result"
471
+ }
472
+ ],
473
+ "source": [
474
+ "def transcribe_audio(audio, current_text, file_input, message_history):\n",
475
+ " if audio is None:\n",
476
+ " return message_history, current_text, None, file_input\n",
477
+ " \n",
478
+ " transcript = transcriber(audio)[\"text\"].strip()\n",
479
+ " \n",
480
+ " updated_history, cleared_text, cleared_file = chat(\n",
481
+ " transcript, \n",
482
+ " file_input, \n",
483
+ " message_history\n",
484
+ " )\n",
485
+ " \n",
486
+ " return updated_history, current_text, None, cleared_file\n",
487
+ "\n",
488
+ "with gr.Blocks(title=\"Medical Assistant\") as demo:\n",
489
+ " gr.Markdown(\"# πŸ₯ Medical Assistant\")\n",
490
+ " gr.Markdown(\"Ask questions using text, voice, or upload medical documents\")\n",
491
+ " \n",
492
+ " chatbot = gr.Chatbot(label=\"Conversation\", height=400)\n",
493
+ " \n",
494
+ " with gr.Row():\n",
495
+ " with gr.Column(scale=3):\n",
496
+ " text_input = gr.Textbox(\n",
497
+ " placeholder=\"Type your medical question here...\",\n",
498
+ " label=\"Text Input\",\n",
499
+ " lines=2\n",
500
+ " )\n",
501
+ " with gr.Column(scale=1):\n",
502
+ " audio_input = gr.Audio(\n",
503
+ " sources=[\"microphone\"],\n",
504
+ " type=\"filepath\",\n",
505
+ " label=\"🎀 Voice\"\n",
506
+ " )\n",
507
+ " with gr.Column(scale=1):\n",
508
+ " file_input = gr.File(\n",
509
+ " label=\"πŸ“„ Upload PDF\",\n",
510
+ " file_types=[\".pdf\"],\n",
511
+ " type=\"filepath\"\n",
512
+ " )\n",
513
+ " \n",
514
+ " with gr.Row():\n",
515
+ " submit_btn = gr.Button(\"Send\", variant=\"primary\")\n",
516
+ " clear_btn = gr.ClearButton([chatbot, text_input, audio_input, file_input])\n",
517
+ " \n",
518
+ " gr.Markdown(\"### Tips:\\n- Upload medical records (PDFs) and I'll process them automatically\\n- Ask about medications, interactions, or symptoms\\n- I can store new medical information you share\")\n",
519
+ " \n",
520
+ " submit_btn.click(\n",
521
+ " chat,\n",
522
+ " inputs=[text_input, file_input, chatbot],\n",
523
+ " outputs=[chatbot, text_input, file_input]\n",
524
+ " )\n",
525
+ " \n",
526
+ " text_input.submit(\n",
527
+ " chat,\n",
528
+ " inputs=[text_input, file_input, chatbot],\n",
529
+ " outputs=[chatbot, text_input, file_input]\n",
530
+ " )\n",
531
+ "\n",
532
+ " audio_input.change(\n",
533
+ " transcribe_audio,\n",
534
+ " inputs=[audio_input, text_input, file_input, chatbot],\n",
535
+ " outputs=[chatbot, text_input, audio_input, file_input] \n",
536
+ " )\n",
537
+ "\n",
538
+ "demo.launch(share=True)"
539
+ ]
540
+ },
541
+ {
542
+ "cell_type": "code",
543
+ "execution_count": 27,
544
+ "id": "0d7db61f",
545
+ "metadata": {},
546
+ "outputs": [
547
+ {
548
+ "data": {
549
+ "text/plain": [
550
+ "<function __main__.<lambda>()>"
551
+ ]
552
+ },
553
+ "execution_count": 27,
554
+ "metadata": {},
555
+ "output_type": "execute_result"
556
+ }
557
+ ],
558
+ "source": [
559
+ "import atexit\n",
560
+ "atexit.register(lambda: conn.close())"
561
+ ]
562
+ },
563
+ {
564
+ "cell_type": "code",
565
+ "execution_count": null,
566
+ "id": "599a937a",
567
+ "metadata": {},
568
+ "outputs": [],
569
+ "source": []
570
+ },
571
+ {
572
+ "cell_type": "code",
573
+ "execution_count": null,
574
+ "id": "a6305c14",
575
+ "metadata": {},
576
+ "outputs": [],
577
+ "source": []
578
+ }
579
+ ],
580
+ "metadata": {
581
+ "kernelspec": {
582
+ "display_name": ".venv (3.11.9)",
583
+ "language": "python",
584
+ "name": "python3"
585
+ },
586
+ "language_info": {
587
+ "codemirror_mode": {
588
+ "name": "ipython",
589
+ "version": 3
590
+ },
591
+ "file_extension": ".py",
592
+ "mimetype": "text/x-python",
593
+ "name": "python",
594
+ "nbconvert_exporter": "python",
595
+ "pygments_lexer": "ipython3",
596
+ "version": "3.11.9"
597
+ }
598
+ },
599
+ "nbformat": 4,
600
+ "nbformat_minor": 5
601
+ }
graph_setup.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3
2
+ from typing_extensions import TypedDict, Annotated
3
+ from langgraph.graph import START, END, StateGraph
4
+ from langgraph.graph.message import add_messages
5
+ from langgraph.prebuilt import ToolNode, tools_condition
6
+ from langgraph.checkpoint.sqlite import SqliteSaver
7
+ from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
8
+
9
+
10
+ class State(TypedDict):
11
+ messages: Annotated[list, add_messages]
12
+
13
+
14
+ class GraphSetup:
15
+ def __init__(self, tools):
16
+ self.tools = tools
17
+ self.llm = self._setup_llm()
18
+ self.llm_with_tools = self.llm.bind_tools(self.tools)
19
+ self.memory = self._setup_memory()
20
+ self.graph = self._build_graph()
21
+
22
+ def _setup_llm(self):
23
+ llm = HuggingFaceEndpoint(
24
+ repo_id="deepseek-ai/DeepSeek-V3",
25
+ task="text-generation",
26
+ max_new_tokens=1024,
27
+ do_sample=False,
28
+ repetition_penalty=1.03,
29
+ provider="auto",
30
+ )
31
+ return ChatHuggingFace(llm=llm)
32
+
33
+ def _setup_memory(self):
34
+ db_path = 'data/long_term_memory.db'
35
+ conn = sqlite3.connect(db_path, check_same_thread=False)
36
+ return SqliteSaver(conn)
37
+
38
+ def _personal_assistant(self, state: State):
39
+ print("assistant responses:")
40
+ print(state["messages"])
41
+ messages = state["messages"]
42
+ return {
43
+ "messages": self.llm_with_tools.invoke(messages)
44
+ }
45
+
46
+ def _build_graph(self):
47
+ graph_builder = StateGraph(State)
48
+ graph_builder.add_node("personal_assistant", self._personal_assistant)
49
+ graph_builder.add_node("tools", ToolNode(self.tools))
50
+ graph_builder.add_conditional_edges("personal_assistant", tools_condition, {"tools": "tools", "__end__": END})
51
+ graph_builder.add_edge(START, "personal_assistant")
52
+ graph_builder.add_edge("tools", "personal_assistant")
53
+
54
+ return graph_builder.compile(checkpointer=self.memory)
55
+
56
+ def get_graph(self):
57
+ return self.graph
main.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from dotenv import load_dotenv
3
+ from rag_setup import RAG_Setup
4
+ from tools import MedicalTools
5
+ from graph_setup import GraphSetup
6
+ from chat_handler import ChatHandler
7
+ from audio_handler import AudioHandler
8
+
9
+
10
+ load_dotenv(override=True)
11
+
12
+ rag = RAG_Setup()
13
+ medical_tools = MedicalTools(rag)
14
+ tools = medical_tools.get_tools()
15
+ graph_setup = GraphSetup(tools)
16
+ graph = graph_setup.get_graph()
17
+ chat_handler = ChatHandler(graph, rag)
18
+ audio_handler = AudioHandler()
19
+
20
+
21
+ def transcribe_audio_wrapper(audio, current_text, file_input, message_history):
22
+ return audio_handler.transcribe_audio(
23
+ audio,
24
+ current_text,
25
+ file_input,
26
+ message_history,
27
+ chat_handler.chat
28
+ )
29
+
30
+
31
+ with gr.Blocks(title="Medical Assistant") as demo:
32
+ gr.Markdown("# πŸ₯ Medical Assistant")
33
+ gr.Markdown("Ask questions using text, voice, or upload medical documents")
34
+
35
+ chatbot = gr.Chatbot(label="Conversation", height=400)
36
+
37
+ with gr.Row():
38
+ with gr.Column(scale=3):
39
+ text_input = gr.Textbox(
40
+ placeholder="Type your medical question here...",
41
+ label="Text Input",
42
+ lines=2
43
+ )
44
+ with gr.Column(scale=1):
45
+ audio_input = gr.Audio(
46
+ sources=["microphone"],
47
+ type="filepath",
48
+ label="🎀 Voice"
49
+ )
50
+ with gr.Column(scale=1):
51
+ file_input = gr.File(
52
+ label="πŸ“„ Upload PDF",
53
+ file_types=[".pdf"],
54
+ type="filepath"
55
+ )
56
+
57
+ with gr.Row():
58
+ submit_btn = gr.Button("Send", variant="primary")
59
+ clear_btn = gr.ClearButton([chatbot, text_input, audio_input, file_input])
60
+
61
+ gr.Markdown("### Tips:\n- Upload medical records (PDFs) and I'll process them automatically\n- Ask about medications, interactions, or symptoms\n- I can store new medical information you share")
62
+
63
+ submit_btn.click(
64
+ chat_handler.chat,
65
+ inputs=[text_input, file_input, chatbot],
66
+ outputs=[chatbot, text_input, file_input]
67
+ )
68
+
69
+ text_input.submit(
70
+ chat_handler.chat,
71
+ inputs=[text_input, file_input, chatbot],
72
+ outputs=[chatbot, text_input, file_input]
73
+ )
74
+
75
+ audio_input.change(
76
+ transcribe_audio_wrapper,
77
+ inputs=[audio_input, text_input, file_input, chatbot],
78
+ outputs=[chatbot, text_input, audio_input, file_input]
79
+ )
80
+
81
+
82
+ if __name__ == "__main__":
83
+ demo.launch(share=True)
prompts.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ REACT_SYSTEM_PROMPT = '''You are a helpful medical assistant with access to patient records and web search.
2
+
3
+ You solve problems using the ReAct (Reasoning and Acting) framework:
4
+ 1. Thought: Reason about what information you need
5
+ 2. Action: Call the appropriate tool
6
+ 3. Observation: Receive the tool result
7
+ 4. Repeat until you can answer confidently
8
+
9
+ AVAILABLE TOOLS:
10
+ - check_medical_history: Search patient's personal medical records (medications, appointments, conditions, lab results)
11
+ - web_search: Search the web for general medical information, drug interactions, side effects, treatment guidelines
12
+
13
+ MULTI-STEP REASONING EXAMPLES:
14
+
15
+ Example 1: Drug Interaction Query
16
+ User: "Can I take ibuprofen with my medicines?"
17
+ Thought: I need to first check what medications the patient is currently taking.
18
+ Action: check_medical_history(query="current medications")
19
+ Observation: Patient takes Metformin, Lisinopril, Atorvastatin, Levothyroxine, Omeprazole, Aspirin, Vitamin D3
20
+ Thought: Now I need to check if ibuprofen interacts with these specific medications, especially Aspirin (both are NSAIDs).
21
+ Action: web_search(query="ibuprofen interactions with aspirin metformin lisinopril atorvastatin")
22
+ Observation: Ibuprofen + Aspirin can reduce aspirin's cardioprotective effect. Risk of bleeding increases. Should avoid concurrent use.
23
+ Answer: Based on your current medications, taking ibuprofen with aspirin is not recommended...
24
+
25
+ Example 2: Simple Patient Query
26
+ User: "What medications am I taking?"
27
+ Thought: This is a straightforward question about the patient's records.
28
+ Action: check_medical_history(query="current medications")
29
+ Observation: [Patient medication list]
30
+ Answer: You are currently taking...
31
+
32
+ Example 3: General Medical Question
33
+ User: "What are the side effects of Metformin?"
34
+ Thought: This is a general medical question, not specific to the patient's records.
35
+ Action: web_search(query="Metformin side effects")
36
+ Observation: [Web search results]
37
+ Answer: Common side effects of Metformin include...
38
+
39
+ CRITICAL RULES:
40
+ - Use multiple tools when needed - don't stop after one tool if more information is required
41
+ - Think step-by-step and be thorough
42
+ '''
rag_setup.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import hashlib
2
+ from langchain_huggingface.embeddings import HuggingFaceEmbeddings
3
+ from langchain_community.document_loaders import PyPDFLoader
4
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
5
+ from langchain_chroma import Chroma
6
+
7
+
8
+ class RAG_Setup:
9
+ def __init__(self):
10
+ self.embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-mpnet-base-v2")
11
+ self.vector_store = Chroma(
12
+ collection_name="medical_history_collection",
13
+ embedding_function=self.embeddings,
14
+ persist_directory="data/patient_record_db",
15
+ )
16
+
17
+ def _calculate_file_hash(self, file_path):
18
+ sha256 = hashlib.sha256()
19
+ with open(file_path, 'rb') as f:
20
+ while chunk := f.read(8192):
21
+ sha256.update(chunk)
22
+ return sha256.hexdigest()
23
+
24
+ def _is_file_uploaded(self, file_hash):
25
+ results = self.vector_store.get(
26
+ where={"file_hash": file_hash},
27
+ limit=1
28
+ )
29
+ return len(results['ids']) > 0
30
+
31
+ def _extract_content(self, file_path):
32
+ pdf_loader = PyPDFLoader(file_path)
33
+ content = pdf_loader.load()
34
+ return content
35
+
36
+ def _split_content(self, content):
37
+ text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, add_start_index=True)
38
+ chunks = text_splitter.split_documents(content)
39
+ return chunks
40
+
41
+ def _embed_content(self, chunks):
42
+ self.vector_store.add_documents(chunks)
43
+
44
+ def store_data(self, file_path):
45
+ file_hash = self._calculate_file_hash(file_path)
46
+
47
+ if self._is_file_uploaded(file_hash):
48
+ return {
49
+ "status": "skipped",
50
+ "message": f"File already exists in database"
51
+ }
52
+
53
+ try:
54
+ content = self._extract_content(file_path)
55
+ chunks = self._split_content(content)
56
+
57
+ for chunk in chunks:
58
+ chunk.metadata.update({
59
+ 'file_hash': file_hash
60
+ })
61
+
62
+ self._embed_content(chunks)
63
+
64
+ return {
65
+ "status": "success",
66
+ "message": f"File successfully uploaded",
67
+ "chunks": len(chunks)
68
+ }
69
+ except Exception as e:
70
+ return {
71
+ "status": "error",
72
+ "message": f"Failed to upload file: {str(e)}"
73
+ }
74
+
75
+ def retrieve_info(self, query: str):
76
+ try:
77
+ results = self.vector_store.similarity_search(query, k=5)
78
+ print("printing tool results", results)
79
+
80
+ if not results:
81
+ return "No medical history found for this query."
82
+
83
+ content = "\n\n---DOCUMENT---\n\n".join([doc.page_content for doc in results])
84
+
85
+ return content
86
+
87
+ except Exception as e:
88
+ return "Failed to retrieve medical record"
readme.md ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: MedQuery-Assist
3
+ app_file: main.py
4
+ sdk: gradio
5
+ sdk_version: 6.4.0
6
+ ---
7
+ # Medical Assistant Chatbot
8
+
9
+ A conversational AI medical assistant that supports text, voice, and document-based interactions. Built with LangGraph, RAG, and Gradio.
10
+
11
+ ## Features
12
+
13
+ - **Multi-modal Input**: Text, voice (Whisper), and PDF document upload
14
+ - **RAG System**: Store and retrieve patient medical records from PDF documents
15
+ - **Web Search**: Access latest medical information via Google Serper API
16
+ - **Conversational Memory**: Maintains context across conversation using LangGraph checkpointing
17
+ - **ReAct Framework**: Step-by-step reasoning with tool usage
18
+ - **Auto-transcription**: Voice messages automatically transcribed and sent
19
+
20
+ ## Architecture
21
+
22
+ ```
23
+ β”œβ”€β”€ rag_setup.py # Document processing and vector store
24
+ β”œβ”€β”€ tools.py # Medical history search and web search tools
25
+ β”œβ”€β”€ graph_setup.py # LangGraph workflow configuration
26
+ β”œβ”€β”€ prompts.py # System prompts
27
+ β”œβ”€β”€ chat_handler.py # Chat logic and session management
28
+ β”œβ”€β”€ audio_handler.py # Audio transcription
29
+ β”œβ”€β”€ main.py # Gradio interface
30
+ └── data/
31
+ β”œβ”€β”€ patient_record_db/ # Chroma vector store
32
+ └── long_term_memory.db # SQLite conversation checkpoints
33
+ ```
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install langgraph langchain-huggingface langchain-community langchain-chroma langgraph-checkpoint-sqlite langchain
39
+ pip install gradio transformers torch
40
+ pip install python-dotenv pypdf sentence-transformers
41
+ ```
42
+
43
+ ## Environment Setup
44
+
45
+ Create a `.env` file:
46
+
47
+ ```env
48
+ HUGGINGFACEHUB_API_TOKEN=your_hf_token
49
+ SERPER_API_KEY=your_serper_key
50
+ ```
51
+
52
+ ## Usage
53
+
54
+ ```bash
55
+ python main.py
56
+ ```
57
+
58
+ Access the interface at `http://127.0.0.1:7860`
59
+
60
+ ## How It Works
61
+
62
+ ### 1. Document Upload
63
+ - Upload PDF medical records
64
+ - Documents are chunked, embedded, and stored in Chroma vector database
65
+ - Duplicate detection via file hashing
66
+
67
+ ### 2. Query Processing
68
+ - User queries are processed through LangGraph workflow
69
+ - LLM decides which tools to use (medical history search or web search)
70
+ - Multi-step reasoning follows ReAct pattern
71
+
72
+ ### 3. Voice Input
73
+ - Record audio via microphone
74
+ - Automatic transcription using Whisper-small
75
+ - Auto-send to chat after transcription
76
+
77
+ ### 4. Response Generation
78
+ - DeepSeek-V3 model generates responses
79
+ - Can make multiple tool calls per query
80
+ - Maintains conversation context via SQLite checkpointing
81
+
82
+ ## Components
83
+
84
+ ### RAG_Setup
85
+ - Embeddings: `sentence-transformers/all-mpnet-base-v2`
86
+ - Vector Store: Chroma with persistence
87
+ - Chunk size: 1000 characters
88
+ - Similarity search returns top 5 results
89
+
90
+ ### GraphSetup
91
+ - LLM: DeepSeek-V3 via HuggingFace Inference
92
+ - Max tokens: 1024
93
+ - Recursion limit: 25
94
+ - Memory: SQLite checkpointing
95
+
96
+ ### Tools
97
+ - `check_medical_history`: Searches patient records
98
+ - `web_search`: Google Serper API for medical information
99
+
100
+ ### AudioHandler
101
+ - Model: `openai/whisper-small`
102
+ - Auto-send after transcription
103
+ - Clears audio input after processing
104
+
105
+ ## Session Management
106
+
107
+ - Each application instance generates a unique session ID
108
+ - All users in the same instance share conversation history
109
+ - Restart application to create new session
110
+
111
+ ## File Structure
112
+
113
+ ```
114
+ data/
115
+ β”œβ”€β”€ patient_record_db/ # Vector embeddings
116
+ β”‚ └── chroma.sqlite3
117
+ └── long_term_memory.db # Conversation checkpoints
118
+ ```
119
+
120
+ ## Limitations
121
+
122
+ - Single global session (all users share history)
123
+ - SQLite connection with `check_same_thread=False` (thread safety concern)
124
+ - No user authentication
125
+ - File uploads not validated beyond extension
126
+ - No cleanup of uploaded temporary files
127
+
128
+ ## Example Queries
129
+
130
+ **Simple Query:**
131
+ ```
132
+ What medications am I taking?
133
+ ```
134
+
135
+ **Complex Query:**
136
+ ```
137
+ Can I take ibuprofen with my current medications?
138
+ ```
139
+
140
+ **Upload Flow:**
141
+ 1. Upload PDF medical record
142
+ 2. System confirms upload success
143
+ 3. Ask questions about the uploaded document
144
+
145
+ ## Dependencies
146
+
147
+ - langgraph
148
+ - langchain-huggingface
149
+ - langchain-community
150
+ - langchain-chroma
151
+ - gradio
152
+ - transformers
153
+ - sentence-transformers
154
+ - pypdf
155
+ - google-serper-api
156
+ - python-dotenv
157
+
158
+ ## Notes
159
+
160
+ - Requires active internet for HuggingFace Inference API
161
+ - Requires Serper API key for web search
162
+ - First run downloads embedding model (~400MB)
163
+ - Whisper model downloads on first audio transcription (~500MB)
tools.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain.tools import tool
2
+ from langchain_community.utilities import GoogleSerperAPIWrapper
3
+
4
+
5
+ class MedicalTools:
6
+ def __init__(self, rag_setup):
7
+ self.rag = rag_setup
8
+ self.serper = GoogleSerperAPIWrapper()
9
+
10
+ def get_tools(self):
11
+ @tool
12
+ def check_medical_history(query: str):
13
+ '''Retrieves relevent medical history of the user
14
+
15
+ Args:
16
+ query: medical history to be searched for
17
+ '''
18
+ return self.rag.retrieve_info(query)
19
+
20
+ @tool
21
+ def web_search(query: str):
22
+ ''' Search web for answering queries with latest information
23
+ Args:
24
+ query: query to be searched on the web
25
+ '''
26
+ print("Websearch tool calling")
27
+ return self.serper.run(query)
28
+
29
+ return [web_search, check_medical_history]