Integrate dynamic AI agent with Ollama
Build Curio HMS / build-and-deploy (push) Successful in 1m16s Details

This commit is contained in:
Deep Koluguri 2026-05-25 23:34:45 -04:00
parent 75562d6ffe
commit abd0940220
6 changed files with 128 additions and 97 deletions

View File

@ -10,21 +10,29 @@ class BotLogic {
}
async handleMessage(patientId, message) {
const context = this.states[patientId] || {};
if (!this.states[patientId]) {
this.states[patientId] = { transcript: [] };
}
const context = this.states[patientId];
context.transcript.push({ role: 'user', content: message });
// Delegate to the Agent
const response = await clinicAgent.processMessage(patientId, message, context);
const response = await clinicAgent.processMessage(patientId, context.transcript);
// Update local context/state based on agent response
if (response.nextStep) {
this.states[patientId] = { lastStep: response.nextStep };
} else {
// Reset or keep context as needed
// Update context based on agent response
if (response.reply) {
context.transcript.push({ role: 'assistant', content: response.reply });
}
// Trim history to last 10 messages
if (context.transcript.length > 10) {
context.transcript = context.transcript.slice(-10);
}
return {
reply: response.reply,
buttons: response.buttons || []
buttons: [] // Deprecated in favor of dynamic conversation
};
}
}

View File

