Opus 4.7 API rejects the temperature parameter. Conditionally skip it for opus models. Add E2E test script for weekly report M2B pipeline. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
181 lines
6.7 KiB
TypeScript
181 lines
6.7 KiB
TypeScript
import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
||
import { join } from 'node:path';
|
||
|
||
const BASE = process.env.API_BASE_URL ?? 'http://localhost:3001';
|
||
const EMAIL = process.env.ADMIN_EMAIL ?? 'gruart@cifem-rus.ru';
|
||
const PASSWORD = process.env.ADMIN_PASSWORD;
|
||
const OUT_DIR = process.env.OUT_DIR ?? './tmp/m2b-reports';
|
||
|
||
if (!PASSWORD) {
|
||
console.error('ADMIN_PASSWORD required (env var)');
|
||
process.exit(1);
|
||
}
|
||
|
||
async function main() {
|
||
console.log(`Target: ${BASE}`);
|
||
console.log(`Admin: ${EMAIL}`);
|
||
|
||
// 1. Login
|
||
const loginRes = await fetch(`${BASE}/api/v1/auth/login`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ email: EMAIL, password: PASSWORD }),
|
||
});
|
||
if (!loginRes.ok) {
|
||
console.error(`Login failed ${loginRes.status}: ${await loginRes.text()}`);
|
||
process.exit(1);
|
||
}
|
||
const { accessToken } = await loginRes.json() as { accessToken: string };
|
||
console.log(`OK Logged in (JWT ${accessToken.substring(0, 20)}...)`);
|
||
|
||
const authHeaders: Record<string, string> = {
|
||
'Authorization': `Bearer ${accessToken}`,
|
||
'Content-Type': 'application/json',
|
||
};
|
||
|
||
// 2. Compute period (default = previous ISO week, Mon 00:00 MSK)
|
||
const periodFrom = process.env.PERIOD_FROM ?? computeMondayMinus7DaysISO();
|
||
const periodTo = process.env.PERIOD_TO ?? computeMondayThisWeekISO();
|
||
console.log(`Period: ${periodFrom} -> ${periodTo}`);
|
||
|
||
// 3. Create DRAFT report via aggregator
|
||
const createRes = await fetch(`${BASE}/api/v1/admin/agent/reports/generate`, {
|
||
method: 'POST',
|
||
headers: authHeaders,
|
||
body: JSON.stringify({ periodFrom, periodTo }),
|
||
});
|
||
if (!createRes.ok) {
|
||
console.error(`Create report failed ${createRes.status}: ${await createRes.text()}`);
|
||
process.exit(1);
|
||
}
|
||
const created = await createRes.json() as { id: string; status: string; periodFrom: string };
|
||
const reportId = created.id;
|
||
console.log(`OK Report DRAFT created: id=${reportId}`);
|
||
console.log(` status=${created.status} periodFrom=${created.periodFrom}`);
|
||
|
||
// 4. Trigger M2B commentary generation (Opus 4.7)
|
||
console.log(`Triggering M2B Opus 4.7... (expect 30-90s)`);
|
||
const start = Date.now();
|
||
const m2bHeaders: Record<string, string> = { 'Authorization': `Bearer ${accessToken}` };
|
||
const m2bRes = await fetch(`${BASE}/api/v1/admin/agent/reports/${reportId}/generate-commentary`, {
|
||
method: 'POST',
|
||
headers: m2bHeaders,
|
||
});
|
||
const m2bDuration = ((Date.now() - start) / 1000).toFixed(1);
|
||
if (!m2bRes.ok) {
|
||
console.error(`M2B failed ${m2bRes.status}: ${await m2bRes.text()}`);
|
||
process.exit(1);
|
||
}
|
||
const m2bResult = await m2bRes.json() as {
|
||
modelUsed?: string;
|
||
promptTokens?: number;
|
||
completionTokens?: number;
|
||
costUsd?: string;
|
||
contentMarkdown?: string;
|
||
status?: string;
|
||
};
|
||
console.log(`OK M2B completed in ${m2bDuration}s`);
|
||
console.log(` modelUsed=${m2bResult.modelUsed}`);
|
||
console.log(` promptTokens=${m2bResult.promptTokens} completionTokens=${m2bResult.completionTokens}`);
|
||
console.log(` costUsd=$${m2bResult.costUsd}`);
|
||
console.log(` status=${m2bResult.status}`);
|
||
|
||
// 5. Fetch report with markdown
|
||
const getRes = await fetch(`${BASE}/api/v1/admin/agent/reports/${reportId}`, {
|
||
headers: { 'Authorization': `Bearer ${accessToken}` },
|
||
});
|
||
const report = await getRes.json() as Record<string, unknown>;
|
||
const markdown = (report.contentMarkdown ?? '') as string;
|
||
if (!markdown) {
|
||
console.error(`No markdown in report response`);
|
||
console.error(JSON.stringify(report, null, 2));
|
||
process.exit(1);
|
||
}
|
||
|
||
// 6. Save to disk
|
||
if (!existsSync(OUT_DIR)) mkdirSync(OUT_DIR, { recursive: true });
|
||
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
||
const mdPath = join(OUT_DIR, `m2b-report-${reportId.substring(0, 8)}-${ts}.md`);
|
||
const metaPath = join(OUT_DIR, `m2b-report-${reportId.substring(0, 8)}-${ts}.meta.json`);
|
||
writeFileSync(mdPath, markdown, 'utf-8');
|
||
writeFileSync(metaPath, JSON.stringify(report, null, 2), 'utf-8');
|
||
console.log(`OK Saved markdown -> ${mdPath}`);
|
||
console.log(`OK Saved meta -> ${metaPath}`);
|
||
|
||
// 7. Pre-check 7 persona criteria (heuristic)
|
||
console.log(`\n=== Persona pre-check ===`);
|
||
const checks = runPersonaChecks(markdown);
|
||
let allPass = true;
|
||
for (const c of checks) {
|
||
console.log(` ${c.pass ? 'PASS' : 'FAIL'} ${c.label}`);
|
||
if (!c.pass) allPass = false;
|
||
}
|
||
console.log(`\nPersona: ${allPass ? 'ALL PASS' : 'SOME FAILED'}`);
|
||
|
||
// 8. Print full markdown to stdout
|
||
console.log(`\n=== MARKDOWN GENERE (${markdown.length} chars) ===\n`);
|
||
console.log(markdown);
|
||
console.log(`\n=== FIN ===`);
|
||
console.log(`\nResume: reportId=${reportId} status=${m2bResult.status} model=${m2bResult.modelUsed} tokens=${m2bResult.promptTokens}+${m2bResult.completionTokens} cost=$${m2bResult.costUsd} latency=${m2bDuration}s`);
|
||
}
|
||
|
||
function runPersonaChecks(md: string): Array<{ label: string; pass: boolean }> {
|
||
return [
|
||
{
|
||
label: 'Titre "Еженедельный отчёт" present',
|
||
pass: /Еженедельный отчёт/i.test(md),
|
||
},
|
||
{
|
||
label: '6 sections (Резюме, Коммерческий, Закупки, Финансы, Действия, Аномалии)',
|
||
pass: ['езюме', 'оммерческ', 'акупк', 'инанс', 'ействи', 'номали'].every(s =>
|
||
md.toLowerCase().includes(s.toLowerCase()),
|
||
),
|
||
},
|
||
{
|
||
label: 'Signature Август presente',
|
||
pass: /Август/i.test(md),
|
||
},
|
||
{
|
||
label: 'Symbole rub correct (pas de "руб" sans signe)',
|
||
pass: !/\d+\s*руб\b/i.test(md) || md.includes('₽'),
|
||
},
|
||
{
|
||
label: 'Pas de prenom translittere (Maksim, Daria, Evgeny)',
|
||
pass: !/\b(Maksim|Daria|Evgeny|Nikita|Tamara|Anna)\b/.test(md),
|
||
},
|
||
{
|
||
label: 'Pas d\'emoji fantaisie',
|
||
pass: !/[\u{1F300}-\u{1FAFF}]/u.test(md),
|
||
},
|
||
{
|
||
label: 'Pas de tournures jugeantes personnelles',
|
||
pass: !/(не выполнил|опоздал|ошибся|провалил|забыл)/i.test(md),
|
||
},
|
||
];
|
||
}
|
||
|
||
function computeMondayMinus7DaysISO(): string {
|
||
const now = new Date();
|
||
const day = now.getUTCDay();
|
||
const diffToMonday = day === 0 ? -6 : 1 - day;
|
||
const lastMonday = new Date(now);
|
||
lastMonday.setUTCDate(now.getUTCDate() + diffToMonday - 7);
|
||
lastMonday.setUTCHours(21, 0, 0, 0);
|
||
return lastMonday.toISOString();
|
||
}
|
||
|
||
function computeMondayThisWeekISO(): string {
|
||
const now = new Date();
|
||
const day = now.getUTCDay();
|
||
const diffToMonday = day === 0 ? -6 : 1 - day;
|
||
const thisMonday = new Date(now);
|
||
thisMonday.setUTCDate(now.getUTCDate() + diffToMonday);
|
||
thisMonday.setUTCHours(21, 0, 0, 0);
|
||
return thisMonday.toISOString();
|
||
}
|
||
|
||
main().catch(err => {
|
||
console.error('Fatal:', err);
|
||
process.exit(1);
|
||
});
|