Omarelrayes commited on
Commit
906250b
·
verified ·
1 Parent(s): 87e1988

Create agent.py

Browse files
Files changed (1) hide show
  1. agent.py +263 -0
agent.py ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+ import string
3
+ import re
4
+ import datetime
5
+ import base64
6
+ import pandas as pd
7
+ from pathlib import Path
8
+ import os
9
+ import logging
10
+ from langchain_core.tools import tool
11
+ from langchain_groq import ChatGroq
12
+ from langgraph.prebuilt import create_react_agent
13
+ from langgraph.checkpoint.memory import MemorySaver
14
+
15
+ import cv2
16
+ import numpy as np
17
+ from PIL import Image
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ # Global variables
22
+ _SESSIONS = None
23
+ _CURRENT_THREAD_ID = None
24
+ _API_CLIENT = None
25
+ _RETRIEVER = None
26
+
27
+ def set_session_storage(sessions_dict: dict) -> None:
28
+ global _SESSIONS
29
+ _SESSIONS = sessions_dict
30
+
31
+ def set_current_thread_id(thread_id: str) -> None:
32
+ global _CURRENT_THREAD_ID
33
+ _CURRENT_THREAD_ID = thread_id
34
+
35
+ def set_api_client(api_client) -> None:
36
+ global _API_CLIENT
37
+ _API_CLIENT = api_client
38
+
39
+ def get_latest_image(thread_id: str) -> dict | None:
40
+ """Get latest image from external API"""
41
+ if _API_CLIENT is None:
42
+ return None
43
+
44
+ result = _API_CLIENT.get_patient_image(thread_id)
45
+ if result.get('ok') and result.get('base64'):
46
+ # Decode base64 image
47
+ try:
48
+ img_data = base64.b64decode(result['base64'])
49
+ image = Image.open(io.BytesIO(img_data))
50
+ return {"image": image}
51
+ except Exception as e:
52
+ logger.error(f"Error decoding image: {e}")
53
+ return None
54
+ return None
55
+
56
+ def _store_image_blobs(blobs: dict) -> None:
57
+ """Store image blobs in session"""
58
+ if not _SESSIONS or not _CURRENT_THREAD_ID:
59
+ return
60
+ if _CURRENT_THREAD_ID not in _SESSIONS:
61
+ _SESSIONS[_CURRENT_THREAD_ID] = {}
62
+ existing = _SESSIONS[_CURRENT_THREAD_ID].get("_image_blobs", {})
63
+ existing.update(blobs)
64
+ _SESSIONS[_CURRENT_THREAD_ID]["_image_blobs"] = existing
65
+ names = list(_SESSIONS[_CURRENT_THREAD_ID].get("_session_image_names", []))
66
+ for n in blobs.keys():
67
+ if n not in names:
68
+ names.append(n)
69
+ _SESSIONS[_CURRENT_THREAD_ID]["_session_image_names"] = names
70
+
71
+ # System Prompt
72
+ SYSTEM_PROMPT = """You are DermaScan AI, a dermatology assistant on a dataset of 1000 skin cancer patients. Built by Eng. Youssef Bastawisy.
73
+
74
+ IMAGES: handled by tools only, on the image already uploaded this session. NEVER say you can't see/read images. On any image request, call the matching tool immediately:
75
+ - segment_my_image (segment/mask/overlay), reanalyze_my_image (analyze/re-analyze), get_my_uploaded_image (show/view photo)
76
+ No image is ever downloaded from the internet or from a patient_id — matching an uploaded image to a dataset record is handled internally by the tool. NEVER mention how the matching works internally (no "feature vectors", "global_features", "similarity", "threshold", "columns", etc.) — just state plainly whether a matching record was found or not.
77
+ If a tool says no matching record was found (new patient), you MUST say clearly this is a new patient not in the database, and STOP there for anything hereditary/genetic — never mention a mutation, family history, or risk score for them, and never reuse a different (previously matched) patient's name or data for this new image.
78
+ After a tool returns [ANALYSIS_RESULT:{data}], discuss only that data (label, confidence, affected area) — never say "shown"/"displayed".
79
+
80
+ LANGUAGE: reply in the same language/dialect the user just used (Arabic or English). Never mix in Russian/Thai/Chinese/Japanese/Korean script — if unsure of a term, describe simply instead.
81
+
82
+ PATIENT MODE STYLE: no jargon (no "asymmetry index", "ABCDE"). Plain everyday words, max 4-6 short sentences/bullets. Never mix English medical terms into Arabic sentences.
83
+
84
+ ROLE ACCESS:
85
+ - You operate in PATIENT mode only. Never reveal other patients' data, only their own uploaded result.
86
+ - If the result is Malignant: stress urgency + next steps + suggest the "📅 Book appointment" button (create a referral ticket only if explicitly asked).
87
+ - If the result is Benign: prevention + monitoring tips.
88
+
89
+ MATCHED PATIENT RECORD: if this block appears, the uploaded image matched an existing patient (same person uploading). Use it to answer diagnosis/family-history questions directly and specifically — summarize relevant parts, don't dump verbatim.
90
+
91
+ STRICT RAG RULE: your knowledge is limited to (1) patient records via matched block, (2) knowledge-base via search_knowledge_base, (3) AI image results, (4) this conversation. Never invent symptoms, history, numbers. If info isn't available say exactly: "This information is not available in the current patient record." (or Arabic equivalent).
92
+
93
+ TOOLS:
94
+ - search_knowledge_base is the default first call for general clinical/knowledge questions (cite the source filename).
95
+ - Use segment_my_image for segmentation/mask/overlay requests.
96
+ - Use reanalyze_my_image for classification + segmentation analysis.
97
+ - STOP after one tool call for a simple lookup, and at most two tool calls for a combined request.
98
+ - Never call the same tool twice.
99
+ - Never call a second different tool just because the first one returned a valid answer.
100
+ - Keep replies SHORT by default (2–4 sentences) unless the user explicitly asks for more detail.
101
+
102
+ QUICK ROUTING (pick one, don't overthink):
103
+ - General symptom/skin question, no specific image → search_knowledge_base only.
104
+ - "my/this image", "analyze it", "the result" → segment_my_image/reanalyze_my_image/get_my_uploaded_image.
105
+ - Own history/family/risk → answer from the [MATCHED PATIENT RECORD] block if present; if absent, say the info isn't available yet.
106
+
107
+ Booking is via the "📅 Book appointment" button. Never print booking IDs, tickets, or debug/system text yourself.
108
+
109
+ ANALYSIS MARKER: if a tool response starts with `[ANALYSIS_RESULT:{...}]`, keep that exact marker at the very start of your reply, unmodified. After it, briefly cover: classification + confidence, affected area, what it means in plain terms, relevant family/hereditary context if present, and next steps. Keep it short — a routine benign result needs only 2-3 sentences; a malignant one with family history can go a bit longer. Remind patients once (not every message) that this doesn't replace a professional diagnosis.
110
+
111
+ If a user gives a local file path, tell them to use the 📎 upload button instead.
112
+ """
113
+
114
+ # Tools
115
+ @tool
116
+ def search_knowledge_base(query: str) -> str:
117
+ """Search the dermatology knowledge base for clinical information."""
118
+ if _RETRIEVER is None:
119
+ return "Retriever not initialized."
120
+ docs = _RETRIEVER.invoke(query)
121
+ if not docs:
122
+ return "No relevant information found."
123
+ formatted = []
124
+ for i, d in enumerate(docs, 1):
125
+ source = d.metadata.get("source", "unknown").replace("\\", "/").split("/")[-1]
126
+ formatted.append(f"[Source {i}: {source}]\n{d.page_content}")
127
+ return "\n\n---\n\n".join(formatted)
128
+
129
+ @tool
130
+ def get_my_uploaded_image() -> str:
131
+ """[PATIENT MODE] Tell patient to use the Show My Image button."""
132
+ return "Your image is stored for this session. Use the 📷 Show My Image button to view it."
133
+
134
+ @tool
135
+ def segment_my_image() -> str:
136
+ """[PATIENT MODE] Run U-Net segmentation on the patient's uploaded image."""
137
+ if not _SESSIONS or not _CURRENT_THREAD_ID:
138
+ return "No session found."
139
+
140
+ if _API_CLIENT is None:
141
+ return "API client not initialized."
142
+
143
+ try:
144
+ result = _API_CLIENT.segment_patient_image(_CURRENT_THREAD_ID)
145
+
146
+ if not result.get('ok'):
147
+ return f"Error: {result.get('error', 'Segmentation failed')}"
148
+
149
+ # Store images
150
+ if 'images' in result:
151
+ _store_image_blobs(result['images'])
152
+
153
+ import json as _json
154
+ result_data = {
155
+ "label": "",
156
+ "confidence_pct": 0,
157
+ "infection_pct": result.get('infection_pct', 0)
158
+ }
159
+ marker = f"[ANALYSIS_RESULT:{_json.dumps(result_data)}]"
160
+
161
+ return f"{marker}\n\nSEGMENTATION OF YOUR IMAGE\nAffected Area: {result['infection_pct']}%"
162
+
163
+ except Exception as e:
164
+ logger.error(f"Error in segment_my_image: {e}", exc_info=True)
165
+ return f"Error segmenting image: {str(e)}"
166
+
167
+ @tool
168
+ def reanalyze_my_image() -> str:
169
+ """[PATIENT MODE] Full AI analysis (classification + segmentation) on the patient's
170
+ most recently uploaded image."""
171
+ if not _SESSIONS or not _CURRENT_THREAD_ID:
172
+ return "No session found."
173
+
174
+ if _API_CLIENT is None:
175
+ return "API client not initialized."
176
+
177
+ try:
178
+ result = _API_CLIENT.reanalyze_patient_image(_CURRENT_THREAD_ID)
179
+
180
+ if not result.get('ok'):
181
+ return f"Error: {result.get('error', 'Analysis failed')}"
182
+
183
+ # Store images
184
+ if 'images' in result:
185
+ _store_image_blobs(result['images'])
186
+
187
+ import json as _json
188
+ result_data = {
189
+ "label": result.get("label", ""),
190
+ "confidence_pct": result.get("confidence_pct", 0),
191
+ "infection_pct": result.get("infection_pct", 0),
192
+ }
193
+ marker = f"[ANALYSIS_RESULT:{_json.dumps(result_data)}]"
194
+
195
+ lines = [
196
+ marker,
197
+ "",
198
+ "ANALYSIS OF YOUR IMAGE",
199
+ f"Classification: {result['label']} ({result['confidence_pct']}%)",
200
+ f"Affected Area: {result['infection_pct']}%"
201
+ ]
202
+
203
+ return "\n".join(lines)
204
+
205
+ except Exception as e:
206
+ logger.error(f"Error in reanalyze_my_image: {e}", exc_info=True)
207
+ return f"Error analyzing image: {str(e)}"
208
+
209
+ @tool
210
+ def create_referral_ticket(issue_summary: str, urgency: str = "routine") -> str:
211
+ """Create a dermatologist referral ticket."""
212
+ ticket_id = "DS-" + "".join(random.choices(string.digits, k=6))
213
+ times = {
214
+ "routine": "within 2–4 weeks",
215
+ "urgent": "within 48–72 hours",
216
+ "emergency": "within 24 hours — go to nearest dermatology clinic",
217
+ }
218
+ timeframe = times.get(urgency, "within 2–4 weeks")
219
+
220
+ DEFAULT_CLINIC = {
221
+ "name": "مركز القاهرة للأمراض الجلدية",
222
+ "address": "شارع التحرير، الدقي، الجيزة",
223
+ "phone": "01011112222",
224
+ }
225
+
226
+ return (
227
+ f"Referral created. Ticket: {ticket_id} | Urgency: {urgency.upper()} | "
228
+ f"Timeframe: {timeframe} | Summary: {issue_summary} | "
229
+ f"Clinic: {DEFAULT_CLINIC['name']} — {DEFAULT_CLINIC['address']}"
230
+ )
231
+
232
+ def build_agent(retriever, model_name: str = "llama-3.1-8b-instant", temperature: float = 0.25):
233
+ global _RETRIEVER
234
+ _RETRIEVER = retriever
235
+
236
+ groq_key = os.environ.get("GROQ_API_KEY")
237
+ if not groq_key:
238
+ raise ValueError("GROQ_API_KEY not set in environment variables.")
239
+
240
+ llm = ChatGroq(
241
+ groq_api_key=groq_key,
242
+ model_name=model_name,
243
+ temperature=temperature,
244
+ max_tokens=512,
245
+ request_timeout=120,
246
+ )
247
+
248
+ tools = [
249
+ search_knowledge_base,
250
+ get_my_uploaded_image,
251
+ segment_my_image,
252
+ reanalyze_my_image,
253
+ create_referral_ticket,
254
+ ]
255
+
256
+ memory = MemorySaver()
257
+
258
+ return create_react_agent(
259
+ model=llm,
260
+ tools=tools,
261
+ prompt=SYSTEM_PROMPT,
262
+ checkpointer=memory,
263
+ )