WeLe Agentic AI: LangGraph multi-agent CRM assistant with voice
This commit is contained in:
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user