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