Stage 3: улучшена логика жизненного цикла и очищены ответы ассистента
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const request = require("supertest");
|
||||
|
||||
const STAGE3_SUITE_RELATIVE = path.join("eval_cases", "assistant_stage3_lifecycle_probe_v0_1.json");
|
||||
const FLAG_KEYS = [
|
||||
"FEATURE_ASSISTANT_PROBLEM_UNITS_V1",
|
||||
"FEATURE_ASSISTANT_ANSWER_POLICY_V11",
|
||||
"FEATURE_ASSISTANT_BROAD_GUARD_V1",
|
||||
"FEATURE_ASSISTANT_MIN_EVIDENCE_GATE_V1",
|
||||
"FEATURE_ASSISTANT_ANTI_GENERIC_RANKING_GUARD_V1",
|
||||
"FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1",
|
||||
"FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1",
|
||||
"FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1",
|
||||
"FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1"
|
||||
];
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {
|
||||
runDir: "",
|
||||
suitePath: "",
|
||||
outputSubdir: path.join("prompt_dialogs", "stage3_lifecycle_probe")
|
||||
};
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const token = argv[i];
|
||||
if (token === "--run-dir") {
|
||||
args.runDir = String(argv[i + 1] ?? "");
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (token === "--suite-path") {
|
||||
args.suitePath = String(argv[i + 1] ?? "");
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (token === "--output-subdir") {
|
||||
args.outputSubdir = String(argv[i + 1] ?? "");
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function ensureDir(dirPath) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
function writeUtf8Bom(filePath, content) {
|
||||
fs.writeFileSync(filePath, `\uFEFF${content}`, "utf8");
|
||||
}
|
||||
|
||||
function toSafeFileToken(value) {
|
||||
return String(value)
|
||||
.trim()
|
||||
.replace(/\s+/g, "_")
|
||||
.replace(/[^a-zA-Z0-9_-]/g, "_")
|
||||
.replace(/_+/g, "_");
|
||||
}
|
||||
|
||||
function readJson(filePath) {
|
||||
const raw = fs.readFileSync(filePath, "utf8").replace(/^\uFEFF/, "");
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
|
||||
function findLatestRunDir(runsRoot) {
|
||||
if (!fs.existsSync(runsRoot)) {
|
||||
throw new Error(`Runs folder not found: ${runsRoot}`);
|
||||
}
|
||||
const dirs = fs
|
||||
.readdirSync(runsRoot, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => path.join(runsRoot, entry.name))
|
||||
.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
|
||||
if (dirs.length === 0) {
|
||||
throw new Error(`No run directories found under: ${runsRoot}`);
|
||||
}
|
||||
return dirs[0];
|
||||
}
|
||||
|
||||
function resolveRunDir(args, runsRoot) {
|
||||
if (args.runDir) {
|
||||
return path.resolve(args.runDir);
|
||||
}
|
||||
return findLatestRunDir(runsRoot);
|
||||
}
|
||||
|
||||
function setLifecycleFlags() {
|
||||
const original = {};
|
||||
for (const key of FLAG_KEYS) {
|
||||
original[key] = process.env[key];
|
||||
}
|
||||
process.env.FEATURE_ASSISTANT_PROBLEM_UNITS_V1 = "1";
|
||||
process.env.FEATURE_ASSISTANT_ANSWER_POLICY_V11 = "1";
|
||||
process.env.FEATURE_ASSISTANT_BROAD_GUARD_V1 = "1";
|
||||
process.env.FEATURE_ASSISTANT_MIN_EVIDENCE_GATE_V1 = "1";
|
||||
process.env.FEATURE_ASSISTANT_ANTI_GENERIC_RANKING_GUARD_V1 = "1";
|
||||
process.env.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1 = "1";
|
||||
process.env.FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1 = "0";
|
||||
process.env.FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1 = "1";
|
||||
process.env.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1 = "1";
|
||||
return original;
|
||||
}
|
||||
|
||||
function restoreFlags(original) {
|
||||
for (const key of FLAG_KEYS) {
|
||||
const value = original[key];
|
||||
if (value === undefined) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function summarizeDebug(debug) {
|
||||
const routeSummary = Array.isArray(debug?.route_summary) ? debug.route_summary : [];
|
||||
const retrievalResults = Array.isArray(debug?.retrieval_results) ? debug.retrieval_results : [];
|
||||
const routed = retrievalResults.filter((item) => String(item?.route ?? "") !== "no_route");
|
||||
const problemUnits = routed.reduce((acc, item) => {
|
||||
const list = Array.isArray(item?.problem_units) ? item.problem_units : [];
|
||||
return acc + list.length;
|
||||
}, 0);
|
||||
return {
|
||||
route_summary: routeSummary,
|
||||
routed_retrieval_count: routed.length,
|
||||
problem_units_count: problemUnits,
|
||||
problem_answer_mode: typeof debug?.problem_answer_mode === "string" ? debug.problem_answer_mode : ""
|
||||
};
|
||||
}
|
||||
|
||||
function buildMarkdown(dialog) {
|
||||
const lines = [];
|
||||
lines.push(`# ${dialog.case_id}`);
|
||||
lines.push("");
|
||||
lines.push(`- session_id: ${dialog.session_id || "n/a"}`);
|
||||
lines.push(`- reply_type: ${dialog.reply_type || "n/a"}`);
|
||||
lines.push(`- trace_id: ${dialog.trace_id || "n/a"}`);
|
||||
lines.push(`- status: ${dialog.http_status}`);
|
||||
lines.push("");
|
||||
lines.push("## User");
|
||||
lines.push(dialog.user_message || "");
|
||||
lines.push("");
|
||||
lines.push("## Assistant");
|
||||
lines.push(dialog.assistant_reply || "");
|
||||
lines.push("");
|
||||
lines.push("## Debug Summary");
|
||||
lines.push("```json");
|
||||
lines.push(JSON.stringify(dialog.debug_summary, null, 2));
|
||||
lines.push("```");
|
||||
lines.push("");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const backendRoot = path.resolve(__dirname, "..");
|
||||
const repoRoot = path.resolve(backendRoot, "..");
|
||||
const runsRoot = path.join(repoRoot, "docs", "runs");
|
||||
const runDir = resolveRunDir(args, runsRoot);
|
||||
const suitePath = args.suitePath ? path.resolve(args.suitePath) : path.join(repoRoot, STAGE3_SUITE_RELATIVE);
|
||||
const suite = readJson(suitePath);
|
||||
const dialogsDir = path.join(runDir, args.outputSubdir);
|
||||
ensureDir(dialogsDir);
|
||||
ensureDir(path.join(runDir, "prompt_dialogs"));
|
||||
|
||||
const originalFlags = setLifecycleFlags();
|
||||
let app;
|
||||
try {
|
||||
const { createApp } = require(path.join(backendRoot, "dist", "server.js"));
|
||||
app = createApp();
|
||||
} finally {
|
||||
restoreFlags(originalFlags);
|
||||
}
|
||||
|
||||
const indexRows = [];
|
||||
const generatedAt = new Date().toISOString();
|
||||
|
||||
for (let i = 0; i < suite.cases.length; i += 1) {
|
||||
const probeCase = suite.cases[i];
|
||||
const caseId = String(probeCase.case_id || `case_${i + 1}`);
|
||||
const userMessage = String(probeCase?.turns?.[0]?.user_message || "");
|
||||
const response = await request(app).post("/api/assistant/message").send({
|
||||
useMock: true,
|
||||
promptVersion: "normalizer_v2_0_2",
|
||||
user_message: userMessage
|
||||
});
|
||||
const body = response.body || {};
|
||||
const sessionId = String(body.session_id || "");
|
||||
|
||||
let session = null;
|
||||
if (sessionId) {
|
||||
const sessionResponse = await request(app).get(`/api/assistant/session/${encodeURIComponent(sessionId)}`);
|
||||
if (sessionResponse.status === 200 && sessionResponse.body?.ok) {
|
||||
session = sessionResponse.body.session ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
const debugSummary = summarizeDebug(body.debug);
|
||||
const artifact = {
|
||||
schema_version: "assistant_prompt_dialog_v0_1",
|
||||
generated_at: generatedAt,
|
||||
suite_id: suite.suite_id,
|
||||
case_id: caseId,
|
||||
scenario_tag: probeCase.scenario_tag || "",
|
||||
expected_hints: probeCase.expected_hints || {},
|
||||
lifecycle_focus: probeCase.lifecycle_focus || {},
|
||||
request: {
|
||||
useMock: true,
|
||||
promptVersion: "normalizer_v2_0_2",
|
||||
user_message: userMessage
|
||||
},
|
||||
http_status: response.status,
|
||||
session_id: sessionId,
|
||||
trace_id: String(body.debug?.trace_id || body.conversation_item?.trace_id || ""),
|
||||
reply_type: String(body.reply_type || ""),
|
||||
assistant_reply: String(body.assistant_reply || ""),
|
||||
user_message: userMessage,
|
||||
conversation: Array.isArray(body.conversation) ? body.conversation : [],
|
||||
conversation_item: body.conversation_item || null,
|
||||
debug_summary: debugSummary,
|
||||
debug: body.debug || {},
|
||||
session
|
||||
};
|
||||
|
||||
const order = String(i + 1).padStart(2, "0");
|
||||
const fileStem = `${order}_${toSafeFileToken(caseId)}`;
|
||||
const jsonFile = `${fileStem}.json`;
|
||||
const mdFile = `${fileStem}.md`;
|
||||
writeUtf8Bom(path.join(dialogsDir, jsonFile), `${JSON.stringify(artifact, null, 2)}\n`);
|
||||
writeUtf8Bom(path.join(dialogsDir, mdFile), buildMarkdown(artifact));
|
||||
|
||||
indexRows.push({
|
||||
case_id: caseId,
|
||||
scenario_tag: String(probeCase.scenario_tag || ""),
|
||||
reply_type: artifact.reply_type,
|
||||
session_id: artifact.session_id,
|
||||
trace_id: artifact.trace_id,
|
||||
routed_retrieval_count: debugSummary.routed_retrieval_count,
|
||||
problem_units_count: debugSummary.problem_units_count,
|
||||
prompt_dialog_json: path.join(args.outputSubdir, jsonFile).replace(/\\/g, "/"),
|
||||
prompt_dialog_md: path.join(args.outputSubdir, mdFile).replace(/\\/g, "/")
|
||||
});
|
||||
}
|
||||
|
||||
const indexPayload = {
|
||||
schema_version: "assistant_prompt_dialog_index_v0_1",
|
||||
generated_at: generatedAt,
|
||||
run_dir: runDir,
|
||||
suite_id: suite.suite_id,
|
||||
scenario_count: suite.scenario_count,
|
||||
dialogs: indexRows
|
||||
};
|
||||
writeUtf8Bom(path.join(runDir, "prompt_dialogs", "index.json"), `${JSON.stringify(indexPayload, null, 2)}\n`);
|
||||
|
||||
process.stdout.write(
|
||||
[
|
||||
`run_dir=${runDir}`,
|
||||
`suite_id=${suite.suite_id}`,
|
||||
`dialogs_generated=${indexRows.length}`,
|
||||
`dialogs_folder=${dialogsDir}`
|
||||
].join("\n") + "\n"
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
process.stderr.write(`${error?.stack || error}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user