diff --git a/backend/botLogic.js b/backend/botLogic.js index c12133f..98c77fd 100644 --- a/backend/botLogic.js +++ b/backend/botLogic.js @@ -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 }; } } diff --git a/backend/clinicAgent.js b/backend/clinicAgent.js index fdd5dfa..38430c6 100644 --- a/backend/clinicAgent.js +++ b/backend/clinicAgent.js @@ -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." - }; } } diff --git a/k8s/app.yaml b/k8s/app.yaml index 0339993..2cb5f03 100644 --- a/k8s/app.yaml +++ b/k8s/app.yaml @@ -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: / diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json index d186b2f..fc03f3a 100644 --- a/node_modules/.package-lock.json +++ b/node_modules/.package-lock.json @@ -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", diff --git a/package-lock.json b/package-lock.json index e3ab43f..38aebc7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index e4078e2..8e19a82 100644 --- a/package.json +++ b/package.json @@ -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"