@mackody/quickenrich-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +39 -0
- package/.env.example +17 -0
- package/.mcp.json +17 -0
- package/README.md +283 -0
- package/agents/AGENTS.md +6 -0
- package/agents/CLAUDE.md +6 -0
- package/package.json +40 -0
- package/scripts/agent-docs-smoke.mjs +72 -0
- package/scripts/agent-envelope-workflow-smoke.mjs +92 -0
- package/scripts/agent-surface-smoke.mjs +102 -0
- package/scripts/benchmark-contract-smoke.mjs +46 -0
- package/scripts/envelope-budget-smoke.mjs +37 -0
- package/scripts/killer-contract-smoke.mjs +206 -0
- package/scripts/lib/benchmark.mjs +92 -0
- package/scripts/lib/core.mjs +3303 -0
- package/scripts/live-benchmark.mjs +518 -0
- package/scripts/live-smoke.mjs +61 -0
- package/scripts/local-floor-benchmark.mjs +366 -0
- package/scripts/mcp-config-smoke.mjs +110 -0
- package/scripts/mcp-live-smoke.mjs +115 -0
- package/scripts/mock-api-smoke.mjs +112 -0
- package/scripts/native-capabilities-smoke.mjs +273 -0
- package/scripts/native-live-smoke.mjs +43 -0
- package/scripts/offline-contract-smoke.mjs +80 -0
- package/scripts/ops-contract-smoke.mjs +270 -0
- package/scripts/paged-finder-floor-smoke.mjs +93 -0
- package/scripts/perf-contract-smoke.mjs +111 -0
- package/scripts/quickenrich-agent-docs.mjs +109 -0
- package/scripts/quickenrich-mcp.mjs +201 -0
- package/scripts/quickenrich-token.mjs +175 -0
- package/scripts/rate-limit-contract-smoke.mjs +70 -0
- package/scripts/reverse-concurrency-floor-smoke.mjs +82 -0
- package/scripts/serialization-floor-smoke.mjs +42 -0
- package/scripts/token-bucket-contract-smoke.mjs +121 -0
- package/scripts/token-cli-smoke.mjs +59 -0
- package/skills/quickenrich/SKILL.md +20 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const serverPath = join(__dirname, "quickenrich-mcp.mjs");
|
|
9
|
+
|
|
10
|
+
function assert(condition, message) {
|
|
11
|
+
if (!condition) throw new Error(message);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function sendRpc(proc, id, method, params = {}) {
|
|
15
|
+
const message = JSON.stringify({ jsonrpc: "2.0", id, method, params });
|
|
16
|
+
proc.stdin.write(`Content-Length: ${Buffer.byteLength(message)}\r\n\r\n${message}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function readResponses(proc, wantedIds, timeoutMs = 5000) {
|
|
20
|
+
let buffer = Buffer.alloc(0);
|
|
21
|
+
const responses = new Map();
|
|
22
|
+
proc.stdout.on("data", (chunk) => {
|
|
23
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
24
|
+
while (true) {
|
|
25
|
+
const headerEnd = buffer.indexOf("\r\n\r\n");
|
|
26
|
+
if (headerEnd === -1) return;
|
|
27
|
+
const header = buffer.slice(0, headerEnd).toString("utf8");
|
|
28
|
+
const match = header.match(/Content-Length:\s*(\d+)/i);
|
|
29
|
+
if (!match) throw new Error(`Invalid MCP header: ${header}`);
|
|
30
|
+
const length = Number(match[1]);
|
|
31
|
+
const bodyStart = headerEnd + 4;
|
|
32
|
+
const bodyEnd = bodyStart + length;
|
|
33
|
+
if (buffer.length < bodyEnd) return;
|
|
34
|
+
const body = buffer.slice(bodyStart, bodyEnd).toString("utf8");
|
|
35
|
+
buffer = buffer.slice(bodyEnd);
|
|
36
|
+
const response = JSON.parse(body);
|
|
37
|
+
responses.set(response.id, response);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
proc.stderr.on("data", (chunk) => process.stderr.write(chunk));
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const timeout = setTimeout(() => reject(new Error(`Timed out waiting for ${wantedIds.join(", ")}`)), timeoutMs);
|
|
43
|
+
const interval = setInterval(() => {
|
|
44
|
+
if (wantedIds.every((id) => responses.has(id))) {
|
|
45
|
+
clearTimeout(timeout);
|
|
46
|
+
clearInterval(interval);
|
|
47
|
+
resolve(wantedIds.map((id) => responses.get(id)));
|
|
48
|
+
}
|
|
49
|
+
}, 25);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function listTools(mode) {
|
|
54
|
+
const proc = spawn(process.execPath, [serverPath], {
|
|
55
|
+
cwd: join(__dirname, ".."),
|
|
56
|
+
env: {
|
|
57
|
+
...process.env,
|
|
58
|
+
QUICKENRICH_API_KEY: "",
|
|
59
|
+
QUICKENRICH_TOKEN: "",
|
|
60
|
+
QUICKENRICH_MCP_TOOL_MODE: mode,
|
|
61
|
+
QUICKENRICH_MCP_CONFIG: join(__dirname, "..", "tmp", "missing-config.json"),
|
|
62
|
+
},
|
|
63
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
64
|
+
});
|
|
65
|
+
try {
|
|
66
|
+
sendRpc(proc, 1, "initialize", { protocolVersion: "2024-11-05" });
|
|
67
|
+
sendRpc(proc, 2, "tools/list");
|
|
68
|
+
const [, list] = await readResponses(proc, [1, 2]);
|
|
69
|
+
return list.result?.tools?.map((tool) => tool.name) || [];
|
|
70
|
+
} finally {
|
|
71
|
+
proc.kill();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const agentTools = await listTools("");
|
|
76
|
+
assert(agentTools.length <= 9, `Default agent mode must expose <=9 tools, got ${agentTools.length}.`);
|
|
77
|
+
for (const required of [
|
|
78
|
+
"quickenrich_auth_status",
|
|
79
|
+
"quickenrich_agent_graph",
|
|
80
|
+
"quickenrich_natural_language",
|
|
81
|
+
"quickenrich_batch_pull_leads",
|
|
82
|
+
"quickenrich_batch_reverse_email",
|
|
83
|
+
"quickenrich_batch_employee_search",
|
|
84
|
+
"quickenrich_pipeline",
|
|
85
|
+
"quickenrich_lookup_values",
|
|
86
|
+
]) {
|
|
87
|
+
assert(agentTools.includes(required), `Agent surface missing ${required}.`);
|
|
88
|
+
}
|
|
89
|
+
for (const lowLevel of [
|
|
90
|
+
"quickenrich_phone_search",
|
|
91
|
+
"quickenrich_employee_search",
|
|
92
|
+
"quickenrich_contact_finder",
|
|
93
|
+
"quickenrich_company_finder",
|
|
94
|
+
]) {
|
|
95
|
+
assert(!agentTools.includes(lowLevel), `Agent surface should hide low-level tool ${lowLevel}.`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const fullTools = await listTools("full");
|
|
99
|
+
assert(fullTools.length >= 11, `Full mode must expose full API surface, got ${fullTools.length}.`);
|
|
100
|
+
assert(fullTools.includes("quickenrich_company_finder"), "Full mode missing company finder.");
|
|
101
|
+
|
|
102
|
+
console.log(JSON.stringify({ ok: true, agentTools: agentTools.length, fullTools: fullTools.length }, null, 2));
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
makeDuplicateDomainBenchmark,
|
|
5
|
+
summarizeBenchmarkComparison,
|
|
6
|
+
} from "./lib/benchmark.mjs";
|
|
7
|
+
|
|
8
|
+
function assert(condition, message) {
|
|
9
|
+
if (!condition) throw new Error(message);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const fixture = makeDuplicateDomainBenchmark({
|
|
13
|
+
uniqueCount: 3,
|
|
14
|
+
duplicateFactor: 2,
|
|
15
|
+
prefix: "qe-mcp-contract",
|
|
16
|
+
title: "Founder",
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
assert(fixture.companies.length === 6, `Expected 6 input companies, got ${fixture.companies.length}.`);
|
|
20
|
+
assert(fixture.uniqueCompanies.length === 3, `Expected 3 unique companies, got ${fixture.uniqueCompanies.length}.`);
|
|
21
|
+
assert(fixture.coalescedCount === 3, `Expected 3 coalesced duplicates, got ${fixture.coalescedCount}.`);
|
|
22
|
+
assert(fixture.companies.every((entry) => entry.title === "Founder"), "Expected title to be propagated to all companies.");
|
|
23
|
+
|
|
24
|
+
for (const company of fixture.uniqueCompanies) {
|
|
25
|
+
const occurrences = fixture.companies.filter((entry) => entry.company_url === company.company_url).length;
|
|
26
|
+
assert(occurrences === 2, `Expected ${company.company_url} twice, got ${occurrences}.`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const comparison = summarizeBenchmarkComparison({
|
|
30
|
+
rows: [
|
|
31
|
+
{ label: "raw_sequential", durationMs: 1200, requestCount: 6, creditsUsed: 0 },
|
|
32
|
+
{ label: "raw_concurrent_deduped", durationMs: 240, requestCount: 3, creditsUsed: 0 },
|
|
33
|
+
{ label: "mcp_batch_stdio", durationMs: 300, requestCount: 3, creditsUsed: 0 },
|
|
34
|
+
],
|
|
35
|
+
baselineLabel: "raw_sequential",
|
|
36
|
+
candidateLabel: "mcp_batch_stdio",
|
|
37
|
+
optimizedLabel: "raw_concurrent_deduped",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
assert(comparison.candidateFasterThanBaseline === true, "Expected MCP batch to be faster than raw sequential.");
|
|
41
|
+
assert(comparison.speedupVsBaseline === 4, `Expected 4x speedup, got ${comparison.speedupVsBaseline}.`);
|
|
42
|
+
assert(comparison.requestReductionVsBaseline === 2, `Expected 2x request reduction, got ${comparison.requestReductionVsBaseline}.`);
|
|
43
|
+
assert(comparison.optimizedDeltaPct === 25, `Expected 25% optimized delta, got ${comparison.optimizedDeltaPct}.`);
|
|
44
|
+
assert(comparison.totalCreditsUsed === 0, `Expected zero credits, got ${comparison.totalCreditsUsed}.`);
|
|
45
|
+
|
|
46
|
+
console.log(JSON.stringify({ ok: true, fixture, comparison }, null, 2));
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { compactToolPayload } from "./lib/core.mjs";
|
|
4
|
+
|
|
5
|
+
function assert(condition, message) {
|
|
6
|
+
if (!condition) throw new Error(message);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const payload = {
|
|
10
|
+
ok: true,
|
|
11
|
+
mode: "domain_search",
|
|
12
|
+
leads: Array.from({ length: 100 }, (_, i) => ({
|
|
13
|
+
first_name: `First${i}`,
|
|
14
|
+
last_name: `Last${i}`,
|
|
15
|
+
title: "Founder and Chief Executive Officer with an intentionally verbose title tail",
|
|
16
|
+
email: `founder${i}@example.com`,
|
|
17
|
+
phone: "+14155550100",
|
|
18
|
+
employee_linkedin: `https://linkedin.com/in/founder-${i}`,
|
|
19
|
+
company_url: `example-${i}.com`,
|
|
20
|
+
company_name: `Example Company ${i}`,
|
|
21
|
+
notes: "x".repeat(1000),
|
|
22
|
+
})),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const compact = compactToolPayload(payload);
|
|
26
|
+
const text = JSON.stringify(compact);
|
|
27
|
+
const meta = compactToolPayload({ ...payload, requestCount: 6, creditsUsed: 0 }, { mode: "meta" });
|
|
28
|
+
|
|
29
|
+
assert(Buffer.byteLength(text, "utf8") <= 12_000, `Default envelope is too large: ${Buffer.byteLength(text, "utf8")} bytes.`);
|
|
30
|
+
assert(compact.leads.length <= 13, `Default envelope should include at most 12 leads plus omitted marker, got ${compact.leads.length}.`);
|
|
31
|
+
assert(compact._meta.truncated === true, "Default envelope should report truncation.");
|
|
32
|
+
assert(!text.includes("x".repeat(800)), "Long fields should be truncated by default.");
|
|
33
|
+
assert(meta.scalars.ok === true, "Meta envelope should preserve scalar ok status.");
|
|
34
|
+
assert(meta.scalars.requestCount === 6, "Meta envelope should preserve scalar requestCount.");
|
|
35
|
+
assert(meta.collections.leads.length === 100, "Meta envelope should preserve collection lengths.");
|
|
36
|
+
|
|
37
|
+
console.log(JSON.stringify({ ok: true, bytes: Buffer.byteLength(text, "utf8"), leads: compact.leads.length, metaScalars: Object.keys(meta.scalars).length }, null, 2));
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { createServer } from "node:http";
|
|
5
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import {
|
|
9
|
+
TOOL_DEFINITIONS,
|
|
10
|
+
agentGraph,
|
|
11
|
+
callQuickEnrichTool,
|
|
12
|
+
createClient,
|
|
13
|
+
selectTools,
|
|
14
|
+
} from "./lib/core.mjs";
|
|
15
|
+
|
|
16
|
+
function assert(condition, message) {
|
|
17
|
+
if (!condition) throw new Error(message);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readJson(path) {
|
|
21
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readJsonl(path) {
|
|
25
|
+
return readFileSync(path, "utf8")
|
|
26
|
+
.split(/\r?\n/)
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
.map((line) => JSON.parse(line));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const tmp = mkdtempSync(join(tmpdir(), "qe-killer-"));
|
|
32
|
+
const requests = [];
|
|
33
|
+
|
|
34
|
+
const server = createServer(async (req, res) => {
|
|
35
|
+
const chunks = [];
|
|
36
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
37
|
+
const bodyText = Buffer.concat(chunks).toString("utf8");
|
|
38
|
+
const body = bodyText ? JSON.parse(bodyText) : null;
|
|
39
|
+
const url = new URL(req.url, "http://127.0.0.1");
|
|
40
|
+
requests.push({ method: req.method, path: url.pathname, query: Object.fromEntries(url.searchParams.entries()), body });
|
|
41
|
+
res.setHeader("content-type", "application/json");
|
|
42
|
+
|
|
43
|
+
if (url.pathname === "/api/employees/email-search") {
|
|
44
|
+
const email = url.searchParams.get("email");
|
|
45
|
+
res.end(JSON.stringify({
|
|
46
|
+
success: true,
|
|
47
|
+
data: {
|
|
48
|
+
first_name: email.startsWith("grace") ? "Grace" : "Ada",
|
|
49
|
+
last_name: "Tester",
|
|
50
|
+
email,
|
|
51
|
+
title: "Founder",
|
|
52
|
+
company_url: "example.com",
|
|
53
|
+
employee_linkedin: `https://linkedin.com/in/${email.split("@")[0]}`,
|
|
54
|
+
},
|
|
55
|
+
meta: { credits_used: 1, remaining_credits: 1000 },
|
|
56
|
+
}));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (url.pathname === "/api/employees/dataset-search") {
|
|
61
|
+
const companyUrl = url.searchParams.get("company_url");
|
|
62
|
+
res.end(JSON.stringify({
|
|
63
|
+
success: true,
|
|
64
|
+
data: [{
|
|
65
|
+
first_name: companyUrl.startsWith("acme") ? "Ari" : "Bea",
|
|
66
|
+
last_name: "Founder",
|
|
67
|
+
title: url.searchParams.get("title") || "Founder",
|
|
68
|
+
email: `founder@${companyUrl}`,
|
|
69
|
+
employee_linkedin: `https://linkedin.com/in/${companyUrl}-founder`,
|
|
70
|
+
company_url: companyUrl,
|
|
71
|
+
company_name: companyUrl.split(".")[0],
|
|
72
|
+
has_email: true,
|
|
73
|
+
}],
|
|
74
|
+
meta: { page: 1, per_page: 20, total: 1, last_page: 1, credits_used: 1, remaining_credits: 999 },
|
|
75
|
+
}));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (url.pathname === "/api/companies/company-finder") {
|
|
80
|
+
res.end(JSON.stringify({
|
|
81
|
+
success: true,
|
|
82
|
+
data: [
|
|
83
|
+
{ company_name: "Acme", url: "acme.test", email_domain: "acme.test", services: ["DevOps"], country_code: "US" },
|
|
84
|
+
{ company_name: "Beta", url: "beta.test", email_domain: "beta.test", services: ["DevOps"], country_code: "US" },
|
|
85
|
+
],
|
|
86
|
+
meta: { page: body?.page || 1, per_page: body?.per_page || 2, total: 2, last_page: 1, credits_used: 2, remaining_credits: 998 },
|
|
87
|
+
}));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
res.statusCode = 404;
|
|
92
|
+
res.end(JSON.stringify({ error: "not found" }));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
96
|
+
const { port } = server.address();
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
process.env.QUICKENRICH_API_KEY = "killer-contract-key";
|
|
100
|
+
process.env.QUICKENRICH_API_BASE_URL = `http://127.0.0.1:${port}`;
|
|
101
|
+
process.env.QUICKENRICH_MCP_CACHE_DIR = join(tmp, "cache");
|
|
102
|
+
process.env.QUICKENRICH_MCP_SHARED_THROTTLE_DIR = join(tmp, "throttle");
|
|
103
|
+
process.env.QUICKENRICH_MCP_LEDGER_PATH = join(tmp, "credits.jsonl");
|
|
104
|
+
|
|
105
|
+
const client = createClient();
|
|
106
|
+
const emailFile = join(tmp, "emails.csv");
|
|
107
|
+
const reverseOut = join(tmp, "reverse.jsonl");
|
|
108
|
+
const reverseResume = join(tmp, "reverse-resume.json");
|
|
109
|
+
writeFileSync(emailFile, "email\nada@example.com\ngrace@example.com\nada@example.com\n");
|
|
110
|
+
writeFileSync(reverseResume, `${JSON.stringify({ completed: ["ada@example.com"] })}\n`);
|
|
111
|
+
|
|
112
|
+
const reverse = await callQuickEnrichTool("quickenrich_batch_reverse_email", {
|
|
113
|
+
input_file: emailFile,
|
|
114
|
+
output_file: reverseOut,
|
|
115
|
+
resume_file: reverseResume,
|
|
116
|
+
cache_mode: "readwrite",
|
|
117
|
+
credit_ledger: true,
|
|
118
|
+
projection: "emails",
|
|
119
|
+
response: { mode: "full" },
|
|
120
|
+
}, client);
|
|
121
|
+
assert(reverse.requestCount === 1, `Resume file should skip completed/collapsed emails; got ${reverse.requestCount}.`);
|
|
122
|
+
assert(reverse.foundCount === 1, `Expected one new reverse-email result, got ${reverse.foundCount}.`);
|
|
123
|
+
assert(reverse.output?.path === reverseOut, "Reverse batch should return output_file metadata.");
|
|
124
|
+
assert(reverse.resume?.path === reverseResume, "Reverse batch should return resume_file metadata.");
|
|
125
|
+
assert(readJson(reverseResume).completed.includes("grace@example.com"), "Resume file should be updated durably.");
|
|
126
|
+
assert(readJsonl(reverseOut).length === 1, "JSONL output file should contain exactly the newly processed result.");
|
|
127
|
+
assert(!("title" in reverse.results[0]), "emails projection should strip non-email fields from inline results.");
|
|
128
|
+
assert(reverse.results[0].quality?.match_status === "matched", "Projected result should retain compact quality metadata.");
|
|
129
|
+
|
|
130
|
+
const reverseRequestCount = requests.filter((entry) => entry.path === "/api/employees/email-search").length;
|
|
131
|
+
const cachedReverse = await callQuickEnrichTool("quickenrich_batch_reverse_email", {
|
|
132
|
+
input_file: emailFile,
|
|
133
|
+
resume_state: { completed: [] },
|
|
134
|
+
cache_mode: "readwrite",
|
|
135
|
+
credit_ledger: true,
|
|
136
|
+
projection: "emails",
|
|
137
|
+
response: { mode: "full" },
|
|
138
|
+
}, client);
|
|
139
|
+
const reverseRequestCountAfterCache = requests.filter((entry) => entry.path === "/api/employees/email-search").length;
|
|
140
|
+
assert(reverseRequestCountAfterCache === reverseRequestCount + 1, "Cache should avoid re-requesting grace@example.com while requesting uncached ada@example.com once.");
|
|
141
|
+
assert(cachedReverse.cache?.hits >= 1, "Batch should expose cache hit count.");
|
|
142
|
+
assert(existsSync(process.env.QUICKENRICH_MCP_LEDGER_PATH), "Credit ledger should be written when enabled.");
|
|
143
|
+
|
|
144
|
+
const sharedThrottlePlan = await callQuickEnrichTool("quickenrich_batch_pull_leads", {
|
|
145
|
+
mode: "domain_search",
|
|
146
|
+
companies: [{ company_url: "shared-throttle.test" }],
|
|
147
|
+
dry_run: true,
|
|
148
|
+
throttle_scope: "shared",
|
|
149
|
+
response: { mode: "full" },
|
|
150
|
+
}, client);
|
|
151
|
+
assert(sharedThrottlePlan.throttle?.scope === "shared", "Shared throttle dry-run should expose shared scope.");
|
|
152
|
+
|
|
153
|
+
const pipelineOut = join(tmp, "pipeline.jsonl");
|
|
154
|
+
const pipeline = await callQuickEnrichTool("quickenrich_pipeline", {
|
|
155
|
+
mode: "company_finder_to_domain_search",
|
|
156
|
+
filters: { services: ["DevOps"], country_code: ["US"] },
|
|
157
|
+
max_companies: 2,
|
|
158
|
+
title: "Founder",
|
|
159
|
+
output_file: pipelineOut,
|
|
160
|
+
projection: "people",
|
|
161
|
+
response: { mode: "full" },
|
|
162
|
+
}, client);
|
|
163
|
+
assert(pipeline.ok === true, "Native pipeline should succeed.");
|
|
164
|
+
assert(pipeline.steps.map((step) => step.tool).join(">") === "quickenrich_batch_pull_leads>quickenrich_batch_pull_leads", "Pipeline should expose compact step graph.");
|
|
165
|
+
assert(pipeline.companyCount === 2, `Expected 2 companies from pipeline, got ${pipeline.companyCount}.`);
|
|
166
|
+
assert(pipeline.leadCount === 2, `Expected 2 domain-search leads from pipeline, got ${pipeline.leadCount}.`);
|
|
167
|
+
assert(readJsonl(pipelineOut).length === 2, "Pipeline should stream JSONL results to output_file.");
|
|
168
|
+
assert(pipeline.leads.every((lead) => lead.quality?.source_endpoint), "Pipeline leads should carry quality metadata.");
|
|
169
|
+
|
|
170
|
+
const graph = agentGraph();
|
|
171
|
+
const agentToolNames = selectTools(TOOL_DEFINITIONS).map((tool) => tool.name);
|
|
172
|
+
assert(agentToolNames.includes("quickenrich_pipeline"), "Default agent surface should expose the native pipeline tool.");
|
|
173
|
+
assert(graph.bulkIO?.input_file && graph.bulkIO?.output_file && graph.bulkIO?.resume_file, "Agent graph should document file-handle bulk IO.");
|
|
174
|
+
assert(graph.cacheLedger?.toolArgs?.includes("cache_mode"), "Agent graph should document cache/ledger controls.");
|
|
175
|
+
assert(graph.projections?.modes?.includes("emails"), "Agent graph should document projection modes.");
|
|
176
|
+
assert(graph.evalBudgets?.toolsListBytes <= 8_000, "Agent graph should publish tools/list byte budget.");
|
|
177
|
+
|
|
178
|
+
const doctor = spawnSync(process.execPath, ["scripts/quickenrich-token.mjs", "doctor", "--json", "--config", join(tmp, "config.json")], {
|
|
179
|
+
cwd: join(new URL(".", import.meta.url).pathname, ".."),
|
|
180
|
+
env: { ...process.env, QUICKENRICH_API_KEY: "", QUICKENRICH_TOKEN: "" },
|
|
181
|
+
encoding: "utf8",
|
|
182
|
+
});
|
|
183
|
+
assert(doctor.status === 0, `doctor should exit 0: ${doctor.stderr || doctor.stdout}`);
|
|
184
|
+
const doctorPayload = JSON.parse(doctor.stdout);
|
|
185
|
+
assert(doctorPayload.ok === true, "doctor --json should report ok=true for local diagnostics.");
|
|
186
|
+
assert(doctorPayload.tools.defaultCount <= 9, "doctor should include compact tool-surface diagnostics.");
|
|
187
|
+
|
|
188
|
+
const installConfig = spawnSync(process.execPath, ["scripts/quickenrich-token.mjs", "install-config", "--json"], {
|
|
189
|
+
cwd: join(new URL(".", import.meta.url).pathname, ".."),
|
|
190
|
+
env: process.env,
|
|
191
|
+
encoding: "utf8",
|
|
192
|
+
});
|
|
193
|
+
assert(installConfig.status === 0, `install-config should exit 0: ${installConfig.stderr || installConfig.stdout}`);
|
|
194
|
+
assert(JSON.parse(installConfig.stdout).mcpServers?.["quick-enrich"], "install-config should print an MCP config snippet.");
|
|
195
|
+
|
|
196
|
+
console.log(JSON.stringify({
|
|
197
|
+
ok: true,
|
|
198
|
+
reverseRequests: reverse.requestCount,
|
|
199
|
+
cachedHits: cachedReverse.cache?.hits || 0,
|
|
200
|
+
pipelineLeads: pipeline.leadCount,
|
|
201
|
+
tools: agentToolNames.length,
|
|
202
|
+
}, null, 2));
|
|
203
|
+
} finally {
|
|
204
|
+
server.close();
|
|
205
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
206
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export const DEFAULT_BENCHMARK_TITLE = "Founder";
|
|
2
|
+
|
|
3
|
+
export function makeDuplicateDomainBenchmark(options = {}) {
|
|
4
|
+
const uniqueCount = clampInteger(options.uniqueCount, 1, 100, 6);
|
|
5
|
+
const duplicateFactor = clampInteger(options.duplicateFactor, 1, 20, 2);
|
|
6
|
+
const prefix = safeDomainPrefix(options.prefix || `qe-mcp-bench-${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
|
7
|
+
const title = String(options.title || DEFAULT_BENCHMARK_TITLE).trim() || DEFAULT_BENCHMARK_TITLE;
|
|
8
|
+
const uniqueCompanies = Array.from({ length: uniqueCount }, (_, index) => ({
|
|
9
|
+
company_url: `${prefix}-${index + 1}.com`,
|
|
10
|
+
title,
|
|
11
|
+
}));
|
|
12
|
+
const companies = uniqueCompanies.flatMap((company) => (
|
|
13
|
+
Array.from({ length: duplicateFactor }, () => ({ ...company }))
|
|
14
|
+
));
|
|
15
|
+
return {
|
|
16
|
+
title,
|
|
17
|
+
uniqueCount,
|
|
18
|
+
duplicateFactor,
|
|
19
|
+
inputCount: companies.length,
|
|
20
|
+
coalescedCount: companies.length - uniqueCompanies.length,
|
|
21
|
+
companies,
|
|
22
|
+
uniqueCompanies,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function summarizeBenchmarkComparison({
|
|
27
|
+
rows,
|
|
28
|
+
baselineLabel = "raw_sequential",
|
|
29
|
+
candidateLabel = "mcp_batch_stdio",
|
|
30
|
+
optimizedLabel = "raw_concurrent_deduped",
|
|
31
|
+
} = {}) {
|
|
32
|
+
const list = Array.isArray(rows) ? rows : [];
|
|
33
|
+
const baseline = requireRow(list, baselineLabel);
|
|
34
|
+
const candidate = requireRow(list, candidateLabel);
|
|
35
|
+
const optimized = optimizedLabel ? list.find((row) => row.label === optimizedLabel) : undefined;
|
|
36
|
+
return {
|
|
37
|
+
baselineLabel,
|
|
38
|
+
candidateLabel,
|
|
39
|
+
optimizedLabel: optimized?.label,
|
|
40
|
+
baselineDurationMs: roundNumber(baseline.durationMs),
|
|
41
|
+
candidateDurationMs: roundNumber(candidate.durationMs),
|
|
42
|
+
baselineRequestCount: roundNumber(baseline.requestCount),
|
|
43
|
+
candidateRequestCount: roundNumber(candidate.requestCount),
|
|
44
|
+
candidateFasterThanBaseline: Number(candidate.durationMs) < Number(baseline.durationMs),
|
|
45
|
+
speedupVsBaseline: ratio(baseline.durationMs, candidate.durationMs),
|
|
46
|
+
requestReductionVsBaseline: ratio(baseline.requestCount, candidate.requestCount),
|
|
47
|
+
optimizedDeltaPct: optimized ? percentDelta(candidate.durationMs, optimized.durationMs) : undefined,
|
|
48
|
+
totalCreditsUsed: roundNumber(list.reduce((sum, row) => sum + Number(row.creditsUsed || 0), 0)),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function requireRow(rows, label) {
|
|
53
|
+
const row = rows.find((entry) => entry.label === label);
|
|
54
|
+
if (!row) throw new Error(`Missing benchmark row: ${label}`);
|
|
55
|
+
return row;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function safeDomainPrefix(value) {
|
|
59
|
+
const cleaned = String(value || "")
|
|
60
|
+
.toLowerCase()
|
|
61
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
62
|
+
.replace(/^-+|-+$/g, "")
|
|
63
|
+
.replace(/--+/g, "-")
|
|
64
|
+
.slice(0, 44);
|
|
65
|
+
return cleaned || "qe-mcp-bench";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function clampInteger(value, min, max, fallback) {
|
|
69
|
+
const number = Number.parseInt(value, 10);
|
|
70
|
+
if (!Number.isFinite(number)) return fallback;
|
|
71
|
+
return Math.min(max, Math.max(min, number));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function ratio(numerator, denominator) {
|
|
75
|
+
const top = Number(numerator);
|
|
76
|
+
const bottom = Number(denominator);
|
|
77
|
+
if (!Number.isFinite(top) || !Number.isFinite(bottom) || bottom <= 0) return undefined;
|
|
78
|
+
return roundNumber(top / bottom);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function percentDelta(value, baseline) {
|
|
82
|
+
const current = Number(value);
|
|
83
|
+
const base = Number(baseline);
|
|
84
|
+
if (!Number.isFinite(current) || !Number.isFinite(base) || base <= 0) return undefined;
|
|
85
|
+
return roundNumber(((current - base) / base) * 100);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function roundNumber(value) {
|
|
89
|
+
const number = Number(value);
|
|
90
|
+
if (!Number.isFinite(number)) return undefined;
|
|
91
|
+
return Math.round(number * 100) / 100;
|
|
92
|
+
}
|