← back to Butlr

routes/agent-log.js

38 lines

// Live AI-agent transcript route — reads pm2 log lines for a given call,
// parses the "[ai-agent] call=<id> heard:/reply:" entries, returns JSON.
// Used by the listen page to render a live transcript without depending
// on Twilio Media Streams (which require an account-level toggle).

const express = require('express');
const { execFile } = require('child_process');
const router = express.Router();

const CALL_ID_RE = /^[A-Za-z0-9_-]{6,16}$/;

router.get('/api/calls/:call_id/agent-log', (req, res) => {
  const callId = req.params.call_id;
  if (!CALL_ID_RE.test(callId)) {
    return res.status(404).json({ ok: false, error: 'bad_id' });
  }
  // pm2 logs butlr --lines 1000 --nostream --raw → grep for this call
  execFile('pm2', ['logs', 'butlr', '--lines', '1000', '--nostream', '--raw'], { timeout: 4000, maxBuffer: 2_000_000 }, (err, stdout) => {
    if (err && !stdout) return res.json({ ok: true, turns: [], error: err.code || 'pm2_err' });
    const lines = (stdout || '').split('\n');
    const turns = [];
    const heardRe = new RegExp(`\\[ai-agent\\] call=${callId} heard: "([^"]*)"\\s+turn=(\\d+)`);
    const replyRe = new RegExp(`\\[ai-agent\\] call=${callId} reply: "([^"]*)"\\s+action=\\(([^)]*)\\)`);
    for (const line of lines) {
      let m;
      if ((m = line.match(heardRe))) {
        turns.push({ kind: 'heard', text: m[1], turn: parseInt(m[2], 10) });
      } else if ((m = line.match(replyRe))) {
        turns.push({ kind: 'reply', text: m[1], action: m[2] });
      }
    }
    res.set('Cache-Control', 'no-store');
    res.json({ ok: true, callId, count: turns.length, turns });
  });
});

module.exports = router;