Stage 2 завершён: problem-first ответы и follow-up continuity - ассистент переведён от entity-heavy логики к problem-first ответам с problem-unit слоем, удержанием контекста в follow-up и очисткой пользовательского ответа от сырых технических ссылок.
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
import request from "supertest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
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"
|
||||
] as const;
|
||||
|
||||
const ORIGINAL_FLAGS: Record<string, string | undefined> = Object.fromEntries(
|
||||
FLAG_KEYS.map((key) => [key, process.env[key]])
|
||||
);
|
||||
|
||||
function restoreFlags(): void {
|
||||
for (const key of FLAG_KEYS) {
|
||||
const original = ORIGINAL_FLAGS[key];
|
||||
if (original === undefined) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = original;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function createAppWithProblemUnitsFlag(flagValue: "0" | "1") {
|
||||
process.env.FEATURE_ASSISTANT_PROBLEM_UNITS_V1 = flagValue;
|
||||
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";
|
||||
vi.resetModules();
|
||||
const { createApp } = await import("../src/server");
|
||||
return createApp();
|
||||
}
|
||||
|
||||
function routedRetrievalResults(body: Record<string, unknown>): Record<string, unknown>[] {
|
||||
const results = Array.isArray((body.debug as { retrieval_results?: unknown[] } | undefined)?.retrieval_results)
|
||||
? ((body.debug as { retrieval_results?: unknown[] }).retrieval_results as Record<string, unknown>[])
|
||||
: [];
|
||||
return results.filter((item) => String(item.route ?? "") !== "no_route");
|
||||
}
|
||||
|
||||
describe.sequential("assistant problem-unit runtime rollout", () => {
|
||||
afterEach(() => {
|
||||
restoreFlags();
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it("emits problem-unit layer on problem-heavy scenarios when flag is ON", async () => {
|
||||
const app = await createAppWithProblemUnitsFlag("1");
|
||||
const cases = [
|
||||
{
|
||||
tag: "chain",
|
||||
user_message: "Разложи цепочку документов и оплат по контрагентам за 2020-06, где разрыв механизма закрытия."
|
||||
},
|
||||
{
|
||||
tag: "anomaly",
|
||||
user_message: "Разложи lifecycle по счету 97 за 2020-06 и покажи аномалии списания по последовательности."
|
||||
},
|
||||
{
|
||||
tag: "contradiction",
|
||||
user_message: "Проверь НДС за 2020-06: где противоречия между документами, проводками и регистрами."
|
||||
},
|
||||
{
|
||||
tag: "period_risk",
|
||||
user_message: "Разложи по счетам 51 и 60 за 2020-06, что создаёт риск закрытия периода и где разрывы цепочки."
|
||||
}
|
||||
];
|
||||
|
||||
const observedTypes = new Set<string>();
|
||||
for (const scenario of cases) {
|
||||
const response = await request(app).post("/api/assistant/message").send({
|
||||
useMock: true,
|
||||
promptVersion: "normalizer_v2_0_2",
|
||||
user_message: scenario.user_message
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const routed = routedRetrievalResults(response.body as Record<string, unknown>);
|
||||
expect(routed.length).toBeGreaterThan(0);
|
||||
|
||||
const withProblemUnits = routed.filter((item) => Array.isArray(item.problem_units) && item.problem_units.length > 0);
|
||||
expect(withProblemUnits.length).toBeGreaterThan(0);
|
||||
|
||||
for (const result of withProblemUnits) {
|
||||
const summary = (result.summary as Record<string, unknown>) ?? {};
|
||||
const candidateEvidence = result.candidate_evidence as Array<Record<string, unknown>>;
|
||||
const problemUnits = result.problem_units as Array<Record<string, unknown>>;
|
||||
const problemSummary = (result.problem_unit_summary as Record<string, unknown>) ?? {};
|
||||
|
||||
expect(summary.problem_units_enabled).toBe(true);
|
||||
expect(summary.candidate_evidence_count).toBe(candidateEvidence.length);
|
||||
expect(summary.problem_units_count).toBe(problemUnits.length);
|
||||
expect(Array.isArray(summary.problem_unit_types)).toBe(true);
|
||||
expect(typeof summary.problem_unit_duplicate_collapses).toBe("number");
|
||||
expect(problemSummary.units_total).toBe(problemUnits.length);
|
||||
|
||||
for (const unit of problemUnits) {
|
||||
expect(typeof unit.problem_unit_id).toBe("string");
|
||||
expect(typeof unit.problem_unit_type).toBe("string");
|
||||
expect(typeof unit.mechanism_summary).toBe("string");
|
||||
expect(typeof unit.severity?.score).toBe("number");
|
||||
expect(typeof unit.confidence?.score).toBe("number");
|
||||
observedTypes.add(String(unit.problem_unit_type));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(observedTypes.size).toBeGreaterThan(0);
|
||||
expect(Array.from(observedTypes).every((item) =>
|
||||
[
|
||||
"document_conflict",
|
||||
"broken_chain_segment",
|
||||
"lifecycle_anomaly_node",
|
||||
"unresolved_settlement_cluster",
|
||||
"period_risk_cluster",
|
||||
"cross_branch_inconsistency_cluster"
|
||||
].includes(item)
|
||||
)).toBe(true);
|
||||
});
|
||||
|
||||
it("does not emit problem-unit layer when flag is OFF", async () => {
|
||||
const app = await createAppWithProblemUnitsFlag("0");
|
||||
|
||||
const response = await request(app).post("/api/assistant/message").send({
|
||||
useMock: true,
|
||||
promptVersion: "normalizer_v2_0_2",
|
||||
user_message: "Разложи цепочку документов и оплат по контрагентам за 2020-06."
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
const routed = routedRetrievalResults(response.body as Record<string, unknown>);
|
||||
expect(routed.length).toBeGreaterThan(0);
|
||||
for (const result of routed) {
|
||||
expect(result.raw_entities).toBeUndefined();
|
||||
expect(result.candidate_evidence).toBeUndefined();
|
||||
expect(result.problem_units).toBeUndefined();
|
||||
expect(result.problem_unit_summary).toBeUndefined();
|
||||
expect((result.summary as Record<string, unknown>).problem_units_enabled).toBeUndefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user