ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов 2.2.1 - фикс деградаций по старым доменам + легкая доводка регрессий перед стартом 3его этапа
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { composeAssistantAnswer } from "../src/services/answerComposer";
|
||||
|
||||
function buildRouteSummary(fallbackType: "none" | "clarification" | "out_of_scope" = "none") {
|
||||
return {
|
||||
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: fallbackType,
|
||||
message: null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function buildCoverageReport() {
|
||||
return {
|
||||
requirements_total: 0,
|
||||
requirements_covered: 0,
|
||||
requirements_uncovered: [],
|
||||
requirements_partially_covered: [],
|
||||
clarification_needed_for: [],
|
||||
out_of_scope_requirements: []
|
||||
};
|
||||
}
|
||||
|
||||
describe("assistant boundary fallback reply", () => {
|
||||
it("uses soft refusal without template sections when domain is not covered", () => {
|
||||
const output = composeAssistantAnswer({
|
||||
userMessage: "Скажи курс доллара на завтра и дай прогноз инфляции.",
|
||||
routeSummary: buildRouteSummary("none"),
|
||||
retrievalResults: [],
|
||||
requirements: [],
|
||||
coverageReport: buildCoverageReport(),
|
||||
groundingCheck: {
|
||||
status: "no_grounded_answer",
|
||||
route_subject_match: false,
|
||||
missing_requirements: [],
|
||||
reasons: ["no grounded support"],
|
||||
why_included_summary: [],
|
||||
selection_reason_summary: []
|
||||
},
|
||||
enableAnswerPolicyV11: true
|
||||
});
|
||||
|
||||
expect(output.reply_type).toBe("clarification_required");
|
||||
expect(output.assistant_reply).toMatch(/мягкий отказ/i);
|
||||
expect(output.assistant_reply).toContain("Что могу сделать рядом по смыслу:");
|
||||
expect(output.assistant_reply).not.toContain("Что сломано:");
|
||||
});
|
||||
|
||||
it("for covered domain without anchors uses soft clarification with nearby capability", () => {
|
||||
const output = composeAssistantAnswer({
|
||||
userMessage: "Покажи где по контрагентам хвосты по оплатам.",
|
||||
routeSummary: buildRouteSummary("none"),
|
||||
retrievalResults: [],
|
||||
requirements: [],
|
||||
coverageReport: buildCoverageReport(),
|
||||
groundingCheck: {
|
||||
status: "no_grounded_answer",
|
||||
route_subject_match: false,
|
||||
missing_requirements: [],
|
||||
reasons: ["no grounded support"],
|
||||
why_included_summary: [],
|
||||
selection_reason_summary: []
|
||||
},
|
||||
enableAnswerPolicyV11: true
|
||||
});
|
||||
|
||||
expect(output.reply_type).toBe("clarification_required");
|
||||
expect(output.assistant_reply).toMatch(/не могу надежно ответить по сценарию/i);
|
||||
expect(output.assistant_reply).toContain("Чтобы сразу перейти к проверке, уточни:");
|
||||
expect(output.assistant_reply).toContain("Если удобнее, могу начать с близкого сценария:");
|
||||
expect(output.assistant_reply).not.toContain("Что сломано:");
|
||||
});
|
||||
|
||||
it("keeps out_of_scope reply type but responds with soft fallback text", () => {
|
||||
const output = composeAssistantAnswer({
|
||||
userMessage: "Скажи прогноз погоды на выходные.",
|
||||
routeSummary: buildRouteSummary("out_of_scope"),
|
||||
retrievalResults: [],
|
||||
requirements: [],
|
||||
coverageReport: buildCoverageReport(),
|
||||
groundingCheck: {
|
||||
status: "no_grounded_answer",
|
||||
route_subject_match: false,
|
||||
missing_requirements: [],
|
||||
reasons: ["out of scope"],
|
||||
why_included_summary: [],
|
||||
selection_reason_summary: []
|
||||
},
|
||||
enableAnswerPolicyV11: true
|
||||
});
|
||||
|
||||
expect(output.reply_type).toBe("out_of_scope");
|
||||
expect(output.assistant_reply).toMatch(/мягкий отказ|не могу надежно/i);
|
||||
expect(output.assistant_reply).not.toContain("Что сломано:");
|
||||
});
|
||||
});
|
||||
@@ -126,6 +126,18 @@ describe("assistant living router mode decision", () => {
|
||||
expect(decision.reason).toBe("assistant_data_scope_query_detected");
|
||||
});
|
||||
|
||||
it("routes slang data-scope wording 'по каким конторам можем общаться' to chat", () => {
|
||||
const decision = resolveLivingAssistantModeDecision({
|
||||
userMessage: "\u043f\u043e \u043a\u0430\u043a\u0438\u043c \u043a\u043e\u043d\u0442\u043e\u0440\u0430\u043c \u043c\u043e\u0436\u0435\u043c \u043e\u0431\u0449\u0430\u0442\u044c\u0441\u044f?",
|
||||
addressLaneTriggered: false,
|
||||
useMock: false,
|
||||
predecomposeMode: "unsupported",
|
||||
predecomposeModeConfidence: "low"
|
||||
});
|
||||
expect(decision.mode).toBe("chat");
|
||||
expect(decision.reason).toBe("assistant_data_scope_query_detected");
|
||||
});
|
||||
|
||||
it("routes data-scope wording without question mark when interrogative token is present", () => {
|
||||
const decision = resolveLivingAssistantModeDecision({
|
||||
userMessage: "каза какой компании подключена к 1с",
|
||||
@@ -185,6 +197,54 @@ describe("assistant orchestration contract", () => {
|
||||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||||
});
|
||||
|
||||
it("routes unsupported turnover-by-organization query to deep analysis", () => {
|
||||
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",
|
||||
effectiveAddressUserMessage: "\u041e\u0431\u043e\u0440\u043e\u0442\u044b \u043f\u043e \u0441\u0447\u0435\u0442\u0443 '\u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430' \u0437\u0430 2020 \u0433\u043e\u0434.",
|
||||
followupContext: null,
|
||||
llmPreDecomposeMeta: {
|
||||
applied: true,
|
||||
llmCanonicalCandidateDetected: true,
|
||||
predecomposeContract: {
|
||||
mode: "unsupported",
|
||||
mode_confidence: "low",
|
||||
intent: "account_balance_snapshot",
|
||||
intent_confidence: "high"
|
||||
}
|
||||
} as any,
|
||||
useMock: false
|
||||
});
|
||||
|
||||
expect(decision.runAddressLane).toBe(false);
|
||||
expect(decision.toolGateDecision).toBe("skip_address_lane");
|
||||
expect(decision.livingMode).toBe("deep_analysis");
|
||||
expect([
|
||||
"address_signal_unsupported_intent_fallback_to_deep",
|
||||
"aggregate_analytics_signal_fallback_to_deep"
|
||||
]).toContain(String(decision.toolGateReason));
|
||||
expect([
|
||||
"unsupported_address_intent_fallback_to_deep",
|
||||
"aggregate_analytics_signal_fallback_to_deep"
|
||||
]).toContain(String(decision.livingReason));
|
||||
});
|
||||
|
||||
it("routes profitability ranking query to deep analysis instead of address lane", () => {
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "\u043a\u0430\u043a\u043e\u0439 \u0441\u0430\u043c\u044b\u0439 \u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0439 \u0433\u043e\u0434?",
|
||||
effectiveAddressUserMessage: "\u043a\u0430\u043a\u043e\u0439 \u0441\u0430\u043c\u044b\u0439 \u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0439 \u0433\u043e\u0434?",
|
||||
followupContext: null,
|
||||
llmPreDecomposeMeta: null as any,
|
||||
useMock: false
|
||||
} as any);
|
||||
|
||||
expect(decision.runAddressLane).toBe(false);
|
||||
expect(decision.toolGateDecision).toBe("skip_address_lane");
|
||||
expect(decision.toolGateReason).toBe("aggregate_analytics_signal_fallback_to_deep");
|
||||
expect(decision.livingMode).toBe("deep_analysis");
|
||||
expect(decision.livingReason).toBe("aggregate_analytics_signal_fallback_to_deep");
|
||||
expect(decision.orchestrationContract?.aggregate_analytics_signal_fallback_to_deep).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps VAT explain follow-up in address lane when followup context is present", () => {
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "почему прогноз к уплате 0?",
|
||||
@@ -236,6 +296,33 @@ describe("assistant orchestration contract", () => {
|
||||
expect(decision.orchestrationContract?.unsupported_address_intent_fallback_to_deep).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps list_open_contracts query in address lane despite 'unclosed' wording", () => {
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "\u041f\u043e\u043a\u0430\u0436\u0438 \u043d\u0435\u0437\u0430\u043a\u0440\u044b\u0442\u044b\u0435 \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u044b \u043d\u0430 2020-12-31",
|
||||
effectiveAddressUserMessage: "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043d\u0435\u0437\u0430\u043a\u0440\u044b\u0442\u044b\u0435 \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u044b \u043f\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044e \u043d\u0430 \u043a\u043e\u043d\u0435\u0446 \u0434\u0435\u043a\u0430\u0431\u0440\u044f 2020 \u0433\u043e\u0434\u0430.",
|
||||
followupContext: {
|
||||
previous_intent: "month_close_costs_20_44"
|
||||
},
|
||||
llmPreDecomposeMeta: {
|
||||
applied: true,
|
||||
llmCanonicalCandidateDetected: true,
|
||||
predecomposeContract: {
|
||||
mode: "address_query",
|
||||
mode_confidence: "high",
|
||||
intent: "list_open_contracts",
|
||||
intent_confidence: "medium"
|
||||
}
|
||||
} as any,
|
||||
useMock: false
|
||||
} as any);
|
||||
|
||||
expect(decision.runAddressLane).toBe(true);
|
||||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||||
expect(decision.livingMode).toBe("address_data");
|
||||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||||
expect(decision.toolGateReason).toBe("address_mode_classifier_detected");
|
||||
});
|
||||
|
||||
it("does not force address lane for deep-analysis unknown intent query with date-like token", () => {
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "найди какие либо ошибки на 21 мая 2022 года",
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { __autoRunsQuestionTestUtils } from "../src/routes/autoRuns";
|
||||
|
||||
describe("autoruns question extraction", () => {
|
||||
it("merges conversational tails instead of producing micro-prompts", () => {
|
||||
const parsed = __autoRunsQuestionTestUtils.splitQuestionCandidates(
|
||||
"Покажи рисковые хвосты по расчетам с поставщиками? и коротко? без воды?"
|
||||
);
|
||||
|
||||
expect(parsed).toHaveLength(1);
|
||||
expect(parsed[0]).toMatch(/коротко/i);
|
||||
expect(parsed[0]).toMatch(/без воды/i);
|
||||
});
|
||||
|
||||
it("keeps independent questions separated", () => {
|
||||
const parsed = __autoRunsQuestionTestUtils.splitQuestionCandidates(
|
||||
"Где зависли оплаты по счету 60? Какие акты сверки с риском расхождения по 62?"
|
||||
);
|
||||
|
||||
expect(parsed).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("extracts questions from JSON payload and skips placeholders", () => {
|
||||
const parsed = __autoRunsQuestionTestUtils.extractQuestionsFromAutogenOutput(
|
||||
JSON.stringify({
|
||||
questions: ["Вопросы", "Покажи хвосты по поставщикам", "и коротко?"]
|
||||
})
|
||||
);
|
||||
|
||||
expect(parsed).toHaveLength(1);
|
||||
expect(parsed[0]).toMatch(/поставщик/i);
|
||||
expect(parsed[0]).toMatch(/коротко/i);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { __evalRouteTestUtils } from "../src/routes/eval";
|
||||
|
||||
describe("eval runtime question splitting", () => {
|
||||
it("merges short conversational tails into previous question", () => {
|
||||
const parsed = __evalRouteTestUtils.splitQuestionCandidate(
|
||||
"Покажи контрагентов с риском несверки по акту? и коротко? без воды?"
|
||||
);
|
||||
|
||||
expect(parsed).toHaveLength(1);
|
||||
expect(parsed[0]).toMatch(/коротко/i);
|
||||
expect(parsed[0]).toMatch(/без воды/i);
|
||||
});
|
||||
|
||||
it("keeps independent full questions as separate items", () => {
|
||||
const parsed = __evalRouteTestUtils.splitQuestionCandidate(
|
||||
"Где зависли оплаты по счету 60? Какие документы не закрылись по 62 за июль 2020?"
|
||||
);
|
||||
|
||||
expect(parsed).toHaveLength(2);
|
||||
expect(parsed[0]).toMatch(/\?/);
|
||||
expect(parsed[1]).toMatch(/\?/);
|
||||
});
|
||||
|
||||
it("normalizes list input and removes placeholders and duplicates", () => {
|
||||
const parsed = __evalRouteTestUtils.normalizeRuntimeQuestions([
|
||||
"Вопросы",
|
||||
"Покажи хвосты по поставщикам",
|
||||
"и коротко?"
|
||||
]);
|
||||
|
||||
expect(parsed).toHaveLength(1);
|
||||
expect(parsed[0]).toMatch(/поставщик/i);
|
||||
expect(parsed[0]).toMatch(/коротко/i);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user