Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions submissions/unfazed/code/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# secrets & local env (NEVER COMMIT)
.env
.env.*
!.env.example
*.local

# Firebase configs
google-services.json
google-services (2).json
google-services*.json

# Databases & local output logs
*.db
*.log
__pycache__/
*.pyc
70 changes: 70 additions & 0 deletions submissions/unfazed/code/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Sahayak — Voice-First Rural Healthcare Triage Agent

Sahayak is a multi-agent system that conducts a voice-based symptom interview in Telugu or Hindi, triages urgency against clinical protocols, books care automatically, and escalates to a human doctor the moment confidence drops — built and hardened end-to-end using Mutagent's specify → build → evaluate → diagnose → optimize lifecycle.

Built for **Mutagent Challenge Track — HackIndia Spark 11**.

## System Architecture

```
User (voice, Telugu/Hindi/English)
┌─────────────────────┐
│ Intake Agent │ → speech-to-text, extracts structured symptoms
└─────────┬───────────┘
┌─────────────────────┐
│ Triage Agent │ → matches symptoms to protocol, outputs urgency
│ │ tier + confidence score
└─────────┬───────────┘
confidence check
┌────┴────┐
▼ ▼
HIGH conf LOW conf
│ │
▼ ▼
┌──────────┐ ┌────────────────┐
│Scheduling│ │Escalation Agent│
│ Agent │ │ (human doctor │
│ │ │ handoff) │
└────┬─────┘ └────────┬───────┘
▼ ▼
Booking API On-call queue
│ │
└───────┬────────┘
Text-to-speech reply
+ SMS/WhatsApp confirmation
```

## Setup and Installation

1. Create a virtual environment and install dependencies:
```bash
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```
2. Configure your API key:
```bash
cp .env.example .env
# Edit .env and add your GEMINI_API_KEY
```
3. Run the interactive orchestrator:
```bash
export PYTHONPATH=.
python orchestrator.py
```

## Evaluation and Mutagent ADL

To run the evaluation script (Mutagent `EVALUATE` phase) over the 40-case dataset, yielding a detailed scorecard tracking **confidence calibration**, **escalation trigger accuracy**, and **false-negative emergency rates**:
```bash
source venv/bin/activate
export PYTHONPATH=.
python eval/evaluate.py
```

The system uses Gemini 2.5 Flash as the underlying model.
81 changes: 81 additions & 0 deletions submissions/unfazed/code/agents/escalation/escalation_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import json
import google.generativeai as genai
from pydantic import BaseModel
import os

# Ensure API key is configured
genai.configure(api_key=os.environ.get("GEMINI_API_KEY", ""))

class EscalationOutput(BaseModel):
case_id: str
symptoms_summary: str
urgency_tier: str
confidence: float
escalation_reason: str
patient_contact: str
message_to_patient: str

def run_escalation_agent_fallback(intake_data: dict, triage_data: dict, patient_contact: str, escalation_trigger: str) -> str:
import json

symptoms = ", ".join(intake_data.get("patient_reported_symptoms", ["unknown symptoms"]))
urgency = triage_data.get("urgency_tier", "routine")
confidence = triage_data.get("confidence", 0.5)

if urgency == "emergency":
msg = "We understand you are experiencing severe symptoms. A doctor and ASHA worker have been alerted and will contact you immediately."
elif urgency == "urgent_24h":
msg = "Your symptoms have been registered. A healthcare provider will contact you within the next 24 hours."
else:
msg = "Your symptoms have been registered. A healthcare provider will contact you shortly to review your case."

fallback_output = {
"case_id": "fallback-case",
"symptoms_summary": f"Patient reported: {symptoms}",
"urgency_tier": urgency,
"confidence": confidence,
"escalation_reason": escalation_trigger,
"patient_contact": patient_contact,
"message_to_patient": msg
}
return json.dumps(fallback_output)

def run_escalation_agent(intake_data: dict, triage_data: dict, patient_contact: str, escalation_trigger: str) -> str:
"""
Runs the Escalation Agent using Gemini, falling back to rule-based formatter on 429/quota errors.
"""
system_instruction = """
You are the Escalation Agent for Sahayak — the safety net.

TASK:
1. Package a case summary for the on-call doctor.
2. Write a plain language message to the patient telling them a doctor will call them back,
with an expected timeframe based on urgency_tier.

RULES:
- ALWAYS escalate on ambiguity. Never let low confidence pass through silently.
- Be empathetic and clear in the message_to_patient.
"""

