← back to Professional Directory

agents/api-agent/routes/chat.js

100 lines

/**
 * /api/chat — AI chat for the per-org mockup pages.
 *
 * Each org's mockup gets a floating chat widget that lets visitors ask
 * questions about the practice (hours, services, location, providers).
 * The chat uses LOCAL Ollama gemma3:12b on MS2 (no Anthropic, no OpenAI),
 * with a system prompt populated from the org's actual data so answers
 * stay grounded.
 *
 *   POST /api/chat
 *   body: { orgId: number, message: string, history?: [{role, content}] }
 *   resp: { reply: string }
 */
const express = require('express');
const { fetch } = require('undici');
const { query } = require('../../shared/db');

const router = express.Router();
const OLLAMA = process.env.OLLAMA_BASE || 'http://127.0.0.1:11434';
const MODEL  = process.env.CHAT_MODEL  || 'gemma3:12b';

async function buildContext(orgId) {
  const o = (await query(`SELECT id, name, type, address, city, state, zip, phone, website FROM organizations WHERE id=$1 AND opted_out=false`, [orgId])).rows[0];
  if (!o) return null;
  const pros = (await query(`
    SELECT p.full_name, p.title, p.primary_specialty
      FROM professional_locations pl JOIN professionals p ON p.id=pl.professional_id
     WHERE pl.organization_id=$1 AND p.opted_out=false
     ORDER BY p.source_confidence_score DESC NULLS LAST LIMIT 8`, [orgId])).rows;
  const emails = (await query(`SELECT email, email_type FROM emails WHERE organization_id=$1 LIMIT 3`, [orgId])).rows;
  return { o, pros, emails };
}

function systemPrompt(ctx) {
  const { o, pros, emails } = ctx;
  const lines = [
    `You are a friendly receptionist for "${o.name}" (a ${o.type.replace(/_/g,' ')}) in ${o.city || 'Los Angeles County'}.`,
    `Answer questions ONLY about this practice. If you don't know, say so and suggest the visitor call.`,
    `Be brief — 2-3 sentences max. No medical advice; for clinical questions, tell them to call the office.`,
    ``,
    `--- Practice details ---`,
    o.address ? `Address: ${o.address}, ${o.city}, ${o.state} ${o.zip}` : '',
    o.phone ? `Phone: ${o.phone}` : '',
    o.website ? `Website: ${o.website}` : '',
    emails.length ? `Email: ${emails.map(e => e.email).join(', ')}` : '',
    pros.length ? `\nProviders on staff: ${pros.map(p => `${p.full_name}${p.title ? ', ' + p.title : ''}${p.primary_specialty ? ' (' + p.primary_specialty + ')' : ''}`).join('; ')}` : '',
    ``,
    `Hours (default): Mon-Fri 9 AM - 5 PM. Sat by appointment. Closed Sun.`,
  ].filter(Boolean);
  return lines.join('\n');
}

router.post('/', async (req, res, next) => {
  try {
    const orgId = Number(req.body?.orgId);
    const message = String(req.body?.message || '').slice(0, 800).trim();
    if (!orgId || !message) return res.status(400).json({ error: 'orgId + message required' });

    const ctx = await buildContext(orgId);
    if (!ctx) return res.status(404).json({ error: 'org not found' });

    const sys = systemPrompt(ctx);
    const history = Array.isArray(req.body?.history) ? req.body.history.slice(-6) : [];

    const ac = new AbortController();
    const t = setTimeout(() => ac.abort(), 30_000);
    let reply = '';
    try {
      const r = await fetch(`${OLLAMA}/api/chat`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          model: MODEL,
          stream: false,
          options: { temperature: 0.4, num_ctx: 4096 },
          messages: [
            { role: 'system', content: sys },
            ...history,
            { role: 'user', content: message },
          ],
        }),
        signal: ac.signal,
      });
      clearTimeout(t);
      if (!r.ok) {
        return res.status(502).json({ error: 'Ollama upstream error' });
      }
      const j = await r.json();
      reply = j?.message?.content || '(no response)';
    } catch (e) {
      clearTimeout(t);
      return res.status(504).json({ error: 'Ollama timeout — check that gemma3:12b is loaded on MS2' });
    }

    res.json({ reply, model: MODEL });
  } catch (e) { next(e); }
});

module.exports = router;