File size: 10,728 Bytes
0d340ad
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98b5940
0d340ad
 
 
 
 
 
 
 
321a905
98b5940
321a905
0d340ad
 
 
 
 
 
 
98b5940
 
321a905
0d340ad
321a905
 
 
0d340ad
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5e1e02b
0d340ad
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321a905
0d340ad
 
 
 
321a905
0d340ad
321a905
98b5940
0d340ad
 
98b5940
0d340ad
 
 
98b5940
0d340ad
98b5940
0d340ad
 
 
98b5940
 
0d340ad
98b5940
0d340ad
 
 
321a905
0d340ad
321a905
98b5940
0d340ad
 
 
98b5940
0d340ad
98b5940
0d340ad
 
98b5940
 
0d340ad
 
321a905
0d340ad
 
 
 
 
 
 
 
 
 
 
 
 
 
98b5940
0d340ad
 
 
 
 
 
98b5940
0d340ad
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321a905
0d340ad
98b5940
0d340ad
 
 
98b5940
0d340ad
 
98b5940
0d340ad
 
98b5940
0d340ad
 
ba882c4
b7f8ba3
0d340ad
 
 
 
 
98b5940
0d340ad
 
 
 
 
 
 
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
import os
import json
import operator
from typing import TypedDict, Annotated

import gradio as gr
from gradio_client import Client, handle_file
from neo4j import GraphDatabase
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from langchain_core.documents import Document
from langchain_experimental.graph_transformers import LLMGraphTransformer
from langgraph.graph import StateGraph, END

# ==========================================
# 1. CONFIGURATION
# ==========================================
AZURE_OPENAI_API_KEY = os.environ.get("AZURE_OPENAI_API_KEY")
AZURE_OPENAI_API_BASE = os.environ.get("AZURE_OPENAI_API_BASE_URL")
AZURE_DEPLOYMENT_NAME = os.environ.get("AZURE_OPENAI_DEPLOYMENT_NAME")
NEO4J_URI = os.environ.get("NEO4J_URI")
NEO4J_USERNAME = os.environ.get("NEO4J_USERNAME", "neo4j")
NEO4J_PASSWORD = os.environ.get("NEO4J_PASSWORD")

# ==========================================
# 2. SETUP
# ==========================================

llm = ChatOpenAI(
    api_key=AZURE_OPENAI_API_KEY,
    base_url=AZURE_OPENAI_API_BASE,
    model=AZURE_DEPLOYMENT_NAME,
    temperature=0
)
try: ChatOpenAI.model_rebuild()
except: pass