prompt = f"""
Intake Data: {json.dumps(intake_data, indent=2)}
Triage Data: {json.dumps(triage_data, indent=2)}
Patient Contact: {patient_contact}
Escalation Trigger: {escalation_trigger}
"""

try:
model = genai.GenerativeModel(
model_name="gemini-2.5-flash",
system_instruction=system_instruction,
generation_config=genai.GenerationConfig(
response_mime_type="application/json",
response_schema=EscalationOutput,
temperature=0.2
)
)
response = model.generate_content(prompt)
return response.text
except Exception as e:
print(f"[EscalationAgent] Warning, falling back to rule-based escalation: {e}")
return run_escalation_agent_fallback(intake_data, triage_data, patient_contact, escalation_trigger)
20 changes: 20 additions & 0 deletions submissions/unfazed/code/agents/escalation/escalation_spec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: EscalationAgent
version: 1.0.0
description: Escalates high-urgency or high-ambiguity cases to a human doctor.
orchestrator:
type: custom
entrypoint: agents.escalation.escalation_agent.run_escalation_agent
stages:
- name: SPEC
runner: default
- name: BUILD
runner: default
- name: EVALUATE
runner: default
- name: DIAGNOSE
runner: default
llm: gemini-1.5-flash
- name: OPTIMIZE
runner: default
memory:
enabled: false
46 changes: 46 additions & 0 deletions submissions/unfazed/code/agents/evaluator/evaluator_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import json
import google.generativeai as genai
import os

genai.configure(api_key=os.environ.get("GEMINI_API_KEY", ""))

def run_evaluator_agent_fallback(intake_data: dict, triage_data: dict) -> str:
symptoms = ", ".join(intake_data.get("patient_reported_symptoms", ["unknown symptoms"]))
urgency = triage_data.get("urgency_tier", "routine")
confidence = triage_data.get("confidence", 0.5)
red_flags = ", ".join(intake_data.get("red_flag_keywords", []))

summary = f"- Patient presents with: {symptoms}.\n"
if red_flags:
summary += f"- ⚠️ RED FLAGS detected: {red_flags}.\n"
summary += f"- Urgent Routing: {urgency.upper()} (Confidence: {int(confidence*100)}%)."
return summary

def run_evaluator_agent(intake_data: dict, triage_data: dict) -> str:
"""
Evaluator Agent summarizes the case for the doctor/hospital dashboard.
"""
system_instruction = """
You are the Evaluator Agent for Sahayak.
Your job is to read the patient's intake data (symptoms, age group, severity)
and the triage data (urgency tier, confidence), and write a concise, medical-grade
summary for the doctor.

Keep it strictly to 2-3 short bullet points. Highlight red flags.
"""

prompt = f"""
Intake Data: {json.dumps(intake_data)}
Triage Data: {json.dumps(triage_data)}
"""

try:
model = genai.GenerativeModel(
model_name="gemini-2.5-flash",
system_instruction=system_instruction
)
response = model.generate_content(prompt)
return response.text
except Exception as e:
print(f"[EvaluatorAgent] Warning, falling back to rule-based summary: {e}")
return run_evaluator_agent_fallback(intake_data, triage_data)
130 changes: 130 additions & 0 deletions submissions/unfazed/code/agents/intake/intake_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import json
import google.generativeai as genai
from pydantic import BaseModel
import os
from typing import Literal

# Ensure API key is configured
genai.configure(api_key=os.environ.get("GEMINI_API_KEY", ""))

class IntakeOutput(BaseModel):
patient_reported_symptoms: list[str]
duration: str
severity_self_rated: Literal["mild", "moderate", "severe", "unknown"]
red_flag_keywords: list[str]
age_group: Literal["child", "adult", "elderly", "unknown"]
language_detected: Literal["te", "hi", "en", "mixed"]
ready_for_triage: bool
clarifying_question: str

def run_intake_agent_fallback(transcribed_text: str, current_state: dict = None) -> str:
import json
text_lower = transcribed_text.lower()

# 1. Red flag keywords check
red_flag_terms = ["chest pain", "breathless", "unconscious", "bleeding", "stroke", "seizure", "infant fever", "gasping", "head trauma"]
detected_red_flags = [term for term in red_flag_terms if term in text_lower]

# If infant is mentioned
is_infant = any(x in text_lower for x in ["baby", "infant", "6-week", "month", "bacha"])
if is_infant and "fever" in text_lower:
detected_red_flags.append("infant fever")

