muhammadnoman76 commited on
Commit
b7dfc73
·
1 Parent(s): 4209845
app/routers/agent_chat.py CHANGED
@@ -24,10 +24,19 @@ class AgentChatDocument(BaseModel):
24
  extension: Optional[str] = None
25
 
26
 
 
 
 
 
 
 
 
 
27
  class AgentChatRequest(BaseModel):
28
  session_id: Optional[str] = None
29
  query: str
30
  document: Optional[AgentChatDocument] = None
 
31
 
32
 
33
  async def stream_agent_response(agent_service: GoogleAgentService, query: str):
@@ -89,6 +98,7 @@ async def agent_chat(
89
  token=token,
90
  session_id=request.session_id,
91
  document=request.document.dict() if request.document else None,
 
92
  )
93
  except Exception as exc:
94
  logger.error("Failed to initialise agent service: %s", exc, exc_info=True)
 
24
  extension: Optional[str] = None
25
 
26
 
27
+ class AgentChatImage(BaseModel):
28
+ path: str
29
+ name: Optional[str] = None
30
+ type: Optional[str] = None
31
+ extension: Optional[str] = None
32
+ prompt: Optional[str] = None
33
+
34
+
35
  class AgentChatRequest(BaseModel):
36
  session_id: Optional[str] = None
37
  query: str
38
  document: Optional[AgentChatDocument] = None
39
+ image: Optional[AgentChatImage] = None
40
 
41
 
42
  async def stream_agent_response(agent_service: GoogleAgentService, query: str):
 
98
  token=token,
99
  session_id=request.session_id,
100
  document=request.document.dict() if request.document else None,
101
+ image=request.image.dict() if request.image else None,
102
  )
103
  except Exception as exc:
104
  logger.error("Failed to initialise agent service: %s", exc, exc_info=True)
app/routers/chat.py CHANGED
@@ -12,7 +12,6 @@ from pydantic import BaseModel
12
  from app.database.database_query import DatabaseQuery
13
  from app.middleware.auth import get_current_user, get_optional_user
14
  from app.services import ChatProcessor
15
- from app.services.image_processor import ImageProcessor
16
  from app.services.skincare_scheduler import SkinCareScheduler
17
  from app.services.wheel import EnvironmentalConditions
18
  from app.services.RAG_evaluation import RAGEvaluation
@@ -339,28 +338,93 @@ async def get_skin_care_wheel(
339
  )
340
 
341
  @router.post('/image_disease_search')
342
- async def disease_search(
 
343
  session_id: str = Form(...),
344
- query: str = Form(...),
345
  num_results: int = Form(3),
346
  num_images: int = Form(3),
347
- image: UploadFile = File(...),
348
  authorization: str = Header(...),
349
  username: str = Depends(get_current_user)
350
  ):
351
  try:
352
- token = authorization.split(" ")[1]
353
- image_processor = ImageProcessor(
354
- token=token,
355
- session_id=session_id,
356
- num_results=num_results,
357
- num_images=num_images,
358
- image=image
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
  )
360
- response = image_processor.web_search(query=query)
361
- return {"response": response}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
  except Exception as e:
363
- raise HTTPException(status_code=500, detail=str(e))
 
 
 
 
 
 
 
 
364
 
365
  @router.post('/get_rag_evaluation')
366
  async def rag_evaluation(
 
12
  from app.database.database_query import DatabaseQuery
13
  from app.middleware.auth import get_current_user, get_optional_user
14
  from app.services import ChatProcessor
 
15
  from app.services.skincare_scheduler import SkinCareScheduler
16
  from app.services.wheel import EnvironmentalConditions
17
  from app.services.RAG_evaluation import RAGEvaluation
 
338
  )
339
 
340
  @router.post('/image_disease_search')
341
+ async def upload_skin_image(
342
+ image: UploadFile = File(...),
343
  session_id: str = Form(...),
344
+ query: str = Form(""),
345
  num_results: int = Form(3),
346
  num_images: int = Form(3),
 
347
  authorization: str = Header(...),
348
  username: str = Depends(get_current_user)
349
  ):
350
  try:
351
+ _ = authorization.split(" ")[1]
352
+
353
+ if not image.filename:
354
+ return JSONResponse(
355
+ status_code=400,
356
+ content={"status": "error", "error": "Empty image file provided"},
357
+ )
358
+
359
+ allowed_extensions = {
360
+ "jpg",
361
+ "jpeg",
362
+ "png",
363
+ "bmp",
364
+ "webp",
365
+ "avif",
366
+ "avifs",
367
+ "heic",
368
+ "heif",
369
+ }
370
+ file_extension = (
371
+ image.filename.rsplit('.', 1)[1].lower() if '.' in image.filename else ''
372
  )
373
+
374
+ if file_extension not in allowed_extensions:
375
+ return JSONResponse(
376
+ status_code=400,
377
+ content={
378
+ "status": "error",
379
+ "error": (
380
+ "Unsupported image type. Allowed types: "
381
+ f"{', '.join(sorted(allowed_extensions))}"
382
+ ),
383
+ },
384
+ )
385
+
386
+ default_upload_root = os.path.abspath(
387
+ os.path.join(os.path.dirname(__file__), "..", "..", "uploads")
388
+ )
389
+ uploads_root = os.getenv('DERMAI_UPLOAD_DIR', default_upload_root)
390
+ session_upload_dir = os.path.join(uploads_root, session_id)
391
+ os.makedirs(session_upload_dir, exist_ok=True)
392
+
393
+ timestamp = datetime.utcnow().strftime('%Y%m%d%H%M%S%f')
394
+ sanitized_name = image.filename.replace(' ', '_')
395
+ stored_filename = f"{timestamp}_{sanitized_name}"
396
+ stored_path = os.path.join(session_upload_dir, stored_filename)
397
+
398
+ content = await image.read()
399
+ with open(stored_path, 'wb') as f:
400
+ f.write(content)
401
+
402
+ absolute_path = os.path.abspath(stored_path)
403
+ relative_root = os.path.abspath(uploads_root)
404
+ relative_path = os.path.relpath(absolute_path, relative_root)
405
+
406
+ return {
407
+ "status": "success",
408
+ "message": "Image uploaded successfully",
409
+ "file": {
410
+ "path": relative_path.replace('\\', '/'),
411
+ "name": image.filename,
412
+ "content_type": image.content_type,
413
+ "size": len(content),
414
+ "extension": file_extension,
415
+ "original_query": query,
416
+ },
417
+ }
418
  except Exception as e:
419
+ logging.error(f"Error in upload_skin_image: {str(e)}")
420
+ raise HTTPException(
421
+ status_code=500,
422
+ detail={
423
+ "status": "error",
424
+ "error": "Internal server error",
425
+ "details": str(e),
426
+ },
427
+ )
428
 
