АРЧ АП11 - Архитектура после регресса: Архитектура: сохранить свежую дату при inventory root restore и очистить data-scope ответы от грязных лейблов
This commit is contained in:
@@ -52,4 +52,20 @@ describe("address counterparty utf8 regression", () => {
|
||||
|
||||
expect(result.intent).toBe("list_documents_by_counterparty");
|
||||
});
|
||||
|
||||
it("classifies direct company activity-age wording with a colloquial organization anchor", () => {
|
||||
const result = resolveCounterpartyAddressIntent(
|
||||
"а по Альтернативе Плюс сколько лет активности в базе 1С?",
|
||||
utf8Deps
|
||||
);
|
||||
|
||||
expect(result?.intent).toBe("counterparty_activity_lifecycle");
|
||||
expect(result?.reasons).toContain("counterparty_activity_lifecycle_signal_detected");
|
||||
});
|
||||
|
||||
it("keeps the main resolver in the supported contour for direct company activity-age wording", () => {
|
||||
const result = resolveAddressIntent("а по Альтернативе Плюс сколько лет активности в базе 1С?");
|
||||
|
||||
expect(result.intent).toBe("counterparty_activity_lifecycle");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -205,4 +205,37 @@ describe("inventory root frame regressions", () => {
|
||||
expect(result?.filters.extracted_filters.period_to).toBe("2019-07-31");
|
||||
expect(result?.filters.extracted_filters.as_of_date).toBe("2019-07-31");
|
||||
});
|
||||
it("keeps the freshest previous date when inventory root restore follows a receivables step", () => {
|
||||
const result = runAddressDecomposeStage("остатки по складу на эту же дату", {
|
||||
previous_intent: "receivables_confirmed_as_of_date",
|
||||
target_intent: "inventory_on_hand_as_of_date",
|
||||
previous_filters: {
|
||||
organization: 'ООО "Альтернатива Плюс"',
|
||||
as_of_date: "2020-03-31",
|
||||
period_from: "2020-03-01",
|
||||
period_to: "2020-03-31"
|
||||
},
|
||||
previous_anchor_type: "organization",
|
||||
previous_anchor_value: 'ООО "Альтернатива Плюс"',
|
||||
root_intent: "inventory_on_hand_as_of_date",
|
||||
root_filters: {
|
||||
organization: 'ООО "Альтернатива Плюс"',
|
||||
as_of_date: "2021-03-31",
|
||||
period_from: "2021-03-01",
|
||||
period_to: "2021-03-31"
|
||||
},
|
||||
root_anchor_type: "organization",
|
||||
root_anchor_value: 'ООО "Альтернатива Плюс"',
|
||||
root_context_only: true,
|
||||
current_frame_kind: "inventory_root"
|
||||
});
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.intent.intent).toBe("inventory_on_hand_as_of_date");
|
||||
expect(result?.intent.reasons).toContain("intent_restored_to_inventory_root_frame");
|
||||
expect(result?.filters.extracted_filters.organization).toBe('ООО "Альтернатива Плюс"');
|
||||
expect(result?.filters.extracted_filters.as_of_date).toBe("2020-03-31");
|
||||
expect(result?.filters.extracted_filters.period_from).toBe("2020-03-01");
|
||||
expect(result?.filters.extracted_filters.period_to).toBe("2020-03-31");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3746,6 +3746,29 @@ describe("address query limited taxonomy and stage diagnostics", { timeout: 1500
|
||||
expect(result?.debug.mcp_call_status).not.toBe("skipped");
|
||||
});
|
||||
|
||||
it("keeps colloquial follow-up activity-age wording in the lifecycle aggregate recipe", async () => {
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle("а по Альтернативе Плюс сколько лет активности в базе 1С?", {
|
||||
activeOrganization: 'ООО "Альтернатива Плюс"'
|
||||
});
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.debug.detected_intent).toBe("counterparty_activity_lifecycle");
|
||||
expect(result?.debug.selected_recipe).toBe("address_counterparty_activity_lifecycle_v1");
|
||||
expect(result?.debug.mcp_call_status).not.toBe("skipped");
|
||||
});
|
||||
|
||||
it("keeps colloquial follow-up activity-age wording grounded to the selected organization", async () => {
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle("а по Альтернативе Плюс сколько лет активности в базе 1С?", {
|
||||
activeOrganization: 'ООО "Альтернатива Плюс"'
|
||||
});
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.debug.detected_intent).toBe("counterparty_activity_lifecycle");
|
||||
expect(result?.debug.selected_recipe).toBe("address_counterparty_activity_lifecycle_v1");
|
||||
expect(result?.debug.extracted_filters.counterparty).toBeUndefined();
|
||||
expect(result?.debug.mcp_call_status).not.toBe("materialized_but_not_anchor_matched");
|
||||
});
|
||||
|
||||
it("routes debt-longevity wording into receivables lane with factual reply", async () => {
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle(
|
||||
|
||||
@@ -8,7 +8,10 @@ function createPolicy() {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
}
|
||||
const text = String(value ?? "").trim();
|
||||
const text = String(value ?? "")
|
||||
.replace(/\\/g, "")
|
||||
.replace(/([А-Яа-яA-Za-z])"([А-Яа-яA-Za-z])/gu, "$1в$2")
|
||||
.trim();
|
||||
return text.length > 0 ? text : null;
|
||||
},
|
||||
toNonEmptyString: (value: unknown) => {
|
||||
@@ -38,6 +41,21 @@ describe("assistantBoundaryPolicy", () => {
|
||||
expect(reply.toLowerCase()).not.toContain("read-only");
|
||||
});
|
||||
|
||||
it("normalizes noisy organization labels in data-scope reply", () => {
|
||||
const policy = createPolicy();
|
||||
|
||||
const reply = policy.buildAssistantDataScopeContractReply({
|
||||
status: "resolved",
|
||||
channel: "default",
|
||||
organizations: ['ООО \\Альтернати"а Плюс\\', 'ООО \\Лайс"уд\\']
|
||||
});
|
||||
|
||||
expect(reply).toContain('ООО Альтернатива Плюс');
|
||||
expect(reply).toContain('ООО Лайсвуд');
|
||||
expect(reply).not.toContain('\\"');
|
||||
expect(reply).not.toContain("\\");
|
||||
});
|
||||
|
||||
it("strips unexpected CJK fragments from live chat reply", () => {
|
||||
const policy = createPolicy();
|
||||
|
||||
|
||||
@@ -409,8 +409,13 @@ describe("assistant living chat mode", () => {
|
||||
|
||||
expect(response.ok).toBe(true);
|
||||
expect(response.reply_type).toBe("factual_with_explanation");
|
||||
expect(String(response.assistant_reply).toLowerCase()).toContain("могу помочь");
|
||||
expect(String(response.assistant_reply).toLowerCase()).toContain("\u0440\u0435\u0436\u0438\u043c\u0435 \u0447\u0442\u0435\u043d\u0438\u044f");
|
||||
expect(String(response.assistant_reply).toLowerCase()).toContain("\u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u044e 1\u0441");
|
||||
expect(String(response.assistant_reply)).not.toContain("vat_period_snapshot");
|
||||
expect(String(response.assistant_reply)).not.toContain("inventory_on_hand_as_of_date");
|
||||
expect(String(response.assistant_reply)).not.toContain("suggest_safe_next_step");
|
||||
expect(String(response.assistant_reply)).not.toContain("explain_boundary");
|
||||
expect(response.debug?.tool_gate_reason).toBe("assistant_capability_query_detected");
|
||||
expect(chatClient.chat).toHaveBeenCalledTimes(0);
|
||||
expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(0);
|
||||
|
||||
@@ -2,6 +2,8 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
mergeKnownOrganizations,
|
||||
normalizeOrganizationScopeSearchText,
|
||||
normalizeOrganizationScopeValue,
|
||||
organizationsLikelySameEntity,
|
||||
resolveOrganizationSelectionFromMessage,
|
||||
scoreOrganizationMentionInMessage
|
||||
} from "../src/services/assistantOrganizationMatcher";
|
||||
@@ -17,8 +19,13 @@ describe("assistant organization matcher", () => {
|
||||
).toEqual(['ООО "Альтернатива Плюс"', "ООО Лайсвуд"]);
|
||||
});
|
||||
|
||||
it("repairs noisy display labels before exposing them to the user", () => {
|
||||
expect(normalizeOrganizationScopeValue('ООО \\Альтернати"а Плюс\\')).toBe("ООО Альтернатива Плюс");
|
||||
expect(normalizeOrganizationScopeValue('ООО \\Лайс"уд\\')).toBe("ООО Лайсвуд");
|
||||
});
|
||||
|
||||
it("matches incomplete or reordered organization mention against live candidates", () => {
|
||||
const resolved = resolveOrganizationSelectionFromMessage("дай что сегодня на складе в конторе ссыт кот", [
|
||||
const resolved = resolveOrganizationSelectionFromMessage("дай что сегодня на складе в конторе кот ссыт", [
|
||||
"ООО КОТ ССЫТ ВО ДВОРЕ",
|
||||
"ООО Альтернатива Плюс"
|
||||
]);
|
||||
@@ -34,4 +41,12 @@ describe("assistant organization matcher", () => {
|
||||
|
||||
expect(score).toBeGreaterThanOrEqual(90);
|
||||
});
|
||||
|
||||
it("treats minor live label corruption as the same organization entity", () => {
|
||||
expect(organizationsLikelySameEntity("Альтернатива Плюс", 'ООО "Альтернати"а Плюс"')).toBe(true);
|
||||
});
|
||||
|
||||
it("does not merge different organizations with only one shared token", () => {
|
||||
expect(organizationsLikelySameEntity('ООО "Альтернатива Плюс"', 'ООО "Альтернатива Минус"')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createAssistantTransitionPolicy } from "../src/services/assistantTransitionPolicy";
|
||||
import { buildRootScopedCarryoverFiltersForTests } from "../src/services/assistantService";
|
||||
|
||||
function toNonEmptyString(value: unknown): string | null {
|
||||
if (value === null || value === undefined) {
|
||||
@@ -76,8 +77,20 @@ function buildPolicy(overrides: Record<string, unknown> = {}) {
|
||||
isInventoryRootFrameIntent: (intent: unknown) => String(intent ?? "") === "inventory_on_hand_as_of_date",
|
||||
findRecentAddressFilterValue: () => null,
|
||||
hasForeignAccountingPivotOverInventoryMessage: () => false,
|
||||
buildRootScopedCarryoverFilters: (_previousFilters: Record<string, unknown>, inventoryRootFrame: Record<string, unknown>) => ({
|
||||
...(inventoryRootFrame?.filters ?? {})
|
||||
buildRootScopedCarryoverFilters: (
|
||||
previousFilters: Record<string, unknown>,
|
||||
inventoryRootFrame: Record<string, unknown>
|
||||
) => ({
|
||||
organization:
|
||||
toNonEmptyString(inventoryRootFrame?.filters?.organization) ?? toNonEmptyString(previousFilters?.organization),
|
||||
warehouse:
|
||||
toNonEmptyString(inventoryRootFrame?.filters?.warehouse) ?? toNonEmptyString(previousFilters?.warehouse),
|
||||
as_of_date:
|
||||
toNonEmptyString(previousFilters?.as_of_date) ?? toNonEmptyString(inventoryRootFrame?.filters?.as_of_date),
|
||||
period_from:
|
||||
toNonEmptyString(previousFilters?.period_from) ?? toNonEmptyString(inventoryRootFrame?.filters?.period_from),
|
||||
period_to:
|
||||
toNonEmptyString(previousFilters?.period_to) ?? toNonEmptyString(inventoryRootFrame?.filters?.period_to)
|
||||
}),
|
||||
inferDisplayedEntityTypeFromIntent: () => "item",
|
||||
extractDisplayedAddressEntityCandidates: () => [],
|
||||
@@ -109,7 +122,7 @@ describe("assistantTransitionPolicy", () => {
|
||||
expect(carryover?.followupContext?.root_context_only).toBe(true);
|
||||
expect(carryover?.followupContext?.target_intent).toBe("inventory_on_hand_as_of_date");
|
||||
expect(carryover?.followupContext?.root_intent).toBe("inventory_on_hand_as_of_date");
|
||||
expect(carryover?.followupContext?.previous_filters).toEqual({
|
||||
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||||
as_of_date: "2020-03-31",
|
||||
organization: 'ООО "Альтернатива Плюс"'
|
||||
});
|
||||
@@ -131,7 +144,7 @@ describe("assistantTransitionPolicy", () => {
|
||||
expect(carryover?.followupSelectionMode).toBe("carry_root_context");
|
||||
expect(carryover?.followupContext?.root_context_only).toBe(true);
|
||||
expect(carryover?.followupContext?.previous_intent).toBeUndefined();
|
||||
expect(carryover?.followupContext?.previous_filters).toEqual({
|
||||
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||||
as_of_date: "2020-03-31",
|
||||
organization: 'ООО "Альтернатива Плюс"'
|
||||
});
|
||||
@@ -169,6 +182,7 @@ describe("assistantTransitionPolicy", () => {
|
||||
expect(contract.anchor_type).toBe("item");
|
||||
expect(contract.anchor_value).toBe("Рабочая станция");
|
||||
});
|
||||
|
||||
it("prefers carryover target intent over llm contract drift in continuation contract", () => {
|
||||
const policy = buildPolicy();
|
||||
|
||||
@@ -257,6 +271,7 @@ describe("assistantTransitionPolicy", () => {
|
||||
expect(carryover?.followupContext?.previous_intent).toBe("list_documents_by_counterparty");
|
||||
expect(carryover?.followupSelectionMode).toBe("carry_previous_intent");
|
||||
});
|
||||
|
||||
it("keeps root-scoped carryover for foreign accounting pivot over inventory drilldown", () => {
|
||||
const policy = buildPolicy({
|
||||
findLastAddressAssistantItem: () => ({
|
||||
@@ -308,4 +323,32 @@ describe("assistantTransitionPolicy", () => {
|
||||
period_to: "2021-03-31"
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers the freshest previous date scope over a stale inventory root frame during same-date pivot", () => {
|
||||
const filters = buildRootScopedCarryoverFiltersForTests(
|
||||
{
|
||||
organization: 'ООО "Альтернатива Плюс"',
|
||||
as_of_date: "2020-03-31",
|
||||
period_from: "2020-03-01",
|
||||
period_to: "2020-03-31"
|
||||
},
|
||||
{
|
||||
filters: {
|
||||
organization: 'ООО "Альтернатива Плюс"',
|
||||
warehouse: "Основной склад",
|
||||
as_of_date: "2021-03-31",
|
||||
period_from: "2021-03-01",
|
||||
period_to: "2021-03-31"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
expect(filters).toEqual({
|
||||
organization: 'ООО "Альтернатива Плюс"',
|
||||
warehouse: "Основной склад",
|
||||
as_of_date: "2020-03-31",
|
||||
period_from: "2020-03-01",
|
||||
period_to: "2020-03-31"
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user