WeLe Agentic AI: LangGraph multi-agent CRM assistant with voice

This commit is contained in:
2026-08-28 02:16:03 +05:30
commit 105e58e02a
69 changed files with 11501 additions and 0 deletions
+32
View File
@@ -0,0 +1,32 @@
import jwt from 'jsonwebtoken';
import dotenv from 'dotenv';
dotenv.config();
const BASE = 'http://localhost:4000';
const ADMIN = '6a00cefe524bebd27037a968';
const tok = (id) => jwt.sign({ id }, process.env.CRM_JWT_SECRET, { expiresIn: '30m' });
const H = (id) => ({ 'content-type': 'application/json', authorization: `Bearer ${tok(id)}` });
const approved = process.argv.includes('--approve');
const sid = 'approve-' + Math.random().toString(36).slice(2, 8);
const r1 = await (await fetch(`${BASE}/api/agent/chat`, {
method: 'POST', headers: H(ADMIN),
body: JSON.stringify({ message: 'Add a note to lead 919043104868 saying: Follow up about the AI Basics batch.', session_id: sid }),
})).json();
console.log('--- TURN 1 ---');
for (const b of r1.blocks || []) {
console.log(' ', b.type, b.type === 'approval' ? `${b.tool} risk=${b.risk}` : (b.markdown || '').slice(0, 150));
}
console.log(' interrupt present:', !!r1.interrupt);
if (r1.interrupt) {
const r2 = await (await fetch(`${BASE}/api/agent/approve`, {
method: 'POST', headers: H(ADMIN),
body: JSON.stringify({ session_id: sid, approved, reason: approved ? undefined : 'Declined by test harness.' }),
})).json();
console.log(`\n--- TURN 2 (${approved ? 'APPROVED' : 'declined'}) ---`);
for (const b of r2.blocks || []) console.log(' ', b.type, (b.markdown || b.summary || '').slice(0, 300));
}
process.exit(0);
+50
View File
@@ -0,0 +1,50 @@
/* Shape of the acquisition collections, so the tools match reality. */
import mongoose from 'mongoose';
import { connectMongo } from '../src/data/mongo.js';
await connectMongo();
const db = mongoose.connection.db;
async function probe(name, { distinct = [], dateField } = {}) {
const col = db.collection(name);
const n = await col.estimatedDocumentCount();
console.log(`\n${'='.repeat(60)}\n${name} — ${n} docs`);
const doc = await col.findOne({}, { sort: { _id: -1 } });
if (!doc) return;
console.log('newest doc:');
for (const [k, v] of Object.entries(doc)) {
if (k === '__v') continue;
const s = v == null ? 'null' : typeof v === 'object' ? JSON.stringify(v).slice(0, 90) : String(v).slice(0, 90);
console.log(` ${k.padEnd(20)} ${s}`);
}
for (const f of distinct) {
const vals = await col.aggregate([
{ $group: { _id: `$${f}`, n: { $sum: 1 } } },
{ $sort: { n: -1 } }, { $limit: 8 },
]).toArray();
console.log(` ${f}: ` + vals.map((v) => `${v._id}=${v.n}`).join(' '));
}
if (dateField) {
const [first] = await col.find({}, { sort: { [dateField]: 1 }, limit: 1, projection: { [dateField]: 1 } }).toArray();
const [last] = await col.find({}, { sort: { [dateField]: -1 }, limit: 1, projection: { [dateField]: 1 } }).toArray();
console.log(` ${dateField}: ${first?.[dateField]} → ${last?.[dateField]}`);
}
}
await probe('capievents', { distinct: ['status', 'event_name', 'stage'], dateField: 'createdAt' });
await probe('instaadleads', { distinct: ['platform', 'campaign_name', 'is_organic'], dateField: 'created_time' });
await probe('commentleads', { distinct: ['source'], dateField: 'createdAt' });
await probe('whatsapptemplleads', { distinct: ['button_text'], dateField: 'createdAt' });
// CAPI failures are the whole point — what do they look like?
const errs = await db.collection('capievents').aggregate([
{ $match: { status: { $ne: 'success' } } },
{ $group: { _id: { status: '$status', error: '$error' }, n: { $sum: 1 } } },
{ $sort: { n: -1 } }, { $limit: 6 },
]).toArray();
console.log('\nCAPI non-success breakdown:');
for (const e of errs) console.log(` ${e.n.toString().padStart(5)} status=${e._id.status} error=${String(e._id.error).slice(0, 80)}`);
process.exit(0);
+31
View File
@@ -0,0 +1,31 @@
/* Exercise every attribution tool against the live database. */
import { connectMongo } from '../src/data/mongo.js';
import { attributionTools } from '../src/tools/crm/attribution.tools.js';
await connectMongo();
const cfg = { configurable: { user: { id: 'x', name: 'Test', role: 'admin' }, runtime: { toolCallCount: 0 } } };
const cases = [
['capi_health', { date_range: 'this_month' }],
['capi_funnel', { date_range: 'this_month' }],
['ad_form_leads', { date_range: 'all_time', group_by: 'campaign_name' }],
['template_responses', { date_range: 'last_90_days' }],
['comment_leads', { date_range: 'last_90_days' }],
];
let pass = 0;
for (const [name, args] of cases) {
const tool = attributionTools.find((t) => t.name === name);
try {
const raw = await tool.invoke(args, cfg);
const res = typeof raw === 'string' ? JSON.parse(raw) : raw;
if (res.ok === false) { console.log(`FAIL ${name}: ${res.error}`); continue; }
pass++;
console.log(`\nPASS ${name}`);
console.log(JSON.stringify(res.data ?? res, null, 1).slice(0, 800));
} catch (e) {
console.log(`FAIL ${name}: ${e.message.slice(0, 200)}`);
}
}
console.log(`\n${pass}/${cases.length} attribution tools passing`);
process.exit(0);
+67
View File
@@ -0,0 +1,67 @@
import fs from 'node:fs';
import { renderChart, chartToPng, chartTable } from '../src/tools/artifacts/chart.js';
const out = 'C:/Users/HP/AppData/Local/Temp/claude/c--Users-HP-Desktop-Dev-Agentic-AI/8b30e040-7edd-46bd-97f0-c9d03d3965cf/scratchpad';
fs.mkdirSync(out, { recursive: true });
const specs = {
bar: {
type: 'bar', title: 'Leads by stage', subtitle: 'Last 90 days · live CRM data',
data: [
{ label: 'new_lead', value: 3417 }, { label: 'contacted', value: 500 },
{ label: 'lost', value: 282 }, { label: 'qualified', value: 59 },
{ label: 'demo', value: 4 }, { label: 'converted', value: 4 },
],
},
barMulti: {
type: 'bar', title: 'Leads vs enrolments by source', subtitle: 'Grouped, two series',
series: [
{ name: 'Leads', data: [{ label: 'ad_campaign', value: 2833 }, { label: 'website', value: 316 }, { label: 'organic', value: 140 }, { label: 'linkedin', value: 62 }] },
{ name: 'Enrolments', data: [{ label: 'ad_campaign', value: 0 }, { label: 'website', value: 1 }, { label: 'organic', value: 3 }, { label: 'linkedin', value: 2 }] },
],
},
hbar: {
type: 'hbar', title: 'Top batches by paid enrolments', format: 'number',
data: [
{ label: 'AI Basics for Manufacturing', value: 59 },
{ label: 'AI Quality Inspection', value: 31 },
{ label: 'Build Your First AI Agent in 60 Minutes (Live)', value: 31 },
{ label: 'AI for IoT & Sensors', value: 14 },
{ label: 'Digital Marketing Mastery', value: 12 },
],
},
line: {
type: 'line', title: 'Lead volume trend', subtitle: 'Weekly buckets',
series: [
{ name: 'New leads', data: [{ label: 'W30', value: 65 }, { label: 'W31', value: 243 }, { label: 'W32', value: 310 }, { label: 'W33', value: 288 }, { label: 'W34', value: 251 }] },
{ name: 'Contacted', data: [{ label: 'W30', value: 20 }, { label: 'W31', value: 88 }, { label: 'W32', value: 140 }, { label: 'W33', value: 96 }, { label: 'W34', value: 74 }] },
],
},
donut: {
type: 'donut', title: 'Payment attempts by status', center_label: 'attempts',
data: [{ label: 'CANCELLED', value: 206 }, { label: 'SUCCESS', value: 39 }, { label: 'FAILED', value: 5 }, { label: 'ERROR', value: 3 }],
},
pieMany: {
type: 'pie', title: 'Slice cap check (6 → 3 + Other)',
data: [{ label: 'A', value: 40 }, { label: 'B', value: 25 }, { label: 'C', value: 15 }, { label: 'D', value: 10 }, { label: 'E', value: 6 }, { label: 'F', value: 4 }],
},
currency: {
type: 'hbar', title: 'Abandoned checkout value by batch', format: 'currency',
data: [{ label: 'AI Basics for Manufacturing', value: 363817 }, { label: 'Applied AI 4-Week', value: 128400 }, { label: 'AI Quality Inspection', value: 64200 }],
},
};
const cards = [];
for (const [name, spec] of Object.entries(specs)) {
const svg = renderChart(spec, 'auto');
fs.writeFileSync(`${out}/chart-${name}.svg`, svg);
const png = await chartToPng(spec, 1.5);
fs.writeFileSync(`${out}/chart-${name}.png`, png);
cards.push(`<figure><figcaption>${name}</figcaption>${svg}</figure>`);
console.log(`${name.padEnd(10)} svg ${String(svg.length).padStart(6)}b png ${String(png.length).padStart(7)}b tableCols=${chartTable(spec).columns.length}`);
}
fs.writeFileSync(`${out}/charts.html`,
`<!doctype html><meta charset="utf-8"><style>body{background:#f4f4f2;font-family:system-ui;padding:20px;display:grid;gap:24px}figure{margin:0}figcaption{font:600 12px system-ui;margin-bottom:6px;color:#555}</style>${cards.join('')}`);
console.log('\nwrote preview:', `${out}/charts.html`);
process.exit(0);
+20
View File
@@ -0,0 +1,20 @@
/* Verify name-based conversation lookup. */
import { connectMongo } from '../src/data/mongo.js';
import { getConversation, searchConversations } from '../src/tools/crm/conversations.tools.js';
await connectMongo();
const cfg = { configurable: { user: { id: 'x', name: 'Test', role: 'admin' }, runtime: { toolCallCount: 0 } } };
const show = (label, r) => {
const j = typeof r === 'string' ? JSON.parse(r) : r;
console.log(`\n── ${label}`);
console.log(JSON.stringify(j, null, 1).slice(0, 900));
};
show('get_conversation by NAME "md salim"', await getConversation.invoke({ person: 'md salim' }, cfg));
show('get_conversation by NAME "Md Salim"', await getConversation.invoke({ person: 'Md Salim' }, cfg));
show('get_conversation by PHONE', await getConversation.invoke({ person: '919361281813' }, cfg));
show('get_conversation, nonexistent', await getConversation.invoke({ person: 'Zzzz Nobody' }, cfg));
show('search_conversations name filter', await searchConversations.invoke({ name: 'salim', limit: 3 }, cfg));
show('search_conversations (names joined in)', await searchConversations.invoke({ limit: 3 }, cfg));
process.exit(0);
+74
View File
@@ -0,0 +1,74 @@
import fs from 'node:fs';
import { BUILDERS } from '../src/tools/artifacts/documents.js';
const out = 'C:/Users/HP/AppData/Local/Temp/claude/c--Users-HP-Desktop-Dev-Agentic-AI/8b30e040-7edd-46bd-97f0-c9d03d3965cf/scratchpad';
const spec = {
title: 'WeLe CRM — Performance Review',
subtitle: 'Last 90 days · generated from live CRM data',
summary:
'Lead volume is healthy at 3,371 new leads, but conversion is the constraint: only 14 of 97 payment '
+ 'attempts completed (14.4%), leaving ₹363,817 in abandoned checkouts. The largest funnel leak is '
+ 'new_lead → contacted, where 91% of leads never receive a first touch.',
sections: [
{
heading: 'Headline figures',
kpis: [
{ label: 'New leads', value: '3,371' },
{ label: 'Paid enrolments', value: '14' },
{ label: 'Payment success', value: '14.4%' },
{ label: 'Abandoned value', value: '₹3.64L' },
],
text: 'Volume is not the problem. Follow-up capacity is.',
},
{
heading: 'Pipeline by stage',
chart: {
type: 'bar', title: 'Leads by stage', format: 'number',
data: [
{ label: 'new_lead', value: 3417 }, { label: 'contacted', value: 500 },
{ label: 'lost', value: 282 }, { label: 'qualified', value: 59 },
{ label: 'demo', value: 4 }, { label: 'converted', value: 4 },
],
},
bullets: [
'76.6% of all leads have never moved past new_lead.',
'Only 59 leads reached qualified in 90 days.',
],
},
{
heading: 'Payment attempts',
chart: {
type: 'donut', title: 'Attempts by status', center_label: 'attempts',
data: [{ label: 'CANCELLED', value: 206 }, { label: 'SUCCESS', value: 39 }, { label: 'FAILED', value: 5 }, { label: 'ERROR', value: 3 }],
},
},
{
heading: 'Top batches',
table: {
columns: ['Batch', 'Paid', 'Attempts', 'Revenue', 'Success %'],
rows: [
['AI Basics for Manufacturing', 12, 59, 18400, 20.3],
['AI Quality Inspection', 8, 31, 9200, 25.8],
['Build Your First AI Agent in 60 Minutes (Live)', 6, 31, 4300, 19.4],
['AI for IoT & Sensors', 3, 14, 1800, 21.4],
['Digital Marketing Mastery', 2, 12, 1200, 16.7],
],
},
},
],
};
for (const [ext, build] of Object.entries(BUILDERS)) {
const t = Date.now();
try {
const buf = await build(spec);
const p = `${out}/report.${ext}`;
fs.writeFileSync(p, buf);
console.log(`PASS ${ext.padEnd(5)} ${String((buf.length / 1024).toFixed(1)).padStart(8)} kB ${Date.now() - t}ms`);
} catch (e) {
console.log(`FAIL ${ext.padEnd(5)} ${e.message}`);
console.log(e.stack?.split('\n').slice(1, 4).join('\n'));
}
}
process.exit(0);
+55
View File
@@ -0,0 +1,55 @@
import fs from 'node:fs';
import ExcelJS from 'exceljs';
const out = 'C:/Users/HP/AppData/Local/Temp/claude/c--Users-HP-Desktop-Dev-Agentic-AI/8b30e040-7edd-46bd-97f0-c9d03d3965cf/scratchpad';
// ── PDF: header, page count, embedded images ──
const pdf = fs.readFileSync(`${out}/report.pdf`);
const pdfStr = pdf.toString('latin1');
const pages = (pdfStr.match(/\/Type\s*\/Page[^s]/g) || []).length;
const images = (pdfStr.match(/\/Subtype\s*\/Image/g) || []).length;
console.log(`PDF header=${pdf.subarray(0, 5).toString()} pages=${pages} images=${images} hasEOF=${pdfStr.trimEnd().endsWith('%%EOF')}`);
// ── OOXML: valid zip + expected parts ──
const zipParts = (buf) => {
const s = buf.toString('latin1');
const names = new Set();
const rx = /PK\x03\x04[\s\S]{22}/g;
let m;
while ((m = rx.exec(s))) {
const nameLen = buf.readUInt16LE(m.index + 26);
const extraLen = buf.readUInt16LE(m.index + 28);
names.add(buf.subarray(m.index + 30, m.index + 30 + nameLen).toString('utf8'));
rx.lastIndex = m.index + 30 + nameLen + extraLen;
}
return [...names];
};
for (const [name, must] of [
['docx', 'word/document.xml'],
['pptx', 'ppt/presentation.xml'],
['xlsx', 'xl/workbook.xml'],
]) {
const buf = fs.readFileSync(`${out}/report.${name}`);
const parts = zipParts(buf);
const media = parts.filter((p) => /media\//.test(p));
const slides = parts.filter((p) => /slides\/slide\d+\.xml$/.test(p));
const sheets = parts.filter((p) => /worksheets\/sheet\d+\.xml$/.test(p));
console.log(
`${name.toUpperCase().padEnd(5)} zip=${buf.subarray(0, 2).toString() === 'PK'} parts=${parts.length} ` +
`core=${parts.includes(must)} media=${media.length}` +
(slides.length ? ` slides=${slides.length}` : '') +
(sheets.length ? ` sheets=${sheets.length}` : ''),
);
}
// ── XLSX: read it back and confirm the data survived ──
const wb = new ExcelJS.Workbook();
await wb.xlsx.readFile(`${out}/report.xlsx`);
console.log('XLSX sheets:', wb.worksheets.map((w) => `${w.name}(${w.rowCount}r)`).join(', '));
const data = wb.getWorksheet('Top batches');
if (data) {
console.log('XLSX header:', data.getRow(1).values.slice(1).join(' | '));
console.log('XLSX row2 :', data.getRow(2).values.slice(1).join(' | '));
}
process.exit(0);
+60
View File
@@ -0,0 +1,60 @@
import jwt from 'jsonwebtoken';
import dotenv from 'dotenv';
dotenv.config();
const BASE = 'http://localhost:4000';
const ADMIN = '6a00cefe524bebd27037a968'; // Thulasiraman S (admin)
const AGENT = '6a4dfd7ecb0ff97137bc99e5'; // Kavyaa K (agent)
const tok = (id) => jwt.sign({ id }, process.env.CRM_JWT_SECRET, { expiresIn: '30m' });
async function chat(userId, message, sessionId) {
const t0 = Date.now();
const res = await fetch(`${BASE}/api/agent/chat`, {
method: 'POST',
headers: { 'content-type': 'application/json', authorization: `Bearer ${tok(userId)}` },
body: JSON.stringify({ message, session_id: sessionId }),
});
const j = await res.json();
return { ...j, ms: Date.now() - t0, status: res.status };
}
function summarise(r) {
if (!r.ok) return ` ERROR ${r.status}: ${r.error}`;
const lines = [];
for (const b of r.blocks || []) {
if (b.type === 'text') lines.push(` [text] ${b.markdown.replace(/\n+/g, ' ').slice(0, 300)}`);
else if (b.type === 'metrics') lines.push(` [metrics] ${b.items.map((i) => `${i.label}=${i.value}`).join(' ')}`);
else if (b.type === 'chart') lines.push(` [chart] ${b.chart_type} "${b.title}" svg=${b.svg.length}b rows=${b.table.rows.length}`);
else if (b.type === 'table') lines.push(` [table] "${b.title}" ${b.columns.length}cols × ${b.row_count}rows`);
else if (b.type === 'file') lines.push(` [file] ${b.filename} ${(b.bytes / 1024).toFixed(1)}kB → ${b.url}`);
else if (b.type === 'approval') lines.push(` [APPROVE] ${b.tool} (${b.risk}) — ${b.summary}`);
else if (b.type === 'notice') lines.push(` [notice] ${b.level}: ${b.message}`);
}
return lines.join('\n');
}
// ── capabilities ──
const caps = await (await fetch(`${BASE}/api/agent/capabilities`, {
headers: { authorization: `Bearer ${tok(AGENT)}` },
})).json();
console.log('=== CAPABILITIES (agent role: Kavyaa K) ===');
console.log('permissions:', caps.user.permissions.join(', '));
for (const a of caps.agents) console.log(` ${a.available ? '✓' : '✗'} ${a.title.padEnd(20)} ${a.tools.length} tools`);
console.log();
const tests = [
[ADMIN, 'How is the business doing over the last 90 days? Give me the headline numbers and a chart of the pipeline.'],
[ADMIN, 'Which lead sources actually convert? Chart it.'],
[ADMIN, 'Who on the team is performing best this quarter, and who has overdue follow-ups?'],
[ADMIN, 'Build me a PDF report on enrolment and payment performance for the last 90 days.'],
[ADMIN, 'Update the lead 919043104868 — set their tag to hot.'],
];
for (const [uid, msg] of tests) {
console.log(`=== "${msg.slice(0, 78)}" ===`);
const r = await chat(uid, msg, `test-${Math.random().toString(36).slice(2, 8)}`);
console.log(summarise(r));
console.log(` (${(r.ms / 1000).toFixed(1)}s)\n`);
}
process.exit(0);
+35
View File
@@ -0,0 +1,35 @@
/* What data exists that no agent can currently reach? */
import mongoose from 'mongoose';
import { connectMongo } from '../src/data/mongo.js';
await connectMongo();
const db = mongoose.connection.db;
// Collections the tools actually query today.
const COVERED = new Set([
'leads', 'conversations', 'leadstages', 'analyticsevents', 'summarynotes',
'workshopstages', 'enrollmentlogs', 'calllogs', 'campaigns', 'followups',
'dsrreports', 'users', 'roles', 'courses', 'trainers',
]);
const cols = await db.listCollections().toArray();
const rows = [];
for (const c of cols) {
const n = await db.collection(c.name).estimatedDocumentCount();
if (n > 0) rows.push([c.name, n, COVERED.has(c.name)]);
}
rows.sort((a, b) => b[1] - a[1]);
console.log('UNREACHABLE by any current agent (non-empty):');
for (const [name, n, cov] of rows) {
if (!cov) console.log(` ${String(n).padStart(6)} ${name}`);
}
// Peek at the biggest gaps to judge whether they're worth an agent.
for (const name of ['capievents', 'instaadleads', 'commentleads', 'whatsapptemplleads', 'tags']) {
const doc = await db.collection(name).findOne({});
if (!doc) continue;
const keys = Object.keys(doc).filter((k) => k !== '_id' && k !== '__v').slice(0, 12);
console.log(`\n${name} → ${keys.join(', ')}`);
}
process.exit(0);
+24
View File
@@ -0,0 +1,24 @@
import { getModel } from '../src/orchestration/llm.js';
import { tool } from '@langchain/core/tools';
import { z } from 'zod';
const ping = tool(async ({ n }) => `pong-${n * 2}`, {
name: 'double',
description: 'Doubles a number.',
schema: z.object({ n: z.number().describe('number to double') }),
});
for (const tier of ['agent', 'supervisor']) {
try {
const llm = getModel(tier, { maxTokens: 1024, effort: 'low' });
const r = await llm.invoke([{ role: 'user', content: 'Reply with exactly: OK' }]);
console.log(`[${tier}] text ->`, JSON.stringify(r.content).slice(0, 120));
const bound = getModel(tier, { maxTokens: 1024, effort: 'low' }).bindTools([ping]);
const t = await bound.invoke([{ role: 'user', content: 'Use the double tool on 21.' }]);
console.log(`[${tier}] toolCalls ->`, JSON.stringify(t.tool_calls));
} catch (e) {
console.error(`[${tier}] ERROR`, e.status || '', e.message?.slice(0, 300));
}
}
process.exit(0);
+31
View File
@@ -0,0 +1,31 @@
/* Reproduces the multi-turn bug: same session, three different questions. */
import jwt from 'jsonwebtoken';
import dotenv from 'dotenv';
dotenv.config();
const BASE = 'http://localhost:4000';
const ADMIN = '6a00cefe524bebd27037a968';
const token = jwt.sign({ id: ADMIN }, process.env.CRM_JWT_SECRET, { expiresIn: '30m' });
const sid = 'multi-' + Math.random().toString(36).slice(2, 8);
const asks = [
'How many leads are in the new_lead stage?',
'How many leads were created today?',
'List 3 lost leads with their names.',
];
for (const q of asks) {
const t0 = Date.now();
const r = await (await fetch(`${BASE}/api/agent/chat`, {
method: 'POST',
headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
body: JSON.stringify({ message: q, session_id: sid }),
})).json();
const text = (r.blocks || []).filter((b) => b.type === 'text').map((b) => b.markdown).join(' ');
const kinds = (r.blocks || []).map((b) => b.type).join(',');
console.log(`Q: ${q}`);
console.log(` blocks=[${kinds}] ${((Date.now() - t0) / 1000).toFixed(1)}s`);
console.log(` A: ${text.replace(/\n+/g, ' ').slice(0, 170)}\n`);
}
process.exit(0);
+34
View File
@@ -0,0 +1,34 @@
/* Can Claude actually serve a request right now? */
import dotenv from 'dotenv';
dotenv.config();
const key = process.env.ANTHROPIC_API_KEY;
if (!key) { console.log('❌ ANTHROPIC_API_KEY is not set.'); process.exit(1); }
const chains = [process.env.LLM_CHAIN_SUPERVISOR, process.env.LLM_CHAIN_AGENT].join(',');
const models = [...new Set(chains.split(',').map((s) => s.replace(/^anthropic:/, '').trim()).filter(Boolean))];
const rows = [];
for (const model of models) {
try {
const res = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: { 'content-type': 'application/json', 'x-api-key': key, 'anthropic-version': '2023-06-01' },
body: JSON.stringify({ model, max_tokens: 16, messages: [{ role: 'user', content: 'Say OK' }] }),
signal: AbortSignal.timeout(30000),
});
const j = await res.json();
rows.push(res.ok
? [model, 'OK', JSON.stringify(j.content?.[0]?.text)]
: [model, `HTTP ${res.status}`, String(j.error?.message || '').slice(0, 110)]);
} catch (e) { rows.push([model, 'ERR', e.message.slice(0, 110)]); }
}
console.log('model'.padEnd(24), 'status'.padEnd(10), 'detail');
console.log('-'.repeat(100));
for (const [m, s, d] of rows) console.log(m.padEnd(24), String(s).padEnd(10), d || '');
const ok = rows.filter(([, s]) => s === 'OK');
console.log(`
${ok.length ? `✅ ${ok.length}/${rows.length} model(s) usable: ${ok.map((r) => r[0]).join(', ')}` : '❌ Claude cannot serve a request right now.'}`);
process.exit(0);
+18
View File
@@ -0,0 +1,18 @@
import { connectMongo } from '../src/data/mongo.js';
import { CapiEvent } from '../src/data/models/index.js';
await connectMongo();
const rows = await CapiEvent.aggregate([
{ $group: { _id: '$event_name', total: { $sum: 1 },
failed: { $sum: { $cond: [{ $eq: ['$status','failed'] }, 1, 0] } } } },
{ $project: { _id:0, event:'$_id', total:1, failed:1,
rate: { $round: [{ $multiply: [{ $divide: ['$failed','$total'] }, 100] }, 1] } } },
{ $sort: { rate: -1 } },
]);
console.log('event total failed fail%');
for (const r of rows) console.log(String(r.event).padEnd(22), String(r.total).padStart(5), String(r.failed).padStart(7), String(r.rate).padStart(6));
const errs = await CapiEvent.aggregate([
{ $match: { status:'failed', event_name:'Purchase' } },
{ $group: { _id:'$error', n:{ $sum:1 } } }, { $sort:{ n:-1 } },
]);
console.log('\nPurchase failure reasons:', JSON.stringify(errs));
process.exit(0);
+36
View File
@@ -0,0 +1,36 @@
/* Does "salim" exist anywhere, and why did the search miss it? */
import { connectMongo } from '../src/data/mongo.js';
import { Lead, Conversation } from '../src/data/models/index.js';
await connectMongo();
const rx = /salim/i;
const leads = await Lead.find({ $or: [{ name: rx }, { wa_name: rx }] })
.select('name wa_name phone_number current_stage').limit(10).lean();
console.log(`LEADS matching /salim/i: ${leads.length}`);
for (const l of leads) console.log(` name="${l.name}" wa_name="${l.wa_name}" phone=${l.phone_number} stage=${l.current_stage}`);
const convs = await Conversation.find({ participant_name: rx })
.select('participant_name phone_number channel messages').limit(10).lean();
console.log(`\nCONVERSATIONS matching participant_name /salim/i: ${convs.length}`);
for (const c of convs) console.log(` "${c.participant_name}" ${c.phone_number} ${c.channel} msgs=${c.messages?.length ?? 0}`);
// If a lead matched, is there a conversation on that phone?
for (const l of leads.slice(0, 3)) {
const byPhone = await Conversation.find({ phone_number: l.phone_number })
.select('participant_name phone_number channel messages').lean();
console.log(`\nconversations for lead phone ${l.phone_number}: ${byPhone.length}`);
for (const c of byPhone) {
console.log(` participant_name="${c.participant_name}" channel=${c.channel} msgs=${c.messages?.length ?? 0}`);
}
}
// How are participant_name values actually populated?
const total = await Conversation.countDocuments();
const named = await Conversation.countDocuments({ participant_name: { $nin: ['', null] } });
console.log(`\nconversations total=${total} with a participant_name=${named} (${Math.round(named / total * 100)}%)`);
const sample = await Conversation.find({}).select('participant_name phone_number').limit(5).lean();
console.log('sample participant_name values:', sample.map((s) => JSON.stringify(s.participant_name)).join(', '));
process.exit(0);
+44
View File
@@ -0,0 +1,44 @@
import jwt from 'jsonwebtoken';
import dotenv from 'dotenv';
dotenv.config();
const BASE = 'http://localhost:4000';
const ADMIN = '6a00cefe524bebd27037a968';
const token = jwt.sign({ id: ADMIN }, process.env.CRM_JWT_SECRET, { expiresIn: '30m' });
const res = await fetch(`${BASE}/api/agent/stream`, {
method: 'POST',
headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
body: JSON.stringify({
message: 'Show me the lead temperature split and how many follow-ups are overdue.',
session_id: 'stream-' + Math.random().toString(36).slice(2, 8),
}),
});
console.log('status', res.status, res.headers.get('content-type'));
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = '';
let events = 0;
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
const frames = buf.split('\n\n');
buf = frames.pop() ?? '';
for (const f of frames) {
const line = f.split('\n').find((l) => l.startsWith('data: '));
if (!line) { if (f.startsWith(':')) console.log(' · heartbeat'); continue; }
const ev = JSON.parse(line.slice(6));
events++;
if (ev.type === 'step') console.log(` → step: ${ev.label}${ev.detail ? ` — ${ev.detail.slice(0, 70)}` : ''}`);
else if (ev.type === 'blocks' || ev.type === 'result') {
const kinds = (ev.blocks || []).map((b) => b.type).join(', ');
if (ev.blocks?.length) console.log(` ✔ ${ev.type}: [${kinds}]`);
} else console.log(` · ${ev.type}`);
}
}
console.log(`\ntotal SSE events: ${events}`);
process.exit(0);
+55
View File
@@ -0,0 +1,55 @@
/* What does ONE supervisor request actually cost in input tokens? */
import dotenv from 'dotenv';
dotenv.config();
import { supervisorPrompt } from '../src/orchestration/supervisor.js';
import { delegationToolsFor, agentMenuFor, writeToolsFor } from '../src/agents/factory.js';
import { artifactTools } from '../src/tools/artifacts/index.js';
import { connectMongo } from '../src/data/mongo.js';
import { toJsonSchema } from '@langchain/core/utils/json_schema';
await connectMongo();
const user = { id: '6a00cefe524bebd27037a968', name: 'Thulasiraman S', role: 'admin', permissions: [] };
const system = supervisorPrompt({
user, agentMenu: agentMenuFor(user), now: new Date(),
channel: 'crm_chat', writeTools: writeToolsFor(user),
});
const tools = [...delegationToolsFor(user), ...artifactTools, ...writeToolsFor(user)];
const apiTools = tools.map((t) => ({
name: t.name,
description: t.description,
input_schema: toJsonSchema(t.schema),
}));
// count_tokens needs credit, so estimate locally.
// ~3.6 chars/token is a good approximation for English prose + JSON schemas.
const CHARS_PER_TOKEN = 3.6;
const est = (str) => Math.round(str.length / CHARS_PER_TOKEN);
const toolsJson = JSON.stringify(apiTools);
const perCall = est(system) + est(toolsJson) + 20;
console.log(`system prompt : ${system.length.toLocaleString()} chars → ~${est(system).toLocaleString()} tokens`);
console.log(`tool schemas : ${toolsJson.length.toLocaleString()} chars → ~${est(toolsJson).toLocaleString()} tokens`);
console.log(`tool count : ${tools.length}`);
console.log(`\nINPUT TOKENS PER SUPERVISOR CALL: ${perCall.toLocaleString()}`);
// Opus 5: $5 / MTok input, $25 / MTok output
const IN = 5 / 1e6;
const OUT = 25 / 1e6;
console.log(`\nOpus 5 input cost, one call: $${(perCall * IN).toFixed(4)}`);
console.log('\n─ A ReAct loop re-sends the whole prompt every iteration ─');
for (const iters of [3, 6, 10, 20]) {
// Input grows each iteration as tool calls + results accumulate; ~1.5k/iter is conservative.
let total = 0;
for (let i = 0; i < iters; i++) total += perCall + i * 1500;
console.log(` ${String(iters).padStart(2)} iterations → ${total.toLocaleString().padStart(9)} input tokens = $${(total * IN).toFixed(3)}`);
}
console.log('\n─ Plus sub-agents: each runs its OWN loop with its own prompt+tools ─');
console.log(' (measured separately below)');
process.exit(0);
+44
View File
@@ -0,0 +1,44 @@
import { connectMongo, disconnectMongo } from '../src/data/mongo.js';
import { leadTools } from '../src/tools/crm/leads.tools.js';
import { analyticsTools } from '../src/tools/crm/analytics.tools.js';
await connectMongo();
const ctx = {
configurable: {
user: { id: '000000000000000000000000', name: 'Smoke Admin', role: 'admin' },
sessionId: 'smoke', traceId: 'smoke', channel: 'crm_chat', runtime: {},
},
};
const cases = [
['search_leads', { lead_tag: 'hot', limit: 3 }],
['lead_funnel_breakdown', { group_by: 'current_stage', date_range: 'all_time' }],
['lead_trend', { date_range: 'last_30_days', bucket: 'week' }],
['find_stale_leads', { inactive_days: 30, limit: 3 }],
['business_snapshot', { date_range: 'last_30_days' }],
['conversion_funnel', { date_range: 'last_90_days' }],
['team_performance', { date_range: 'last_90_days' }],
['source_effectiveness', { date_range: 'last_90_days' }],
['activity_analytics', { date_range: 'last_7_days' }],
['enrollment_report', { date_range: 'last_90_days' }],
];
const all = [...leadTools, ...analyticsTools];
for (const [name, args] of cases) {
const t = all.find((x) => x.name === name);
if (!t) { console.log(`?? ${name} NOT FOUND`); continue; }
const started = Date.now();
try {
const out = await t.invoke(args, ctx);
const parsed = JSON.parse(out);
const ok = parsed.ok !== false;
const preview = JSON.stringify(parsed.data ?? parsed).slice(0, 220);
console.log(`${ok ? 'PASS' : 'FAIL'} ${name} (${Date.now() - started}ms) ${preview}`);
} catch (e) {
console.log(`ERR ${name}: ${e.message}`);
}
}
await disconnectMongo();
process.exit(0);