DEV Community

Cover image for Building a Multi-Agent Medical Triage System with Angular, Firebase & Genkit
Duncan Maina
Duncan Maina

Posted on • Edited on

Building a Multi-Agent Medical Triage System with Angular, Firebase & Genkit

When building AI applications for healthcare, the stakes are incredibly high. If a standard chatbot hallucinates a recipe, dinner is ruined. If a medical triage bot hallucinates a diagnosis, lives are at risk.

To explore how to safely constrain LLMs in high-risk environments, I built AfyaSenseβ€”an AI-powered medical triage platform.

The goal wasn't just to wrap a frontend around a Gemini prompt. I wanted to build a system that extracts patient symptoms, calculates clinical risk levels (EMERGENCY, CLINIC, or SELF_CARE), and dynamically generates interactive maps to nearby hospitals, all while strictly preventing the AI from going off-topic.

Here is how I used Angular, Firebase Cloud Functions, and a 5-Agent Routing Architecture via Google Genkit to build a safe, hallucination-resistant medical app.

The Problem with "God Prompts" in Healthcare

Initially, developers try to stuff every instruction into a single, massive system prompt: "You are a doctor. Be polite. Extract these symptoms. Format as JSON. Give map coordinates." This "God Prompt" approach fails. The LLM gets confused, breaks JSON schemas, or gets easily jailbroken by users asking off-topic questions.

To solve this, I adopted a Multi-Agent Architecture. By splitting the workload into five specialized, isolated functions, the system became exponentially more reliable and predictable.

The AfyaSense Agent Pipeline

Instead of one AI doing everything, AfyaSense uses a sequential relay race orchestrated by Firebase Genkit.

1. The Traffic Cop (Intent Router)

Before any medical reasoning happens, the first agent simply reads the user's input and classifies the intent. If a user asks about the English Premier League or politics, the Router immediately blocks the request and stops the execution flow. It only allows medical, greeting, or location intents to pass.

2. The Intake Nurse (Data Extraction)

If the query is medical, it is handed to the Nurse agent. Crucially, this agent is forbidden from giving advice. Its only job is to read the chat history and extract exact data points into a strict Zod schema:

const PatientIntakeSchema = z.object({
  symptoms: z.array(z.string()),
  durationDays: z.number().optional(),
  severity: z.number().min(1).max(10).optional(),
  isBleedingOrUnconscious: z.boolean(), // Critical flag
});

Enter fullscreen mode Exit fullscreen mode

3. Hardcoded Emergency Guardrails

This is where traditional engineering meets AI. I don't trust an LLM to decide if severe bleeding is an emergency. Instead, I intercept the parsed data natively in TypeScript. If the isBleedingOrUnconscious flag is true, my backend bypasses the reasoning AI entirely and instantly returns an emergency response.

// Bypassing LLM Reasoning for Emergencies
if (intakeData.isBleedingOrUnconscious) {
  return {
    riskLevel: 'EMERGENCY', 
    reasoning: 'Patient reported critical red-flag symptoms.',
    suggestedAction: 'Go to the nearest emergency room immediately.',
  };
}

Enter fullscreen mode Exit fullscreen mode

4. The Diagnostic Engine (Reasoning)

Only if the patient is stable does the data proceed to the "Doctor" agent (powered by Gemini 2.5 Pro). This agent takes the clean JSON from the Nurse and calculates the differential diagnosis and triage instructions.

Headless Mapping: Avoiding API Costs

One of the coolest features of AfyaSense is its ability to generate interactive Google Maps directly inside the chat UI without relying on expensive Google Places API calls.

To achieve this, I built a headless mapping pipeline in the Angular frontend:

  1. Fetch: The browser grabs the user's raw GPS coordinates.
  2. Scan: The Angular app pings the free, open-source OpenStreetMap (Overpass API) to invisibly scan an 8km radius and extract the names of nearby clinics.
  3. Inject: It takes those raw string names and dynamically constructs free Google Maps Embed URLs.
// 1. Headless fetch from OpenStreetMap
const query = `[out:json];node["amenity"="hospital"](around:8000,${lat},${lng});out body 3;`;
const res = await fetch(`https://overpass-api.de/api/interpreter?data=${encodeURIComponent(query)}`);
const data = await res.json();

const names = data.elements.map((e: any) => e.tags.name).slice(0, 3);

// 2. Build Google Maps iframes safely
this.hospitalMaps = names.map(name => {
  const mapUrl = `https://maps.google.com/maps?q=${encodeURIComponent(name)}+near+${lat},${lng}&t=m&z=14&output=embed`;
  return { 
    name, 
    // Bypassing Angular's strict XSS protections for our generated iframes
    url: this.sanitizer.bypassSecurityTrustResourceUrl(mapUrl) 
  };
});

Enter fullscreen mode Exit fullscreen mode

Tying it all together with Angular

The frontend is built on Angular 18. Because we are dynamically injecting HTML iframes into a chat bubble, Angular's strict Cross-Site Scripting (XSS) protections will normally block the maps. By explicitly running our generated URLs through DomSanitizer, we can render secure, beautiful maps right inside the user's chat flow without ever kicking them to another app.

The Takeaway

You don't need a massive monolithic LLM prompt to build intelligent apps. By breaking down your logic into micro-agents using Genkit and combining AI with traditional programming guardrails, you can build systems that are not only smarter, but significantly safer.

Live Demo: [https://afya-sense.web.app/]

Source Code: [https://github.com/Maina-Duncan/AfyaSense]

Top comments (0)