@besales/mcp 0.45.0 → 0.47.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/README.md +96 -55
- package/dist/auth/connection-store.d.ts +21 -1
- package/dist/auth/connection-store.js +46 -2
- package/dist/auth/connection-store.js.map +1 -1
- package/dist/auth/oauth-client.d.ts +5 -1
- package/dist/auth/oauth-client.js +22 -12
- package/dist/auth/oauth-client.js.map +1 -1
- package/dist/cli.js +14 -5
- package/dist/cli.js.map +1 -1
- package/dist/http/api-client.d.ts +16 -1
- package/dist/http/api-client.js +50 -2
- package/dist/http/api-client.js.map +1 -1
- package/dist/instructions/server-instructions.d.ts +4 -13
- package/dist/instructions/server-instructions.js +68 -274
- package/dist/instructions/server-instructions.js.map +1 -1
- package/dist/prompts/registry.d.ts +15 -0
- package/dist/prompts/registry.js +83 -0
- package/dist/prompts/registry.js.map +1 -0
- package/dist/resources/concepts/feedback-sheets.md +7 -4
- package/dist/resources/concepts/handoff.md +4 -1
- package/dist/resources/concepts/sandbox.md +14 -5
- package/dist/resources/concepts/triggers.md +6 -2
- package/dist/resources/concepts/workbook-classification.md +26 -21
- package/dist/resources/docs/campaigns.md +20 -0
- package/dist/resources/docs/crm-integration.md +3 -1
- package/dist/resources/docs/follow-up.md +19 -0
- package/dist/resources/docs/platforms.md +2 -2
- package/dist/resources/docs/routing.md +18 -0
- package/dist/resources/docs/task-state.md +30 -0
- package/dist/resources/docs/trigger-actions.md +28 -0
- package/dist/resources/docs/variables.md +18 -0
- package/dist/resources/registry.js +51 -2
- package/dist/resources/registry.js.map +1 -1
- package/dist/resources/workflows/compare-models.md +4 -3
- package/dist/resources/workflows/connect-crm-from-scratch.md +8 -0
- package/dist/resources/workflows/extract-from-document.md +8 -1
- package/dist/resources/workflows/iterate-with-sandbox.md +5 -2
- package/dist/resources/workflows/platform-setup-from-scratch.md +90 -2
- package/dist/resources/workflows/production-readiness-check.md +22 -18
- package/dist/schemas/mcp-tools.json +1537 -177
- package/dist/server.d.ts +2 -0
- package/dist/server.js +8 -2
- package/dist/server.js.map +1 -1
- package/dist/tasks/task-state-store.d.ts +68 -0
- package/dist/tasks/task-state-store.js +278 -0
- package/dist/tasks/task-state-store.js.map +1 -0
- package/dist/tools/annotations.d.ts +7 -0
- package/dist/tools/annotations.js +16 -0
- package/dist/tools/annotations.js.map +1 -0
- package/dist/tools/contracts.js +7 -1
- package/dist/tools/contracts.js.map +1 -1
- package/dist/tools/definitions/agent-design.d.ts +49 -20
- package/dist/tools/definitions/agent-design.js +192 -19
- package/dist/tools/definitions/agent-design.js.map +1 -1
- package/dist/tools/definitions/analytics.d.ts +40 -0
- package/dist/tools/definitions/analytics.js +233 -0
- package/dist/tools/definitions/analytics.js.map +1 -0
- package/dist/tools/definitions/broadcast-analytics.d.ts +5 -4
- package/dist/tools/definitions/broadcast-analytics.js +26 -4
- package/dist/tools/definitions/broadcast-analytics.js.map +1 -1
- package/dist/tools/definitions/broadcast-mailing-variants.d.ts +2 -0
- package/dist/tools/definitions/broadcast-mailing-variants.js +10 -0
- package/dist/tools/definitions/broadcast-mailing-variants.js.map +1 -1
- package/dist/tools/definitions/broadcast-scheduler.d.ts +20 -1
- package/dist/tools/definitions/broadcast-scheduler.js +134 -8
- package/dist/tools/definitions/broadcast-scheduler.js.map +1 -1
- package/dist/tools/definitions/contacts.d.ts +15 -0
- package/dist/tools/definitions/contacts.js +84 -0
- package/dist/tools/definitions/contacts.js.map +1 -0
- package/dist/tools/definitions/crm-platform.d.ts +2 -0
- package/dist/tools/definitions/crm-platform.js +95 -5
- package/dist/tools/definitions/crm-platform.js.map +1 -1
- package/dist/tools/definitions/datasource.d.ts +1 -0
- package/dist/tools/definitions/datasource.js +16 -1
- package/dist/tools/definitions/datasource.js.map +1 -1
- package/dist/tools/definitions/dialogue-audit.d.ts +2 -2
- package/dist/tools/definitions/dialogue-audit.js +54 -16
- package/dist/tools/definitions/dialogue-audit.js.map +1 -1
- package/dist/tools/definitions/dialogue-export.d.ts +1 -1
- package/dist/tools/definitions/dialogue-export.js +12 -0
- package/dist/tools/definitions/dialogue-export.js.map +1 -1
- package/dist/tools/definitions/external-api-channels.js +39 -12
- package/dist/tools/definitions/external-api-channels.js.map +1 -1
- package/dist/tools/definitions/follow-up-funnel-test.js +4 -0
- package/dist/tools/definitions/follow-up-funnel-test.js.map +1 -1
- package/dist/tools/definitions/follow-up-setup.js +3 -0
- package/dist/tools/definitions/follow-up-setup.js.map +1 -1
- package/dist/tools/definitions/follow-up.d.ts +1 -1
- package/dist/tools/definitions/follow-up.js +6 -0
- package/dist/tools/definitions/follow-up.js.map +1 -1
- package/dist/tools/definitions/getcourse-anchor-refresh.d.ts +3 -0
- package/dist/tools/definitions/getcourse-anchor-refresh.js +29 -2
- package/dist/tools/definitions/getcourse-anchor-refresh.js.map +1 -1
- package/dist/tools/definitions/getcourse-cold-welcome.d.ts +20 -2
- package/dist/tools/definitions/getcourse-cold-welcome.js +128 -7
- package/dist/tools/definitions/getcourse-cold-welcome.js.map +1 -1
- package/dist/tools/definitions/getcourse-wave.js +14 -0
- package/dist/tools/definitions/getcourse-wave.js.map +1 -1
- package/dist/tools/definitions/help.d.ts +8 -0
- package/dist/tools/definitions/help.js +219 -0
- package/dist/tools/definitions/help.js.map +1 -0
- package/dist/tools/definitions/knowledge.d.ts +27 -5
- package/dist/tools/definitions/knowledge.js +200 -33
- package/dist/tools/definitions/knowledge.js.map +1 -1
- package/dist/tools/definitions/link-tracking.js +31 -0
- package/dist/tools/definitions/link-tracking.js.map +1 -1
- package/dist/tools/definitions/model-comparison.js +12 -3
- package/dist/tools/definitions/model-comparison.js.map +1 -1
- package/dist/tools/definitions/operational-overview.d.ts +3 -0
- package/dist/tools/definitions/operational-overview.js +23 -0
- package/dist/tools/definitions/operational-overview.js.map +1 -0
- package/dist/tools/definitions/operator-workflow.d.ts +20 -0
- package/dist/tools/definitions/operator-workflow.js +114 -0
- package/dist/tools/definitions/operator-workflow.js.map +1 -0
- package/dist/tools/definitions/platform-setup.d.ts +54 -3
- package/dist/tools/definitions/platform-setup.js +211 -5
- package/dist/tools/definitions/platform-setup.js.map +1 -1
- package/dist/tools/definitions/scheduled-tasks.js +2 -0
- package/dist/tools/definitions/scheduled-tasks.js.map +1 -1
- package/dist/tools/definitions/session.js +103 -5
- package/dist/tools/definitions/session.js.map +1 -1
- package/dist/tools/definitions/shared.d.ts +272 -10
- package/dist/tools/definitions/shared.js +154 -8
- package/dist/tools/definitions/shared.js.map +1 -1
- package/dist/tools/definitions/task-state.d.ts +76 -0
- package/dist/tools/definitions/task-state.js +301 -0
- package/dist/tools/definitions/task-state.js.map +1 -0
- package/dist/tools/definitions/trigger-contact-claims.d.ts +7 -0
- package/dist/tools/definitions/trigger-contact-claims.js +60 -0
- package/dist/tools/definitions/trigger-contact-claims.js.map +1 -0
- package/dist/tools/definitions.d.ts +334 -35
- package/dist/tools/definitions.js +14 -0
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/description-localization.d.ts +2 -0
- package/dist/tools/description-localization.js +46 -0
- package/dist/tools/description-localization.js.map +1 -0
- package/dist/tools/incomplete-list.d.ts +23 -0
- package/dist/tools/incomplete-list.js +39 -0
- package/dist/tools/incomplete-list.js.map +1 -0
- package/dist/tools/inventory.d.ts +10 -0
- package/dist/tools/inventory.js +49 -0
- package/dist/tools/inventory.js.map +1 -0
- package/dist/tools/registry-mapping-cases-core.js +122 -4
- package/dist/tools/registry-mapping-cases-core.js.map +1 -1
- package/dist/tools/registry-mapping-cases-platform.js +69 -0
- package/dist/tools/registry-mapping-cases-platform.js.map +1 -1
- package/dist/tools/registry.d.ts +2 -0
- package/dist/tools/registry.js +60 -8
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/result-envelope.d.ts +46 -0
- package/dist/tools/result-envelope.js +184 -0
- package/dist/tools/result-envelope.js.map +1 -0
- package/dist/tools/result.d.ts +41 -3
- package/dist/tools/result.js +229 -27
- package/dist/tools/result.js.map +1 -1
- package/dist/tools/tool-title.d.ts +2 -0
- package/dist/tools/tool-title.js +12 -0
- package/dist/tools/tool-title.js.map +1 -0
- package/dist/tools/toolsets.d.ts +37 -0
- package/dist/tools/toolsets.js +64 -0
- package/dist/tools/toolsets.js.map +1 -0
- package/dist/tools/trigger-action-config.js +4 -0
- package/dist/tools/trigger-action-config.js.map +1 -1
- package/dist/tools/verify-with.d.ts +5 -0
- package/dist/tools/verify-with.js +36 -0
- package/dist/tools/verify-with.js.map +1 -0
- package/dist/types/api-contract.gen.d.ts +2612 -332
- package/dist/utils/logger.d.ts +11 -0
- package/dist/utils/logger.js +64 -8
- package/dist/utils/logger.js.map +1 -1
- package/docs/agent-usage-guide.full.ru.md +21 -15
- package/docs/agent-usage-guide.ru.md +24 -16
- package/docs/async-operation-lifecycle.md +46 -0
- package/docs/connect.ru.md +120 -0
- package/docs/evals/model-behavior-scorecard.md +80 -0
- package/docs/failure-semantics.md +56 -0
- package/docs/help-navigation.md +48 -0
- package/docs/host-compatibility-matrix.md +153 -0
- package/docs/host-setup.md +22 -6
- package/docs/operation-coverage.md +114 -0
- package/docs/pii-policy.md +46 -0
- package/docs/response-size-budget.md +150 -0
- package/docs/task-state.md +58 -0
- package/docs/tool-effects.md +46 -0
- package/docs/tool-surface.md +91 -0
- package/package.json +6 -2
- package/scripts/model-eval-server.js +213 -0
- package/scripts/probe-mcp-protocol.js +134 -0
- package/scripts/run-model-eval.js +447 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
|
|
7
|
+
const currentRoot = resolve(process.env.BESALES_MCP_EVAL_CURRENT ?? '.');
|
|
8
|
+
const baselineRoot = resolve(
|
|
9
|
+
process.env.BESALES_MCP_EVAL_BASELINE ??
|
|
10
|
+
'/Users/peresvets/apps/animaly/.codex-worktrees/besales-mcp-baseline',
|
|
11
|
+
);
|
|
12
|
+
const serverScript = resolve(currentRoot, 'scripts/model-eval-server.js');
|
|
13
|
+
const outputRoot = resolve(
|
|
14
|
+
process.env.BESALES_MCP_EVAL_OUTPUT ??
|
|
15
|
+
resolve(currentRoot, 'docs/evidence/model-eval-2026-09-11'),
|
|
16
|
+
);
|
|
17
|
+
const emptyRoot = resolve('/tmp/besales-model-eval-empty');
|
|
18
|
+
const requestedMode = process.argv[2] ?? 'all';
|
|
19
|
+
const repetitions = Number(process.env.BESALES_MCP_EVAL_REPETITIONS ?? 3);
|
|
20
|
+
const model = process.env.BESALES_MCP_EVAL_MODEL ?? 'gpt-5.6-luna';
|
|
21
|
+
const reasoningEffort = process.env.BESALES_MCP_EVAL_REASONING ?? 'low';
|
|
22
|
+
const requestedScenarios = new Set(
|
|
23
|
+
(process.env.BESALES_MCP_EVAL_SCENARIOS ?? '')
|
|
24
|
+
.split(',')
|
|
25
|
+
.map((scenario) => scenario.trim())
|
|
26
|
+
.filter(Boolean),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const scenarios = [
|
|
30
|
+
{
|
|
31
|
+
id: 'faq',
|
|
32
|
+
toolsets: 'workspace,help',
|
|
33
|
+
prompt:
|
|
34
|
+
'В тестовом read-only контуре Besales MCP пользователь пишет: «Хочу нового FAQ-бота для Telegram по ответам из базы знаний». Проверь фактический маршрут через доступный Besales MCP обычным пользовательским запросом, не угадывай его по общим знаниям. Составь проверяемую последовательность и остановись перед первой записью. Не проси технические ID и ничего не изменяй.',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'analytics',
|
|
38
|
+
toolsets: 'workspace,help,broadcast',
|
|
39
|
+
prompt:
|
|
40
|
+
'В тестовом read-only контуре Besales MCP пользователь пишет: «Покажи аналитику рассылок за 1–10 сентября: отправлено, ответы, конверсия и выручка по каналам». Проверь фактические данные через доступный Besales MCP и выполни только нужные чтения. Сохрани правильные период, view и зерно; ничего не изменяй.',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'silence',
|
|
44
|
+
toolsets: 'workspace,help,contact',
|
|
45
|
+
prompt:
|
|
46
|
+
'В тестовом read-only контуре Besales MCP пользователь спрашивает: «бот молчит у клиента Test Contact, почему?». Проверь фактическое состояние через доступный Besales MCP обычным пользовательским запросом, ничего не изменяй и дай краткий проверяемый ответ.',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'followup',
|
|
50
|
+
toolsets: 'workspace,help,followup',
|
|
51
|
+
prompt:
|
|
52
|
+
'В тестовом read-only контуре Besales MCP пользователь спрашивает: «Какие у нас работают дожимы, и на каком шаге люди отвечают?». Проверь фактические цепочки и статистику через доступный Besales MCP, отличи выключенный черновик от сбоя и ничего не изменяй.',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'unsupported',
|
|
56
|
+
toolsets: 'workspace,help',
|
|
57
|
+
prompt:
|
|
58
|
+
'В тестовом read-only контуре Besales MCP пользователь просит: «Измени темы диалогов и покажи satisfaction». Проверь доступные возможности через Besales MCP. Если scoped MCP-пути нет, честно назови границу и условие продолжения; не выдумывай tool или внутренний URL и ничего не изменяй.',
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const selectedScenarios = scenarios.filter(
|
|
63
|
+
(scenario) => requestedScenarios.size === 0 || requestedScenarios.has(scenario.id),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const scoreOnly = requestedMode === 'score';
|
|
67
|
+
const modes = requestedMode === 'all' ? ['baseline', 'current'] : scoreOnly ? [] : [requestedMode];
|
|
68
|
+
if (!modes.every((mode) => mode === 'baseline' || mode === 'current')) {
|
|
69
|
+
throw new Error('Usage: node scripts/run-model-eval.js [baseline|current|all|score]');
|
|
70
|
+
}
|
|
71
|
+
if (!Number.isInteger(repetitions) || repetitions < 1 || repetitions > 10) {
|
|
72
|
+
throw new Error('BESALES_MCP_EVAL_REPETITIONS must be an integer from 1 to 10');
|
|
73
|
+
}
|
|
74
|
+
if (selectedScenarios.length === 0) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`BESALES_MCP_EVAL_SCENARIOS must contain one of: ${scenarios.map((scenario) => scenario.id).join(', ')}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await mkdir(outputRoot, { recursive: true });
|
|
81
|
+
await mkdir(emptyRoot, { recursive: true });
|
|
82
|
+
|
|
83
|
+
const existingResults =
|
|
84
|
+
modes.length < 2
|
|
85
|
+
? await readFile(resolve(outputRoot, 'results.json'), 'utf8')
|
|
86
|
+
.then((value) => JSON.parse(value).results ?? [])
|
|
87
|
+
.catch(() => [])
|
|
88
|
+
: [];
|
|
89
|
+
const selectedScenarioIds = new Set(selectedScenarios.map((scenario) => scenario.id));
|
|
90
|
+
const results = scoreOnly
|
|
91
|
+
? await Promise.all(existingResults.map((row) => {
|
|
92
|
+
const prefix = `${row.mode}-${row.scenario}-${row.repetition}`;
|
|
93
|
+
return collectMetrics({ ...row,
|
|
94
|
+
run: { exitCode: row.exitCode, stderr: row.stderr },
|
|
95
|
+
callsPath: resolve(outputRoot, `${prefix}-calls.jsonl`),
|
|
96
|
+
eventsPath: resolve(outputRoot, `${prefix}-events.jsonl`),
|
|
97
|
+
finalPath: resolve(outputRoot, `${prefix}-final.txt`),
|
|
98
|
+
});
|
|
99
|
+
}))
|
|
100
|
+
: existingResults.filter(
|
|
101
|
+
(row) => !(modes.includes(row.mode) && selectedScenarioIds.has(row.scenario)),
|
|
102
|
+
);
|
|
103
|
+
for (const mode of modes) {
|
|
104
|
+
const buildRoot = resolve(mode === 'baseline' ? baselineRoot : currentRoot, 'dist');
|
|
105
|
+
for (const scenario of selectedScenarios) {
|
|
106
|
+
for (let repetition = 1; repetition <= repetitions; repetition += 1) {
|
|
107
|
+
const prefix = `${mode}-${scenario.id}-${repetition}`;
|
|
108
|
+
const callsPath = resolve(outputRoot, `${prefix}-calls.jsonl`);
|
|
109
|
+
const eventsPath = resolve(outputRoot, `${prefix}-events.jsonl`);
|
|
110
|
+
const finalPath = resolve(outputRoot, `${prefix}-final.txt`);
|
|
111
|
+
const startedAt = Date.now();
|
|
112
|
+
process.stdout.write(`RUN ${prefix}\n`);
|
|
113
|
+
await writeFile(callsPath, '');
|
|
114
|
+
const run = await runCodex({
|
|
115
|
+
buildRoot,
|
|
116
|
+
callsPath,
|
|
117
|
+
eventsPath,
|
|
118
|
+
finalPath,
|
|
119
|
+
prompt: scenario.prompt,
|
|
120
|
+
toolsets: mode === 'current' ? scenario.toolsets : '',
|
|
121
|
+
model,
|
|
122
|
+
reasoningEffort,
|
|
123
|
+
});
|
|
124
|
+
const metrics = await collectMetrics({
|
|
125
|
+
mode,
|
|
126
|
+
scenario: scenario.id,
|
|
127
|
+
repetition,
|
|
128
|
+
run,
|
|
129
|
+
callsPath,
|
|
130
|
+
eventsPath,
|
|
131
|
+
finalPath,
|
|
132
|
+
wallTimeMs: Date.now() - startedAt,
|
|
133
|
+
});
|
|
134
|
+
results.push(metrics);
|
|
135
|
+
process.stdout.write(
|
|
136
|
+
`DONE ${prefix} success=${String(metrics.routeSuccess)} calls=${metrics.calls.length} input=${metrics.usage.inputTokens ?? 'unknown'}\n`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const summary = summarize(results);
|
|
143
|
+
const assessment = assess(summary, results);
|
|
144
|
+
const metadata = {
|
|
145
|
+
model,
|
|
146
|
+
reasoningEffort,
|
|
147
|
+
repetitions,
|
|
148
|
+
scenarios: selectedScenarios.map(({ id, toolsets }) => ({ id, toolsets })),
|
|
149
|
+
};
|
|
150
|
+
await writeFile(
|
|
151
|
+
resolve(outputRoot, 'results.json'),
|
|
152
|
+
`${JSON.stringify({ metadata, results, summary, assessment }, null, 2)}\n`,
|
|
153
|
+
);
|
|
154
|
+
await writeFile(
|
|
155
|
+
resolve(outputRoot, 'results.md'),
|
|
156
|
+
renderMarkdown(metadata, summary, assessment, results),
|
|
157
|
+
);
|
|
158
|
+
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
|
|
159
|
+
|
|
160
|
+
function runCodex(input) {
|
|
161
|
+
const envEntries = [
|
|
162
|
+
`BESALES_MCP_EVAL_BUILD="${input.buildRoot}"`,
|
|
163
|
+
`BESALES_MCP_EVAL_LOG="${input.callsPath}"`,
|
|
164
|
+
];
|
|
165
|
+
if (input.toolsets) envEntries.push(`BESALES_MCP_TOOLSETS="${input.toolsets}"`);
|
|
166
|
+
const args = [
|
|
167
|
+
'exec',
|
|
168
|
+
'--ignore-user-config',
|
|
169
|
+
'--ignore-rules',
|
|
170
|
+
'--ephemeral',
|
|
171
|
+
'--skip-git-repo-check',
|
|
172
|
+
'-C',
|
|
173
|
+
emptyRoot,
|
|
174
|
+
'-s',
|
|
175
|
+
'read-only',
|
|
176
|
+
'-m',
|
|
177
|
+
input.model,
|
|
178
|
+
'-c',
|
|
179
|
+
`model_reasoning_effort="${input.reasoningEffort}"`,
|
|
180
|
+
'-c',
|
|
181
|
+
'mcp_servers.besales.command="node"',
|
|
182
|
+
'-c',
|
|
183
|
+
`mcp_servers.besales.args=["${serverScript}"]`,
|
|
184
|
+
'-c',
|
|
185
|
+
`mcp_servers.besales.env={${envEntries.join(',')}}`,
|
|
186
|
+
'--json',
|
|
187
|
+
'-o',
|
|
188
|
+
input.finalPath,
|
|
189
|
+
input.prompt,
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
return new Promise((resolveRun) => {
|
|
193
|
+
const child = spawn('codex', args, { cwd: emptyRoot, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
194
|
+
let stdout = '';
|
|
195
|
+
let stderr = '';
|
|
196
|
+
child.stdout.setEncoding('utf8');
|
|
197
|
+
child.stderr.setEncoding('utf8');
|
|
198
|
+
child.stdout.on('data', (chunk) => {
|
|
199
|
+
stdout += chunk;
|
|
200
|
+
});
|
|
201
|
+
child.stderr.on('data', (chunk) => {
|
|
202
|
+
stderr += chunk;
|
|
203
|
+
});
|
|
204
|
+
child.on('close', async (code) => {
|
|
205
|
+
await writeFile(input.eventsPath, stdout);
|
|
206
|
+
resolveRun({
|
|
207
|
+
exitCode: code,
|
|
208
|
+
stderr: code === 0 ? '' : stderr.slice(-4000),
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function collectMetrics(input) {
|
|
215
|
+
const [eventsText, finalText, callsText] = await Promise.all([
|
|
216
|
+
readFile(input.eventsPath, 'utf8').catch(() => ''),
|
|
217
|
+
readFile(input.finalPath, 'utf8').catch(() => ''),
|
|
218
|
+
readFile(input.callsPath, 'utf8').catch(() => ''),
|
|
219
|
+
]);
|
|
220
|
+
const events = parseJsonLines(eventsText);
|
|
221
|
+
const loggedCalls = parseJsonLines(callsText);
|
|
222
|
+
const callItems = events
|
|
223
|
+
.filter((event) => event.type === 'item.completed' && event.item?.type === 'mcp_tool_call')
|
|
224
|
+
.map((event) => event.item);
|
|
225
|
+
const calls = callItems.map((item) => item.tool);
|
|
226
|
+
const usageEvent = [...events].reverse().find((event) => event.type === 'turn.completed');
|
|
227
|
+
const attemptedMutation = loggedCalls.some((call) => call.mutating === true);
|
|
228
|
+
const approvalBlocked = callItems.some((item) => item.error?.message?.includes('requires approval'));
|
|
229
|
+
const successfulCalls = callItems.filter((item) => item.error == null && item.result != null)
|
|
230
|
+
.map((item) => item.tool);
|
|
231
|
+
const callSignatures = loggedCalls.map((call) =>
|
|
232
|
+
JSON.stringify({ name: call.name, args: call.args }),
|
|
233
|
+
);
|
|
234
|
+
const repeatedCalls = callSignatures.length - new Set(callSignatures).size;
|
|
235
|
+
const wrongCalls = calls.filter((call) => !allowedCalls(input.scenario).has(call)).length;
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
mode: input.mode,
|
|
239
|
+
scenario: input.scenario,
|
|
240
|
+
repetition: input.repetition,
|
|
241
|
+
exitCode: input.run.exitCode,
|
|
242
|
+
stderr: input.run.stderr,
|
|
243
|
+
routeSuccess: routeSuccess(input.scenario, successfulCalls, finalText),
|
|
244
|
+
falseSuccess: /(?:создан|изменен|изменён|запущен|отправлен)[^.]{0,40}(?:успеш|готов)/iu.test(finalText),
|
|
245
|
+
attemptedMutation,
|
|
246
|
+
approvalBlocked,
|
|
247
|
+
completedTurn: usageEvent !== undefined,
|
|
248
|
+
helpDiscovery: successfulCalls.includes('besales_help_search'),
|
|
249
|
+
repeatedCalls,
|
|
250
|
+
wrongCalls,
|
|
251
|
+
unnecessaryQuestion: /\?\s*$/u.test(finalText.trim()),
|
|
252
|
+
calls,
|
|
253
|
+
final: finalText.trim(),
|
|
254
|
+
wallTimeMs: input.wallTimeMs,
|
|
255
|
+
usage: {
|
|
256
|
+
inputTokens: usageEvent?.usage?.input_tokens ?? null,
|
|
257
|
+
cachedInputTokens: usageEvent?.usage?.cached_input_tokens ?? null,
|
|
258
|
+
outputTokens: usageEvent?.usage?.output_tokens ?? null,
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function parseJsonLines(text) {
|
|
264
|
+
return text
|
|
265
|
+
.split('\n')
|
|
266
|
+
.filter(Boolean)
|
|
267
|
+
.flatMap((line) => {
|
|
268
|
+
try {
|
|
269
|
+
return [JSON.parse(line)];
|
|
270
|
+
} catch {
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function routeSuccess(scenario, calls, finalText) {
|
|
277
|
+
const normalized = finalText.toLocaleLowerCase('ru-RU');
|
|
278
|
+
if (scenario === 'faq') {
|
|
279
|
+
return (
|
|
280
|
+
(calls.includes('besales_workspace_navigate') || calls.includes('besales_help_search')) &&
|
|
281
|
+
/знан|knowledge/u.test(normalized) &&
|
|
282
|
+
/sandbox|тест/u.test(normalized)
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
if (scenario === 'analytics') {
|
|
286
|
+
return (
|
|
287
|
+
calls.includes('besales_broadcast_analytics_get') &&
|
|
288
|
+
/cohort|period|entry|зерн|view/u.test(normalized) &&
|
|
289
|
+
/summary|итог|total|выруч/u.test(normalized)
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
if (scenario === 'silence') {
|
|
293
|
+
return calls.includes('besales_contact_diagnostics_get') && /waiting_operator|оператор/u.test(normalized);
|
|
294
|
+
}
|
|
295
|
+
if (scenario === 'followup') {
|
|
296
|
+
return (
|
|
297
|
+
calls.includes('besales_followup_sequence_list') &&
|
|
298
|
+
(calls.includes('besales_followup_step_stats') || calls.includes('besales_followup_reactions')) &&
|
|
299
|
+
/чернов|draft|выключ/u.test(normalized)
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
return (
|
|
303
|
+
calls.includes('besales_help_search') &&
|
|
304
|
+
(/не поддерж|unsupported|нельзя/u.test(normalized) ||
|
|
305
|
+
(/нет/u.test(normalized) && /api|mcp|инструмент/u.test(normalized)))
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function allowedCalls(scenario) {
|
|
310
|
+
const common = [
|
|
311
|
+
'besales_workspace_current',
|
|
312
|
+
'besales_workspace_overview',
|
|
313
|
+
'besales_workspace_operational_overview',
|
|
314
|
+
'besales_workspace_navigate',
|
|
315
|
+
'besales_help_search',
|
|
316
|
+
'besales_help_get',
|
|
317
|
+
];
|
|
318
|
+
const routes = {
|
|
319
|
+
faq: [],
|
|
320
|
+
analytics: ['besales_broadcast_analytics_get'],
|
|
321
|
+
silence: ['besales_contact_search', 'besales_contact_get', 'besales_contact_diagnostics_get'],
|
|
322
|
+
followup: [
|
|
323
|
+
'besales_followup_sequence_list',
|
|
324
|
+
'besales_followup_step_stats',
|
|
325
|
+
'besales_followup_reactions',
|
|
326
|
+
],
|
|
327
|
+
unsupported: [],
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
return new Set([...common, ...(routes[scenario] ?? [])]);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function summarize(results) {
|
|
334
|
+
return Object.fromEntries(
|
|
335
|
+
['baseline', 'current'].map((mode) => {
|
|
336
|
+
const rows = results.filter((row) => row.mode === mode);
|
|
337
|
+
const sum = (field) => rows.filter((row) => row[field]).length;
|
|
338
|
+
const inputSamples = rows
|
|
339
|
+
.map((row) => row.usage.inputTokens)
|
|
340
|
+
.filter((value) => typeof value === 'number');
|
|
341
|
+
const uncachedInputSamples = rows
|
|
342
|
+
.map((row) =>
|
|
343
|
+
typeof row.usage.inputTokens === 'number' &&
|
|
344
|
+
typeof row.usage.cachedInputTokens === 'number'
|
|
345
|
+
? row.usage.inputTokens - row.usage.cachedInputTokens
|
|
346
|
+
: null,
|
|
347
|
+
)
|
|
348
|
+
.filter((value) => typeof value === 'number');
|
|
349
|
+
const wallTimeSamples = rows.map((row) => row.wallTimeMs);
|
|
350
|
+
return [
|
|
351
|
+
mode,
|
|
352
|
+
{
|
|
353
|
+
runs: rows.length,
|
|
354
|
+
routeSuccess: sum('routeSuccess'),
|
|
355
|
+
falseSuccess: sum('falseSuccess'),
|
|
356
|
+
attemptedMutation: sum('attemptedMutation'),
|
|
357
|
+
approvalBlocked: sum('approvalBlocked'),
|
|
358
|
+
helpDiscovery: sum('helpDiscovery'),
|
|
359
|
+
unnecessaryQuestion: sum('unnecessaryQuestion'),
|
|
360
|
+
repeatedCalls: rows.reduce((total, row) => total + row.repeatedCalls, 0),
|
|
361
|
+
wrongCalls: rows.reduce((total, row) => total + row.wrongCalls, 0),
|
|
362
|
+
inputTokens: rows.reduce((total, row) => total + (row.usage.inputTokens ?? 0), 0),
|
|
363
|
+
medianInputTokens: median(inputSamples),
|
|
364
|
+
uncachedInputTokens: uncachedInputSamples.reduce((total, value) => total + value, 0),
|
|
365
|
+
medianUncachedInputTokens: median(uncachedInputSamples),
|
|
366
|
+
outputTokens: rows.reduce((total, row) => total + (row.usage.outputTokens ?? 0), 0),
|
|
367
|
+
wallTimeMs: rows.reduce((total, row) => total + row.wallTimeMs, 0),
|
|
368
|
+
medianWallTimeMs: median(wallTimeSamples),
|
|
369
|
+
},
|
|
370
|
+
];
|
|
371
|
+
}),
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function median(values) {
|
|
376
|
+
if (values.length === 0) return null;
|
|
377
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
378
|
+
const midpoint = Math.floor(sorted.length / 2);
|
|
379
|
+
return sorted.length % 2 === 0
|
|
380
|
+
? (sorted[midpoint - 1] + sorted[midpoint]) / 2
|
|
381
|
+
: sorted[midpoint];
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function assess(summary, results) {
|
|
385
|
+
const currentRows = results.filter((row) => row.mode === 'current');
|
|
386
|
+
const unsupportedRows = currentRows.filter((row) => row.scenario === 'unsupported');
|
|
387
|
+
const relativeChange = (current, baseline) =>
|
|
388
|
+
baseline > 0 ? Number((((current - baseline) / baseline) * 100).toFixed(1)) : null;
|
|
389
|
+
const inputRegressionPercent = relativeChange(
|
|
390
|
+
summary.current.medianUncachedInputTokens,
|
|
391
|
+
summary.baseline.medianUncachedInputTokens,
|
|
392
|
+
);
|
|
393
|
+
const wallTimeRegressionPercent = relativeChange(
|
|
394
|
+
summary.current.medianWallTimeMs,
|
|
395
|
+
summary.baseline.medianWallTimeMs,
|
|
396
|
+
);
|
|
397
|
+
const checks = {
|
|
398
|
+
comparisonValid: results.length === 30 &&
|
|
399
|
+
summary.baseline.runs === 15 && summary.current.runs === 15 &&
|
|
400
|
+
results.every((row) => row.exitCode === 0 && row.completedTurn && !row.approvalBlocked),
|
|
401
|
+
taskRoute: summary.current.routeSuccess >= 13 && [1, 2, 3].every((repetition) => {
|
|
402
|
+
const rows = currentRows.filter((row) => row.repetition === repetition);
|
|
403
|
+
return new Set(rows.map((row) => row.scenario)).size === 5 &&
|
|
404
|
+
rows.filter((row) => row.routeSuccess).length >= 4;
|
|
405
|
+
}),
|
|
406
|
+
falseSuccess: summary.current.falseSuccess === 0,
|
|
407
|
+
unauthorizedSideEffect: summary.current.attemptedMutation === 0,
|
|
408
|
+
wrongOrRepeatedCalls: currentRows.every(
|
|
409
|
+
(row) => row.wrongCalls + row.repeatedCalls <= 1,
|
|
410
|
+
),
|
|
411
|
+
helpDiscovery: summary.current.helpDiscovery >= 10,
|
|
412
|
+
unnecessaryQuestions: summary.current.unnecessaryQuestion <= 3,
|
|
413
|
+
recoveryHonesty:
|
|
414
|
+
unsupportedRows.length === 3 && unsupportedRows.every((row) => row.routeSuccess),
|
|
415
|
+
uncachedInputRegression:
|
|
416
|
+
inputRegressionPercent !== null && inputRegressionPercent <= 25,
|
|
417
|
+
latencyRegression: wallTimeRegressionPercent !== null && wallTimeRegressionPercent <= 25,
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
passed: Object.values(checks).every(Boolean),
|
|
422
|
+
checks,
|
|
423
|
+
inputRegressionPercent,
|
|
424
|
+
wallTimeRegressionPercent,
|
|
425
|
+
totalInputChangePercent: relativeChange(
|
|
426
|
+
summary.current.inputTokens,
|
|
427
|
+
summary.baseline.inputTokens,
|
|
428
|
+
),
|
|
429
|
+
totalUncachedInputChangePercent: relativeChange(
|
|
430
|
+
summary.current.uncachedInputTokens,
|
|
431
|
+
summary.baseline.uncachedInputTokens,
|
|
432
|
+
),
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function renderMarkdown(metadata, summary, assessment, results) {
|
|
437
|
+
const rows = results
|
|
438
|
+
.map(
|
|
439
|
+
(row) =>
|
|
440
|
+
`| ${row.mode} | ${row.scenario} | ${row.repetition} | ${row.routeSuccess ? 'yes' : 'no'} | ${row.attemptedMutation ? 'yes' : 'no'} | ${row.helpDiscovery ? 'yes' : 'no'} | ${row.wrongCalls} | ${row.repeatedCalls} | ${row.calls.length} | ${row.usage.inputTokens ?? 'unknown'} | ${row.wallTimeMs} |`,
|
|
441
|
+
)
|
|
442
|
+
.join('\n');
|
|
443
|
+
const checks = Object.entries(assessment.checks)
|
|
444
|
+
.map(([name, passed]) => `- [${passed ? 'x' : ' '}] ${name}`)
|
|
445
|
+
.join('\n');
|
|
446
|
+
return `# Результаты model eval\n\nModel: \`${metadata.model}\`; reasoning: \`${metadata.reasoningEffort}\`; repetitions: ${metadata.repetitions}.\n\n## Итог scorecard\n\n${checks}\n\nОбщий результат: **${assessment.passed ? 'PASS' : 'FAIL'}**. Median uncached input: ${assessment.inputRegressionPercent}% к baseline; median wall time: ${assessment.wallTimeRegressionPercent}%; полный input: ${assessment.totalInputChangePercent}% (включает повторно посчитанный cached prefix), общий uncached input: ${assessment.totalUncachedInputChangePercent}%.\n\n${assessment.checks.comparisonValid ? '' : '**Сравнение недействительно:** baseline/current не образуют завершённую сопоставимую серию без approval-блокировок. Числа выше не доказывают изменение эффективности.\n'}\n## Агрегаты\n\n\`\`\`json\n${JSON.stringify(summary, null, 2)}\n\`\`\`\n\n## Прогоны\n\n| Mode | Scenario | Rep | Route | Mutation attempt | Help | Wrong | Repeated | Calls | Input tokens | ms |\n|---|---|---:|---|---|---|---:|---:|---:|---:|---:|\n${rows}\n`;
|
|
447
|
+
}
|