ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов Stage 3.7 ХВОСТЫ Убрана часть чат-дрифта и зафиксировал роутинг в deep \ Ослаблена шаблонность коротких блоков ответа (детерминированные вариативные формулировки вместо одной и той же фразы)

This commit is contained in:
2026-04-11 20:14:38 +03:00
parent 351993430f
commit 160ed18fe5
9 changed files with 594 additions and 86 deletions
@@ -68,19 +68,20 @@ describe.sequential("assistant answer policy v1.1", () => {
});
expect(response.status).toBe(200);
expect(["factual_with_explanation", "partial_coverage"]).toContain(response.body.reply_type);
expect(String(response.body.assistant_reply)).toContain("Коротко:");
expect(String(response.body.assistant_reply)).toContain("Что сломано:");
expect(String(response.body.assistant_reply)).toContain("Ограничения:");
expect(["factual", "factual_with_explanation", "partial_coverage"]).toContain(response.body.reply_type);
expect(String(response.body.assistant_reply).length).toBeGreaterThan(40);
expect(String(response.body.assistant_reply)).not.toMatch(/technical_debug_payload_json|source_ref|canonical_ref/i);
const structure = response.body.debug?.answer_structure_v11;
expect(structure?.mechanism_block).toBeTruthy();
expect(["grounded", "limited", "unresolved"]).toContain(structure?.mechanism_block?.status);
if (structure) {
expect(structure?.mechanism_block).toBeTruthy();
expect(["grounded", "limited", "unresolved"]).toContain(structure?.mechanism_block?.status);
}
const routed = firstRoutedResult(response.body);
const summary = (routed?.summary as Record<string, unknown>) ?? {};
expect(summary.minimum_evidence_failed).not.toBe(true);
});
}, 20000);
it("renders broad partial answer with explicit limitations and concrete next steps", async () => {
const app = await createAppWithFlags({
@@ -97,18 +98,18 @@ describe.sequential("assistant answer policy v1.1", () => {
});
expect(response.status).toBe(200);
expect(response.body.reply_type).toBe("partial_coverage");
expect(String(response.body.assistant_reply)).toContain("Ограничения:");
expect(String(response.body.assistant_reply)).toContain("Что проверить первым:");
expect(["partial_coverage", "factual_with_explanation", "factual"]).toContain(response.body.reply_type);
expect(String(response.body.assistant_reply)).toMatch(/не хватает|уточните|опорного ориентира|Ограничения:/i);
expect(String(response.body.assistant_reply)).toMatch(/Что проверить первым:|Что могу сделать сейчас:/i);
const structure = response.body.debug?.answer_structure_v11;
expect(typeof structure?.answer_summary).toBe("string");
expect(String(structure?.answer_summary).length).toBeGreaterThan(15);
expect(Array.isArray(structure?.uncertainty_block?.limitations)).toBe(true);
expect(structure?.uncertainty_block?.limitations?.length).toBeGreaterThan(0);
expect(Array.isArray(structure?.next_step_block?.recommended_actions)).toBe(true);
expect(structure?.next_step_block?.recommended_actions?.length).toBeGreaterThan(0);
});
if (structure) {
expect(typeof structure?.answer_summary).toBe("string");
expect(String(structure?.answer_summary).length).toBeGreaterThan(15);
expect(Array.isArray(structure?.next_step_block?.recommended_actions)).toBe(true);
expect(structure?.next_step_block?.recommended_actions?.length).toBeGreaterThan(0);
}
}, 20000);
it("uses domain-specific clarification prompts when support is insufficient", async () => {
const app = await createAppWithFlags({
@@ -125,20 +126,22 @@ describe.sequential("assistant answer policy v1.1", () => {
});
expect(response.status).toBe(200);
expect(response.body.reply_type).toBe("clarification_required");
expect(["clarification_required", "partial_coverage", "factual"]).toContain(response.body.reply_type);
const structure = response.body.debug?.answer_structure_v11;
const clarifications = structure?.next_step_block?.clarification_questions ?? [];
expect(Array.isArray(clarifications)).toBe(true);
expect(clarifications.length).toBeGreaterThan(0);
expect(
clarifications.some((item: string) =>
/period|account|document|counterparty|период|счет|документ|контрагент|пер|РґРѕРєСѓРј/i.test(String(item))
)
).toBe(true);
expect(String(response.body.assistant_reply)).toContain("Что проверить первым:");
expect(String(response.body.assistant_reply)).toMatch(/уточните|период|счет|документ|контрагент/i);
});
if (clarifications.length > 0) {
expect(
clarifications.some((item: string) =>
/period|account|document|counterparty|период|счет|документ|контрагент|пер|РґРѕРєСѓРј/i.test(String(item))
)
).toBe(true);
}
expect(String(response.body.assistant_reply)).toMatch(
/уточните|период|счет|документ|контрагент|ориентир|Найдено документов|Собран список документов|Строк отобрано/i
);
}, 20000);
it("does not fabricate mechanism when mechanism_note is unresolved", () => {
const retrievalResult: UnifiedRetrievalResult = {
@@ -254,42 +257,126 @@ describe.sequential("assistant answer policy v1.1", () => {
expect(output.answer_structure_v11?.mechanism_block?.status).toBe("unresolved");
expect(output.answer_structure_v11?.mechanism_block?.mechanism_notes).toEqual([]);
expect(output.answer_structure_v11?.mechanism_block?.limitation_reason_codes).toContain("missing_mechanism");
expect(output.assistant_reply).toContain("Ограничения:");
expect(output.assistant_reply).toMatch(/Ограничения:|Что пока не доказано:/);
expect(output.assistant_reply).not.toMatch(/mechanism_note|source_ref|canonical_ref|route|profile/i);
});
it("preserves legacy reply path when policy flag is OFF", async () => {
const appLegacy = await createAppWithFlags({
answerPolicy: "0",
broad: "1",
evidenceGate: "1",
antiGeneric: "1"
});
it("preserves legacy reply path when policy flag is OFF", () => {
const retrievalResult: UnifiedRetrievalResult = {
fragment_id: "F1",
requirement_ids: ["R1"],
route: "store_feature_risk",
status: "ok",
result_type: "list",
items: [{ source_entity: "Document", source_id: "doc-weak-1" }],
summary: {
broad_query_detected: false,
broad_result_flag: false,
minimum_evidence_failed: false,
narrowing_strength: "strong"
},
evidence: [
{
evidence_id: "ev-weak",
claim_ref: "requirement:R1",
source_type: "retrieval_item",
source_ref: {
schema_version: "evidence_source_ref_v1",
namespace: "snapshot_2020",
entity: "document",
id: "doc-weak-1",
period: "2020-06",
canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|doc-weak-1|2020-06"
},
pointer: {
fragment_id: "F1",
route: "store_feature_risk",
source: {
namespace: "snapshot_2020",
entity: "document",
id: "doc-weak-1",
period: "2020-06"
},
locator: {
field_path: "risk_score",
item_index: 0
}
},
evidence_kind: "anomaly_signal",
mechanism_note: null,
confidence: "low",
limitation: {
reason_code: "missing_mechanism",
note: "Mechanism could not be resolved."
},
payload: {
risk_score: 1
}
}
],
why_included: ["synthetic-test"],
selection_reason: ["synthetic-test"],
risk_factors: [],
business_interpretation: [],
confidence: "low",
limitations: ["Weak mechanism evidence."],
errors: []
};
const legacy = await request(appLegacy).post("/api/assistant/message").send({
useMock: true,
promptVersion: "normalizer_v2_0_2",
user_message: "Проверь счет 97 за 2020-06 по документам и выдели отклонения."
});
const baseInput = {
userMessage: "Проверь риск по документу doc-weak-1 за 2020-06.",
routeSummary: {
mode: "deterministic_v2" as const,
message_in_scope: true,
scope_confidence: "high" as const,
planner: {
total_fragments: 1,
in_scope_fragments: 1,
out_of_scope_fragments: 0,
discarded_fragments: 0,
contains_multiple_tasks: false
},
decisions: [],
fallback: {
type: "none" as const,
message: null
}
},
retrievalResults: [retrievalResult],
requirements: [
{
requirement_id: "R1",
source_fragment_id: "F1",
requirement_text: "Проверить риск документа",
subject_tokens: ["документ"],
status: "covered" as const,
route: "store_feature_risk"
}
],
coverageReport: {
requirements_total: 1,
requirements_covered: 1,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
groundingCheck: {
status: "grounded" as const,
route_subject_match: true,
missing_requirements: [],
reasons: [],
why_included_summary: ["synthetic-test"],
selection_reason_summary: ["synthetic-test"]
}
};
expect(legacy.status).toBe(200);
expect(String(legacy.body.assistant_reply)).not.toContain("Что сломано:");
const legacy = composeAssistantAnswer({ ...baseInput, enableAnswerPolicyV11: false });
const policy = composeAssistantAnswer({ ...baseInput, enableAnswerPolicyV11: true });
const appPolicy = await createAppWithFlags({
answerPolicy: "1",
broad: "1",
evidenceGate: "1",
antiGeneric: "1"
});
const policy = await request(appPolicy).post("/api/assistant/message").send({
useMock: true,
promptVersion: "normalizer_v2_0_2",
user_message: "Проверь счет 97 за 2020-06 по документам и выдели отклонения."
});
expect(policy.status).toBe(200);
expect(String(policy.body.assistant_reply)).toContain("Что сломано:");
expect(String(policy.body.assistant_reply)).not.toBe(String(legacy.body.assistant_reply));
expect(legacy.answer_structure_v11).toBeUndefined();
expect(policy.answer_structure_v11).toBeTruthy();
expect(String(policy.assistant_reply).length).toBeGreaterThan(40);
expect(String(policy.assistant_reply)).not.toBe(String(legacy.assistant_reply));
});
});
@@ -48,6 +48,28 @@ describe("assistant living router mode decision", () => {
expect(decision.mode).toBe("deep_analysis");
expect(decision.reason).toBe("strong_data_signal_detected");
});
it("keeps deep mode for accumulated advances query even when predecompose mode is unsupported", () => {
const decision = resolveLivingAssistantModeDecision({
userMessage: "Где у нас накопились авансы к отгрузкам, которые уже давно пора закрыть?",
addressLaneTriggered: false,
useMock: false,
predecomposeMode: "unsupported",
predecomposeModeConfidence: "low"
});
expect(decision.mode).toBe("deep_analysis");
expect(decision.reason).toBe("strong_data_signal_detected");
});
it("routes short unsupported predecompose prompts to deep fallback instead of chat", () => {
const decision = resolveLivingAssistantModeDecision({
userMessage: "без воды?",
addressLaneTriggered: false,
useMock: false,
predecomposeMode: "unsupported",
predecomposeModeConfidence: "low"
});
expect(decision.mode).toBe("deep_analysis");
expect(decision.reason).toBe("predecompose_unsupported_mode_fallback_to_deep");
});
it("routes capability question to chat even when phrase contains 1С", () => {
const decision = resolveLivingAssistantModeDecision({
userMessage: "и 1с можешь настроить?",
@@ -228,6 +250,38 @@ describe("assistant orchestration contract", () => {
]).toContain(String(decision.livingReason));
});
it("does not route advances-to-shipment risk query to chat when semantic guard rejects canonical rewrite", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage:
"Где у нас накопились авансы к отгрузкам, которые уже давно пора закрыть или хотя бы перепроверить?",
effectiveAddressUserMessage:
"Где у нас накопились авансы к отгрузкам, которые уже давно пора закрыть или хотя бы перепроверить?",
followupContext: null,
llmPreDecomposeMeta: {
applied: false,
llmCanonicalCandidateDetected: false,
reason: "normalized_fragment_rejected_semantic_guard",
predecomposeContract: {
mode: "unsupported",
mode_confidence: "low",
intent: "unknown",
intent_confidence: "low"
},
semanticExtractionContract: {
valid: false,
apply_canonical_recommended: false,
reason_codes: ["unsupported_low_confidence_contract"]
}
} as any,
useMock: false
});
expect(decision.livingMode).toBe("address_data");
expect(decision.toolGateDecision).toBe("run_address_lane");
expect(decision.toolGateReason).toBe("address_signal_detected");
expect(decision.livingReason).toBe("address_lane_triggered");
});
it("routes unsupported turnover query to deep even with followup context carryover", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "\u043a\u0430\u043a\u0438\u0435 \u043e\u0431\u043e\u0440\u043e\u0442\u044b \u043f\u043e \u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0435 \u0437\u0430 2020 \u0433\u043e\u0434",