@ -7,110 +7,85 @@ const patientManager = require('./patientManager');
const queueManager = require('./queueManager');
const pharmacyAgent = require('./pharmacyAgent');
const configManager = require('./configManager');
const { OpenAI } = require('openai');
const openai = new OpenAI({
baseURL: process.env.LLM_BASE_URL || 'http://ollama.ai-core.svc.cluster.local:11434/v1',
apiKey: process.env.LLM_API_KEY || 'ollama', // Ollama doesn't require a real key
});
const LLM_MODEL = process.env.LLM_MODEL || 'qwen2.5:3b';
class ClinicAgent {
constructor() {
this.systemPrompt = `
You are 'Curio AI', the assistant for Curio Health.
Your goal is to help patients book appointments, manage family members, and check clinic status.
Your goal is to help patients book appointments.
Be extremely concise, friendly, and natural. Do not use markdown.
To book an appointment, you MUST collect:
1. Department (e.g. Cardiology, Neurology, General)
2. Date (Format as YYYY-MM-DD or today/tomorrow)
3. Time (Format as HH:MM AM/PM)
If any of these 3 pieces of information are missing, naturally ask the user for them.
Do NOT output JSON until you have ALL THREE pieces of information.
TOOLS:
- list_family: Shows all members linked to the phone.
- add_member: Registers a new family member name.
- book_today: Books an appointment for today.
- check_status: Checks current queue position.
RULES:
1. Always be polite and professional.
2. If a patient asks for someone not in their family list, offer to add them.
3. If multiple people have same name, ask for clarification.
If you have all three pieces of information, you MUST reply with ONLY a JSON block like this (no other text):
{"action": "BOOK", "department": "Cardiology", "date": "2026-05-26", "time": "10:00"}
`;
}
async processMessage(phone, message, context = {}) {
const text = message.toLowerCase();
console.log(`[Agent] Processing for ${phone}: "${message}"`);
async processMessage(phone, transcript) {
console.log(`[Agent] Processing for ${phone} via Ollama`);
// Mock LLM Intent Extraction & Tool Execution
// In a real app, this would be: const response = await llm.call({ prompt: ..., tools: [...] });
const family = patientManager.getProfiles(phone);
// Scenario 1: Greeting + Profile Check
if (text.includes("hi") || text.includes("hello")) {
if (family.length === 0) {
return {
reply: "Hello! I'm Curio AI. I don't see a profile for this number yet. What is your full name so I can get you started?",
nextStep: "AWAIT_REGISTRATION"
};
}
return {
reply: `Hello again! I see profiles for ${family.map(p => p.name).join(', ')}. Who can I help you with today?`,
buttons: [...family.map(p => p.name), "+ Add New Member"]
};
}
try {
const response = await openai.chat.completions.create({
model: LLM_MODEL,
messages: [
{ role: 'system', content: this.systemPrompt },
...transcript
],
temperature: 0.2,
});
// Scenario 2: Adding a Member
if (context.lastStep === "AWAIT_REGISTRATION" || text.includes("add new member")) {
if (text.includes("add new member")) {
return { reply: "Sure! What is the name of the family member you'd like to add?" };
}
const newId = patientManager.addProfile(phone, { name: message, relation: "Family" });
return {
reply: `Perfect! I've added ${message} to your family account. Would you like to book an appointment for them today?`,
buttons: ["Book for Today", "Check Queue"]
};
}
const replyContent = response.choices[0].message.content.trim();
// Scenario 3: Booking Intent
if (text.includes("book") || text.includes("today")) {
return {
reply: "I can help with that. Which time works best for you?",
buttons: ["10:00 AM", "11:00 AM", "06:00 PM"],
nextStep: "AWAIT_TIME"
};
}
// Scenario 3.1: Handling time selection
if (context.lastStep === "AWAIT_TIME") {
let time = "";
if (text.includes("1") || text.includes("10")) time = "10:00";
else if (text.includes("2") || text.includes("11")) time = "11:00";
else if (text.includes("3") || text.includes("6") || text.includes("06")) time = "18:00";
if (time) {
const today = new Date().toISOString().split('T')[0];
const result = await queueManager.bookOnline(today, time, phone, 'sharma-clinic');
if (result.success) {
return { reply: `Awesome! Your token is ${result.token}. You should receive a confirmation message shortly.` };
} else {
return { reply: `Sorry, ${result.message}` };
// Try to parse JSON intent
try {
// Remove potential markdown code blocks if the LLM wrapped it
const cleanJson = replyContent.replace(/```json/g, '').replace(/```/g, '').trim();
const intent = JSON.parse(cleanJson);
if (intent.action === 'BOOK') {
// Extract HH:MM
let timeVal = "10:00"; // fallback
const timeMatch = intent.time.match(/(\d{1,2}:\d{2})/);
if (timeMatch) timeVal = timeMatch[1];
// Parse date
let dateVal = new Date().toISOString().split('T')[0];
if (intent.date && intent.date.match(/^\d{4}-\d{2}-\d{2}$/)) {
dateVal = intent.date;
}
// Book the token using Dr. Sharma's Cardiology ("sharma-clinic")
// In a real system, we'd map intent.department to tenantId
const result = await queueManager.bookOnline(dateVal, timeVal, phone, 'sharma-clinic');
if (result.success) {
return { reply: `Awesome! Your appointment for ${intent.department} is booked. Your token is ${result.token}. You should receive a confirmation message shortly.` };
} else {
return { reply: `Sorry, ${result.message}` };
}
}
} catch (e) {
// Not JSON, just regular conversational text
return { reply: replyContent };
}
return { reply: replyContent };
} catch (error) {
console.error('[ClinicAgent] LLM Error:', error.message);
return { reply: "I'm having trouble connecting to my AI brain right now. Please try again in a moment." };
}
// Scenario 3.5: Medicine Query
if (text.includes("have") || text.includes("medicine") || text.includes("syrup") || text.includes("tablet")) {
const pharmacyReply = pharmacyAgent.handlePatientQuery(message);
return {
reply: "💊 *Pharmacy Update*: " + pharmacyReply,
buttons: ["Book for Today", "Check Other Meds"]
};
}
// Scenario 4: Natural Language Match for Family Members
const matchedMember = family.find(p => text.includes(p.name.toLowerCase()));
if (matchedMember) {
return {
reply: `I've switched context to ${matchedMember.name}. What would you like to do for them?`,
buttons: ["Book for Today", "View History"]
};
}
// Fallback to "Smart" LLM reply
return {
reply: "I'm not sure how to help with that yet, but I'm learning! You can ask me to book an appointment or add a family member."
};
}
}

View File

@ -34,6 +34,10 @@ spec:
value: "rN5RpKAbFVCv0G45ehluf1JbTWt2dgNW"
- name: TWILIO_WHATSAPP_FROM
value: "whatsapp:+14155238886"
- name: LLM_BASE_URL
value: "http://ollama.ai-core.svc.cluster.local:11434/v1"
- name: LLM_MODEL
value: "qwen2.5:3b"
livenessProbe:
httpGet:
path: /

21
node_modules/.package-lock.json generated vendored
View File

@ -1163,6 +1163,27 @@
"node": ">= 0.8"
}
},
"node_modules/openai": {
"version": "6.39.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-6.39.0.tgz",
"integrity": "sha512-O61LIsimY3acVabwvomwFhwrnN36yvHY2quIfy9keEcFytGgWeV35yLHQ6NVMLSBxRpHmcg2yuhCnlu2HT4pLQ==",
"license": "Apache-2.0",
"bin": {
"openai": "bin/cli"
},
"peerDependencies": {
"ws": "^8.18.0",
"zod": "^3.25 || ^4.0"
},
"peerDependenciesMeta": {
"ws": {
"optional": true
},
"zod": {
"optional": true
}
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",

22
package-lock.json generated
View File

@ -13,6 +13,7 @@
"dotenv": "^17.4.2",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.3",
"openai": "^6.39.0",
"pg": "^8.21.0",
"twilio": "^6.0.2",
"uuid": "^14.0.0"
@ -1177,6 +1178,27 @@
"node": ">= 0.8"
}
},
"node_modules/openai": {
"version": "6.39.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-6.39.0.tgz",
"integrity": "sha512-O61LIsimY3acVabwvomwFhwrnN36yvHY2quIfy9keEcFytGgWeV35yLHQ6NVMLSBxRpHmcg2yuhCnlu2HT4pLQ==",
"license": "Apache-2.0",
"bin": {
"openai": "bin/cli"
},
"peerDependencies": {
"ws": "^8.18.0",
"zod": "^3.25 || ^4.0"
},
"peerDependenciesMeta": {
"ws": {
"optional": true
},
"zod": {
"optional": true
}
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",

View File

@ -13,6 +13,7 @@
"dotenv": "^17.4.2",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.3",
"openai": "^6.39.0",
"pg": "^8.21.0",
"twilio": "^6.0.2",
"uuid": "^14.0.0"