← back to Whatsmystyle

scripts/debate.js

87 lines

/**
 * Dream-team debate harness — wraps the local Ollama panel for design decisions.
 *
 * Usage:
 *   node scripts/debate.js "topic" "context blob"
 *
 * Fires three local LLMs (qwen3:14b, deepseek-r1:14b, phi4:14b) and asks each
 * to defend a position, then writes the panel + a synthesized verdict to the
 * SQLite `debates` table.
 *
 * Why local-only: Steve's yolo-overnight rule forbids metered LLM cost.
 * The CCK / claude-codex relays are reserved for the morning review.
 */
const Database = require('better-sqlite3');
const path = require('path');
const fetch = require('node-fetch');

const OLLAMA = process.env.OLLAMA_URL || 'http://127.0.0.1:11434';
const PANEL = [
  { role: 'DEFENDER',  model: process.env.PANEL_DEFENDER  || 'qwen3:14b' },
  { role: 'PROSECUTOR',model: process.env.PANEL_PROSECUTOR|| 'deepseek-r1:14b' },
  { role: 'PRAGMATIST',model: process.env.PANEL_PRAGMATIST|| 'phi4:14b' },
];

async function ask(model, system, user) {
  try {
    const r = await fetch(`${OLLAMA}/api/chat`, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        model,
        stream: false,
        messages: [
          { role: 'system', content: system },
          { role: 'user', content: user },
        ],
        options: { temperature: 0.6 },
      }),
    });
    if (!r.ok) return { error: `HTTP ${r.status}` };
    const j = await r.json();
    return { content: j.message?.content || j.response || '' };
  } catch (e) {
    return { error: e.message };
  }
}

async function debate(topic, context) {
  const transcripts = [];
  for (const p of PANEL) {
    const sys = `You are the ${p.role} in a design debate about WhatsMyStyle, an AI personal-stylist app.
Be concise (≤ 8 bullets). Identify risks the other two panelists will miss.
End with one line: VERDICT: <ship|hold|modify> — <one sentence>.`;
    const usr = `TOPIC: ${topic}\n\nCONTEXT:\n${context}`;
    const out = await ask(p.model, sys, usr);
    transcripts.push({ role: p.role, model: p.model, output: out.content || `[${out.error}]` });
  }
  // synth
  const synth = await ask(PANEL[0].model,
    `You are the synthesizer. Read three panelist transcripts and produce a single VERDICT line plus 3 bullets of agreed actions.`,
    transcripts.map(t => `### ${t.role} (${t.model})\n${t.output}`).join('\n\n'));
  return { panel: transcripts, verdict: synth.content || '[synth failed]' };
}

async function main() {
  const [, , topic, context] = process.argv;
  if (!topic) {
    console.error('usage: node debate.js "topic" "context"');
    process.exit(1);
  }
  const dbPath = path.join(__dirname, '..', 'data', 'whatsmystyle.db');
  const db = new Database(dbPath);
  db.exec(`CREATE TABLE IF NOT EXISTS debates (
    id INTEGER PRIMARY KEY, topic TEXT NOT NULL, context TEXT, verdict TEXT, panel TEXT,
    created_at TEXT DEFAULT (datetime('now'))
  )`);
  const result = await debate(topic, context || '');
  const { lastInsertRowid } = db.prepare(
    'INSERT INTO debates (topic, context, verdict, panel) VALUES (?, ?, ?, ?)'
  ).run(topic, context, result.verdict, JSON.stringify(result.panel));
  console.log(JSON.stringify({ id: lastInsertRowid, verdict: result.verdict }, null, 2));
}

if (require.main === module) main().catch(e => { console.error(e); process.exit(1); });

module.exports = { debate };