def ingest_data():
    if not NEO4J_URI or not NEO4J_PASSWORD:
        print("⚠️ Neo4j Credentials missing. Skipping ingestion.")
        return
    print("--- 🔄 Initializing Knowledge Graph ---")
    try:
        driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USERNAME, NEO4J_PASSWORD))
        prime_kg_data = [
            ("Ischemic Stroke", "associated_with", "Hypertension"),
            ("Ischemic Stroke", "associated_with", "Atrial Fibrillation"),
            ("Ischemic Stroke", "treated_by", "Alteplase"),
            ("Ischemic Stroke", "treated_by", "Aspirin"),
            ("Ischemic Stroke", "treated_by", "Clopidogrel"),
            ("Alteplase", "mechanism", "Fibrinolytic Agent"),
            ("Alteplase", "contraindicated_in", "Active Bleeding"),
            ("Intracranial Hemorrhage", "associated_with", "Hypertension"),
            ("Intracranial Hemorrhage", "associated_with", "Cerebral Amyloid Angiopathy"),
            ("Intracranial Hemorrhage", "treated_by", "Mannitol"),
            ("Intracranial Hemorrhage", "treated_by", "Nicardipine"),
            ("Mannitol", "mechanism", "Osmotic Diuretic"),
            ("Ischemic Stroke", "presents_as", "Hemiparesis"),
            ("Ischemic Stroke", "presents_as", "Aphasia"),
            ("Intracranial Hemorrhage", "presents_as", "Severe Headache"),
            ("Intracranial Hemorrhage", "presents_as", "Vomiting")
        ]
        query = """
        UNWIND $data as row
        MERGE (h:MedicalEntity {name: row[0]})
        MERGE (t:MedicalEntity {name: row[2]})
        MERGE (h)-[:PRIMEKG_REL {type: row[1]}]->(t)
        """
        with driver.session() as session:
            session.run("MATCH (n) DETACH DELETE n")
            session.run(query, data=prime_kg_data)
        
        aha_guideline_text = """
        For patients eligible for intravenous fibrinolytic therapy, Alteplase (0.9 mg/kg) is recommended within 3 hours of onset of ischemic stroke.
        Mechanical Thrombectomy is indicated for patients with large vessel occlusion (LVO) in the anterior circulation within 6 hours.
        For patients with acute Intracranial Hemorrhage, patients with systolic blood pressure >220 mmHg should be treated with aggressive blood pressure reduction.
        Aspirin is recommended for secondary prevention 24 to 48 hours after thrombolysis.
        """
        docs = [Document(page_content=aha_guideline_text)]
        llm_transformer = LLMGraphTransformer(llm=llm)
        graph_documents = llm_transformer.convert_to_graph_documents(docs)
        with driver.session() as session:
            for doc in graph_documents:
                for node in doc.nodes:
                    session.run("MERGE (n:GuidelineEntity {id: $id, type: $type})", id=node.id, type=node.type)
                for edge in doc.relationships:
                    session.run("""
                        MATCH (s:GuidelineEntity {id: $source})
                        MATCH (t:GuidelineEntity {id: $target})
                        MERGE (s)-[:GUIDELINE_REL {type: $type}]->(t)
                        """, source=edge.source.id, target=edge.target.id, type=edge.type)
        driver.close()
        print("✅ Knowledge Graph Built Successfully.")
    except Exception as e:
        print(f"❌ KG Initialization Failed: {e}")

ingest_data()

# ==========================================
# 3. TOOLS
# ==========================================

@tool
def detect_stroke_type(image_path: str) -> str:
    """Analyzes a patient's CT scan using the remote Vision AI model."""
    HF_SPACE_URL = "Siyad798/stroke-diagnosis-ai" 
    print(f"--- 🧠 Calling Vision AI for {image_path} ---")
    if not os.path.exists(image_path): return json.dumps({"error": f"File not found locally: {image_path}"})
    try:
        client = Client(HF_SPACE_URL)
        result = client.predict(files=[handle_file(image_path)], api_name="/predict")
        ai_output = {}
        if isinstance(result, (list, tuple)) and len(result) > 1:
            json_data = result[1]
            if isinstance(json_data, dict): ai_output = json_data
            elif isinstance(json_data, str) and os.path.exists(json_data):
                with open(json_data, 'r') as f: ai_output = json.load(f)
            elif isinstance(json_data, str):
                try: ai_output = json.loads(json_data)
                except: ai_output = {"raw_output": json_data}
        else: ai_output = result
        if not ai_output: ai_output = {"error": "Empty response", "diagnosis": "Unknown"}
        return json.dumps(ai_output)
    except Exception as e: return json.dumps({"error": f"Inference Failed: {str(e)}", "diagnosis": "Unknown"})

@tool
def get_treatment_guideline(stroke_type: str) -> str:
    """Queries AHA/ASA Guidelines from Neo4j Knowledge Graph."""
    search_term = "Ischemic Stroke" if "Ischemic" in stroke_type else "Intracranial Hemorrhage"
    print(f"--- 🕸️ Querying KG for: {search_term} ---")
    if not NEO4J_URI or not NEO4J_PASSWORD: return json.dumps({"error": "Neo4j credentials not configured."})
    try:
        driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USERNAME, NEO4J_PASSWORD))
        with driver.session() as session:
            q1 = "MATCH (s:StrokeType {name: $st})-[r:INDICATED_FOR]->(t:Treatment) RETURN t.name as Treatment, t.class as Class, t.dosage as Dosage, r.criteria as Criteria"
            treatments = session.run(q1, st=search_term).data()
            q2 = "MATCH (c:ClinicalCondition)-[:CONTRAINDICATES]->(t:Treatment)<-[:INDICATED_FOR]-(s:StrokeType {name: $st}) RETURN t.name as Drug, c.name as Contraindication"
            safety = session.run(q2, st=search_term).data()
        driver.close()
        return json.dumps({"Guidelines_Source": "AHA/ASA 2019", "Recommended_Treatments": treatments, "Safety_Warnings": safety})
    except Exception as e: return json.dumps({"error": str(e)})

