ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов Stage 3.7 ХВОСТЫ Убрана часть чат-дрифта и зафиксировал роутинг в deep \ Ослаблена шаблонность коротких блоков ответа (детерминированные вариативные формулировки вместо одной и той же фразы)
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user