Skip to content
// Combine AI extraction (if it ran and returned output) with the rule-based extraction. Code decides; AI only reads.
const s = $('Settings (edit me)').first().json;
const item = $input.first();
const fromAi = item.json.ok === undefined; // the AI chain outputs only { output } or { error }
const x = fromAi ? $('Intake').first().json : item.json;
if (!x.ok) return [{ json: x }];
const t = x.text.toLowerCase();

// --- rules (fallback, and for anything the AI left empty)
const rules = {};
// Most keyword hits wins; ties go to the earlier (more specific) service in Settings
const svc = s.services.map((sv) => ({ sv, hits: sv.keywords.filter((k) => t.includes(k)).length })).filter((x) => x.hits).sort((a, b) => b.hits - a.hits)[0]?.sv;
if (svc) rules.need = svc.name;
const money = [...t.replace(/,/g, '').matchAll(/\$\s?(\d+(?:\.\d+)?)\s?(k)?|(\d+(?:\.\d+)?)\s?(k)\b|(\d{4,})\s*(?:usd|dollars)?/g)]
  .map((m) => Number(m[1] ?? m[3] ?? m[5]) * ((m[2] || m[4]) ? 1000 : 1)).filter((n) => n >= 100);
if (money.length && (x.asked === 'budget' || /budget|\$|usd|dollars|\bk\b/.test(t))) rules.budget = Math.max(...money);
const tl = [[/\b(asap|right away|immediately|this week|urgent)\b/, 1], [/\bnext week\b/, 2], [/\bthis month\b/, 3], [/\bnext month\b/, 6],
  [/\b(this|next) quarter\b|\b(2|3|two|three) months?\b/, 12], [/\b(6|six) months?\b|\blater this year\b/, 26], [/\bnext year\b|\bjust (researching|looking|exploring)\b|\bno rush\b/, 52]];
const wk = t.match(/\b(\d+)\s*(week|month)s?\b/);
for (const [re, w] of tl) if (re.test(t)) { rules.timeline_weeks = w; break; }
if (rules.timeline_weeks === undefined && wk && x.asked === 'timeline_weeks') rules.timeline_weeks = Number(wk[1]) * (wk[2] === 'month' ? 4 : 1);
if (/\b(i'?m the (owner|founder|ceo|decision maker)|i (decide|make the call|sign off)|yes,? (it'?s|that'?s) me|i am the decision)/.test(t) || (x.asked === 'decision_maker' && /^\s*(yes|yep|yeah|i do|me)\b/.test(t)))
  rules.decision_maker = true;
else if (/\b(my (boss|manager|partner|cfo)|need to (check|ask|run it by)|not the decision|committee)\b/.test(t) || (x.asked === 'decision_maker' && /^\s*(no|nope)\b/.test(t)))
  rules.decision_maker = false;
const wants_human = /\b(human|real person|talk to (someone|a person)|call me|speak to (someone|a person))\b/.test(t);

const ai = fromAi ? item.json.output : null;
const clean = (o) => Object.fromEntries(Object.entries(o ?? {}).filter(([, v]) => v !== null && v !== undefined && v !== ''));
const extracted = { ...rules, ...clean(ai ? { need: ai.need, budget: ai.budget, timeline_weeks: ai.timeline_weeks, decision_maker: ai.decision_maker } : {}) };
return [{ json: { ...x, extracted, wants_human: wants_human || ai?.wants_human === true, slot_hint: ai?.slot_choice ?? null,
  extraction_source: ai ? 'ai' : 'rules', ai_error: fromAi && !ai ? String(item.json.error?.message ?? item.json.error ?? 'no AI output').slice(0, 200) : undefined } }];