# 2. Extract symptoms
symptoms = []
if "chest pain" in text_lower: symptoms.append("chest pain")
if "fever" in text_lower: symptoms.append("fever")
if "cough" in text_lower: symptoms.append("cough")
if "cold" in text_lower: symptoms.append("cold")
if "pain" in text_lower and "chest" not in text_lower: symptoms.append("pain")
if any(x in text_lower for x in ["breath", "saans", "asthma"]): symptoms.append("breathing difficulty")
if len(symptoms) == 0:
symptoms.append(transcribed_text)

# 3. Severity self rated
severity = "mild"
if len(detected_red_flags) > 0 or "severe" in text_lower or "bahut" in text_lower or "high" in text_lower:
severity = "severe"
elif "moderate" in text_lower:
severity = "moderate"

# 4. Age group
age_group = "adult"
if is_infant or "child" in text_lower or "kid" in text_lower or "beta" in text_lower:
age_group = "child"
elif any(x in text_lower for x in ["old", "elderly", "dadaji", "nanaji", "grandfather", "grandmother"]):
age_group = "elderly"

# 5. Language
from tools.intake_tools import detect_language
language = detect_language(transcribed_text)

# 6. JSON output
fallback_output = {
"patient_reported_symptoms": symptoms,
"duration": "1 day" if any(x in text_lower for x in ["today", "morning", "aaj"]) else "unknown",
"severity_self_rated": severity,
"red_flag_keywords": list(set(detected_red_flags)),
"age_group": age_group,
"language_detected": language,
"ready_for_triage": True,
"clarifying_question": ""
}
return json.dumps(fallback_output)

def run_intake_agent(transcribed_text: str, current_state: dict = None) -> str:
"""
Runs the Intake Agent using Gemini, falling back to rule-based parser on 429/quota errors.
"""
system_instruction = """
You are the Intake Agent for Sahayak, a rural healthcare triage system.

INPUT: transcribed patient speech (Telugu, Hindi, or English), possibly
mixed-language or grammatically informal.

TASK:
1. Extract structured symptom data from free-form speech.
2. Ask ONE clarifying question at a time if critical fields are missing.
Never ask more than 3 clarifying questions total.
3. Output strictly in this JSON schema once you have enough information or need to ask a question.
If you don't need to ask a question, leave clarifying_question empty.

RULES:
- If any red_flag_keyword is detected (chest pain, severe bleeding,
unconsciousness, breathing difficulty, stroke symptoms), set
ready_for_triage=true IMMEDIATELY even with incomplete data and flag
urgent=true. Do not keep asking questions in an emergency.
- Never diagnose. Never suggest medication. You only extract structure.
- Keep spoken responses under 2 sentences — this is a voice interface.
- If patient's language is unclear, default to the language they used.
"""

prompt = f"""
New Input: {transcribed_text}

Current State (if any):
{json.dumps(current_state or {}, indent=2)}
"""

try:
model = genai.GenerativeModel(
model_name="gemini-2.5-flash",
system_instruction=system_instruction,
generation_config=genai.GenerationConfig(
response_mime_type="application/json",
response_schema=IntakeOutput,
temperature=0.2
)
)
response = model.generate_content(prompt)
return response.text
except Exception as e:
print(f"[IntakeAgent] Warning, falling back to rule-based intake: {e}")
return run_intake_agent_fallback(transcribed_text, current_state)

if __name__ == "__main__":
from tools.intake_tools import detect_language
text = "mera chest pain ho raha hai aur saans lene mein problem hai"
lang = detect_language(text)
print(f"Language: {lang}")
print(run_intake_agent(text))
26 changes: 26 additions & 0 deletions submissions/unfazed/code/agents/intake/intake_spec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: IntakeAgent
version: 1.0.0
description: Intake agent that processes speech, extracts symptoms and decides if ready for triage.
orchestrator:
type: custom
entrypoint: agents.intake.intake_agent.run_intake_agent
stages:
- name: SPEC
runner: default
- name: BUILD
runner: default
- name: EVALUATE
runner: default
- name: DIAGNOSE
runner: default
llm: gemini-1.5-flash
- name: OPTIMIZE
runner: default
evaluation:
dataset: eval/intake_dataset.json
scorecard: eval/scorecard_intake.json
criteria:
accuracy: 0.90
memory:
enabled: true
type: persistent
Loading