Integrate dynamic AI agent with Ollama
Build Curio HMS / build-and-deploy (push) Successful in 1m16s
Details
Build Curio HMS / build-and-deploy (push) Successful in 1m16s
Details
This commit is contained in:
parent
75562d6ffe
commit
abd0940220
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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: /
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue