ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов Stage 3.6 Усилина оркестрация и детект data-scope для живого сленга. Убрана шаблонность soft-refusal в limited-ответах. Подрезана словарнаю жирность в фильтр-экстракторе

This commit is contained in:
2026-04-11 19:45:20 +03:00
parent 88d5561fad
commit 351993430f
20 changed files with 2060 additions and 830 deletions
@@ -1883,6 +1883,15 @@ describe("address filter extraction for balance drilldown", () => {
expect(extracted.warnings).toContain("counterparty_anchor_dropped_low_quality");
});
it("does not capture narrative filler as counterparty in broad docs-vs-money question", () => {
const extracted = extractAddressFilters(
"В каких случаях мы видим ситуацию, когда документы есть, а денег нет и пока не предвидится?",
"list_documents_by_counterparty"
);
expect(extracted.extracted_filters.counterparty).toBeUndefined();
expect(String(extracted.extracted_filters.counterparty ?? "")).not.toContain("случая");
});
it("does not derive fake counterparty anchor for open-contracts stale-advance wording", () => {
const extracted = extractAddressFilters(
"по каким поставщикам мы видим проблемные авансы, которые давно не закрыты документами?",
@@ -2070,10 +2079,7 @@ describe("address filter extraction for balance drilldown", () => {
it("extracts leading counterparty token for short bank phrase", () => {
const result = extractAddressFilters("свк списания/поступления за 2020", "bank_operations_by_counterparty");
expect(result.extracted_filters.counterparty).toBe("свк");
expect(
result.warnings.includes("counterparty_anchor_derived_from_leading_token") ||
result.warnings.includes("counterparty_anchor_derived_from_free_text_heuristic")
).toBe(true);
expect(result.warnings).toContain("counterparty_anchor_derived_from_leading_token");
});
it("treats 'за весь период' as all-time hint and does not force 90-day default", () => {
@@ -2119,7 +2125,7 @@ describe("address filter extraction for balance drilldown", () => {
expect(result.extracted_filters.period_to).toBe("2020-12-31");
});
it("extracts free-text counterparty and relaxed short-year period from noisy phrase", () => {
it("extracts compact counterparty and relaxed short-year period from noisy phrase", () => {
const result = extractAddressFilters(
"свк 20 год - покажи доки плс",
"list_documents_by_counterparty"
@@ -2127,7 +2133,7 @@ describe("address filter extraction for balance drilldown", () => {
expect(result.extracted_filters.counterparty).toBe("свк");
expect(result.extracted_filters.period_from).toBe("2020-01-01");
expect(result.extracted_filters.period_to).toBe("2020-12-31");
expect(result.warnings).toContain("counterparty_anchor_derived_from_free_text_heuristic");
expect(result.warnings).toContain("counterparty_anchor_derived_from_leading_token");
expect(result.warnings).toContain("period_derived_from_year_phrase");
expect(result.extracted_filters.counterparty).not.toBe("плс");
});
@@ -2172,8 +2178,8 @@ describe("address filter extraction for balance drilldown", () => {
expect(
result.warnings.some(
(warning) =>
warning === "counterparty_anchor_derived_from_free_text_heuristic" ||
warning === "counterparty_anchor_derived_from_implicit_phrase"
warning === "counterparty_anchor_derived_from_implicit_phrase" ||
warning === "counterparty_anchor_derived_from_leading_token"
)
).toBe(true);
expect(result.warnings).toContain("period_derived_from_year_phrase");
@@ -272,6 +272,28 @@ describe("assistant orchestration contract", () => {
).toBe(true);
});
it("routes standalone aggregate query to deep even when stale followup context exists and LLM predecompose is unavailable", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "какие обороты по альтернативе за 2020 год",
effectiveAddressUserMessage: "какие обороты по альтернативе за 2020 год",
followupContext: {
previous_intent: "list_documents_by_contract",
previous_filters: {
organization: "ООО Альтернатива Плюс"
}
},
llmPreDecomposeMeta: null as any,
useMock: true
} 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("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?",
@@ -289,6 +311,47 @@ describe("assistant orchestration contract", () => {
expect(decision.orchestrationContract?.aggregate_analytics_signal_fallback_to_deep).toBe(true);
});
it("keeps unsupported retrieval query in address lane when LLM runtime is unavailable", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage:
"\u0413\u0434\u0435 \u0443 \u043d\u0430\u0441 \u043d\u0430\u043a\u043e\u043f\u0438\u043b\u0438\u0441\u044c \u0430\u0432\u0430\u043d\u0441\u044b \u043a \u043e\u0442\u0433\u0440\u0443\u0437\u043a\u0430\u043c, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0434\u0430\u0432\u043d\u043e \u043f\u043e\u0440\u0430 \u0437\u0430\u043a\u0440\u044b\u0442\u044c?",
effectiveAddressUserMessage:
"\u0413\u0434\u0435 \u0443 \u043d\u0430\u0441 \u043d\u0430\u043a\u043e\u043f\u0438\u043b\u0438\u0441\u044c \u0430\u0432\u0430\u043d\u0441\u044b \u043a \u043e\u0442\u0433\u0440\u0443\u0437\u043a\u0430\u043c, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0434\u0430\u0432\u043d\u043e \u043f\u043e\u0440\u0430 \u0437\u0430\u043a\u0440\u044b\u0442\u044c?",
followupContext: null,
llmPreDecomposeMeta: {
applied: false,
reason: "error:OpenAI API key is missing.",
llmCanonicalCandidateDetected: false,
predecomposeContract: {
mode: "unsupported",
mode_confidence: "low",
intent: "unknown",
intent_confidence: "low"
},
semanticExtractionContract: {
valid: false,
apply_canonical_recommended: false,
reason_codes: ["unsupported_low_confidence_contract", "deep_investigation_signal_detected"],
guard_hints: {
deep_investigation_signal_detected: true
},
extraction: {
query_shape: "VERIFY_FACTUAL",
aggregation_profile: "unknown"
}
}
} as any,
useMock: false
});
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.orchestrationContract?.unsupported_address_intent_fallback_to_deep).toBe(false);
expect(decision.orchestrationContract?.deep_analysis_signal_fallback_to_deep).toBe(false);
});
it("keeps VAT explain follow-up in address lane when followup context is present", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "почему прогноз к уплате 0?",
@@ -367,6 +430,48 @@ describe("assistant orchestration contract", () => {
expect(decision.toolGateReason).toBe("address_mode_classifier_detected");
});
it("keeps open-contracts request in address lane even with stale deep followup context when LLM contract is absent", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "Покажи незакрытые договоры на 2020-12-31",
effectiveAddressUserMessage: "Покажи незакрытые договоры на 2020-12-31",
followupContext: {
previous_intent: "month_close_costs_20_44"
},
llmPreDecomposeMeta: null as any,
useMock: true
} 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");
});
it("routes 'по каким конторам можем общаться?' to chat/data-scope in orchestration contract", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "по каким конторам можем общаться?",
effectiveAddressUserMessage: "по каким контрагентам у нас есть активность или договоры?",
followupContext: null,
llmPreDecomposeMeta: {
applied: true,
llmCanonicalCandidateDetected: true,
predecomposeContract: {
mode: "address_query",
mode_confidence: "medium",
intent: "list_documents_by_contract",
intent_confidence: "medium"
}
} as any,
useMock: false
} as any);
expect(decision.runAddressLane).toBe(false);
expect(decision.toolGateDecision).toBe("skip_address_lane");
expect(decision.toolGateReason).toBe("assistant_data_scope_query_detected");
expect(decision.livingMode).toBe("chat");
expect(decision.livingReason).toBe("assistant_data_scope_query_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,180 @@
import { describe, expect, it } from "vitest";
import { composeAssistantAnswer } from "../src/services/answerComposer";
import type { UnifiedRetrievalResult } from "../src/types/assistant";
function routeSummary() {
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: "none" as const,
message: null
}
};
}
describe("assistant soft policy reply", () => {
it("renders soft non-template reply for weak broad-partial envelope", () => {
const retrieval: UnifiedRetrievalResult = {
fragment_id: "F1",
requirement_ids: ["R1"],
route: "store_feature_risk",
status: "partial",
result_type: "summary",
items: [{ note: "weak candidate" }],
summary: {
broad_query_detected: true,
broad_result_flag: true,
minimum_evidence_failed: true,
narrowing_strength: "weak"
},
evidence: [],
why_included: [],
selection_reason: [],
risk_factors: [],
business_interpretation: [],
confidence: "low",
limitations: ["Weak evidence envelope"],
errors: []
};
const output = composeAssistantAnswer({
userMessage: "Покажи общую картину рисков и аномалий по документам.",
routeSummary: routeSummary(),
retrievalResults: [retrieval],
requirements: [
{
requirement_id: "R1",
source_fragment_id: "F1",
requirement_text: "общая картина рисков",
subject_tokens: [],
status: "partially_covered",
route: "store_feature_risk"
}
],
coverageReport: {
requirements_total: 1,
requirements_covered: 0,
requirements_uncovered: [],
requirements_partially_covered: ["R1"],
clarification_needed_for: [],
out_of_scope_requirements: []
},
groundingCheck: {
status: "partial",
route_subject_match: true,
missing_requirements: ["R1"],
reasons: ["insufficient_detail"],
why_included_summary: [],
selection_reason_summary: []
},
enableAnswerPolicyV11: true
});
expect(output.reply_type).toBe("partial_coverage");
expect(output.assistant_reply).toContain("Что могу сделать сейчас:");
expect(output.assistant_reply).toContain("Что пока не доказано:");
expect(output.assistant_reply).not.toContain("Что сломано:");
});
it("keeps structured sectioned reply for focused grounded envelope", () => {
const retrieval: UnifiedRetrievalResult = {
fragment_id: "F1",
requirement_ids: ["R1"],
route: "store_feature_risk",
status: "ok",
result_type: "summary",
items: [{ doc: "A-1" }],
summary: {
broad_query_detected: false,
broad_result_flag: false,
minimum_evidence_failed: false,
narrowing_strength: "strong"
},
evidence: [
{
evidence_id: "ev-1",
claim_ref: "requirement:R1",
source_type: "retrieval_item",
source_ref: {
schema_version: "evidence_source_ref_v1",
namespace: "snapshot_2020",
entity: "document",
id: "A-1",
period: "2020-06",
canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|A-1|2020-06"
},
pointer: {
fragment_id: "F1",
route: "store_feature_risk",
source: {
namespace: "snapshot_2020",
entity: "document",
id: "A-1",
period: "2020-06"
}
},
evidence_kind: "fact",
mechanism_note: "Переход подтвержден документом и проводкой.",
confidence: "high",
payload: {
amount: 100
}
}
],
why_included: ["релевантная запись"],
selection_reason: ["сильное совпадение"],
risk_factors: [],
business_interpretation: [],
confidence: "high",
limitations: [],
errors: []
};
const output = composeAssistantAnswer({
userMessage: "Проверь документ A-1 за июнь 2020.",
routeSummary: routeSummary(),
retrievalResults: [retrieval],
requirements: [
{
requirement_id: "R1",
source_fragment_id: "F1",
requirement_text: "проверить документ",
subject_tokens: [],
status: "covered",
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",
route_subject_match: true,
missing_requirements: [],
reasons: [],
why_included_summary: ["релевантная запись"],
selection_reason_summary: ["сильное совпадение"]
},
enableAnswerPolicyV11: true
});
expect(output.reply_type).toBe("factual_with_explanation");
expect(output.assistant_reply).toContain("Что сломано:");
expect(output.assistant_reply).toContain("Ограничения:");
});
});
@@ -0,0 +1,74 @@
import { describe, expect, it } from "vitest";
import { __evalRouteAsyncTestUtils } from "../src/routes/eval";
describe("eval async job sync", () => {
it("marks cases as completed when report already contains case results", () => {
const job = {
job_id: "job-sync-report",
status: "running",
created_at: "2026-04-11T00:00:00.000Z",
updated_at: "2026-04-11T00:00:00.000Z",
eval_target: "assistant_stage1",
run_id: "assistant-stage1-ut-sync-report",
case_set_file: "dummy.json",
analysis_date: "2017-07-10",
total_cases: 2,
completed_cases: 0,
cases: [
{
case_id: "AUTO-001",
turns_total: 1,
status: "queued",
messages: []
},
{
case_id: "AUTO-002",
turns_total: 1,
status: "queued",
messages: []
}
],
error: null,
report: {
results: [{ case_id: "AUTO-001" }, { case_id: "AUTO-002" }]
}
} as any;
__evalRouteAsyncTestUtils.syncJobWithSessions(job);
expect(job.completed_cases).toBe(2);
expect(job.status).toBe("completed");
expect(job.cases.map((item: any) => item.status)).toEqual(["completed", "completed"]);
});
it("keeps terminal case state when job is already completed and session file is missing", () => {
const job = {
job_id: "job-sync-sticky",
status: "completed",
created_at: "2026-04-11T00:00:00.000Z",
updated_at: "2026-04-11T00:00:00.000Z",
eval_target: "assistant_stage1",
run_id: "assistant-stage1-ut-sync-sticky",
case_set_file: "dummy.json",
analysis_date: null,
total_cases: 1,
completed_cases: 1,
cases: [
{
case_id: "AUTO-001",
turns_total: 1,
status: "completed",
messages: []
}
],
error: null,
report: null
} as any;
__evalRouteAsyncTestUtils.syncJobWithSessions(job);
expect(job.completed_cases).toBe(1);
expect(job.status).toBe("completed");
expect(job.cases[0].status).toBe("completed");
});
});