Spaces:
Sleeping
Sleeping
Commit
·
b7dfc73
1
Parent(s):
4209845
update
Browse files- app/routers/agent_chat.py +10 -0
- app/routers/chat.py +78 -14
- app/services/__init__.py +0 -2
- app/services/agentic_prompt.py +118 -34
- app/services/google_agent_service.py +75 -12
- app/services/image_classification_vit.py +5 -1
- app/services/image_processor.py +0 -459
- app/services/tools.py +245 -1
- pyproject.toml +165 -22
- requirements.txt +0 -0
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
|
|
|
|
| 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 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
)
|
| 360 |
-
|
| 361 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
except Exception as e:
|
| 363 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
"
|
| 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 |
-
"
|
| 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 |
-
|
| 112 |
-
You are Dr. DermAI, an evidence-based dermatology consultant.
|
| 113 |
-
|
| 114 |
-
PRIMARY DIRECTIVES:
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 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 |
-
|
| 142 |
-
You are Dr. DermAI, a dermatologist with access to a curated clinical knowledge base.
|
| 143 |
-
|
| 144 |
-
PRIMARY DIRECTIVES:
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 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 |
-
|
| 200 |
if self.document and self.document.get("path"):
|
| 201 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 240 |
-
|
| 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 |
-
|
|
|
|
| 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
|
| 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
|
|
|
|
| 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 |
-
"
|
| 13 |
-
"
|
| 14 |
-
"
|
| 15 |
-
"
|
| 16 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
"nltk==3.9.1",
|
| 18 |
"numpy==2.2.4",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
"pillow==11.2.1",
|
| 20 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
"pymongo==4.12.1",
|
|
|
|
| 22 |
"pypdf==5.4.0",
|
| 23 |
-
"
|
|
|
|
| 24 |
"python-dotenv==1.1.0",
|
| 25 |
-
"
|
| 26 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
"
|
| 33 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
"uvicorn==0.34.1",
|
| 35 |
-
"
|
| 36 |
-
"
|
| 37 |
-
"
|
| 38 |
-
"
|
| 39 |
-
"
|
| 40 |
-
"
|
| 41 |
-
"
|
| 42 |
-
"
|
| 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
|