ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов Stage 3.6 Усилина оркестрация и детект data-scope для живого сленга. Убрана шаблонность soft-refusal в limited-ответах. Подрезана словарнаю жирность в фильтр-экстракторе
This commit is contained in:
@@ -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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user