Upload 12 files
Browse files- .gitattributes +1 -0
- Dockerfile +16 -0
- README.md +38 -10
- app.py +68 -0
- disease.db +3 -0
- main.py +6 -0
- pyproject.toml +7 -0
- requirements.txt +8 -0
- templates/index.html +389 -0
- utils/__pycache__/sql_queries.cpython-311.pyc +0 -0
- utils/__pycache__/vector_search.cpython-311.pyc +0 -0
- utils/sql_queries.py +77 -0
- utils/vector_search.py +37 -0
.gitattributes
CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
36 |
+
disease.db filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.10-slim
|
2 |
+
|
3 |
+
WORKDIR /app
|
4 |
+
|
5 |
+
# Copy requirements file
|
6 |
+
COPY requirements.txt .
|
7 |
+
|
8 |
+
# Install dependencies
|
9 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
10 |
+
|
11 |
+
# Copy the rest of the application
|
12 |
+
COPY . .
|
13 |
+
|
14 |
+
|
15 |
+
# Run the application
|
16 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
@@ -1,10 +1,38 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Medical Symptom Checker
|
2 |
+
|
3 |
+
An interactive medical symptom checker that helps users explore symptoms and related diseases using vector similarity search and a database of medical conditions.
|
4 |
+
|
5 |
+
## Features
|
6 |
+
|
7 |
+
- Enter symptoms to find related diseases
|
8 |
+
- See semantically similar symptoms through vector search
|
9 |
+
- Discover related symptoms from matching diseases
|
10 |
+
- Interactive UI for refining symptom selection
|
11 |
+
|
12 |
+
## How It Works
|
13 |
+
|
14 |
+
1. **Vector Search**: Uses Sentence Transformers and ChromaDB to find semantically similar symptoms
|
15 |
+
2. **Database Matching**: Finds diseases that match all selected symptoms
|
16 |
+
3. **Related Symptoms**: Shows other symptoms from matching diseases
|
17 |
+
|
18 |
+
## Usage
|
19 |
+
|
20 |
+
Enter a symptom in the search box (e.g., "fever", "cough", "headache") and click "Search" or press Enter.
|
21 |
+
|
22 |
+
The system will:
|
23 |
+
1. Show diseases that have that symptom
|
24 |
+
2. Display semantically similar symptoms
|
25 |
+
3. Show other symptoms from the matching diseases
|
26 |
+
|
27 |
+
Click on any symptom in the "Similar Symptoms" or "Related Symptoms" sections to add it to your selection. This will narrow down the diseases to those that have ALL selected symptoms.
|
28 |
+
|
29 |
+
## Technical Details
|
30 |
+
|
31 |
+
- **Frontend**: HTML, CSS, JavaScript
|
32 |
+
- **Backend**: FastAPI, SQLite
|
33 |
+
- **Vector Search**: Sentence Transformers, ChromaDB
|
34 |
+
- **Data**: Diseases and symptoms dataset
|
35 |
+
|
36 |
+
## Deployment
|
37 |
+
|
38 |
+
This application is deployed on Hugging Face Spaces.
|
app.py
ADDED
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import FastAPI, Query, Request
|
2 |
+
from fastapi.responses import HTMLResponse
|
3 |
+
from fastapi.templating import Jinja2Templates
|
4 |
+
from fastapi.staticfiles import StaticFiles
|
5 |
+
from fastapi.middleware.cors import CORSMiddleware
|
6 |
+
from utils.vector_search import get_similar_symptoms
|
7 |
+
from utils.sql_queries import get_diseases_by_symptoms, get_related_symptoms
|
8 |
+
import sqlite3
|
9 |
+
|
10 |
+
app = FastAPI()
|
11 |
+
|
12 |
+
# Add CORS middleware
|
13 |
+
app.add_middleware(
|
14 |
+
CORSMiddleware,
|
15 |
+
allow_origins=["*"], # Allow all origins
|
16 |
+
allow_credentials=True,
|
17 |
+
allow_methods=["*"], # Allow all methods
|
18 |
+
allow_headers=["*"], # Allow all headers
|
19 |
+
)
|
20 |
+
|
21 |
+
templates = Jinja2Templates(directory="templates")
|
22 |
+
|
23 |
+
@app.get("/", response_class=HTMLResponse)
|
24 |
+
def index(request: Request):
|
25 |
+
return templates.TemplateResponse("index.html", {"request": request})
|
26 |
+
|
27 |
+
# Helper function to check if a symptom exists in the database
|
28 |
+
def is_valid_symptom(symptom):
|
29 |
+
conn = sqlite3.connect("disease.db")
|
30 |
+
cur = conn.cursor()
|
31 |
+
cur.execute("SELECT COUNT(*) FROM symptom WHERE name = ?", (symptom,))
|
32 |
+
count = cur.fetchone()[0]
|
33 |
+
conn.close()
|
34 |
+
return count > 0
|
35 |
+
|
36 |
+
@app.get("/search")
|
37 |
+
def search(symptom: str, selected: list[str] = Query([])):
|
38 |
+
# Get similar symptoms from vector search
|
39 |
+
similar = get_similar_symptoms(symptom)
|
40 |
+
|
41 |
+
# Check if the exact symptom exists in our database
|
42 |
+
if is_valid_symptom(symptom):
|
43 |
+
# Use the exact symptom if it exists
|
44 |
+
search_symptoms = [symptom] + selected
|
45 |
+
else:
|
46 |
+
# Otherwise, try to use the first similar symptom that exists in our database
|
47 |
+
valid_symptom_found = False
|
48 |
+
for sim in similar:
|
49 |
+
if is_valid_symptom(sim):
|
50 |
+
search_symptoms = [sim] + selected
|
51 |
+
valid_symptom_found = True
|
52 |
+
break
|
53 |
+
|
54 |
+
# If no valid symptom found, just use all similar symptoms
|
55 |
+
if not valid_symptom_found:
|
56 |
+
search_symptoms = similar + selected
|
57 |
+
|
58 |
+
# Get diseases that have ALL the selected search symptoms
|
59 |
+
diseases = get_diseases_by_symptoms(search_symptoms)
|
60 |
+
|
61 |
+
# Get related symptoms from the matching diseases, excluding already selected symptoms
|
62 |
+
suggestions = get_related_symptoms(diseases, exclude=search_symptoms)
|
63 |
+
|
64 |
+
return {
|
65 |
+
"matching_diseases": diseases,
|
66 |
+
"related_symptoms": suggestions,
|
67 |
+
"semantic_matches": similar
|
68 |
+
}
|
disease.db
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:e27f4415320952a05ceca7b615c053bc08b151ed3963e0ec714d431f519a2b1d
|
3 |
+
size 172032
|
main.py
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
def main():
|
2 |
+
print("Hello from llamadr!")
|
3 |
+
|
4 |
+
|
5 |
+
if __name__ == "__main__":
|
6 |
+
main()
|
pyproject.toml
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[project]
|
2 |
+
name = "llamadr"
|
3 |
+
version = "0.1.0"
|
4 |
+
description = "Add your description here"
|
5 |
+
readme = "README.md"
|
6 |
+
requires-python = ">=3.11"
|
7 |
+
dependencies = []
|
requirements.txt
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
fastapi>=0.95.0
|
2 |
+
uvicorn>=0.22.0
|
3 |
+
jinja2>=3.1.2
|
4 |
+
qdrant-client>=1.13.3
|
5 |
+
sentence-transformers>=2.2.2
|
6 |
+
datasets>=2.12.0
|
7 |
+
pandas>=2.0.0
|
8 |
+
python-dotenv>=1.1.0
|
templates/index.html
ADDED
@@ -0,0 +1,389 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8" />
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
6 |
+
<title>Medical Symptom Checker</title>
|
7 |
+
<style>
|
8 |
+
body {
|
9 |
+
font-family: 'Segoe UI', sans-serif;
|
10 |
+
background: linear-gradient(135deg, #fdfbfb, #ebedee);
|
11 |
+
margin: 0;
|
12 |
+
padding: 40px 20px;
|
13 |
+
color: #333;
|
14 |
+
}
|
15 |
+
|
16 |
+
h1 {
|
17 |
+
text-align: center;
|
18 |
+
font-size: 2.5rem;
|
19 |
+
margin-bottom: 30px;
|
20 |
+
background: linear-gradient(to right, #3b82f6, #9333ea);
|
21 |
+
-webkit-background-clip: text;
|
22 |
+
background-clip: text;
|
23 |
+
-webkit-text-fill-color: transparent;
|
24 |
+
}
|
25 |
+
|
26 |
+
.search-container {
|
27 |
+
text-align: center;
|
28 |
+
margin-bottom: 30px;
|
29 |
+
}
|
30 |
+
|
31 |
+
input[type="text"] {
|
32 |
+
padding: 12px 20px;
|
33 |
+
width: 60%;
|
34 |
+
max-width: 500px;
|
35 |
+
border-radius: 30px;
|
36 |
+
border: 1px solid #ccc;
|
37 |
+
box-shadow: 0 4px 10px rgba(0,0,0,0.05);
|
38 |
+
transition: all 0.3s ease;
|
39 |
+
}
|
40 |
+
|
41 |
+
input[type="text"]:focus {
|
42 |
+
outline: none;
|
43 |
+
border-color: #6366f1;
|
44 |
+
box-shadow: 0 4px 15px rgba(99,102,241,0.2);
|
45 |
+
}
|
46 |
+
|
47 |
+
button {
|
48 |
+
padding: 12px 25px;
|
49 |
+
margin-left: 10px;
|
50 |
+
border: none;
|
51 |
+
border-radius: 30px;
|
52 |
+
background: linear-gradient(to right, #3b82f6, #9333ea);
|
53 |
+
color: white;
|
54 |
+
cursor: pointer;
|
55 |
+
font-weight: bold;
|
56 |
+
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
|
57 |
+
transition: background 0.3s ease;
|
58 |
+
}
|
59 |
+
|
60 |
+
button:hover {
|
61 |
+
background: linear-gradient(to right, #2563eb, #7e22ce);
|
62 |
+
}
|
63 |
+
|
64 |
+
.selected-symptoms {
|
65 |
+
display: flex;
|
66 |
+
flex-wrap: wrap;
|
67 |
+
justify-content: center;
|
68 |
+
gap: 10px;
|
69 |
+
margin-bottom: 30px;
|
70 |
+
}
|
71 |
+
|
72 |
+
.symptom-tag {
|
73 |
+
background: #e0e7ff;
|
74 |
+
color: #1e40af;
|
75 |
+
padding: 8px 15px;
|
76 |
+
border-radius: 20px;
|
77 |
+
display: flex;
|
78 |
+
align-items: center;
|
79 |
+
animation: fadeIn 0.3s ease;
|
80 |
+
}
|
81 |
+
|
82 |
+
.remove-symptom {
|
83 |
+
margin-left: 10px;
|
84 |
+
cursor: pointer;
|
85 |
+
color: #555;
|
86 |
+
font-weight: bold;
|
87 |
+
}
|
88 |
+
|
89 |
+
.container {
|
90 |
+
display: flex;
|
91 |
+
flex-wrap: wrap;
|
92 |
+
gap: 20px;
|
93 |
+
justify-content: center;
|
94 |
+
}
|
95 |
+
|
96 |
+
.panel {
|
97 |
+
flex: 1 1 300px;
|
98 |
+
background: white;
|
99 |
+
border-radius: 15px;
|
100 |
+
padding: 20px;
|
101 |
+
box-shadow: 0 10px 20px rgba(0,0,0,0.05);
|
102 |
+
transition: transform 0.2s ease;
|
103 |
+
animation: fadeIn 0.5s ease;
|
104 |
+
}
|
105 |
+
|
106 |
+
.panel:hover {
|
107 |
+
transform: translateY(-5px);
|
108 |
+
}
|
109 |
+
|
110 |
+
.panel h2 {
|
111 |
+
margin-top: 0;
|
112 |
+
background: linear-gradient(to right, #2563eb, #9333ea);
|
113 |
+
-webkit-background-clip: text;
|
114 |
+
background-clip: text;
|
115 |
+
-webkit-text-fill-color: transparent;
|
116 |
+
font-size: 1.5rem;
|
117 |
+
}
|
118 |
+
|
119 |
+
ul {
|
120 |
+
list-style: none;
|
121 |
+
padding: 0;
|
122 |
+
}
|
123 |
+
|
124 |
+
li {
|
125 |
+
padding: 10px 0;
|
126 |
+
border-bottom: 1px solid #eee;
|
127 |
+
transition: background 0.2s ease;
|
128 |
+
}
|
129 |
+
|
130 |
+
.clickable {
|
131 |
+
color: #6366f1;
|
132 |
+
cursor: pointer;
|
133 |
+
}
|
134 |
+
|
135 |
+
.clickable:hover {
|
136 |
+
text-decoration: underline;
|
137 |
+
}
|
138 |
+
|
139 |
+
#error-notification {
|
140 |
+
background-color: #fee2e2;
|
141 |
+
color: #b91c1c;
|
142 |
+
padding: 12px 20px;
|
143 |
+
border-radius: 8px;
|
144 |
+
margin: 0 auto 20px auto;
|
145 |
+
max-width: 600px;
|
146 |
+
text-align: center;
|
147 |
+
display: none;
|
148 |
+
}
|
149 |
+
|
150 |
+
@keyframes fadeIn {
|
151 |
+
from { opacity: 0; transform: translateY(10px); }
|
152 |
+
to { opacity: 1; transform: translateY(0); }
|
153 |
+
}
|
154 |
+
|
155 |
+
@media screen and (max-width: 768px) {
|
156 |
+
input[type="text"] {
|
157 |
+
width: 80%;
|
158 |
+
}
|
159 |
+
.container {
|
160 |
+
flex-direction: column;
|
161 |
+
align-items: stretch;
|
162 |
+
}
|
163 |
+
}
|
164 |
+
</style>
|
165 |
+
</head>
|
166 |
+
<body>
|
167 |
+
<h1>Medical Symptom Checker</h1>
|
168 |
+
|
169 |
+
<div class="search-container">
|
170 |
+
<input type="text" id="symptom-search" placeholder="Enter a symptom..."/>
|
171 |
+
<button id="search-button">Search</button>
|
172 |
+
</div>
|
173 |
+
|
174 |
+
<div id="error-notification"></div>
|
175 |
+
|
176 |
+
<div class="selected-symptoms" id="selected-symptoms">
|
177 |
+
<!-- Selected symptoms will appear here -->
|
178 |
+
</div>
|
179 |
+
|
180 |
+
<div class="container">
|
181 |
+
<div class="panel">
|
182 |
+
<h2>Matching Diseases</h2>
|
183 |
+
<ul id="diseases-list"></ul>
|
184 |
+
</div>
|
185 |
+
<div class="panel">
|
186 |
+
<h2>Similar Symptoms</h2>
|
187 |
+
<ul id="similar-list"></ul>
|
188 |
+
</div>
|
189 |
+
<div class="panel">
|
190 |
+
<h2>Related Symptoms</h2>
|
191 |
+
<ul id="related-list"></ul>
|
192 |
+
</div>
|
193 |
+
</div>
|
194 |
+
|
195 |
+
<!-- Keep your JavaScript at the bottom (same as before, or I can refactor it too if you want) -->
|
196 |
+
<script>
|
197 |
+
// Store selected symptoms
|
198 |
+
let selectedSymptoms = [];
|
199 |
+
|
200 |
+
// DOM elements
|
201 |
+
const searchInput = document.getElementById('symptom-search');
|
202 |
+
const searchButton = document.getElementById('search-button');
|
203 |
+
const selectedSymptomsDiv = document.getElementById('selected-symptoms');
|
204 |
+
const diseasesList = document.getElementById('diseases-list');
|
205 |
+
const similarList = document.getElementById('similar-list');
|
206 |
+
const relatedList = document.getElementById('related-list');
|
207 |
+
const errorElement = document.getElementById('error-notification');
|
208 |
+
|
209 |
+
// Function to show error message
|
210 |
+
function showError(message) {
|
211 |
+
errorElement.textContent = message;
|
212 |
+
errorElement.style.display = 'block';
|
213 |
+
setTimeout(() => {
|
214 |
+
errorElement.style.display = 'none';
|
215 |
+
}, 5000);
|
216 |
+
}
|
217 |
+
|
218 |
+
// New function to handle both initial search and updates after selections
|
219 |
+
async function performSearch(inputSymptom) {
|
220 |
+
// Clear any previous errors
|
221 |
+
errorElement.style.display = 'none';
|
222 |
+
|
223 |
+
try {
|
224 |
+
// Create base URL with inputSymptom if provided, otherwise use first selected symptom
|
225 |
+
let symptomParam = inputSymptom;
|
226 |
+
if (!symptomParam && selectedSymptoms.length > 0) {
|
227 |
+
symptomParam = selectedSymptoms[0];
|
228 |
+
}
|
229 |
+
|
230 |
+
if (!symptomParam) return; // Exit if no symptom to search
|
231 |
+
|
232 |
+
let url = `${window.location.origin}/search?symptom=${encodeURIComponent(symptomParam)}`;
|
233 |
+
|
234 |
+
// Add remaining selected symptoms to query
|
235 |
+
if (selectedSymptoms.length > 0) {
|
236 |
+
selectedSymptoms.forEach((s, index) => {
|
237 |
+
// Skip the first one if it's the same as symptomParam
|
238 |
+
if (index === 0 && s === symptomParam) return;
|
239 |
+
url += `&selected=${encodeURIComponent(s)}`;
|
240 |
+
});
|
241 |
+
}
|
242 |
+
|
243 |
+
const response = await fetch(url);
|
244 |
+
|
245 |
+
if (!response.ok) {
|
246 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
247 |
+
}
|
248 |
+
|
249 |
+
const data = await response.json();
|
250 |
+
|
251 |
+
displayResults(data);
|
252 |
+
|
253 |
+
// Clear search input only if this was initiated from the search box
|
254 |
+
if (inputSymptom) {
|
255 |
+
searchInput.value = '';
|
256 |
+
}
|
257 |
+
} catch (error) {
|
258 |
+
showError(`Error searching for symptom: ${error.message}. Please try again.`);
|
259 |
+
console.error('Error fetching data:', error);
|
260 |
+
}
|
261 |
+
}
|
262 |
+
|
263 |
+
// Update search function to use the new performSearch function
|
264 |
+
async function searchSymptoms() {
|
265 |
+
const symptom = searchInput.value.trim();
|
266 |
+
if (!symptom) return;
|
267 |
+
performSearch(symptom);
|
268 |
+
}
|
269 |
+
|
270 |
+
// Display results
|
271 |
+
function displayResults(data) {
|
272 |
+
// Update diseases list
|
273 |
+
diseasesList.innerHTML = '';
|
274 |
+
if (!data.matching_diseases || data.matching_diseases.length === 0) {
|
275 |
+
diseasesList.innerHTML = '<li>No matching diseases found</li>';
|
276 |
+
} else {
|
277 |
+
data.matching_diseases.forEach(disease => {
|
278 |
+
diseasesList.innerHTML += `<li>${escapeHTML(disease)}</li>`;
|
279 |
+
});
|
280 |
+
}
|
281 |
+
|
282 |
+
// Update similar symptoms
|
283 |
+
similarList.innerHTML = '';
|
284 |
+
if (data.semantic_matches && data.semantic_matches.length > 0) {
|
285 |
+
data.semantic_matches.forEach(symptom => {
|
286 |
+
if (!selectedSymptoms.includes(symptom)) {
|
287 |
+
similarList.innerHTML += `<li class="clickable" onclick="addSymptom('${escapeJS(symptom)}')">${escapeHTML(symptom)}</li>`;
|
288 |
+
}
|
289 |
+
});
|
290 |
+
} else {
|
291 |
+
similarList.innerHTML = '<li>No similar symptoms found</li>';
|
292 |
+
}
|
293 |
+
|
294 |
+
// Update related symptoms - sort them alphabetically
|
295 |
+
relatedList.innerHTML = '';
|
296 |
+
if (!data.related_symptoms || data.related_symptoms.length === 0) {
|
297 |
+
relatedList.innerHTML = '<li>No related symptoms found</li>';
|
298 |
+
} else {
|
299 |
+
// Sort the related symptoms alphabetically
|
300 |
+
const sortedSymptoms = [...data.related_symptoms].sort((a, b) => a.localeCompare(b));
|
301 |
+
|
302 |
+
sortedSymptoms.forEach(symptom => {
|
303 |
+
if (!selectedSymptoms.includes(symptom)) {
|
304 |
+
relatedList.innerHTML += `<li class="clickable" onclick="addSymptom('${escapeJS(symptom)}')">${escapeHTML(symptom)}</li>`;
|
305 |
+
}
|
306 |
+
});
|
307 |
+
}
|
308 |
+
|
309 |
+
// Update selected symptoms
|
310 |
+
updateSelectedSymptoms();
|
311 |
+
}
|
312 |
+
|
313 |
+
// Helper function to escape HTML
|
314 |
+
function escapeHTML(str) {
|
315 |
+
return str
|
316 |
+
.replace(/&/g, '&')
|
317 |
+
.replace(/</g, '<')
|
318 |
+
.replace(/>/g, '>')
|
319 |
+
.replace(/"/g, '"')
|
320 |
+
.replace(/'/g, ''');
|
321 |
+
}
|
322 |
+
|
323 |
+
// Helper function to escape strings for JavaScript
|
324 |
+
function escapeJS(str) {
|
325 |
+
return str
|
326 |
+
.replace(/\\/g, '\\\\')
|
327 |
+
.replace(/'/g, "\\'")
|
328 |
+
.replace(/"/g, '\\"')
|
329 |
+
.replace(/\n/g, '\\n')
|
330 |
+
.replace(/\r/g, '\\r')
|
331 |
+
.replace(/\t/g, '\\t');
|
332 |
+
}
|
333 |
+
|
334 |
+
// Add a symptom to the selected list
|
335 |
+
function addSymptom(symptom) {
|
336 |
+
if (!selectedSymptoms.includes(symptom)) {
|
337 |
+
selectedSymptoms.push(symptom);
|
338 |
+
updateSelectedSymptoms();
|
339 |
+
|
340 |
+
// Instead of calling searchSymptoms which expects input from search box,
|
341 |
+
// directly make a request with the current selected symptoms
|
342 |
+
performSearch(null);
|
343 |
+
}
|
344 |
+
}
|
345 |
+
|
346 |
+
// Remove a symptom from the selected list
|
347 |
+
function removeSymptom(symptom) {
|
348 |
+
selectedSymptoms = selectedSymptoms.filter(s => s !== symptom);
|
349 |
+
updateSelectedSymptoms();
|
350 |
+
if (selectedSymptoms.length > 0) {
|
351 |
+
performSearch(null);
|
352 |
+
} else {
|
353 |
+
// Clear results if no symptoms are selected
|
354 |
+
diseasesList.innerHTML = '<li>Enter a symptom to get started</li>';
|
355 |
+
similarList.innerHTML = '';
|
356 |
+
relatedList.innerHTML = '';
|
357 |
+
}
|
358 |
+
}
|
359 |
+
|
360 |
+
// Update the selected symptoms display
|
361 |
+
function updateSelectedSymptoms() {
|
362 |
+
selectedSymptomsDiv.innerHTML = '';
|
363 |
+
selectedSymptoms.forEach(symptom => {
|
364 |
+
const tag = document.createElement('div');
|
365 |
+
tag.className = 'symptom-tag';
|
366 |
+
tag.innerHTML = `
|
367 |
+
${escapeHTML(symptom)}
|
368 |
+
<span class="remove-symptom" onclick="removeSymptom('${escapeJS(symptom)}')">×</span>
|
369 |
+
`;
|
370 |
+
selectedSymptomsDiv.appendChild(tag);
|
371 |
+
});
|
372 |
+
}
|
373 |
+
|
374 |
+
// Event listeners
|
375 |
+
searchButton.addEventListener('click', searchSymptoms);
|
376 |
+
searchInput.addEventListener('keypress', function(e) {
|
377 |
+
if (e.key === 'Enter') {
|
378 |
+
searchSymptoms();
|
379 |
+
}
|
380 |
+
});
|
381 |
+
|
382 |
+
// Initialize
|
383 |
+
diseasesList.innerHTML = '<li>Enter a symptom to get started</li>';
|
384 |
+
|
385 |
+
// Paste your exact same JS from earlier here — no changes needed.
|
386 |
+
// Let me know if you'd like animation when adding/removing tags.
|
387 |
+
</script>
|
388 |
+
</body>
|
389 |
+
</html>
|
utils/__pycache__/sql_queries.cpython-311.pyc
ADDED
Binary file (3.94 kB). View file
|
|
utils/__pycache__/vector_search.cpython-311.pyc
ADDED
Binary file (1.55 kB). View file
|
|
utils/sql_queries.py
ADDED
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import sqlite3
|
2 |
+
|
3 |
+
def get_diseases_by_symptoms(symptoms):
|
4 |
+
"""
|
5 |
+
Get diseases that have ALL the given symptoms.
|
6 |
+
|
7 |
+
Parameters:
|
8 |
+
- symptoms: List of symptom names
|
9 |
+
"""
|
10 |
+
if not symptoms:
|
11 |
+
return []
|
12 |
+
|
13 |
+
conn = sqlite3.connect("disease.db")
|
14 |
+
cur = conn.cursor()
|
15 |
+
|
16 |
+
try:
|
17 |
+
placeholder = ",".join(["?"] * len(symptoms))
|
18 |
+
|
19 |
+
# Find diseases that have ALL the symptoms
|
20 |
+
query = f"""
|
21 |
+
SELECT d.name
|
22 |
+
FROM disease d
|
23 |
+
WHERE (
|
24 |
+
SELECT COUNT(DISTINCT s.name)
|
25 |
+
FROM disease_symptom ds
|
26 |
+
JOIN symptom s ON s.id = ds.symptom_id
|
27 |
+
WHERE ds.disease_id = d.id AND s.name IN ({placeholder})
|
28 |
+
) = ?
|
29 |
+
"""
|
30 |
+
|
31 |
+
cur.execute(query, symptoms + [len(symptoms)])
|
32 |
+
result = [row[0] for row in cur.fetchall()]
|
33 |
+
return result
|
34 |
+
except Exception as e:
|
35 |
+
print(f"SQL Error in get_diseases_by_symptoms: {e}")
|
36 |
+
return []
|
37 |
+
finally:
|
38 |
+
conn.close()
|
39 |
+
|
40 |
+
def get_related_symptoms(disease_names, exclude=[]):
|
41 |
+
"""
|
42 |
+
Get ALL symptoms from the specified diseases, excluding the ones provided.
|
43 |
+
|
44 |
+
Parameters:
|
45 |
+
- disease_names: List of disease names to get symptoms from
|
46 |
+
- exclude: List of symptom names to exclude from the results
|
47 |
+
"""
|
48 |
+
if not disease_names:
|
49 |
+
return []
|
50 |
+
|
51 |
+
conn = sqlite3.connect("disease.db")
|
52 |
+
cur = conn.cursor()
|
53 |
+
|
54 |
+
try:
|
55 |
+
placeholder = ",".join(["?"] * len(disease_names))
|
56 |
+
exclude_clause = ""
|
57 |
+
if exclude:
|
58 |
+
exclude_placeholder = ",".join(["?"] * len(exclude))
|
59 |
+
exclude_clause = f"AND s.name NOT IN ({exclude_placeholder})"
|
60 |
+
|
61 |
+
# Get ALL OTHER symptoms from the matching diseases
|
62 |
+
query = f"""
|
63 |
+
SELECT DISTINCT s.name
|
64 |
+
FROM symptom s
|
65 |
+
JOIN disease_symptom ds ON s.id = ds.symptom_id
|
66 |
+
JOIN disease d ON d.id = ds.disease_id
|
67 |
+
WHERE d.name IN ({placeholder}) {exclude_clause}
|
68 |
+
"""
|
69 |
+
|
70 |
+
cur.execute(query, disease_names + (exclude if exclude else []))
|
71 |
+
result = [row[0] for row in cur.fetchall()]
|
72 |
+
return result
|
73 |
+
except Exception as e:
|
74 |
+
print(f"SQL Error in get_related_symptoms: {e}")
|
75 |
+
return []
|
76 |
+
finally:
|
77 |
+
conn.close()
|
utils/vector_search.py
ADDED
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from qdrant_client import QdrantClient
|
2 |
+
from sentence_transformers import SentenceTransformer
|
3 |
+
import os
|
4 |
+
from dotenv import load_dotenv
|
5 |
+
|
6 |
+
load_dotenv()
|
7 |
+
|
8 |
+
# Initialize Qdrant Cloud client
|
9 |
+
url=os.environ.get("QDRANT_URL")
|
10 |
+
api_key=os.environ.get("QDRANT_API_KEY")
|
11 |
+
qdrant_client = QdrantClient(
|
12 |
+
url=url, # Use environment variable
|
13 |
+
api_key=api_key, # Use environment variable
|
14 |
+
timeout=30.0
|
15 |
+
)
|
16 |
+
|
17 |
+
# Initialize SentenceTransformer model
|
18 |
+
model = SentenceTransformer("all-MiniLM-L6-v2")
|
19 |
+
|
20 |
+
# Collection name (must match the one used during upsert)
|
21 |
+
collection_name = "symptoms"
|
22 |
+
|
23 |
+
def get_similar_symptoms(symptom):
|
24 |
+
# Encode the input symptom
|
25 |
+
vector = model.encode(symptom)
|
26 |
+
|
27 |
+
# Perform similarity search
|
28 |
+
results = qdrant_client.search(
|
29 |
+
collection_name=collection_name,
|
30 |
+
query_vector=vector.tolist(),
|
31 |
+
limit=5, # Return top 5 similar symptoms
|
32 |
+
with_payload=True # Include payload (e.g., symptom name)
|
33 |
+
)
|
34 |
+
|
35 |
+
# Extract symptom names from results
|
36 |
+
similar_symptoms = [result.payload["name"] for result in results]
|
37 |
+
return similar_symptoms
|