From 8619ddbdcfc3b658373bc43b753dfa0c1a3c635e Mon Sep 17 00:00:00 2001 From: Deep Koluguri Date: Tue, 26 May 2026 22:36:15 -0400 Subject: [PATCH] feat: Pluggable AI Agent Engine and Copilot config UI --- backend/agents/AgentManager.js | 63 +++++++++++++++++ backend/agents/BaseAgent.js | 35 ++++++++++ backend/agents/BillingAgent.js | 38 ++++++++++ backend/agents/CDSSAgent.js | 41 +++++++++++ backend/agents/FollowUpAgent.js | 36 ++++++++++ backend/agents/InventoryAgent.js | 34 +++++++++ backend/agents/TriageAgent.js | 37 ++++++++++ backend/db.js | 12 ++++ backend/server.js | 35 ++++++++++ settings.html | 116 +++++++++++++++++++++++++++++++ 10 files changed, 447 insertions(+) create mode 100644 backend/agents/AgentManager.js create mode 100644 backend/agents/BaseAgent.js create mode 100644 backend/agents/BillingAgent.js create mode 100644 backend/agents/CDSSAgent.js create mode 100644 backend/agents/FollowUpAgent.js create mode 100644 backend/agents/InventoryAgent.js create mode 100644 backend/agents/TriageAgent.js diff --git a/backend/agents/AgentManager.js b/backend/agents/AgentManager.js new file mode 100644 index 0000000..536829b --- /dev/null +++ b/backend/agents/AgentManager.js @@ -0,0 +1,63 @@ +const { query } = require('../db'); +const BillingAgent = require('./BillingAgent'); +const TriageAgent = require('./TriageAgent'); +const FollowUpAgent = require('./FollowUpAgent'); +const InventoryAgent = require('./InventoryAgent'); +const CDSSAgent = require('./CDSSAgent'); + +class AgentManager { + constructor() { + this.agents = {}; + this.registerAgent('BILLING', BillingAgent); + this.registerAgent('TRIAGE', TriageAgent); + this.registerAgent('FOLLOWUP', FollowUpAgent); + this.registerAgent('INVENTORY', InventoryAgent); + this.registerAgent('CDSS', CDSSAgent); + } + + /** + * Register an agent instance mapped to its type enum. + */ + registerAgent(agentType, agentInstance) { + this.agents[agentType] = agentInstance; + console.log(`[AgentManager] Registered: ${agentType}`); + } + + /** + * The core event dispatcher. + * E.g. dispatch('sharma-clinic', 'BILLING', { clinicalNotes: '...' }) + */ + async dispatch(tenantId, agentType, payload) { + if (!this.agents[agentType]) { + console.error(`[AgentManager] Agent type ${agentType} is not registered in the system.`); + return null; + } + + try { + // Check if clinic has this agent enabled + const dbCheck = await query( + 'SELECT is_active, config FROM agent_configs WHERE tenant_id = $1 AND agent_type = $2', + [tenantId, agentType] + ); + + if (dbCheck.rows.length === 0 || !dbCheck.rows[0].is_active) { + console.log(`[AgentManager] Skipped ${agentType} for tenant ${tenantId} (Not enabled)`); + return null; + } + + const config = dbCheck.rows[0].config; + console.log(`[AgentManager] Dispatched ${agentType} for tenant ${tenantId}`); + + // Execute the agent asynchronously so it doesn't block the main thread + return this.agents[agentType].execute(config, payload).catch(err => { + console.error(`[AgentManager] ${agentType} execution error:`, err); + }); + + } catch (err) { + console.error(`[AgentManager] Dispatch error for ${agentType}:`, err); + } + } +} + +// Singleton instance +module.exports = new AgentManager(); diff --git a/backend/agents/BaseAgent.js b/backend/agents/BaseAgent.js new file mode 100644 index 0000000..22f3ea3 --- /dev/null +++ b/backend/agents/BaseAgent.js @@ -0,0 +1,35 @@ +class BaseAgent { + constructor(name) { + if (this.constructor === BaseAgent) { + throw new Error("Cannot instantiate abstract class BaseAgent"); + } + this.name = name; + } + + /** + * The core execution method that all agents must implement. + * @param {Object} config - The JSON configuration from the database. + * @param {Object} payload - Event specific data. + */ + async execute(config, payload) { + throw new Error(`Agent ${this.name} must implement execute() method.`); + } + + /** + * Helper to mock LLM calls. In production, this would call OpenAI/Gemini. + */ + async mockLLMCall(prompt, responseMocks) { + console.log(`[${this.name}] Sending prompt to LLM...`); + // Simulate network delay + await new Promise(resolve => setTimeout(resolve, 800)); + + // Pick a mock response based on keywords or return default + const text = prompt.toLowerCase(); + for (const [keyword, mockResponse] of Object.entries(responseMocks)) { + if (text.includes(keyword)) return mockResponse; + } + return responseMocks.default || "I cannot process this right now."; + } +} + +module.exports = BaseAgent; diff --git a/backend/agents/BillingAgent.js b/backend/agents/BillingAgent.js new file mode 100644 index 0000000..7fbe307 --- /dev/null +++ b/backend/agents/BillingAgent.js @@ -0,0 +1,38 @@ +const BaseAgent = require('./BaseAgent'); + +class BillingAgent extends BaseAgent { + constructor() { + super('BillingAgent'); + this.responseMocks = { + 'headache': JSON.stringify([{ code: 'R51.9', desc: 'Headache, unspecified', type: 'ICD-10' }, { code: '99213', desc: 'Office Visit (Level 3)', type: 'CPT' }]), + 'fever': JSON.stringify([{ code: 'R50.9', desc: 'Fever, unspecified', type: 'ICD-10' }, { code: '99214', desc: 'Office Visit (Level 4)', type: 'CPT' }]), + 'default': JSON.stringify([{ code: 'Z00.00', desc: 'Encounter for general adult medical examination without abnormal findings', type: 'ICD-10' }, { code: '99212', desc: 'Office Visit (Level 2)', type: 'CPT' }]) + }; + } + + async execute(config, payload) { + const { clinicalSummary } = payload; + + console.log(`[BillingAgent] Analyzing clinical summary for ICD-10/CPT codes...`); + + const prompt = ` + Extract the most likely ICD-10 diagnosis codes and CPT procedure codes from this summary: + ${clinicalSummary} + + Return ONLY a JSON array of objects with { code, desc, type }. + `; + + const llmResponse = await this.mockLLMCall(prompt, this.responseMocks); + + try { + const codes = JSON.parse(llmResponse); + console.log(`[BillingAgent] Suggested Codes: `, codes); + return codes; + } catch (e) { + console.error('[BillingAgent] Failed to parse LLM response', e); + return []; + } + } +} + +module.exports = new BillingAgent(); diff --git a/backend/agents/CDSSAgent.js b/backend/agents/CDSSAgent.js new file mode 100644 index 0000000..764eb93 --- /dev/null +++ b/backend/agents/CDSSAgent.js @@ -0,0 +1,41 @@ +const BaseAgent = require('./BaseAgent'); + +class CDSSAgent extends BaseAgent { + constructor() { + super('CDSSAgent'); + this.responseMocks = { + 'warfarin': JSON.stringify({ hasWarning: true, warningLevel: 'HIGH', message: 'Potential drug interaction: Aspirin and Warfarin increase bleeding risk.' }), + 'penicillin': JSON.stringify({ hasWarning: true, warningLevel: 'CRITICAL', message: 'Patient has a documented allergy to Penicillin!' }), + 'default': JSON.stringify({ hasWarning: false }) + }; + } + + async execute(config, payload) { + const { currentPrescription, patientHistory } = payload; + + console.log(`[CDSSAgent] Checking prescription against patient history for interactions...`); + + const prompt = ` + Check this new prescription: "${currentPrescription}" + Against patient history: "${patientHistory}" + Return a JSON object: { "hasWarning": boolean, "warningLevel": "LOW/MEDIUM/HIGH/CRITICAL", "message": "..." } + `; + + const llmResponse = await this.mockLLMCall(prompt, this.responseMocks); + + try { + const result = JSON.parse(llmResponse); + if (result.hasWarning) { + console.log(`[CDSSAgent] โš ๏ธ ${result.warningLevel} WARNING: ${result.message}`); + } else { + console.log(`[CDSSAgent] Safe to prescribe.`); + } + return result; + } catch (e) { + console.error('[CDSSAgent] Failed to parse LLM response', e); + return { hasWarning: false }; + } + } +} + +module.exports = new CDSSAgent(); diff --git a/backend/agents/FollowUpAgent.js b/backend/agents/FollowUpAgent.js new file mode 100644 index 0000000..9b5b1c6 --- /dev/null +++ b/backend/agents/FollowUpAgent.js @@ -0,0 +1,36 @@ +const BaseAgent = require('./BaseAgent'); + +class FollowUpAgent extends BaseAgent { + constructor() { + super('FollowUpAgent'); + this.responseMocks = { + 'default': JSON.stringify({ message: "Hello! It has been a few days since your visit. How are you feeling today?" }) + }; + } + + async execute(config, payload) { + const { patientName, phone, daysSinceVisit, context } = payload; + + console.log(`[FollowUpAgent] Generating follow-up for ${patientName} (${daysSinceVisit} days post-visit)`); + + const prompt = ` + Draft a friendly, empathetic WhatsApp follow-up message for patient ${patientName}. + Context of last visit: "${context}" + Return a JSON object: { "message": "..." } + `; + + const llmResponse = await this.mockLLMCall(prompt, this.responseMocks); + + try { + const result = JSON.parse(llmResponse); + console.log(`[FollowUpAgent] Message to send: ${result.message}`); + // Here we would integrate with WhatsApp API to actually send it + return result; + } catch (e) { + console.error('[FollowUpAgent] Failed to parse LLM response', e); + return { message: "Hi! Just checking in on your health. Let us know if you need anything." }; + } + } +} + +module.exports = new FollowUpAgent(); diff --git a/backend/agents/InventoryAgent.js b/backend/agents/InventoryAgent.js new file mode 100644 index 0000000..7a33b21 --- /dev/null +++ b/backend/agents/InventoryAgent.js @@ -0,0 +1,34 @@ +const BaseAgent = require('./BaseAgent'); + +class InventoryAgent extends BaseAgent { + constructor() { + super('InventoryAgent'); + } + + async execute(config, payload) { + const { itemName, currentStock, salesVelocity } = payload; + + // config can hold the threshold + const threshold = config.low_stock_threshold || 10; + + console.log(`[InventoryAgent] Checking stock for ${itemName} (Current: ${currentStock}, Threshold: ${threshold})`); + + if (currentStock < threshold) { + console.log(`[InventoryAgent] ๐Ÿšจ Stock is below threshold! Generating purchase order...`); + + // In a real scenario, this would draft an email or WhatsApp to the vendor + const orderQty = (config.restock_amount || 50) + (salesVelocity || 0); + + return { + action: 'DRAFT_PO', + item: itemName, + quantity: orderQty, + urgency: currentStock === 0 ? 'CRITICAL' : 'HIGH' + }; + } + + return { action: 'NONE', status: 'Healthy' }; + } +} + +module.exports = new InventoryAgent(); diff --git a/backend/agents/TriageAgent.js b/backend/agents/TriageAgent.js new file mode 100644 index 0000000..1d211e4 --- /dev/null +++ b/backend/agents/TriageAgent.js @@ -0,0 +1,37 @@ +const BaseAgent = require('./BaseAgent'); + +class TriageAgent extends BaseAgent { + constructor() { + super('TriageAgent'); + this.responseMocks = { + 'chest pain': JSON.stringify({ category: "URGENT", followUp: "Are you also experiencing shortness of breath or arm pain?" }), + 'fever': JSON.stringify({ category: "MODERATE", followUp: "What is your current temperature?" }), + 'default': JSON.stringify({ category: "ROUTINE", followUp: "How long have you been feeling this way?" }) + }; + } + + async execute(config, payload) { + const { symptoms } = payload; + + console.log(`[TriageAgent] Analyzing symptoms: "${symptoms}"`); + + const prompt = ` + Analyze these symptoms: "${symptoms}" + Determine the urgency (URGENT, MODERATE, ROUTINE) and suggest a follow-up question. + Return a JSON object: { "category": "...", "followUp": "..." } + `; + + const llmResponse = await this.mockLLMCall(prompt, this.responseMocks); + + try { + const result = JSON.parse(llmResponse); + console.log(`[TriageAgent] Triage Result: `, result); + return result; + } catch (e) { + console.error('[TriageAgent] Failed to parse LLM response', e); + return { category: 'ROUTINE', followUp: 'Please describe your symptoms in more detail.' }; + } + } +} + +module.exports = new TriageAgent(); diff --git a/backend/db.js b/backend/db.js index ebd9398..e7d6cc4 100644 --- a/backend/db.js +++ b/backend/db.js @@ -74,10 +74,22 @@ CREATE TABLE IF NOT EXISTS transactions ( created_at TIMESTAMPTZ DEFAULT now() ); +CREATE TABLE IF NOT EXISTS agent_configs ( + id SERIAL PRIMARY KEY, + tenant_id TEXT REFERENCES tenants(id) ON DELETE CASCADE, + agent_type TEXT NOT NULL CHECK (agent_type IN ('BILLING', 'TRIAGE', 'FOLLOWUP', 'INVENTORY', 'CDSS')), + is_active BOOLEAN DEFAULT false, + config JSONB DEFAULT '{}', + created_at TIMESTAMPTZ DEFAULT now(), + UNIQUE(tenant_id, agent_type) +); + CREATE INDEX IF NOT EXISTS idx_users_tenant ON users(tenant_id); CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); CREATE INDEX IF NOT EXISTS idx_pending_status ON pending_registrations(status); CREATE INDEX IF NOT EXISTS idx_transactions_order ON transactions(order_id); +CREATE INDEX IF NOT EXISTS idx_agent_configs_tenant ON agent_configs(tenant_id); + -- Migrations for existing DBs ALTER TABLE tenants ADD COLUMN IF NOT EXISTS services_catalog JSONB DEFAULT '[]'; diff --git a/backend/server.js b/backend/server.js index dad0d05..fbfa202 100644 --- a/backend/server.js +++ b/backend/server.js @@ -13,6 +13,7 @@ const { query, initSchema } = require('./db'); const { signToken, requireAuth } = require('./authMiddleware'); const Razorpay = require('razorpay'); const { upload, initStorage, uploadFile } = require('./storage'); +const AgentManager = require('./agents/AgentManager'); const razorpayInstance = new Razorpay({ key_id: process.env.RAZORPAY_KEY_ID || 'rzp_test_dummy_key_id', @@ -463,6 +464,40 @@ app.post('/api/super/pending/:id/reject', requireAuth('SUPER_ADMIN'), async (req } }); +// โ”€โ”€ AI AGENTS CONFIGURATION โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +app.get('/api/clinic/agents', requireAuth('OWNER'), async (req, res) => { + try { + const result = await query( + 'SELECT agent_type, is_active, config FROM agent_configs WHERE tenant_id = $1', + [req.user.tenantId] + ); + res.json(result.rows); + } catch (err) { + console.error('[Agents API]', err); + res.status(500).json({ error: 'Failed to fetch agent configs' }); + } +}); + +app.patch('/api/clinic/agents/:type', requireAuth('OWNER'), async (req, res) => { + const { type } = req.params; + const { isActive, config } = req.body; + + try { + await query( + `INSERT INTO agent_configs (tenant_id, agent_type, is_active, config) + VALUES ($1, $2, $3, $4::jsonb) + ON CONFLICT (tenant_id, agent_type) + DO UPDATE SET is_active = EXCLUDED.is_active, config = EXCLUDED.config`, + [req.user.tenantId, type.toUpperCase(), isActive, JSON.stringify(config || {})] + ); + res.json({ success: true }); + } catch (err) { + console.error('[Agents API Update]', err); + res.status(500).json({ error: 'Failed to update agent config' }); + } +}); + // โ”€โ”€ CLINIC OWNER ROUTES โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ /** diff --git a/settings.html b/settings.html index 99dc45a..332e6a3 100644 --- a/settings.html +++ b/settings.html @@ -106,6 +106,69 @@ + +
+

AI Copilots & Automation

+

Enable and configure autonomous agents to assist with your clinic operations.

+ +
+ +
+
+ ๐Ÿงพ Automated Medical Billing + +
+

Automatically extracts ICD-10 and CPT codes from Zen Scribe clinical summaries.

+
+ +
+
+ ๐Ÿ’ฌ Patient Triage Agent + +
+

Listens to WhatsApp messages and categorizes patient symptoms by urgency.

+
+ +
+
+ ๐Ÿ”„ Post-Visit Follow-Up + +
+

Automatically sends empathetic check-in messages to patients a few days after their visit.

+
+ +
+
+ ๐Ÿ“ฆ Pharmacy Inventory Agent + +
+

Monitors pharmacy stock and automatically drafts purchase orders when supplies run low.

+
+ +
+
+ ๐Ÿง  Clinical Decision Support + +
+

Analyzes prescriptions against patient history to warn doctors of drug-drug interactions.

+
+
+