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
@@ -0,0 +1,102 @@
import { describe, expect, it } from "vitest";
import { composeAssistantAnswer } from "../src/services/answerComposer";
import type { UnifiedRetrievalResult } from "../src/types/assistant";
function buildRetrievalWithMojibake(): UnifiedRetrievalResult {
return {
fragment_id: "F1",
requirement_ids: ["R1"],
route: "hybrid_store_plus_live",
status: "ok",
result_type: "chain",
items: [
{
counterparty_id: "CP-1",
operations_count: 12,
document_refs_count: 3
}
],
summary: {
route_focus: "cross_entity_breakage",
source_records: 262,
filtered_records_after_narrowing: 24,
checked_records: 24,
semantic_narrowing_applied: true,
ranking_basis: ["closure_risk", "repeatability", "financial_impact"]
},
evidence: [],
why_included: [
"Семантическое сужение выполнено по профилю cross_entity_breakage.",
"После narrowing осталось 24 из 262 записей."
],
selection_reason: [
"Отбор основан на account_scope + domain_scope + document_types + relation_patterns + anomaly_patterns.",
"Ранжирование по basis: closure_risk, repeatability, financial_impact."
],
risk_factors: ["broken_chain", "period_close_risk"],
business_interpretation: [],
confidence: "medium",
limitations: [],
errors: []
};
}
describe("assistant answer encoding sanitizer", () => {
it("filters mojibake in explainable answer and falls back to readable reasoning", () => {
const output = composeAssistantAnswer({
userMessage: "Разложи цепочку и покажи хвосты по расчетам за 2020-06.",
routeSummary: {
mode: "deterministic_v2",
message_in_scope: true,
scope_confidence: "high",
planner: {
total_fragments: 1,
in_scope_fragments: 1,
out_of_scope_fragments: 0,
discarded_fragments: 0,
contains_multiple_tasks: false
},
decisions: [],
fallback: {
type: "none",
message: null
}
},
retrievalResults: [buildRetrievalWithMojibake()],
requirements: [
{
requirement_id: "R1",
source_fragment_id: "F1",
requirement_text: "Проверка цепочки расчетов",
subject_tokens: ["chain", "account_60"],
status: "covered",
route: "hybrid_store_plus_live"
}
],
coverageReport: {
requirements_total: 1,
requirements_covered: 1,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
groundingCheck: {
status: "grounded",
route_subject_match: true,
missing_requirements: [],
reasons: [],
why_included_summary: [],
selection_reason_summary: []
},
enableAnswerPolicyV11: false
});
expect(output.reply_type).toBe("factual_with_explanation");
expect(output.assistant_reply).toContain("Почему это попало в ответ:");
expect(output.assistant_reply).not.toMatch(/(?:Р.|С.){5,}/u);
expect(output.assistant_reply).toContain("Проверка выполнена по профилю cross_entity_breakage.");
expect(output.assistant_reply).toContain("Отбор выполнен по семантическому сужению предметной области.");
});
});