Spaces:
Sleeping
Sleeping
Commit Β·
db33ebc
0
Parent(s):
add code for mediquery-assist
Browse files- .gitignore +6 -0
- audio_handler.py +20 -0
- chat_handler.py +67 -0
- experimentation.ipynb +601 -0
- graph_setup.py +57 -0
- main.py +83 -0
- prompts.py +42 -0
- rag_setup.py +88 -0
- readme.md +163 -0
- 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]
|