429
  @router.post('/get_rag_evaluation')
430
  async def rag_evaluation(
app/services/__init__.py CHANGED
@@ -1,5 +1,4 @@
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
@@ -14,7 +13,6 @@ from app.services.wheel import EnvironmentalConditions
14
  from app.services.MagicConvert import MagicConvert
15
 
16
  __all__ = [
17
- "ImageProcessor",
18
  "AISkinDetector",
19
  "SkinDiseaseClassifier",
20
  "Model",
 
1
  # app/services/__init__.py
 
2
  from app.services.image_classification_vit import SkinDiseaseClassifier
3
  from app.services.llm_model import Model
4
  from app.services.chat_processor import ChatProcessor
 
13
  from app.services.MagicConvert import MagicConvert
14
 
15
  __all__ = [
 
16
  "AISkinDetector",
17
  "SkinDiseaseClassifier",
18
  "Model",
app/services/agentic_prompt.py CHANGED
@@ -10,7 +10,7 @@ def _append_personalization(prompt: str, user_data: Dict) -> str:
10
  prompt += (
11
  "\n\n## Personalized Data Access:\n"
12
  f"Call `{personalized_tool}` exactly once to retrieve the patient's questionnaire-driven context after you finish using the search tools and before writing the final JSON response. "
13
- "Use only the returned facts when crafting the `## Personalization Recommendation` section. Do not invent details beyond the tool output."
14
  )
15
  else:
16
  prompt += (
@@ -22,7 +22,7 @@ def _append_personalization(prompt: str, user_data: Dict) -> str:
22
  prompt += (
23
  "\n\n## Environmental Data Access:\n"
24
  f"Call `{environmental_tool}` exactly once when environmental guidance is relevant, ideally after the core search tools and personalization step. "
25
- "Interpret the returned metrics responsibly before presenting them in the `## Environmental Condition` section."
26
  )
27
  else:
28
  prompt += (
@@ -53,6 +53,23 @@ def _append_document_guidance(prompt: str, user_data: Dict) -> str:
53
  return prompt
54
 
55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  def _format_json_guidance(user_data: Dict) -> str:
57
  references_instruction = (
58
  "Populate the `references` array with source links mapped from your tool calls."
@@ -85,6 +102,10 @@ def _format_json_guidance(user_data: Dict) -> str:
85
  if has_environmental
86
  else "Omit the `## Environmental Condition` section because environmental data is unavailable."
87
  )
 
 
 
 
88
  safety_instruction = (
89
  "If the question is outside dermatology/medical scope or evidence is insufficient, respond with a safe refusal inside the JSON instead of guessing."
90
  )
@@ -101,6 +122,7 @@ def _format_json_guidance(user_data: Dict) -> str:
101
  f"{images_instruction}\n"
102
  f"{personalization_instruction}\n"
103
  f"{environmental_instruction}\n"
 
104
  f"{safety_instruction}\n"
105
  "Return only JSON (no prose outside the JSON object).\n"
106
  "Do NOT hallucinate or invent facts.\n"
@@ -108,22 +130,52 @@ def _format_json_guidance(user_data: Dict) -> str:
108
 
109
 
110
  def get_web_search_prompt(user_data: Dict) -> str:
111
- prompt = """
112
- You are Dr. DermAI, an evidence-based dermatology consultant.
113
-
114
- PRIMARY DIRECTIVES:
115
- 1. ALWAYS call the `get_web_search` tool first to gather the latest medical knowledge.
116
- 2. After reviewing text sources, call `get_image_search` if images are permitted to enrich the answer.
117
- 3. Base every statement on retrieved sources; do not rely solely on prior training.
118
- 4. Cite the supporting evidence inline using [1], [2], etc.
119
- 5. Answer ONLY dermatology or medically relevant queries. Politely refuse others inside the JSON response.
120
- 6. If the retrieved evidence is insufficient, acknowledge the limitation rather than speculating.
121
-
122
- TOOL CALL ORDER:
123
- - Step 1: get_web_search(query=<user question>)
124
- - Step 2: get_image_search(query=<key medical term>) when images are allowed
125
- - Step 3: Synthesize findings into the structured JSON response.
126
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  recent_history = user_data.get('recent_history')
128
  if recent_history:
129
  prompt += (
@@ -132,28 +184,59 @@ TOOL CALL ORDER:
132
  "Use this context to maintain continuity before answering the new query."
133
  )
134
  prompt = _append_document_guidance(prompt, user_data)
 
135
  prompt += _format_json_guidance(user_data)
136
  prompt = _append_personalization(prompt, user_data)
137
  return prompt.strip()
138
 
139
 
140
  def get_vector_search_prompt(user_data: Dict) -> str:
141
- prompt = """
142
- You are Dr. DermAI, a dermatologist with access to a curated clinical knowledge base.
143
-
144
- PRIMARY DIRECTIVES:
145
- 1. ALWAYS call the `get_vector_search` tool first to retrieve authoritative dermatology passages.
146
- 2. After reviewing text sources, call `get_image_search` if images are permitted.
147
- 3. Ground every recommendation in the retrieved evidence and cite it inline using [1], [2], etc.
148
- 4. If the knowledge base lacks coverage, state this explicitly and provide a best-effort safety answer.
149
- 5. Answer ONLY dermatology or medically relevant queries. Politely refuse others inside the JSON response.
150
- 6. If evidence is insufficient, acknowledge the limitation instead of speculating.
151
-
152
- TOOL CALL ORDER:
153
- - Step 1: get_vector_search(query=<user question>)
154
- - Step 2: get_image_search(query=<key medical term>) when images are allowed
155
- - Step 3: Synthesize findings into the structured JSON response.
156
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  recent_history = user_data.get('recent_history')
158
  if recent_history:
159
  prompt += (
@@ -162,6 +245,7 @@ TOOL CALL ORDER:
162
  "Use this context to maintain continuity before answering the new query."
163
  )
164
  prompt = _append_document_guidance(prompt, user_data)
 
165
  prompt += _format_json_guidance(user_data)
166
  prompt = _append_personalization(prompt, user_data)
167
  return prompt.strip()
 
10
  prompt += (
11
  "\n\n## Personalized Data Access:\n"
12
  f"Call `{personalized_tool}` exactly once to retrieve the patient's questionnaire-driven context after you finish using the search tools and before writing the final JSON response. "
13
+ "Summarize only the details that materially influence your assessment; avoid dumping the questionnaire verbatim."
14
  )
15
  else:
16
  prompt += (
 
22
  prompt += (
23
  "\n\n## Environmental Data Access:\n"
24
  f"Call `{environmental_tool}` exactly once when environmental guidance is relevant, ideally after the core search tools and personalization step. "
25
+ "Reference these metrics only if they concretely affect the user's condition; explain the connection instead of listing raw numbers."
26
  )
27
  else:
28
  prompt += (
 
53
  return prompt
54
 
55
 
56
+ def _append_image_guidance(prompt: str, user_data: Dict) -> str:
57
+ image_info = user_data.get('image_info') or {}
58
+ tool_name = user_data.get('image_tool_name')
59
+ image_path = image_info.get('path')
60
+
61
+ if image_info and tool_name and image_path:
62
+ image_name = image_info.get('name') or 'Uploaded image'
63
+ prompt += (
64
+ "\n\n## Uploaded Image\n"
65
+ f"The user shared `{image_name}` located at `{image_path}`. "
66
+ f"Call `{tool_name}` exactly once before drafting the final JSON response to analyse the skin photo. "
67
+ "If the tool reports low confidence or that the picture is not skin, clearly explain the outcome and suggest the appropriate next steps."
68
+ )
69
+
70
+ return prompt
71
+
72
+
73
  def _format_json_guidance(user_data: Dict) -> str:
74
  references_instruction = (
75
  "Populate the `references` array with source links mapped from your tool calls."
 
102
  if has_environmental
103
  else "Omit the `## Environmental Condition` section because environmental data is unavailable."
104
  )
105
+ image_instruction = (
106
+ "If the skin-image analysis tool succeeds, integrate its findings into `## Response from References`, cite it explicitly (e.g., [image]) and describe clinical caution."
107
+ " If it reports an error or low confidence, explain the limitation and advise on next best steps instead of guessing."
108
+ )
109
  safety_instruction = (
110
  "If the question is outside dermatology/medical scope or evidence is insufficient, respond with a safe refusal inside the JSON instead of guessing."
111
  )
 
122
  f"{images_instruction}\n"
123
  f"{personalization_instruction}\n"
124
  f"{environmental_instruction}\n"
125
+ f"{image_instruction}\n"
126
  f"{safety_instruction}\n"
127
  "Return only JSON (no prose outside the JSON object).\n"
128
  "Do NOT hallucinate or invent facts.\n"
 
130
 
131
 
132
  def get_web_search_prompt(user_data: Dict) -> str:
133
+ prompt_lines = [
134
+ "You are Dr. DermAI, an evidence-based dermatology consultant.",
135
+ "",
136
+ "PRIMARY DIRECTIVES:",
137
+ ]
138
+
139
+ if user_data.get('image_info') and user_data.get('image_tool_name'):
140
+ prompt_lines.append(
141
+ "0. If an uploaded skin image is provided, call the `analyze_skin_image` tool BEFORE any other tool."
142
+ )
143
+ prompt_lines.append(
144
+ " Summarize its findings (or explain any errors) in your final response, citing it as [image]."
145
+ )
146
+
147
+ prompt_lines.extend(
148
+ [
149
+ "1. ALWAYS call the `get_web_search` tool next to gather the latest medical knowledge.",
150
+ "2. After reviewing text sources, call `get_image_search` if fresh comparison imagery would help.",
151
+ "3. Base every statement on retrieved sources or tool outputs; do not rely solely on prior training.",
152
+ "4. Cite the supporting evidence inline using [1], [2], etc.",
153
+ "5. Answer ONLY dermatology or medically relevant queries. Politely refuse others inside the JSON response.",
154
+ "6. If evidence is insufficient, state the limitation rather than speculating.",
155
+ ]
156
+ )
157
+
158
+ prompt_lines.extend(
159
+ [
160
+ "",
161
+ "TOOL CALL ORDER:",
162
+ ]
163
+ )
164
+
165
+ if user_data.get('image_info') and user_data.get('image_tool_name'):
166
+ prompt_lines.append(
167
+ f"- Step 0: {user_data.get('image_tool_name')}(file_path=<uploaded image path>)"
168
+ )
169
+
170
+ prompt_lines.extend(
171
+ [
172
+ "- Step 1: get_web_search(query=<user question>)",
173
+ "- Step 2: get_image_search(query=<key medical term>) when live imagery adds value",
174
+ "- Step 3: Synthesize findings into the structured JSON response.",
175
+ ]
176
+ )
177
+
178
+ prompt = "\n".join(prompt_lines)
179
  recent_history = user_data.get('recent_history')
180
  if recent_history:
181
  prompt += (
 
184
  "Use this context to maintain continuity before answering the new query."
185
  )
186
  prompt = _append_document_guidance(prompt, user_data)
187
+ prompt = _append_image_guidance(prompt, user_data)
188
  prompt += _format_json_guidance(user_data)
189
  prompt = _append_personalization(prompt, user_data)
190
  return prompt.strip()
191
 
192
 
193
  def get_vector_search_prompt(user_data: Dict) -> str:
194
+ prompt_lines = [
195
+ "You are Dr. DermAI, a dermatologist with access to a curated clinical knowledge base.",
196
+ "",
197
+ "PRIMARY DIRECTIVES:",
198
+ ]
199
+
200
+ if user_data.get('image_info') and user_data.get('image_tool_name'):
201
+ prompt_lines.append(
202
+ "0. If an uploaded skin image is provided, call the `analyze_skin_image` tool BEFORE any database queries."
203
+ )
204
+ prompt_lines.append(
205
+ " Summarize its findings (or any failure) using [image] in the final JSON response."
206
+ )
207
+
208
+ prompt_lines.extend(
209
+ [
210
+ "1. ALWAYS call the `get_vector_search` tool next to retrieve authoritative dermatology passages.",
211
+ "2. After reviewing text sources, call `get_image_search` if comparison imagery supports your reasoning.",
212
+ "3. Ground every recommendation in tool-derived evidence and cite it inline using [1], [2], etc.",
213
+ "4. If the knowledge base lacks coverage, state this explicitly and provide the safest guidance you can.",
214
+ "5. Answer ONLY dermatology or medically relevant queries. Politely refuse others inside the JSON response.",
215
+ "6. If evidence is insufficient, acknowledge the limitation instead of speculating.",
216
+ ]
217
+ )
218
+
219
+ prompt_lines.extend(
220
+ [
221
+ "",
222
+ "TOOL CALL ORDER:",
223
+ ]
224
+ )
225
+
226
+ if user_data.get('image_info') and user_data.get('image_tool_name'):
227
+ prompt_lines.append(
228
+ f"- Step 0: {user_data.get('image_tool_name')}(file_path=<uploaded image path>)"
229
+ )
230
+
231
+ prompt_lines.extend(
232
+ [
233
+ "- Step 1: get_vector_search(query=<user question>)",
234
+ "- Step 2: get_image_search(query=<key medical term>) when imagery helps",
235
+ "- Step 3: Synthesize findings into the structured JSON response.",
236
+ ]
237
+ )
238
+
239
+ prompt = "\n".join(prompt_lines)
240
  recent_history = user_data.get('recent_history')
241
  if recent_history:
242
  prompt += (
 
245
  "Use this context to maintain continuity before answering the new query."
246
  )
247
  prompt = _append_document_guidance(prompt, user_data)
248
+ prompt = _append_image_guidance(prompt, user_data)
249
  prompt += _format_json_guidance(user_data)
250
  prompt = _append_personalization(prompt, user_data)
251
  return prompt.strip()
app/services/google_agent_service.py CHANGED
@@ -19,6 +19,7 @@ from app.services.agentic_prompt import (
19
  from app.services.chathistory import ChatSession
20
  from app.services.environmental_condition import EnvironmentalData
21
  from app.services.tools import (
 
22
  convert_document_to_markdown,
23
  get_image_search,
24
  get_vector_search,
@@ -33,6 +34,7 @@ DEFAULT_MODEL_NAME = os.getenv("GEMINI_MODEL", "gemini-2.0-flash-exp")
33
  PERSONALIZED_TOOL_NAME = "get_personalized_context"
34
  ENVIRONMENT_TOOL_NAME = "get_environmental_context"
35
  DOCUMENT_CONVERSION_TOOL_NAME = "convert_uploaded_document"
 
36
 
37
  if not os.getenv("GOOGLE_API_KEY") and GOOGLE_API_KEY:
38
  os.environ["GOOGLE_API_KEY"] = GOOGLE_API_KEY
@@ -52,6 +54,7 @@ class GoogleAgentService:
52
  token: str,
53
  session_id: Optional[str] = None,
54
  document: Optional[Dict[str, Any]] = None,
 
55
  ) -> None:
56
  self.token = token
57
  self.session_id = session_id
@@ -63,6 +66,7 @@ class GoogleAgentService:
63
  self.user_city = self.chat_session.get_city()
64
  self.environment_data = self._load_environmental_data()
65
  self.document = document
 
66
 
67
  async def process_message_async(
68
  self, query: str
@@ -196,9 +200,12 @@ class GoogleAgentService:
196
  merged_images = self._dedupe_list(collected_images + response_images)
197
  merged_references = self._dedupe_list(collected_references + response_refs)
198
 
199
- context_payload = ""
200
  if self.document and self.document.get("path"):
201
- context_payload = f"document:{self.document.get('path')}"
 
 
 
202
 
203
  chat_payload = {
204
  "query": query,
@@ -235,10 +242,17 @@ class GoogleAgentService:
235
  else get_vector_search_prompt(user_data)
236
  )
237
  search_tool = get_web_search if mode == "web" else get_vector_search
238
- tools = [
239
- FunctionTool(search_tool),
240
- FunctionTool(get_image_search),
241
- ]
 
 
 
 
 
 
 
242
 
243
  if self.document and self.document.get("path"):
244
  tools.append(FunctionTool(self._create_document_tool()))
@@ -268,25 +282,23 @@ class GoogleAgentService:
268
  allowed_path = (document_record.get("path") or "").strip()
269
 
270
  def run_document_conversion(
271
- file_path: str,
272
  file_extension: Optional[str] = None,
273
  ) -> Dict[str, Any]:
274
- if not file_path:
 
275
  return {
276
  "status": "error",
277
  "error_message": "file_path is required to convert a document.",
278
  }
279
 
280
- normalized_input = file_path.replace("\\", "/").strip()
281
  allowed_normalized = allowed_path.replace("\\", "/").strip()
282
- if allowed_normalized and normalized_input != allowed_normalized:
283
  return {
284
  "status": "error",
285
  "error_message": "The provided file_path does not match the uploaded document for this session.",
286
  }
287
 
288
- target_path = allowed_path or file_path
289
-
290
  result = convert_document_to_markdown(
291
  file_path=target_path,
292
  file_extension=file_extension or document_record.get("extension"),
@@ -308,6 +320,45 @@ class GoogleAgentService:
308
  )
309
  return run_document_conversion
310
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
  def _create_personalized_data_tool(self, data: str) -> Callable[[], Dict[str, Any]]:
312
  sanitized = (data or "").strip()
313
 
@@ -411,6 +462,16 @@ class GoogleAgentService:
411
  "extension": self.document.get("extension"),
412
  }
413
 
 
 
 
 
 
 
 
 
 
 
414
  return {
415
  "name": self.user_profile.get("name"),
416
  "age": self.user_profile.get("age"),
@@ -442,6 +503,8 @@ class GoogleAgentService:
442
  "document_tool_name": DOCUMENT_CONVERSION_TOOL_NAME
443
  if document_info
444
  else None,
 
 
445
  }
446
 
447
  def _get_recent_history(self, limit: int = 10) -> str:
 
19
  from app.services.chathistory import ChatSession
20
  from app.services.environmental_condition import EnvironmentalData
21
  from app.services.tools import (
22
+ analyze_skin_image,
23
  convert_document_to_markdown,
24
  get_image_search,
25
  get_vector_search,
 
34
  PERSONALIZED_TOOL_NAME = "get_personalized_context"
35
  ENVIRONMENT_TOOL_NAME = "get_environmental_context"
36
  DOCUMENT_CONVERSION_TOOL_NAME = "convert_uploaded_document"
37
+ IMAGE_ANALYSIS_TOOL_NAME = "analyze_skin_image"
38
 
39
  if not os.getenv("GOOGLE_API_KEY") and GOOGLE_API_KEY:
40
  os.environ["GOOGLE_API_KEY"] = GOOGLE_API_KEY
 
54
  token: str,
55
  session_id: Optional[str] = None,
56
  document: Optional[Dict[str, Any]] = None,
57
+ image: Optional[Dict[str, Any]] = None,
58
  ) -> None:
59
  self.token = token
60
  self.session_id = session_id
 
66
  self.user_city = self.chat_session.get_city()
67
  self.environment_data = self._load_environmental_data()
68
  self.document = document
69
+ self.image = image
70
 
71
  async def process_message_async(
72
  self, query: str
 
200
  merged_images = self._dedupe_list(collected_images + response_images)
201
  merged_references = self._dedupe_list(collected_references + response_refs)
202
 
203
+ context_chunks: List[str] = []
204
  if self.document and self.document.get("path"):
205
+ context_chunks.append(f"document:{self.document.get('path')}")
206
+ if self.image and self.image.get("path"):
207
+ context_chunks.append(f"image:{self.image.get('path')}")
208
+ context_payload = " ".join(context_chunks)
209
 
210
  chat_payload = {
211
  "query": query,
 
242
  else get_vector_search_prompt(user_data)
243
  )
244
  search_tool = get_web_search if mode == "web" else get_vector_search
245
+ tools: List[FunctionTool] = []
246
+
247
+ if self.image and self.image.get("path"):
248
+ tools.append(FunctionTool(self._create_image_tool()))
249
+
250
+ tools.extend(
251
+ [
252
+ FunctionTool(search_tool),
253
+ FunctionTool(get_image_search),
254
+ ]
255
+ )
256
 
257
  if self.document and self.document.get("path"):
258
  tools.append(FunctionTool(self._create_document_tool()))
 
282
  allowed_path = (document_record.get("path") or "").strip()
283
 
284
  def run_document_conversion(
285
+ file_path: Optional[str] = None,
286
  file_extension: Optional[str] = None,
287
  ) -> Dict[str, Any]:
288
+ target_path = (file_path or allowed_path or "").replace("\\", "/").strip()
289
+ if not target_path:
290
  return {
291
  "status": "error",
292
  "error_message": "file_path is required to convert a document.",
293
  }
294
 
 
295
  allowed_normalized = allowed_path.replace("\\", "/").strip()
296
+ if allowed_normalized and target_path != allowed_normalized:
297
  return {
298
  "status": "error",
299
  "error_message": "The provided file_path does not match the uploaded document for this session.",
300
  }
301
 
 
 
302
  result = convert_document_to_markdown(
303
  file_path=target_path,
304
  file_extension=file_extension or document_record.get("extension"),
 
320
  )
321
  return run_document_conversion
322
 
323
+ def _create_image_tool(self) -> Callable[..., Dict[str, Any]]:
324
+ image_record = self.image or {}
325
+ allowed_path = (image_record.get("path") or "").strip()
326
+
327
+ def run_image_analysis(
328
+ file_path: Optional[str] = None,
329
+ language: Optional[str] = None,
330
+ ) -> Dict[str, Any]:
331
+ target_path = (file_path or allowed_path or "").replace("\\", "/").strip()
332
+ if not target_path:
333
+ return {
334
+ "status": "error",
335
+ "error_message": "file_path is required to analyse the image.",
336
+ }
337
+
338
+ allowed_normalized = allowed_path.replace("\\", "/").strip()
339
+ if allowed_normalized and target_path != allowed_normalized:
340
+ return {
341
+ "status": "error",
342
+ "error_message": "The provided file_path does not match the uploaded image for this session.",
343
+ }
344
+
345
+ result = analyze_skin_image(
346
+ file_path=target_path,
347
+ language=language or self.language,
348
+ )
349
+
350
+ if result.get("status") == "success" and allowed_path:
351
+ result["image_path"] = allowed_path
352
+
353
+ return result
354
+
355
+ run_image_analysis.__name__ = IMAGE_ANALYSIS_TOOL_NAME
356
+ run_image_analysis.__doc__ = (
357
+ "Analyse the user's uploaded skin image. Provide the `file_path` exactly as supplied "
358
+ "in the conversation context to run the classifier."
359
+ )
360
+ return run_image_analysis
361
+
362
  def _create_personalized_data_tool(self, data: str) -> Callable[[], Dict[str, Any]]:
363
  sanitized = (data or "").strip()
364
 
 
462
  "extension": self.document.get("extension"),
463
  }
464
 
465
+ image_info = None
466
+ if self.image and self.image.get("path"):
467
+ image_info = {
468
+ "path": self.image.get("path"),
469
+ "name": self.image.get("name") or "Uploaded image",
470
+ "type": self.image.get("type"),
471
+ "extension": self.image.get("extension"),
472
+ "prompt": self.image.get("prompt"),
473
+ }
474
+
475
  return {
476
  "name": self.user_profile.get("name"),
477
  "age": self.user_profile.get("age"),
 
503
  "document_tool_name": DOCUMENT_CONVERSION_TOOL_NAME
504
  if document_info
505
  else None,
506
+ "image_info": image_info,
507
+ "image_tool_name": IMAGE_ANALYSIS_TOOL_NAME if image_info else None,
508
  }
509
 
510
  def _get_recent_history(self, limit: int = 10) -> str:
app/services/image_classification_vit.py CHANGED
@@ -14,6 +14,9 @@ load_dotenv()
14
 
15
  HUGGINGFACE_TOKEN = os.getenv("HUGGINGFACE_TOKEN")
16
 
 
 
 
17
 
18
  class SkinDiseaseClassifier:
19
  CLASS_NAMES = [
@@ -23,7 +26,7 @@ class SkinDiseaseClassifier:
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()
@@ -94,6 +97,7 @@ class SkinDiseaseClassifier:
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)
 
14
 
15
  HUGGINGFACE_TOKEN = os.getenv("HUGGINGFACE_TOKEN")
16
 
17
+ # Set environment variables for better network handling
18
+ os.environ['HF_HUB_DOWNLOAD_TIMEOUT'] = '300' # Increase timeout to 5 minutes
19
+ os.environ['TRANSFORMERS_OFFLINE'] = '0' # Ensure online mode
20
 
21
  class SkinDiseaseClassifier:
22
  CLASS_NAMES = [
 
26
  "Vitiligo", "Warts Molluscum and other Viral Infections"
27
  ]
28
 
29
+ def __init__(self, repo_id="muhammadnoman76/skin-disease-classifier", cache_dir=None):
30
  self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
31
  self.repo_id = repo_id
32
  self.model = self.load_trained_model()
 
97
  if self.device.type == 'cuda':
98
  image_tensor = image_tensor.half()
99
  image_tensor = image_tensor.to(self.device)
100
+
101
  with torch.inference_mode():
102
  outputs = self.model(pixel_values=image_tensor).logits
103
  probabilities = F.softmax(outputs, dim=1)
app/services/image_processor.py DELETED
@@ -1,459 +0,0 @@
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/tools.py CHANGED
@@ -1,8 +1,11 @@
1
- import logging
 
2
  import os
3
  from pathlib import Path
4
  from typing import Any, Dict, List, Optional
5
 
 
 
6
  from app.services.vector_database_search import VectorDatabaseSearch
7
  from app.services.websearch import WebSearch
8
  from app.services.MagicConvert import (
@@ -10,6 +13,21 @@ from app.services.MagicConvert import (
10
  FileConversionException,
11
  UnsupportedFormatException,
12
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  logger = logging.getLogger(__name__)
15
 
@@ -27,6 +45,8 @@ _UPLOADS_ROOT = Path(
27
 
28
 
29
  _magic_converter = MagicConvert()
 
 
30
 
31
 
32
  def get_web_search(query: str, num_results: int = 4) -> Dict[str, Any]:
@@ -160,6 +180,230 @@ def get_image_search(query: str, max_images: int = 3) -> Dict[str, Any]:
160
  }
161
 
162
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  def convert_document_to_markdown(
164
  file_path: str,
165
  file_extension: Optional[str] = None,
 
1
+ import io
2
+ import logging
3
  import os
4
  from pathlib import Path
5
  from typing import Any, Dict, List, Optional
6
 
7
+ from PIL import Image
8
+
9
  from app.services.vector_database_search import VectorDatabaseSearch
10
  from app.services.websearch import WebSearch
11
  from app.services.MagicConvert import (
 
13
  FileConversionException,
14
  UnsupportedFormatException,
15
  )
16
+ from app.services.llm_model import Model
17
+ from app.services.prompts import (
18
+ SKIN_NON_SKIN_PROMPT,
19
+ ADVICE_REPORT_SUGGESTION,
20
+ URDU_ADVICE_REPORT_SUGGESTION,
21
+ )
22
+ from app.services.image_classification_vit import SkinDiseaseClassifier
23
+
24
+ try:
25
+ from pillow_heif import register_heif_opener
26
+
27
+ register_heif_opener()
28
+ _HEIF_SUPPORTED = True
29
+ except Exception:
30
+ _HEIF_SUPPORTED = False
31
 
32
  logger = logging.getLogger(__name__)
33
 
 
45
 
46
 
47
  _magic_converter = MagicConvert()
48
+ _skin_classifier: Optional[SkinDiseaseClassifier] = None
49
+ _skin_classifier_error: Optional[str] = None
50
 
51
 
52
  def get_web_search(query: str, num_results: int = 4) -> Dict[str, Any]:
 
180
  }
181
 
182
 
183
+ def _get_classifier() -> SkinDiseaseClassifier:
184
+ global _skin_classifier, _skin_classifier_error
185
+ if _skin_classifier is not None:
186
+ return _skin_classifier
187
+ if _skin_classifier_error:
188
+ raise RuntimeError(_skin_classifier_error)
189
+ try:
190
+ _skin_classifier = SkinDiseaseClassifier()
191
+ return _skin_classifier
192
+ except Exception as exc:
193
+ _skin_classifier_error = str(exc)
194
+ raise
195
+
196
+
197
+ def analyze_skin_image(
198
+ file_path: str,
199
+ language: Optional[str] = None,
200
+ ) -> Dict[str, Any]:
201
+ """Assess an uploaded image for dermatology analysis.
202
+
203
+ The tool verifies the file exists in the uploads directory, determines whether
204
+ the picture focuses on skin, and if so runs the skin disease classifier. When
205
+ confidence is below the 50% threshold it reports the uncertainty instead of a
206
+ diagnosis and nudges the user toward alternative options.
207
+ """
208
+
209
+ if not file_path or not str(file_path).strip():
210
+ return {"status": "error", "error_message": "file_path is required."}
211
+
212
+ try:
213
+ candidate = Path(file_path)
214
+ if not candidate.is_absolute():
215
+ candidate = (_UPLOADS_ROOT / candidate).resolve()
216
+ else:
217
+ candidate = candidate.resolve()
218
+
219
+ uploads_root = _UPLOADS_ROOT
220
+ uploads_root.mkdir(parents=True, exist_ok=True)
221
+ uploads_root = uploads_root.resolve()
222
+
223
+ if uploads_root not in candidate.parents and candidate != uploads_root:
224
+ return {
225
+ "status": "error",
226
+ "error_message": "Access to the requested file path is not permitted.",
227
+ }
228
+
229
+ if not candidate.exists() or not candidate.is_file():
230
+ return {
231
+ "status": "error",
232
+ "error_message": f"Image not found at '{candidate}'.",
233
+ }
234
+
235
+ try:
236
+ with candidate.open("rb") as fh:
237
+ image_bytes = fh.read()
238
+
239
+ image_stream = io.BytesIO(image_bytes)
240
+ pil_image = Image.open(image_stream)
241
+ pil_image.load()
242
+ pil_image = pil_image.convert("RGB")
243
+ except Exception as exc:
244
+ logger.exception("Unable to open image for analysis: %s", exc)
245
+
246
+ signature = image_bytes[:12] if 'image_bytes' in locals() else b''
247
+ looks_like_heif = signature.startswith(b"\x00\x00\x00\x20ftyp") and any(
248
+ codec in signature for codec in (b"heic", b"heix", b"hevc", b"avif")
249
+ )
250
+
251
+ if looks_like_heif and not _HEIF_SUPPORTED:
252
+ return {
253
+ "status": "error",
254
+ "error_message": (
255
+ "The uploaded image appears to be in HEIC/AVIF format, which is not supported. "
256
+ "Please convert the photo to JPG or PNG and try again."
257
+ ),
258
+ }
259
+
260
+ return {
261
+ "status": "error",
262
+ "error_message": f"Unable to open the image: {exc}",
263
+ }
264
+
265
+ user_language = (language or "english").strip().lower()
266
+
267
+ # Determine whether the photo is actually about skin.
268
+ skin_prompt_output = ""
269
+ try:
270
+ model = Model()
271
+ skin_prompt_output = model.llm_image(
272
+ text=SKIN_NON_SKIN_PROMPT,
273
+ image=pil_image,
274
+ )
275
+ except Exception as exc:
276
+ logger.exception("Vision model failed to classify skin-vs-non-skin: %s", exc)
277
+
278
+ skin_prompt_lower = skin_prompt_output.lower().strip()
279
+ is_skin_photo = not any(
280
+ marker in skin_prompt_lower for marker in ("<no>", " no", "not skin")
281
+ )
282
+ looks_like_document = "document" in skin_prompt_lower or "report" in skin_prompt_lower
283
+
284
+ if not is_skin_photo:
285
+ if user_language == "urdu":
286
+ message = (
287
+ "براہ کرم جلد کا واضح قریبی فوٹو فراہم کریں۔ اگر یہ میڈیکل رپورٹ ہے "
288
+ "تو اسے دستاویز اپ لوڈ ٹول کے ذریعے شیئر کریں تاکہ میں اس کا تجزیہ کر سکوں۔"
289
+ )
290
+ else:
291
+ message = (
292
+ "Please upload a close, well-lit photo of the affected skin area. "
293
+ "If this image is a document — such as a medical report — "
294
+ "use the document conversion tool so I can read and analyse it."
295
+ )
296
+
297
+ return {
298
+ "status": "success",
299
+ "is_skin": False,
300
+ "looks_like_document": looks_like_document,
301
+ "message": message,
302
+ "advice": ADVICE_REPORT_SUGGESTION if user_language != "urdu" else URDU_ADVICE_REPORT_SUGGESTION,
303
+ "raw_assessment": skin_prompt_output,
304
+ "image_path": str(candidate.relative_to(uploads_root)).replace("\\", "/"),
305
+ }
306
+
307
+ try:
308
+ classifier = _get_classifier()
309
+ except Exception as exc:
310
+ logger.error("Skin classifier unavailable: %s", exc)
311
+ return {
312
+ "status": "error",
313
+ "error_message": (
314
+ "Skin analysis is temporarily unavailable. "
315
+ "Ensure the classifier weights are accessible (set SKIN_CLASSIFIER_WEIGHTS to a local file "
316
+ "or configure HUGGINGFACE_TOKEN with network access) and try again."
317
+ ),
318
+ "details": str(exc),
319
+ }
320
+
321
+ disease_name, confidence = classifier.predict(pil_image, 5)
322
+
323
+ confidence_value = float(confidence)
324
+ below_threshold = confidence_value < 50.0
325
+
326
+ if user_language == "urdu":
327
+ unable_message = (
328
+ "معذرت، میں اس وقت جلد کی بیماری کی درست شناخت نہیں کر پا رہا۔ "
329
+ "براہ کرم بہتر روشنی میں ایک قریب کی تصویر اپ لوڈ کریں یا اپنی تشخیص کے لیے ڈاکٹر سے رجوع کریں۔"
330
+ )
331
+ diagnosis_message = (
332
+ f"مجھے لگتا ہے کہ یہ {disease_name} ہے اور میری اعتماد کی سطح {confidence_value:.2f}% ہے۔ "
333
+ "براہ کرم حتمی تشخیص کے لیے ماہر ڈرماٹولوجسٹ سے مشورہ کریں۔"
334
+ )
335
+ else:
336
+ unable_message = (
337
+ "I’m not confident enough to identify a condition from this photo. "
338
+ "Please upload a clearer close-up image with good lighting, or consult a dermatologist for an in-person diagnosis."
339
+ )
340
+ diagnosis_message = (
341
+ f"I suspect this may be {disease_name} with a confidence of {confidence_value:.2f}%. "
342
+ "Please consult a dermatologist for a professional evaluation and treatment plan."
343
+ )
344
+
345
+ message = unable_message if below_threshold else diagnosis_message
346
+
347
+ if user_language == "urdu":
348
+ advice_lines = ["## تصویری تجزیہ کی بنیاد پر"]
349
+ if not below_threshold:
350
+ advice_lines.append(
351
+ f"- مشتبہ بیماری: {disease_name} (اعتماد {confidence_value:.2f}%)."
352
+ )
353
+ advice_lines.append(
354
+ "- یہ نتیجہ تخمینی ہے، حتمی تشخیص کے لئے ماہر امراض جلد سے رجوع کریں۔"
355
+ )
356
+ else:
357
+ advice_lines.append(
358
+ "- ماڈل کا اعتماد 50٪ سے کم ہے، اس لئے قابل اعتماد تشخیص ممکن نہیں۔"
359
+ )
360
+ advice_lines.append(
361
+ "- متاثرہ جلد کی واضح اور روشنی میں تصویر لیں اور فلٹرز سے پرہیز کریں۔"
362
+ )
363
+ advice_lines.append(
364
+ "- اگر علامات بگڑتی یا پھیلتی ہیں تو فوری طبی معائنہ کروائیں۔"
365
+ )
366
+ else:
367
+ advice_lines = ["## Based on the Image Analysis"]
368
+ if not below_threshold:
369
+ advice_lines.append(
370
+ f"- Suspected condition: {disease_name} (confidence {confidence_value:.2f}%)."
371
+ )
372
+ advice_lines.append(
373
+ "- This prediction is probabilistic; please obtain confirmation from a dermatologist."
374
+ )
375
+ else:
376
+ advice_lines.append(
377
+ "- The model's confidence is below 50%, so no reliable diagnosis is available."
378
+ )
379
+ advice_lines.append(
380
+ "- Capture well-lit close-up photos of the affected area and avoid heavy filters."
381
+ )
382
+ advice_lines.append(
383
+ "- Seek urgent in-person care if symptoms worsen or spread rapidly."
384
+ )
385
+
386
+ advice = "\n".join(advice_lines)
387
+
388
+ return {
389
+ "status": "success",
390
+ "is_skin": True,
391
+ "diagnosis": None if below_threshold else disease_name,
392
+ "confidence": confidence_value,
393
+ "confidence_below_threshold": below_threshold,
394
+ "message": message,
395
+ "advice": advice,
396
+ "raw_assessment": skin_prompt_output,
397
+ "image_path": str(candidate.relative_to(uploads_root)).replace("\\", "/"),
398
+ }
399
+ except Exception as exc:
400
+ logger.exception("Unexpected error during image analysis: %s", exc)
401
+ return {
402
+ "status": "error",
403
+ "error_message": f"Unexpected error: {exc}",
404
+ }
405
+
406
+
407
  def convert_document_to_markdown(
408
  file_path: str,
409
  file_extension: Optional[str] = None,
pyproject.toml CHANGED
@@ -6,43 +6,186 @@ 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.36.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
- "langchain-google-genai"
46
  ]
47
 
48
  [build-system]
 
6
  { name = "Muhammad Noman", email = "muhammadnoman76@gmail.com" }
7
  ]
8
  dependencies = [
9
+ "absolufy-imports==0.3.1",
10
+ "aiohappyeyeballs==2.6.1",
11
+ "aiohttp==3.12.15",
12
+ "aiosignal==1.4.0",
13
+ "alembic==1.16.5",
14
+ "annotated-types==0.7.0",
15
+ "anyio==4.10.0",
16
+ "attrs==25.3.0",
17
+ "Authlib==1.6.3",
18
  "beautifulsoup4==4.13.4",
19
+ "Brotli==1.1.0",
20
+ "cachetools==5.5.2",
21
+ "certifi==2025.8.3",
22
+ "cffi==2.0.0",
23
+ "charset-normalizer==3.4.1",
24
+ "click==8.2.1",
25
+ "cloudpickle==3.1.1",
26
+ "cobble==0.1.4",
27
+ "colorama==0.4.6",
28
+ "cryptography==45.0.7",
29
+ "dataclasses-json==0.6.7",
30
+ "dnspython==2.8.0",
31
+ "docstring_parser==0.17.0",
32
+ "email-validator==2.3.0",
33
  "fastapi==0.115.12",
34
+ "filelock==3.19.1",
35
+ "filetype==1.2.0",
36
+ "frozenlist==1.7.0",
37
+ "fsspec==2025.9.0",
38
+ "g4f==0.5.2.1",
39
+ "google-adk==1.14.0",
40
+ "google-ai-generativelanguage==0.6.18",
41
+ "google-api-core==2.25.1",
42
+ "google-api-python-client==2.181.0",
43
+ "google-auth==2.40.3",
44
+ "google-auth-httplib2==0.2.0",
45
+ "google-cloud-aiplatform==1.113.0",
46
+ "google-cloud-appengine-logging==1.6.2",
47
+ "google-cloud-audit-log==0.3.2",
48
+ "google-cloud-bigquery==3.37.0",
49
+ "google-cloud-bigtable==2.32.0",
50
+ "google-cloud-core==2.4.3",
51
+ "google-cloud-logging==3.12.1",
52
+ "google-cloud-resource-manager==1.14.2",
53
+ "google-cloud-secret-manager==2.24.0",
54
+ "google-cloud-spanner==3.57.0",
55
+ "google-cloud-speech==2.33.0",
56
+ "google-cloud-storage==2.19.0",
57
+ "google-cloud-trace==1.16.2",
58
+ "google-crc32c==1.7.1",
59
  "google-genai==1.36.0",
60
+ "google-resumable-media==2.7.2",
61
+ "googleapis-common-protos==1.70.0",
62
+ "graphviz==0.21",
63
+ "greenlet==3.2.4",
64
+ "grpc-google-iam-v1==0.14.2",
65
+ "grpc-interceptor==0.15.4",
66
+ "grpcio==1.74.0",
67
+ "grpcio-status==1.74.0",
68
+ "h11==0.16.0",
69
+ "h2==4.3.0",
70
+ "hpack==4.1.0",
71
+ "httpcore==1.0.9",
72
+ "httplib2==0.31.0",
73
+ "httpx==0.28.1",
74
+ "httpx-sse==0.4.1",
75
+ "huggingface-hub==0.30.2",
76
+ "hyperframe==6.1.0",
77
+ "idna==3.10",
78
+ "importlib_metadata==8.7.0",
79
+ "jellyfish==1.2.0",
80
+ "Jinja2==3.1.6",
81
+ "joblib==1.5.2",
82
+ "jsonpatch==1.33",
83
+ "jsonpointer==3.0.0",
84
+ "jsonschema==4.25.1",
85
+ "jsonschema-specifications==2025.9.1",
86
+ "langchain==0.3.26",
87
+ "langchain-community==0.3.23",
88
+ "langchain-core==0.3.76",
89
+ "langchain-google-genai==2.1.4",
90
+ "langchain-qdrant==0.2.0",
91
+ "langchain-text-splitters==0.3.8",
92
+ "langsmith==0.3.45",
93
+ "lxml==6.0.1",
94
+ "Mako==1.3.10",
95
+ "mammoth==1.9.0",
96
+ "markdownify==1.1.0",
97
+ "MarkupSafe==3.0.2",
98
+ "marshmallow==3.26.1",
99
+ "mcp==1.14.0",
100
+ "mpmath==1.3.0",
101
+ "multidict==6.6.4",
102
+ "mypy_extensions==1.1.0",
103
+ "nest-asyncio==1.6.0",
104
+ "networkx==3.5",
105
  "nltk==3.9.1",
106
  "numpy==2.2.4",
107
+ "opentelemetry-api==1.37.0",
108
+ "opentelemetry-exporter-gcp-trace==1.9.0",
109
+ "opentelemetry-resourcedetector-gcp==1.9.0a0",
110
+ "opentelemetry-sdk==1.37.0",
111
+ "opentelemetry-semantic-conventions==0.58b0",
112
+ "orjson==3.11.3",
113
+ "packaging==25.0",
114
+ "pandas==2.2.3",
115
+ "pdfminer.six==20250416",
116
  "pillow==11.2.1",
117
+ "portalocker==2.10.1",
118
+ "propcache==0.3.2",
119
+ "proto-plus==1.26.1",
120
+ "protobuf==6.32.1",
121
+ "puremagic==1.28",
122
+ "pyasn1==0.6.1",
123
+ "pyasn1_modules==0.4.2",
124
+ "pycparser==2.23",
125
+ "pycryptodome==3.23.0",
126
+ "pydantic==2.11.3",
127
+ "pydantic_core==2.33.1",
128
+ "pydantic-settings==2.10.1",
129
+ "PyJWT==1.7.1",
130
  "pymongo==4.12.1",
131
+ "pyparsing==3.2.4",
132
  "pypdf==5.4.0",
133
+ "pytesseract==0.3.13",
134
+ "python-dateutil==2.9.0.post0",
135
  "python-dotenv==1.1.0",
136
+ "python-http-client==3.3.7",
137
+ "python-multipart==0.0.20",
138
+ "python-pptx==1.0.2",
139
+ "pytz==2025.2",
140
+ "pywin32==311",
141
+ "PyYAML==6.0.2",
142
+ "qdrant-client==1.14.2",
143
+ "referencing==0.36.2",
144
+ "regex==2025.9.1",
145
+ "requests==2.32.5",
146
+ "requests-toolbelt==1.0.0",
147
+ "rpds-py==0.27.1",
148
+ "rsa==4.9.1",
149
+ "safetensors==0.6.2",
150
  "scikit-learn==1.6.1",
151
+ "scipy==1.16.2",
152
+ "segtok==1.5.11",
153
  "sendgrid==6.11.0",
154
+ "shapely==2.1.1",
155
+ "six==1.17.0",
156
+ "sniffio==1.3.1",
157
+ "soupsieve==2.8",
158
+ "SQLAlchemy==2.0.43",
159
+ "sqlalchemy-spanner==1.16.0",
160
+ "sqlparse==0.5.3",
161
+ "sse-starlette==3.0.2",
162
+ "starkbank-ecdsa==2.2.0",
163
+ "starlette==0.46.2",
164
+ "sympy==1.13.1",
165
+ "tabulate==0.9.0",
166
+ "tenacity==8.5.0",
167
+ "threadpoolctl==3.6.0",
168
+ "tokenizers==0.21.4",
169
  "torch==2.5.1",
170
  "torchvision==0.20.1",
171
+ "tqdm==4.67.1",
172
  "transformers==4.51.3",
173
+ "typing_extensions==4.15.0",
174
+ "typing-inspect==0.9.0",
175
+ "typing-inspection==0.4.1",
176
+ "tzdata==2025.2",
177
+ "tzlocal==5.3.1",
178
+ "uritemplate==4.2.0",
179
+ "urllib3==2.5.0",
180
  "uvicorn==0.34.1",
181
+ "watchdog==6.0.0",
182
+ "websockets==15.0.1",
183
+ "Werkzeug==3.1.3",
184
+ "xlsxwriter==3.2.8",
185
+ "yake==0.4.8",
186
+ "yarl==1.20.1",
187
+ "zipp==3.23.0",
188
+ "zstandard==0.23.0"
 
 
 
189
  ]
190
 
191
  [build-system]
requirements.txt ADDED
File without changes