Spaces:
Running
Running
Commit
·
75e2b6c
1
Parent(s):
ba09305
update
Browse files- .dockerignore +39 -0
- .gitignore +44 -0
- Dockerfile +60 -0
- LICENSE +201 -0
- README.md +7 -8
- app.py +5 -0
- app/__init__.py +8 -0
- app/config/__init__.py +3 -0
- app/config/config.py +7 -0
- app/database/__init__.py +4 -0
- app/database/database_query.py +684 -0
- app/database/db.py +21 -0
- app/main.py +40 -0
- app/middleware/auth.py +47 -0
- app/routers/admin.py +96 -0
- app/routers/auth.py +291 -0
- app/routers/chat.py +385 -0
- app/routers/chat_session.py +97 -0
- app/routers/language.py +60 -0
- app/routers/location.py +37 -0
- app/routers/preferences.py +51 -0
- app/routers/profile.py +113 -0
- app/routers/questionnaire.py +100 -0
- app/services/MagicConvert.py +685 -0
- app/services/RAG_evaluation.py +85 -0
- app/services/__init__.py +32 -0
- app/services/chat_processor.py +278 -0
- app/services/chathistory.py +309 -0
- app/services/environmental_condition.py +70 -0
- app/services/image_classification_vit.py +110 -0
- app/services/image_processor.py +459 -0
- app/services/llm_model.py +102 -0
- app/services/prompts.py +483 -0
- app/services/report_process.py +93 -0
- app/services/skincare_scheduler.py +62 -0
- app/services/vector_database_search.py +100 -0
- app/services/websearch.py +141 -0
- app/services/wheel.py +94 -0
- docker-compose.yml +13 -0
- pyproject.toml +49 -0
.dockerignore
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Version control
|
2 |
+
.git
|
3 |
+
.gitignore
|
4 |
+
|
5 |
+
# Environment files
|
6 |
+
.env
|
7 |
+
.venv
|
8 |
+
env/
|
9 |
+
venv/
|
10 |
+
ENV/
|
11 |
+
|
12 |
+
# Python cache files
|
13 |
+
__pycache__/
|
14 |
+
*.py[cod]
|
15 |
+
*$py.class
|
16 |
+
*.so
|
17 |
+
.Python
|
18 |
+
.pytest_cache/
|
19 |
+
.coverage
|
20 |
+
htmlcov/
|
21 |
+
|
22 |
+
# Build directories
|
23 |
+
build/
|
24 |
+
dist/
|
25 |
+
*.egg-info/
|
26 |
+
|
27 |
+
# Data directories that should be mounted as volumes
|
28 |
+
temp/
|
29 |
+
uploads/
|
30 |
+
|
31 |
+
# IDE files
|
32 |
+
.idea/
|
33 |
+
.vscode/
|
34 |
+
*.swp
|
35 |
+
*.swo
|
36 |
+
|
37 |
+
# OS specific files
|
38 |
+
.DS_Store
|
39 |
+
Thumbs.db
|
.gitignore
ADDED
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Flask specific
|
2 |
+
instance/*
|
3 |
+
!instance/.gitignore
|
4 |
+
.webassets-cache
|
5 |
+
.env
|
6 |
+
|
7 |
+
# Python related
|
8 |
+
__pycache__/
|
9 |
+
*.py[cod]
|
10 |
+
*$py.class
|
11 |
+
*.so
|
12 |
+
.Python
|
13 |
+
build/
|
14 |
+
develop-eggs/
|
15 |
+
dist/
|
16 |
+
downloads/
|
17 |
+
eggs/
|
18 |
+
.eggs/
|
19 |
+
lib/
|
20 |
+
lib64/
|
21 |
+
parts/
|
22 |
+
sdist/
|
23 |
+
var/
|
24 |
+
wheels/
|
25 |
+
*.egg-info/
|
26 |
+
.installed.cfg
|
27 |
+
*.egg
|
28 |
+
|
29 |
+
# Virtual Environment
|
30 |
+
.venv/
|
31 |
+
venv/
|
32 |
+
ENV/
|
33 |
+
|
34 |
+
# IDE specific (common ones)
|
35 |
+
.idea/
|
36 |
+
.vscode/
|
37 |
+
*.swp
|
38 |
+
*.swo
|
39 |
+
.DS_Store
|
40 |
+
|
41 |
+
# Local development
|
42 |
+
*.log
|
43 |
+
.env.local
|
44 |
+
.env.*.local
|
Dockerfile
ADDED
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Use Python 3.12 as the base image
|
2 |
+
FROM python:3.12-slim
|
3 |
+
|
4 |
+
# Set working directory in the container
|
5 |
+
WORKDIR /app
|
6 |
+
|
7 |
+
# Install system dependencies including Tesseract OCR
|
8 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
9 |
+
gcc \
|
10 |
+
python3-dev \
|
11 |
+
tesseract-ocr \
|
12 |
+
libtesseract-dev \
|
13 |
+
tesseract-ocr-eng \
|
14 |
+
&& apt-get clean \
|
15 |
+
&& rm -rf /var/lib/apt/lists/*
|
16 |
+
|
17 |
+
# Create a non-root user and set ownership
|
18 |
+
RUN useradd -m -u 1000 appuser && \
|
19 |
+
chown -R appuser:appuser /app
|
20 |
+
|
21 |
+
# Copy requirements first to leverage Docker cache
|
22 |
+
COPY pyproject.toml .
|
23 |
+
|
24 |
+
# Install Python dependencies
|
25 |
+
RUN pip install --no-cache-dir --upgrade pip && \
|
26 |
+
pip install --no-cache-dir .
|
27 |
+
|
28 |
+
# Copy the rest of the application
|
29 |
+
COPY . .
|
30 |
+
|
31 |
+
# Create ALL needed directories with proper permissions
|
32 |
+
RUN mkdir -p temp uploads \
|
33 |
+
/app/.cache \
|
34 |
+
/app/nltk_data \
|
35 |
+
/app/app/routers/temp \
|
36 |
+
/app/app/config/temp && \
|
37 |
+
chown -R appuser:appuser /app && \
|
38 |
+
chmod -R 777 temp uploads /app/.cache /app/nltk_data /app/app/routers/temp /app/app/config/temp
|
39 |
+
|
40 |
+
# Set environment variables for cache directories and Tesseract
|
41 |
+
ENV HF_HOME=/app/.cache/huggingface \
|
42 |
+
TRANSFORMERS_CACHE=/app/.cache/huggingface/transformers \
|
43 |
+
PYTORCH_PRETRAINED_BERT_CACHE=/app/.cache/torch \
|
44 |
+
NLTK_DATA=/app/nltk_data \
|
45 |
+
XDG_CACHE_HOME=/app/.cache \
|
46 |
+
TESSDATA_PREFIX=/usr/share/tesseract-ocr/5/tessdata \
|
47 |
+
TESSERACT_CMD=/usr/bin/tesseract \
|
48 |
+
PATH=/usr/bin:$PATH
|
49 |
+
|
50 |
+
# Verify Tesseract installation
|
51 |
+
RUN tesseract --version
|
52 |
+
|
53 |
+
# Switch to non-root user
|
54 |
+
USER appuser
|
55 |
+
|
56 |
+
# Expose the port that Hugging Face Spaces expects
|
57 |
+
EXPOSE 7860
|
58 |
+
|
59 |
+
# Command to run the application using Uvicorn on port 7860
|
60 |
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
LICENSE
ADDED
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Apache License
|
2 |
+
Version 2.0, January 2004
|
3 |
+
http://www.apache.org/licenses/
|
4 |
+
|
5 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
6 |
+
|
7 |
+
1. Definitions.
|
8 |
+
|
9 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
10 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
11 |
+
|
12 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
13 |
+
the copyright owner that is granting the License.
|
14 |
+
|
15 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
16 |
+
other entities that control, are controlled by, or are under common
|
17 |
+
control with that entity. For the purposes of this definition,
|
18 |
+
"control" means (i) the power, direct or indirect, to cause the
|
19 |
+
direction or management of such entity, whether by contract or
|
20 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
21 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
22 |
+
|
23 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
24 |
+
exercising permissions granted by this License.
|
25 |
+
|
26 |
+
"Source" form shall mean the preferred form for making modifications,
|
27 |
+
including but not limited to software source code, documentation
|
28 |
+
source, and configuration files.
|
29 |
+
|
30 |
+
"Object" form shall mean any form resulting from mechanical
|
31 |
+
transformation or translation of a Source form, including but
|
32 |
+
not limited to compiled object code, generated documentation,
|
33 |
+
and conversions to other media types.
|
34 |
+
|
35 |
+
"Work" shall mean the work of authorship, whether in Source or
|
36 |
+
Object form, made available under the License, as indicated by a
|
37 |
+
copyright notice that is included in or attached to the work
|
38 |
+
(an example is provided in the Appendix below).
|
39 |
+
|
40 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
41 |
+
form, that is based on (or derived from) the Work and for which the
|
42 |
+
editorial revisions, annotations, elaborations, or other modifications
|
43 |
+
represent, as a whole, an original work of authorship. For the purposes
|
44 |
+
of this License, Derivative Works shall not include works that remain
|
45 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
46 |
+
the Work and Derivative Works thereof.
|
47 |
+
|
48 |
+
"Contribution" shall mean any work of authorship, including
|
49 |
+
the original version of the Work and any modifications or additions
|
50 |
+
to that Work or Derivative Works thereof, that is intentionally
|
51 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
52 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
53 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
54 |
+
means any form of electronic, verbal, or written communication sent
|
55 |
+
to the Licensor or its representatives, including but not limited to
|
56 |
+
communication on electronic mailing lists, source code control systems,
|
57 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
58 |
+
Licensor for the purpose of discussing and improving the Work, but
|
59 |
+
excluding communication that is conspicuously marked or otherwise
|
60 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
61 |
+
|
62 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
63 |
+
on behalf of whom a Contribution has been received by Licensor and
|
64 |
+
subsequently incorporated within the Work.
|
65 |
+
|
66 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
67 |
+
this License, each Contributor hereby grants to You a perpetual,
|
68 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
69 |
+
copyright license to reproduce, prepare Derivative Works of,
|
70 |
+
publicly display, publicly perform, sublicense, and distribute the
|
71 |
+
Work and such Derivative Works in Source or Object form.
|
72 |
+
|
73 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
74 |
+
this License, each Contributor hereby grants to You a perpetual,
|
75 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
76 |
+
(except as stated in this section) patent license to make, have made,
|
77 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
78 |
+
where such license applies only to those patent claims licensable
|
79 |
+
by such Contributor that are necessarily infringed by their
|
80 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
81 |
+
with the Work to which such Contribution(s) was submitted. If You
|
82 |
+
institute patent litigation against any entity (including a
|
83 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
84 |
+
or a Contribution incorporated within the Work constitutes direct
|
85 |
+
or contributory patent infringement, then any patent licenses
|
86 |
+
granted to You under this License for that Work shall terminate
|
87 |
+
as of the date such litigation is filed.
|
88 |
+
|
89 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
90 |
+
Work or Derivative Works thereof in any medium, with or without
|
91 |
+
modifications, and in Source or Object form, provided that You
|
92 |
+
meet the following conditions:
|
93 |
+
|
94 |
+
(a) You must give any other recipients of the Work or
|
95 |
+
Derivative Works a copy of this License; and
|
96 |
+
|
97 |
+
(b) You must cause any modified files to carry prominent notices
|
98 |
+
stating that You changed the files; and
|
99 |
+
|
100 |
+
(c) You must retain, in the Source form of any Derivative Works
|
101 |
+
that You distribute, all copyright, patent, trademark, and
|
102 |
+
attribution notices from the Source form of the Work,
|
103 |
+
excluding those notices that do not pertain to any part of
|
104 |
+
the Derivative Works; and
|
105 |
+
|
106 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
107 |
+
distribution, then any Derivative Works that You distribute must
|
108 |
+
include a readable copy of the attribution notices contained
|
109 |
+
within such NOTICE file, excluding those notices that do not
|
110 |
+
pertain to any part of the Derivative Works, in at least one
|
111 |
+
of the following places: within a NOTICE text file distributed
|
112 |
+
as part of the Derivative Works; within the Source form or
|
113 |
+
documentation, if provided along with the Derivative Works; or,
|
114 |
+
within a display generated by the Derivative Works, if and
|
115 |
+
wherever such third-party notices normally appear. The contents
|
116 |
+
of the NOTICE file are for informational purposes only and
|
117 |
+
do not modify the License. You may add Your own attribution
|
118 |
+
notices within Derivative Works that You distribute, alongside
|
119 |
+
or as an addendum to the NOTICE text from the Work, provided
|
120 |
+
that such additional attribution notices cannot be construed
|
121 |
+
as modifying the License.
|
122 |
+
|
123 |
+
You may add Your own copyright statement to Your modifications and
|
124 |
+
may provide additional or different license terms and conditions
|
125 |
+
for use, reproduction, or distribution of Your modifications, or
|
126 |
+
for any such Derivative Works as a whole, provided Your use,
|
127 |
+
reproduction, and distribution of the Work otherwise complies with
|
128 |
+
the conditions stated in this License.
|
129 |
+
|
130 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
131 |
+
any Contribution intentionally submitted for inclusion in the Work
|
132 |
+
by You to the Licensor shall be under the terms and conditions of
|
133 |
+
this License, without any additional terms or conditions.
|
134 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
135 |
+
the terms of any separate license agreement you may have executed
|
136 |
+
with Licensor regarding such Contributions.
|
137 |
+
|
138 |
+
6. Trademarks. This License does not grant permission to use the trade
|
139 |
+
names, trademarks, service marks, or product names of the Licensor,
|
140 |
+
except as required for reasonable and customary use in describing the
|
141 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
142 |
+
|
143 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
144 |
+
agreed to in writing, Licensor provides the Work (and each
|
145 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
146 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
147 |
+
implied, including, without limitation, any warranties or conditions
|
148 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
149 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
150 |
+
appropriateness of using or redistributing the Work and assume any
|
151 |
+
risks associated with Your exercise of permissions under this License.
|
152 |
+
|
153 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
154 |
+
whether in tort (including negligence), contract, or otherwise,
|
155 |
+
unless required by applicable law (such as deliberate and grossly
|
156 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
157 |
+
liable to You for damages, including any direct, indirect, special,
|
158 |
+
incidental, or consequential damages of any character arising as a
|
159 |
+
result of this License or out of the use or inability to use the
|
160 |
+
Work (including but not limited to damages for loss of goodwill,
|
161 |
+
work stoppage, computer failure or malfunction, or any and all
|
162 |
+
other commercial damages or losses), even if such Contributor
|
163 |
+
has been advised of the possibility of such damages.
|
164 |
+
|
165 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
166 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
167 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
168 |
+
or other liability obligations and/or rights consistent with this
|
169 |
+
License. However, in accepting such obligations, You may act only
|
170 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
171 |
+
of any other Contributor, and only if You agree to indemnify,
|
172 |
+
defend, and hold each Contributor harmless for any liability
|
173 |
+
incurred by, or claims asserted against, such Contributor by reason
|
174 |
+
of your accepting any such warranty or additional liability.
|
175 |
+
|
176 |
+
END OF TERMS AND CONDITIONS
|
177 |
+
|
178 |
+
APPENDIX: How to apply the Apache License to your work.
|
179 |
+
|
180 |
+
To apply the Apache License to your work, attach the following
|
181 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
182 |
+
replaced with your own identifying information. (Don't include
|
183 |
+
the brackets!) The text should be enclosed in the appropriate
|
184 |
+
comment syntax for the file format. We also recommend that a
|
185 |
+
file or class name and description of purpose be included on the
|
186 |
+
same "printed page" as the copyright notice for easier
|
187 |
+
identification within third-party archives.
|
188 |
+
|
189 |
+
Copyright [yyyy] [name of copyright owner]
|
190 |
+
|
191 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
192 |
+
you may not use this file except in compliance with the License.
|
193 |
+
You may obtain a copy of the License at
|
194 |
+
|
195 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
196 |
+
|
197 |
+
Unless required by applicable law or agreed to in writing, software
|
198 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
199 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
200 |
+
See the License for the specific language governing permissions and
|
201 |
+
limitations under the License.
|
README.md
CHANGED
@@ -1,11 +1,10 @@
|
|
1 |
---
|
2 |
-
title:
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
5 |
-
colorTo:
|
6 |
sdk: docker
|
|
|
|
|
7 |
pinned: false
|
8 |
-
|
9 |
-
---
|
10 |
-
|
11 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
1 |
---
|
2 |
+
title: DermAI
|
3 |
+
emoji: 🐳
|
4 |
+
colorFrom: gray
|
5 |
+
colorTo: blue
|
6 |
sdk: docker
|
7 |
+
sdk_version: "1.0.0"
|
8 |
+
app_file: app.py
|
9 |
pinned: false
|
10 |
+
---
|
|
|
|
|
|
app.py
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import uvicorn
|
2 |
+
from app.main import app
|
3 |
+
|
4 |
+
if __name__ == "__main__":
|
5 |
+
uvicorn.run("app.main:app", host="0.0.0.0", port=5000, reload=True)
|
app/__init__.py
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# app/__init__.py
|
2 |
+
from app.main import app
|
3 |
+
|
4 |
+
__all__ = [
|
5 |
+
"app",
|
6 |
+
]
|
7 |
+
|
8 |
+
|
app/config/__init__.py
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
from app.config.config import Config
|
2 |
+
|
3 |
+
config = Config()
|
app/config/config.py
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
|
3 |
+
class Config:
|
4 |
+
JWT_SECRET_KEY = os.getenv('JWT_SECRET_KEY')
|
5 |
+
JWT_ACCESS_TOKEN_EXPIRES = int(os.getenv('JWT_ACCESS_TOKEN_EXPIRES'))
|
6 |
+
CORS_ORIGINS = ["http://localhost:3000"]
|
7 |
+
UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'temp')
|
app/database/__init__.py
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from app.database.db import get_db, db
|
2 |
+
from app.database.database_query import DatabaseQuery
|
3 |
+
|
4 |
+
__all__ = ["get_db", "db", "DatabaseQuery"]
|
app/database/database_query.py
ADDED
@@ -0,0 +1,684 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from app.database.db import db
|
2 |
+
import re
|
3 |
+
from bson import ObjectId
|
4 |
+
from datetime import datetime, timezone, timedelta
|
5 |
+
from pymongo import DESCENDING
|
6 |
+
from typing import Optional
|
7 |
+
|
8 |
+
|
9 |
+
class DatabaseQuery:
|
10 |
+
def __init__(self):
|
11 |
+
pass
|
12 |
+
|
13 |
+
def create_chat_session(self, chat_session):
|
14 |
+
try:
|
15 |
+
db.chat_sessions.insert_one(chat_session)
|
16 |
+
except Exception as e:
|
17 |
+
raise Exception(f"Error creating chat session: {str(e)}")
|
18 |
+
|
19 |
+
def get_user_chat_sessions(self, user_id):
|
20 |
+
try:
|
21 |
+
sessions = list(db.chat_sessions.find(
|
22 |
+
{"user_id": user_id},
|
23 |
+
{"_id": 0}
|
24 |
+
).sort("last_accessed", -1))
|
25 |
+
return sessions
|
26 |
+
except Exception as e:
|
27 |
+
raise Exception(f"Error retrieving user chat sessions: {str(e)}")
|
28 |
+
|
29 |
+
def create_chat(self, chat_data):
|
30 |
+
try:
|
31 |
+
db.chats.insert_one(chat_data)
|
32 |
+
return True
|
33 |
+
except Exception as e:
|
34 |
+
raise Exception(f"Error creating chat: {str(e)}")
|
35 |
+
|
36 |
+
def update_last_accessed_time(self, session_id):
|
37 |
+
try:
|
38 |
+
db.chat_sessions.update_one(
|
39 |
+
{"session_id": session_id},
|
40 |
+
{"$set": {"last_accessed": datetime.now(timezone.utc)}}
|
41 |
+
)
|
42 |
+
except Exception as e:
|
43 |
+
raise Exception(f"Error updating last accessed time: {str(e)}")
|
44 |
+
|
45 |
+
def get_session_chats(self, session_id, user_id):
|
46 |
+
try:
|
47 |
+
chats = list(db.chats.find(
|
48 |
+
{"session_id": session_id, "user_id": user_id},
|
49 |
+
{"_id": 0}
|
50 |
+
).sort("timestamp", 1))
|
51 |
+
return chats
|
52 |
+
except Exception as e:
|
53 |
+
raise Exception(f"Error retrieving session chats: {str(e)}")
|
54 |
+
|
55 |
+
def get_user_by_identifier(self, identifier):
|
56 |
+
try:
|
57 |
+
user = db.users.find_one({'$or': [{'username': identifier}, {'email': identifier}]})
|
58 |
+
return user
|
59 |
+
except Exception as e:
|
60 |
+
raise Exception(f"Error retrieving user by identifier: {str(e)}")
|
61 |
+
|
62 |
+
def add_token_to_blacklist(self, jti):
|
63 |
+
try:
|
64 |
+
db.blacklist.insert_one({'jti': jti})
|
65 |
+
except Exception as e:
|
66 |
+
raise Exception(f"Error adding token to blacklist: {str(e)}")
|
67 |
+
|
68 |
+
|
69 |
+
def create_indexes(self):
|
70 |
+
try:
|
71 |
+
db.chat_sessions.create_index([("user_id", 1), ("last_accessed", -1)])
|
72 |
+
db.chat_sessions.create_index([("session_id", 1)])
|
73 |
+
db.chats.create_index([("session_id", 1), ("timestamp", 1)])
|
74 |
+
db.chats.create_index([("user_id", 1)])
|
75 |
+
except Exception as e:
|
76 |
+
raise Exception(f"Error creating indexes: {str(e)}")
|
77 |
+
|
78 |
+
def check_chat_session(self, session_id):
|
79 |
+
try:
|
80 |
+
chat_session = db.chat_sessions.find_one({'session_id': session_id})
|
81 |
+
return chat_session is not None
|
82 |
+
except Exception as e:
|
83 |
+
raise Exception(f"Error checking chat session: {str(e)}")
|
84 |
+
|
85 |
+
def get_user_profile(self, username):
|
86 |
+
try:
|
87 |
+
user = db.users.find_one({'username': username}, {'password': 0})
|
88 |
+
return user
|
89 |
+
except Exception as e:
|
90 |
+
raise Exception(f"Error getting user profile: {str(e)}")
|
91 |
+
|
92 |
+
def update_user_profile(self, username, update_fields):
|
93 |
+
try:
|
94 |
+
result = db.users.update_one(
|
95 |
+
{'username': username},
|
96 |
+
{'$set': update_fields}
|
97 |
+
)
|
98 |
+
return result.modified_count > 0
|
99 |
+
except Exception as e:
|
100 |
+
raise Exception(f"Error updating user profile: {str(e)}")
|
101 |
+
|
102 |
+
def delete_user_account(self, username):
|
103 |
+
try:
|
104 |
+
result = db.users.delete_one({'username': username})
|
105 |
+
return result.deleted_count > 0
|
106 |
+
except Exception as e:
|
107 |
+
raise Exception(f"Error deleting user account: {str(e)}")
|
108 |
+
|
109 |
+
def is_username_or_email_exists(self, username, email):
|
110 |
+
try:
|
111 |
+
user = db.users.find_one({'$or': [{'username': username}, {'email': email}]})
|
112 |
+
return user is not None
|
113 |
+
except Exception as e:
|
114 |
+
raise Exception(f"Error checking if username or email exists: {str(e)}")
|
115 |
+
|
116 |
+
def create_or_update_temp_user(self, username, email, temp_user_data):
|
117 |
+
try:
|
118 |
+
db.temp_users.update_one(
|
119 |
+
{'$or': [{'username': username}, {'email': email}]},
|
120 |
+
{'$set': temp_user_data},
|
121 |
+
upsert=True
|
122 |
+
)
|
123 |
+
except Exception as e:
|
124 |
+
raise Exception(f"Error creating/updating temp user: {str(e)}")
|
125 |
+
|
126 |
+
def get_temp_user_by_username(self, username):
|
127 |
+
try:
|
128 |
+
temp_user = db.temp_users.find_one({'username': username})
|
129 |
+
return temp_user
|
130 |
+
except Exception as e:
|
131 |
+
raise Exception(f"Error retrieving temp user by username: {str(e)}")
|
132 |
+
|
133 |
+
def delete_temp_user(self, username):
|
134 |
+
try:
|
135 |
+
db.temp_users.delete_one({'username': username})
|
136 |
+
except Exception as e:
|
137 |
+
raise Exception(f"Error deleting temp user: {str(e)}")
|
138 |
+
|
139 |
+
def create_user_from_data(self, user_data):
|
140 |
+
try:
|
141 |
+
db.users.insert_one(user_data)
|
142 |
+
return user_data
|
143 |
+
except Exception as e:
|
144 |
+
raise Exception(f"Error creating user from data: {str(e)}")
|
145 |
+
|
146 |
+
def create_user(self, username, email, hashed_password, name, age, created_at,
|
147 |
+
is_verified=False, verification_code=None, code_expiration=None):
|
148 |
+
try:
|
149 |
+
new_user = {
|
150 |
+
'username': username,
|
151 |
+
'email': email,
|
152 |
+
'password': hashed_password,
|
153 |
+
'name': name,
|
154 |
+
'age': age,
|
155 |
+
'created_at': created_at,
|
156 |
+
'is_verified': is_verified
|
157 |
+
}
|
158 |
+
if verification_code and code_expiration:
|
159 |
+
new_user['verification_code'] = verification_code
|
160 |
+
new_user['code_expiration'] = code_expiration
|
161 |
+
|
162 |
+
db.users.insert_one(new_user)
|
163 |
+
return new_user
|
164 |
+
except Exception as e:
|
165 |
+
raise Exception(f"Error creating user: {str(e)}")
|
166 |
+
|
167 |
+
def get_user_by_username(self, username):
|
168 |
+
try:
|
169 |
+
user = db.users.find_one({'username': username})
|
170 |
+
return user
|
171 |
+
except Exception as e:
|
172 |
+
raise Exception(f"Error retrieving user by username: {str(e)}")
|
173 |
+
|
174 |
+
def verify_user_email(self, username):
|
175 |
+
try:
|
176 |
+
result = db.users.update_one(
|
177 |
+
{'username': username},
|
178 |
+
{'$set': {'is_verified': True}, '$unset': {'verification_code': '', 'code_expiration': ''}}
|
179 |
+
)
|
180 |
+
return result.modified_count > 0
|
181 |
+
except Exception as e:
|
182 |
+
raise Exception(f"Error verifying user email: {str(e)}")
|
183 |
+
|
184 |
+
def update_verification_code(self, username, verification_code, code_expiration):
|
185 |
+
try:
|
186 |
+
result = db.users.update_one(
|
187 |
+
{'username': username},
|
188 |
+
{'$set': {'verification_code': verification_code, 'code_expiration': code_expiration}}
|
189 |
+
)
|
190 |
+
return result.modified_count > 0
|
191 |
+
except Exception as e:
|
192 |
+
raise Exception(f"Error updating verification code: {str(e)}")
|
193 |
+
|
194 |
+
def is_valid_email(self, email):
|
195 |
+
try:
|
196 |
+
email_regex = r'^\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}\b'
|
197 |
+
return re.match(email_regex, email) is not None
|
198 |
+
except Exception as e:
|
199 |
+
raise Exception(f"Error validating email: {str(e)}")
|
200 |
+
|
201 |
+
def add_or_update_location(self, username, location):
|
202 |
+
try:
|
203 |
+
db.locations.update_one(
|
204 |
+
{'username': username},
|
205 |
+
{'$set': {'location': location, 'updated_at': datetime.now(timezone.utc)}},
|
206 |
+
upsert=True
|
207 |
+
)
|
208 |
+
except Exception as e:
|
209 |
+
raise Exception(f"Error adding/updating location: {str(e)}")
|
210 |
+
|
211 |
+
def get_location(self, username):
|
212 |
+
try:
|
213 |
+
location = db.locations.find_one({'username': username})
|
214 |
+
return location
|
215 |
+
except Exception as e:
|
216 |
+
raise Exception(f"Error retrieving location: {str(e)}")
|
217 |
+
|
218 |
+
def submit_questionnaire(self, user_id, answers):
|
219 |
+
try:
|
220 |
+
questionnaire_data = {
|
221 |
+
'user_id': user_id,
|
222 |
+
'answers': answers,
|
223 |
+
'created_at': datetime.now(timezone.utc),
|
224 |
+
'updated_at': datetime.now(timezone.utc)
|
225 |
+
}
|
226 |
+
result = db.questionnaires.insert_one(questionnaire_data)
|
227 |
+
return str(result.inserted_id)
|
228 |
+
except Exception as e:
|
229 |
+
raise Exception(f"Error submitting questionnaire: {str(e)}")
|
230 |
+
|
231 |
+
def get_latest_questionnaire(self, user_id):
|
232 |
+
try:
|
233 |
+
questionnaire = db.questionnaires.find_one(
|
234 |
+
{'user_id': user_id},
|
235 |
+
sort=[('created_at', -1)]
|
236 |
+
)
|
237 |
+
if questionnaire:
|
238 |
+
questionnaire['_id'] = str(questionnaire['_id'])
|
239 |
+
return questionnaire
|
240 |
+
except Exception as e:
|
241 |
+
raise Exception(f"Error getting latest questionnaire: {str(e)}")
|
242 |
+
|
243 |
+
def update_questionnaire(self, questionnaire_id, user_id, answers):
|
244 |
+
try:
|
245 |
+
result = db.questionnaires.update_one(
|
246 |
+
{'_id': ObjectId(questionnaire_id), 'user_id': user_id},
|
247 |
+
{
|
248 |
+
'$set': {
|
249 |
+
'answers': answers,
|
250 |
+
'updated_at': datetime.now(timezone.utc)
|
251 |
+
}
|
252 |
+
}
|
253 |
+
)
|
254 |
+
return result.modified_count > 0
|
255 |
+
except Exception as e:
|
256 |
+
raise Exception(f"Error updating questionnaire: {str(e)}")
|
257 |
+
|
258 |
+
def delete_questionnaire(self, questionnaire_id, user_id):
|
259 |
+
try:
|
260 |
+
result = db.questionnaires.delete_one(
|
261 |
+
{'_id': ObjectId(questionnaire_id), 'user_id': user_id}
|
262 |
+
)
|
263 |
+
return result.deleted_count > 0
|
264 |
+
except Exception as e:
|
265 |
+
raise Exception(f"Error deleting questionnaire: {str(e)}")
|
266 |
+
|
267 |
+
|
268 |
+
def count_answered_questions(self, username):
|
269 |
+
try:
|
270 |
+
answered_count = db.questions.count_documents({
|
271 |
+
'username': username,
|
272 |
+
'answer': {'$ne': None}
|
273 |
+
})
|
274 |
+
return answered_count
|
275 |
+
except Exception as e:
|
276 |
+
raise Exception(f"Error counting answered questions: {str(e)}")
|
277 |
+
|
278 |
+
def get_user_preferences(self, username):
|
279 |
+
try:
|
280 |
+
user_preferences = db.preferences.find_one({'username': username})
|
281 |
+
if not user_preferences:
|
282 |
+
return {
|
283 |
+
'keywords': False,
|
284 |
+
'references': False,
|
285 |
+
'websearch': False,
|
286 |
+
'personalized_recommendations': False,
|
287 |
+
'environmental_recommendations': False
|
288 |
+
}
|
289 |
+
return {
|
290 |
+
'keywords': user_preferences.get('keywords', False),
|
291 |
+
'references': user_preferences.get('references', False),
|
292 |
+
'websearch': user_preferences.get('websearch', False),
|
293 |
+
'personalized_recommendations': user_preferences.get('personalized_recommendations', False),
|
294 |
+
'environmental_recommendations': user_preferences.get('environmental_recommendations', False)
|
295 |
+
}
|
296 |
+
except Exception as e:
|
297 |
+
raise Exception(f"Error getting user preferences: {str(e)}")
|
298 |
+
|
299 |
+
def set_user_preferences(self, username, preferences):
|
300 |
+
try:
|
301 |
+
preferences_data = {
|
302 |
+
'username': username,
|
303 |
+
'keywords': bool(preferences.get('keywords', False)),
|
304 |
+
'references': bool(preferences.get('references', False)),
|
305 |
+
'websearch': bool(preferences.get('websearch', False)),
|
306 |
+
'personalized_recommendations': bool(preferences.get('personalized_recommendations', False)),
|
307 |
+
'environmental_recommendations': bool(preferences.get('environmental_recommendations', False)),
|
308 |
+
'updated_at': datetime.now(timezone.utc)
|
309 |
+
}
|
310 |
+
result = db.preferences.update_one(
|
311 |
+
{'username': username},
|
312 |
+
{'$set': preferences_data},
|
313 |
+
upsert=True
|
314 |
+
)
|
315 |
+
return preferences_data
|
316 |
+
except Exception as e:
|
317 |
+
raise Exception(f"Error setting user preferences: {str(e)}")
|
318 |
+
|
319 |
+
def get_user_theme(self, username):
|
320 |
+
try:
|
321 |
+
user_theme = db.user_themes.find_one({'username': username})
|
322 |
+
if not user_theme:
|
323 |
+
return 'light'
|
324 |
+
return user_theme.get('theme', 'light')
|
325 |
+
except Exception as e:
|
326 |
+
raise Exception(f"Error getting user theme: {str(e)}")
|
327 |
+
|
328 |
+
def set_user_theme(self, username, theme):
|
329 |
+
try:
|
330 |
+
theme_data = {
|
331 |
+
'username': username,
|
332 |
+
'theme': "dark" if theme else "light",
|
333 |
+
'updated_at': datetime.now(timezone.utc)
|
334 |
+
}
|
335 |
+
db.user_themes.update_one(
|
336 |
+
{'username': username},
|
337 |
+
{'$set': theme_data},
|
338 |
+
upsert=True
|
339 |
+
)
|
340 |
+
return theme_data
|
341 |
+
except Exception as e:
|
342 |
+
raise Exception(f"Error setting user theme: {str(e)}")
|
343 |
+
|
344 |
+
|
345 |
+
def verify_session(self, session_id, user_id):
|
346 |
+
try:
|
347 |
+
session = db.chat_sessions.find_one({
|
348 |
+
"session_id": session_id,
|
349 |
+
"user_id": user_id
|
350 |
+
})
|
351 |
+
return session is not None
|
352 |
+
except Exception as e:
|
353 |
+
raise Exception(f"Error verifying session: {str(e)}")
|
354 |
+
|
355 |
+
def update_chat_session_title(self, session_id, new_title):
|
356 |
+
try:
|
357 |
+
result = db.chat_sessions.update_one(
|
358 |
+
{"session_id": session_id},
|
359 |
+
{"$set": {"title": new_title}}
|
360 |
+
)
|
361 |
+
if result.matched_count == 0:
|
362 |
+
raise Exception("Chat session not found")
|
363 |
+
return result.modified_count > 0
|
364 |
+
except Exception as e:
|
365 |
+
raise Exception(f"Error updating chat session title: {str(e)}")
|
366 |
+
|
367 |
+
def delete_chat_session(self, session_id, user_id):
|
368 |
+
try:
|
369 |
+
session_result = db.chat_sessions.delete_one({
|
370 |
+
"session_id": session_id,
|
371 |
+
"user_id": user_id
|
372 |
+
})
|
373 |
+
chats_result = db.chats.delete_many({
|
374 |
+
"session_id": session_id,
|
375 |
+
"user_id": user_id
|
376 |
+
})
|
377 |
+
|
378 |
+
return {
|
379 |
+
"session_deleted": session_result.deleted_count > 0,
|
380 |
+
"chats_deleted": chats_result.deleted_count
|
381 |
+
}
|
382 |
+
except Exception as e:
|
383 |
+
raise Exception(f"Error deleting chat session and chats: {str(e)}")
|
384 |
+
|
385 |
+
def delete_all_user_sessions_and_chats(self, user_id):
|
386 |
+
try:
|
387 |
+
chats_result = db.chats.delete_many({"user_id": user_id})
|
388 |
+
sessions_result = db.chat_sessions.delete_many({"user_id": user_id})
|
389 |
+
return {
|
390 |
+
"deleted_chats": chats_result.deleted_count,
|
391 |
+
"deleted_sessions": sessions_result.deleted_count
|
392 |
+
}
|
393 |
+
except Exception as e:
|
394 |
+
raise Exception(f"Error deleting user sessions and chats: {str(e)}")
|
395 |
+
|
396 |
+
def get_all_user_chats(self, user_id):
|
397 |
+
try:
|
398 |
+
sessions = list(db.chat_sessions.find(
|
399 |
+
{"user_id": user_id},
|
400 |
+
{"_id": 0}
|
401 |
+
).sort("last_accessed", -1))
|
402 |
+
all_chats = []
|
403 |
+
for session in sessions:
|
404 |
+
session_chats = list(db.chats.find(
|
405 |
+
{"session_id": session["session_id"], "user_id": user_id},
|
406 |
+
{"_id": 0}
|
407 |
+
).sort("timestamp", 1))
|
408 |
+
|
409 |
+
all_chats.append({
|
410 |
+
"session_id": session["session_id"],
|
411 |
+
"title": session.get("title", "New Chat"),
|
412 |
+
"created_at": session.get("created_at"),
|
413 |
+
"last_accessed": session.get("last_accessed"),
|
414 |
+
"chats": session_chats
|
415 |
+
})
|
416 |
+
|
417 |
+
return all_chats
|
418 |
+
except Exception as e:
|
419 |
+
raise Exception(f"Error retrieving all user chats: {str(e)}")
|
420 |
+
|
421 |
+
def store_reset_token(self, email, token, expiration):
|
422 |
+
try:
|
423 |
+
db.password_resets.update_one(
|
424 |
+
{'email': email},
|
425 |
+
{
|
426 |
+
'$set': {
|
427 |
+
'token': token,
|
428 |
+
'expiration': expiration
|
429 |
+
}
|
430 |
+
},
|
431 |
+
upsert=True
|
432 |
+
)
|
433 |
+
except Exception as e:
|
434 |
+
raise Exception(f"Error storing reset token: {str(e)}")
|
435 |
+
|
436 |
+
def verify_reset_token(self, token):
|
437 |
+
try:
|
438 |
+
reset_info = db.password_resets.find_one({
|
439 |
+
'token': token,
|
440 |
+
'expiration': {'$gt': datetime.now(timezone.utc)}
|
441 |
+
})
|
442 |
+
return reset_info
|
443 |
+
except Exception as e:
|
444 |
+
raise Exception(f"Error verifying reset token: {str(e)}")
|
445 |
+
|
446 |
+
def update_password(self, email, hashed_password):
|
447 |
+
try:
|
448 |
+
db.users.update_one(
|
449 |
+
{'email': email},
|
450 |
+
{'$set': {'password': hashed_password}}
|
451 |
+
)
|
452 |
+
except Exception as e:
|
453 |
+
raise Exception(f"Error updating password: {str(e)}")
|
454 |
+
|
455 |
+
def delete_reset_token(self, token):
|
456 |
+
try:
|
457 |
+
db.password_resets.delete_one({'token': token})
|
458 |
+
except Exception as e:
|
459 |
+
raise Exception(f"Error deleting reset token: {str(e)}")
|
460 |
+
|
461 |
+
def delete_account_permanently(self, username):
|
462 |
+
try:
|
463 |
+
chat_deletion_result = self.delete_all_user_sessions_and_chats(username)
|
464 |
+
preferences_result = db.preferences.delete_one({'username': username})
|
465 |
+
theme_result = db.user_themes.delete_one({'username': username})
|
466 |
+
location_result = db.locations.delete_one({'username': username})
|
467 |
+
questionnaire_result = db.questionnaires.delete_many({'user_id': username})
|
468 |
+
user_result = db.users.delete_one({'username': username})
|
469 |
+
|
470 |
+
return {
|
471 |
+
'success': True,
|
472 |
+
'deleted_data': {
|
473 |
+
'chats': chat_deletion_result['deleted_chats'],
|
474 |
+
'chat_sessions': chat_deletion_result['deleted_sessions'],
|
475 |
+
'preferences': preferences_result.deleted_count,
|
476 |
+
'theme': theme_result.deleted_count,
|
477 |
+
'location': location_result.deleted_count,
|
478 |
+
'questionnaires': questionnaire_result.deleted_count,
|
479 |
+
'user_account': user_result.deleted_count
|
480 |
+
}
|
481 |
+
}
|
482 |
+
except Exception as e:
|
483 |
+
raise Exception(f"Error deleting account permanently: {str(e)}")
|
484 |
+
|
485 |
+
def store_reset_token(self, email, token, expiration):
|
486 |
+
try:
|
487 |
+
db.password_resets.update_one(
|
488 |
+
{'email': email},
|
489 |
+
{
|
490 |
+
'$set': {
|
491 |
+
'token': token,
|
492 |
+
'expiration': expiration
|
493 |
+
}
|
494 |
+
},
|
495 |
+
upsert=True
|
496 |
+
)
|
497 |
+
except Exception as e:
|
498 |
+
raise Exception(f"Error storing reset token: {str(e)}")
|
499 |
+
|
500 |
+
def verify_reset_token(self, token):
|
501 |
+
try:
|
502 |
+
reset_info = db.password_resets.find_one({
|
503 |
+
'token': token,
|
504 |
+
'expiration': {'$gt': datetime.now(timezone.utc)}
|
505 |
+
})
|
506 |
+
return reset_info
|
507 |
+
except Exception as e:
|
508 |
+
raise Exception(f"Error verifying reset token: {str(e)}")
|
509 |
+
|
510 |
+
def update_password(self, email, new_password):
|
511 |
+
try:
|
512 |
+
db.users.update_one(
|
513 |
+
{'email': email},
|
514 |
+
{'$set': {'password': new_password}}
|
515 |
+
)
|
516 |
+
except Exception as e:
|
517 |
+
raise Exception(f"Error updating password: {str(e)}")
|
518 |
+
|
519 |
+
def get_user_language(self, user_id):
|
520 |
+
try:
|
521 |
+
language = db.languages.find_one({'user_id': user_id})
|
522 |
+
return language.get('language') if language else None
|
523 |
+
except Exception as e:
|
524 |
+
raise Exception(f"Error retrieving user language: {str(e)}")
|
525 |
+
|
526 |
+
def set_user_language(self, user_id, language):
|
527 |
+
try:
|
528 |
+
language_data = {
|
529 |
+
'user_id': user_id,
|
530 |
+
'language': language,
|
531 |
+
'updated_at': datetime.now(timezone.utc)
|
532 |
+
}
|
533 |
+
result = db.languages.update_one(
|
534 |
+
{'user_id': user_id},
|
535 |
+
{'$set': language_data},
|
536 |
+
upsert=True
|
537 |
+
)
|
538 |
+
return language_data
|
539 |
+
except Exception as e:
|
540 |
+
raise Exception(f"Error setting user language: {str(e)}")
|
541 |
+
|
542 |
+
def delete_user_language(self, user_id):
|
543 |
+
try:
|
544 |
+
result = db.languages.delete_one({'user_id': user_id})
|
545 |
+
return result.deleted_count > 0
|
546 |
+
except Exception as e:
|
547 |
+
raise Exception(f"Error deleting user language: {str(e)}")
|
548 |
+
|
549 |
+
def get_today_schedule(self, user_id):
|
550 |
+
try:
|
551 |
+
# Get today's date at midnight UTC
|
552 |
+
today = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
|
553 |
+
tomorrow = today.replace(hour=23, minute=59, second=59)
|
554 |
+
|
555 |
+
schedule = db.skin_schedules.find_one({
|
556 |
+
"user_id": user_id,
|
557 |
+
"created_at": {
|
558 |
+
"$gte": today,
|
559 |
+
"$lte": tomorrow
|
560 |
+
}
|
561 |
+
})
|
562 |
+
return schedule
|
563 |
+
except Exception as e:
|
564 |
+
raise Exception(f"Error retrieving today's schedule: {str(e)}")
|
565 |
+
|
566 |
+
def save_schedule(self, user_id, schedule_data):
|
567 |
+
try:
|
568 |
+
existing_schedule = self.get_today_schedule(user_id)
|
569 |
+
if existing_schedule:
|
570 |
+
return str(existing_schedule["_id"])
|
571 |
+
|
572 |
+
schedule = {
|
573 |
+
"user_id": user_id,
|
574 |
+
"schedule_data": schedule_data,
|
575 |
+
"created_at": datetime.now(timezone.utc)
|
576 |
+
}
|
577 |
+
result = db.skin_schedules.insert_one(schedule)
|
578 |
+
return str(result.inserted_id)
|
579 |
+
except Exception as e:
|
580 |
+
raise Exception(f"Error saving schedule: {str(e)}")
|
581 |
+
|
582 |
+
def get_last_seven_days_schedules(self, user_id):
|
583 |
+
try:
|
584 |
+
seven_days_ago = datetime.now(timezone.utc) - timedelta(days=7)
|
585 |
+
schedules = db.skin_schedules.find({
|
586 |
+
"user_id": user_id,
|
587 |
+
"created_at": {"$gte": seven_days_ago}
|
588 |
+
}).sort("created_at", -1)
|
589 |
+
return list(schedules)
|
590 |
+
except Exception as e:
|
591 |
+
raise Exception(f"Error fetching last 7 days schedules: {str(e)}")
|
592 |
+
|
593 |
+
def save_rag_interaction(self, user_id: str, session_id: str, context: str, query: str,
|
594 |
+
response: str, rag_start_time: datetime, rag_end_time: datetime):
|
595 |
+
try:
|
596 |
+
interaction = {
|
597 |
+
"interaction_id": str(ObjectId()),
|
598 |
+
"user_id": user_id,
|
599 |
+
"session_id": session_id,
|
600 |
+
"context": context,
|
601 |
+
"query": query,
|
602 |
+
"response": response,
|
603 |
+
"rag_start_time": rag_start_time.astimezone(timezone.utc),
|
604 |
+
"rag_end_time": rag_end_time.astimezone(timezone.utc),
|
605 |
+
"created_at": datetime.now(timezone.utc)
|
606 |
+
}
|
607 |
+
|
608 |
+
result = db.rag_interactions.insert_one(interaction)
|
609 |
+
return interaction["interaction_id"]
|
610 |
+
|
611 |
+
except Exception as e:
|
612 |
+
raise Exception(f"Error saving RAG interaction: {str(e)}")
|
613 |
+
|
614 |
+
def get_rag_interactions(
|
615 |
+
self,
|
616 |
+
user_id: Optional[str] = None,
|
617 |
+
page: int = 1,
|
618 |
+
page_size: int = 5
|
619 |
+
) -> dict:
|
620 |
+
try:
|
621 |
+
query_filter = {}
|
622 |
+
if user_id:
|
623 |
+
query_filter["user_id"] = user_id
|
624 |
+
|
625 |
+
skip = (page - 1) * page_size
|
626 |
+
total = db.rag_interactions.count_documents(query_filter)
|
627 |
+
interactions = db.rag_interactions.find(
|
628 |
+
query_filter,
|
629 |
+
{"_id": 0}
|
630 |
+
).sort("created_at", DESCENDING).skip(skip).limit(page_size)
|
631 |
+
|
632 |
+
result_list = []
|
633 |
+
for interaction in interactions:
|
634 |
+
interaction["rag_start_time"] = interaction["rag_start_time"].isoformat()
|
635 |
+
interaction["rag_end_time"] = interaction["rag_end_time"].isoformat()
|
636 |
+
interaction["created_at"] = interaction["created_at"].isoformat()
|
637 |
+
result_list.append(interaction)
|
638 |
+
|
639 |
+
return {
|
640 |
+
"total_interactions": total,
|
641 |
+
"page": page,
|
642 |
+
"page_size": page_size,
|
643 |
+
"total_pages": (total + page_size - 1) // page_size,
|
644 |
+
"results": result_list
|
645 |
+
}
|
646 |
+
except Exception as e:
|
647 |
+
raise Exception(f"Error retrieving RAG interactions: {str(e)}")
|
648 |
+
|
649 |
+
def log_image_upload(self, user_id):
|
650 |
+
"""Log an image upload for a user"""
|
651 |
+
try:
|
652 |
+
timestamp = datetime.now(timezone.utc) # This is timezone-aware
|
653 |
+
db.image_uploads.insert_one({
|
654 |
+
"user_id": user_id,
|
655 |
+
"timestamp": timestamp
|
656 |
+
})
|
657 |
+
return True
|
658 |
+
except Exception as e:
|
659 |
+
raise Exception(f"Error logging image upload: {str(e)}")
|
660 |
+
|
661 |
+
def get_user_daily_uploads(self, user_id):
|
662 |
+
"""Get number of images uploaded by user in the last 24 hours"""
|
663 |
+
try:
|
664 |
+
now = datetime.now(timezone.utc)
|
665 |
+
yesterday = now - timedelta(days=1)
|
666 |
+
|
667 |
+
count = db.image_uploads.count_documents({
|
668 |
+
"user_id": user_id,
|
669 |
+
"timestamp": {"$gte": yesterday}
|
670 |
+
})
|
671 |
+
return count
|
672 |
+
except Exception as e:
|
673 |
+
raise Exception(f"Error retrieving user daily uploads: {str(e)}")
|
674 |
+
|
675 |
+
def get_user_last_upload_time(self, user_id):
|
676 |
+
"""Get the timestamp of user's most recent image upload"""
|
677 |
+
try:
|
678 |
+
last_upload = db.image_uploads.find_one(
|
679 |
+
{"user_id": user_id},
|
680 |
+
sort=[("timestamp", DESCENDING)]
|
681 |
+
)
|
682 |
+
return last_upload["timestamp"] if last_upload else None
|
683 |
+
except Exception as e:
|
684 |
+
raise Exception(f"Error retrieving last upload time: {str(e)}")
|
app/database/db.py
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
from pymongo.mongo_client import MongoClient
|
3 |
+
from pymongo.server_api import ServerApi
|
4 |
+
|
5 |
+
|
6 |
+
uri = os.getenv('MONGO_URI')
|
7 |
+
|
8 |
+
mongo_uri = os.getenv('MONGO_URI')
|
9 |
+
if not mongo_uri:
|
10 |
+
raise ValueError("MONGO_URI environment variable is not set")
|
11 |
+
|
12 |
+
|
13 |
+
def get_db():
|
14 |
+
client = MongoClient(uri, server_api=ServerApi('1'))
|
15 |
+
try:
|
16 |
+
client.admin.command('ping')
|
17 |
+
except Exception as e:
|
18 |
+
print(e)
|
19 |
+
return client.get_database("dermai")
|
20 |
+
|
21 |
+
db = get_db()
|
app/main.py
ADDED
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# app/main.py
|
2 |
+
from fastapi import FastAPI
|
3 |
+
from fastapi.middleware.cors import CORSMiddleware
|
4 |
+
from fastapi.staticfiles import StaticFiles
|
5 |
+
import os
|
6 |
+
from dotenv import load_dotenv
|
7 |
+
from app.config.config import Config
|
8 |
+
from app.routers import admin, auth, chat, location, preferences, profile, questionnaire, language, chat_session
|
9 |
+
|
10 |
+
load_dotenv()
|
11 |
+
|
12 |
+
app = FastAPI(title="Skin AI API")
|
13 |
+
|
14 |
+
# Configure CORS
|
15 |
+
app.add_middleware(
|
16 |
+
CORSMiddleware,
|
17 |
+
allow_origins=["*"],
|
18 |
+
allow_credentials=True,
|
19 |
+
allow_methods=["*"],
|
20 |
+
allow_headers=["*"],
|
21 |
+
)
|
22 |
+
|
23 |
+
# Mount static files for uploads
|
24 |
+
os.makedirs(Config.UPLOAD_FOLDER, exist_ok=True)
|
25 |
+
app.mount("/uploads", StaticFiles(directory=Config.UPLOAD_FOLDER), name="uploads")
|
26 |
+
|
27 |
+
# Register routers
|
28 |
+
app.include_router(admin.router, prefix="/api", tags=["admin"])
|
29 |
+
app.include_router(auth.router, prefix="/api", tags=["auth"])
|
30 |
+
app.include_router(chat.router, prefix="/api", tags=["chat"])
|
31 |
+
app.include_router(location.router, prefix="/api", tags=["location"])
|
32 |
+
app.include_router(preferences.router, prefix="/api", tags=["preferences"])
|
33 |
+
app.include_router(profile.router, prefix="/api", tags=["profile"])
|
34 |
+
app.include_router(questionnaire.router, prefix="/api", tags=["questionnaire"])
|
35 |
+
app.include_router(language.router, prefix="/api", tags=["language"])
|
36 |
+
app.include_router(chat_session.router, prefix="/api", tags=["chat_session"])
|
37 |
+
|
38 |
+
@app.get("/")
|
39 |
+
async def root():
|
40 |
+
return {"message": "API is running", "status": "healthy"}
|
app/middleware/auth.py
ADDED
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# app/middleware/auth.py
|
2 |
+
from fastapi import Depends, HTTPException, status
|
3 |
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
4 |
+
import jwt
|
5 |
+
from datetime import datetime, timedelta
|
6 |
+
import os
|
7 |
+
|
8 |
+
security = HTTPBearer()
|
9 |
+
JWT_SECRET_KEY = os.getenv('JWT_SECRET_KEY')
|
10 |
+
JWT_ACCESS_TOKEN_EXPIRES = int(os.getenv('JWT_ACCESS_TOKEN_EXPIRES'))
|
11 |
+
|
12 |
+
def create_access_token(data: dict):
|
13 |
+
to_encode = data.copy()
|
14 |
+
expire = datetime.utcnow() + timedelta(seconds=JWT_ACCESS_TOKEN_EXPIRES)
|
15 |
+
to_encode.update({"exp": expire})
|
16 |
+
encoded_jwt = jwt.encode(to_encode, JWT_SECRET_KEY, algorithm="HS256")
|
17 |
+
return encoded_jwt
|
18 |
+
|
19 |
+
def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
|
20 |
+
try:
|
21 |
+
payload = jwt.decode(credentials.credentials, JWT_SECRET_KEY, algorithms=["HS256"])
|
22 |
+
username: str = payload.get("sub")
|
23 |
+
if username is None:
|
24 |
+
raise HTTPException(
|
25 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
26 |
+
detail="Invalid authentication credentials",
|
27 |
+
headers={"WWW-Authenticate": "Bearer"},
|
28 |
+
)
|
29 |
+
return username
|
30 |
+
except jwt.PyJWTError:
|
31 |
+
raise HTTPException(
|
32 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
33 |
+
detail="Invalid authentication credentials",
|
34 |
+
headers={"WWW-Authenticate": "Bearer"},
|
35 |
+
)
|
36 |
+
|
37 |
+
def get_current_user(username: str = Depends(verify_token)):
|
38 |
+
return username
|
39 |
+
|
40 |
+
# For optional JWT authentication (some endpoints allow unauthenticated access)
|
41 |
+
def get_optional_user(authorization: HTTPAuthorizationCredentials = Depends(security)):
|
42 |
+
try:
|
43 |
+
payload = jwt.decode(authorization.credentials, JWT_SECRET_KEY, algorithms=["HS256"])
|
44 |
+
username: str = payload.get("sub")
|
45 |
+
return username
|
46 |
+
except:
|
47 |
+
return None
|
app/routers/admin.py
ADDED
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# app/routers/admin.py
|
2 |
+
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
|
3 |
+
from typing import List
|
4 |
+
import os
|
5 |
+
from app.database.database_query import DatabaseQuery
|
6 |
+
from app.services.vector_database_search import VectorDatabaseSearch
|
7 |
+
from app.middleware.auth import get_current_user
|
8 |
+
from pydantic import BaseModel
|
9 |
+
|
10 |
+
router = APIRouter()
|
11 |
+
vector_db = VectorDatabaseSearch()
|
12 |
+
query = DatabaseQuery()
|
13 |
+
TEMP_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'temp')
|
14 |
+
os.makedirs(TEMP_DIR, exist_ok=True)
|
15 |
+
|
16 |
+
class SearchQuery(BaseModel):
|
17 |
+
query: str
|
18 |
+
k: int = 5
|
19 |
+
|
20 |
+
@router.get('/books')
|
21 |
+
async def get_books(username: str = Depends(get_current_user)):
|
22 |
+
try:
|
23 |
+
book_info = vector_db.get_book_info()
|
24 |
+
return {
|
25 |
+
'status': 'success',
|
26 |
+
'data': book_info
|
27 |
+
}
|
28 |
+
except Exception as e:
|
29 |
+
raise HTTPException(status_code=500, detail=str(e))
|
30 |
+
|
31 |
+
@router.post('/books', status_code=201)
|
32 |
+
async def add_books(files: List[UploadFile] = File(...), username: str = Depends(get_current_user)):
|
33 |
+
try:
|
34 |
+
pdf_paths = []
|
35 |
+
for file in files:
|
36 |
+
if file.filename.endswith('.pdf'):
|
37 |
+
safe_filename = os.path.basename(file.filename)
|
38 |
+
temp_path = os.path.join(TEMP_DIR, safe_filename)
|
39 |
+
|
40 |
+
with open(temp_path, "wb") as buffer:
|
41 |
+
content = await file.read()
|
42 |
+
buffer.write(content)
|
43 |
+
|
44 |
+
pdf_paths.append(temp_path)
|
45 |
+
|
46 |
+
if not pdf_paths:
|
47 |
+
raise HTTPException(status_code=400, detail="No valid PDF files provided")
|
48 |
+
|
49 |
+
success_count = 0
|
50 |
+
for pdf_path in pdf_paths:
|
51 |
+
if vector_db.add_pdf(pdf_path):
|
52 |
+
success_count += 1
|
53 |
+
|
54 |
+
# Clean up temporary files
|
55 |
+
for path in pdf_paths:
|
56 |
+
try:
|
57 |
+
if os.path.exists(path):
|
58 |
+
os.remove(path)
|
59 |
+
except Exception:
|
60 |
+
pass
|
61 |
+
|
62 |
+
return {
|
63 |
+
'status': 'success',
|
64 |
+
'message': f'Successfully added {success_count} of {len(pdf_paths)} books'
|
65 |
+
}
|
66 |
+
|
67 |
+
except Exception as e:
|
68 |
+
# Clean up temporary files in case of error
|
69 |
+
for path in pdf_paths:
|
70 |
+
try:
|
71 |
+
if os.path.exists(path):
|
72 |
+
os.remove(path)
|
73 |
+
except:
|
74 |
+
pass
|
75 |
+
|
76 |
+
if isinstance(e, HTTPException):
|
77 |
+
raise e
|
78 |
+
raise HTTPException(status_code=500, detail=str(e))
|
79 |
+
|
80 |
+
@router.post('/search')
|
81 |
+
async def search_books(search_data: SearchQuery, username: str = Depends(get_current_user)):
|
82 |
+
try:
|
83 |
+
query_text = search_data.query
|
84 |
+
k = search_data.k
|
85 |
+
|
86 |
+
results = vector_db.search(
|
87 |
+
query=query_text,
|
88 |
+
top_k=k
|
89 |
+
)
|
90 |
+
|
91 |
+
return {
|
92 |
+
'status': 'success',
|
93 |
+
'data': results
|
94 |
+
}
|
95 |
+
except Exception as e:
|
96 |
+
raise HTTPException(status_code=500, detail=str(e))
|
app/routers/auth.py
ADDED
@@ -0,0 +1,291 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# app/routers/auth.py
|
2 |
+
from fastapi import APIRouter, HTTPException, Depends
|
3 |
+
from pydantic import BaseModel, EmailStr
|
4 |
+
from werkzeug.security import generate_password_hash, check_password_hash
|
5 |
+
from datetime import datetime, timedelta
|
6 |
+
import random
|
7 |
+
import string
|
8 |
+
from sendgrid import SendGridAPIClient
|
9 |
+
from sendgrid.helpers.mail import Mail
|
10 |
+
import os
|
11 |
+
from app.database.database_query import DatabaseQuery
|
12 |
+
from app.middleware.auth import create_access_token, get_current_user
|
13 |
+
from dotenv import load_dotenv
|
14 |
+
|
15 |
+
|
16 |
+
load_dotenv()
|
17 |
+
|
18 |
+
SENDGRID_API_KEY = os.getenv("SENDGRID_API_KEY")
|
19 |
+
FROM_EMAIL = os.getenv("FROM_EMAIL")
|
20 |
+
|
21 |
+
|
22 |
+
router = APIRouter()
|
23 |
+
query = DatabaseQuery()
|
24 |
+
|
25 |
+
class LoginRequest(BaseModel):
|
26 |
+
identifier: str
|
27 |
+
password: str
|
28 |
+
|
29 |
+
class LoginResponse(BaseModel):
|
30 |
+
message: str
|
31 |
+
token: str
|
32 |
+
|
33 |
+
class RegisterRequest(BaseModel):
|
34 |
+
username: str
|
35 |
+
email: EmailStr
|
36 |
+
password: str
|
37 |
+
name: str
|
38 |
+
age: int
|
39 |
+
|
40 |
+
class VerifyEmailRequest(BaseModel):
|
41 |
+
username: str
|
42 |
+
code: str
|
43 |
+
|
44 |
+
class ResendCodeRequest(BaseModel):
|
45 |
+
username: str
|
46 |
+
|
47 |
+
class ForgotPasswordRequest(BaseModel):
|
48 |
+
email: EmailStr
|
49 |
+
|
50 |
+
class ResetPasswordRequest(BaseModel):
|
51 |
+
token: str
|
52 |
+
password: str
|
53 |
+
|
54 |
+
class ChatSessionCheck(BaseModel):
|
55 |
+
session_id: str
|
56 |
+
|
57 |
+
@router.post('/login', response_model=LoginResponse)
|
58 |
+
async def login(login_data: LoginRequest):
|
59 |
+
try:
|
60 |
+
identifier = login_data.identifier
|
61 |
+
password = login_data.password
|
62 |
+
|
63 |
+
user = query.get_user_by_identifier(identifier)
|
64 |
+
if user:
|
65 |
+
if not user.get('is_verified'):
|
66 |
+
raise HTTPException(status_code=401, detail="Please verify your email before logging in")
|
67 |
+
|
68 |
+
if check_password_hash(user['password'], password):
|
69 |
+
access_token = create_access_token({"sub": user['username']})
|
70 |
+
return {"message": "Login successful", "token": access_token}
|
71 |
+
|
72 |
+
raise HTTPException(status_code=401, detail="Invalid username/email or password")
|
73 |
+
except Exception as e:
|
74 |
+
if isinstance(e, HTTPException):
|
75 |
+
raise e
|
76 |
+
raise HTTPException(status_code=500, detail=str(e))
|
77 |
+
|
78 |
+
@router.post('/register', status_code=201)
|
79 |
+
async def register(register_data: RegisterRequest):
|
80 |
+
try:
|
81 |
+
username = register_data.username
|
82 |
+
email = register_data.email
|
83 |
+
password = register_data.password
|
84 |
+
name = register_data.name
|
85 |
+
age = register_data.age
|
86 |
+
|
87 |
+
if query.is_username_or_email_exists(username, email):
|
88 |
+
raise HTTPException(status_code=409, detail="Username or email already exists")
|
89 |
+
|
90 |
+
verification_code = ''.join(random.choices(string.digits, k=6))
|
91 |
+
code_expiration = datetime.utcnow() + timedelta(minutes=10)
|
92 |
+
hashed_password = generate_password_hash(password)
|
93 |
+
created_at = datetime.utcnow()
|
94 |
+
|
95 |
+
temp_user = {
|
96 |
+
'username': username,
|
97 |
+
'email': email,
|
98 |
+
'password': hashed_password,
|
99 |
+
'name': name,
|
100 |
+
'age': age,
|
101 |
+
'created_at': created_at,
|
102 |
+
'verification_code': verification_code,
|
103 |
+
'code_expiration': code_expiration
|
104 |
+
}
|
105 |
+
|
106 |
+
query.create_or_update_temp_user(username, email, temp_user)
|
107 |
+
|
108 |
+
message = Mail(
|
109 |
+
from_email=FROM_EMAIL,
|
110 |
+
to_emails=email,
|
111 |
+
subject='Verify your email address',
|
112 |
+
html_content=f'''
|
113 |
+
<p>Hi {name},</p>
|
114 |
+
<p>Thank you for registering. Please use the following code to verify your email address:</p>
|
115 |
+
<h2>{verification_code}</h2>
|
116 |
+
<p>This code will expire in 10 minutes.</p>
|
117 |
+
'''
|
118 |
+
)
|
119 |
+
|
120 |
+
try:
|
121 |
+
sg = SendGridAPIClient(SENDGRID_API_KEY)
|
122 |
+
sg.send(message)
|
123 |
+
except Exception as e:
|
124 |
+
raise HTTPException(status_code=500, detail="Failed to send verification email")
|
125 |
+
|
126 |
+
return {"message": "Registration successful. A verification code has been sent to your email."}
|
127 |
+
except Exception as e:
|
128 |
+
if isinstance(e, HTTPException):
|
129 |
+
raise e
|
130 |
+
raise HTTPException(status_code=500, detail=str(e))
|
131 |
+
|
132 |
+
@router.post('/verify-email')
|
133 |
+
async def verify_email(verify_data: VerifyEmailRequest):
|
134 |
+
try:
|
135 |
+
username = verify_data.username
|
136 |
+
code = verify_data.code
|
137 |
+
|
138 |
+
temp_user = query.get_temp_user_by_username(username)
|
139 |
+
if not temp_user:
|
140 |
+
raise HTTPException(status_code=404, detail="User not found or already verified")
|
141 |
+
|
142 |
+
if temp_user['verification_code'] != code:
|
143 |
+
raise HTTPException(status_code=400, detail="Invalid verification code")
|
144 |
+
|
145 |
+
if datetime.utcnow() > temp_user['code_expiration']:
|
146 |
+
raise HTTPException(status_code=400, detail="Verification code has expired")
|
147 |
+
|
148 |
+
user_data = temp_user.copy()
|
149 |
+
user_data['is_verified'] = True
|
150 |
+
user_data.pop('verification_code', None)
|
151 |
+
user_data.pop('code_expiration', None)
|
152 |
+
user_data.pop('_id', None)
|
153 |
+
|
154 |
+
query.create_user_from_data(user_data)
|
155 |
+
query.delete_temp_user(username)
|
156 |
+
|
157 |
+
# Set default language to English
|
158 |
+
query.set_user_language(username, "English")
|
159 |
+
|
160 |
+
# Set default theme to light (passing false for dark theme)
|
161 |
+
query.set_user_theme(username, False)
|
162 |
+
|
163 |
+
default_preferences = {
|
164 |
+
'keywords': True,
|
165 |
+
'references': True,
|
166 |
+
'websearch': False,
|
167 |
+
'personalized_recommendations': True,
|
168 |
+
'environmental_recommendations': True
|
169 |
+
}
|
170 |
+
|
171 |
+
query.set_user_preferences(username, default_preferences)
|
172 |
+
|
173 |
+
return {"message": "Email verification successful"}
|
174 |
+
except Exception as e:
|
175 |
+
if isinstance(e, HTTPException):
|
176 |
+
raise e
|
177 |
+
raise HTTPException(status_code=500, detail=str(e))
|
178 |
+
|
179 |
+
@router.post('/resend-code')
|
180 |
+
async def resend_code(resend_data: ResendCodeRequest):
|
181 |
+
try:
|
182 |
+
username = resend_data.username
|
183 |
+
|
184 |
+
temp_user = query.get_temp_user_by_username(username)
|
185 |
+
if not temp_user:
|
186 |
+
raise HTTPException(status_code=404, detail="User not found or already verified")
|
187 |
+
|
188 |
+
verification_code = ''.join(random.choices(string.digits, k=6))
|
189 |
+
code_expiration = datetime.utcnow() + timedelta(minutes=10)
|
190 |
+
|
191 |
+
temp_user['verification_code'] = verification_code
|
192 |
+
temp_user['code_expiration'] = code_expiration
|
193 |
+
|
194 |
+
query.create_or_update_temp_user(username, temp_user['email'], temp_user)
|
195 |
+
|
196 |
+
message = Mail(
|
197 |
+
from_email=FROM_EMAIL,
|
198 |
+
to_emails=temp_user['email'],
|
199 |
+
subject='Your new verification code',
|
200 |
+
html_content=f'''
|
201 |
+
<p>Hi {temp_user['name']},</p>
|
202 |
+
<p>You requested a new verification code. Please use the following code to verify your email address:</p>
|
203 |
+
<h2>{verification_code}</h2>
|
204 |
+
<p>This code will expire in 10 minutes.</p>
|
205 |
+
'''
|
206 |
+
)
|
207 |
+
|
208 |
+
try:
|
209 |
+
sg = SendGridAPIClient(SENDGRID_API_KEY)
|
210 |
+
sg.send(message)
|
211 |
+
except Exception as e:
|
212 |
+
raise HTTPException(status_code=500, detail="Failed to send verification email")
|
213 |
+
|
214 |
+
return {"message": "A new verification code has been sent to your email."}
|
215 |
+
except Exception as e:
|
216 |
+
if isinstance(e, HTTPException):
|
217 |
+
raise e
|
218 |
+
raise HTTPException(status_code=500, detail=str(e))
|
219 |
+
|
220 |
+
@router.post('/checkChatsession')
|
221 |
+
async def check_chatsession(data: ChatSessionCheck, username: str = Depends(get_current_user)):
|
222 |
+
session_id = data.session_id
|
223 |
+
is_chat_exit = query.check_chat_session(session_id)
|
224 |
+
return {"ischatexit": is_chat_exit}
|
225 |
+
|
226 |
+
@router.get('/check-token')
|
227 |
+
async def check_token(username: str = Depends(get_current_user)):
|
228 |
+
try:
|
229 |
+
return {'valid': True, 'user': username}
|
230 |
+
except Exception as e:
|
231 |
+
raise HTTPException(status_code=401, detail=str(e))
|
232 |
+
|
233 |
+
@router.post('/forgot-password')
|
234 |
+
async def forgot_password(data: ForgotPasswordRequest):
|
235 |
+
try:
|
236 |
+
email = data.email
|
237 |
+
|
238 |
+
user = query.get_user_by_identifier(email)
|
239 |
+
if not user:
|
240 |
+
raise HTTPException(status_code=404, detail="Email not found")
|
241 |
+
|
242 |
+
reset_token = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
243 |
+
expiration = datetime.utcnow() + timedelta(hours=1)
|
244 |
+
|
245 |
+
query.store_reset_token(email, reset_token, expiration)
|
246 |
+
|
247 |
+
reset_link = f"http://localhost:3000/reset-password?token={reset_token}"
|
248 |
+
|
249 |
+
message = Mail(
|
250 |
+
from_email=FROM_EMAIL,
|
251 |
+
to_emails=email,
|
252 |
+
subject='Reset Your Password',
|
253 |
+
html_content=f'''
|
254 |
+
<p>Hi,</p>
|
255 |
+
<p>You requested to reset your password. Click the link below to reset it:</p>
|
256 |
+
<p><a href="{reset_link}">Reset Password</a></p>
|
257 |
+
<p>This link will expire in 1 hour.</p>
|
258 |
+
<p>If you didn't request this, please ignore this email.</p>
|
259 |
+
'''
|
260 |
+
)
|
261 |
+
|
262 |
+
sg = SendGridAPIClient(SENDGRID_API_KEY)
|
263 |
+
sg.send(message)
|
264 |
+
|
265 |
+
return {"message": "Password reset instructions sent to email"}
|
266 |
+
except Exception as e:
|
267 |
+
if isinstance(e, HTTPException):
|
268 |
+
raise e
|
269 |
+
raise HTTPException(status_code=500, detail=str(e))
|
270 |
+
|
271 |
+
@router.post('/reset-password')
|
272 |
+
async def reset_password(data: ResetPasswordRequest):
|
273 |
+
try:
|
274 |
+
token = data.token
|
275 |
+
new_password = data.password
|
276 |
+
|
277 |
+
if not token or not new_password:
|
278 |
+
raise HTTPException(status_code=400, detail="Token and new password are required")
|
279 |
+
|
280 |
+
reset_info = query.verify_reset_token(token)
|
281 |
+
if not reset_info:
|
282 |
+
raise HTTPException(status_code=400, detail="Invalid or expired reset token")
|
283 |
+
|
284 |
+
hashed_password = generate_password_hash(new_password)
|
285 |
+
query.update_password(reset_info['email'], hashed_password)
|
286 |
+
|
287 |
+
return {"message": "Password successfully reset"}
|
288 |
+
except Exception as e:
|
289 |
+
if isinstance(e, HTTPException):
|
290 |
+
raise e
|
291 |
+
raise HTTPException(status_code=500, detail=str(e))
|
app/routers/chat.py
ADDED
@@ -0,0 +1,385 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# app/routers/chat.py
|
2 |
+
import logging
|
3 |
+
import os
|
4 |
+
import json
|
5 |
+
import tempfile
|
6 |
+
from datetime import datetime
|
7 |
+
|
8 |
+
from bson import ObjectId
|
9 |
+
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Header
|
10 |
+
from fastapi.responses import JSONResponse, FileResponse
|
11 |
+
from pydantic import BaseModel
|
12 |
+
|
13 |
+
from app.database.database_query import DatabaseQuery
|
14 |
+
from app.middleware.auth import get_current_user, get_optional_user
|
15 |
+
from app.services import ChatProcessor
|
16 |
+
from app.services.image_processor import ImageProcessor
|
17 |
+
from app.services.report_process import Report
|
18 |
+
from app.services.skincare_scheduler import SkinCareScheduler
|
19 |
+
from app.services.wheel import EnvironmentalConditions
|
20 |
+
from app.services.RAG_evaluation import RAGEvaluation
|
21 |
+
|
22 |
+
router = APIRouter()
|
23 |
+
query = DatabaseQuery()
|
24 |
+
|
25 |
+
class ChatSessionTitleUpdate(BaseModel):
|
26 |
+
title: str
|
27 |
+
|
28 |
+
@router.get('/image/{filename}')
|
29 |
+
async def serve_image(filename: str):
|
30 |
+
try:
|
31 |
+
# Use an absolute path or environment variable to ensure consistency
|
32 |
+
upload_dir = os.path.abspath('uploads')
|
33 |
+
file_path = os.path.join(upload_dir, filename)
|
34 |
+
|
35 |
+
# Add logging to debug
|
36 |
+
print(f"Attempting to serve file from: {file_path}")
|
37 |
+
if not os.path.exists(file_path):
|
38 |
+
print(f"File not found: {file_path}")
|
39 |
+
raise FileNotFoundError()
|
40 |
+
|
41 |
+
return FileResponse(file_path)
|
42 |
+
except FileNotFoundError:
|
43 |
+
raise HTTPException(status_code=404, detail="Image not found")
|
44 |
+
|
45 |
+
@router.post('/chat-sessions', status_code=201)
|
46 |
+
async def create_chat_session(username: str = Depends(get_current_user)):
|
47 |
+
try:
|
48 |
+
session_id = str(ObjectId())
|
49 |
+
|
50 |
+
chat_session = {
|
51 |
+
"user_id": username,
|
52 |
+
"session_id": session_id,
|
53 |
+
"created_at": datetime.utcnow(),
|
54 |
+
"last_accessed": datetime.utcnow(),
|
55 |
+
"title": "New Chat"
|
56 |
+
}
|
57 |
+
query.create_chat_session(chat_session)
|
58 |
+
return {"message": "Chat session created", "session_id": session_id}
|
59 |
+
except Exception as e:
|
60 |
+
raise HTTPException(status_code=500, detail=str(e))
|
61 |
+
|
62 |
+
@router.get('/chat-sessions')
|
63 |
+
async def get_user_chat_sessions(username: str = Depends(get_current_user)):
|
64 |
+
try:
|
65 |
+
sessions = query.get_user_chat_sessions(username)
|
66 |
+
return sessions
|
67 |
+
except Exception as e:
|
68 |
+
raise HTTPException(status_code=500, detail=str(e))
|
69 |
+
|
70 |
+
@router.delete('/chat-sessions/{session_id}')
|
71 |
+
async def delete_chat_session(session_id: str, username: str = Depends(get_current_user)):
|
72 |
+
try:
|
73 |
+
result = query.delete_chat_session(session_id, username)
|
74 |
+
|
75 |
+
if result["session_deleted"]:
|
76 |
+
return {
|
77 |
+
"message": "Chat session and associated chats deleted successfully",
|
78 |
+
"chats_deleted": result["chats_deleted"]
|
79 |
+
}
|
80 |
+
raise HTTPException(status_code=404, detail="Chat session not found or unauthorized")
|
81 |
+
except Exception as e:
|
82 |
+
raise HTTPException(status_code=500, detail=str(e))
|
83 |
+
|
84 |
+
@router.put('/chat-sessions/{session_id}/title')
|
85 |
+
async def update_chat_title(
|
86 |
+
session_id: str,
|
87 |
+
title_data: ChatSessionTitleUpdate,
|
88 |
+
username: str = Depends(get_current_user)
|
89 |
+
):
|
90 |
+
try:
|
91 |
+
new_title = title_data.title
|
92 |
+
|
93 |
+
if not query.verify_session(session_id, username):
|
94 |
+
raise HTTPException(status_code=404, detail="Chat session not found or unauthorized")
|
95 |
+
|
96 |
+
if query.update_chat_session_title(session_id, new_title):
|
97 |
+
return {
|
98 |
+
'message': 'Chat session title updated successfully',
|
99 |
+
'session_id': session_id,
|
100 |
+
'new_title': new_title
|
101 |
+
}
|
102 |
+
|
103 |
+
raise HTTPException(status_code=500, detail="Failed to update chat session title")
|
104 |
+
except Exception as e:
|
105 |
+
raise HTTPException(status_code=500, detail=str(e))
|
106 |
+
|
107 |
+
@router.delete('/chat-sessions/all')
|
108 |
+
async def delete_all_sessions_and_chats(username: str = Depends(get_current_user)):
|
109 |
+
try:
|
110 |
+
result = query.delete_all_user_sessions_and_chats(username)
|
111 |
+
|
112 |
+
return {
|
113 |
+
"message": "Successfully deleted all chat sessions and chats",
|
114 |
+
"deleted_chats": result["deleted_chats"],
|
115 |
+
"deleted_sessions": result["deleted_sessions"]
|
116 |
+
}
|
117 |
+
except Exception as e:
|
118 |
+
raise HTTPException(status_code=500, detail=str(e))
|
119 |
+
|
120 |
+
@router.get('/chats/session/{session_id}')
|
121 |
+
async def get_session_chats(session_id: str, username: str = Depends(get_current_user)):
|
122 |
+
try:
|
123 |
+
chats = query.get_session_chats(session_id, username)
|
124 |
+
return chats
|
125 |
+
except Exception as e:
|
126 |
+
raise HTTPException(status_code=500, detail=str(e))
|
127 |
+
|
128 |
+
@router.get('/export-chat/{session_id}')
|
129 |
+
async def export_chat(session_id: str, username: str = Depends(get_current_user)):
|
130 |
+
try:
|
131 |
+
if not query.verify_session(session_id, username):
|
132 |
+
raise HTTPException(status_code=404, detail="Chat session not found or unauthorized")
|
133 |
+
|
134 |
+
chats = query.get_session_chats(session_id, username)
|
135 |
+
|
136 |
+
formatted_chats = []
|
137 |
+
for chat in chats:
|
138 |
+
formatted_chat = {
|
139 |
+
'query': chat.get('query', ''),
|
140 |
+
'response': chat.get('response', ''),
|
141 |
+
'references': chat.get('references', []),
|
142 |
+
'page_no': chat.get('page_no', ''),
|
143 |
+
'date': chat.get('timestamp', ''),
|
144 |
+
'chat_id': chat.get('chat_id', '')
|
145 |
+
}
|
146 |
+
formatted_chats.append(formatted_chat)
|
147 |
+
|
148 |
+
export_data = {
|
149 |
+
'session_id': session_id,
|
150 |
+
'export_date': datetime.utcnow().isoformat(),
|
151 |
+
'chats': formatted_chats
|
152 |
+
}
|
153 |
+
|
154 |
+
return export_data
|
155 |
+
except Exception as e:
|
156 |
+
raise HTTPException(status_code=500, detail=str(e))
|
157 |
+
|
158 |
+
@router.get('/export-all-chats')
|
159 |
+
async def export_all_chats(username: str = Depends(get_current_user)):
|
160 |
+
try:
|
161 |
+
all_chats = query.get_all_user_chats(username)
|
162 |
+
formatted_sessions = []
|
163 |
+
for session in all_chats:
|
164 |
+
formatted_chats = []
|
165 |
+
for chat in session['chats']:
|
166 |
+
formatted_chat = {
|
167 |
+
'query': chat.get('query', ''),
|
168 |
+
'response': chat.get('response', ''),
|
169 |
+
'references': chat.get('references', []),
|
170 |
+
'page_no': chat.get('page_no', ''),
|
171 |
+
'timestamp': chat.get('timestamp', ''),
|
172 |
+
'chat_id': chat.get('chat_id', '')
|
173 |
+
}
|
174 |
+
formatted_chats.append(formatted_chat)
|
175 |
+
|
176 |
+
formatted_session = {
|
177 |
+
'session_id': session['session_id'],
|
178 |
+
'title': session['title'],
|
179 |
+
'created_at': session['created_at'],
|
180 |
+
'last_accessed': session['last_accessed'],
|
181 |
+
'chats': formatted_chats
|
182 |
+
}
|
183 |
+
formatted_sessions.append(formatted_session)
|
184 |
+
|
185 |
+
export_data = {
|
186 |
+
'user': username,
|
187 |
+
'export_date': datetime.utcnow().isoformat(),
|
188 |
+
'sessions': formatted_sessions
|
189 |
+
}
|
190 |
+
|
191 |
+
return export_data
|
192 |
+
except Exception as e:
|
193 |
+
raise HTTPException(status_code=500, detail=str(e))
|
194 |
+
|
195 |
+
@router.post('/web-search')
|
196 |
+
async def web_search(
|
197 |
+
data: dict,
|
198 |
+
authorization: str = Header(None),
|
199 |
+
username: str = Depends(get_current_user)
|
200 |
+
):
|
201 |
+
try:
|
202 |
+
token = authorization.split(" ")[1]
|
203 |
+
session_id = data.get("session_id")
|
204 |
+
query = data.get("query")
|
205 |
+
num_results = data.get("num_results", 3)
|
206 |
+
num_images = data.get("num_images", 3)
|
207 |
+
|
208 |
+
if not session_id or not query:
|
209 |
+
return JSONResponse(
|
210 |
+
status_code=400,
|
211 |
+
content={"error": "session_id and query are required"}
|
212 |
+
)
|
213 |
+
|
214 |
+
chat_processor = ChatProcessor(token=token, session_id=session_id, num_results=num_results, num_images=num_images)
|
215 |
+
response = chat_processor.web_search(query=query)
|
216 |
+
|
217 |
+
return {"response": response}
|
218 |
+
except Exception as e:
|
219 |
+
raise HTTPException(status_code=500, detail=str(e))
|
220 |
+
|
221 |
+
@router.post('/report-analysis')
|
222 |
+
async def analyze_report(
|
223 |
+
file: UploadFile = File(...),
|
224 |
+
query: str = Form(...),
|
225 |
+
session_id: str = Form(...),
|
226 |
+
authorization: str = Header(None),
|
227 |
+
username: str = Depends(get_current_user)
|
228 |
+
):
|
229 |
+
try:
|
230 |
+
token = authorization.split(" ")[1]
|
231 |
+
|
232 |
+
if not file.filename:
|
233 |
+
return JSONResponse(
|
234 |
+
status_code=400,
|
235 |
+
content={"status": "error", "error": "Empty file provided"}
|
236 |
+
)
|
237 |
+
|
238 |
+
if not query.strip():
|
239 |
+
return JSONResponse(
|
240 |
+
status_code=400,
|
241 |
+
content={"status": "error", "error": "Query is required"}
|
242 |
+
)
|
243 |
+
|
244 |
+
file_extension = file.filename.rsplit('.', 1)[1].lower() if '.' in file.filename else ''
|
245 |
+
allowed_extensions = {
|
246 |
+
'pdf': 'pdf',
|
247 |
+
'xlsx': 'excel',
|
248 |
+
'xls': 'excel',
|
249 |
+
'csv': 'csv',
|
250 |
+
'jpg': 'image',
|
251 |
+
'jpeg': 'image',
|
252 |
+
'png': 'image',
|
253 |
+
'doc': 'word',
|
254 |
+
'docx': 'word',
|
255 |
+
'ppt': 'ppt',
|
256 |
+
'txt': 'text',
|
257 |
+
'html': 'html'
|
258 |
+
}
|
259 |
+
|
260 |
+
if file_extension not in allowed_extensions:
|
261 |
+
return JSONResponse(
|
262 |
+
status_code=200,
|
263 |
+
content={
|
264 |
+
"status": "success",
|
265 |
+
"message": f"Unsupported file type. Allowed types: {', '.join(allowed_extensions.keys())}",
|
266 |
+
"analysis": result
|
267 |
+
}
|
268 |
+
)
|
269 |
+
|
270 |
+
temp_dir = tempfile.mkdtemp()
|
271 |
+
temp_file_path = os.path.join(temp_dir, file.filename)
|
272 |
+
|
273 |
+
try:
|
274 |
+
content = await file.read()
|
275 |
+
with open(temp_file_path, "wb") as f:
|
276 |
+
f.write(content)
|
277 |
+
|
278 |
+
processor = Report(token=token, session_id=session_id)
|
279 |
+
result = processor.process_chat(
|
280 |
+
query=query,
|
281 |
+
report_file=temp_file_path,
|
282 |
+
file_type=allowed_extensions[file_extension]
|
283 |
+
)
|
284 |
+
|
285 |
+
return {
|
286 |
+
"status": "success",
|
287 |
+
"message": "Report analyzed successfully",
|
288 |
+
"analysis": result
|
289 |
+
}
|
290 |
+
finally:
|
291 |
+
# Clean up temporary files
|
292 |
+
if os.path.exists(temp_file_path):
|
293 |
+
os.remove(temp_file_path)
|
294 |
+
os.rmdir(temp_dir)
|
295 |
+
|
296 |
+
except Exception as e:
|
297 |
+
logging.error(f"Error in analyze_report: {str(e)}")
|
298 |
+
raise HTTPException(
|
299 |
+
status_code=500,
|
300 |
+
detail={
|
301 |
+
"status": "error",
|
302 |
+
"error": "Internal server error",
|
303 |
+
"details": str(e)
|
304 |
+
}
|
305 |
+
)
|
306 |
+
|
307 |
+
@router.get('/skin-care-schedule')
|
308 |
+
async def get_skin_care_schedule(
|
309 |
+
authorization: str = Header(None),
|
310 |
+
username: str = Depends(get_current_user)
|
311 |
+
):
|
312 |
+
try:
|
313 |
+
token = authorization.split(" ")[1]
|
314 |
+
scheduler = SkinCareScheduler(token, "session_id")
|
315 |
+
schedule = scheduler.createTable()
|
316 |
+
return json.loads(schedule)
|
317 |
+
except Exception as e:
|
318 |
+
logging.error(f"Error generating skin care schedule: {str(e)}")
|
319 |
+
raise HTTPException(
|
320 |
+
status_code=500,
|
321 |
+
detail={"error": "Failed to generate skin care schedule"}
|
322 |
+
)
|
323 |
+
|
324 |
+
@router.get('/skin-care-wheel')
|
325 |
+
async def get_skin_care_wheel(
|
326 |
+
authorization: str = Header(...),
|
327 |
+
username: str = Depends(get_current_user)
|
328 |
+
):
|
329 |
+
try:
|
330 |
+
token = authorization.split(" ")[1]
|
331 |
+
condition = EnvironmentalConditions(session_id=token)
|
332 |
+
condition_data = condition.get_conditon()
|
333 |
+
return condition_data
|
334 |
+
except Exception as e:
|
335 |
+
logging.error(f"Error generating skin care wheel: {str(e)}")
|
336 |
+
raise HTTPException(
|
337 |
+
status_code=500,
|
338 |
+
detail={
|
339 |
+
"error": "Failed to generate skin care wheel",
|
340 |
+
"message": "An unexpected error occurred"
|
341 |
+
}
|
342 |
+
)
|
343 |
+
|
344 |
+
@router.post('/image_disease_search')
|
345 |
+
async def disease_search(
|
346 |
+
session_id: str = Form(...),
|
347 |
+
query: str = Form(...),
|
348 |
+
num_results: int = Form(3),
|
349 |
+
num_images: int = Form(3),
|
350 |
+
image: UploadFile = File(...),
|
351 |
+
authorization: str = Header(...),
|
352 |
+
username: str = Depends(get_current_user)
|
353 |
+
):
|
354 |
+
try:
|
355 |
+
token = authorization.split(" ")[1]
|
356 |
+
image_processor = ImageProcessor(
|
357 |
+
token=token,
|
358 |
+
session_id=session_id,
|
359 |
+
num_results=num_results,
|
360 |
+
num_images=num_images,
|
361 |
+
image=image
|
362 |
+
)
|
363 |
+
response = image_processor.web_search(query=query)
|
364 |
+
return {"response": response}
|
365 |
+
except Exception as e:
|
366 |
+
raise HTTPException(status_code=500, detail=str(e))
|
367 |
+
|
368 |
+
@router.post('/get_rag_evaluation')
|
369 |
+
async def rag_evaluation(
|
370 |
+
page: int = Form(3),
|
371 |
+
page_size: int = Form(3),
|
372 |
+
authorization: str = Header(...),
|
373 |
+
username: str = Depends(get_current_user)
|
374 |
+
):
|
375 |
+
try:
|
376 |
+
token = authorization.split(" ")[1]
|
377 |
+
evaluator = RAGEvaluation(
|
378 |
+
token=token,
|
379 |
+
page=page,
|
380 |
+
page_size=page_size
|
381 |
+
)
|
382 |
+
report = evaluator.generate_evaluation_report()
|
383 |
+
return {"response": report}
|
384 |
+
except Exception as e:
|
385 |
+
raise HTTPException(status_code=500, detail=str(e))
|
app/routers/chat_session.py
ADDED
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# app/routers/chat_session.py
|
2 |
+
from datetime import datetime
|
3 |
+
from bson import ObjectId
|
4 |
+
from fastapi import APIRouter, Depends, HTTPException
|
5 |
+
from pydantic import BaseModel
|
6 |
+
|
7 |
+
from app.database.database_query import DatabaseQuery
|
8 |
+
from app.middleware.auth import get_current_user
|
9 |
+
|
10 |
+
router = APIRouter()
|
11 |
+
query = DatabaseQuery()
|
12 |
+
|
13 |
+
class ChatSessionTitleUpdate(BaseModel):
|
14 |
+
title: str
|
15 |
+
|
16 |
+
@router.post('/chat-sessions', status_code=201)
|
17 |
+
async def create_chat_session(username: str = Depends(get_current_user)):
|
18 |
+
try:
|
19 |
+
session_id = str(ObjectId())
|
20 |
+
|
21 |
+
chat_session = {
|
22 |
+
"user_id": username,
|
23 |
+
"session_id": session_id,
|
24 |
+
"created_at": datetime.utcnow(),
|
25 |
+
"last_accessed": datetime.utcnow(),
|
26 |
+
"title": "New Chat"
|
27 |
+
}
|
28 |
+
query.create_chat_session(chat_session)
|
29 |
+
return {"message": "Chat session created", "session_id": session_id}
|
30 |
+
except Exception as e:
|
31 |
+
raise HTTPException(status_code=500, detail=str(e))
|
32 |
+
|
33 |
+
@router.get('/chat-sessions')
|
34 |
+
async def get_user_chat_sessions(username: str = Depends(get_current_user)):
|
35 |
+
try:
|
36 |
+
sessions = query.get_user_chat_sessions(username)
|
37 |
+
return sessions
|
38 |
+
except Exception as e:
|
39 |
+
raise HTTPException(status_code=500, detail=str(e))
|
40 |
+
|
41 |
+
@router.delete('/chat-sessions/{session_id}')
|
42 |
+
async def delete_chat_session(session_id: str, username: str = Depends(get_current_user)):
|
43 |
+
try:
|
44 |
+
result = query.delete_chat_session(session_id, username)
|
45 |
+
|
46 |
+
if result["session_deleted"]:
|
47 |
+
return {
|
48 |
+
"message": "Chat session and associated chats deleted successfully",
|
49 |
+
"chats_deleted": result["chats_deleted"]
|
50 |
+
}
|
51 |
+
raise HTTPException(status_code=404, detail="Chat session not found or unauthorized")
|
52 |
+
except Exception as e:
|
53 |
+
raise HTTPException(status_code=500, detail=str(e))
|
54 |
+
|
55 |
+
@router.put('/chat-sessions/{session_id}/title')
|
56 |
+
async def update_chat_title(
|
57 |
+
session_id: str,
|
58 |
+
title_data: ChatSessionTitleUpdate,
|
59 |
+
username: str = Depends(get_current_user)
|
60 |
+
):
|
61 |
+
try:
|
62 |
+
new_title = title_data.title
|
63 |
+
|
64 |
+
if not query.verify_session(session_id, username):
|
65 |
+
raise HTTPException(status_code=404, detail="Chat session not found or unauthorized")
|
66 |
+
|
67 |
+
if query.update_chat_session_title(session_id, new_title):
|
68 |
+
return {
|
69 |
+
'message': 'Chat session title updated successfully',
|
70 |
+
'session_id': session_id,
|
71 |
+
'new_title': new_title
|
72 |
+
}
|
73 |
+
|
74 |
+
raise HTTPException(status_code=500, detail="Failed to update chat session title")
|
75 |
+
except Exception as e:
|
76 |
+
raise HTTPException(status_code=500, detail=str(e))
|
77 |
+
|
78 |
+
@router.delete('/chat-sessions/all')
|
79 |
+
async def delete_all_sessions_and_chats(username: str = Depends(get_current_user)):
|
80 |
+
try:
|
81 |
+
result = query.delete_all_user_sessions_and_chats(username)
|
82 |
+
|
83 |
+
return {
|
84 |
+
"message": "Successfully deleted all chat sessions and chats",
|
85 |
+
"deleted_chats": result["deleted_chats"],
|
86 |
+
"deleted_sessions": result["deleted_sessions"]
|
87 |
+
}
|
88 |
+
except Exception as e:
|
89 |
+
raise HTTPException(status_code=500, detail=str(e))
|
90 |
+
|
91 |
+
@router.get('/chats/session/{session_id}')
|
92 |
+
async def get_session_chats(session_id: str, username: str = Depends(get_current_user)):
|
93 |
+
try:
|
94 |
+
chats = query.get_session_chats(session_id, username)
|
95 |
+
return chats
|
96 |
+
except Exception as e:
|
97 |
+
raise HTTPException(status_code=500, detail=str(e))
|
app/routers/language.py
ADDED
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, Depends, HTTPException
|
2 |
+
from pydantic import BaseModel
|
3 |
+
from app.middleware.auth import get_current_user
|
4 |
+
|
5 |
+
from app.database.database_query import DatabaseQuery
|
6 |
+
|
7 |
+
router = APIRouter()
|
8 |
+
query = DatabaseQuery()
|
9 |
+
|
10 |
+
class LanguageSettings(BaseModel):
|
11 |
+
language: str
|
12 |
+
|
13 |
+
@router.post('/language', status_code=201)
|
14 |
+
async def set_language(
|
15 |
+
language_data: LanguageSettings,
|
16 |
+
username: str = Depends(get_current_user)
|
17 |
+
):
|
18 |
+
try:
|
19 |
+
language = language_data.language
|
20 |
+
|
21 |
+
if not language:
|
22 |
+
raise HTTPException(status_code=400, detail="Language is required")
|
23 |
+
|
24 |
+
result = query.set_user_language(username, language)
|
25 |
+
|
26 |
+
return {
|
27 |
+
"message": "Language set successfully",
|
28 |
+
"language": result["language"]
|
29 |
+
}
|
30 |
+
except Exception as e:
|
31 |
+
raise HTTPException(status_code=500, detail=str(e))
|
32 |
+
|
33 |
+
@router.get('/language')
|
34 |
+
async def get_language(username: str = Depends(get_current_user)):
|
35 |
+
try:
|
36 |
+
language = query.get_user_language(username)
|
37 |
+
|
38 |
+
if language is None:
|
39 |
+
raise HTTPException(status_code=404, detail="Language not set")
|
40 |
+
|
41 |
+
return {
|
42 |
+
"message": "Language retrieved successfully",
|
43 |
+
"language": language
|
44 |
+
}
|
45 |
+
except Exception as e:
|
46 |
+
raise HTTPException(status_code=500, detail=str(e))
|
47 |
+
|
48 |
+
@router.delete('/language')
|
49 |
+
async def delete_language(username: str = Depends(get_current_user)):
|
50 |
+
try:
|
51 |
+
result = query.delete_user_language(username)
|
52 |
+
|
53 |
+
if not result:
|
54 |
+
raise HTTPException(status_code=404, detail="Language not found or already deleted")
|
55 |
+
|
56 |
+
return {
|
57 |
+
"message": "Language deleted successfully"
|
58 |
+
}
|
59 |
+
except Exception as e:
|
60 |
+
raise HTTPException(status_code=500, detail=str(e))
|
app/routers/location.py
ADDED
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# app/routers/location.py
|
2 |
+
from fastapi import APIRouter, Depends, HTTPException
|
3 |
+
from pydantic import BaseModel
|
4 |
+
from app.database.database_query import DatabaseQuery
|
5 |
+
from app.middleware.auth import get_current_user
|
6 |
+
|
7 |
+
router = APIRouter()
|
8 |
+
query = DatabaseQuery()
|
9 |
+
|
10 |
+
class LocationData(BaseModel):
|
11 |
+
location: str
|
12 |
+
|
13 |
+
@router.post('/location', status_code=201)
|
14 |
+
async def add_location(location_data: LocationData, username: str = Depends(get_current_user)):
|
15 |
+
try:
|
16 |
+
location = location_data.location
|
17 |
+
|
18 |
+
if not location:
|
19 |
+
raise HTTPException(status_code=400, detail="Location is required")
|
20 |
+
|
21 |
+
query.add_or_update_location(username, location)
|
22 |
+
|
23 |
+
return {'message': 'Location added/updated successfully'}
|
24 |
+
except Exception as e:
|
25 |
+
raise HTTPException(status_code=500, detail=str(e))
|
26 |
+
|
27 |
+
@router.get('/location')
|
28 |
+
async def get_location(username: str = Depends(get_current_user)):
|
29 |
+
try:
|
30 |
+
location_data = query.get_location(username)
|
31 |
+
|
32 |
+
if not location_data:
|
33 |
+
raise HTTPException(status_code=404, detail="No location found for this user")
|
34 |
+
|
35 |
+
return {'location': location_data['location']}
|
36 |
+
except Exception as e:
|
37 |
+
raise HTTPException(status_code=500, detail=str(e))
|
app/routers/preferences.py
ADDED
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# app/routers/preferences.py
|
2 |
+
from fastapi import APIRouter, Depends, HTTPException
|
3 |
+
from pydantic import BaseModel
|
4 |
+
from typing import Dict, Any
|
5 |
+
from app.database.database_query import DatabaseQuery
|
6 |
+
from app.middleware.auth import get_current_user
|
7 |
+
|
8 |
+
router = APIRouter()
|
9 |
+
query = DatabaseQuery()
|
10 |
+
|
11 |
+
class ThemeSettings(BaseModel):
|
12 |
+
theme: bool
|
13 |
+
|
14 |
+
@router.get('/preferences')
|
15 |
+
async def get_preferences(username: str = Depends(get_current_user)):
|
16 |
+
try:
|
17 |
+
user_preferences = query.get_user_preferences(username)
|
18 |
+
return {'preferences': user_preferences}
|
19 |
+
except Exception as e:
|
20 |
+
raise HTTPException(status_code=500, detail=str(e))
|
21 |
+
|
22 |
+
@router.post('/preferences')
|
23 |
+
async def set_preferences(preferences: Dict[str, Any], username: str = Depends(get_current_user)):
|
24 |
+
try:
|
25 |
+
preferences_result = query.set_user_preferences(username, preferences)
|
26 |
+
return {
|
27 |
+
'message': 'Preferences updated successfully',
|
28 |
+
'preferences': preferences_result
|
29 |
+
}
|
30 |
+
except Exception as e:
|
31 |
+
raise HTTPException(status_code=500, detail=str(e))
|
32 |
+
|
33 |
+
@router.get('/theme')
|
34 |
+
async def get_theme(username: str = Depends(get_current_user)):
|
35 |
+
try:
|
36 |
+
user_theme = query.get_user_theme(username)
|
37 |
+
return {'theme': user_theme}
|
38 |
+
except Exception as e:
|
39 |
+
raise HTTPException(status_code=500, detail=str(e))
|
40 |
+
|
41 |
+
@router.post('/theme')
|
42 |
+
async def set_theme(theme_data: ThemeSettings, username: str = Depends(get_current_user)):
|
43 |
+
try:
|
44 |
+
theme = theme_data.theme
|
45 |
+
theme_data = query.set_user_theme(username, theme)
|
46 |
+
return {
|
47 |
+
'message': 'Theme updated successfully',
|
48 |
+
'theme': theme_data['theme']
|
49 |
+
}
|
50 |
+
except Exception as e:
|
51 |
+
raise HTTPException(status_code=500, detail=str(e))
|
app/routers/profile.py
ADDED
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, Depends, HTTPException, Body
|
2 |
+
from pydantic import BaseModel, EmailStr, validator
|
3 |
+
from typing import Optional
|
4 |
+
from werkzeug.security import generate_password_hash
|
5 |
+
|
6 |
+
from app.database.database_query import DatabaseQuery
|
7 |
+
from app.middleware.auth import get_current_user
|
8 |
+
|
9 |
+
router = APIRouter()
|
10 |
+
query = DatabaseQuery()
|
11 |
+
|
12 |
+
class ProfileUpdateRequest(BaseModel):
|
13 |
+
email: Optional[EmailStr] = None
|
14 |
+
password: Optional[str] = None
|
15 |
+
name: Optional[str] = None
|
16 |
+
age: Optional[int] = None
|
17 |
+
|
18 |
+
@validator('password')
|
19 |
+
def password_length(cls, v):
|
20 |
+
if v is not None and len(v) < 6:
|
21 |
+
raise ValueError('Password must be at least 6 characters')
|
22 |
+
return v
|
23 |
+
|
24 |
+
@validator('age')
|
25 |
+
def age_range(cls, v):
|
26 |
+
if v is not None and (v < 13 or v > 120):
|
27 |
+
raise ValueError('Age must be between 13 and 120')
|
28 |
+
return v
|
29 |
+
|
30 |
+
@router.get('/profile')
|
31 |
+
async def get_profile(username: str = Depends(get_current_user)):
|
32 |
+
try:
|
33 |
+
user = query.get_user_profile(username)
|
34 |
+
|
35 |
+
if not user:
|
36 |
+
raise HTTPException(status_code=404, detail="User not found")
|
37 |
+
|
38 |
+
return {
|
39 |
+
'username': user['username'],
|
40 |
+
'email': user['email'],
|
41 |
+
'name': user['name'],
|
42 |
+
'age': user['age'],
|
43 |
+
'created_at': user['created_at']
|
44 |
+
}
|
45 |
+
|
46 |
+
except Exception as e:
|
47 |
+
if isinstance(e, HTTPException):
|
48 |
+
raise e
|
49 |
+
raise HTTPException(status_code=500, detail=str(e))
|
50 |
+
|
51 |
+
@router.put('/profile')
|
52 |
+
async def update_profile(
|
53 |
+
update_data: ProfileUpdateRequest = Body(...),
|
54 |
+
username: str = Depends(get_current_user)
|
55 |
+
):
|
56 |
+
try:
|
57 |
+
update_fields = {}
|
58 |
+
|
59 |
+
if update_data.email:
|
60 |
+
if not query.is_valid_email(update_data.email):
|
61 |
+
raise HTTPException(status_code=400, detail="Invalid email format")
|
62 |
+
update_fields['email'] = update_data.email
|
63 |
+
|
64 |
+
if update_data.password:
|
65 |
+
update_fields['password'] = generate_password_hash(update_data.password)
|
66 |
+
|
67 |
+
if update_data.name:
|
68 |
+
update_fields['name'] = update_data.name
|
69 |
+
|
70 |
+
if update_data.age is not None:
|
71 |
+
update_fields['age'] = update_data.age
|
72 |
+
|
73 |
+
if update_fields:
|
74 |
+
if query.update_user_profile(username, update_fields):
|
75 |
+
return {"message": "Profile updated successfully"}
|
76 |
+
|
77 |
+
return {"message": "No changes made"}
|
78 |
+
|
79 |
+
except Exception as e:
|
80 |
+
if isinstance(e, HTTPException):
|
81 |
+
raise e
|
82 |
+
raise HTTPException(status_code=500, detail=str(e))
|
83 |
+
|
84 |
+
@router.delete('/profile')
|
85 |
+
async def delete_account(username: str = Depends(get_current_user)):
|
86 |
+
try:
|
87 |
+
if query.delete_user_account(username):
|
88 |
+
return {"message": "Account deleted successfully"}
|
89 |
+
|
90 |
+
raise HTTPException(status_code=404, detail="User not found")
|
91 |
+
|
92 |
+
except Exception as e:
|
93 |
+
if isinstance(e, HTTPException):
|
94 |
+
raise e
|
95 |
+
raise HTTPException(status_code=500, detail=str(e))
|
96 |
+
|
97 |
+
@router.delete('/delete-account-permanently')
|
98 |
+
async def delete_account_permanently(username: str = Depends(get_current_user)):
|
99 |
+
try:
|
100 |
+
result = query.delete_account_permanently(username)
|
101 |
+
|
102 |
+
if result['success']:
|
103 |
+
return {
|
104 |
+
'message': 'Account and all associated data deleted successfully',
|
105 |
+
'details': result['deleted_data']
|
106 |
+
}
|
107 |
+
else:
|
108 |
+
raise HTTPException(status_code=500, detail="Failed to delete account")
|
109 |
+
|
110 |
+
except Exception as e:
|
111 |
+
if isinstance(e, HTTPException):
|
112 |
+
raise e
|
113 |
+
raise HTTPException(status_code=500, detail=str(e))
|
app/routers/questionnaire.py
ADDED
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, Depends, HTTPException
|
2 |
+
from pydantic import BaseModel
|
3 |
+
from typing import Dict, Any
|
4 |
+
|
5 |
+
from app.database.database_query import DatabaseQuery
|
6 |
+
from app.middleware.auth import get_current_user
|
7 |
+
|
8 |
+
router = APIRouter()
|
9 |
+
query = DatabaseQuery()
|
10 |
+
|
11 |
+
class QuestionnaireSubmission(BaseModel):
|
12 |
+
answers: Dict[str, Any]
|
13 |
+
|
14 |
+
@router.post('/questionnaires', status_code=201)
|
15 |
+
async def submit_questionnaire(
|
16 |
+
submission: QuestionnaireSubmission,
|
17 |
+
username: str = Depends(get_current_user)
|
18 |
+
):
|
19 |
+
try:
|
20 |
+
if not submission.answers:
|
21 |
+
raise HTTPException(status_code=400, detail="Answers are required")
|
22 |
+
|
23 |
+
questionnaire_id = query.submit_questionnaire(username, submission.answers)
|
24 |
+
return {
|
25 |
+
'message': 'Questionnaire submitted successfully',
|
26 |
+
'questionnaire_id': questionnaire_id
|
27 |
+
}
|
28 |
+
except Exception as e:
|
29 |
+
if isinstance(e, HTTPException):
|
30 |
+
raise e
|
31 |
+
raise HTTPException(status_code=500, detail=str(e))
|
32 |
+
|
33 |
+
@router.get('/questionnaires')
|
34 |
+
async def get_questionnaire(username: str = Depends(get_current_user)):
|
35 |
+
try:
|
36 |
+
questionnaire = query.get_latest_questionnaire(username)
|
37 |
+
|
38 |
+
if not questionnaire:
|
39 |
+
return {'message': 'No questionnaire found', 'data': None}
|
40 |
+
|
41 |
+
return {'message': 'Success', 'data': questionnaire}
|
42 |
+
except Exception as e:
|
43 |
+
raise HTTPException(status_code=500, detail=str(e))
|
44 |
+
|
45 |
+
@router.put('/questionnaires/{questionnaire_id}')
|
46 |
+
async def update_questionnaire(
|
47 |
+
questionnaire_id: str,
|
48 |
+
submission: QuestionnaireSubmission,
|
49 |
+
username: str = Depends(get_current_user)
|
50 |
+
):
|
51 |
+
try:
|
52 |
+
if not submission.answers:
|
53 |
+
raise HTTPException(status_code=400, detail="Answers are required")
|
54 |
+
|
55 |
+
if query.update_questionnaire(questionnaire_id, username, submission.answers):
|
56 |
+
return {'message': 'Questionnaire updated successfully'}
|
57 |
+
|
58 |
+
raise HTTPException(
|
59 |
+
status_code=404,
|
60 |
+
detail='Questionnaire not found or unauthorized'
|
61 |
+
)
|
62 |
+
except Exception as e:
|
63 |
+
if isinstance(e, HTTPException):
|
64 |
+
raise e
|
65 |
+
raise HTTPException(status_code=500, detail=str(e))
|
66 |
+
|
67 |
+
@router.delete('/questionnaires/{questionnaire_id}')
|
68 |
+
async def delete_questionnaire(
|
69 |
+
questionnaire_id: str,
|
70 |
+
username: str = Depends(get_current_user)
|
71 |
+
):
|
72 |
+
try:
|
73 |
+
if query.delete_questionnaire(questionnaire_id, username):
|
74 |
+
return {'message': 'Questionnaire deleted successfully'}
|
75 |
+
|
76 |
+
raise HTTPException(
|
77 |
+
status_code=404,
|
78 |
+
detail='Questionnaire not found or unauthorized'
|
79 |
+
)
|
80 |
+
except Exception as e:
|
81 |
+
if isinstance(e, HTTPException):
|
82 |
+
raise e
|
83 |
+
raise HTTPException(status_code=500, detail=str(e))
|
84 |
+
|
85 |
+
@router.get('/check-answers')
|
86 |
+
async def check_answers(username: str = Depends(get_current_user)):
|
87 |
+
try:
|
88 |
+
answered_count = query.count_answered_questions(username)
|
89 |
+
return {'has_at_least_two_answers': answered_count >= 2}
|
90 |
+
except Exception as e:
|
91 |
+
raise HTTPException(status_code=500, detail=str(e))
|
92 |
+
|
93 |
+
@router.get('/check-questionnaire')
|
94 |
+
async def check_questionnaire_submission(username: str = Depends(get_current_user)):
|
95 |
+
try:
|
96 |
+
questionnaire = query.get_latest_questionnaire(username)
|
97 |
+
has_questionnaire = questionnaire is not None
|
98 |
+
return {'has_questionnaire': has_questionnaire}
|
99 |
+
except Exception as e:
|
100 |
+
raise HTTPException(status_code=500, detail=str(e))
|
app/services/MagicConvert.py
ADDED
@@ -0,0 +1,685 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import copy
|
2 |
+
import html
|
3 |
+
import mimetypes
|
4 |
+
import os
|
5 |
+
import re
|
6 |
+
import tempfile
|
7 |
+
import traceback
|
8 |
+
from typing import Any, Dict, List, Optional, Union
|
9 |
+
from urllib.parse import quote, unquote, urlparse, urlunparse
|
10 |
+
from warnings import warn, resetwarnings, catch_warnings
|
11 |
+
import mammoth
|
12 |
+
import markdownify
|
13 |
+
import pandas as pd
|
14 |
+
import pdfminer
|
15 |
+
import pdfminer.high_level
|
16 |
+
import pptx
|
17 |
+
import puremagic
|
18 |
+
import requests
|
19 |
+
from bs4 import BeautifulSoup
|
20 |
+
from charset_normalizer import from_path
|
21 |
+
from PIL import Image
|
22 |
+
import pytesseract
|
23 |
+
import warnings
|
24 |
+
warnings.filterwarnings("ignore")
|
25 |
+
|
26 |
+
# Set Tesseract path for Linux (Hugging Face Spaces)
|
27 |
+
pytesseract.pytesseract.tesseract_cmd = "/usr/bin/tesseract"
|
28 |
+
|
29 |
+
class OCRReader:
|
30 |
+
def __init__(self, tesseract_cmd: Optional[str] = None, config: Optional[Dict] = None):
|
31 |
+
# Use provided tesseract_cmd or fallback to environment default
|
32 |
+
if tesseract_cmd:
|
33 |
+
pytesseract.pytesseract.tesseract_cmd = tesseract_cmd
|
34 |
+
self.config = config or {}
|
35 |
+
|
36 |
+
def read_text_from_image(self, image: Image.Image) -> str:
|
37 |
+
try:
|
38 |
+
text = pytesseract.image_to_string(image, **self.config)
|
39 |
+
return text.strip()
|
40 |
+
except Exception as e:
|
41 |
+
raise Exception(f"Error processing image: {str(e)}")
|
42 |
+
|
43 |
+
|
44 |
+
class _CustomMarkdownify(markdownify.MarkdownConverter):
|
45 |
+
def __init__(self, **options: Any):
|
46 |
+
options["heading_style"] = options.get("heading_style", markdownify.ATX)
|
47 |
+
super().__init__(**options)
|
48 |
+
|
49 |
+
def convert_a(self, el: Any, text: str, *args, **kwargs):
|
50 |
+
prefix, suffix, text = markdownify.chomp(text)
|
51 |
+
if not text:
|
52 |
+
return ""
|
53 |
+
href = el.get("href")
|
54 |
+
title = el.get("title")
|
55 |
+
if href:
|
56 |
+
try:
|
57 |
+
parsed_url = urlparse(href) # type: ignore
|
58 |
+
if parsed_url.scheme and parsed_url.scheme.lower() not in ["http", "https", "file"]: # type: ignore
|
59 |
+
return "%s%s%s" % (prefix, text, suffix)
|
60 |
+
href = urlunparse(parsed_url._replace(path=quote(unquote(parsed_url.path)))) # type: ignore
|
61 |
+
except ValueError:
|
62 |
+
return "%s%s%s" % (prefix, text, suffix)
|
63 |
+
if (
|
64 |
+
self.options["autolinks"]
|
65 |
+
and text.replace(r"\_", "_") == href
|
66 |
+
and not title
|
67 |
+
and not self.options["default_title"]
|
68 |
+
):
|
69 |
+
return "<%s>" % href
|
70 |
+
if self.options["default_title"] and not title:
|
71 |
+
title = href
|
72 |
+
title_part = ' "%s"' % title.replace('"', r"\"") if title else ""
|
73 |
+
return (
|
74 |
+
"%s[%s](%s%s)%s" % (prefix, text, href, title_part, suffix)
|
75 |
+
if href
|
76 |
+
else text
|
77 |
+
)
|
78 |
+
|
79 |
+
def convert_hn(self, n: int, el: Any, text: str, convert_as_inline: bool) -> str:
|
80 |
+
if not convert_as_inline:
|
81 |
+
if not re.search(r"^\n", text):
|
82 |
+
return "\n" + super().convert_hn(n, el, text, convert_as_inline) # type: ignore
|
83 |
+
|
84 |
+
return super().convert_hn(n, el, text, convert_as_inline) # type: ignore
|
85 |
+
|
86 |
+
def convert_img(self, el: Any, text: str, *args, **kwargs) -> str:
|
87 |
+
# Handle both old and new calling patterns
|
88 |
+
convert_as_inline = kwargs.get('convert_as_inline', False)
|
89 |
+
if len(args) > 0:
|
90 |
+
convert_as_inline = args[0]
|
91 |
+
|
92 |
+
alt = el.attrs.get("alt", None) or ""
|
93 |
+
src = el.attrs.get("src", None) or ""
|
94 |
+
title = el.attrs.get("title", None) or ""
|
95 |
+
title_part = ' "%s"' % title.replace('"', r"\"") if title else ""
|
96 |
+
if (
|
97 |
+
convert_as_inline
|
98 |
+
and el.parent.name not in self.options["keep_inline_images_in"]
|
99 |
+
):
|
100 |
+
return alt
|
101 |
+
if src.startswith("data:"):
|
102 |
+
src = src.split(",")[0] + "..."
|
103 |
+
|
104 |
+
return "" % (alt, src, title_part)
|
105 |
+
|
106 |
+
def convert_soup(self, soup: Any) -> str:
|
107 |
+
return super().convert_soup(soup)
|
108 |
+
|
109 |
+
|
110 |
+
class DocumentConverterResult:
|
111 |
+
def __init__(self, title: Union[str, None] = None, text_content: str = ""):
|
112 |
+
self.title: Union[str, None] = title
|
113 |
+
self.text_content: str = text_content
|
114 |
+
|
115 |
+
|
116 |
+
class DocumentConverter:
|
117 |
+
def convert(self, local_path: str, **kwargs: Any) -> Union[None, DocumentConverterResult]:
|
118 |
+
raise NotImplementedError()
|
119 |
+
|
120 |
+
def supports_extension(self, ext: str) -> bool:
|
121 |
+
"""Return True if this converter supports the given extension."""
|
122 |
+
raise NotImplementedError()
|
123 |
+
|
124 |
+
|
125 |
+
class PlainTextConverter(DocumentConverter):
|
126 |
+
def convert(
|
127 |
+
self, local_path: str, **kwargs: Any
|
128 |
+
) -> Union[None, DocumentConverterResult]:
|
129 |
+
content_type, _ = mimetypes.guess_type(
|
130 |
+
"__placeholder" + kwargs.get("file_extension", "")
|
131 |
+
)
|
132 |
+
if content_type is None:
|
133 |
+
return None
|
134 |
+
elif "text/" not in content_type.lower():
|
135 |
+
return None
|
136 |
+
|
137 |
+
text_content = str(from_path(local_path).best())
|
138 |
+
return DocumentConverterResult(
|
139 |
+
title=None,
|
140 |
+
text_content=text_content,
|
141 |
+
)
|
142 |
+
|
143 |
+
|
144 |
+
class HtmlConverter(DocumentConverter):
|
145 |
+
def convert(
|
146 |
+
self, local_path: str, **kwargs: Any
|
147 |
+
) -> Union[None, DocumentConverterResult]:
|
148 |
+
extension = kwargs.get("file_extension", "")
|
149 |
+
if extension.lower() not in [".html", ".htm"]:
|
150 |
+
return None
|
151 |
+
|
152 |
+
result = None
|
153 |
+
with open(local_path, "rt", encoding="utf-8") as fh:
|
154 |
+
result = self._convert(fh.read())
|
155 |
+
|
156 |
+
return result
|
157 |
+
|
158 |
+
def _convert(self, html_content: str) -> Union[None, DocumentConverterResult]:
|
159 |
+
soup = BeautifulSoup(html_content, "html.parser")
|
160 |
+
for script in soup(["script", "style"]):
|
161 |
+
script.extract()
|
162 |
+
body_elm = soup.find("body")
|
163 |
+
webpage_text = ""
|
164 |
+
if body_elm:
|
165 |
+
webpage_text = _CustomMarkdownify().convert_soup(body_elm)
|
166 |
+
else:
|
167 |
+
webpage_text = _CustomMarkdownify().convert_soup(soup)
|
168 |
+
|
169 |
+
assert isinstance(webpage_text, str)
|
170 |
+
|
171 |
+
return DocumentConverterResult(
|
172 |
+
title=None if soup.title is None else soup.title.string,
|
173 |
+
text_content=webpage_text,
|
174 |
+
)
|
175 |
+
|
176 |
+
|
177 |
+
class PdfConverter(DocumentConverter):
|
178 |
+
def supports_extension(self, ext: str) -> bool:
|
179 |
+
return ext.lower() == '.pdf'
|
180 |
+
|
181 |
+
def convert(self, local_path, **kwargs) -> Union[None, DocumentConverterResult]:
|
182 |
+
extension = kwargs.get("file_extension", "")
|
183 |
+
if extension.lower() != ".pdf":
|
184 |
+
return None
|
185 |
+
return DocumentConverterResult(
|
186 |
+
title=None,
|
187 |
+
text_content=pdfminer.high_level.extract_text(local_path),
|
188 |
+
)
|
189 |
+
|
190 |
+
|
191 |
+
class DocxConverter(HtmlConverter):
|
192 |
+
def supports_extension(self, ext: str) -> bool:
|
193 |
+
return ext.lower() == '.docx'
|
194 |
+
|
195 |
+
def convert(self, local_path, **kwargs) -> Union[None, DocumentConverterResult]:
|
196 |
+
extension = kwargs.get("file_extension", "")
|
197 |
+
if extension.lower() != ".docx":
|
198 |
+
return None
|
199 |
+
result = None
|
200 |
+
with open(local_path, "rb") as docx_file:
|
201 |
+
style_map = kwargs.get("style_map", None)
|
202 |
+
result = mammoth.convert_to_html(docx_file, style_map=style_map)
|
203 |
+
html_content = result.value
|
204 |
+
result = self._convert(html_content)
|
205 |
+
return result
|
206 |
+
|
207 |
+
|
208 |
+
class XlsxConverter(HtmlConverter):
|
209 |
+
def convert(self, local_path, **kwargs) -> Union[None, DocumentConverterResult]:
|
210 |
+
extension = kwargs.get("file_extension", "")
|
211 |
+
if extension.lower() != ".xlsx":
|
212 |
+
return None
|
213 |
+
|
214 |
+
sheets = pd.read_excel(local_path, sheet_name=None)
|
215 |
+
md_content = ""
|
216 |
+
for s in sheets:
|
217 |
+
md_content += f"## {s}\n"
|
218 |
+
html_content = sheets[s].to_html(index=False)
|
219 |
+
md_content += self._convert(html_content).text_content.strip() + "\n\n"
|
220 |
+
|
221 |
+
return DocumentConverterResult(
|
222 |
+
title=None,
|
223 |
+
text_content=md_content.strip(),
|
224 |
+
)
|
225 |
+
|
226 |
+
|
227 |
+
class PptxConverter(HtmlConverter):
|
228 |
+
def supports_extension(self, ext: str) -> bool:
|
229 |
+
return ext.lower() == '.pptx'
|
230 |
+
|
231 |
+
def convert(self, local_path, **kwargs) -> Union[None, DocumentConverterResult]:
|
232 |
+
extension = kwargs.get("file_extension", "")
|
233 |
+
if extension.lower() != ".pptx":
|
234 |
+
return None
|
235 |
+
md_content = ""
|
236 |
+
presentation = pptx.Presentation(local_path)
|
237 |
+
slide_num = 0
|
238 |
+
for slide in presentation.slides:
|
239 |
+
slide_num += 1
|
240 |
+
|
241 |
+
md_content += f"\n\n<!-- Slide number: {slide_num} -->\n"
|
242 |
+
|
243 |
+
title = slide.shapes.title
|
244 |
+
for shape in slide.shapes:
|
245 |
+
if self._is_picture(shape):
|
246 |
+
alt_text = ""
|
247 |
+
try:
|
248 |
+
alt_text = shape._element._nvXxPr.cNvPr.attrib.get("descr", "")
|
249 |
+
except Exception:
|
250 |
+
pass
|
251 |
+
filename = re.sub(r"\W", "", shape.name) + ".jpg"
|
252 |
+
md_content += (
|
253 |
+
"\n\n"
|
258 |
+
)
|
259 |
+
|
260 |
+
# Tables
|
261 |
+
if self._is_table(shape):
|
262 |
+
html_table = "<html><body><table>"
|
263 |
+
first_row = True
|
264 |
+
for row in shape.table.rows:
|
265 |
+
html_table += "<tr>"
|
266 |
+
for cell in row.cells:
|
267 |
+
if first_row:
|
268 |
+
html_table += "<th>" + html.escape(cell.text) + "</th>"
|
269 |
+
else:
|
270 |
+
html_table += "<td>" + html.escape(cell.text) + "</td>"
|
271 |
+
html_table += "</tr>"
|
272 |
+
first_row = False
|
273 |
+
html_table += "</table></body></html>"
|
274 |
+
md_content += (
|
275 |
+
"\n" + self._convert(html_table).text_content.strip() + "\n"
|
276 |
+
)
|
277 |
+
if shape.has_chart:
|
278 |
+
md_content += self._convert_chart_to_markdown(shape.chart)
|
279 |
+
elif shape.has_text_frame:
|
280 |
+
if shape == title:
|
281 |
+
md_content += "# " + shape.text.lstrip() + "\n"
|
282 |
+
else:
|
283 |
+
md_content += shape.text + "\n"
|
284 |
+
|
285 |
+
md_content = md_content.strip()
|
286 |
+
|
287 |
+
if slide.has_notes_slide:
|
288 |
+
md_content += "\n\n### Notes:\n"
|
289 |
+
notes_frame = slide.notes_slide.notes_text_frame
|
290 |
+
if notes_frame is not None:
|
291 |
+
md_content += notes_frame.text
|
292 |
+
md_content = md_content.strip()
|
293 |
+
|
294 |
+
return DocumentConverterResult(
|
295 |
+
title=None,
|
296 |
+
text_content=md_content.strip(),
|
297 |
+
)
|
298 |
+
|
299 |
+
def _is_picture(self, shape):
|
300 |
+
if shape.shape_type == pptx.enum.shapes.MSO_SHAPE_TYPE.PICTURE:
|
301 |
+
return True
|
302 |
+
if shape.shape_type == pptx.enum.shapes.MSO_SHAPE_TYPE.PLACEHOLDER:
|
303 |
+
if hasattr(shape, "image"):
|
304 |
+
return True
|
305 |
+
return False
|
306 |
+
|
307 |
+
def _is_table(self, shape):
|
308 |
+
if shape.shape_type == pptx.enum.shapes.MSO_SHAPE_TYPE.TABLE:
|
309 |
+
return True
|
310 |
+
return False
|
311 |
+
|
312 |
+
def _convert_chart_to_markdown(self, chart):
|
313 |
+
md = "\n\n### Chart"
|
314 |
+
if chart.has_title:
|
315 |
+
md += f": {chart.chart_title.text_frame.text}"
|
316 |
+
md += "\n\n"
|
317 |
+
data = []
|
318 |
+
category_names = [c.label for c in chart.plots[0].categories]
|
319 |
+
series_names = [s.name for s in chart.series]
|
320 |
+
data.append(["Category"] + series_names)
|
321 |
+
|
322 |
+
for idx, category in enumerate(category_names):
|
323 |
+
row = [category]
|
324 |
+
for series in chart.series:
|
325 |
+
row.append(series.values[idx])
|
326 |
+
data.append(row)
|
327 |
+
|
328 |
+
markdown_table = []
|
329 |
+
for row in data:
|
330 |
+
markdown_table.append("| " + " | ".join(map(str, row)) + " |")
|
331 |
+
header = markdown_table[0]
|
332 |
+
separator = "|" + "|".join(["---"] * len(data[0])) + "|"
|
333 |
+
return md + "\n".join([header, separator] + markdown_table[1:])
|
334 |
+
|
335 |
+
|
336 |
+
class FileConversionException(BaseException):
|
337 |
+
pass
|
338 |
+
|
339 |
+
|
340 |
+
class UnsupportedFormatException(BaseException):
|
341 |
+
pass
|
342 |
+
|
343 |
+
|
344 |
+
class ImageConverter(DocumentConverter):
|
345 |
+
def __init__(self, ocr_reader: Optional[OCRReader] = None):
|
346 |
+
self.ocr_reader = ocr_reader or OCRReader()
|
347 |
+
|
348 |
+
def convert(self, local_path: str, **kwargs: Any) -> Union[None, DocumentConverterResult]:
|
349 |
+
extension = kwargs.get("file_extension", "").lower()
|
350 |
+
if extension not in ['.png', '.jpg', '.jpeg', '.tiff', '.bmp']:
|
351 |
+
return None
|
352 |
+
|
353 |
+
try:
|
354 |
+
image = Image.open(local_path)
|
355 |
+
text_content = self.ocr_reader.read_text_from_image(image)
|
356 |
+
markdown_content = self._convert_to_markdown_structure(text_content)
|
357 |
+
return DocumentConverterResult(
|
358 |
+
title=None,
|
359 |
+
text_content=markdown_content
|
360 |
+
)
|
361 |
+
except Exception as e:
|
362 |
+
raise FileConversionException(f"Failed to process image: {str(e)}")
|
363 |
+
|
364 |
+
def _convert_to_markdown_structure(self, text_content: str) -> str:
|
365 |
+
lines = text_content.split('\n')
|
366 |
+
markdown = []
|
367 |
+
current_table = []
|
368 |
+
in_table = False
|
369 |
+
|
370 |
+
i = 0
|
371 |
+
while i < len(lines):
|
372 |
+
line = lines[i].strip()
|
373 |
+
next_line = lines[i + 1].strip() if i + 1 < len(lines) else ""
|
374 |
+
|
375 |
+
if not line:
|
376 |
+
if in_table:
|
377 |
+
markdown.extend(self._format_table(current_table))
|
378 |
+
current_table = []
|
379 |
+
in_table = False
|
380 |
+
markdown.append("")
|
381 |
+
i += 1
|
382 |
+
continue
|
383 |
+
|
384 |
+
header_level = self._detect_header_level(line, next_line)
|
385 |
+
if header_level:
|
386 |
+
if in_table:
|
387 |
+
markdown.extend(self._format_table(current_table))
|
388 |
+
current_table = []
|
389 |
+
in_table = False
|
390 |
+
markdown.append(f"{'#' * header_level} {line}")
|
391 |
+
i += 2 if header_level > 0 and next_line and set(next_line) in [set('='), set('-')] else 1
|
392 |
+
continue
|
393 |
+
|
394 |
+
list_format = self._detect_list_format(line)
|
395 |
+
if list_format:
|
396 |
+
if in_table:
|
397 |
+
markdown.extend(self._format_table(current_table))
|
398 |
+
current_table = []
|
399 |
+
in_table = False
|
400 |
+
markdown.append(list_format)
|
401 |
+
i += 1
|
402 |
+
continue
|
403 |
+
if self._is_likely_table_row(line):
|
404 |
+
in_table = True
|
405 |
+
current_table.append(line)
|
406 |
+
i += 1
|
407 |
+
continue
|
408 |
+
if in_table:
|
409 |
+
markdown.extend(self._format_table(current_table))
|
410 |
+
current_table = []
|
411 |
+
in_table = False
|
412 |
+
line = self._format_emphasis(line)
|
413 |
+
|
414 |
+
markdown.append(line)
|
415 |
+
i += 1
|
416 |
+
if current_table:
|
417 |
+
markdown.extend(self._format_table(current_table))
|
418 |
+
|
419 |
+
return "\n\n".join([l for l in markdown if l])
|
420 |
+
|
421 |
+
def _detect_header_level(self, line: str, next_line: str) -> int:
|
422 |
+
if line.startswith('#'):
|
423 |
+
return len(re.match(r'^#+', line).group())
|
424 |
+
if next_line:
|
425 |
+
if set(next_line) == set('='):
|
426 |
+
return 1
|
427 |
+
if set(next_line) == set('-'):
|
428 |
+
return 2
|
429 |
+
if len(line) <= 100 and line.strip():
|
430 |
+
words = line.split()
|
431 |
+
if all(word[0].isupper() for word in words if word):
|
432 |
+
return 1
|
433 |
+
if line[0].isupper() and len(words) <= 10:
|
434 |
+
return 2
|
435 |
+
|
436 |
+
return 0
|
437 |
+
|
438 |
+
def _detect_list_format(self, line: str) -> Optional[str]:
|
439 |
+
bullet_points = ['-', '•', '*', '○', '►', '·']
|
440 |
+
for bullet in bullet_points:
|
441 |
+
if line.lstrip().startswith(bullet):
|
442 |
+
content = line.lstrip()[1:].strip()
|
443 |
+
return f"- {content}"
|
444 |
+
|
445 |
+
if re.match(r'^\d+[\.\)]', line):
|
446 |
+
content = re.sub(r'^\d+[\.\)]', '', line).strip()
|
447 |
+
return f"1. {content}"
|
448 |
+
|
449 |
+
return None
|
450 |
+
|
451 |
+
def _is_likely_table_row(self, line: str) -> bool:
|
452 |
+
parts = [p for p in re.split(r'\s{2,}', line) if p.strip()]
|
453 |
+
if len(parts) >= 2:
|
454 |
+
lengths = [len(p) for p in parts]
|
455 |
+
avg_length = sum(lengths) / len(lengths)
|
456 |
+
if all(abs(l - avg_length) <= 5 for l in lengths):
|
457 |
+
return True
|
458 |
+
return False
|
459 |
+
|
460 |
+
def _format_table(self, table_rows: List[str]) -> List[str]:
|
461 |
+
if not table_rows:
|
462 |
+
return []
|
463 |
+
split_rows = [re.split(r'\s{2,}', row.strip()) for row in table_rows]
|
464 |
+
max_cols = max(len(row) for row in split_rows)
|
465 |
+
normalized_rows = []
|
466 |
+
for row in split_rows:
|
467 |
+
while len(row) < max_cols:
|
468 |
+
row.append('')
|
469 |
+
normalized_rows.append(row)
|
470 |
+
col_widths = []
|
471 |
+
for col in range(max_cols):
|
472 |
+
width = max(len(row[col]) for row in normalized_rows)
|
473 |
+
col_widths.append(width)
|
474 |
+
markdown_table = []
|
475 |
+
|
476 |
+
header = "| " + " | ".join(cell.ljust(width) for cell, width in zip(normalized_rows[0], col_widths)) + " |"
|
477 |
+
markdown_table.append(header)
|
478 |
+
|
479 |
+
separator = "|" + "|".join("-" * (width + 2) for width in col_widths) + "|"
|
480 |
+
markdown_table.append(separator)
|
481 |
+
|
482 |
+
for row in normalized_rows[1:]:
|
483 |
+
formatted_row = "| " + " | ".join(cell.ljust(width) for cell, width in zip(row, col_widths)) + " |"
|
484 |
+
markdown_table.append(formatted_row)
|
485 |
+
|
486 |
+
return markdown_table
|
487 |
+
|
488 |
+
def _format_emphasis(self, text: str) -> str:
|
489 |
+
text = re.sub(r'\b([A-Z]{2,})\b', r'**\1**', text)
|
490 |
+
text = re.sub(r'[_/](.*?)[_/]', r'*\1*', text)
|
491 |
+
return text
|
492 |
+
|
493 |
+
class MagicConvert:
|
494 |
+
def __init__(
|
495 |
+
self,
|
496 |
+
requests_session: Optional[requests.Session] = None,
|
497 |
+
style_map: Optional[str] = None,
|
498 |
+
):
|
499 |
+
if requests_session is None:
|
500 |
+
self._requests_session = requests.Session()
|
501 |
+
else:
|
502 |
+
self._requests_session = requests_session
|
503 |
+
|
504 |
+
self._style_map = style_map
|
505 |
+
self._page_converters: List[DocumentConverter] = []
|
506 |
+
|
507 |
+
ocr_reader = OCRReader()
|
508 |
+
|
509 |
+
self.register_page_converter(ImageConverter(ocr_reader))
|
510 |
+
self.register_page_converter(PlainTextConverter())
|
511 |
+
self.register_page_converter(HtmlConverter())
|
512 |
+
self.register_page_converter(DocxConverter())
|
513 |
+
self.register_page_converter(XlsxConverter())
|
514 |
+
self.register_page_converter(PptxConverter())
|
515 |
+
self.register_page_converter(PdfConverter())
|
516 |
+
|
517 |
+
def magic(
|
518 |
+
self, source: Union[str, requests.Response], **kwargs: Any
|
519 |
+
) -> DocumentConverterResult:
|
520 |
+
if isinstance(source, str):
|
521 |
+
if (
|
522 |
+
source.startswith("http://")
|
523 |
+
or source.startswith("https://")
|
524 |
+
or source.startswith("file://")
|
525 |
+
):
|
526 |
+
return self.convert_url(source, **kwargs)
|
527 |
+
else:
|
528 |
+
return self.convert_local(source, **kwargs)
|
529 |
+
elif isinstance(source, requests.Response):
|
530 |
+
return self.convert_response(source, **kwargs)
|
531 |
+
|
532 |
+
def convert_local(
|
533 |
+
self, path: str, **kwargs: Any
|
534 |
+
) -> DocumentConverterResult:
|
535 |
+
ext = kwargs.get("file_extension")
|
536 |
+
extensions = [ext] if ext is not None else []
|
537 |
+
base, ext = os.path.splitext(path)
|
538 |
+
self._append_ext(extensions, ext)
|
539 |
+
|
540 |
+
for g in self._guess_ext_magic(path):
|
541 |
+
self._append_ext(extensions, g)
|
542 |
+
return self._convert(path, extensions, **kwargs)
|
543 |
+
|
544 |
+
def convert_stream(
|
545 |
+
self, stream: Any, **kwargs: Any
|
546 |
+
) -> DocumentConverterResult:
|
547 |
+
ext = kwargs.get("file_extension")
|
548 |
+
extensions = [ext] if ext is not None else []
|
549 |
+
handle, temp_path = tempfile.mkstemp()
|
550 |
+
fh = os.fdopen(handle, "wb")
|
551 |
+
result = None
|
552 |
+
try:
|
553 |
+
content = stream.read()
|
554 |
+
if isinstance(content, str):
|
555 |
+
fh.write(content.encode("utf-8"))
|
556 |
+
else:
|
557 |
+
fh.write(content)
|
558 |
+
fh.close()
|
559 |
+
for g in self._guess_ext_magic(temp_path):
|
560 |
+
self._append_ext(extensions, g)
|
561 |
+
result = self._convert(temp_path, extensions, **kwargs)
|
562 |
+
finally:
|
563 |
+
try:
|
564 |
+
fh.close()
|
565 |
+
except Exception:
|
566 |
+
pass
|
567 |
+
os.unlink(temp_path)
|
568 |
+
|
569 |
+
return result
|
570 |
+
|
571 |
+
def convert_url(
|
572 |
+
self, url: str, **kwargs: Any
|
573 |
+
) -> DocumentConverterResult:
|
574 |
+
response = self._requests_session.get(url, stream=True)
|
575 |
+
response.raise_for_status()
|
576 |
+
return self.convert_response(response, **kwargs)
|
577 |
+
|
578 |
+
def convert_response(
|
579 |
+
self, response: requests.Response, **kwargs: Any
|
580 |
+
) -> DocumentConverterResult:
|
581 |
+
ext = kwargs.get("file_extension")
|
582 |
+
extensions = [ext] if ext is not None else []
|
583 |
+
content_type = response.headers.get("content-type", "").split(";")[0]
|
584 |
+
self._append_ext(extensions, mimetypes.guess_extension(content_type))
|
585 |
+
content_disposition = response.headers.get("content-disposition", "")
|
586 |
+
m = re.search(r"filename=([^;]+)", content_disposition)
|
587 |
+
if m:
|
588 |
+
base, ext = os.path.splitext(m.group(1).strip("\"'"))
|
589 |
+
self._append_ext(extensions, ext)
|
590 |
+
base, ext = os.path.splitext(urlparse(response.url).path)
|
591 |
+
self._append_ext(extensions, ext)
|
592 |
+
handle, temp_path = tempfile.mkstemp()
|
593 |
+
fh = os.fdopen(handle, "wb")
|
594 |
+
result = None
|
595 |
+
try:
|
596 |
+
for chunk in response.iter_content(chunk_size=512):
|
597 |
+
fh.write(chunk)
|
598 |
+
fh.close()
|
599 |
+
for g in self._guess_ext_magic(temp_path):
|
600 |
+
self._append_ext(extensions, g)
|
601 |
+
|
602 |
+
result = self._convert(temp_path, extensions, url=response.url, **kwargs)
|
603 |
+
finally:
|
604 |
+
try:
|
605 |
+
fh.close()
|
606 |
+
except Exception:
|
607 |
+
pass
|
608 |
+
os.unlink(temp_path)
|
609 |
+
|
610 |
+
return result
|
611 |
+
|
612 |
+
def _convert(
|
613 |
+
self, local_path: str, extensions: List[Union[str, None]], **kwargs
|
614 |
+
) -> DocumentConverterResult:
|
615 |
+
error_trace = ""
|
616 |
+
for ext in extensions + [None]:
|
617 |
+
for converter in self._page_converters:
|
618 |
+
_kwargs = copy.deepcopy(kwargs)
|
619 |
+
if ext is None:
|
620 |
+
if "file_extension" in _kwargs:
|
621 |
+
del _kwargs["file_extension"]
|
622 |
+
else:
|
623 |
+
_kwargs.update({"file_extension": ext})
|
624 |
+
|
625 |
+
_kwargs["_parent_converters"] = self._page_converters
|
626 |
+
if "style_map" not in _kwargs and self._style_map is not None:
|
627 |
+
_kwargs["style_map"] = self._style_map
|
628 |
+
|
629 |
+
try:
|
630 |
+
res = converter.convert(local_path, **_kwargs)
|
631 |
+
if res is not None:
|
632 |
+
res.text_content = "\n".join(
|
633 |
+
[line.rstrip() for line in re.split(r"\r?\n", res.text_content)]
|
634 |
+
)
|
635 |
+
res.text_content = re.sub(r"\n{3,}", "\n\n", res.text_content)
|
636 |
+
return res
|
637 |
+
except Exception as e:
|
638 |
+
# If this converter supports the extension and fails, raise the exception
|
639 |
+
if ext is not None and converter.supports_extension(ext):
|
640 |
+
raise FileConversionException(
|
641 |
+
f"Could not convert '{local_path}' to Markdown with {converter.__class__.__name__} "
|
642 |
+
f"for extension '{ext}'. The following error occurred:\n\n{traceback.format_exc()}"
|
643 |
+
)
|
644 |
+
# Otherwise, store the error and continue
|
645 |
+
error_trace = ("\n\n" + traceback.format_exc()).strip()
|
646 |
+
|
647 |
+
if len(error_trace) > 0:
|
648 |
+
raise FileConversionException(
|
649 |
+
f"Could not convert '{local_path}' to Markdown. File type was recognized as {extensions}. "
|
650 |
+
f"While converting the file, the following error was encountered:\n\n{error_trace}"
|
651 |
+
)
|
652 |
+
raise UnsupportedFormatException(
|
653 |
+
f"Could not convert '{local_path}' to Markdown. The formats {extensions} are not supported."
|
654 |
+
)
|
655 |
+
|
656 |
+
def _append_ext(self, extensions, ext):
|
657 |
+
if ext is None:
|
658 |
+
return
|
659 |
+
ext = ext.strip()
|
660 |
+
if ext == "":
|
661 |
+
return
|
662 |
+
extensions.append(ext)
|
663 |
+
|
664 |
+
def _guess_ext_magic(self, path):
|
665 |
+
try:
|
666 |
+
guesses = puremagic.magic_file(path)
|
667 |
+
extensions = list()
|
668 |
+
for g in guesses:
|
669 |
+
ext = g.extension.strip()
|
670 |
+
if len(ext) > 0:
|
671 |
+
if not ext.startswith("."):
|
672 |
+
ext = "." + ext
|
673 |
+
if ext not in extensions:
|
674 |
+
extensions.append(ext)
|
675 |
+
return extensions
|
676 |
+
except FileNotFoundError:
|
677 |
+
pass
|
678 |
+
except IsADirectoryError:
|
679 |
+
pass
|
680 |
+
except PermissionError:
|
681 |
+
pass
|
682 |
+
return []
|
683 |
+
|
684 |
+
def register_page_converter(self, converter: DocumentConverter) -> None:
|
685 |
+
self._page_converters.insert(0, converter)
|
app/services/RAG_evaluation.py
ADDED
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Dict, Any
|
2 |
+
import re
|
3 |
+
from datetime import datetime
|
4 |
+
import nltk
|
5 |
+
from nltk.corpus import stopwords
|
6 |
+
from nltk.stem import WordNetLemmatizer
|
7 |
+
from sklearn.feature_extraction.text import TfidfVectorizer
|
8 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
9 |
+
from app.services.chathistory import ChatSession
|
10 |
+
import os
|
11 |
+
|
12 |
+
# # Set NLTK data path to a writable location
|
13 |
+
# nltk_data_dir = os.path.join(os.getcwd(), "nltk_data")
|
14 |
+
# os.makedirs(nltk_data_dir, exist_ok=True)
|
15 |
+
# nltk.data.path.append(nltk_data_dir)
|
16 |
+
|
17 |
+
# # Download NLTK resources to the specified directory
|
18 |
+
# nltk.download('stopwords', download_dir=nltk_data_dir)
|
19 |
+
# nltk.download('wordnet', download_dir=nltk_data_dir)
|
20 |
+
|
21 |
+
|
22 |
+
class RAGEvaluation:
|
23 |
+
def __init__(self, token: str, page: int = 1, page_size: int = 5):
|
24 |
+
self.chat_session = ChatSession(token, "session_id")
|
25 |
+
self.page = page
|
26 |
+
self.page_size = page_size
|
27 |
+
self.lemmatizer = WordNetLemmatizer()
|
28 |
+
self.stop_words = set(stopwords.words('english'))
|
29 |
+
|
30 |
+
def _preprocess_text(self, text: str) -> str:
|
31 |
+
text = re.sub(r'[^a-zA-Z0-9\s]', '', text.lower())
|
32 |
+
words = text.split()
|
33 |
+
lemmatized_words = [self.lemmatizer.lemmatize(word) for word in words]
|
34 |
+
filtered_words = [word for word in lemmatized_words if word not in self.stop_words]
|
35 |
+
seen = set()
|
36 |
+
cleaned_words = []
|
37 |
+
for word in filtered_words:
|
38 |
+
if word not in seen:
|
39 |
+
seen.add(word)
|
40 |
+
cleaned_words.append(word)
|
41 |
+
|
42 |
+
return ' '.join(cleaned_words)
|
43 |
+
|
44 |
+
def _calculate_cosine_similarity(self, context: str, response: str) -> float:
|
45 |
+
clean_context = self._preprocess_text(context)
|
46 |
+
clean_response = self._preprocess_text(response)
|
47 |
+
vectorizer = TfidfVectorizer(vocabulary=clean_context.split())
|
48 |
+
|
49 |
+
try:
|
50 |
+
context_vector = vectorizer.fit_transform([clean_context])
|
51 |
+
response_vector = vectorizer.transform([clean_response])
|
52 |
+
return cosine_similarity(context_vector, response_vector)[0][0]
|
53 |
+
except ValueError:
|
54 |
+
return 0.0
|
55 |
+
|
56 |
+
def _calculate_time_difference(self, start_time: str, end_time: str) -> float:
|
57 |
+
start = datetime.fromisoformat(start_time)
|
58 |
+
end = datetime.fromisoformat(end_time)
|
59 |
+
return (end - start).total_seconds()
|
60 |
+
|
61 |
+
def _process_interaction(self, interaction: Dict[str, Any]) -> Dict[str, Any]:
|
62 |
+
processed = interaction.copy()
|
63 |
+
processed['accuracy'] = self._calculate_cosine_similarity(
|
64 |
+
interaction['context'],
|
65 |
+
interaction['response']
|
66 |
+
)
|
67 |
+
processed['overall_time'] = self._calculate_time_difference(
|
68 |
+
interaction['rag_start_time'],
|
69 |
+
interaction['rag_end_time']
|
70 |
+
)
|
71 |
+
return processed
|
72 |
+
|
73 |
+
def generate_evaluation_report(self) -> Dict[str, Any]:
|
74 |
+
raw_data = self.chat_session.get_save_details(
|
75 |
+
page=self.page,
|
76 |
+
page_size=self.page_size
|
77 |
+
)
|
78 |
+
|
79 |
+
return {
|
80 |
+
'total_interactions': raw_data['total_interactions'],
|
81 |
+
'page': raw_data['page'],
|
82 |
+
'page_size': raw_data['page_size'],
|
83 |
+
'total_pages': raw_data['total_pages'],
|
84 |
+
'results': [self._process_interaction(i) for i in raw_data['results']]
|
85 |
+
}
|
app/services/__init__.py
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# app/services/__init__.py
|
2 |
+
from app.services.image_processor import ImageProcessor
|
3 |
+
from app.services.image_classification_vit import SkinDiseaseClassifier
|
4 |
+
from app.services.llm_model import Model
|
5 |
+
from app.services.chat_processor import ChatProcessor
|
6 |
+
from app.services.chathistory import ChatSession
|
7 |
+
from app.services.environmental_condition import EnvironmentalData
|
8 |
+
from app.services.prompts import *
|
9 |
+
from app.services.RAG_evaluation import RAGEvaluation
|
10 |
+
from app.services.report_process import Report
|
11 |
+
from app.services.skincare_scheduler import SkinCareScheduler
|
12 |
+
from app.services.vector_database_search import VectorDatabaseSearch
|
13 |
+
from app.services.websearch import WebSearch
|
14 |
+
from app.services.wheel import EnvironmentalConditions
|
15 |
+
from app.services.MagicConvert import MagicConvert
|
16 |
+
|
17 |
+
__all__ = [
|
18 |
+
"ImageProcessor",
|
19 |
+
"AISkinDetector",
|
20 |
+
"SkinDiseaseClassifier",
|
21 |
+
"Model",
|
22 |
+
"ChatProcessor",
|
23 |
+
"ChatSession",
|
24 |
+
"EnvironmentalData",
|
25 |
+
"RAGEvaluation",
|
26 |
+
"Report",
|
27 |
+
"SkinCareScheduler",
|
28 |
+
"VectorDatabaseSearch",
|
29 |
+
"WebSearch",
|
30 |
+
"EnvironmentalConditions"
|
31 |
+
"MagicConvert"
|
32 |
+
]
|
app/services/chat_processor.py
ADDED
@@ -0,0 +1,278 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import datetime, timezone
|
2 |
+
from typing import Optional, Dict, Any
|
3 |
+
from concurrent.futures import ThreadPoolExecutor
|
4 |
+
from yake import KeywordExtractor
|
5 |
+
from app.services.chathistory import ChatSession
|
6 |
+
from app.services.websearch import WebSearch
|
7 |
+
from app.services.llm_model import Model
|
8 |
+
from app.services.environmental_condition import EnvironmentalData
|
9 |
+
from app.services.prompts import *
|
10 |
+
from app.services.vector_database_search import VectorDatabaseSearch
|
11 |
+
import re
|
12 |
+
vectordb = VectorDatabaseSearch()
|
13 |
+
|
14 |
+
class ChatProcessor:
|
15 |
+
def __init__(self, token: str, session_id: Optional[str] = None, num_results: int = 3, num_images: int = 3):
|
16 |
+
self.token = token
|
17 |
+
self.session_id = session_id
|
18 |
+
self.num_results = num_results
|
19 |
+
self.num_images = num_images
|
20 |
+
self.chat_session = ChatSession(token, session_id)
|
21 |
+
self.user_city = self.chat_session.get_city()
|
22 |
+
city = self.user_city if self.user_city else ''
|
23 |
+
self.environment_data = EnvironmentalData(city)
|
24 |
+
self.web_searcher = WebSearch(num_results=num_results, max_images=num_images)
|
25 |
+
self.web_search_required = True
|
26 |
+
|
27 |
+
def extract_keywords_yake(self, text: str, language: str, max_ngram_size: int = 2, num_keywords: int = 4) -> list:
|
28 |
+
lang_code = "en"
|
29 |
+
if language.lower() == "urdu":
|
30 |
+
lang_code = "ur"
|
31 |
+
|
32 |
+
kw_extractor = KeywordExtractor(
|
33 |
+
lan=lang_code,
|
34 |
+
n=max_ngram_size,
|
35 |
+
top=num_keywords,
|
36 |
+
features=None
|
37 |
+
)
|
38 |
+
keywords = kw_extractor.extract_keywords(text)
|
39 |
+
return [kw[0] for kw in keywords]
|
40 |
+
|
41 |
+
def ensure_valid_session(self, title: str = None) -> str:
|
42 |
+
if not self.session_id or not self.session_id.strip():
|
43 |
+
self.chat_session.create_new_session(title=title)
|
44 |
+
self.session_id = self.chat_session.session_id
|
45 |
+
else:
|
46 |
+
try:
|
47 |
+
if not self.chat_session.validate_session(self.session_id, title=title):
|
48 |
+
self.chat_session.create_new_session(title=title)
|
49 |
+
self.session_id = self.chat_session.session_id
|
50 |
+
except ValueError:
|
51 |
+
self.chat_session.create_new_session(title=title)
|
52 |
+
self.session_id = self.chat_session.session_id
|
53 |
+
return self.session_id
|
54 |
+
|
55 |
+
def process_chat(self, query: str) -> Dict[str, Any]:
|
56 |
+
try:
|
57 |
+
profile = self.chat_session.get_name_and_age()
|
58 |
+
name = profile['name']
|
59 |
+
age = profile['age']
|
60 |
+
self.chat_session.load_chat_history()
|
61 |
+
self.chat_session.update_title(self.session_id,query)
|
62 |
+
history = self.chat_session.format_history()
|
63 |
+
|
64 |
+
history_based_prompt = HISTORY_BASED_PROMPT.format(history=history,query= query)
|
65 |
+
|
66 |
+
enhanced_query = Model().send_message_openrouter(history_based_prompt)
|
67 |
+
|
68 |
+
self.session_id = self.ensure_valid_session(title=enhanced_query)
|
69 |
+
permission = self.chat_session.get_user_preferences()
|
70 |
+
websearch_enabled = permission.get('websearch', False)
|
71 |
+
env_recommendations = permission.get('environmental_recommendations', False)
|
72 |
+
personalized_recommendations = permission.get('personalized_recommendations', False)
|
73 |
+
keywords_permission = permission.get('keywords', False)
|
74 |
+
reference_permission = permission.get('references', False)
|
75 |
+
language = self.chat_session.get_language().lower()
|
76 |
+
|
77 |
+
|
78 |
+
language_prompt = LANGUAGE_RESPONSE_PROMPT.format(language = language)
|
79 |
+
|
80 |
+
if websearch_enabled :
|
81 |
+
with ThreadPoolExecutor(max_workers=2) as executor:
|
82 |
+
future_web = executor.submit(self.web_searcher.search, enhanced_query)
|
83 |
+
future_images = executor.submit(self.web_searcher.search_images, enhanced_query)
|
84 |
+
web_results = future_web.result()
|
85 |
+
image_results = future_images.result()
|
86 |
+
|
87 |
+
context_parts = []
|
88 |
+
references = []
|
89 |
+
|
90 |
+
for idx, result in enumerate(web_results, 1):
|
91 |
+
if result['text']:
|
92 |
+
context_parts.append(f"From Source {idx}: {result['text']}\n")
|
93 |
+
references.append(result['link'])
|
94 |
+
|
95 |
+
context = "\n".join(context_parts)
|
96 |
+
|
97 |
+
if env_recommendations and personalized_recommendations:
|
98 |
+
prompt = ENVIRONMENTAL_PERSONALIZED_PROMPT.format(
|
99 |
+
user_name=name,
|
100 |
+
user_age=age,
|
101 |
+
history=history,
|
102 |
+
user_details=self.chat_session.get_personalized_recommendation(),
|
103 |
+
environmental_condition=self.environment_data.get_environmental_data(),
|
104 |
+
previous_history=history,
|
105 |
+
context=context,
|
106 |
+
current_query=enhanced_query
|
107 |
+
)
|
108 |
+
elif personalized_recommendations:
|
109 |
+
prompt = PERSONALIZED_PROMPT.format(
|
110 |
+
user_name=name,
|
111 |
+
user_age=age,
|
112 |
+
user_details=self.chat_session.get_personalized_recommendation(),
|
113 |
+
previous_history=history,
|
114 |
+
context=context,
|
115 |
+
current_query=enhanced_query
|
116 |
+
)
|
117 |
+
elif env_recommendations :
|
118 |
+
prompt = ENVIRONMENTAL_PROMPT.format(
|
119 |
+
user_name=name,
|
120 |
+
user_age=age,
|
121 |
+
environmental_condition=self.environment_data.get_environmental_data(),
|
122 |
+
previous_history=history,
|
123 |
+
context=context,
|
124 |
+
current_query=enhanced_query
|
125 |
+
)
|
126 |
+
else:
|
127 |
+
prompt = DEFAULT_PROMPT.format(
|
128 |
+
previous_history=history,
|
129 |
+
context=context,
|
130 |
+
current_query=enhanced_query
|
131 |
+
)
|
132 |
+
|
133 |
+
prompt = prompt + language_prompt
|
134 |
+
|
135 |
+
response = Model().llm(prompt,enhanced_query)
|
136 |
+
|
137 |
+
keywords = ""
|
138 |
+
|
139 |
+
if (keywords_permission):
|
140 |
+
keywords = self.extract_keywords_yake(response, language=language)
|
141 |
+
if (not reference_permission):
|
142 |
+
references = ""
|
143 |
+
|
144 |
+
chat_data = {
|
145 |
+
"query": enhanced_query,
|
146 |
+
"response": response,
|
147 |
+
"references": references,
|
148 |
+
"page_no": "",
|
149 |
+
"keywords": keywords,
|
150 |
+
"images": image_results,
|
151 |
+
"context": context,
|
152 |
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
153 |
+
"session_id": self.chat_session.session_id
|
154 |
+
}
|
155 |
+
|
156 |
+
if not self.chat_session.save_chat(chat_data):
|
157 |
+
raise ValueError("Failed to save chat message")
|
158 |
+
return chat_data
|
159 |
+
|
160 |
+
else:
|
161 |
+
attach_image = False
|
162 |
+
|
163 |
+
with ThreadPoolExecutor(max_workers=2) as executor:
|
164 |
+
future_images = executor.submit(self.web_searcher.search_images, enhanced_query)
|
165 |
+
image_results = future_images.result()
|
166 |
+
|
167 |
+
start_time = datetime.now(timezone.utc)
|
168 |
+
|
169 |
+
results = vectordb.search( query=enhanced_query, top_k=3)
|
170 |
+
|
171 |
+
context_parts = []
|
172 |
+
references = []
|
173 |
+
seen_pages = set()
|
174 |
+
|
175 |
+
for result in results:
|
176 |
+
confidence = result['confidence']
|
177 |
+
if confidence > 60:
|
178 |
+
context_parts.append(f"Content: {result['content']}")
|
179 |
+
page = result['page']
|
180 |
+
if page not in seen_pages: # Only append if page is not seen
|
181 |
+
references.append(f"Source: {result['source']}, Page: {page}")
|
182 |
+
seen_pages.add(page)
|
183 |
+
attach_image = True
|
184 |
+
|
185 |
+
context = "\n".join(context_parts)
|
186 |
+
|
187 |
+
if not context or len(context) < 10:
|
188 |
+
context = "There is no context found unfortunately"
|
189 |
+
|
190 |
+
if env_recommendations and personalized_recommendations:
|
191 |
+
prompt = ENVIRONMENTAL_PERSONALIZED_PROMPT.format(
|
192 |
+
user_name=name,
|
193 |
+
user_age = age,
|
194 |
+
history=history,
|
195 |
+
user_details=self.chat_session.get_personalized_recommendation(),
|
196 |
+
environmental_condition=self.environment_data.get_environmental_data(),
|
197 |
+
previous_history=history,
|
198 |
+
context=context,
|
199 |
+
current_query=enhanced_query
|
200 |
+
)
|
201 |
+
elif personalized_recommendations:
|
202 |
+
prompt = PERSONALIZED_PROMPT.format(
|
203 |
+
user_name=name,
|
204 |
+
user_age=age,
|
205 |
+
user_details=self.chat_session.get_personalized_recommendation(),
|
206 |
+
previous_history=history,
|
207 |
+
context=context,
|
208 |
+
current_query=enhanced_query
|
209 |
+
)
|
210 |
+
elif env_recommendations :
|
211 |
+
prompt = ENVIRONMENTAL_PROMPT.format(
|
212 |
+
user_name=name,
|
213 |
+
user_age=age,
|
214 |
+
environmental_condition=self.environment_data.get_environmental_data(),
|
215 |
+
previous_history=history,
|
216 |
+
context=context,
|
217 |
+
current_query=enhanced_query
|
218 |
+
)
|
219 |
+
else:
|
220 |
+
prompt = DEFAULT_PROMPT.format(
|
221 |
+
previous_history=history,
|
222 |
+
context=context,
|
223 |
+
current_query=enhanced_query
|
224 |
+
)
|
225 |
+
|
226 |
+
prompt = prompt + language_prompt
|
227 |
+
|
228 |
+
response = Model().response = Model().llm(prompt,query)
|
229 |
+
|
230 |
+
end_time = datetime.now(timezone.utc)
|
231 |
+
|
232 |
+
keywords = ""
|
233 |
+
|
234 |
+
if (keywords_permission):
|
235 |
+
keywords = self.extract_keywords_yake(response, language=language)
|
236 |
+
|
237 |
+
if (not reference_permission):
|
238 |
+
references = ""
|
239 |
+
|
240 |
+
if not attach_image:
|
241 |
+
image_results = ""
|
242 |
+
keywords = ""
|
243 |
+
|
244 |
+
chat_data = {
|
245 |
+
"query": enhanced_query,
|
246 |
+
"response": response,
|
247 |
+
"references": references,
|
248 |
+
"page_no": "",
|
249 |
+
"keywords": keywords,
|
250 |
+
"images": image_results,
|
251 |
+
"context": context,
|
252 |
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
253 |
+
"session_id": self.chat_session.session_id
|
254 |
+
}
|
255 |
+
match = re.search(r'(## Personal Recommendations|## Environmental Considerations)', response)
|
256 |
+
if match:
|
257 |
+
truncated_response = response[:match.start()].strip()
|
258 |
+
else:
|
259 |
+
truncated_response = response
|
260 |
+
if not self.chat_session.save_details(session_id=self.session_id , context= context , query= enhanced_query , response=truncated_response , rag_start_time=start_time , rag_end_time=end_time ):
|
261 |
+
raise ValueError("Failed to save the RAG details")
|
262 |
+
if not self.chat_session.save_chat(chat_data):
|
263 |
+
raise ValueError("Failed to save chat message")
|
264 |
+
return chat_data
|
265 |
+
|
266 |
+
except Exception as e:
|
267 |
+
return {
|
268 |
+
"error": str(e),
|
269 |
+
"query": query,
|
270 |
+
"response": "Sorry, there was an error processing your request.",
|
271 |
+
"timestamp": datetime.now(timezone.utc).isoformat()
|
272 |
+
}
|
273 |
+
|
274 |
+
def web_search(self, query: str) -> Dict[str, Any]:
|
275 |
+
if self.session_id and len(self.session_id) > 5:
|
276 |
+
return self.process_chat(query=query)
|
277 |
+
else:
|
278 |
+
return self.process_chat(query=query)
|
app/services/chathistory.py
ADDED
@@ -0,0 +1,309 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from app.database.database_query import DatabaseQuery
|
2 |
+
import os
|
3 |
+
import jwt
|
4 |
+
from dotenv import load_dotenv
|
5 |
+
from typing import Optional, Dict, List
|
6 |
+
from bson import ObjectId
|
7 |
+
from datetime import datetime
|
8 |
+
|
9 |
+
load_dotenv()
|
10 |
+
jwt_secret_key = os.getenv('JWT_SECRET_KEY')
|
11 |
+
query = DatabaseQuery()
|
12 |
+
|
13 |
+
class ChatSession:
|
14 |
+
def __init__(self, token: str , session_id: str):
|
15 |
+
self.token = token
|
16 |
+
self.session_id = session_id
|
17 |
+
self.chats = []
|
18 |
+
self.identity = self._decode_token(token)
|
19 |
+
self.query = query
|
20 |
+
|
21 |
+
def _decode_token(self, token: str) -> str:
|
22 |
+
try:
|
23 |
+
decoded_token = jwt.decode(token, jwt_secret_key, algorithms=["HS256"])
|
24 |
+
identity = decoded_token['sub']
|
25 |
+
return identity
|
26 |
+
except jwt.ExpiredSignatureError:
|
27 |
+
raise ValueError("The token has expired.")
|
28 |
+
except jwt.InvalidTokenError:
|
29 |
+
raise ValueError("Invalid token.")
|
30 |
+
except Exception as e:
|
31 |
+
raise ValueError(f"Failed to decode token: {e}")
|
32 |
+
|
33 |
+
def get_user_preferences(self) -> dict:
|
34 |
+
current_user = self.identity
|
35 |
+
preferences = self.query.get_user_preferences(current_user)
|
36 |
+
if preferences is not None:
|
37 |
+
return preferences
|
38 |
+
raise ValueError("Failed to fetch user preferences.")
|
39 |
+
|
40 |
+
def get_personalized_recommendation(self) -> Optional[str]:
|
41 |
+
current_user = self.identity
|
42 |
+
response = self.query.get_latest_questionnaire(current_user)
|
43 |
+
|
44 |
+
if not response:
|
45 |
+
return None
|
46 |
+
|
47 |
+
answers = response.get('answers', {})
|
48 |
+
if not answers:
|
49 |
+
return None
|
50 |
+
|
51 |
+
def format_answer(answer):
|
52 |
+
if answer is None:
|
53 |
+
return None
|
54 |
+
if isinstance(answer, str):
|
55 |
+
stripped_answer = answer.strip().lower()
|
56 |
+
if stripped_answer in ['none', ''] or len(stripped_answer) < 3:
|
57 |
+
return None
|
58 |
+
return stripped_answer
|
59 |
+
if isinstance(answer, list):
|
60 |
+
filtered_answer = [item for item in answer if "Other" not in item
|
61 |
+
and item.strip().lower() not in ['none', '']
|
62 |
+
and len(item.strip()) >= 3]
|
63 |
+
return ", ".join(filtered_answer) if filtered_answer else None
|
64 |
+
return answer
|
65 |
+
|
66 |
+
questions = {
|
67 |
+
"skinType": "How would you describe your skin type?",
|
68 |
+
"currentConditions": "Do you currently have any skin conditions?",
|
69 |
+
"autoImmuneConditions": "Do you have a history of autoimmune or hormonal conditions?",
|
70 |
+
"allergies": "Do you have any known allergies to skincare ingredients?",
|
71 |
+
"medications": "Are you currently taking any medications that might affect your skin?",
|
72 |
+
"hormonal": "Do you experience hormonal changes that affect your skin?",
|
73 |
+
"diet": "Have you noticed any foods that trigger skin reactions?",
|
74 |
+
"diabetes": "Do you have diabetes?",
|
75 |
+
"outdoorTime": "How much time do you spend outdoors during the day?",
|
76 |
+
"sleep": "How many hours of sleep do you get on average?",
|
77 |
+
"familyHistory": "Do you have a family history of skin conditions?",
|
78 |
+
"products": "What skincare products are you currently using?"
|
79 |
+
}
|
80 |
+
|
81 |
+
valid_answers = {key: format_answer(answers.get(key))
|
82 |
+
for key in questions
|
83 |
+
if format_answer(answers.get(key)) is not None}
|
84 |
+
|
85 |
+
if not valid_answers:
|
86 |
+
return None
|
87 |
+
|
88 |
+
formatted_response = []
|
89 |
+
for key, answer in valid_answers.items():
|
90 |
+
question = questions.get(key)
|
91 |
+
formatted_response.append(f"question: {question}\nUser answer: {answer}")
|
92 |
+
|
93 |
+
profile = self.get_profile()
|
94 |
+
name = profile.get('name', 'Unknown')
|
95 |
+
age = profile.get('age', 'Unknown')
|
96 |
+
|
97 |
+
return f"user name: {name}\nuser age: {age}\n\n" + "\n\n".join(formatted_response)
|
98 |
+
|
99 |
+
|
100 |
+
def create_new_session(self, title: str = None) -> bool:
|
101 |
+
current_user = self.identity
|
102 |
+
session_id = str(ObjectId())
|
103 |
+
|
104 |
+
chat_session = {
|
105 |
+
"user_id": current_user,
|
106 |
+
"session_id": session_id,
|
107 |
+
"created_at": datetime.utcnow(),
|
108 |
+
"last_accessed": datetime.utcnow(),
|
109 |
+
"title": title if title else "New Chat"
|
110 |
+
}
|
111 |
+
|
112 |
+
try:
|
113 |
+
self.query.create_chat_session(chat_session)
|
114 |
+
self.session_id = session_id
|
115 |
+
return True
|
116 |
+
except Exception as e:
|
117 |
+
raise Exception(f"Failed to create session: {str(e)}")
|
118 |
+
|
119 |
+
def verify_session_exists(self, session_id: str) -> bool:
|
120 |
+
current_user = self.identity
|
121 |
+
return self.query.verify_session(session_id, current_user)
|
122 |
+
|
123 |
+
def validate_session(self, session_id: Optional[str] = None, title: str = None) -> bool:
|
124 |
+
if not session_id or not session_id.strip():
|
125 |
+
return self.create_new_session(title=title)
|
126 |
+
|
127 |
+
if self.verify_session_exists(session_id):
|
128 |
+
self.session_id = session_id
|
129 |
+
return self.load_chat_history()
|
130 |
+
|
131 |
+
return self.create_new_session(title=title)
|
132 |
+
|
133 |
+
def load_session(self, session_id: str) -> bool:
|
134 |
+
return self.validate_session(session_id)
|
135 |
+
|
136 |
+
def load_chat_history(self) -> bool:
|
137 |
+
if not self.session_id:
|
138 |
+
raise ValueError("No session ID provided.")
|
139 |
+
|
140 |
+
current_user = self.identity
|
141 |
+
try:
|
142 |
+
self.chats = self.query.get_session_chats(self.session_id, current_user)
|
143 |
+
return True
|
144 |
+
except Exception as e:
|
145 |
+
raise Exception(f"Failed to load chat history: {str(e)}")
|
146 |
+
|
147 |
+
def get_chat_history(self) -> List[Dict]:
|
148 |
+
return self.chats
|
149 |
+
|
150 |
+
def format_history(self) -> str:
|
151 |
+
formatted_chats = []
|
152 |
+
for chat in self.chats:
|
153 |
+
query = chat.get('query', '').strip()
|
154 |
+
response = chat.get('response', '').strip()
|
155 |
+
if query and response:
|
156 |
+
formatted_chats.append(f"User: {query}")
|
157 |
+
formatted_chats.append(f"Assistant: {response}")
|
158 |
+
return "\n".join(formatted_chats) if formatted_chats else ""
|
159 |
+
|
160 |
+
def save_chat(self, chat_data: Dict) -> bool:
|
161 |
+
if not self.session_id:
|
162 |
+
raise ValueError("No active session to save chat")
|
163 |
+
|
164 |
+
current_user = self.identity
|
165 |
+
|
166 |
+
data = {
|
167 |
+
"user_id": current_user,
|
168 |
+
"session_id": self.session_id,
|
169 |
+
"query": chat_data.get("query", "").strip(),
|
170 |
+
"response": chat_data.get("response", "").strip(),
|
171 |
+
"references": chat_data.get("references", []),
|
172 |
+
"page_no": chat_data.get("page_no", []),
|
173 |
+
"keywords": chat_data.get("keywords", []),
|
174 |
+
"images": chat_data.get("images", []),
|
175 |
+
"context": chat_data.get("context", ""),
|
176 |
+
"timestamp": datetime.utcnow(),
|
177 |
+
"chat_id": str(ObjectId())
|
178 |
+
}
|
179 |
+
|
180 |
+
try:
|
181 |
+
if self.query.create_chat(data):
|
182 |
+
self.query.update_last_accessed_time(self.session_id)
|
183 |
+
self.chats.append(data)
|
184 |
+
return True
|
185 |
+
return False
|
186 |
+
except Exception as e:
|
187 |
+
raise Exception(f"Failed to save chat: {str(e)}")
|
188 |
+
|
189 |
+
def get_name_and_age(self):
|
190 |
+
current_user = self.identity
|
191 |
+
try:
|
192 |
+
user_profile = self.query.get_user_profile(current_user)
|
193 |
+
return user_profile
|
194 |
+
except Exception as e:
|
195 |
+
raise Exception(f"Failed to get user name and age: {str(e)}")
|
196 |
+
|
197 |
+
def get_profile(self):
|
198 |
+
current_user = self.identity
|
199 |
+
try:
|
200 |
+
user = query.get_user_profile(current_user)
|
201 |
+
if not user:
|
202 |
+
return {'error': 'User not found'}
|
203 |
+
return {
|
204 |
+
'username': user['username'],
|
205 |
+
'email': user['email'],
|
206 |
+
'name': user['name'],
|
207 |
+
'age': user['age'],
|
208 |
+
'created_at': user['created_at']
|
209 |
+
}
|
210 |
+
except Exception as e:
|
211 |
+
return {'error': str(e)}
|
212 |
+
|
213 |
+
def update_title(self , sessionId , new_title):
|
214 |
+
query.update_chat_session_title(sessionId, new_title)
|
215 |
+
|
216 |
+
def get_city(self) -> Optional[str]:
|
217 |
+
current_user = self.identity
|
218 |
+
try:
|
219 |
+
location_data = self.query.get_location(current_user)
|
220 |
+
if location_data and 'location' in location_data:
|
221 |
+
return location_data['location']
|
222 |
+
return None
|
223 |
+
except Exception as e:
|
224 |
+
raise Exception(f"Failed to get user city: {str(e)}")
|
225 |
+
|
226 |
+
def get_language(self) -> Optional[str]:
|
227 |
+
current_user = self.identity
|
228 |
+
try:
|
229 |
+
language = self.query.get_user_language(current_user)
|
230 |
+
if not language :
|
231 |
+
return "english"
|
232 |
+
else:
|
233 |
+
return language
|
234 |
+
return None
|
235 |
+
except Exception as e:
|
236 |
+
raise Exception(f"Failed to get user city: {str(e)}")
|
237 |
+
|
238 |
+
|
239 |
+
def get_language(self) -> Optional[str]:
|
240 |
+
current_user = self.identity
|
241 |
+
try:
|
242 |
+
language = self.query.get_user_language(current_user)
|
243 |
+
if not language :
|
244 |
+
return "english"
|
245 |
+
else:
|
246 |
+
return language
|
247 |
+
return None
|
248 |
+
except Exception as e:
|
249 |
+
raise Exception(f"Failed to get user city: {str(e)}")
|
250 |
+
|
251 |
+
def get_today_schedule(self):
|
252 |
+
data = self.query.get_today_schedule(user_id=self.identity)
|
253 |
+
if not data:
|
254 |
+
return ""
|
255 |
+
return data
|
256 |
+
|
257 |
+
def save_schedule(self, schedule_data):
|
258 |
+
return self.query.save_schedule(user_id=self.identity, schedule_data=schedule_data)
|
259 |
+
|
260 |
+
def get_last_seven_days_schedules(self):
|
261 |
+
data = self.query.get_last_seven_days_schedules(user_id=self.identity)
|
262 |
+
if not data:
|
263 |
+
return ""
|
264 |
+
return data
|
265 |
+
|
266 |
+
|
267 |
+
def save_details(self, session_id, context, query, response, rag_start_time, rag_end_time):
|
268 |
+
data = self.query.save_rag_interaction(
|
269 |
+
user_id="admin",
|
270 |
+
session_id=session_id,
|
271 |
+
context=context,
|
272 |
+
query=query,
|
273 |
+
response=response,
|
274 |
+
rag_start_time=rag_start_time,
|
275 |
+
rag_end_time=rag_end_time
|
276 |
+
)
|
277 |
+
return data
|
278 |
+
|
279 |
+
def get_save_details(self, page: int, page_size: int) -> dict:
|
280 |
+
data = self.query.get_rag_interactions(
|
281 |
+
user_id="admin",
|
282 |
+
page=page,
|
283 |
+
page_size=page_size
|
284 |
+
)
|
285 |
+
return data
|
286 |
+
|
287 |
+
def log_user_image_upload(self):
|
288 |
+
"""Log an image upload for the current user"""
|
289 |
+
try:
|
290 |
+
return self.query.log_image_upload(self.identity)
|
291 |
+
except Exception as e:
|
292 |
+
raise ValueError(f"Failed to log image upload: {e}")
|
293 |
+
|
294 |
+
def get_user_daily_uploads(self):
|
295 |
+
"""Get number of images uploaded by current user in the last 24 hours"""
|
296 |
+
try:
|
297 |
+
return self.query.get_user_daily_uploads(self.identity)
|
298 |
+
except Exception as e:
|
299 |
+
raise ValueError(f"Failed to get user daily uploads: {e}")
|
300 |
+
|
301 |
+
def get_user_last_upload_time(self):
|
302 |
+
"""Get the timestamp of current user's most recent image upload"""
|
303 |
+
try:
|
304 |
+
return self.query.get_user_last_upload_time(self.identity)
|
305 |
+
except Exception as e:
|
306 |
+
raise ValueError(f"Failed to get user's last upload time: {e}")
|
307 |
+
|
308 |
+
|
309 |
+
|
app/services/environmental_condition.py
ADDED
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import requests
|
2 |
+
from bs4 import BeautifulSoup
|
3 |
+
|
4 |
+
|
5 |
+
class EnvironmentalData:
|
6 |
+
def __init__(self, city):
|
7 |
+
self.city = city
|
8 |
+
self.aqi_url = f"https://api.waqi.info/feed/{city}/?token=466cde4d55e7c5d6cc658ad9c391214b593f46b9"
|
9 |
+
self.uv_url = f"https://www.weatheronline.co.uk/Pakistan/{city}/UVindex.html"
|
10 |
+
|
11 |
+
def fetch_aqi_data(self):
|
12 |
+
try:
|
13 |
+
response = requests.get(self.aqi_url)
|
14 |
+
data = response.json()
|
15 |
+
|
16 |
+
if data["status"] == "ok":
|
17 |
+
return {
|
18 |
+
"Temperature": data["data"]["iaqi"].get("t", {}).get("v", "N/A"),
|
19 |
+
"Humidity": data["data"]["iaqi"].get("h", {}).get("v", "N/A"),
|
20 |
+
"Wind Speed": data["data"]["iaqi"].get("w", {}).get("v", "N/A"),
|
21 |
+
"Pressure": data["data"]["iaqi"].get("p", {}).get("v", "N/A"),
|
22 |
+
"AQI": data["data"].get("aqi", "N/A"),
|
23 |
+
"Dominant Pollutant": data["data"].get("dominentpol", "N/A"),
|
24 |
+
}
|
25 |
+
return self.get_default_aqi_data()
|
26 |
+
except:
|
27 |
+
return self.get_default_aqi_data()
|
28 |
+
|
29 |
+
def get_default_aqi_data(self):
|
30 |
+
return {
|
31 |
+
"Temperature": "N/A",
|
32 |
+
"Humidity": "N/A",
|
33 |
+
"Wind Speed": "N/A",
|
34 |
+
"Pressure": "N/A",
|
35 |
+
"AQI": "N/A",
|
36 |
+
"Dominant Pollutant": "N/A"
|
37 |
+
}
|
38 |
+
|
39 |
+
def fetch_uv_data(self):
|
40 |
+
try:
|
41 |
+
response = requests.get(self.uv_url)
|
42 |
+
soup = BeautifulSoup(response.text, 'html.parser')
|
43 |
+
gr1_elements = soup.find_all(class_='gr1')
|
44 |
+
|
45 |
+
if gr1_elements:
|
46 |
+
tr_elements = gr1_elements[0].find_all('tr')
|
47 |
+
if len(tr_elements) > 1:
|
48 |
+
second_tr = tr_elements[1]
|
49 |
+
td_elements = second_tr.find_all('td')
|
50 |
+
if len(td_elements) > 1:
|
51 |
+
return int(td_elements[1].text.strip())
|
52 |
+
return "N/A"
|
53 |
+
except:
|
54 |
+
return "N/A"
|
55 |
+
|
56 |
+
def get_environmental_data(self):
|
57 |
+
aqi_data = self.fetch_aqi_data()
|
58 |
+
uv_index = self.fetch_uv_data()
|
59 |
+
|
60 |
+
environmental_data = {
|
61 |
+
"Temperature": f"{aqi_data['Temperature']} °C" if aqi_data['Temperature'] != "N/A" else "N/A",
|
62 |
+
"Humidity": f"{aqi_data['Humidity']} %" if aqi_data['Humidity'] != "N/A" else "N/A",
|
63 |
+
"Wind Speed": f"{aqi_data['Wind Speed']} m/s" if aqi_data['Wind Speed'] != "N/A" else "N/A",
|
64 |
+
"Pressure": f"{aqi_data['Pressure']} hPa" if aqi_data['Pressure'] != "N/A" else "N/A",
|
65 |
+
"Air Quality Index": aqi_data['AQI'],
|
66 |
+
"Dominant Pollutant": aqi_data["Dominant Pollutant"],
|
67 |
+
"UV_Index": uv_index
|
68 |
+
}
|
69 |
+
|
70 |
+
return environmental_data
|
app/services/image_classification_vit.py
ADDED
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import torch
|
2 |
+
from PIL import Image
|
3 |
+
import torch.nn.functional as F
|
4 |
+
from torchvision import transforms
|
5 |
+
from transformers import AutoModelForImageClassification, AutoConfig
|
6 |
+
import requests
|
7 |
+
from io import BytesIO
|
8 |
+
import os
|
9 |
+
from huggingface_hub import hf_hub_download
|
10 |
+
from dotenv import load_dotenv
|
11 |
+
|
12 |
+
|
13 |
+
load_dotenv()
|
14 |
+
|
15 |
+
HUGGINGFACE_TOKEN = os.getenv("HUGGINGFACE_TOKEN")
|
16 |
+
|
17 |
+
|
18 |
+
class SkinDiseaseClassifier:
|
19 |
+
CLASS_NAMES = [
|
20 |
+
"Acne", "Basal Cell Carcinoma", "Benign Keratosis-like Lesions", "Chickenpox", "Eczema", "Healthy Skin",
|
21 |
+
"Measles", "Melanocytic Nevi", "Melanoma", "Monkeypox", "Psoriasis Lichen Planus and related diseases",
|
22 |
+
"Seborrheic Keratoses and other Benign Tumors", "Tinea Ringworm Candidiasis and other Fungal Infections",
|
23 |
+
"Vitiligo", "Warts Molluscum and other Viral Infections"
|
24 |
+
]
|
25 |
+
|
26 |
+
def __init__(self, repo_id="muhammadnoman76/skin-disease-classifier"):
|
27 |
+
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
28 |
+
self.repo_id = repo_id
|
29 |
+
self.model = self.load_trained_model()
|
30 |
+
self.transform = self.get_inference_transform()
|
31 |
+
|
32 |
+
def load_trained_model(self):
|
33 |
+
model_path= hf_hub_download(repo_id=self.repo_id, filename="healthy.pth", token=HUGGINGFACE_TOKEN)
|
34 |
+
|
35 |
+
checkpoint = torch.load(model_path, map_location=self.device, weights_only=True)
|
36 |
+
classifier_weight = checkpoint['model_state_dict']['classifier.3.weight']
|
37 |
+
num_classes = classifier_weight.size(0)
|
38 |
+
|
39 |
+
config = AutoConfig.from_pretrained("google/vit-base-patch16-224-in21k", num_labels=num_classes)
|
40 |
+
model = AutoModelForImageClassification.from_pretrained(
|
41 |
+
"google/vit-base-patch16-224-in21k",
|
42 |
+
config=config,
|
43 |
+
ignore_mismatched_sizes=True
|
44 |
+
)
|
45 |
+
|
46 |
+
in_features = model.classifier.in_features
|
47 |
+
model.classifier = torch.nn.Sequential(
|
48 |
+
torch.nn.Linear(in_features, 512),
|
49 |
+
torch.nn.ReLU(),
|
50 |
+
torch.nn.Dropout(0.3),
|
51 |
+
torch.nn.Linear(512, num_classes)
|
52 |
+
)
|
53 |
+
|
54 |
+
model.load_state_dict(checkpoint['model_state_dict'])
|
55 |
+
model = model.to(self.device)
|
56 |
+
if self.device.type == 'cuda':
|
57 |
+
model = model.half()
|
58 |
+
|
59 |
+
model.eval()
|
60 |
+
return model
|
61 |
+
|
62 |
+
def get_inference_transform(self):
|
63 |
+
return transforms.Compose([
|
64 |
+
transforms.Resize(256),
|
65 |
+
transforms.CenterCrop(224),
|
66 |
+
transforms.ToTensor(),
|
67 |
+
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
|
68 |
+
])
|
69 |
+
|
70 |
+
def load_image(self, image_input):
|
71 |
+
try:
|
72 |
+
if isinstance(image_input, Image.Image):
|
73 |
+
image = image_input
|
74 |
+
elif isinstance(image_input, str):
|
75 |
+
if image_input.startswith(('http://', 'https://')):
|
76 |
+
response = requests.get(image_input)
|
77 |
+
image = Image.open(BytesIO(response.content))
|
78 |
+
else:
|
79 |
+
if not os.path.exists(image_input):
|
80 |
+
raise FileNotFoundError(f"Image file not found: {image_input}")
|
81 |
+
image = Image.open(image_input)
|
82 |
+
elif hasattr(image_input, 'read'):
|
83 |
+
image = Image.open(image_input)
|
84 |
+
else:
|
85 |
+
raise ValueError("Unsupported image input type")
|
86 |
+
return image.convert('RGB')
|
87 |
+
except Exception as e:
|
88 |
+
raise Exception(f"Error loading image: {str(e)}")
|
89 |
+
|
90 |
+
def predict(self, image_input, confidence_threshold=0.3):
|
91 |
+
try:
|
92 |
+
image = self.load_image(image_input)
|
93 |
+
image_tensor = self.transform(image).unsqueeze(0)
|
94 |
+
if self.device.type == 'cuda':
|
95 |
+
image_tensor = image_tensor.half()
|
96 |
+
image_tensor = image_tensor.to(self.device)
|
97 |
+
with torch.inference_mode():
|
98 |
+
outputs = self.model(pixel_values=image_tensor).logits
|
99 |
+
probabilities = F.softmax(outputs, dim=1)
|
100 |
+
confidence, predicted = torch.max(probabilities, 1)
|
101 |
+
|
102 |
+
confidence = confidence.item()
|
103 |
+
predicted_class_idx = predicted.item()
|
104 |
+
confidence_percentage = round(confidence * 100, 2)
|
105 |
+
predicted_class_name = self.CLASS_NAMES[predicted_class_idx]
|
106 |
+
|
107 |
+
return predicted_class_name, confidence_percentage
|
108 |
+
|
109 |
+
except Exception as e:
|
110 |
+
raise Exception(f"Error during prediction: {str(e)}")
|
app/services/image_processor.py
ADDED
@@ -0,0 +1,459 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import datetime, timezone, timedelta
|
2 |
+
from typing import Dict, Any
|
3 |
+
from concurrent.futures import ThreadPoolExecutor
|
4 |
+
from yake import KeywordExtractor
|
5 |
+
from app.services.chathistory import ChatSession
|
6 |
+
from app.services.websearch import WebSearch
|
7 |
+
from app.services.llm_model import Model
|
8 |
+
from app.services.environmental_condition import EnvironmentalData
|
9 |
+
from app.services.prompts import *
|
10 |
+
from app.services.vector_database_search import VectorDatabaseSearch
|
11 |
+
from app.services.image_classification_vit import SkinDiseaseClassifier
|
12 |
+
import io
|
13 |
+
from PIL import Image
|
14 |
+
import os
|
15 |
+
import shutil
|
16 |
+
from werkzeug.utils import secure_filename
|
17 |
+
|
18 |
+
temp_dir = "temp"
|
19 |
+
if not os.path.exists(temp_dir):
|
20 |
+
os.makedirs(temp_dir)
|
21 |
+
|
22 |
+
upload_dir = "uploads"
|
23 |
+
if not os.path.exists(upload_dir):
|
24 |
+
os.makedirs(upload_dir)
|
25 |
+
|
26 |
+
class ImageProcessor:
|
27 |
+
def __init__(self, token: str, session_id: str, num_results: int, num_images: int, image):
|
28 |
+
self.token = token
|
29 |
+
self.image = image
|
30 |
+
self.session_id = session_id
|
31 |
+
self.num_results = num_results
|
32 |
+
self.num_images = num_images
|
33 |
+
self.vectordb = VectorDatabaseSearch()
|
34 |
+
self.chat_session = ChatSession(token, session_id)
|
35 |
+
self.user_city = self.chat_session.get_city()
|
36 |
+
city = self.user_city if self.user_city else ''
|
37 |
+
self.environment_data = EnvironmentalData(city)
|
38 |
+
self.web_searcher = WebSearch(num_results=num_results, max_images=num_images)
|
39 |
+
|
40 |
+
def extract_keywords_yake(self, text: str, language: str, max_ngram_size: int = 2, num_keywords: int = 4) -> list:
|
41 |
+
lang_code = "en"
|
42 |
+
if language.lower() == "urdu":
|
43 |
+
lang_code = "ur"
|
44 |
+
|
45 |
+
kw_extractor = KeywordExtractor(
|
46 |
+
lan=lang_code,
|
47 |
+
n=max_ngram_size,
|
48 |
+
top=num_keywords,
|
49 |
+
features=None
|
50 |
+
)
|
51 |
+
keywords = kw_extractor.extract_keywords(text)
|
52 |
+
return [kw[0] for kw in keywords]
|
53 |
+
|
54 |
+
def ensure_valid_session(self, title: str = None) -> str:
|
55 |
+
if not self.session_id or not self.session_id.strip():
|
56 |
+
self.chat_session.create_new_session(title=title)
|
57 |
+
self.session_id = self.chat_session.session_id
|
58 |
+
else:
|
59 |
+
try:
|
60 |
+
if not self.chat_session.validate_session(self.session_id, title=title):
|
61 |
+
self.chat_session.create_new_session(title=title)
|
62 |
+
self.session_id = self.chat_session.session_id
|
63 |
+
except ValueError:
|
64 |
+
self.chat_session.create_new_session(title=title)
|
65 |
+
self.session_id = self.chat_session.session_id
|
66 |
+
return self.session_id
|
67 |
+
|
68 |
+
def validate_upload(self):
|
69 |
+
"""Validate if user can upload an image based on daily limit and time restriction"""
|
70 |
+
try:
|
71 |
+
# Check daily upload limit
|
72 |
+
daily_uploads = self.chat_session.get_user_daily_uploads()
|
73 |
+
print(f"Daily uploads: {daily_uploads}")
|
74 |
+
|
75 |
+
if daily_uploads >= 5:
|
76 |
+
if self.chat_session.get_language().lower() == "urdu":
|
77 |
+
return False, "آپ کی روزانہ کی حد (5 تصاویر) پوری ہو چکی ہے۔ براہ کرم کل کوشش کریں۔"
|
78 |
+
else:
|
79 |
+
return False, "You've reached your daily limit (5 images). Please try again tomorrow."
|
80 |
+
|
81 |
+
# Check time between uploads
|
82 |
+
last_upload_time = self.chat_session.get_user_last_upload_time()
|
83 |
+
print(f"Last upload time: {last_upload_time}")
|
84 |
+
|
85 |
+
if last_upload_time:
|
86 |
+
# Ensure last_upload_time is timezone-aware
|
87 |
+
if last_upload_time.tzinfo is None:
|
88 |
+
# If naive, make it timezone-aware by attaching UTC
|
89 |
+
last_upload_time = last_upload_time.replace(tzinfo=timezone.utc)
|
90 |
+
|
91 |
+
# Now get the current time (which is already timezone-aware)
|
92 |
+
now = datetime.now(timezone.utc)
|
93 |
+
|
94 |
+
# Now both times are timezone-aware, so the subtraction will work
|
95 |
+
time_since_last = now - last_upload_time
|
96 |
+
print(f"Time since last: {time_since_last}")
|
97 |
+
|
98 |
+
if time_since_last < timedelta(minutes=1):
|
99 |
+
seconds_remaining = 60 - time_since_last.seconds
|
100 |
+
print(f"Seconds remaining: {seconds_remaining}")
|
101 |
+
|
102 |
+
if self.chat_session.get_language().lower() == "urdu":
|
103 |
+
return False, f"براہ کرم {seconds_remaining} سیکنڈ انتظار کریں اور دوبارہ کوشش کریں۔"
|
104 |
+
else:
|
105 |
+
return False, f"Please wait {seconds_remaining} seconds before uploading another image."
|
106 |
+
|
107 |
+
# Log this upload
|
108 |
+
result = self.chat_session.log_user_image_upload()
|
109 |
+
print(f"Logged upload: {result}")
|
110 |
+
return True, ""
|
111 |
+
except Exception as e:
|
112 |
+
print(f"Error in validate_upload: {str(e)}")
|
113 |
+
# Fail safely - if we can't validate, we should allow the upload
|
114 |
+
return True, ""
|
115 |
+
|
116 |
+
def process_chat(self, query: str) -> Dict[str, Any]:
|
117 |
+
try:
|
118 |
+
is_valid, message = self.validate_upload()
|
119 |
+
if not is_valid:
|
120 |
+
return {
|
121 |
+
"query": query,
|
122 |
+
"response": message,
|
123 |
+
"references": "",
|
124 |
+
"page_no": "",
|
125 |
+
"keywords": "",
|
126 |
+
"images": "",
|
127 |
+
"context": "",
|
128 |
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
129 |
+
"session_id": self.session_id or ""
|
130 |
+
}
|
131 |
+
|
132 |
+
profile = self.chat_session.get_name_and_age()
|
133 |
+
name = profile['name']
|
134 |
+
age = profile['age']
|
135 |
+
self.chat_session.load_chat_history()
|
136 |
+
self.chat_session.update_title(self.session_id, query)
|
137 |
+
history = self.chat_session.format_history()
|
138 |
+
language = self.chat_session.get_language().lower()
|
139 |
+
|
140 |
+
filename = secure_filename(self.image.filename)
|
141 |
+
temp_path = os.path.join(temp_dir, filename)
|
142 |
+
upload_path = os.path.join(upload_dir, filename)
|
143 |
+
|
144 |
+
content = self.image.file.read()
|
145 |
+
|
146 |
+
with open(temp_path, 'wb') as buffer:
|
147 |
+
buffer.write(content)
|
148 |
+
self.image.file.seek(0)
|
149 |
+
|
150 |
+
img_content = io.BytesIO(content)
|
151 |
+
pil_image = Image.open(img_content)
|
152 |
+
|
153 |
+
self.image.file.seek(0)
|
154 |
+
|
155 |
+
def background_file_ops(src, dst):
|
156 |
+
shutil.copy2(src, dst)
|
157 |
+
os.remove(src)
|
158 |
+
|
159 |
+
with ThreadPoolExecutor(max_workers=1) as file_executor:
|
160 |
+
file_executor.submit(background_file_ops, temp_path, upload_path)
|
161 |
+
|
162 |
+
if language != "urdu":
|
163 |
+
response1 = "Please provide a clear image of your skin with good lighting and a proper angle, without any filters! we can only analysis the image of skin :)"
|
164 |
+
response3 = "You have healthy skin, MaShaAllah! I don't notice any issues at the moment. However, based on my current confidence level of {diseases_detection_confidence}, I recommend consulting a doctor for more detailed advice and analysis."
|
165 |
+
response4 = "I'm sorry, I'm not able to identify your skin condition yet as I'm still learning, but I hope to be able to detect any skin issues in the future. :) Right now, my confidence in identifying your skin is below 50%."
|
166 |
+
response5 = ADVICE_REPORT_SUGGESTION
|
167 |
+
else:
|
168 |
+
response1 = "براہ کرم اپنی جلد کی واضح تصویر اچھی روشنی اور مناسب زاویے سے فراہم کریں، کسی فلٹر کے بغیر! ہم صرف جلد کی تصویر کا تجزیہ کر سکتے ہیں"
|
169 |
+
response3 = "آپ کی جلد صحت مند ہے، ماشاءاللہ! مجھے اس وقت کوئی مسئلہ نظر نہیں آ رہا۔ تاہم، میری موجودہ اعتماد کی سطح {diseases_detection_confidence} کی بنیاد پر، میں مزید تفصیلی مشورے اور تجزیے کے لیے ڈاکٹر سے رجوع کرنے کی تجویز کرتا ہوں۔"
|
170 |
+
response4 = "معذرت، میں ابھی آپ کی جلد کی حالت کی شناخت کرنے کے قابل نہیں ہوں کیونکہ میں ابھی سیکھ رہا ہوں، لیکن مجھے امید ہے کہ مستقبل میں جلد کے کسی بھی مسئلے کو پہچان سکوں گا۔ :) اس وقت آپ کی جلد کی شناخت میں میرا اعتماد 50% سے کم ہے۔"
|
171 |
+
response5 = URDU_ADVICE_REPORT_SUGGESTION
|
172 |
+
|
173 |
+
model = Model()
|
174 |
+
result = model.llm_image(text=SKIN_NON_SKIN_PROMPT, image=pil_image)
|
175 |
+
result_lower = result.lower().strip()
|
176 |
+
is_negative = any(marker in result_lower for marker in ["<no>", "no"])
|
177 |
+
|
178 |
+
if is_negative:
|
179 |
+
chat_data = {
|
180 |
+
"query": query,
|
181 |
+
"response": response1,
|
182 |
+
"references": "",
|
183 |
+
"page_no": filename,
|
184 |
+
"keywords": "",
|
185 |
+
"images": "",
|
186 |
+
"context": "",
|
187 |
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
188 |
+
"session_id": self.chat_session.session_id
|
189 |
+
}
|
190 |
+
|
191 |
+
if not self.chat_session.save_chat(chat_data):
|
192 |
+
raise ValueError("Failed to save chat message")
|
193 |
+
|
194 |
+
return chat_data
|
195 |
+
|
196 |
+
diseases_detector = SkinDiseaseClassifier()
|
197 |
+
diseases_name, diseases_detection_confidence = diseases_detector.predict(pil_image, 5)
|
198 |
+
|
199 |
+
if diseases_name == "Healthy Skin":
|
200 |
+
chat_data = {
|
201 |
+
"query": query,
|
202 |
+
"response": response3.format(diseases_detection_confidence=diseases_detection_confidence),
|
203 |
+
"references": "",
|
204 |
+
"page_no": filename,
|
205 |
+
"keywords": "",
|
206 |
+
"images": "",
|
207 |
+
"context": "",
|
208 |
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
209 |
+
"session_id": self.chat_session.session_id
|
210 |
+
}
|
211 |
+
|
212 |
+
if not self.chat_session.save_chat(chat_data):
|
213 |
+
raise ValueError("Failed to save chat message")
|
214 |
+
|
215 |
+
return chat_data
|
216 |
+
|
217 |
+
elif diseases_detection_confidence < 46:
|
218 |
+
chat_data = {
|
219 |
+
"query": query,
|
220 |
+
"response": response4,
|
221 |
+
"references": "",
|
222 |
+
"page_no": filename,
|
223 |
+
"keywords": "",
|
224 |
+
"images": "",
|
225 |
+
"context": "",
|
226 |
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
227 |
+
"session_id": self.chat_session.session_id
|
228 |
+
}
|
229 |
+
|
230 |
+
if not self.chat_session.save_chat(chat_data):
|
231 |
+
raise ValueError("Failed to save chat message")
|
232 |
+
return chat_data
|
233 |
+
|
234 |
+
|
235 |
+
if not result:
|
236 |
+
chat_data = {
|
237 |
+
"query": query,
|
238 |
+
"response": response1,
|
239 |
+
"references": "",
|
240 |
+
"page_no": filename,
|
241 |
+
"keywords": "",
|
242 |
+
"images": "",
|
243 |
+
"context": "",
|
244 |
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
245 |
+
"session_id": self.chat_session.session_id
|
246 |
+
}
|
247 |
+
|
248 |
+
if not self.chat_session.save_chat(chat_data):
|
249 |
+
raise ValueError("Failed to save chat message")
|
250 |
+
|
251 |
+
return chat_data
|
252 |
+
|
253 |
+
self.session_id = self.ensure_valid_session(title=query)
|
254 |
+
permission = self.chat_session.get_user_preferences()
|
255 |
+
websearch_enabled = permission.get('websearch', False)
|
256 |
+
env_recommendations = permission.get('environmental_recommendations', False)
|
257 |
+
personalized_recommendations = permission.get('personalized_recommendations', False)
|
258 |
+
keywords_permission = permission.get('keywords', False)
|
259 |
+
reference_permission = permission.get('references', False)
|
260 |
+
language = self.chat_session.get_language().lower()
|
261 |
+
language_prompt = LANGUAGE_RESPONSE_PROMPT.format(language=language)
|
262 |
+
|
263 |
+
if websearch_enabled:
|
264 |
+
with ThreadPoolExecutor(max_workers=2) as executor:
|
265 |
+
future_web = executor.submit(self.web_searcher.search, diseases_name)
|
266 |
+
future_images = executor.submit(self.web_searcher.search_images, diseases_name)
|
267 |
+
web_results = future_web.result()
|
268 |
+
image_results = future_images.result()
|
269 |
+
|
270 |
+
context_parts = []
|
271 |
+
references = []
|
272 |
+
|
273 |
+
for idx, result in enumerate(web_results, 1):
|
274 |
+
if result['text']:
|
275 |
+
context_parts.append(f"From Source {idx}: {result['text']}\n")
|
276 |
+
references.append(result['link'])
|
277 |
+
|
278 |
+
context = "\n".join(context_parts)
|
279 |
+
|
280 |
+
if env_recommendations and personalized_recommendations:
|
281 |
+
prompt = ENVIRONMENTAL_PERSONALIZED_PROMPT.format(
|
282 |
+
user_name=name,
|
283 |
+
user_age=age,
|
284 |
+
user_details=self.chat_session.get_personalized_recommendation(),
|
285 |
+
environmental_condition=self.environment_data.get_environmental_data(),
|
286 |
+
previous_history="",
|
287 |
+
context=context,
|
288 |
+
current_query=query
|
289 |
+
)
|
290 |
+
elif personalized_recommendations:
|
291 |
+
prompt = PERSONALIZED_PROMPT.format(
|
292 |
+
user_name=name,
|
293 |
+
user_age=age,
|
294 |
+
user_details=self.chat_session.get_personalized_recommendation(),
|
295 |
+
previous_history="",
|
296 |
+
context=context,
|
297 |
+
current_query=query
|
298 |
+
)
|
299 |
+
elif env_recommendations:
|
300 |
+
prompt = ENVIRONMENTAL_PROMPT.format(
|
301 |
+
user_name=name,
|
302 |
+
user_age=age,
|
303 |
+
environmental_condition=self.environment_data.get_environmental_data(),
|
304 |
+
previous_history="",
|
305 |
+
context=context,
|
306 |
+
current_query=query
|
307 |
+
)
|
308 |
+
else:
|
309 |
+
prompt = DEFAULT_PROMPT.format(
|
310 |
+
previous_history="",
|
311 |
+
context=context,
|
312 |
+
current_query=query
|
313 |
+
)
|
314 |
+
|
315 |
+
prompt = prompt + f"\the query is related to {diseases_name}" + language_prompt
|
316 |
+
|
317 |
+
llm_response = Model().llm(prompt, query)
|
318 |
+
|
319 |
+
response = response5.format(
|
320 |
+
diseases_name=diseases_name,
|
321 |
+
diseases_detection_confidence=diseases_detection_confidence,
|
322 |
+
response=llm_response
|
323 |
+
)
|
324 |
+
|
325 |
+
keywords = ""
|
326 |
+
|
327 |
+
if keywords_permission:
|
328 |
+
keywords = self.extract_keywords_yake(response, language=language)
|
329 |
+
if not reference_permission:
|
330 |
+
references = ""
|
331 |
+
|
332 |
+
chat_data = {
|
333 |
+
"query": query,
|
334 |
+
"response": response,
|
335 |
+
"references": references,
|
336 |
+
"page_no": filename,
|
337 |
+
"keywords": keywords,
|
338 |
+
"images": image_results,
|
339 |
+
"context": context,
|
340 |
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
341 |
+
"session_id": self.chat_session.session_id
|
342 |
+
}
|
343 |
+
|
344 |
+
if not self.chat_session.save_chat(chat_data):
|
345 |
+
raise ValueError("Failed to save chat message")
|
346 |
+
return chat_data
|
347 |
+
|
348 |
+
else:
|
349 |
+
attach_image = False
|
350 |
+
|
351 |
+
with ThreadPoolExecutor(max_workers=2) as executor:
|
352 |
+
future_images = executor.submit(self.web_searcher.search_images, diseases_name)
|
353 |
+
image_results = future_images.result()
|
354 |
+
|
355 |
+
results = self.vectordb.search(diseases_name , top_k= 3)
|
356 |
+
|
357 |
+
context_parts = []
|
358 |
+
references = []
|
359 |
+
seen_pages = set()
|
360 |
+
|
361 |
+
for result in results:
|
362 |
+
confidence = result['confidence']
|
363 |
+
if confidence > 60:
|
364 |
+
context_parts.append(f"Content: {result['content']}")
|
365 |
+
page = result['page']
|
366 |
+
if page not in seen_pages:
|
367 |
+
references.append(f"Source: {result['source']}, Page: {page}")
|
368 |
+
seen_pages.add(page)
|
369 |
+
attach_image = True
|
370 |
+
|
371 |
+
context = "\n".join(context_parts)
|
372 |
+
|
373 |
+
if not context or len(context) < 10:
|
374 |
+
context = "There is no context found unfortunately please do not answer anything and ignore previous information or recommendations that were mentioned earlier in the context."
|
375 |
+
|
376 |
+
if env_recommendations and personalized_recommendations:
|
377 |
+
prompt = ENVIRONMENTAL_PERSONALIZED_PROMPT.format(
|
378 |
+
user_name=name,
|
379 |
+
user_age=age,
|
380 |
+
user_details=self.chat_session.get_personalized_recommendation(),
|
381 |
+
environmental_condition=self.environment_data.get_environmental_data(),
|
382 |
+
previous_history="",
|
383 |
+
context=context,
|
384 |
+
current_query=query
|
385 |
+
)
|
386 |
+
elif personalized_recommendations:
|
387 |
+
prompt = PERSONALIZED_PROMPT.format(
|
388 |
+
user_name=name,
|
389 |
+
user_age=age,
|
390 |
+
user_details=self.chat_session.get_personalized_recommendation(),
|
391 |
+
previous_history="",
|
392 |
+
context=context,
|
393 |
+
current_query=query
|
394 |
+
)
|
395 |
+
elif env_recommendations:
|
396 |
+
prompt = ENVIRONMENTAL_PROMPT.format(
|
397 |
+
user_name=name,
|
398 |
+
user_age=age,
|
399 |
+
environmental_condition=self.environment_data.get_environmental_data(),
|
400 |
+
previous_history=history,
|
401 |
+
context=context,
|
402 |
+
current_query=query
|
403 |
+
)
|
404 |
+
else:
|
405 |
+
prompt = DEFAULT_PROMPT.format(
|
406 |
+
previous_history="",
|
407 |
+
context=context,
|
408 |
+
current_query=query
|
409 |
+
)
|
410 |
+
|
411 |
+
prompt = prompt + f"\the query is related to {diseases_name}" + language_prompt
|
412 |
+
|
413 |
+
llm_response = Model().llm(prompt, query)
|
414 |
+
|
415 |
+
response = response5.format(
|
416 |
+
diseases_name=diseases_name,
|
417 |
+
diseases_detection_confidence=diseases_detection_confidence,
|
418 |
+
response=llm_response
|
419 |
+
)
|
420 |
+
|
421 |
+
keywords = ""
|
422 |
+
|
423 |
+
if keywords_permission:
|
424 |
+
keywords = self.extract_keywords_yake(response, language=language)
|
425 |
+
if not reference_permission:
|
426 |
+
references = ""
|
427 |
+
if not attach_image:
|
428 |
+
image_results = ""
|
429 |
+
keywords = ""
|
430 |
+
|
431 |
+
chat_data = {
|
432 |
+
"query": query,
|
433 |
+
"response": response,
|
434 |
+
"references": references,
|
435 |
+
"page_no": filename,
|
436 |
+
"keywords": keywords,
|
437 |
+
"images": image_results,
|
438 |
+
"context": context,
|
439 |
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
440 |
+
"session_id": self.chat_session.session_id
|
441 |
+
}
|
442 |
+
|
443 |
+
if not self.chat_session.save_chat(chat_data):
|
444 |
+
raise ValueError("Failed to save chat message")
|
445 |
+
return chat_data
|
446 |
+
|
447 |
+
except Exception as e:
|
448 |
+
return {
|
449 |
+
"error": str(e),
|
450 |
+
"query": query,
|
451 |
+
"response": "Sorry, there was an error processing your request.",
|
452 |
+
"timestamp": datetime.now(timezone.utc).isoformat()
|
453 |
+
}
|
454 |
+
|
455 |
+
def web_search(self, query: str) -> Dict[str, Any]:
|
456 |
+
if self.session_id and len(self.session_id) > 5:
|
457 |
+
return self.process_chat(query=query)
|
458 |
+
else:
|
459 |
+
return self.process_chat(query=query)
|
app/services/llm_model.py
ADDED
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
from google import genai
|
3 |
+
from dotenv import load_dotenv
|
4 |
+
import os
|
5 |
+
from google import genai
|
6 |
+
from google.genai import types
|
7 |
+
import re
|
8 |
+
from g4f.client import Client
|
9 |
+
|
10 |
+
load_dotenv()
|
11 |
+
|
12 |
+
class Model:
|
13 |
+
def __init__(self):
|
14 |
+
self.gemini_api_key = os.getenv("GEMINI_API_KEY")
|
15 |
+
self.gemini_model = os.getenv("GEMINI_MODEL")
|
16 |
+
self.client = genai.Client(api_key=self.gemini_api_key)
|
17 |
+
|
18 |
+
def fall_back_llm(self, prompt):
|
19 |
+
"""Fallback method using gpt-4o-mini when Gemini fails"""
|
20 |
+
try:
|
21 |
+
response = Client().chat.completions.create(
|
22 |
+
model="gpt-4o-mini",
|
23 |
+
messages=[{"role": "user", "content": prompt}],
|
24 |
+
web_search=False
|
25 |
+
)
|
26 |
+
return response.choices[0].message.content
|
27 |
+
except Exception as e:
|
28 |
+
return f"Both primary and fallback models failed. Error: {str(e)}"
|
29 |
+
|
30 |
+
def send_message_openrouter(self, prompt):
|
31 |
+
try:
|
32 |
+
response = self.client.models.generate_content(
|
33 |
+
model=self.gemini_model,
|
34 |
+
contents=prompt
|
35 |
+
)
|
36 |
+
return response.text
|
37 |
+
except Exception as e:
|
38 |
+
print(f"Gemini failed: {str(e)}. Trying fallback model...")
|
39 |
+
return self.fall_back_llm(prompt)
|
40 |
+
|
41 |
+
def llm(self, prompt, query):
|
42 |
+
try:
|
43 |
+
combined_content = f"{prompt}\n\n{query}"
|
44 |
+
response = self.client.models.generate_content(
|
45 |
+
model=self.gemini_model,
|
46 |
+
contents=combined_content
|
47 |
+
)
|
48 |
+
return response.text
|
49 |
+
except Exception as e:
|
50 |
+
print(f"Gemini failed: {str(e)}. Trying fallback model...")
|
51 |
+
return self.fall_back_llm(f"{prompt}\n\n{query}")
|
52 |
+
|
53 |
+
def llm_image(self, text, image):
|
54 |
+
try:
|
55 |
+
response = self.client.models.generate_content(
|
56 |
+
model=self.gemini_model,
|
57 |
+
contents=[image, text],
|
58 |
+
)
|
59 |
+
return response.text
|
60 |
+
except Exception as e:
|
61 |
+
print(f"Error in llm_image: {str(e)}")
|
62 |
+
return f"Error: {str(e)}"
|
63 |
+
|
64 |
+
def clean_json_response(self, response_text):
|
65 |
+
"""Clean the model's response to extract valid JSON."""
|
66 |
+
start = response_text.find('[')
|
67 |
+
end = response_text.rfind(']') + 1
|
68 |
+
if start != -1 and end != -1:
|
69 |
+
json_str = re.sub(r",\s*]", "]", response_text[start:end])
|
70 |
+
return json_str
|
71 |
+
return response_text
|
72 |
+
|
73 |
+
def skinScheduler(self, prompt, max_retries=3):
|
74 |
+
"""Generate a skincare schedule with retries and cleaning."""
|
75 |
+
for attempt in range(max_retries):
|
76 |
+
try:
|
77 |
+
response = self.client.models.generate_content(
|
78 |
+
model=self.gemini_model,
|
79 |
+
contents=prompt
|
80 |
+
)
|
81 |
+
cleaned_response = self.clean_json_response(response.text)
|
82 |
+
return json.loads(cleaned_response)
|
83 |
+
except json.JSONDecodeError as je:
|
84 |
+
if attempt == max_retries - 1:
|
85 |
+
# If all Gemini retries fail, try fallback model
|
86 |
+
print(f"Gemini failed to produce valid JSON after {max_retries} retries. Trying fallback model...")
|
87 |
+
fallback_response = self.fall_back_llm(prompt)
|
88 |
+
try:
|
89 |
+
cleaned_fallback = self.clean_json_response(fallback_response)
|
90 |
+
return json.loads(cleaned_fallback)
|
91 |
+
except json.JSONDecodeError:
|
92 |
+
return {"error": f"Both models failed to produce valid JSON"}
|
93 |
+
except Exception as e:
|
94 |
+
# For other exceptions, go directly to fallback
|
95 |
+
print(f"Gemini API Error: {str(e)}. Trying fallback model...")
|
96 |
+
fallback_response = self.fall_back_llm(prompt)
|
97 |
+
try:
|
98 |
+
cleaned_fallback = self.clean_json_response(fallback_response)
|
99 |
+
return json.loads(cleaned_fallback)
|
100 |
+
except json.JSONDecodeError:
|
101 |
+
return {"error": "Both models failed to produce valid JSON"}
|
102 |
+
return {"error": "Max retries reached"}
|
app/services/prompts.py
ADDED
@@ -0,0 +1,483 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
NON_WEB_DEFAULT_PROMPT = """
|
2 |
+
Previous Conversation Context:
|
3 |
+
{previous_history}
|
4 |
+
|
5 |
+
Note: If the current question is unrelated to the previous conversation, disregard the history and treat it as a new topic.
|
6 |
+
|
7 |
+
**Instruction:**
|
8 |
+
- If the user's current question is related to the previous conversation, maintain continuity.
|
9 |
+
- If the current question is unrelated, smoothly transition to the new topic while remaining friendly and conversational.
|
10 |
+
|
11 |
+
**Your Role:**
|
12 |
+
You are a **Friendly Doctor/Dermatologist** who is approachable, knowledgeable, and easy to talk to. You not only address medical questions but also answer other non-medical queries in a concise, clear, and friendly manner.
|
13 |
+
---
|
14 |
+
|
15 |
+
### Communication Style:
|
16 |
+
1. **Approachable & Warm:**
|
17 |
+
- Be kind, conversational, and supportive.
|
18 |
+
- Avoid overly formal or technical language unless the user explicitly asks for it.
|
19 |
+
|
20 |
+
2. **Direct & Clear:**
|
21 |
+
- Provide straightforward, accurate answers.
|
22 |
+
- Avoid long-winded or overly complicated explanations.
|
23 |
+
|
24 |
+
3. **Engaging & Adaptive:**
|
25 |
+
- Engage naturally with the user and adapt to their tone or style.
|
26 |
+
- Balance being informative with being personable.
|
27 |
+
|
28 |
+
---
|
29 |
+
|
30 |
+
### Key Principles for Response:
|
31 |
+
1. **Be Consistently Helpful:**
|
32 |
+
- Prioritize offering practical, actionable, and supportive advice.
|
33 |
+
- If a question is outside your scope as a dermatologist, still aim to help by providing brief and accurate responses.
|
34 |
+
|
35 |
+
2. **Provide Guidance Based on Medical Knowledge:**
|
36 |
+
- Share insights grounded in credible and up-to-date dermatological and medical understanding.
|
37 |
+
- Avoid speculation and stay within the scope of evidence-based medicine.
|
38 |
+
|
39 |
+
3. **Ensure Enjoyable Interaction:**
|
40 |
+
- Keep the tone friendly, relaxed, and enjoyable while staying professional.
|
41 |
+
- Avoid coming across as robotic or overly formal.
|
42 |
+
|
43 |
+
4. **Handle Non-Medical Topics:**
|
44 |
+
- If the user asks a non-medical question, address it concisely and directly without diverting too much.
|
45 |
+
|
46 |
+
---
|
47 |
+
|
48 |
+
### Your Primary Objectives in Each Response:
|
49 |
+
1. **Understand the Query Clearly:**
|
50 |
+
- Contextualize the current query against previous conversations.
|
51 |
+
- If the query is unclear, ask politely for clarification.
|
52 |
+
|
53 |
+
2. **Deliver a High-Quality Answer:**
|
54 |
+
- For medical topics, ensure the answer is medically accurate and understandable.
|
55 |
+
- For non-medical topics, provide a helpful and to-the-point response.
|
56 |
+
|
57 |
+
3. **Maintain a Friendly and Approachable Tone:**
|
58 |
+
- Balance being professional and conversational to make the user feel comfortable.
|
59 |
+
|
60 |
+
---
|
61 |
+
|
62 |
+
**Current User Query:**
|
63 |
+
{current_query}
|
64 |
+
"""
|
65 |
+
|
66 |
+
HISTORY_BASED_PROMPT = """
|
67 |
+
You are an expert in formulating queries based on history. You have no concern with the answer to the user's question; your focus is solely on the question asked by the user.
|
68 |
+
|
69 |
+
History: {history}
|
70 |
+
|
71 |
+
new_question_by_human: {query}
|
72 |
+
|
73 |
+
Given the conversation history and the new question, return only the concise question that should be understood by a web search engine. Ensure that the final question correctly reflects the context of the conversation, especially when a specific term (like things "disease", "person", "anything") is implied. Do not include any explanation or additional text, just return the final question and remember if the question is not related to history then just write the same question without changing anything even a single word.
|
74 |
+
|
75 |
+
Weather what language give you, you should strictly just response english
|
76 |
+
"""
|
77 |
+
|
78 |
+
DEFAULT_PROMPT = """
|
79 |
+
Previous Conversation Context:
|
80 |
+
{previous_history}
|
81 |
+
|
82 |
+
Note: If the current question is unrelated to the previous conversation, disregard the history and treat it as a new topic. Do not much focus on previous history response more focus on query of user.
|
83 |
+
|
84 |
+
You are a dermatology assistant. Your task is to answer the user's question based SOLELY on the following context. If the context doesn't contain enough information to fully answer the question, say so and provide only the information that is available in the context.
|
85 |
+
|
86 |
+
Context:
|
87 |
+
{context}
|
88 |
+
User Question: {current_query}
|
89 |
+
|
90 |
+
Answer the question using ONLY the information provided in the context above. Do not use any external knowledge or make assumptions. If the context doesn't provide enough information to answer fully, state that clearly.
|
91 |
+
|
92 |
+
and please do not mention the instruction which I am given you strictly.
|
93 |
+
"""
|
94 |
+
|
95 |
+
|
96 |
+
WEB_SEARCH_FILTER_PROMPT_VECTOR_DB = """
|
97 |
+
History: {history}
|
98 |
+
new_question_by_human: {query}
|
99 |
+
|
100 |
+
Return ONLY "yes" if the question requires factual information, current data, or specific knowledge that would benefit from web search. Return <NO_WEB_SEARCH_REQUIRED> if the question can be answered without external information.
|
101 |
+
|
102 |
+
CRITICAL RULES:
|
103 |
+
1. Output ONLY "yes" or <NO_WEB_SEARCH_REQUIRED> - nothing else
|
104 |
+
2. Return "yes" for questions that need:
|
105 |
+
- Factual information
|
106 |
+
- Current events/data
|
107 |
+
- Specific details
|
108 |
+
- Statistics
|
109 |
+
- Product information
|
110 |
+
- Research findings
|
111 |
+
- Historical facts
|
112 |
+
- Technical specifications
|
113 |
+
|
114 |
+
3. Return <NO_WEB_SEARCH_REQUIRED> for:
|
115 |
+
- Greetings ("hi", "how are you")
|
116 |
+
- Basic chat
|
117 |
+
- Opinion questions
|
118 |
+
- Hypothetical scenarios
|
119 |
+
- Simple calculations
|
120 |
+
- Basic coding help
|
121 |
+
- General advice
|
122 |
+
- Logic puzzles
|
123 |
+
- Questions about the assistant
|
124 |
+
|
125 |
+
4. If unclear, return <NO_WEB_SEARCH_REQUIRED>
|
126 |
+
|
127 |
+
5.If question is related to medical term then return"yes".
|
128 |
+
|
129 |
+
Most importantly that if any qustion ask before and its required web search then allow it web search then use "yes"
|
130 |
+
"""
|
131 |
+
|
132 |
+
PERSONALIZED_PROMPT = """
|
133 |
+
User who is talking to you name is {user_name} and age is {user_age}
|
134 |
+
|
135 |
+
Here is User {user_name} Details:
|
136 |
+
<user_details>
|
137 |
+
{user_details}
|
138 |
+
</user_details>
|
139 |
+
|
140 |
+
Previous Conversation Context of user {user_name}:
|
141 |
+
{previous_history}
|
142 |
+
|
143 |
+
Note: If the current question is unrelated to the previous conversation, disregard the history and treat it as a new topic.
|
144 |
+
|
145 |
+
You are a dermatology assistant. Your task is to answer the user's question based **solely** on the following context. If the context doesn't contain enough information to fully answer the question, state clearly that "I don't have enough information" and provide only the information that is available in the context.
|
146 |
+
|
147 |
+
Context:
|
148 |
+
{context}
|
149 |
+
|
150 |
+
Here is User {user_name} Question: {current_query}
|
151 |
+
|
152 |
+
Answer the question using **only** the information provided in the context above. Do not use any external knowledge or make assumptions. If the context doesn't provide enough information to answer fully, state clearly that "I don't have enough information".
|
153 |
+
|
154 |
+
After answering the question:
|
155 |
+
1. First determine if the question is EXPLICITLY about a medical/dermatological topic
|
156 |
+
- If NO: Provide only the direct answer
|
157 |
+
- If YES: Then check if ALL these conditions are met:
|
158 |
+
* The question requires personalized medical advice AND
|
159 |
+
* The provided context contains relevant medical information AND
|
160 |
+
* The user details directly impact the medical condition
|
161 |
+
* dont show that you are using context for answer just behave like an friendly dermatologist and sound like human dermatologist and give answer even context is have not so much information but have bit knowledge about the query but never said (The context provided does not specifically mention or The context mentions that) just say what context say simple.
|
162 |
+
|
163 |
+
2. Only if ALL above conditions are met in Step 1, include the following sections:
|
164 |
+
## Personal Recommendations
|
165 |
+
|
166 |
+
For all other cases, provide ONLY the direct answer with no additional sections or explanations about recommendations.
|
167 |
+
|
168 |
+
Response Structure:
|
169 |
+
## TITLE OF TOPIC
|
170 |
+
[Provide an answer strictly based on the provided context, and nothing else.]
|
171 |
+
|
172 |
+
[The following sections should ONLY be included if ALL medical conditions above are met:]
|
173 |
+
## Personal Recommendations
|
174 |
+
|
175 |
+
[At Last add this disclaimer]
|
176 |
+
*We acknowledge the possibility of errors, so it is always recommended to consult with a doctor for a thorough check-up.*
|
177 |
+
"""
|
178 |
+
|
179 |
+
ENVIRONMENTAL_PROMPT = """
|
180 |
+
User who is talking to you name is {user_name} and age is {user_age}
|
181 |
+
|
182 |
+
Environmental Condition of user {user_name} location
|
183 |
+
<environmental_info>
|
184 |
+
{environmental_condition}
|
185 |
+
</environmental_info>
|
186 |
+
|
187 |
+
Previous Conversation Context of user {user_name}:
|
188 |
+
{previous_history}
|
189 |
+
|
190 |
+
Note: If the current question is unrelated to the previous conversation, disregard the history and treat it as a new topic.
|
191 |
+
|
192 |
+
You are a dermatology assistant. Your task is to answer the user's question based **solely** on the following context. If the context doesn't contain enough information to fully answer the question, state clearly that "I don't have enough information" and provide only the information that is available in the context.
|
193 |
+
|
194 |
+
Context:
|
195 |
+
{context}
|
196 |
+
|
197 |
+
Here is User {user_name} Question: {current_query}
|
198 |
+
|
199 |
+
Answer the question using **only** the information provided in the context above. Do not use any external knowledge or make assumptions. If the context doesn't provide enough information to answer fully, state clearly that "I don't have enough information".
|
200 |
+
|
201 |
+
After answering the question, if and only if:
|
202 |
+
After answering the question:
|
203 |
+
1. First determine if the question is EXPLICITLY about a medical/dermatological topic
|
204 |
+
- If NO: Provide only the direct answer
|
205 |
+
- If YES: Then check if ALL these conditions are met:
|
206 |
+
* The question requires personalized medical advice AND
|
207 |
+
* The provided context contains relevant medical information AND
|
208 |
+
* The user details directly impact the medical condition
|
209 |
+
* dont show that you are using context for answer just behave like an friendly dermatologist and sound like human dermatologist and give answer even context is have not so much information but have bit knowledge about the query but never said (The context provided does not specifically mention or The context mentions that) just say what context say simple.
|
210 |
+
|
211 |
+
2. Only if ALL above conditions are met in Step 1, include the following sections:
|
212 |
+
## Environmental Considerations
|
213 |
+
|
214 |
+
For all other cases, provide ONLY the direct answer with no additional sections or explanations about recommendations.
|
215 |
+
|
216 |
+
Response Structure:
|
217 |
+
## TITLE OF TOPIC
|
218 |
+
[Provide an answer strictly based on the provided context, and nothing else.]
|
219 |
+
|
220 |
+
[The following sections should ONLY be included if ALL medical conditions above are met:]
|
221 |
+
## Environmental Considerations
|
222 |
+
|
223 |
+
[At Last add this disclaimer]
|
224 |
+
*We acknowledge the possibility of errors, so it is always recommended to consult with a doctor for a thorough check-up.*
|
225 |
+
"""
|
226 |
+
|
227 |
+
|
228 |
+
ENVIRONMENTAL_PERSONALIZED_PROMPT = """
|
229 |
+
User who is talking to you name is {user_name} and age is {user_age}
|
230 |
+
|
231 |
+
Here is User {user_name} Details:
|
232 |
+
<user_details>
|
233 |
+
{user_details}
|
234 |
+
</user_details>
|
235 |
+
|
236 |
+
Environmental Condition of user {user_name} location
|
237 |
+
<environmental_info>
|
238 |
+
{environmental_condition}
|
239 |
+
</environmental_info>
|
240 |
+
|
241 |
+
Previous Conversation Context of user {user_name}:
|
242 |
+
{previous_history}
|
243 |
+
|
244 |
+
Note: If the current question is unrelated to the previous conversation, disregard the history and treat it as a new topic.
|
245 |
+
|
246 |
+
You are a dermatology assistant. Your task is to answer the user's question based **solely** on the following context. If the context doesn't contain enough information to fully answer the question, state clearly that "I don't have enough information" and provide only the information that is available in the context.
|
247 |
+
|
248 |
+
Context:
|
249 |
+
{context}
|
250 |
+
|
251 |
+
Here is User {user_name} Question: {current_query}
|
252 |
+
|
253 |
+
Answer the question using **only** the information provided in the context above. Do not use any external knowledge or make assumptions. If the context doesn't provide enough information to answer fully, state clearly that "I don't have enough information".
|
254 |
+
|
255 |
+
After answering the question:
|
256 |
+
1. First determine if the question is EXPLICITLY about a medical/dermatological topic
|
257 |
+
- If NO: Provide only the direct answer
|
258 |
+
- If YES: Then check if ALL these conditions are met:
|
259 |
+
* The question requires personalized medical advice AND
|
260 |
+
* The provided context contains relevant medical information AND
|
261 |
+
* The user/environmental details directly impact the medical condition
|
262 |
+
* dont show that you are using context for answer just behave like an friendly dermatologist and sound like human dermatologist and give answer even context is have not so much information but have bit knowledge about the query but never said (The context provided does not specifically mention or The context mentions that) just say what context say simple.
|
263 |
+
|
264 |
+
2. Only if ALL above conditions are met in Step 1, include the following sections:
|
265 |
+
## Personal Recommendations
|
266 |
+
## Environmental Considerations
|
267 |
+
|
268 |
+
For all other cases, provide ONLY the direct answer with no additional sections or explanations about recommendations.
|
269 |
+
|
270 |
+
Response Structure:
|
271 |
+
## TITLE OF TOPIC
|
272 |
+
[Provide an answer strictly based on the provided context, and nothing else.]
|
273 |
+
|
274 |
+
[The following sections should ONLY be included if ALL medical conditions above are met:]
|
275 |
+
## Personal Recommendations
|
276 |
+
## Environmental Considerations
|
277 |
+
|
278 |
+
[At Last add this disclaimer]
|
279 |
+
*We acknowledge the possibility of errors, so it is always recommended to consult with a doctor for a thorough check-up.*
|
280 |
+
"""
|
281 |
+
|
282 |
+
MEDICAL_REPORT_ANALYSIS_PROMPT = """
|
283 |
+
You are an advanced medical report analysis system specializing in dermatology. Your purpose is to analyze and interpret the medical report for patient with the highest level of accuracy and clinical relevance.
|
284 |
+
|
285 |
+
CONTEXT AND CONSTRAINTS:
|
286 |
+
- Base your analysis EXCLUSIVELY on the provided medical report content
|
287 |
+
- Do not make assumptions or introduce external medical knowledge
|
288 |
+
- Maintain strict medical privacy and confidentiality standards
|
289 |
+
|
290 |
+
MEDICAL REPORT:
|
291 |
+
{report}
|
292 |
+
|
293 |
+
CURRENT QUERY:
|
294 |
+
{current_query}
|
295 |
+
|
296 |
+
ANALYSIS GUIDELINES:
|
297 |
+
1. Primary Findings
|
298 |
+
- Identify and explain key clinical observations
|
299 |
+
- Highlight any critical diagnostic information
|
300 |
+
- Note any abnormal results or concerning findings
|
301 |
+
|
302 |
+
2. Clinical Interpretation
|
303 |
+
- Analyze the findings in their clinical context
|
304 |
+
- Connect related symptoms and observations
|
305 |
+
- Identify any patterns or correlations in the data
|
306 |
+
|
307 |
+
3. Response Format:
|
308 |
+
- Start with a clear, direct answer to the query
|
309 |
+
- Support your response with specific evidence from the report
|
310 |
+
- Use medical terminology appropriately with plain language explanations
|
311 |
+
- Clearly separate facts from interpretations
|
312 |
+
- Structure information in a logical, easy-to-follow manner
|
313 |
+
|
314 |
+
4. Information Gaps:
|
315 |
+
- If any critical information is missing, clearly state: "The report does not contain sufficient information regarding [specific aspect]"
|
316 |
+
- Specify what additional information would be needed for a complete assessment
|
317 |
+
|
318 |
+
IMPORTANT NOTES:
|
319 |
+
- If the report contains laboratory values or measurements, include the relevant numbers and reference ranges
|
320 |
+
- For any medical terms used, provide brief explanations in parentheses
|
321 |
+
- If multiple interpretations are possible, list them in order of likelihood based on the report data
|
322 |
+
- Flag any urgent or critical findings that may require immediate attention
|
323 |
+
|
324 |
+
Please analyze the provided report and respond to the query while adhering to these guidelines. Maintain professional medical communication standards while ensuring clarity for the reader.
|
325 |
+
|
326 |
+
[At Last add this disclaimer]
|
327 |
+
*We acknowledge the possibility of errors, so it is always recommended to consult with a doctor for a thorough check-up.*
|
328 |
+
|
329 |
+
And If the report is not Related to Medical the just write
|
330 |
+
`Sorry Please upload Medical related Report`
|
331 |
+
"""
|
332 |
+
|
333 |
+
|
334 |
+
|
335 |
+
LANGUAGE_RESPONSE_PROMPT = """
|
336 |
+
STRICT LANGUAGE REQUIREMENTS:
|
337 |
+
1. Response must be written EXCLUSIVELY in {language} using its official script/orthography
|
338 |
+
2. English terms ONLY permitted when:
|
339 |
+
- There's no direct translation (technical terms/proper nouns)
|
340 |
+
- Retention is crucial for meaning preservation
|
341 |
+
3. STRICTLY PROHIBITED:
|
342 |
+
- Code-switching/mixing languages
|
343 |
+
- Transliterations of {language} words using Latin script
|
344 |
+
- Non-native punctuation/formatting
|
345 |
+
4. Ensure:
|
346 |
+
- Correct grammatical structure for {language}
|
347 |
+
- Proper script-specific punctuation
|
348 |
+
- Native character set compliance
|
349 |
+
5. Formatting must follow {language}'s typographical conventions
|
350 |
+
6. If unsure about translations: Use native {language} equivalents first
|
351 |
+
|
352 |
+
Respond ONLY in {language} script. Never include translations/explanations.
|
353 |
+
"""
|
354 |
+
|
355 |
+
|
356 |
+
SKIN_CARE_SCHEDULER = """As a skincare expert, generate a daily schedule based on:
|
357 |
+
- User's skin profile: {personalized_condition}
|
358 |
+
- Current environmental conditions: {environmental_values}
|
359 |
+
- Historical routines: {historical_data}
|
360 |
+
|
361 |
+
Create EXACTLY 5 entries in this JSON format:
|
362 |
+
[
|
363 |
+
{{
|
364 |
+
"time": "6:00 AM - 8:00 AM",
|
365 |
+
"recommendation": "Cleanse with [Product Name]",
|
366 |
+
"icon": "💧",
|
367 |
+
"category": "morning"
|
368 |
+
}},
|
369 |
+
{{
|
370 |
+
"time": "8:00 AM - 10:00 AM",
|
371 |
+
"recommendation": "Apply [Sunscreen Name] SPF 50",
|
372 |
+
"icon": "☀️",
|
373 |
+
"category": "morning"
|
374 |
+
}},
|
375 |
+
{{
|
376 |
+
"time": "12:00 PM - 2:00 PM",
|
377 |
+
"recommendation": "Reapply sunscreen",
|
378 |
+
"icon": "🌤️",
|
379 |
+
"category": "afternoon"
|
380 |
+
}},
|
381 |
+
{{
|
382 |
+
"time": "6:00 PM - 8:00 PM",
|
383 |
+
"recommendation": "Evening cleansing routine",
|
384 |
+
"icon": "🌙",
|
385 |
+
"category": "evening"
|
386 |
+
}},
|
387 |
+
{{
|
388 |
+
"time": "9:00 PM - 11:00 PM",
|
389 |
+
"recommendation": "Night serum application",
|
390 |
+
"icon": "✨",
|
391 |
+
"category": "night"
|
392 |
+
}}
|
393 |
+
]
|
394 |
+
|
395 |
+
Important rules:
|
396 |
+
1. Use only double quotes
|
397 |
+
2. Maintain category order: morning, morning, afternoon, evening, night
|
398 |
+
3. Include specific product names from historical data when available
|
399 |
+
4. Never add comments or text outside the JSON array
|
400 |
+
5. Time ranges must follow "HH:MM AM/PM - HH:MM AM/PM" format
|
401 |
+
6. Use appropriate emojis for each activity
|
402 |
+
"""
|
403 |
+
|
404 |
+
|
405 |
+
DEFAULT_SCHEDULE = [
|
406 |
+
{
|
407 |
+
"time": "6:00 AM - 8:00 AM",
|
408 |
+
"recommendation": "Cleanse with a gentle cleanser",
|
409 |
+
"icon": "💧",
|
410 |
+
"category": "Dummy"
|
411 |
+
},
|
412 |
+
{
|
413 |
+
"time": "8:00 AM - 10:00 AM",
|
414 |
+
"recommendation": "Apply sunscreen SPF 30+",
|
415 |
+
"icon": "☀️",
|
416 |
+
"category": "morning"
|
417 |
+
},
|
418 |
+
{
|
419 |
+
"time": "12:00 PM - 2:00 PM",
|
420 |
+
"recommendation": "Reapply sunscreen if needed",
|
421 |
+
"icon": "🌤️",
|
422 |
+
"category": "afternoon"
|
423 |
+
},
|
424 |
+
{
|
425 |
+
"time": "6:00 PM - 8:00 PM",
|
426 |
+
"recommendation": "Evening cleansing routine",
|
427 |
+
"icon": "🌙",
|
428 |
+
"category": "evening"
|
429 |
+
},
|
430 |
+
{
|
431 |
+
"time": "9:00 PM - 11:00 PM",
|
432 |
+
"recommendation": "Apply night cream or serum",
|
433 |
+
"icon": "✨",
|
434 |
+
"category": "night"
|
435 |
+
}
|
436 |
+
]
|
437 |
+
|
438 |
+
DISEASE_BASED_PROMPT = """You are an expert at generating web search queries based on a predicted disease and user question. Use this format:
|
439 |
+
|
440 |
+
Disease from image model: {disease_name}
|
441 |
+
User question: {query}
|
442 |
+
|
443 |
+
Generate a concise English search query that either:
|
444 |
+
1. Combines disease name with question context when relevant, OR
|
445 |
+
2. Returns original question unchanged if unrelated to disease
|
446 |
+
|
447 |
+
Output ONLY the final search query. Never explain. Maintain original language intent but output must be English.
|
448 |
+
|
449 |
+
Examples:
|
450 |
+
- "How to treat?" → "{disease_name} treatment"
|
451 |
+
- "Causes?" → "{disease_name} causes"
|
452 |
+
- "Unrelated question" → "Unrelated question"
|
453 |
+
"""
|
454 |
+
|
455 |
+
ADVICE_REPORT_SUGGESTION = """
|
456 |
+
## Based on your Image Analysis:
|
457 |
+
|
458 |
+
|
459 |
+
We have identified the presence of {diseases_name} with a confidence level of {diseases_detection_confidence}.
|
460 |
+
|
461 |
+
|
462 |
+
{response}
|
463 |
+
"""
|
464 |
+
|
465 |
+
URDU_ADVICE_REPORT_SUGGESTION = """
|
466 |
+
## آپ کی تصویر کے تجزیے کی بنیاد پر:
|
467 |
+
|
468 |
+
|
469 |
+
ہم نے {diseases_detection_confidence} کی اعتماد کی سطح کے ساتھ {diseases_name} کی موجودگی کی شناخت کی ہے۔
|
470 |
+
|
471 |
+
|
472 |
+
{response}
|
473 |
+
"""
|
474 |
+
|
475 |
+
SKIN_NON_SKIN_PROMPT = """
|
476 |
+
You are an expert at analyzing whether an image shows human skin or not.
|
477 |
+
Your task is to determine if the given image should be processed by a skin disease model.
|
478 |
+
Examine the image carefully and provide a clear two-word response:
|
479 |
+
answer <YES> if the image shows human skin, otherwise answer <NO>.
|
480 |
+
"""
|
481 |
+
|
482 |
+
|
483 |
+
|
app/services/report_process.py
ADDED
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import datetime, timezone
|
2 |
+
from typing import Optional, Dict, Any
|
3 |
+
from yake import KeywordExtractor
|
4 |
+
from app.services.chathistory import ChatSession
|
5 |
+
from app.services.llm_model import Model
|
6 |
+
from app.services.environmental_condition import EnvironmentalData
|
7 |
+
from app.services.prompts import *
|
8 |
+
from app.services.MagicConvert import MagicConvert
|
9 |
+
|
10 |
+
class Report:
|
11 |
+
def __init__(self, token: str, session_id: Optional[str] = None):
|
12 |
+
self.token = token
|
13 |
+
self.session_id = session_id
|
14 |
+
self.chat_session = ChatSession(token, session_id)
|
15 |
+
self.user_city = self.chat_session.get_city()
|
16 |
+
city = self.user_city if self.user_city else ''
|
17 |
+
self.environment_data = EnvironmentalData(city)
|
18 |
+
self.markitdown = MagicConvert()
|
19 |
+
|
20 |
+
def extract_keywords_yake(self, text: str, max_ngram_size: int = 2, num_keywords: int = 4) -> list:
|
21 |
+
kw_extractor = KeywordExtractor(
|
22 |
+
lan="en",
|
23 |
+
n=max_ngram_size,
|
24 |
+
top=num_keywords,
|
25 |
+
features=None
|
26 |
+
)
|
27 |
+
keywords = kw_extractor.extract_keywords(text)
|
28 |
+
return [kw[0] for kw in keywords]
|
29 |
+
|
30 |
+
def ensure_valid_session(self, title: str = None) -> str:
|
31 |
+
if not self.session_id or not self.session_id.strip():
|
32 |
+
self.chat_session.create_new_session(title=title)
|
33 |
+
self.session_id = self.chat_session.session_id
|
34 |
+
else:
|
35 |
+
try:
|
36 |
+
if not self.chat_session.validate_session(self.session_id, title=title):
|
37 |
+
self.chat_session.create_new_session(title=title)
|
38 |
+
self.session_id = self.chat_session.session_id
|
39 |
+
except ValueError:
|
40 |
+
self.chat_session.create_new_session(title=title)
|
41 |
+
self.session_id = self.chat_session.session_id
|
42 |
+
return self.session_id
|
43 |
+
|
44 |
+
def process_chat(self, query: str, report_file: str, file_type: Optional[str] = None) -> Dict[str, Any]:
|
45 |
+
try:
|
46 |
+
profile = self.chat_session.get_name_and_age()
|
47 |
+
self.chat_session.update_title(self.session_id, query)
|
48 |
+
self.session_id = self.ensure_valid_session(title=query)
|
49 |
+
language = self.chat_session.get_language().lower()
|
50 |
+
language_prompt = LANGUAGE_RESPONSE_PROMPT.format(language=language)
|
51 |
+
if not report_file or not file_type:
|
52 |
+
return {
|
53 |
+
"error": "Report file or file type missing",
|
54 |
+
"query": query,
|
55 |
+
"response": "Sorry, report file or file type is missing.",
|
56 |
+
"timestamp": datetime.now(timezone.utc).isoformat()
|
57 |
+
}
|
58 |
+
report_file_name = report_file + " (File Uploaded)"
|
59 |
+
conversion_result = self.markitdown.magic(report_file)
|
60 |
+
report_text = conversion_result.text_content
|
61 |
+
|
62 |
+
prompt = MEDICAL_REPORT_ANALYSIS_PROMPT.format(
|
63 |
+
report=report_text,
|
64 |
+
current_query=query
|
65 |
+
)
|
66 |
+
|
67 |
+
response = Model().response = Model().llm(prompt + "\n" + language_prompt , query)
|
68 |
+
keywords = self.extract_keywords_yake(response)
|
69 |
+
|
70 |
+
chat_data = {
|
71 |
+
"query": report_file_name + "\n" +query,
|
72 |
+
"response": response,
|
73 |
+
"references": "",
|
74 |
+
"page_no": "",
|
75 |
+
"keywords": keywords,
|
76 |
+
"images": "",
|
77 |
+
"context": report_text,
|
78 |
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
79 |
+
"session_id": self.chat_session.session_id
|
80 |
+
}
|
81 |
+
|
82 |
+
if not self.chat_session.save_chat(chat_data):
|
83 |
+
raise ValueError("Failed to save chat message")
|
84 |
+
|
85 |
+
return chat_data
|
86 |
+
|
87 |
+
except Exception as e:
|
88 |
+
return {
|
89 |
+
"error": str(e),
|
90 |
+
"query": query,
|
91 |
+
"response": "Sorry, there was an error processing your request.",
|
92 |
+
"timestamp": datetime.now(timezone.utc).isoformat()
|
93 |
+
}
|
app/services/skincare_scheduler.py
ADDED
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
import logging
|
3 |
+
|
4 |
+
from app.services.chathistory import ChatSession
|
5 |
+
from app.services.llm_model import Model
|
6 |
+
from app.services.environmental_condition import EnvironmentalData
|
7 |
+
from app.services.prompts import SKIN_CARE_SCHEDULER, DEFAULT_SCHEDULE
|
8 |
+
|
9 |
+
class SkinCareScheduler:
|
10 |
+
def __init__(self, token, session_id):
|
11 |
+
self.token = token
|
12 |
+
self.session_id = session_id
|
13 |
+
self.chat_session = ChatSession(token, session_id)
|
14 |
+
self.user_city = self.chat_session.get_city() or ''
|
15 |
+
self.environment_data = EnvironmentalData(self.user_city)
|
16 |
+
|
17 |
+
def get_historical_data(self):
|
18 |
+
"""Retrieve the last 7 days of schedules."""
|
19 |
+
schedules = self.chat_session.get_last_seven_days_schedules()
|
20 |
+
return [schedule["schedule_data"] for schedule in schedules]
|
21 |
+
|
22 |
+
def createTable(self):
|
23 |
+
"""Generate and return a daily skincare schedule."""
|
24 |
+
try:
|
25 |
+
# Check for an existing valid schedule
|
26 |
+
existing_schedule = self.chat_session.get_today_schedule()
|
27 |
+
if existing_schedule and isinstance(existing_schedule.get("schedule_data"), list):
|
28 |
+
return json.dumps(existing_schedule["schedule_data"], indent=2)
|
29 |
+
|
30 |
+
# Gather input data
|
31 |
+
historical_data = self.get_historical_data()
|
32 |
+
personalized_condition = self.chat_session.get_personalized_recommendation() or "No specific skin conditions provided"
|
33 |
+
environmental_data = self.environment_data.get_environmental_data()
|
34 |
+
|
35 |
+
# Format the prompt
|
36 |
+
formatted_prompt = SKIN_CARE_SCHEDULER.format(
|
37 |
+
personalized_condition=personalized_condition,
|
38 |
+
environmental_values=json.dumps(environmental_data, indent=2),
|
39 |
+
historical_data=json.dumps(historical_data, indent=2)
|
40 |
+
)
|
41 |
+
|
42 |
+
# Generate schedule with the model
|
43 |
+
model = Model()
|
44 |
+
result = model.skinScheduler(formatted_prompt)
|
45 |
+
|
46 |
+
# Handle errors by falling back to default schedule
|
47 |
+
if isinstance(result, dict) and "error" in result:
|
48 |
+
logging.error(f"Model error: {result['error']}")
|
49 |
+
result = DEFAULT_SCHEDULE
|
50 |
+
|
51 |
+
# Validate basic structure (optional, but ensures 5 entries)
|
52 |
+
if not isinstance(result, list) or len(result) != 5:
|
53 |
+
logging.warning("Generated schedule invalid; using default.")
|
54 |
+
result = DEFAULT_SCHEDULE
|
55 |
+
|
56 |
+
# Save and return the schedule
|
57 |
+
self.chat_session.save_schedule(result)
|
58 |
+
return json.dumps(result, indent=2)
|
59 |
+
|
60 |
+
except Exception as e:
|
61 |
+
logging.error(f"Schedule generation failed: {str(e)}")
|
62 |
+
return json.dumps(DEFAULT_SCHEDULE, indent=2)
|
app/services/vector_database_search.py
ADDED
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import uuid
|
3 |
+
from langchain_community.document_loaders import PyPDFLoader
|
4 |
+
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
5 |
+
from langchain_google_genai import GoogleGenerativeAIEmbeddings
|
6 |
+
from langchain_qdrant import Qdrant
|
7 |
+
from qdrant_client import QdrantClient, models
|
8 |
+
from dotenv import load_dotenv
|
9 |
+
|
10 |
+
load_dotenv()
|
11 |
+
|
12 |
+
os.environ["GOOGLE_API_KEY"] = os.getenv("GEMINI_API_KEY")
|
13 |
+
QDRANT_URL = os.getenv("QDRANT_URL")
|
14 |
+
QDRANT_API_KEY = os.getenv("QDRANT_API_KEY")
|
15 |
+
QDRANT_COLLECTION_NAME = os.getenv("QDRANT_COLLECTION_NAME")
|
16 |
+
|
17 |
+
class VectorDatabaseSearch:
|
18 |
+
def __init__(self, collection_name=QDRANT_COLLECTION_NAME):
|
19 |
+
self.collection_name = collection_name
|
20 |
+
self.embeddings = GoogleGenerativeAIEmbeddings(model="models/embedding-001")
|
21 |
+
self.client = QdrantClient(url=QDRANT_URL, api_key=QDRANT_API_KEY)
|
22 |
+
self._initialize_collection()
|
23 |
+
|
24 |
+
self.vectorstore = Qdrant(
|
25 |
+
client=self.client,
|
26 |
+
collection_name=collection_name,
|
27 |
+
embeddings=self.embeddings
|
28 |
+
)
|
29 |
+
|
30 |
+
def _initialize_collection(self):
|
31 |
+
"""Initialize Qdrant collection if it doesn't exist"""
|
32 |
+
try:
|
33 |
+
collections = self.client.get_collections()
|
34 |
+
if not any(c.name == self.collection_name for c in collections.collections):
|
35 |
+
self.client.create_collection(
|
36 |
+
collection_name=self.collection_name,
|
37 |
+
vectors_config=models.VectorParams(
|
38 |
+
size=768,
|
39 |
+
distance=models.Distance.COSINE
|
40 |
+
)
|
41 |
+
)
|
42 |
+
print(f"Created collection: {self.collection_name}")
|
43 |
+
except Exception as e:
|
44 |
+
print(f"Error initializing collection: {e}")
|
45 |
+
|
46 |
+
def add_pdf(self, pdf_path):
|
47 |
+
"""Add PDF to vector database"""
|
48 |
+
try:
|
49 |
+
loader = PyPDFLoader(pdf_path)
|
50 |
+
docs = loader.load()
|
51 |
+
splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
|
52 |
+
split_docs = splitter.split_documents(docs)
|
53 |
+
|
54 |
+
book_name = os.path.splitext(os.path.basename(pdf_path))[0]
|
55 |
+
for doc in split_docs:
|
56 |
+
doc.metadata = {
|
57 |
+
"source": book_name,
|
58 |
+
"page": doc.metadata.get('page', 1),
|
59 |
+
"id": str(uuid.uuid4())
|
60 |
+
}
|
61 |
+
|
62 |
+
self.vectorstore.add_documents(split_docs)
|
63 |
+
print(f"Added {len(split_docs)} chunks from {book_name}")
|
64 |
+
return True
|
65 |
+
except Exception as e:
|
66 |
+
print(f"Error adding PDF: {e}")
|
67 |
+
return False
|
68 |
+
|
69 |
+
def search(self, query, top_k=5):
|
70 |
+
"""Search documents based on query"""
|
71 |
+
try:
|
72 |
+
results = self.vectorstore.similarity_search_with_score(query, k=top_k)
|
73 |
+
|
74 |
+
formatted = []
|
75 |
+
for doc, score in results:
|
76 |
+
formatted.append({
|
77 |
+
"source": doc.metadata['source'],
|
78 |
+
"page": doc.metadata['page'],
|
79 |
+
"content": doc.page_content[:500],
|
80 |
+
"confidence": round(score * 100, 2)
|
81 |
+
})
|
82 |
+
return formatted
|
83 |
+
except Exception as e:
|
84 |
+
print(f"Search error: {e}")
|
85 |
+
return []
|
86 |
+
|
87 |
+
def get_book_info(self):
|
88 |
+
"""Retrieve list of unique book sources in the collection"""
|
89 |
+
try:
|
90 |
+
points = self.client.scroll(
|
91 |
+
collection_name=self.collection_name,
|
92 |
+
limit=1000,
|
93 |
+
with_payload=True
|
94 |
+
)[0]
|
95 |
+
|
96 |
+
books = set(point.payload.get('source', '') for point in points if point.payload)
|
97 |
+
return list(books)
|
98 |
+
except Exception as e:
|
99 |
+
print(f"Error retrieving book info: {e}")
|
100 |
+
return []
|
app/services/websearch.py
ADDED
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import re
|
2 |
+
import warnings
|
3 |
+
import requests
|
4 |
+
from bs4 import BeautifulSoup
|
5 |
+
import urllib.parse
|
6 |
+
import time
|
7 |
+
import random
|
8 |
+
|
9 |
+
warnings.simplefilter('ignore', requests.packages.urllib3.exceptions.InsecureRequestWarning)
|
10 |
+
|
11 |
+
class WebSearch:
|
12 |
+
def __init__(self, num_results=4, max_chars_per_page=6000 , max_images=10):
|
13 |
+
self.num_results = num_results
|
14 |
+
self.max_chars_per_page = max_chars_per_page
|
15 |
+
self.reference = []
|
16 |
+
self.results = []
|
17 |
+
self.max_images = max_images
|
18 |
+
self.headers = {
|
19 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
20 |
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
21 |
+
'Accept-Language': 'en-US,en;q=0.5',
|
22 |
+
'Accept-Encoding': 'gzip, deflate',
|
23 |
+
'DNT': '1',
|
24 |
+
'Connection': 'keep-alive',
|
25 |
+
}
|
26 |
+
|
27 |
+
def extract_text_from_webpage(self, html_content):
|
28 |
+
soup = BeautifulSoup(html_content, "html.parser")
|
29 |
+
for tag in soup(["script", "style", "header", "footer", "nav", "form", "svg"]):
|
30 |
+
tag.extract()
|
31 |
+
visible_text = soup.get_text(strip=True)
|
32 |
+
return visible_text
|
33 |
+
|
34 |
+
def search(self, query):
|
35 |
+
results = []
|
36 |
+
encoded_query = urllib.parse.quote(query)
|
37 |
+
url = f'https://html.duckduckgo.com/html/?q={encoded_query}'
|
38 |
+
|
39 |
+
try:
|
40 |
+
with requests.Session() as session:
|
41 |
+
session.headers.update(self.headers)
|
42 |
+
|
43 |
+
response = session.get(url, timeout=10)
|
44 |
+
soup = BeautifulSoup(response.text, 'html.parser')
|
45 |
+
search_results = soup.find_all('div', class_='result')[:self.num_results]
|
46 |
+
|
47 |
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
48 |
+
|
49 |
+
def fetch_page(link):
|
50 |
+
try:
|
51 |
+
time.sleep(random.uniform(0.2, 0.5))
|
52 |
+
page_response = session.get(link, timeout=5)
|
53 |
+
page_soup = BeautifulSoup(page_response.text, 'lxml')
|
54 |
+
|
55 |
+
[tag.decompose() for tag in page_soup(['script', 'style', 'header', 'footer', 'nav'])]
|
56 |
+
|
57 |
+
text = ' '.join(page_soup.stripped_strings)
|
58 |
+
return {
|
59 |
+
'link': link,
|
60 |
+
'text': text[:self.max_chars_per_page]
|
61 |
+
}
|
62 |
+
except Exception as e:
|
63 |
+
return None
|
64 |
+
|
65 |
+
links = [result.find('a', class_='result__a')['href']
|
66 |
+
for result in search_results
|
67 |
+
if result.find('a', class_='result__a')]
|
68 |
+
|
69 |
+
with ThreadPoolExecutor(max_workers=min(len(links), 4)) as executor:
|
70 |
+
future_to_url = {executor.submit(fetch_page, link): link for link in links}
|
71 |
+
|
72 |
+
for future in as_completed(future_to_url):
|
73 |
+
result = future.result()
|
74 |
+
if result:
|
75 |
+
results.append(result)
|
76 |
+
|
77 |
+
return results
|
78 |
+
|
79 |
+
except Exception as e:
|
80 |
+
return []
|
81 |
+
|
82 |
+
def search_images(self, query):
|
83 |
+
images = []
|
84 |
+
|
85 |
+
headers = {
|
86 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
87 |
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
88 |
+
'Accept-Language': 'en-US,en;q=0.5',
|
89 |
+
'Accept-Encoding': 'gzip, deflate',
|
90 |
+
'DNT': '1',
|
91 |
+
'Connection': 'keep-alive',
|
92 |
+
'Upgrade-Insecure-Requests': '1'
|
93 |
+
}
|
94 |
+
|
95 |
+
url = f"https://www.google.com/search?q={query}&tbm=isch&hl=en"
|
96 |
+
response = requests.get(url, headers=headers, verify=False)
|
97 |
+
|
98 |
+
soup = BeautifulSoup(response.text, 'html.parser')
|
99 |
+
|
100 |
+
for img in soup.find_all('img'):
|
101 |
+
src = img.get('src', '')
|
102 |
+
if src.startswith('http') and self.is_image_url(src):
|
103 |
+
images.append(src)
|
104 |
+
|
105 |
+
for script in soup.find_all('script'):
|
106 |
+
if script.string:
|
107 |
+
urls = re.findall(r'https?://[^\s<>"\']+?(?:\.(?:jpg|jpeg|png|gif|bmp|webp))', script.string)
|
108 |
+
for url in urls:
|
109 |
+
if self.is_image_url(url):
|
110 |
+
images.append(url)
|
111 |
+
|
112 |
+
alternative_url = f"https://www.google.com/search?q={query}&source=lnms&tbm=isch"
|
113 |
+
response = requests.get(alternative_url, headers=headers, verify=False)
|
114 |
+
soup = BeautifulSoup(response.text, 'html.parser')
|
115 |
+
|
116 |
+
for script in soup.find_all('script'):
|
117 |
+
if script.string and 'AF_initDataCallback' in script.string:
|
118 |
+
matches = re.findall(r'https?://[^\s<>"\']+?(?:\.(?:jpg|jpeg|png|gif|bmp|webp))', script.string)
|
119 |
+
for url in matches:
|
120 |
+
if self.is_image_url(url):
|
121 |
+
images.append(url)
|
122 |
+
|
123 |
+
images = [self.clean_url(url) for url in images]
|
124 |
+
|
125 |
+
seen = set()
|
126 |
+
images = [x for x in images if not (x in seen or seen.add(x))]
|
127 |
+
|
128 |
+
return images[:self.max_images]
|
129 |
+
|
130 |
+
def is_image_url(self, url):
|
131 |
+
image_extensions = ('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp')
|
132 |
+
return any(url.lower().endswith(ext) for ext in image_extensions)
|
133 |
+
|
134 |
+
def clean_url(self, url):
|
135 |
+
base_url = url.split('?')[0]
|
136 |
+
return base_url
|
137 |
+
|
138 |
+
|
139 |
+
|
140 |
+
|
141 |
+
|
app/services/wheel.py
ADDED
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from app.services.chathistory import ChatSession
|
2 |
+
from app.services.environmental_condition import EnvironmentalData
|
3 |
+
|
4 |
+
|
5 |
+
def map_air_quality_index(aqi):
|
6 |
+
if aqi <= 50:
|
7 |
+
return {"displayValue": "Good", "value": aqi, "color": "#00C853"}
|
8 |
+
elif aqi <= 100:
|
9 |
+
return {"displayValue": "Moderate", "value": aqi, "color": "#FFB74D"}
|
10 |
+
elif aqi <= 150:
|
11 |
+
return {"displayValue": "Unhealthy Tolerate", "value": aqi, "color": "#FF7043"}
|
12 |
+
elif aqi <= 200:
|
13 |
+
return {"displayValue": "Unhealthy", "value": aqi, "color": "#E53935"}
|
14 |
+
else:
|
15 |
+
return {"displayValue": "Very Unhealthy", "value": aqi, "color": "#8E24AA"}
|
16 |
+
|
17 |
+
|
18 |
+
def map_pollution_level(aqi):
|
19 |
+
if aqi <= 50:
|
20 |
+
return 20
|
21 |
+
elif aqi <= 100:
|
22 |
+
return 40
|
23 |
+
elif aqi <= 150:
|
24 |
+
return 60
|
25 |
+
elif aqi <= 200:
|
26 |
+
return 80
|
27 |
+
else:
|
28 |
+
return 100
|
29 |
+
|
30 |
+
class CityNotProvidedError(Exception):
|
31 |
+
pass
|
32 |
+
|
33 |
+
|
34 |
+
class EnvironmentalConditions:
|
35 |
+
def __init__(self, session_id):
|
36 |
+
self.session_id = session_id
|
37 |
+
self.chat_session = ChatSession(session_id, "session_id")
|
38 |
+
self.user_city = self.chat_session.get_city()
|
39 |
+
|
40 |
+
if not self.user_city:
|
41 |
+
raise CityNotProvidedError("City information is required but not provided")
|
42 |
+
|
43 |
+
self.city = self.user_city
|
44 |
+
self.environment_data = EnvironmentalData(self.city)
|
45 |
+
|
46 |
+
def get_conditon(self):
|
47 |
+
data = self.environment_data.get_environmental_data()
|
48 |
+
|
49 |
+
formatted_data = [
|
50 |
+
{
|
51 |
+
"label": "Humidity",
|
52 |
+
# Handle decimal values by converting to float first
|
53 |
+
"value": int(float(data['Humidity'].strip(' %'))),
|
54 |
+
"color": "#4FC3F7",
|
55 |
+
"icon": "FaTint",
|
56 |
+
"type": "numeric"
|
57 |
+
},
|
58 |
+
{
|
59 |
+
"label": "UV Rays",
|
60 |
+
"value": data['UV_Index'] * 10,
|
61 |
+
"color": "#FFB74D",
|
62 |
+
"icon": "FaSun",
|
63 |
+
"type": "numeric"
|
64 |
+
},
|
65 |
+
{
|
66 |
+
"label": "Pollution",
|
67 |
+
"value": map_pollution_level(data['Air Quality Index']),
|
68 |
+
"color": "#F06292",
|
69 |
+
"icon": "FaLeaf",
|
70 |
+
"type": "numeric"
|
71 |
+
},
|
72 |
+
{
|
73 |
+
"label": "Air Quality",
|
74 |
+
**map_air_quality_index(data['Air Quality Index']),
|
75 |
+
"icon": "FaCloud",
|
76 |
+
"type": "categorical"
|
77 |
+
},
|
78 |
+
{
|
79 |
+
"label": "Wind",
|
80 |
+
"value": float(data['Wind Speed'].strip(' m/s')) * 10,
|
81 |
+
"color": "#9575CD",
|
82 |
+
"icon": "FaWind",
|
83 |
+
"type": "numeric"
|
84 |
+
},
|
85 |
+
{
|
86 |
+
"label": "Temperature",
|
87 |
+
"value": int(float(data['Temperature'].strip(' °C'))),
|
88 |
+
"color": "#FF7043",
|
89 |
+
"icon": "FaThermometerHalf",
|
90 |
+
"type": "numeric"
|
91 |
+
}
|
92 |
+
]
|
93 |
+
|
94 |
+
return formatted_data
|
docker-compose.yml
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
version: '3'
|
2 |
+
|
3 |
+
services:
|
4 |
+
api:
|
5 |
+
build: .
|
6 |
+
ports:
|
7 |
+
- "5000:5000"
|
8 |
+
volumes:
|
9 |
+
- ./temp:/app/temp
|
10 |
+
- ./uploads:/app/uploads
|
11 |
+
env_file:
|
12 |
+
- .env
|
13 |
+
restart: unless-stopped
|
pyproject.toml
ADDED
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[project]
|
2 |
+
name = "derm_ai"
|
3 |
+
version = "0.1.0"
|
4 |
+
description = "This is derm_ai backend"
|
5 |
+
authors = [
|
6 |
+
{ name = "Muhammad Noman", email = "muhammadnoman76@gmail.com" }
|
7 |
+
]
|
8 |
+
dependencies = [
|
9 |
+
"beautifulsoup4==4.13.4",
|
10 |
+
"fastapi==0.115.12",
|
11 |
+
"google-genai==1.13.0",
|
12 |
+
"huggingface_hub==0.30.2",
|
13 |
+
"langchain_community==0.3.23",
|
14 |
+
"langchain_google_genai==2.1.4",
|
15 |
+
"langchain_qdrant==0.2.0",
|
16 |
+
"langchain_text_splitters==0.3.8",
|
17 |
+
"nltk==3.9.1",
|
18 |
+
"numpy==2.2.4",
|
19 |
+
"pillow==11.2.1",
|
20 |
+
"pydantic[email]==2.11.3",
|
21 |
+
"pymongo==4.12.1",
|
22 |
+
"pypdf==5.4.0",
|
23 |
+
"PyJWT==1.7.1",
|
24 |
+
"python-dotenv==1.1.0",
|
25 |
+
"qdrant_client==1.14.2",
|
26 |
+
"requests==2.32.3",
|
27 |
+
"scikit-learn==1.6.1",
|
28 |
+
"sendgrid==6.11.0",
|
29 |
+
"torch==2.5.1",
|
30 |
+
"torchvision==0.20.1",
|
31 |
+
"transformers==4.51.3",
|
32 |
+
"werkzeug==3.1.3",
|
33 |
+
"yake==0.4.8",
|
34 |
+
"uvicorn==0.34.1",
|
35 |
+
"python-multipart==0.0.20",
|
36 |
+
"g4f==0.5.2.1",
|
37 |
+
"mammoth==1.9.0",
|
38 |
+
"markdownify==1.1.0",
|
39 |
+
"pandas==2.2.3",
|
40 |
+
"pdfminer.six==20250416",
|
41 |
+
"python-pptx==1.0.2",
|
42 |
+
"puremagic==1.28",
|
43 |
+
"charset-normalizer==3.4.1",
|
44 |
+
"pytesseract==0.3.13"
|
45 |
+
]
|
46 |
+
|
47 |
+
[build-system]
|
48 |
+
requires = ["setuptools>=61.0"]
|
49 |
+
build-backend = "setuptools.build_meta"
|