|
|
"""
|
|
|
Healthcare Agents - Multi-Agent System
|
|
|
Contains all agent definitions and LangGraph workflow
|
|
|
"""
|
|
|
|
|
|
from typing import TypedDict
|
|
|
from langchain_core.prompts import ChatPromptTemplate
|
|
|
from langgraph.graph import StateGraph, END
|
|
|
|
|
|
|
|
|
|
|
|
class AgentState(TypedDict):
|
|
|
user_input: str
|
|
|
symptom_analysis: str
|
|
|
medication_advice: str
|
|
|
home_remedies: str
|
|
|
diet_lifestyle: str
|
|
|
doctor_recommendations: str
|
|
|
error: str
|
|
|
|
|
|
|
|
|
|
|
|
def search_tavily(tavily_client, query: str, max_results: int = 3) -> str:
|
|
|
"""Search Tavily API for verified health information"""
|
|
|
if not tavily_client:
|
|
|
return "Tavily client not initialized."
|
|
|
try:
|
|
|
response = tavily_client.search(
|
|
|
query=query,
|
|
|
search_depth="advanced",
|
|
|
max_results=max_results
|
|
|
)
|
|
|
if response.get("results"):
|
|
|
sources = []
|
|
|
for result in response["results"]:
|
|
|
title = result.get("title", "")
|
|
|
content = result.get("content", "")
|
|
|
url = result.get("url", "")
|
|
|
sources.append(f"**{title}**\n{content}\nSource: {url}")
|
|
|
return "\n\n".join(sources)
|
|
|
return "No verified information found."
|
|
|
except Exception as e:
|
|
|
return f"Error fetching data: {str(e)}"
|
|
|
|
|
|
|
|
|
|
|
|
def symptom_analyzer_agent(state: AgentState, llm, tavily_client) -> AgentState:
|
|
|
"""Analyze symptoms and identify possible conditions"""
|
|
|
if not llm:
|
|
|
state["symptom_analysis"] = "LLM not initialized."
|
|
|
return state
|
|
|
|
|
|
user_input = state["user_input"]
|
|
|
|
|
|
|
|
|
tavily_info = search_tavily(tavily_client, f"medical symptoms analysis {user_input}")
|
|
|
|
|
|
prompt = ChatPromptTemplate.from_messages([
|
|
|
("system", """You are a clinical triage assistant. Use verified Tavily evidence to deliver a concise, non-repetitive assessment.
|
|
|
|
|
|
Output STRICTLY in this structure (markdown):
|
|
|
|
|
|
**Structured Symptom Analysis**
|
|
|
- **Summary:** • sentence 1 • sentence 2 (max 25 words total)
|
|
|
- **Possible Conditions:** markdown table with columns `Condition | Key Indicators | Likelihood` (3–4 rows, concise phrases)
|
|
|
- **Assessment:** markdown table with rows for `Severity`, `Red-Flag Risks`, `Who Needs Urgent Care`
|
|
|
- **Next Steps:** numbered list (max 4 items) combining home monitoring + escalation criteria (include temperature threshold + specific danger signs)
|
|
|
|
|
|
Rules:
|
|
|
- Keep total word count ≤ 220.
|
|
|
- Do not repeat medication, home-remedy, or doctor lists (other agents provide them).
|
|
|
- Reference Tavily evidence implicitly; no raw URLs.
|
|
|
- Never add extra sections or disclaimers.
|
|
|
"""),
|
|
|
("human", f"User symptoms: {user_input}\n\nVerified medical information:\n{tavily_info}\n\nProvide the Structured Symptom Analysis as specified.")
|
|
|
])
|
|
|
|
|
|
try:
|
|
|
chain = prompt | llm
|
|
|
response = chain.invoke({})
|
|
|
state["symptom_analysis"] = response.content
|
|
|
except Exception as e:
|
|
|
state["symptom_analysis"] = f"Error in symptom analysis: {str(e)}"
|
|
|
state["error"] = str(e)
|
|
|
|
|
|
return state
|
|
|
|
|
|
|
|
|
|
|
|
def medication_agent(state: AgentState, llm, tavily_client) -> AgentState:
|
|
|
"""Suggest medications with dosage, side effects, and precautions"""
|
|
|
if not llm:
|
|
|
state["medication_advice"] = "LLM not initialized."
|
|
|
return state
|
|
|
|
|
|
user_input = state["user_input"]
|
|
|
symptom_analysis = state.get("symptom_analysis", "")
|
|
|
|
|
|
|
|
|
tavily_info1 = search_tavily(tavily_client, f"medication names brand names treatment {user_input} prescription drugs", max_results=5)
|
|
|
tavily_info2 = search_tavily(tavily_client, f"medication dosage timing frequency {user_input} how to take", max_results=5)
|
|
|
tavily_info = f"{tavily_info1}\n\n{tavily_info2}"
|
|
|
|
|
|
prompt = ChatPromptTemplate.from_messages([
|
|
|
("system", """You are a clinical pharmacology assistant. Produce a concise, data-backed plan without repetition.
|
|
|
|
|
|
Requirements:
|
|
|
- Begin and end with the exact disclaimer: "⚠️ CONSULT A DOCTOR BEFORE TAKING ANY MEDICATION. This information is for educational purposes only."
|
|
|
- Limit to the top 3 evidence-backed medications (prioritize those appearing in Tavily results). If fewer than 3 are appropriate, list fewer.
|
|
|
- Present medications in a single markdown table with columns: `Medication (Generic / Brand) | When & How to Take | Adult Dose | Key Precautions | Common Side Effects | Rx Status`.
|
|
|
- Timing must include meal relation and time-of-day guidance when available. Keep each cell ≤ 25 words; no sub-bullets.
|
|
|
- After the table, add one short paragraph titled **Missed Dose & Duration** (≤ 40 words) summarizing course length and what to do if a dose is missed.
|
|
|
- Do NOT restate home remedies, lifestyle tips, or doctor information.
|
|
|
- No additional sections or repeated text.
|
|
|
"""),
|
|
|
("human", f"Symptoms: {user_input}\n\nSymptom Analysis:\n{symptom_analysis}\n\nVERIFIED MEDICATION INFORMATION FROM WEB SEARCH:\n{tavily_info}\n\nDeliver the concise table and summary as specified, using only medications supported by the verified information.")
|
|
|
])
|
|
|
|
|
|
try:
|
|
|
chain = prompt | llm
|
|
|
response = chain.invoke({})
|
|
|
state["medication_advice"] = response.content
|
|
|
except Exception as e:
|
|
|
state["medication_advice"] = f"Error in medication advice: {str(e)}"
|
|
|
state["error"] = str(e)
|
|
|
|
|
|
return state
|
|
|
|
|
|
|
|
|
|
|
|
def home_remedies_agent(state: AgentState, llm, tavily_client) -> AgentState:
|
|
|
"""Suggest safe natural remedies and self-care practices"""
|
|
|
if not llm:
|
|
|
state["home_remedies"] = "LLM not initialized."
|
|
|
return state
|
|
|
|
|
|
user_input = state["user_input"]
|
|
|
symptom_analysis = state.get("symptom_analysis", "")
|
|
|
|
|
|
|
|
|
tavily_info = search_tavily(tavily_client, f"home remedies natural treatment {user_input} self-care")
|
|
|
|
|
|
prompt = ChatPromptTemplate.from_messages([
|
|
|
("system", """You are a clinical self-care advisor. Produce a concise, evidence-backed plan without duplication or fluff.
|
|
|
|
|
|
Output format (markdown, ≤ 180 words):
|
|
|
|
|
|
**Home Remedies & Self-Care**
|
|
|
- Markdown table with columns `Remedy | How to Use | Why It Helps | Caution` (max 4 rows, ≤ 20 words per cell)
|
|
|
- **Stop Home Care If:** bullet list of 3 concrete escalation triggers (temperatures/symptoms)
|
|
|
- **Evidence Notes:** bullet list (max 2) citing source type (e.g., "WHO fever guidance", "Mayo Clinic patient sheet") without URLs
|
|
|
|
|
|
Rules:
|
|
|
- Mention only remedies supported by Tavily evidence.
|
|
|
- Do NOT repeat medication dosages, diet plans, or doctor advice.
|
|
|
- No additional sections, disclaimers, or repeated text.
|
|
|
"""),
|
|
|
("human", f"Symptoms: {user_input}\n\nSymptom Analysis:\n{symptom_analysis}\n\nVerified home remedy information:\n{tavily_info}\n\nProvide the structured response exactly as specified.")
|
|
|
])
|
|
|
|
|
|
try:
|
|
|
chain = prompt | llm
|
|
|
response = chain.invoke({})
|
|
|
state["home_remedies"] = response.content
|
|
|
except Exception as e:
|
|
|
state["home_remedies"] = f"Error in home remedies: {str(e)}"
|
|
|
state["error"] = str(e)
|
|
|
|
|
|
return state
|
|
|
|
|
|
|
|
|
|
|
|
def diet_lifestyle_agent(state: AgentState, llm, tavily_client) -> AgentState:
|
|
|
"""Provide diet, meal plans, and lifestyle recommendations"""
|
|
|
if not llm:
|
|
|
state["diet_lifestyle"] = "LLM not initialized."
|
|
|
return state
|
|
|
|
|
|
user_input = state["user_input"]
|
|
|
symptom_analysis = state.get("symptom_analysis", "")
|
|
|
|
|
|
|
|
|
tavily_info = search_tavily(tavily_client, f"diet nutrition lifestyle recommendations {user_input} meal plan")
|
|
|
|
|
|
prompt = ChatPromptTemplate.from_messages([
|
|
|
("system", """You are a clinical nutrition & lifestyle coach. Deliver a succinct plan tailored to the reported symptoms.
|
|
|
|
|
|
Output format (markdown, ≤ 220 words):
|
|
|
- **Hydration & Monitoring:** bullet list (max 3) with fluid targets and temperature-check cadence.
|
|
|
- **Day Plan Table:** table with columns `Meal Time | What to Eat | Why It Helps | Portion Tips` covering Breakfast, Lunch, Dinner, Snacks (≤ 18 words per cell).
|
|
|
- **Foods to Limit:** bullet list (max 4) focusing on items that hinder recovery.
|
|
|
- **Rest & Activity:** bullet list (max 3) for sleep, movement, environment (no overlap with other bullets).
|
|
|
- **Prevention After Recovery:** single sentence (≤ 20 words) highlighting one sustainable habit.
|
|
|
|
|
|
Rules:
|
|
|
- Do NOT repeat medication names, home remedies, or doctor contacts.
|
|
|
- Keep language direct; no duplicated advice.
|
|
|
- Reference Tavily evidence implicitly; no URLs.
|
|
|
"""),
|
|
|
("human", f"Symptoms: {user_input}\n\nSymptom Analysis:\n{symptom_analysis}\n\nVerified diet and lifestyle information:\n{tavily_info}\n\nProvide the structured response exactly as specified.")
|
|
|
])
|
|
|
|
|
|
try:
|
|
|
chain = prompt | llm
|
|
|
response = chain.invoke({})
|
|
|
state["diet_lifestyle"] = response.content
|
|
|
except Exception as e:
|
|
|
state["diet_lifestyle"] = f"Error in diet/lifestyle advice: {str(e)}"
|
|
|
state["error"] = str(e)
|
|
|
|
|
|
return state
|
|
|
|
|
|
|
|
|
|
|
|
def doctor_recommendation_agent(state: AgentState, llm, tavily_client) -> AgentState:
|
|
|
"""Suggest relevant specialists and telemedicine platforms"""
|
|
|
if not llm:
|
|
|
state["doctor_recommendations"] = "LLM not initialized."
|
|
|
return state
|
|
|
|
|
|
user_input = state["user_input"]
|
|
|
symptom_analysis = state.get("symptom_analysis", "")
|
|
|
|
|
|
|
|
|
tavily_info1 = search_tavily(tavily_client, f"doctor names specialists {user_input} healthcare providers hospitals", max_results=5)
|
|
|
tavily_info2 = search_tavily(tavily_client, f"doctor appointment booking consultation hours availability {user_input}", max_results=5)
|
|
|
tavily_info3 = search_tavily(tavily_client, f"telemedicine platforms online doctors {user_input} virtual consultation", max_results=5)
|
|
|
tavily_info = f"{tavily_info1}\n\n{tavily_info2}\n\n{tavily_info3}"
|
|
|
|
|
|
prompt = ChatPromptTemplate.from_messages([
|
|
|
("system", """You are a medical referral coordinator. Provide concise, non-redundant guidance using verified Tavily data.
|
|
|
|
|
|
Output format (markdown, ≤ 220 words):
|
|
|
- **Best Specialist Type & Why:** one sentence.
|
|
|
- **In-Person Options:** table with columns `Doctor / Facility | Specialty | Location | Key Hours | How to Book`. Include up to 4 rows drawn from Tavily results (≤ 18 words per cell, no duplicate facilities).
|
|
|
- **Telemedicine Options:** table with columns `Platform | Available Providers | Service Hours | Booking Method | Typical Wait`. Up to 3 rows.
|
|
|
- **Escalate Immediately If:** bullet list (max 3) noting concrete red-flag scenarios.
|
|
|
- **Emergency Access:** one sentence naming the 24/7 facility from search results.
|
|
|
|
|
|
Rules:
|
|
|
- Do NOT repeat medication, home-remedy, or diet advice.
|
|
|
- Mention each contact number or booking URL only once (omit full URLs; note “call” or “app”).
|
|
|
- Keep wording tight; no repeated phrases across rows.
|
|
|
"""),
|
|
|
("human", f"Symptoms: {user_input}\n\nSymptom Analysis:\n{symptom_analysis}\n\nVERIFIED DOCTOR AND HEALTHCARE INFORMATION FROM WEB SEARCH:\n{tavily_info}\n\nProvide the structured response exactly as specified, extracting real doctor/platform names and hours from the verified information.")
|
|
|
])
|
|
|
|
|
|
try:
|
|
|
chain = prompt | llm
|
|
|
response = chain.invoke({})
|
|
|
state["doctor_recommendations"] = response.content
|
|
|
except Exception as e:
|
|
|
state["doctor_recommendations"] = f"Error in doctor recommendations: {str(e)}"
|
|
|
state["error"] = str(e)
|
|
|
|
|
|
return state
|
|
|
|
|
|
|
|
|
|
|
|
def create_workflow(llm, tavily_client):
|
|
|
"""Create the LangGraph workflow with all agents"""
|
|
|
|
|
|
|
|
|
def symptom_wrapper(state):
|
|
|
return symptom_analyzer_agent(state, llm, tavily_client)
|
|
|
|
|
|
def medication_wrapper(state):
|
|
|
return medication_agent(state, llm, tavily_client)
|
|
|
|
|
|
def home_remedies_wrapper(state):
|
|
|
return home_remedies_agent(state, llm, tavily_client)
|
|
|
|
|
|
def diet_lifestyle_wrapper(state):
|
|
|
return diet_lifestyle_agent(state, llm, tavily_client)
|
|
|
|
|
|
def doctor_recommendation_wrapper(state):
|
|
|
return doctor_recommendation_agent(state, llm, tavily_client)
|
|
|
|
|
|
workflow = StateGraph(AgentState)
|
|
|
|
|
|
|
|
|
workflow.add_node("symptom_analyzer", symptom_wrapper)
|
|
|
workflow.add_node("medication", medication_wrapper)
|
|
|
workflow.add_node("home_remedies", home_remedies_wrapper)
|
|
|
workflow.add_node("diet_lifestyle", diet_lifestyle_wrapper)
|
|
|
workflow.add_node("doctor_recommendation", doctor_recommendation_wrapper)
|
|
|
|
|
|
|
|
|
workflow.set_entry_point("symptom_analyzer")
|
|
|
workflow.add_edge("symptom_analyzer", "medication")
|
|
|
workflow.add_edge("medication", "home_remedies")
|
|
|
workflow.add_edge("home_remedies", "diet_lifestyle")
|
|
|
workflow.add_edge("diet_lifestyle", "doctor_recommendation")
|
|
|
workflow.add_edge("doctor_recommendation", END)
|
|
|
|
|
|
return workflow.compile()
|
|
|
|
|
|
|