File size: 16,010 Bytes
3028f96 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 | #!/usr/bin/env python3
"""
DrRetina β LangChain Agentic Layer (SRS Β§2.2, FR-05, FR-06)
Uses LangChain with Qwen3-8B via Featherless AI (OpenAI-compatible endpoint).
The agent has access to clinical tools to:
- Explain DR grades
- Provide treatment recommendations
- Answer follow-up clinical questions
- Generate structured diagnostic reports
"""
import os
from typing import Optional
from langchain_openai import ChatOpenAI
from langchain.agents import create_agent
from langchain.tools import tool
from langchain_core.messages import SystemMessage, HumanMessage
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# FEATHERLESS AI β Qwen via OpenAI-compatible endpoint
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
_DEFAULT_KEY = "rc_c871260215042ae1dc87e28ef5672b1658b30652445af3837d0211b17edee2b8"
FEATHERLESS_KEY = os.environ.get("FEATHERLESS_API_KEY", _DEFAULT_KEY)
def get_llm(temperature: float = 0.3, max_tokens: int = 800, stop_tokens: list = None):
"""Returns LangChain ChatOpenAI configured for Qwen3-8B via Featherless."""
if not FEATHERLESS_KEY:
return None
kwargs = {}
if stop_tokens:
kwargs["stop"] = stop_tokens
return ChatOpenAI(
model="Qwen/Qwen3-8B",
openai_api_key=FEATHERLESS_KEY,
openai_api_base="https://api.featherless.ai/v1",
temperature=temperature,
max_tokens=max_tokens,
model_kwargs=kwargs
)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# DR KNOWLEDGE BASE (tools use this)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
DR_GRADES = {
0: {
"name": "No Diabetic Retinopathy",
"description": "No visible signs of diabetic retinopathy detected.",
"lesions": "None expected.",
"urgency": "Routine follow-up in 12 months.",
"treatment": "No retinal treatment needed. Maintain HbA1c <7%, BP <130/80, regular exercise.",
"lifestyle": "Control blood sugar, blood pressure, and cholesterol. Annual eye screening.",
"severity": "None",
},
1: {
"name": "Mild Diabetic Retinopathy",
"description": "Early stage with microaneurysms only β small bulges in blood vessels.",
"lesions": "Microaneurysms (tiny red dots on the retina).",
"urgency": "Follow-up in 6 months.",
"treatment": "Optimise HbA1c <7%, BP <130/80. No direct retinal treatment yet.",
"lifestyle": "Strict diabetes management, smoking cessation, dietary changes.",
"severity": "Mild",
},
2: {
"name": "Moderate Diabetic Retinopathy",
"description": "More extensive retinal changes with multiple lesion types.",
"lesions": "Microaneurysms, hard exudates (lipid deposits), retinal haemorrhages, macular oedema possible.",
"urgency": "Ophthalmology referral within 3 months.",
"treatment": "Focal laser photocoagulation for macular oedema; anti-VEGF injections if oedema present.",
"lifestyle": "Urgent diabetes optimisation; blood pressure control critical.",
"severity": "Moderate",
},
3: {
"name": "Severe Diabetic Retinopathy",
"description": "Advanced non-proliferative DR with significant retinal ischaemia.",
"lesions": "More than 20 intraretinal haemorrhages per quadrant, venous beading, IRMA (intraretinal microvascular abnormalities).",
"urgency": "Urgent ophthalmology referral within 1 month.",
"treatment": "Pan-retinal photocoagulation (PRP) laser; anti-VEGF injections; close monitoring.",
"lifestyle": "Immediate hospitalisation risk if untreated; strict metabolic control essential.",
"severity": "Severe",
},
4: {
"name": "Proliferative Diabetic Retinopathy",
"description": "Most advanced stage with new abnormal blood vessel growth (neovascularisation).",
"lesions": "Neovascularisation of disc/retina, vitreous haemorrhage, tractional retinal detachment risk.",
"urgency": "Emergency referral β immediate risk of blindness.",
"treatment": "Anti-VEGF injections (bevacizumab/ranibizumab); PRP laser; vitreoretinal surgery if haemorrhage.",
"lifestyle": "Emergency condition β do not delay. Same-day or next-day ophthalmologist visit required.",
"severity": "Critical/Emergency",
},
}
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# LANGCHAIN TOOLS (SRS: Agent Layer Tools)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@tool
def get_grade_info(grade: int) -> str:
"""Get detailed clinical information about a specific DR grade (0-4).
Use this when the user asks what their grade means."""
if grade not in DR_GRADES:
return "Invalid grade. DR grades range from 0 (No DR) to 4 (Proliferative DR)."
g = DR_GRADES[grade]
return (
f"**Grade {grade} β {g['name']}**\n"
f"Description: {g['description']}\n"
f"Severity: {g['severity']}\n"
f"Expected Lesions: {g['lesions']}\n"
f"Urgency: {g['urgency']}\n"
f"Treatment: {g['treatment']}"
)
@tool
def get_treatment_options(grade: int) -> str:
"""Get treatment options and recommendations for a specific DR grade (0-4).
Use this when the user asks about treatment, what to do next, or how to manage their condition."""
if grade not in DR_GRADES:
return "Invalid grade. Please specify a grade between 0 and 4."
g = DR_GRADES[grade]
return (
f"**Treatment for Grade {grade} ({g['name']}):**\n"
f"β’ Medical Treatment: {g['treatment']}\n"
f"β’ Urgency: {g['urgency']}\n"
f"β’ Lifestyle: {g['lifestyle']}"
)
@tool
def get_urgency_level(grade: int) -> str:
"""Get the urgency level and recommended follow-up timeline for a DR grade (0-4).
Use this when asked how serious/urgent the condition is."""
if grade not in DR_GRADES:
return "Invalid grade."
g = DR_GRADES[grade]
return (
f"**Urgency for Grade {grade} ({g['name']}):**\n"
f"Severity: {g['severity']}\n"
f"Action Required: {g['urgency']}\n"
f"Details: {g['description']}"
)
@tool
def get_lifestyle_advice(grade: int) -> str:
"""Get lifestyle and diabetes management advice for a specific DR grade (0-4).
Use this when asked about lifestyle changes, diet, exercise, or diabetes management."""
if grade not in DR_GRADES:
return "Invalid grade."
g = DR_GRADES[grade]
return (
f"**Lifestyle Advice for Grade {grade} ({g['name']}):**\n"
f"β’ {g['lifestyle']}\n"
f"General recommendations:\n"
f"β’ Keep HbA1c below 7%\n"
f"β’ Maintain blood pressure below 130/80 mmHg\n"
f"β’ Quit smoking immediately\n"
f"β’ Regular aerobic exercise (30 min/day)\n"
f"β’ Low-glycaemic diet\n"
f"β’ Annual dilated eye examination"
)
@tool
def compare_grades(grade_a: int, grade_b: int) -> str:
"""Compare two DR grades to explain the difference in severity.
Use when user asks about progression or wants to understand how serious their grade is relative to others."""
if grade_a not in DR_GRADES or grade_b not in DR_GRADES:
return "Invalid grades. Please specify grades between 0 and 4."
a = DR_GRADES[grade_a]
b = DR_GRADES[grade_b]
return (
f"**Comparison: Grade {grade_a} vs Grade {grade_b}**\n"
f"Grade {grade_a} ({a['name']}): {a['severity']} severity β {a['urgency']}\n"
f"Grade {grade_b} ({b['name']}): {b['severity']} severity β {b['urgency']}"
)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# F4: REFERRAL LETTER TOOLS
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@tool
def analyze_severity(grade: int, confidence: float) -> str:
"""Analyze the clinical severity of the findings based on grade and confidence."""
if grade not in DR_GRADES:
return "Invalid grade."
g = DR_GRADES[grade]
return f"Severity Analysis: Grade {grade} ({g['name']}). Confidence is {confidence:.1f}%. Expected lesions: {g['lesions']}. Risk level is {g['severity']}."
@tool
def recommend_treatment(grade: int) -> str:
"""Provide evidence-based treatment protocol for a given DR grade."""
if grade not in DR_GRADES:
return "Invalid grade."
return f"Recommended Intervention: {DR_GRADES[grade]['treatment']}"
@tool
def calculate_urgency(grade: int, progression_rate: str = "Unknown") -> str:
"""Calculate the referral timeline and urgency based on grade."""
if grade not in DR_GRADES:
return "Invalid grade."
return f"Suggested Timeline: {DR_GRADES[grade]['urgency']}. Progression: {progression_rate}."
@tool
def generate_referral_letter(patient_name: str, findings: str, urgency: str) -> str:
"""Generate a formatted clinical referral letter."""
import datetime
today = datetime.datetime.now().strftime("%B %d, %Y")
return f"""Date: {today}
From: DrRetina Clinical AI System
To: Vitreoretinal Specialist
RE: Referral β {patient_name}
I am referring this patient for evaluation of Diabetic Retinopathy.
AI Analysis Findings:
{findings}
{urgency}
Generated by DrRetina v1.0 | AMD MI300X | Kappa: 0.9097
"""
TOOLS = [get_grade_info, get_treatment_options, get_urgency_level,
get_lifestyle_advice, compare_grades,
analyze_severity, recommend_treatment, calculate_urgency, generate_referral_letter]
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# AGENT BUILDER LOGIC REPLACED BY create_agent IN agent_qa
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# AGENT REPORT GENERATION (FR-05)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def agent_generate_report(grade: int, probs, language: str = "English") -> Optional[str]:
"""Generate diagnostic report using LangChain + Qwen."""
llm = get_llm(temperature=0.3, max_tokens=1500, stop_tokens=["End of report", "AI Disclaimer", "Β©"])
if not llm:
return None
try:
grade_info = DR_GRADES[grade]
prob_txt = " | ".join(
f"Grade {i} ({DR_GRADES[i]['name'][:8]}): {p*100:.1f}%"
for i, p in enumerate(probs)
)
messages = [
SystemMessage(content=(
f"You are an expert ophthalmologist generating a clinical report in {language}. "
f"Patient has Grade {grade} DR ({grade_info['name']}). "
f"Severity: {grade_info['severity']}. "
f"Instructions: Be compassionate, professional, and clinically accurate. "
f"IMPORTANT: Do not repeat any sections. Stop immediately after the AI disclaimer. "
f"Use simple, non-technical terms if the language is not English."
)),
HumanMessage(content=(
f"Generate a structured clinical diagnostic report for this DR screening result in {language}:\n\n"
f"**Detected Grade:** {grade} β {grade_info['name']}\n"
f"**Confidence:** {probs[grade]*100:.1f}%\n"
f"**All Probabilities:** {prob_txt}\n\n"
f"The report MUST include these sections ONLY (translated to {language}):\n"
f"## 1. Diagnosis Summary\n"
f"## 2. Severity Assessment\n"
f"## 3. Expected Lesions\n"
f"## 4. Treatment Options\n"
f"## 5. Follow-up Timeline\n"
f"## 6. Clinical Recommendation\n\n"
f"Finish the report with: 'End of report.' followed by a brief AI disclaimer. "
f"Do not write more than 400 words. Do not repeat sections."
)),
]
response = llm.invoke(messages)
return response.content
except Exception as e:
print(f"[Agent Report Error] {e}")
return None
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# AGENT Q&A (FR-06)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def agent_qa(question: str, grade: int, confidence: float, report: str, history: list = None) -> Optional[str]:
"""Answer clinical questions quickly without slow tool roundtrips."""
llm = get_llm(temperature=0.6, max_tokens=1500)
if not llm:
return None
g_info = DR_GRADES[grade]
sys_msg = f"""You are DrRetina, a clinical AI assistant specializing in Diabetic Retinopathy (DR).
Patient's current condition:
- DR Grade: {grade} β {g_info['name']}
- Severity: {g_info['severity']}
- Expected Lesions: {g_info['lesions']}
- Urgency: {g_info['urgency']}
- Recommended Treatment: {g_info['treatment']}
- Lifestyle Advice: {g_info['lifestyle']}
- Confidence: {confidence:.1f}%
IMPORTANT INSTRUCTIONS:
1. Use the clinical context above to answer the user's questions accurately.
2. Be compassionate, clear, and professional.
3. Always recommend consulting a qualified ophthalmologist.
4. MULTILINGUAL SUPPORT: You MUST reply in the exact same language that the user asks the question in (e.g., if they ask in Urdu, reply in fluent Urdu; if Hindi, reply in Hindi)."""
try:
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
msg = [SystemMessage(content=sys_msg)]
if history:
for h in history:
if h["role"] == "user": msg.append(HumanMessage(content=h["content"]))
else: msg.append(AIMessage(content=h["content"]))
msg.append(HumanMessage(content=question))
return llm.invoke(msg).content
except Exception as e:
print(f"[Agent QA Error] {e}")
return None
|