# ==========================================
# 4. AGENT WORKFLOW
# ==========================================

class AgentState(TypedDict):
    messages: Annotated[list, operator.add]
    image_path: str
    stroke_diagnosis: dict
    guidelines: dict
    final_report: str

def detector_node(state: AgentState):
    print("--- Node: Stroke Detector ---")
    result_json = detect_stroke_type.invoke(state["image_path"])
    diagnosis = json.loads(result_json)
    msg = f"Imaging Analysis Complete. Diagnosis: {diagnosis.get('diagnosis')}."
    return {"messages": [HumanMessage(content=msg)], "stroke_diagnosis": diagnosis}

def advisor_node(state: AgentState):
    print("--- Node: Guideline Advisor ---")
    diagnosis = state["stroke_diagnosis"].get("diagnosis", "Unknown")
    guidelines_json = get_treatment_guideline.invoke(diagnosis)
    guidelines = json.loads(guidelines_json)
    return {"messages": [HumanMessage(content=f"Guidelines Retrieved for {diagnosis}.")], "guidelines": guidelines}

def synthesizer_node(state: AgentState):
    print("--- Node: Report Synthesizer ---")
    prompt = f"""
    You are a Stroke Neurologist Assistant. Write a clinical report based on:
    1. **Imaging AI Result**: {json.dumps(state['stroke_diagnosis'], indent=2)}
    2. **Clinical Guidelines**: {json.dumps(state['guidelines'], indent=2)}
    3. **Patient Context**: {state['messages'][0].content}
    **Structure:**
    - Diagnosis & Severity
    - Imaging Findings
    - Recommended Interventions (Cite Evidence)
    - Safety Checks & Contraindications
    """
    response = llm.invoke(prompt)
    return {"final_report": response.content}

workflow = StateGraph(AgentState)
workflow.add_node("detector", detector_node)
workflow.add_node("advisor", advisor_node)
workflow.add_node("synthesizer", synthesizer_node)
workflow.set_entry_point("detector")
workflow.add_edge("detector", "advisor")
workflow.add_edge("advisor", "synthesizer")
workflow.add_edge("synthesizer", END)
app = workflow.compile()

# ==========================================
# 5. GRADIO UI
# ==========================================

def predict_stroke_report(uploaded_file, patient_context_input):
    if not uploaded_file: return "Please upload a CT scan file."
    image_path = uploaded_file.name
    inputs = {"messages": [HumanMessage(content=patient_context_input)], "image_path": image_path}
    final_report_content = "Processing..."
    try:
        for event in app.stream(inputs, stream_mode="values"): pass 
        final_state = event
        final_report_content = final_state.get("final_report", "Error generating report.")
    except Exception as e: final_report_content = f"Critical Error: {str(e)}"
    return final_report_content

# Safe initialization without arguments
with gr.Blocks() as demo:
    gr.Markdown("# 🧠 AI Stroke Diagnosis & Guideline Agent")
    gr.Markdown("Integrates Vision AI (DenseNet) + Knowledge Graph (Neo4j) + LLM (Azure OpenAI)")
    with gr.Row():
        with gr.Column():
            file_input = gr.File(label="Upload CT (ZIP/DCM)", file_types=[".zip", ".dcm"])
            patient_context = gr.Textbox(label="Patient Context", lines=4, value="Patient 55M, onset 2 hours ago. BP 150/90. No history of bleeding.")
            submit_btn = gr.Button("Generate Report", variant="primary")
        with gr.Column():
            report_output = gr.Markdown(label="Clinical Report")
    submit_btn.click(predict_stroke_report, inputs=[file_input, patient_context], outputs=report_output)

if __name__ == "__main__":
    demo.launch(server_name="0.0.0.0", server_port=7860)