mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-25 23:32:22 +01:00
audit search variety with model provenance and per-turn latency
This commit is contained in:
@@ -11,6 +11,7 @@ const owner = 'sft_alex_creator';
|
|||||||
const endpointId = process.env.ENDPOINT_ID || '1d1022ef';
|
const endpointId = process.env.ENDPOINT_ID || '1d1022ef';
|
||||||
const endpointUrl = process.env.ENDPOINT_URL || (() => { throw new Error("ENDPOINT_URL is required"); })();
|
const endpointUrl = process.env.ENDPOINT_URL || (() => { throw new Error("ENDPOINT_URL is required"); })();
|
||||||
const model = process.env.MODEL || 'odysseus-qwen3.5-tools-pre-heretic';
|
const model = process.env.MODEL || 'odysseus-qwen3.5-tools-pre-heretic';
|
||||||
|
const varietyOnly = process.env.VARIETY_ONLY === '1';
|
||||||
const run = new Date().toISOString().replace(/[:.]/g, '-');
|
const run = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
const reportPath = path.resolve(process.env.REPORT_PATH || path.join(root, `reports/clean-v3-search-quality-${run}.json`));
|
const reportPath = path.resolve(process.env.REPORT_PATH || path.join(root, `reports/clean-v3-search-quality-${run}.json`));
|
||||||
if (!reportPath.startsWith(path.join(root, 'reports') + path.sep) || fs.existsSync(reportPath)) throw Error('Report path must be new and under reports/');
|
if (!reportPath.startsWith(path.join(root, 'reports') + path.sep) || fs.existsSync(reportPath)) throw Error('Report path must be new and under reports/');
|
||||||
@@ -19,7 +20,7 @@ const token = Object.entries(sessions).find(([, value]) => value?.username === o
|
|||||||
if (!token) throw Error(`No active ${owner} session`);
|
if (!token) throw Error(`No active ${owner} session`);
|
||||||
|
|
||||||
const marker = `ody-search-${crypto.randomUUID()}`;
|
const marker = `ody-search-${crypto.randomUUID()}`;
|
||||||
const report = { run, owner, marker, status: 'running', scenarios: [], privacy: 'Public synthetic queries only; fetched bodies and private data are not retained.' };
|
const report = { run, owner, marker, model, endpointUrl, status: 'running', scenarios: [], turns: [], privacy: 'Public synthetic queries only; fetched bodies and private data are not retained.' };
|
||||||
const save = () => fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + '\n');
|
const save = () => fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + '\n');
|
||||||
fs.mkdirSync(path.dirname(reportPath), { recursive: true }); save();
|
fs.mkdirSync(path.dirname(reportPath), { recursive: true }); save();
|
||||||
const canonical = value => String(value || '').replace(/^mcp__email__/, '');
|
const canonical = value => String(value || '').replace(/^mcp__email__/, '');
|
||||||
@@ -51,6 +52,7 @@ async function preparePage(context, id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function send(page, prompt) {
|
async function send(page, prompt) {
|
||||||
|
const started = performance.now();
|
||||||
const responsePromise = page.waitForResponse(r => new URL(r.url()).pathname === '/api/chat_stream' && r.request().method() === 'POST', { timeout: 120000 });
|
const responsePromise = page.waitForResponse(r => new URL(r.url()).pathname === '/api/chat_stream' && r.request().method() === 'POST', { timeout: 120000 });
|
||||||
await page.locator('textarea#message:visible').fill(prompt);
|
await page.locator('textarea#message:visible').fill(prompt);
|
||||||
await page.locator('textarea#message:visible').press('Enter');
|
await page.locator('textarea#message:visible').press('Enter');
|
||||||
@@ -60,7 +62,16 @@ async function send(page, prompt) {
|
|||||||
const starts = events.filter(event => event.type === 'tool_start').map(event => ({ tool: canonical(event.tool), args: event.command || '' }));
|
const starts = events.filter(event => event.type === 'tool_start').map(event => ({ tool: canonical(event.tool), args: event.command || '' }));
|
||||||
const outputs = events.filter(event => event.type === 'tool_output').map(event => ({ tool: canonical(event.tool), exit_code: event.exit_code ?? null, error: Boolean(event.error) }));
|
const outputs = events.filter(event => event.type === 'tool_output').map(event => ({ tool: canonical(event.tool), exit_code: event.exit_code ?? null, error: Boolean(event.error) }));
|
||||||
const final = events.filter(event => event.type === 'final_response').map(event => event.content || '').join('') || events.filter(event => typeof event.delta === 'string').map(event => event.delta).join('');
|
const final = events.filter(event => event.type === 'final_response').map(event => event.content || '').join('') || events.filter(event => typeof event.delta === 'string').map(event => event.delta).join('');
|
||||||
return { http_ok: response.ok(), contract, starts, outputs, final };
|
const metrics = events.find(event => event.type === 'metrics')?.data;
|
||||||
|
const observation = {
|
||||||
|
prompt, seconds: (performance.now() - started) / 1000,
|
||||||
|
rounds: metrics?.agent_rounds ?? null,
|
||||||
|
actual_model: metrics?.model ?? null,
|
||||||
|
tools: starts, outputs, final,
|
||||||
|
runtime_error: events.some(event => event.type === 'error') || /v3 test encountered an error/i.test(final),
|
||||||
|
};
|
||||||
|
report.turns.push(observation); save();
|
||||||
|
return { http_ok: response.ok(), contract, starts, outputs, final, ...observation };
|
||||||
}
|
}
|
||||||
|
|
||||||
let browser;
|
let browser;
|
||||||
@@ -69,6 +80,51 @@ try {
|
|||||||
const context = await browser.newContext({ serviceWorkers: 'block', extraHTTPHeaders: { 'Accept-Encoding': 'identity' } });
|
const context = await browser.newContext({ serviceWorkers: 'block', extraHTTPHeaders: { 'Accept-Encoding': 'identity' } });
|
||||||
await context.addCookies([{ name: 'odysseus_session', value: token, url: base }]);
|
await context.addCookies([{ name: 'odysseus_session', value: token, url: base }]);
|
||||||
|
|
||||||
|
if (varietyOnly) {
|
||||||
|
const cases = [
|
||||||
|
['news-typo', ['latset ai neews?', 'more about the second story, with sources'], true],
|
||||||
|
['country-casual', ['whats new in japan rn'], true],
|
||||||
|
['software-typo', ['latest pythno verison? official source pls'], true],
|
||||||
|
['manual', ['find official english manual for Sony WH-1000XM5'], true],
|
||||||
|
['comparison', ['compare current firefox and chrome privacy features with sources'], true],
|
||||||
|
['research', ['How do sodium ion batteries compare with lithium ion for home storage? Find evidence and explain tradeoffs.'], true],
|
||||||
|
['no-web-typo', ['whats 12 tims 7'], false],
|
||||||
|
['no-web-greeting', ['helo'], false],
|
||||||
|
];
|
||||||
|
async function runCase([name, prompts, needsWeb]) {
|
||||||
|
const scenario = { name, status: 'running', turns: [] };
|
||||||
|
report.scenarios.push(scenario); save();
|
||||||
|
let id, page;
|
||||||
|
try {
|
||||||
|
id = await createSession(context, `[search-variety] ${name} ${marker}`);
|
||||||
|
page = await preparePage(context, id);
|
||||||
|
for (const prompt of prompts) {
|
||||||
|
const turn = await send(page, prompt);
|
||||||
|
const tools = turn.starts.map(x => x.tool);
|
||||||
|
const checks = {
|
||||||
|
no_runtime_error: !turn.runtime_error,
|
||||||
|
model_matches: turn.actual_model === model,
|
||||||
|
nonempty_answer: turn.final.trim().length > 0,
|
||||||
|
no_reasoning_leak: noLeak(turn.final),
|
||||||
|
expected_web_use: needsWeb ? tools.some(x => ['web_search', 'web_fetch', 'private_browser'].includes(x)) : tools.length === 0,
|
||||||
|
};
|
||||||
|
scenario.turns.push({ prompt, checks, seconds: turn.seconds, tools,
|
||||||
|
status: Object.values(checks).every(Boolean) ? 'mechanics_passed' : 'failed',
|
||||||
|
quality_review: 'pending_manual_evidence_review' });
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
scenario.status = scenario.turns.every(x => x.status === 'mechanics_passed') ? 'needs_quality_review' : 'failed';
|
||||||
|
} catch (error) { scenario.status = 'failed'; scenario.error = String(error).slice(0, 300); }
|
||||||
|
finally {
|
||||||
|
if (id) scenario.cleanup = { session_removed: (await context.request.delete(`${base}/api/session/${id}`)).ok() };
|
||||||
|
if (page) await page.close(); save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Two simultaneous conversations keep endpoint contention bounded.
|
||||||
|
const queue = [...cases];
|
||||||
|
await Promise.all([0, 1].map(async () => { while (queue.length) await runCase(queue.shift()); }));
|
||||||
|
} else {
|
||||||
|
|
||||||
// One conversation proves discovery, evidence reuse, then explicit page inspection.
|
// One conversation proves discovery, evidence reuse, then explicit page inspection.
|
||||||
{
|
{
|
||||||
const scenario = { name: 'official-search-summary-fetch', status: 'running', turns: [] };
|
const scenario = { name: 'official-search-summary-fetch', status: 'running', turns: [] };
|
||||||
@@ -88,6 +144,8 @@ try {
|
|||||||
const expected = index === 0 ? 'web_search' : index === 2 ? 'web_fetch' : null;
|
const expected = index === 0 ? 'web_search' : index === 2 ? 'web_fetch' : null;
|
||||||
const checks = {
|
const checks = {
|
||||||
http_ok: turn.http_ok,
|
http_ok: turn.http_ok,
|
||||||
|
no_runtime_error: !turn.runtime_error,
|
||||||
|
model_matches: turn.actual_model === model,
|
||||||
clean_route: turn.contract?.selection_mode === 'clean_compact_v3_preview',
|
clean_route: turn.contract?.selection_mode === 'clean_compact_v3_preview',
|
||||||
expected_tool: expected ? tools.includes(expected) : tools.length === 0,
|
expected_tool: expected ? tools.includes(expected) : tools.length === 0,
|
||||||
successful_tools: turn.outputs.length === 0 || (() => {
|
successful_tools: turn.outputs.length === 0 || (() => {
|
||||||
@@ -95,7 +153,12 @@ try {
|
|||||||
return !last.error && (last.exit_code == null || last.exit_code === 0);
|
return !last.error && (last.exit_code == null || last.exit_code === 0);
|
||||||
})(),
|
})(),
|
||||||
no_reasoning_leak: noLeak(turn.final),
|
no_reasoning_leak: noLeak(turn.final),
|
||||||
grounded_answer: index === 0 ? /python|pypa|packag/i.test(turn.final) : index === 2 ? /pyproject|build|sdist|wheel|pip|twine/i.test(turn.final) : turn.final.trim().length > 15,
|
answer_contains_requested_information: index === 0
|
||||||
|
? /https:\/\/packaging\.python\.org\b/.test(turn.final)
|
||||||
|
: index === 2
|
||||||
|
? /pyproject\.toml/i.test(turn.final) && /\bwheel\b/i.test(turn.final)
|
||||||
|
&& /\b(?:sdist|source distribution)\b/i.test(turn.final)
|
||||||
|
: /Python Packaging User Guide|PyPA/i.test(turn.final),
|
||||||
};
|
};
|
||||||
scenario.turns.push({ index, tools, output_statuses: turn.outputs, final: turn.final.slice(0, 500), final_chars: turn.final.length, checks, status: Object.values(checks).every(Boolean) ? 'passed' : 'failed' }); save();
|
scenario.turns.push({ index, tools, output_statuses: turn.outputs, final: turn.final.slice(0, 500), final_chars: turn.final.length, checks, status: Object.values(checks).every(Boolean) ? 'passed' : 'failed' }); save();
|
||||||
}
|
}
|
||||||
@@ -121,6 +184,8 @@ try {
|
|||||||
const checks = {
|
const checks = {
|
||||||
http_ok: turn.http_ok, clean_route: turn.contract?.selection_mode === 'clean_compact_v3_preview',
|
http_ok: turn.http_ok, clean_route: turn.contract?.selection_mode === 'clean_compact_v3_preview',
|
||||||
searched: searches.length >= 1, corrected_query: /market/i.test(query) && !/mraket/i.test(query),
|
searched: searches.length >= 1, corrected_query: /market/i.test(query) && !/mraket/i.test(query),
|
||||||
|
no_runtime_error: !turn.runtime_error, model_matches: turn.actual_model === model,
|
||||||
|
covers_requested_indexes: /S&P\s*500/i.test(turn.final) && /Dow/i.test(turn.final) && /Nasdaq/i.test(turn.final),
|
||||||
successful_tools: turn.outputs.every(x => !x.error && (x.exit_code == null || x.exit_code === 0)),
|
successful_tools: turn.outputs.every(x => !x.error && (x.exit_code == null || x.exit_code === 0)),
|
||||||
no_reasoning_leak: noLeak(turn.final), no_irrelevant_misspelling_results: !/telegram|marketing|mraket/i.test(turn.final),
|
no_reasoning_leak: noLeak(turn.final), no_irrelevant_misspelling_results: !/telegram|marketing|mraket/i.test(turn.final),
|
||||||
};
|
};
|
||||||
@@ -146,6 +211,7 @@ try {
|
|||||||
const checks = {
|
const checks = {
|
||||||
http_ok: turn.http_ok, clean_route: turn.contract?.selection_mode === 'clean_compact_v3_preview',
|
http_ok: turn.http_ok, clean_route: turn.contract?.selection_mode === 'clean_compact_v3_preview',
|
||||||
bounded_search: searches.length >= 1 && searches.length <= 2,
|
bounded_search: searches.length >= 1 && searches.length <= 2,
|
||||||
|
no_runtime_error: !turn.runtime_error, model_matches: turn.actual_model === model,
|
||||||
successful_tools: turn.outputs.every(x => !x.error && (x.exit_code == null || x.exit_code === 0)),
|
successful_tools: turn.outputs.every(x => !x.error && (x.exit_code == null || x.exit_code === 0)),
|
||||||
no_reasoning_leak: noLeak(turn.final), honest_gap: /couldn.t find|cannot find|no (?:current )?(?:reliable|supporting|public|matching)|not (?:available|found|listed)|fictional|insufficient/i.test(turn.final),
|
no_reasoning_leak: noLeak(turn.final), honest_gap: /couldn.t find|cannot find|no (?:current )?(?:reliable|supporting|public|matching)|not (?:available|found|listed)|fictional|insufficient/i.test(turn.final),
|
||||||
};
|
};
|
||||||
@@ -157,9 +223,10 @@ try {
|
|||||||
if (page) await page.close(); save();
|
if (page) await page.close(); save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} finally { if (browser) await browser.close(); }
|
} finally { if (browser) await browser.close(); }
|
||||||
|
|
||||||
report.status = report.scenarios.length === 3 && report.scenarios.every(x => x.status === 'passed' && x.cleanup?.session_removed) ? 'passed' : 'failed';
|
report.status = varietyOnly ? 'needs_quality_review' : report.scenarios.length === 3 && report.scenarios.every(x => x.status === 'passed' && x.cleanup?.session_removed) ? 'passed' : 'failed';
|
||||||
report.summary = { passed: report.scenarios.filter(x => x.status === 'passed').length, total: report.scenarios.length };
|
report.summary = { passed: report.scenarios.filter(x => x.status === 'passed').length, total: report.scenarios.length };
|
||||||
save();
|
save();
|
||||||
console.log(JSON.stringify({ report: path.relative(root, reportPath), status: report.status, summary: report.summary }));
|
console.log(JSON.stringify({ report: path.relative(root, reportPath), status: report.status, summary: report.summary }));
|
||||||
|
|||||||
Reference in New Issue
Block a user