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) {
|
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
|
// 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
|
// Update context based on agent response
|
||||||
if (response.nextStep) {
|
if (response.reply) {
|
||||||
this.states[patientId] = { lastStep: response.nextStep };
|
context.transcript.push({ role: 'assistant', content: response.reply });
|
||||||
} else {
|
}
|
||||||
// Reset or keep context as needed
|
|
||||||
|
// Trim history to last 10 messages
|
||||||
|
if (context.transcript.length > 10) {
|
||||||
|
context.transcript = context.transcript.slice(-10);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
reply: response.reply,
|
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 queueManager = require('./queueManager');
|
||||||
const pharmacyAgent = require('./pharmacyAgent');
|
const pharmacyAgent = require('./pharmacyAgent');
|
||||||
const configManager = require('./configManager');
|
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 {
|
class ClinicAgent {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.systemPrompt = `
|
this.systemPrompt = `
|
||||||
You are 'Curio AI', the assistant for Curio Health.
|
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)
|
||||||
|
|
||||||
TOOLS:
|
If any of these 3 pieces of information are missing, naturally ask the user for them.
|
||||||
- list_family: Shows all members linked to the phone.
|
Do NOT output JSON until you have ALL THREE pieces of information.
|
||||||
- add_member: Registers a new family member name.
|
|
||||||
- book_today: Books an appointment for today.
|
|
||||||
- check_status: Checks current queue position.
|
|
||||||
|
|
||||||
RULES:
|
If you have all three pieces of information, you MUST reply with ONLY a JSON block like this (no other text):
|
||||||
1. Always be polite and professional.
|
{"action": "BOOK", "department": "Cardiology", "date": "2026-05-26", "time": "10:00"}
|
||||||
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.
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async processMessage(phone, message, context = {}) {
|
async processMessage(phone, transcript) {
|
||||||
const text = message.toLowerCase();
|
console.log(`[Agent] Processing for ${phone} via Ollama`);
|
||||||
console.log(`[Agent] Processing for ${phone}: "${message}"`);
|
|
||||||
|
|
||||||
// Mock LLM Intent Extraction & Tool Execution
|
try {
|
||||||
// In a real app, this would be: const response = await llm.call({ prompt: ..., tools: [...] });
|
const response = await openai.chat.completions.create({
|
||||||
|
model: LLM_MODEL,
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: this.systemPrompt },
|
||||||
|
...transcript
|
||||||
|
],
|
||||||
|
temperature: 0.2,
|
||||||
|
});
|
||||||
|
|
||||||
const family = patientManager.getProfiles(phone);
|
const replyContent = response.choices[0].message.content.trim();
|
||||||
|
|
||||||
// Scenario 1: Greeting + Profile Check
|
// Try to parse JSON intent
|
||||||
if (text.includes("hi") || text.includes("hello")) {
|
try {
|
||||||
if (family.length === 0) {
|
// Remove potential markdown code blocks if the LLM wrapped it
|
||||||
return {
|
const cleanJson = replyContent.replace(/```json/g, '').replace(/```/g, '').trim();
|
||||||
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?",
|
const intent = JSON.parse(cleanJson);
|
||||||
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"]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scenario 2: Adding a Member
|
if (intent.action === 'BOOK') {
|
||||||
if (context.lastStep === "AWAIT_REGISTRATION" || text.includes("add new member")) {
|
// Extract HH:MM
|
||||||
if (text.includes("add new member")) {
|
let timeVal = "10:00"; // fallback
|
||||||
return { reply: "Sure! What is the name of the family member you'd like to add?" };
|
const timeMatch = intent.time.match(/(\d{1,2}:\d{2})/);
|
||||||
}
|
if (timeMatch) timeVal = timeMatch[1];
|
||||||
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"]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scenario 3: Booking Intent
|
// Parse date
|
||||||
if (text.includes("book") || text.includes("today")) {
|
let dateVal = new Date().toISOString().split('T')[0];
|
||||||
return {
|
if (intent.date && intent.date.match(/^\d{4}-\d{2}-\d{2}$/)) {
|
||||||
reply: "I can help with that. Which time works best for you?",
|
dateVal = intent.date;
|
||||||
buttons: ["10:00 AM", "11:00 AM", "06:00 PM"],
|
}
|
||||||
nextStep: "AWAIT_TIME"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scenario 3.1: Handling time selection
|
// Book the token using Dr. Sharma's Cardiology ("sharma-clinic")
|
||||||
if (context.lastStep === "AWAIT_TIME") {
|
// In a real system, we'd map intent.department to tenantId
|
||||||
let time = "";
|
const result = await queueManager.bookOnline(dateVal, timeVal, phone, 'sharma-clinic');
|
||||||
if (text.includes("1") || text.includes("10")) time = "10:00";
|
if (result.success) {
|
||||||
else if (text.includes("2") || text.includes("11")) time = "11:00";
|
return { reply: `Awesome! Your appointment for ${intent.department} is booked. Your token is ${result.token}. You should receive a confirmation message shortly.` };
|
||||||
else if (text.includes("3") || text.includes("6") || text.includes("06")) time = "18:00";
|
} else {
|
||||||
|
return { reply: `Sorry, ${result.message}` };
|
||||||
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}` };
|
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Not JSON, just regular conversational text
|
||||||
|
return { reply: replyContent };
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Scenario 3.5: Medicine Query
|
return { reply: replyContent };
|
||||||
if (text.includes("have") || text.includes("medicine") || text.includes("syrup") || text.includes("tablet")) {
|
} catch (error) {
|
||||||
const pharmacyReply = pharmacyAgent.handlePatientQuery(message);
|
console.error('[ClinicAgent] LLM Error:', error.message);
|
||||||
return {
|
return { reply: "I'm having trouble connecting to my AI brain right now. Please try again in a moment." };
|
||||||
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"
|
value: "rN5RpKAbFVCv0G45ehluf1JbTWt2dgNW"
|
||||||
- name: TWILIO_WHATSAPP_FROM
|
- name: TWILIO_WHATSAPP_FROM
|
||||||
value: "whatsapp:+14155238886"
|
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:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /
|
path: /
|
||||||
|
|
|
||||||
|
|
@ -1163,6 +1163,27 @@
|
||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
|
"openai": "^6.39.0",
|
||||||
"pg": "^8.21.0",
|
"pg": "^8.21.0",
|
||||||
"twilio": "^6.0.2",
|
"twilio": "^6.0.2",
|
||||||
"uuid": "^14.0.0"
|
"uuid": "^14.0.0"
|
||||||
|
|
@ -1177,6 +1178,27 @@
|
||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
|
"openai": "^6.39.0",
|
||||||
"pg": "^8.21.0",
|
"pg": "^8.21.0",
|
||||||
"twilio": "^6.0.2",
|
"twilio": "^6.0.2",
|
||||||
"uuid": "^14.0.0"
|
"uuid": "^14.0.0"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue