АДРЕСНЫЙ РЕЖИМ - fix(address): закрыт S019 predecompose-anchor drift; nightly 102/102+25/25 PASS; docs synced
This commit is contained in:
@@ -85,6 +85,16 @@ describe("address query shape classifier", () => {
|
||||
expect(result.mode).toBe("address_query");
|
||||
});
|
||||
|
||||
it("keeps noisy management phrase about years alive in address lane", () => {
|
||||
const result = detectAddressQuestionMode("за какие года база ваще живая?");
|
||||
expect(result.mode).toBe("address_query");
|
||||
});
|
||||
|
||||
it("keeps noisy month-peak phrase in address lane", () => {
|
||||
const result = detectAddressQuestionMode("а теперь месяц-пик по операциям");
|
||||
expect(result.mode).toBe("address_query");
|
||||
});
|
||||
|
||||
it("keeps management contract usage overview question in address lane", () => {
|
||||
const result = detectAddressQuestionMode("Сколько всего договоров заведено и сколько из них реально использовались?");
|
||||
expect(result.mode).toBe("address_query");
|
||||
@@ -1151,6 +1161,11 @@ describe("address intent resolver expansion (M2.3a)", () => {
|
||||
expect(result.intent).toBe("list_documents_by_counterparty");
|
||||
});
|
||||
|
||||
it("resolves transliterated docy slang as documents by counterparty intent", () => {
|
||||
const result = resolveAddressIntent("svk poka docy za 2020");
|
||||
expect(result.intent).toBe("list_documents_by_counterparty");
|
||||
});
|
||||
|
||||
it("resolves bank operations by supplier phrase", () => {
|
||||
const result = resolveAddressIntent("Покажи платежи по поставщику Альфа за июль 2020");
|
||||
expect(result.intent).toBe("bank_operations_by_counterparty");
|
||||
@@ -1273,6 +1288,16 @@ describe("address intent resolver expansion (M2.3a)", () => {
|
||||
expect(result.intent).toBe("period_coverage_profile");
|
||||
});
|
||||
|
||||
it("resolves period coverage profile for noisy active-year-by-docs phrase", () => {
|
||||
const result = resolveAddressIntent("какой год тут самый движовый по докам");
|
||||
expect(result.intent).toBe("period_coverage_profile");
|
||||
});
|
||||
|
||||
it("resolves period coverage profile for month-peak follow-up phrase", () => {
|
||||
const result = resolveAddressIntent("а теперь месяц-пик по операциям");
|
||||
expect(result.intent).toBe("period_coverage_profile");
|
||||
});
|
||||
|
||||
it("resolves document+section profile for document type usage question", () => {
|
||||
const result = resolveAddressIntent("Какие типы документов используются чаще всего в базе?");
|
||||
expect(result.intent).toBe("document_type_and_account_section_profile");
|
||||
@@ -1293,6 +1318,16 @@ describe("address intent resolver expansion (M2.3a)", () => {
|
||||
expect(result.intent).toBe("document_type_and_account_section_profile");
|
||||
});
|
||||
|
||||
it("resolves document+section profile for noisy docs usage phrase", () => {
|
||||
const result = resolveAddressIntent("каких доков у нас больше всего крутится?");
|
||||
expect(result.intent).toBe("document_type_and_account_section_profile");
|
||||
});
|
||||
|
||||
it("resolves document+section profile for summary by doc types and share phrase", () => {
|
||||
const result = resolveAddressIntent("Сформируй сводку по типам документов и их доле в общем объеме.");
|
||||
expect(result.intent).toBe("document_type_and_account_section_profile");
|
||||
});
|
||||
|
||||
it("resolves counterparty population intent for total unique counterparties question", () => {
|
||||
const result = resolveAddressIntent("Сколько всего уникальных контрагентов в базе?");
|
||||
expect(result.intent).toBe("counterparty_population_and_roles");
|
||||
@@ -1363,6 +1398,11 @@ describe("address intent resolver expansion (M2.3a)", () => {
|
||||
expect(result.intent).toBe("customer_revenue_and_payments");
|
||||
});
|
||||
|
||||
it("resolves customer revenue intent from small deals by budget slang wording", () => {
|
||||
const result = resolveAddressIntent("покажи топ-20 самых маленьких сделок по бюджету");
|
||||
expect(result.intent).toBe("customer_revenue_and_payments");
|
||||
});
|
||||
|
||||
it("resolves customer revenue intent from typo highest-check wording", () => {
|
||||
const result = resolveAddressIntent("с каких кликентов самый высокий чек");
|
||||
expect(result.intent).toBe("customer_revenue_and_payments");
|
||||
@@ -1382,6 +1422,11 @@ describe("address intent resolver expansion (M2.3a)", () => {
|
||||
const result = resolveAddressIntent("покажи договора все по жуковке 51");
|
||||
expect(result.intent).toBe("list_contracts_by_counterparty");
|
||||
});
|
||||
|
||||
it("prefers documents-by-contract intent for explicit document follow-up wording", () => {
|
||||
const result = resolveAddressIntent("покажи документы по этому же договору");
|
||||
expect(result.intent).toBe("list_documents_by_contract");
|
||||
});
|
||||
});
|
||||
|
||||
describe("address filter extraction for balance drilldown", () => {
|
||||
@@ -1630,6 +1675,14 @@ describe("address filter extraction for balance drilldown", () => {
|
||||
expect(result.warnings).toContain("period_derived_from_year_phrase");
|
||||
});
|
||||
|
||||
it("does not use filler token 'есть' as counterparty when explicit shorthand anchor exists", () => {
|
||||
const result = extractAddressFilters("какие у свк есть доки за 2020?", "list_documents_by_counterparty");
|
||||
expect(result.extracted_filters.counterparty).toBe("свк");
|
||||
expect(result.extracted_filters.counterparty).not.toBe("есть");
|
||||
expect(result.extracted_filters.period_from).toBe("2020-01-01");
|
||||
expect(result.extracted_filters.period_to).toBe("2020-12-31");
|
||||
});
|
||||
|
||||
it("extracts free-text counterparty and relaxed short-year period from noisy phrase", () => {
|
||||
const result = extractAddressFilters(
|
||||
"свк 20 год - покажи доки плс",
|
||||
@@ -1690,6 +1743,17 @@ describe("address filter extraction for balance drilldown", () => {
|
||||
expect(result.warnings).toContain("period_derived_from_year_phrase");
|
||||
});
|
||||
|
||||
it("does not treat transliterated filler verb as counterparty in docy phrase", () => {
|
||||
const result = extractAddressFilters(
|
||||
"svk poka docy za 2020",
|
||||
"list_documents_by_counterparty"
|
||||
);
|
||||
expect(result.extracted_filters.counterparty).toBe("svk");
|
||||
expect(result.extracted_filters.counterparty).not.toBe("poka");
|
||||
expect(result.extracted_filters.period_from).toBe("2020-01-01");
|
||||
expect(result.extracted_filters.period_to).toBe("2020-12-31");
|
||||
});
|
||||
|
||||
it("repairs mojibake phrase before extracting counterparty filters", () => {
|
||||
const result = extractAddressFilters(
|
||||
"Показать документы СВК за 2020 год.",
|
||||
@@ -1785,6 +1849,14 @@ describe("address filter extraction for balance drilldown", () => {
|
||||
expect(result.extracted_filters.period_to).toBe("2020-12-31");
|
||||
});
|
||||
|
||||
it("trims explanatory tail after contract token", () => {
|
||||
const result = extractAddressFilters(
|
||||
"документы по договору 19/15 выведите связанные документы",
|
||||
"list_documents_by_contract"
|
||||
);
|
||||
expect(result.extracted_filters.contract).toBe("19/15");
|
||||
});
|
||||
|
||||
it("extracts multiline year range period from phrase", () => {
|
||||
const result = extractAddressFilters(
|
||||
"Какие документы по СВК за 2000 - 2025\n год?",
|
||||
@@ -2158,6 +2230,46 @@ describe("address decompose stage follow-up carryover", () => {
|
||||
expect(result?.baseReasons).toContain("intent_adjusted_to_contract_followup_context");
|
||||
});
|
||||
|
||||
it("replaces noisy follow-up contract anchor with previous contract from context", () => {
|
||||
const result = runAddressDecomposeStage("а документы по этому же договору за тот же период", {
|
||||
previous_intent: "bank_operations_by_contract",
|
||||
previous_filters: {
|
||||
contract: "19/15",
|
||||
period_from: "2020-01-01",
|
||||
period_to: "2020-12-31"
|
||||
},
|
||||
previous_anchor_type: "contract",
|
||||
previous_anchor_value: "19/15"
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.intent.intent).toBe("list_documents_by_contract");
|
||||
expect(result?.filters.extracted_filters.contract).toBe("19/15");
|
||||
expect(
|
||||
result?.baseReasons?.includes("contract_replaced_from_followup_context") ||
|
||||
result?.baseReasons?.includes("contract_from_followup_context")
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("replaces noisy referential counterparty anchor with previous counterparty from context", () => {
|
||||
const result = runAddressDecomposeStage("а теперь документы по нему", {
|
||||
previous_intent: "bank_operations_by_counterparty",
|
||||
previous_filters: {
|
||||
counterparty: "свк",
|
||||
period_from: "2020-11-01",
|
||||
period_to: "2020-11-30"
|
||||
},
|
||||
previous_anchor_type: "counterparty",
|
||||
previous_anchor_value: "свк"
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.intent.intent).toBe("list_documents_by_counterparty");
|
||||
expect(result?.filters.extracted_filters.counterparty).toBe("свк");
|
||||
expect(
|
||||
result?.baseReasons?.includes("counterparty_replaced_from_followup_context") ||
|
||||
result?.baseReasons?.includes("counterparty_from_followup_context")
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("promotes open-items intent from follow-up wording with inherited contract anchor", () => {
|
||||
const result = runAddressDecomposeStage("а теперь открытые позиции по нему", {
|
||||
previous_intent: "bank_operations_by_contract",
|
||||
|
||||
@@ -159,6 +159,291 @@ describe("assistant address llm pre-decompose candidate preference", () => {
|
||||
expect(["unknown", "list_documents_by_counterparty"]).toContain(response.debug?.llm_predecompose_contract?.intent);
|
||||
});
|
||||
|
||||
it("keeps counterparty anchor for docy typo when llm fragment drops anchor", async () => {
|
||||
const calls: Array<{ message: string }> = [];
|
||||
const addressQueryService = {
|
||||
tryHandle: vi.fn(async (message: string) => {
|
||||
calls.push({ message });
|
||||
return buildAddressLaneResult(message);
|
||||
})
|
||||
} as any;
|
||||
|
||||
const normalizerService = {
|
||||
normalize: vi.fn(async () => ({
|
||||
trace_id: "norm-predecompose-docy",
|
||||
ok: true,
|
||||
normalized: {
|
||||
schema_version: "normalized_query_v2_0_2",
|
||||
user_message_raw: "svk poka docy za 2020",
|
||||
message_in_scope: true,
|
||||
scope_confidence: "medium",
|
||||
contains_multiple_tasks: false,
|
||||
fragments: [
|
||||
{
|
||||
fragment_id: "F1",
|
||||
raw_fragment_text: "svk poka docy za 2020",
|
||||
normalized_fragment_text: "Покажи документы за 2020 год",
|
||||
domain_relevance: "in_scope",
|
||||
business_scope: "company_specific_accounting",
|
||||
entity_hints: [],
|
||||
account_hints: [],
|
||||
document_hints: ["документы"],
|
||||
register_hints: [],
|
||||
time_scope: {
|
||||
type: "explicit",
|
||||
value: "2020",
|
||||
confidence: "high"
|
||||
},
|
||||
flags: {
|
||||
has_multi_entity_scope: false,
|
||||
asks_for_chain_explanation: false,
|
||||
asks_for_ranking_or_top: false,
|
||||
asks_for_period_summary: false,
|
||||
asks_for_rule_check: false,
|
||||
asks_for_anomaly_scan: false,
|
||||
asks_for_exact_object_trace: false,
|
||||
asks_for_evidence: false,
|
||||
mentions_period_close_context: false
|
||||
},
|
||||
candidate_labels: ["simple_factual"],
|
||||
confidence: "medium",
|
||||
execution_readiness: "executable",
|
||||
clarification_reason: null,
|
||||
soft_assumption_used: [],
|
||||
route_status: "routed",
|
||||
no_route_reason: null
|
||||
}
|
||||
],
|
||||
discarded_fragments: [],
|
||||
global_notes: {
|
||||
needs_clarification: false,
|
||||
clarification_reason: null
|
||||
}
|
||||
},
|
||||
raw_model_output: null,
|
||||
validation: { passed: true, errors: [] },
|
||||
usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 },
|
||||
latency_ms: 10,
|
||||
prompt_version: "normalizer_v2_0_2",
|
||||
schema_version: "v2_0_2",
|
||||
request_count_for_case: 1
|
||||
}))
|
||||
} as any;
|
||||
|
||||
const sessions = new AssistantSessionStore();
|
||||
const service = new AssistantService(
|
||||
normalizerService,
|
||||
sessions as any,
|
||||
{} as any,
|
||||
{ persistSession: vi.fn() } as any,
|
||||
addressQueryService
|
||||
);
|
||||
|
||||
const response = await service.handleMessage({
|
||||
session_id: `asst-predecompose-docy-${Date.now()}`,
|
||||
user_message: "svk poka docy za 2020",
|
||||
llmProvider: "local",
|
||||
useMock: false
|
||||
} as any);
|
||||
|
||||
expect(response.ok).toBe(true);
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0].message.toLowerCase()).toContain("svk");
|
||||
expect(calls[0].message).not.toBe("Покажи документы за 2020 год");
|
||||
expect(String(response.debug?.llm_decomposition_effective_message ?? "").toLowerCase()).toContain("svk");
|
||||
expect(response.debug?.llm_decomposition_reason).not.toBe("normalized_fragment_applied");
|
||||
});
|
||||
|
||||
it("does not treat service verb as counterparty anchor when llm rewrites noisy bank phrase", async () => {
|
||||
const calls: Array<{ message: string }> = [];
|
||||
const addressQueryService = {
|
||||
tryHandle: vi.fn(async (message: string) => {
|
||||
calls.push({ message });
|
||||
return buildAddressLaneResult(message);
|
||||
})
|
||||
} as any;
|
||||
|
||||
const normalizerService = {
|
||||
normalize: vi.fn(async () => ({
|
||||
trace_id: "norm-predecompose-skazhi-anchor",
|
||||
ok: true,
|
||||
normalized: {
|
||||
schema_version: "normalized_query_v2_0_2",
|
||||
user_message_raw: "свк списания/поступления за 2020",
|
||||
message_in_scope: true,
|
||||
scope_confidence: "medium",
|
||||
contains_multiple_tasks: false,
|
||||
fragments: [
|
||||
{
|
||||
fragment_id: "F1",
|
||||
raw_fragment_text: "свк списания/поступления за 2020",
|
||||
normalized_fragment_text: "скажи списания/поступления за 2020",
|
||||
domain_relevance: "in_scope",
|
||||
business_scope: "company_specific_accounting",
|
||||
entity_hints: [],
|
||||
account_hints: [],
|
||||
document_hints: ["списания", "поступления"],
|
||||
register_hints: [],
|
||||
time_scope: {
|
||||
type: "explicit",
|
||||
value: "2020",
|
||||
confidence: "high"
|
||||
},
|
||||
flags: {
|
||||
has_multi_entity_scope: false,
|
||||
asks_for_chain_explanation: false,
|
||||
asks_for_ranking_or_top: false,
|
||||
asks_for_period_summary: false,
|
||||
asks_for_rule_check: false,
|
||||
asks_for_anomaly_scan: false,
|
||||
asks_for_exact_object_trace: false,
|
||||
asks_for_evidence: false,
|
||||
mentions_period_close_context: false
|
||||
},
|
||||
candidate_labels: ["simple_factual"],
|
||||
confidence: "medium",
|
||||
execution_readiness: "executable",
|
||||
clarification_reason: null,
|
||||
soft_assumption_used: [],
|
||||
route_status: "routed",
|
||||
no_route_reason: null
|
||||
}
|
||||
],
|
||||
discarded_fragments: [],
|
||||
global_notes: {
|
||||
needs_clarification: false,
|
||||
clarification_reason: null
|
||||
}
|
||||
},
|
||||
raw_model_output: null,
|
||||
validation: { passed: true, errors: [] },
|
||||
usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 },
|
||||
latency_ms: 10,
|
||||
prompt_version: "normalizer_v2_0_2",
|
||||
schema_version: "v2_0_2",
|
||||
request_count_for_case: 1
|
||||
}))
|
||||
} as any;
|
||||
|
||||
const sessions = new AssistantSessionStore();
|
||||
const service = new AssistantService(
|
||||
normalizerService,
|
||||
sessions as any,
|
||||
{} as any,
|
||||
{ persistSession: vi.fn() } as any,
|
||||
addressQueryService
|
||||
);
|
||||
|
||||
const response = await service.handleMessage({
|
||||
session_id: `asst-predecompose-skazhi-anchor-${Date.now()}`,
|
||||
user_message: "свк списания/поступления за 2020",
|
||||
llmProvider: "local",
|
||||
useMock: false
|
||||
} as any);
|
||||
|
||||
expect(response.ok).toBe(true);
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0].message.toLowerCase()).toContain("свк");
|
||||
expect(calls[0].message.toLowerCase()).not.toContain("скажи");
|
||||
expect(response.debug?.llm_decomposition_reason).not.toBe("normalized_fragment_applied");
|
||||
});
|
||||
|
||||
it("rejects follow-up intent injection when llm adds documents to same-date account prompt", async () => {
|
||||
const calls: Array<{ message: string }> = [];
|
||||
const addressQueryService = {
|
||||
tryHandle: vi.fn(async (message: string) => {
|
||||
calls.push({ message });
|
||||
return buildAddressLaneResult(message);
|
||||
})
|
||||
} as any;
|
||||
|
||||
const normalizerService = {
|
||||
normalize: vi.fn(async () => ({
|
||||
trace_id: "norm-predecompose-followup-injection",
|
||||
ok: true,
|
||||
normalized: {
|
||||
schema_version: "normalized_query_v2_0_2",
|
||||
user_message_raw: "а на ту же дату по 62",
|
||||
message_in_scope: true,
|
||||
scope_confidence: "medium",
|
||||
contains_multiple_tasks: false,
|
||||
fragments: [
|
||||
{
|
||||
fragment_id: "F1",
|
||||
raw_fragment_text: "а на ту же дату по 62",
|
||||
normalized_fragment_text: "документы или проводки по счету 62 на ту же дату",
|
||||
domain_relevance: "in_scope",
|
||||
business_scope: "company_specific_accounting",
|
||||
entity_hints: [],
|
||||
account_hints: ["62"],
|
||||
document_hints: ["документы"],
|
||||
register_hints: [],
|
||||
time_scope: {
|
||||
type: "explicit",
|
||||
value: "same-date",
|
||||
confidence: "medium"
|
||||
},
|
||||
flags: {
|
||||
has_multi_entity_scope: false,
|
||||
asks_for_chain_explanation: false,
|
||||
asks_for_ranking_or_top: false,
|
||||
asks_for_period_summary: false,
|
||||
asks_for_rule_check: false,
|
||||
asks_for_anomaly_scan: false,
|
||||
asks_for_exact_object_trace: false,
|
||||
asks_for_evidence: false,
|
||||
mentions_period_close_context: false
|
||||
},
|
||||
candidate_labels: ["simple_factual"],
|
||||
confidence: "medium",
|
||||
execution_readiness: "executable",
|
||||
clarification_reason: null,
|
||||
soft_assumption_used: [],
|
||||
route_status: "routed",
|
||||
no_route_reason: null
|
||||
}
|
||||
],
|
||||
discarded_fragments: [],
|
||||
global_notes: {
|
||||
needs_clarification: false,
|
||||
clarification_reason: null
|
||||
}
|
||||
},
|
||||
raw_model_output: null,
|
||||
validation: { passed: true, errors: [] },
|
||||
usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 },
|
||||
latency_ms: 10,
|
||||
prompt_version: "normalizer_v2_0_2",
|
||||
schema_version: "v2_0_2",
|
||||
request_count_for_case: 1
|
||||
}))
|
||||
} as any;
|
||||
|
||||
const sessions = new AssistantSessionStore();
|
||||
const service = new AssistantService(
|
||||
normalizerService,
|
||||
sessions as any,
|
||||
{} as any,
|
||||
{ persistSession: vi.fn() } as any,
|
||||
addressQueryService
|
||||
);
|
||||
|
||||
const sourceMessage = "а на ту же дату по 62";
|
||||
const response = await service.handleMessage({
|
||||
session_id: `asst-predecompose-followup-injection-${Date.now()}`,
|
||||
user_message: sourceMessage,
|
||||
llmProvider: "local",
|
||||
useMock: false
|
||||
} as any);
|
||||
|
||||
expect(response.ok).toBe(true);
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0].message).toBe(sourceMessage);
|
||||
expect(response.debug?.llm_decomposition_applied).toBe(false);
|
||||
expect(response.debug?.llm_decomposition_reason).toBe("normalized_fragment_rejected_followup_intent_injection");
|
||||
expect(response.debug?.llm_decomposition_effective_message).toBe(sourceMessage);
|
||||
});
|
||||
|
||||
it("applies deterministic fallback rule when llm fragment is unusable", async () => {
|
||||
const calls: Array<{ message: string }> = [];
|
||||
const addressQueryService = {
|
||||
|
||||
Reference in New Issue
Block a user