muhammadnoman76 commited on
Commit
75e2b6c
·
1 Parent(s): ba09305
.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: Derm Ai
3
- emoji: 🌍
4
- colorFrom: blue
5
- colorTo: green
6
  sdk: docker
 
 
7
  pinned: false
8
- license: afl-3.0
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 "![%s](%s%s)" % (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!["
254
+ + (alt_text if alt_text else shape.name)
255
+ + "]("
256
+ + filename
257
+ + ")\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"