Stage 2 завершён: problem-first ответы и follow-up continuity - ассистент переведён от entity-heavy логики к problem-first ответам с problem-unit слоем, удержанием контекста в follow-up и очисткой пользовательского ответа от сырых технических ссылок.

This commit is contained in:
2026-03-26 14:53:52 +03:00
parent ece1abed76
commit 96353cfd48
2474 changed files with 21678 additions and 3292445 deletions
@@ -4,7 +4,11 @@ import { afterEach, describe, expect, it, vi } from "vitest";
const FLAG_KEYS = [
"FEATURE_ASSISTANT_INVESTIGATION_STATE_V1",
"FEATURE_ASSISTANT_STATE_FOLLOWUP_BINDING_V1",
"FEATURE_ASSISTANT_CONTRACTS_V11"
"FEATURE_ASSISTANT_CONTRACTS_V11",
"FEATURE_ASSISTANT_PROBLEM_UNITS_V1",
"FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1",
"FEATURE_ASSISTANT_ANSWER_POLICY_V11",
"FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1"
] as const;
const ORIGINAL_FLAGS: Record<string, string | undefined> = Object.fromEntries(
@@ -26,10 +30,18 @@ async function createAppWithFlags(flags: {
state: "0" | "1";
binding: "0" | "1";
contracts?: "0" | "1";
problemUnits?: "0" | "1";
continuity?: "0" | "1";
answerPolicy?: "0" | "1";
problemCentric?: "0" | "1";
}) {
process.env.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 = flags.state;
process.env.FEATURE_ASSISTANT_STATE_FOLLOWUP_BINDING_V1 = flags.binding;
process.env.FEATURE_ASSISTANT_CONTRACTS_V11 = flags.contracts ?? "1";
process.env.FEATURE_ASSISTANT_PROBLEM_UNITS_V1 = flags.problemUnits ?? "0";
process.env.FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1 = flags.continuity ?? "0";
process.env.FEATURE_ASSISTANT_ANSWER_POLICY_V11 = flags.answerPolicy ?? "0";
process.env.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1 = flags.problemCentric ?? "0";
vi.resetModules();
const { createApp } = await import("../src/server");
return createApp();
@@ -119,4 +131,111 @@ describe.sequential("assistant follow-up state binding", () => {
expect(response.body.debug?.investigation_state_snapshot).toBeNull();
expect(response.body.debug?.followup_state_usage).toBeUndefined();
});
it("applies problem continuity hints only when continuity flag is ON and follow-up has no strong new anchors", async () => {
const app = await createAppWithFlags({
state: "1",
binding: "1",
problemUnits: "1",
continuity: "1"
});
const sessionId = `asst-wave4-problem-continuity-${Date.now()}`;
const first = await request(app).post("/api/assistant/message").send({
session_id: sessionId,
useMock: true,
promptVersion: "normalizer_v2_0_2",
user_message: "Разложи цепочку документов и оплат по контрагентам за 2020-06, где разрыв механизма закрытия."
});
expect(first.status).toBe(200);
expect(first.body.debug?.investigation_state_snapshot?.problem_unit_state).toBeTruthy();
const second = await request(app).post("/api/assistant/message").send({
session_id: sessionId,
useMock: true,
promptVersion: "normalizer_v2_0_2",
user_message: "И по тому же разрыву добавь уточнение."
});
expect(second.status).toBe(200);
expect(second.body.debug?.followup_state_usage?.applied).toBe(true);
expect(second.body.debug?.followup_state_usage?.context_patch?.problem_continuity_available).toBe(true);
expect(second.body.debug?.followup_state_usage?.context_patch?.problem_continuity_applied).toBe(true);
expect(second.body.debug?.followup_state_usage?.context_patch?.strong_new_anchor_detected).toBe(false);
});
it("does not apply follow-up continuity when user gives strong new anchors", async () => {
const app = await createAppWithFlags({
state: "1",
binding: "1",
problemUnits: "1",
continuity: "1"
});
const sessionId = `asst-wave4-strong-anchor-${Date.now()}`;
const first = await request(app).post("/api/assistant/message").send({
session_id: sessionId,
useMock: true,
promptVersion: "normalizer_v2_0_2",
user_message: "Разбери хвосты по счету 60 за 2020-06 и покажи проблемные цепочки."
});
expect(first.status).toBe(200);
expect(first.body.debug?.investigation_state_snapshot?.turn_index).toBe(1);
const second = await request(app).post("/api/assistant/message").send({
session_id: sessionId,
useMock: true,
promptVersion: "normalizer_v2_0_2",
user_message: "И отдельно по счету 97 за 2020-07."
});
expect(second.status).toBe(200);
expect(second.body.debug?.followup_state_usage).toBeUndefined();
expect(second.body.debug?.investigation_state_snapshot?.turn_index).toBe(2);
});
it("keeps UTF-8 follow-up period refinement in-scope with soft continuity hints", async () => {
const app = await createAppWithFlags({
state: "1",
binding: "1",
problemUnits: "1",
continuity: "1",
answerPolicy: "1",
problemCentric: "1"
});
const first = await request(app).post("/api/assistant/message").send({
useMock: true,
promptVersion: "normalizer_v2_0_2",
user_message: "Посмотри, пожалуйста, где по поставщикам сейчас хвосты уже похожи именно на проблему, а не просто на шум."
});
expect(first.status).toBe(200);
expect(first.body.reply_type).not.toBe("out_of_scope");
const second = await request(app).post("/api/assistant/message").send({
session_id: first.body.session_id,
useMock: true,
promptVersion: "normalizer_v2_0_2",
user_message: "А если только за июнь 2020 смотреть, по кому это сильнее всего видно и что из этого реально может мешать закрытию?"
});
expect(second.status).toBe(200);
expect(second.body.reply_type).not.toBe("out_of_scope");
expect(second.body.debug?.followup_state_usage?.applied).toBe(true);
expect(second.body.debug?.followup_state_usage?.context_patch?.problem_continuity_applied).toBe(true);
expect(second.body.debug?.followup_state_usage?.context_patch?.strong_new_anchor_detected).toBe(false);
expect(
(second.body.debug?.routes ?? []).some((item: { route?: string }) => item.route && item.route !== "no_route")
).toBe(true);
const third = await request(app).post("/api/assistant/message").send({
useMock: true,
promptVersion: "normalizer_v2_0_2",
user_message: "Проверь, пожалуйста, по 60-му счёту за июнь 2020, где есть самый явный проблемный участок по расчётам с поставщиками."
});
expect(third.status).toBe(200);
expect(third.body.reply_type).not.toBe("out_of_scope");
});
});