Inventory breadth: закрепить stock provenance и sale-trace контуры
This commit is contained in:
@@ -24,6 +24,31 @@ describe("address coverage evidence policy", () => {
|
||||
expect(contract.as_of_date_basis).toBe("explicit_as_of_date");
|
||||
});
|
||||
|
||||
it("keeps relative current snapshot basis even when it materializes to today's ISO date", () => {
|
||||
const todayIso = new Date().toISOString().slice(0, 10);
|
||||
const contract = resolveAddressCoverageEvidence({
|
||||
intent: "inventory_on_hand_as_of_date",
|
||||
selectedRecipe: "address_inventory_on_hand_as_of_date_v1",
|
||||
filters: {
|
||||
as_of_date: todayIso
|
||||
},
|
||||
semanticFrame: {
|
||||
scope_kind: "implicit_self_scope",
|
||||
anchor_kind: "self_scope",
|
||||
anchor_value: null,
|
||||
date_scope_kind: "implicit_current",
|
||||
date_basis_hint: "implicit_current_snapshot",
|
||||
self_scope_detected: true,
|
||||
selected_object_scope_detected: false
|
||||
},
|
||||
responseType: "FACTUAL_LIST",
|
||||
rowsMatched: 1
|
||||
});
|
||||
|
||||
expect(contract.as_of_date_basis).toBe("implicit_current_snapshot");
|
||||
expect(contract.reason_codes).toContain("as_of_date_basis_implicit_current_snapshot");
|
||||
});
|
||||
|
||||
it("treats factual exact negatives as full confirmed-balance evidence instead of partial noise", () => {
|
||||
const contract = resolveAddressCoverageEvidence({
|
||||
intent: "payables_confirmed_as_of_date",
|
||||
|
||||
@@ -32,6 +32,13 @@ describe("addressInventoryIntentSignals", () => {
|
||||
expect(result.reasons).toContain("inventory_purchase_date_signal_detected");
|
||||
});
|
||||
|
||||
it("classifies direct Russian purchase-date wording with an explicit item", () => {
|
||||
const result = resolveAddressIntent("Когда был куплен товар Столешница 600*3050*26 дуб ниагара");
|
||||
|
||||
expect(result.intent).toBe("inventory_purchase_provenance_for_item");
|
||||
expect(result.reasons).toContain("inventory_purchase_date_signal_detected");
|
||||
});
|
||||
|
||||
it("does not steal non-inventory open-items wording into the inventory owner", () => {
|
||||
const result = resolveInventoryAddressIntent("хвосты покажи по счету 60 на август 2022");
|
||||
|
||||
|
||||
@@ -114,6 +114,52 @@ describe("inventory purchase-date selected-object follow-up", () => {
|
||||
expect(String(result?.reply_text ?? "")).not.toContain("Блок 1");
|
||||
});
|
||||
|
||||
it("routes direct canonical purchase-date wording with an explicit item through the exact provenance lane", async () => {
|
||||
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||
fetched_rows: 1,
|
||||
matched_rows: 1,
|
||||
raw_rows: [
|
||||
{
|
||||
Period: "2019-02-12T00:00:00Z",
|
||||
Registrator: "Поступление товаров и услуг 00000000003 от 12.02.2019 0:00:00",
|
||||
AccountDt: "41.01",
|
||||
AccountKt: "60.01",
|
||||
Amount: 3724.17,
|
||||
SubcontoDt1: "Столешница 600*3050*26 дуб ниагара",
|
||||
SubcontoDt3: "Основной склад",
|
||||
SubcontoKt1: "Торговый дом \\Союз МСК\\",
|
||||
SubcontoKt2: "Договор поставки № 12 от 01.02.2019",
|
||||
Organization: "ООО \\Альтернатива Плюс\\"
|
||||
}
|
||||
],
|
||||
rows: [],
|
||||
error: null
|
||||
});
|
||||
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle("Когда был куплен товар Столешница 600*3050*26 дуб ниагара", {
|
||||
followupContext: {
|
||||
previous_intent: "inventory_purchase_provenance_for_item",
|
||||
previous_filters: {
|
||||
item: "Столешница 600*3050*26 дуб ниагара",
|
||||
warehouse: "Основной склад",
|
||||
as_of_date: "2019-03-31"
|
||||
},
|
||||
previous_anchor_type: "unknown",
|
||||
previous_anchor_value: null
|
||||
}
|
||||
});
|
||||
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
|
||||
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
||||
expect(result?.debug.selected_recipe).toBe("address_inventory_purchase_provenance_for_item_v1");
|
||||
expect(result?.debug.extracted_filters?.item).toBe("Столешница 600*3050*26 дуб ниагара");
|
||||
expect(result?.debug.extracted_filters?.as_of_date).toBe("2019-03-31");
|
||||
expect(String(result?.reply_text ?? "").split("\n")[0]).toContain("12.02.2019");
|
||||
expect(String(result?.reply_text ?? "")).not.toContain("Блок 1");
|
||||
});
|
||||
|
||||
it("routes 'когда примерно мы купили' follow-up to compact purchase-date answer with the carried item", async () => {
|
||||
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||
fetched_rows: 1,
|
||||
|
||||
@@ -87,14 +87,15 @@ describe("inventory sale trace movement route", () => {
|
||||
|
||||
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
||||
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
|
||||
expect(query).toContain("РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто КАК Движения");
|
||||
expect(query).toContain('ПОДСТРОКА(ЕСТЬNULL(Движения.СчетКт.Код, ""), 1, 5) = "41.01"');
|
||||
expect(query).toContain("Движения.СубконтоКт1 В (ВЫБРАТЬ Номенклатура.Ссылка");
|
||||
expect(query).toContain("Документ.РеализацияТоваровУслуг.Товары КАК Товары");
|
||||
expect(query).toContain('"41.01" КАК СчетКт');
|
||||
expect(query).toContain("Товары.Номенклатура В (ВЫБРАТЬ Номенклатура.Ссылка");
|
||||
expect(query).toContain(
|
||||
'Номенклатура.Наименование = "Рабочая станция универсального специалиста (индивидуальное изготовление)"'
|
||||
);
|
||||
expect(query).toContain("ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Контрагент) КАК Контрагент");
|
||||
expect(query).not.toContain("2016-06-30");
|
||||
expect(query).not.toContain("2016-06-01");
|
||||
expect(query).not.toContain('ПРЕДСТАВЛЕНИЕ(Движения.Организация) = "ООО \\Альтернатива Плюс\\"');
|
||||
expect(query).not.toContain('ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Организация) = "ООО \\Альтернатива Плюс\\"');
|
||||
});
|
||||
});
|
||||
|
||||
+6
-2
@@ -71,7 +71,9 @@ describe("inventory sale trace selected-object regressions", () => {
|
||||
|
||||
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
||||
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
|
||||
expect(query).toContain("РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто КАК Движения");
|
||||
expect(query).toContain("Документ.РеализацияТоваровУслуг.Товары КАК Товары");
|
||||
expect(query).toContain("ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Контрагент) КАК Контрагент");
|
||||
expect(query).toContain("Товары.Номенклатура В (ВЫБРАТЬ Номенклатура.Ссылка");
|
||||
expect(query).toContain('Номенклатура.Наименование = "Кромка с клеем 33 дуб ниагара 137 м"');
|
||||
expect(query).not.toContain('Номенклатура.Наименование = "Кромка"');
|
||||
});
|
||||
@@ -133,7 +135,9 @@ describe("inventory sale trace selected-object regressions", () => {
|
||||
|
||||
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
||||
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
|
||||
expect(query).toContain("РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто КАК Движения");
|
||||
expect(query).toContain("Документ.РеализацияТоваровУслуг.Товары КАК Товары");
|
||||
expect(query).toContain("ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Контрагент) КАК Контрагент");
|
||||
expect(query).toContain("Товары.Номенклатура В (ВЫБРАТЬ Номенклатура.Ссылка");
|
||||
expect(query).toContain('Номенклатура.Наименование = "Кромка с клеем 33 дуб ниагара 137 м"');
|
||||
expect(query).not.toContain('Номенклатура.Наименование = "Кромка"');
|
||||
});
|
||||
|
||||
@@ -197,7 +197,8 @@ describe("inventory selected-object follow-up", () => {
|
||||
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
|
||||
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
||||
expect(result?.debug.extracted_filters?.item).toBe("Столешница 600*3050*26 дуб ниагара");
|
||||
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
||||
expect(result?.debug.extracted_filters?.as_of_date).toBe("2019-03-31");
|
||||
expect(result?.debug.reasons).toContain("as_of_date_from_followup_context");
|
||||
expect(String(result?.reply_text ?? "")).toContain("Торговый дом \\Союз МСК\\");
|
||||
});
|
||||
|
||||
@@ -246,7 +247,8 @@ describe("inventory selected-object follow-up", () => {
|
||||
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
|
||||
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
||||
expect(result?.debug.extracted_filters?.item).toBe("Столешница 600*3050*26 альмандин");
|
||||
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
||||
expect(result?.debug.extracted_filters?.as_of_date).toBe("2021-03-31");
|
||||
expect(result?.debug.reasons).toContain("as_of_date_from_followup_context");
|
||||
expect(result?.debug.capability_id).toBe("inventory_inventory_purchase_provenance_for_item");
|
||||
expect(String(result?.reply_text ?? "")).toContain("Торговый дом \\Союз");
|
||||
});
|
||||
@@ -296,7 +298,8 @@ describe("inventory selected-object follow-up", () => {
|
||||
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
|
||||
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
||||
expect(result?.debug.extracted_filters?.item).toBe("Рабочая станция универсального специалиста (индивидуальное изготовление)");
|
||||
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
||||
expect(result?.debug.extracted_filters?.as_of_date).toBe("2016-07-31");
|
||||
expect(result?.debug.reasons).toContain("as_of_date_from_followup_context");
|
||||
expect(String(result?.reply_text ?? "")).toContain("ООО \\Производство мебели\\");
|
||||
});
|
||||
|
||||
@@ -346,9 +349,10 @@ describe("inventory selected-object follow-up", () => {
|
||||
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
||||
expect(result?.debug.selected_recipe).toBe("address_inventory_purchase_provenance_for_item_v1");
|
||||
expect(result?.debug.extracted_filters?.item).toBe("Конструкция трансформер рабочей станции 1300*900*2000");
|
||||
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
||||
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-06-30");
|
||||
expect(result?.debug.extracted_filters?.period_from).toBeUndefined();
|
||||
expect(result?.debug.extracted_filters?.period_to).toBeUndefined();
|
||||
expect(result?.debug.reasons).toContain("as_of_date_from_followup_context");
|
||||
expect(result?.debug.capability_id).toBe("inventory_inventory_purchase_provenance_for_item");
|
||||
expect(result?.debug.capability_route_mode).toBe("exact");
|
||||
expect(String(result?.reply_text ?? "")).toContain("ООО \\Гамма-мебель\\");
|
||||
@@ -399,7 +403,8 @@ describe("inventory selected-object follow-up", () => {
|
||||
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
|
||||
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
||||
expect(result?.debug.extracted_filters?.item).toBe("Рабочая станция универсального специалиста (индивидуальное изготовление)");
|
||||
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
||||
expect(result?.debug.extracted_filters?.as_of_date).toBe("2016-05-31");
|
||||
expect(result?.debug.reasons).toContain("as_of_date_from_followup_context");
|
||||
expect(String(result?.reply_text ?? "")).toContain("ООО \\Производство мебели\\");
|
||||
});
|
||||
|
||||
@@ -448,7 +453,8 @@ describe("inventory selected-object follow-up", () => {
|
||||
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
|
||||
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
||||
expect(result?.debug.extracted_filters?.item).toBe("Рабочая станция универсального специалиста (индивидуальное изготовление)");
|
||||
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
||||
expect(result?.debug.extracted_filters?.as_of_date).toBe("2016-05-31");
|
||||
expect(result?.debug.reasons).toContain("as_of_date_from_followup_context");
|
||||
expect(String(result?.reply_text ?? "")).toContain("ООО \\Производство мебели\\");
|
||||
});
|
||||
|
||||
@@ -498,7 +504,8 @@ describe("inventory selected-object follow-up", () => {
|
||||
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
||||
expect(result?.debug.selected_recipe).toBe("address_inventory_purchase_provenance_for_item_v1");
|
||||
expect(result?.debug.extracted_filters?.item).toBe("Рабочая станция универсального специалиста (индивидуальное изготовление)");
|
||||
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
||||
expect(result?.debug.extracted_filters?.as_of_date).toBe("2016-05-31");
|
||||
expect(result?.debug.reasons).toContain("as_of_date_from_followup_context");
|
||||
expect(String(result?.reply_text ?? "")).toContain("ООО \\Производство мебели\\");
|
||||
});
|
||||
|
||||
@@ -545,7 +552,8 @@ describe("inventory selected-object follow-up", () => {
|
||||
expect(result?.debug.detected_intent).toBe("inventory_purchase_documents_for_item");
|
||||
expect(result?.debug.selected_recipe).toBe("address_inventory_purchase_documents_for_item_v1");
|
||||
expect(result?.debug.extracted_filters?.item).toBe("Столешница 600*3050*26 дуб ниагара");
|
||||
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
||||
expect(result?.debug.extracted_filters?.as_of_date).toBe("2019-03-31");
|
||||
expect(result?.debug.reasons).toContain("as_of_date_from_followup_context");
|
||||
expect(String(result?.reply_text ?? "")).toContain("Поступление товаров и услуг 00000000077");
|
||||
});
|
||||
|
||||
@@ -646,7 +654,7 @@ describe("inventory selected-object follow-up", () => {
|
||||
expect(result?.debug.detected_intent).toBe("inventory_sale_trace_for_item");
|
||||
expect(result?.debug.selected_recipe).toBe("address_inventory_sale_trace_for_item_v1");
|
||||
expect(result?.debug.extracted_filters?.item).toBe("Четки Пост (84*117)");
|
||||
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
||||
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-03-31");
|
||||
expect(String(result?.reply_text ?? "").split("\n")[0]).toContain("ИП Покупатель");
|
||||
expect(String(result?.reply_text ?? "")).toContain("Документы выбытия");
|
||||
});
|
||||
@@ -692,7 +700,7 @@ describe("inventory selected-object follow-up", () => {
|
||||
expect(result?.debug.detected_intent).toBe("inventory_sale_trace_for_item");
|
||||
expect(result?.debug.selected_recipe).toBe("address_inventory_sale_trace_for_item_v1");
|
||||
expect(result?.debug.extracted_filters?.item).toBe("Пуф арий");
|
||||
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
||||
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-05-31");
|
||||
expect(result?.debug.reasons).toContain("inventory_selected_object_sale_trace_signal_detected");
|
||||
expect(String(result?.reply_text ?? "").split("\n")[0]).toContain("ООО \\Ромашка\\");
|
||||
expect(String(result?.reply_text ?? "")).toContain("Документы выбытия");
|
||||
@@ -740,7 +748,7 @@ describe("inventory selected-object follow-up", () => {
|
||||
expect(result?.debug.detected_intent).toBe("inventory_sale_trace_for_item");
|
||||
expect(result?.debug.selected_recipe).toBe("address_inventory_sale_trace_for_item_v1");
|
||||
expect(result?.debug.extracted_filters?.item).toBe("Кромка с клеем 33 альмандин 137 м");
|
||||
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
||||
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-03-31");
|
||||
expect(String(result?.reply_text ?? "")).toContain("ООО \\Покупатель\\");
|
||||
});
|
||||
|
||||
@@ -787,7 +795,7 @@ describe("inventory selected-object follow-up", () => {
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.response_type).toBe("FACTUAL_LIST");
|
||||
expect(result?.debug.detected_intent).toBe("inventory_sale_trace_for_item");
|
||||
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
||||
expect(result?.debug.extracted_filters?.as_of_date).toBe("2019-03-31");
|
||||
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
||||
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
|
||||
expect(query).not.toContain("2019-03-31");
|
||||
@@ -826,6 +834,164 @@ describe("inventory selected-object follow-up", () => {
|
||||
expect(String(result?.reply_text ?? "")).not.toContain("совпадений не нашлось");
|
||||
});
|
||||
|
||||
it("keeps semantic stock period for unresolved supplier-link follow-up while querying purchase history up to as-of date", async () => {
|
||||
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||
fetched_rows: 1,
|
||||
matched_rows: 1,
|
||||
raw_rows: [
|
||||
{
|
||||
Period: "2020-06-04T00:00:00Z",
|
||||
Registrator: "Поступление товаров и услуг 00000000012 от 04.06.2020 13:36:29",
|
||||
AccountDt: "41.01",
|
||||
AccountKt: "60.01",
|
||||
Amount: 439000,
|
||||
SubcontoDt1: "Кресло для посетителей Экокожа/хром Цвет - оранжевый",
|
||||
SubcontoDt3: "Основной склад",
|
||||
Organization: "ООО \\Альтернатива Плюс\\"
|
||||
}
|
||||
],
|
||||
rows: [],
|
||||
error: null
|
||||
});
|
||||
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle("Какие товары сейчас висят в остатке без понятной привязки к поставщику", {
|
||||
analysisDateHint: "2021-09-30",
|
||||
followupContext: {
|
||||
previous_intent: "inventory_aging_by_purchase_date",
|
||||
previous_filters: {
|
||||
as_of_date: "2021-09-30",
|
||||
organization: "ООО \\Альтернатива Плюс\\"
|
||||
},
|
||||
root_intent: "inventory_on_hand_as_of_date",
|
||||
root_filters: {
|
||||
period_from: "2021-09-01",
|
||||
period_to: "2021-09-30",
|
||||
as_of_date: "2021-09-30",
|
||||
organization: "ООО \\Альтернатива Плюс\\"
|
||||
},
|
||||
previous_anchor_type: "unknown",
|
||||
previous_anchor_value: null
|
||||
}
|
||||
});
|
||||
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.debug.detected_intent).toBe("inventory_supplier_stock_overlap_as_of_date");
|
||||
expect(result?.debug.extracted_filters?.period_from).toBe("2021-09-01");
|
||||
expect(result?.debug.extracted_filters?.period_to).toBe("2021-09-30");
|
||||
expect(result?.debug.extracted_filters?.as_of_date).toBe("2021-09-30");
|
||||
expect(result?.debug.reasons).toContain("period_window_semantic_from_inventory_snapshot_context");
|
||||
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
|
||||
expect(query).not.toContain("ДАТАВРЕМЯ(2021, 9, 1");
|
||||
expect(query).toContain("ДАТАВРЕМЯ(2021, 9, 30, 23, 59, 59)");
|
||||
});
|
||||
|
||||
it("derives semantic stock month for unresolved supplier-link when dialog continuation starts a new topic", async () => {
|
||||
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||
fetched_rows: 1,
|
||||
matched_rows: 1,
|
||||
raw_rows: [
|
||||
{
|
||||
Period: "2020-06-04T00:00:00Z",
|
||||
Registrator: "Purchase document 00000000012 from 2020-06-04",
|
||||
AccountDt: "41.01",
|
||||
AccountKt: "60.01",
|
||||
Amount: 439000,
|
||||
SubcontoDt1: "Office chair",
|
||||
SubcontoDt3: "Main warehouse",
|
||||
Organization: "OOO Test Org"
|
||||
}
|
||||
],
|
||||
rows: [],
|
||||
error: null
|
||||
});
|
||||
|
||||
const service = new AddressQueryService();
|
||||
const unresolvedSupplierLinkQuestion =
|
||||
"\u041a\u0430\u043a\u0438\u0435 \u0442\u043e\u0432\u0430\u0440\u044b \u0441\u0435\u0439\u0447\u0430\u0441 \u0432\u0438\u0441\u044f\u0442 \u0432 \u043e\u0441\u0442\u0430\u0442\u043a\u0435 \u0431\u0435\u0437 \u043f\u043e\u043d\u044f\u0442\u043d\u043e\u0439 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438 \u043a \u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\u0443";
|
||||
const result = await service.tryHandle(unresolvedSupplierLinkQuestion, {
|
||||
analysisDateHint: "2021-09-30",
|
||||
activeOrganization: "OOO Test Org"
|
||||
});
|
||||
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.debug.detected_intent).toBe("inventory_supplier_stock_overlap_as_of_date");
|
||||
expect(result?.debug.extracted_filters?.period_from).toBe("2021-09-01");
|
||||
expect(result?.debug.extracted_filters?.period_to).toBe("2021-09-30");
|
||||
expect(result?.debug.extracted_filters?.as_of_date).toBe("2021-09-30");
|
||||
expect(result?.debug.reasons).toContain("period_window_semantic_from_inventory_as_of_month");
|
||||
expect(result?.debug.reasons).not.toContain("period_window_semantic_from_inventory_snapshot_context");
|
||||
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
|
||||
expect(query).not.toContain("(2021, 9, 1");
|
||||
expect(query).toContain("(2021, 9, 30, 23, 59, 59)");
|
||||
});
|
||||
|
||||
it("treats supplier in purchase-to-sale chain as verification party, not stale organization scope", async () => {
|
||||
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||
fetched_rows: 2,
|
||||
matched_rows: 2,
|
||||
raw_rows: [
|
||||
{
|
||||
Период: "2019-12-10T00:00:00Z",
|
||||
Регистратор: "Поступление товаров и услуг 00000000111 от 10.12.2019 12:00:01",
|
||||
СчетДт: "41.01",
|
||||
СчетКт: "",
|
||||
Сумма: 855000,
|
||||
Номенклатура: "Шкаф картотечный 1000*400*2100",
|
||||
Контрагент: "ЭталонМебель",
|
||||
Организация: "ООО \\Альтернатива Плюс\\",
|
||||
Количество: 15
|
||||
},
|
||||
{
|
||||
Период: "2020-04-01T00:00:00Z",
|
||||
Регистратор: "Реализация товаров и услуг 00000000001 от 01.04.2020 0:00:00",
|
||||
СчетДт: "",
|
||||
СчетКт: "41.01",
|
||||
Сумма: 855000,
|
||||
Номенклатура: "Шкаф картотечный 1000*400*2100",
|
||||
Контрагент: "ЭталонМебель",
|
||||
Организация: "ООО \\Альтернатива Плюс\\",
|
||||
Количество: 15
|
||||
}
|
||||
],
|
||||
rows: [],
|
||||
error: null
|
||||
});
|
||||
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle(
|
||||
"Есть ли документально подтвержденная цепочка: поставщик Гамма-мебель, ООО -> товар Шкаф картотечный 1000*400*2100 -> покупатель Департамент капитального ремонта города Москвы",
|
||||
{
|
||||
activeOrganization: "ООО \\Альтернатива Плюс\\",
|
||||
llmSemanticHints: {
|
||||
scope_target_kind: "organization",
|
||||
scope_target_text: "Гамма-мебель, ООО",
|
||||
date_scope_kind: "explicit",
|
||||
self_scope_detected: false,
|
||||
selected_object_scope_detected: false
|
||||
},
|
||||
followupContext: {
|
||||
previous_intent: "inventory_on_hand_as_of_date",
|
||||
previous_filters: {
|
||||
as_of_date: "2020-03-31",
|
||||
organization: "ООО \\Альтернатива Плюс\\"
|
||||
},
|
||||
previous_anchor_type: "organization",
|
||||
previous_anchor_value: "ООО \\Альтернатива Плюс\\"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
|
||||
expect(result?.debug.detected_intent).toBe("inventory_purchase_to_sale_chain");
|
||||
expect(result?.debug.extracted_filters?.organization).toBe("ООО Альтернатива Плюс");
|
||||
expect(result?.debug.reasons).toContain("organization_restored_from_inventory_chain_context");
|
||||
expect(result?.debug.reasons).toContain("inventory_chain_counterparty_anchor_kept_for_verification");
|
||||
expect(String(result?.reply_text ?? "").split("\n")[0]).toContain("полностью не подтверждена");
|
||||
expect(String(result?.reply_text ?? "")).toContain("ЭталонМебель");
|
||||
});
|
||||
|
||||
it.skip("keeps the full selected item when sale trace is asked in canonical wording after provenance", async () => {
|
||||
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||
fetched_rows: 1,
|
||||
@@ -914,9 +1080,10 @@ describe("inventory selected-object follow-up", () => {
|
||||
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
|
||||
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
||||
expect(result?.debug.extracted_filters?.item).toBe("Кресло орион");
|
||||
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
||||
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-03-31");
|
||||
expect(result?.debug.extracted_filters?.period_from).toBeUndefined();
|
||||
expect(result?.debug.extracted_filters?.period_to).toBeUndefined();
|
||||
expect(result?.debug.reasons).toContain("as_of_date_from_followup_context");
|
||||
expect(result?.debug.reasons ?? []).not.toContain("lifecycle_execution_detached_from_snapshot_date");
|
||||
expect(result?.debug.reasons ?? []).not.toContain("as_of_date_cleared_for_history_recovery");
|
||||
expect(result?.debug.limitations ?? []).not.toContain("lifecycle_execution_detached_from_snapshot_date");
|
||||
|
||||
@@ -93,4 +93,26 @@ describe("inventory warehouse anchor extraction", () => {
|
||||
|
||||
expect(filters.warehouse).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not materialize current-moment canonical tail as warehouse anchor", () => {
|
||||
const filters = extractAddressFilters(
|
||||
"Какие товары находятся на складе в текущий момент",
|
||||
"inventory_on_hand_as_of_date"
|
||||
).extracted_filters;
|
||||
|
||||
expect(filters.warehouse).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not split organization-generic stock wording into stale warehouse and counterparty anchors", () => {
|
||||
const filters = extractAddressFilters(
|
||||
"Какие товары находились на складе компании в марте 2019 года?",
|
||||
"inventory_on_hand_as_of_date"
|
||||
).extracted_filters;
|
||||
|
||||
expect(filters.period_from).toBe("2019-03-01");
|
||||
expect(filters.period_to).toBe("2019-03-31");
|
||||
expect(filters.as_of_date).toBe("2019-03-31");
|
||||
expect(filters.warehouse).toBeUndefined();
|
||||
expect(filters.counterparty).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -157,6 +157,17 @@ describe("address query shape classifier", () => {
|
||||
expect(filters.warehouse).toBeUndefined();
|
||||
});
|
||||
|
||||
it("extracts supplier anchor from supplier-item-buyer chain without swallowing the arrow tail", () => {
|
||||
const extraction = extractAddressFilters(
|
||||
"Есть ли документально подтвержденная цепочка: поставщик Гамма-мебель, ООО -> товар Шкаф картотечный 1000*400*2100 -> покупатель Департамент капитального ремонта города Москвы",
|
||||
"inventory_purchase_to_sale_chain"
|
||||
);
|
||||
|
||||
expect(extraction.extracted_filters.counterparty).toBe("Гамма-мебель, ООО");
|
||||
expect(extraction.extracted_filters.item).toBe("Шкаф картотечный 1000*400*2100");
|
||||
expect(extraction.warnings).toContain("supplier_anchor_derived_for_inventory_documentary_chain");
|
||||
});
|
||||
|
||||
it("cuts inventory item anchor before purchase-doc residue tail", () => {
|
||||
const filters = extractAddressFilters(
|
||||
"По каким документам был куплен товар Диван трехместный для остатка на складе Основной склад",
|
||||
@@ -317,6 +328,33 @@ describe("address query shape classifier", () => {
|
||||
expect(plan.query).toContain("41.01");
|
||||
});
|
||||
|
||||
it("builds sale-trace query from sale document rows with explicit buyer fields", () => {
|
||||
const selected = selectAddressRecipe("inventory_sale_trace_for_item", {
|
||||
item: "Шкаф картотечный"
|
||||
});
|
||||
expect(selected.selected_recipe?.recipe_id).toBe("address_inventory_sale_trace_for_item_v1");
|
||||
const plan = buildAddressRecipePlan(selected.selected_recipe!, {
|
||||
item: "Шкаф картотечный"
|
||||
});
|
||||
expect(plan.query).toContain("Документ.РеализацияТоваровУслуг.Товары КАК Товары");
|
||||
expect(plan.query).toContain("ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Контрагент) КАК Контрагент");
|
||||
expect(plan.query).toContain("Товары.Номенклатура В (ВЫБРАТЬ Номенклатура.Ссылка");
|
||||
});
|
||||
|
||||
it("builds purchase-to-sale chain query as purchase and sale document union", () => {
|
||||
const selected = selectAddressRecipe("inventory_purchase_to_sale_chain", {
|
||||
item: "Шкаф картотечный"
|
||||
});
|
||||
expect(selected.selected_recipe?.recipe_id).toBe("address_inventory_purchase_to_sale_chain_v1");
|
||||
const plan = buildAddressRecipePlan(selected.selected_recipe!, {
|
||||
item: "Шкаф картотечный"
|
||||
});
|
||||
expect(plan.query).toContain("Документ.ПоступлениеТоваровУслуг.Товары КАК Товары");
|
||||
expect(plan.query).toContain("ОБЪЕДИНИТЬ ВСЕ");
|
||||
expect(plan.query).toContain("Документ.РеализацияТоваровУслуг.Товары КАК Товары");
|
||||
expect(plan.query).toContain("ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Контрагент) КАК Контрагент");
|
||||
});
|
||||
|
||||
it("renders inventory purchase documents from purchase-side 41.01 movements", () => {
|
||||
const reply = composeFactualReply(
|
||||
"inventory_purchase_documents_for_item",
|
||||
@@ -372,7 +410,7 @@ describe("address query shape classifier", () => {
|
||||
expect(reply.semantics?.balance_confirmed).toBe(true);
|
||||
});
|
||||
|
||||
it("renders inventory sale trace from credit-side 41.01 movements", () => {
|
||||
it("renders inventory sale trace from sale document rows", () => {
|
||||
const reply = composeFactualReply(
|
||||
"inventory_sale_trace_for_item",
|
||||
[
|
||||
@@ -400,7 +438,7 @@ describe("address query shape classifier", () => {
|
||||
expect(reply.text).toContain("Департамент капитального ремонта города Москвы");
|
||||
});
|
||||
|
||||
it("renders purchase-to-sale chain from both sides of 41.01", () => {
|
||||
it("renders purchase-to-sale chain from purchase and sale document rows", () => {
|
||||
const reply = composeFactualReply(
|
||||
"inventory_purchase_to_sale_chain",
|
||||
[
|
||||
@@ -437,6 +475,45 @@ describe("address query shape classifier", () => {
|
||||
expect(reply.text).toContain("Реализация товаров и услуг 0007");
|
||||
expect(reply.semantics?.result_mode).toBe("confirmed_balance");
|
||||
});
|
||||
|
||||
it("states when requested supplier-to-buyer chain parties are not confirmed by item documents", () => {
|
||||
const reply = composeFactualReply(
|
||||
"inventory_purchase_to_sale_chain",
|
||||
[
|
||||
{
|
||||
period: "2019-12-10T00:00:00Z",
|
||||
registrator: "Поступление товаров и услуг 00000000150 от 10.12.2019 0:00:00",
|
||||
account_dt: "41.01",
|
||||
account_kt: "",
|
||||
amount: 712500,
|
||||
analytics: ["Шкаф картотечный 1000*400*2100", "ЭталонМебель"],
|
||||
item: "Шкаф картотечный 1000*400*2100",
|
||||
counterparty: "ЭталонМебель",
|
||||
organization: "ООО Альтернатива Плюс"
|
||||
},
|
||||
{
|
||||
period: "2020-04-01T00:00:00Z",
|
||||
registrator: "Реализация товаров и услуг 00000000001 от 01.04.2020 0:00:00",
|
||||
account_dt: "",
|
||||
account_kt: "41.01",
|
||||
amount: 855000,
|
||||
analytics: ["Шкаф картотечный 1000*400*2100", "ЭталонМебель"],
|
||||
item: "Шкаф картотечный 1000*400*2100",
|
||||
counterparty: "ЭталонМебель",
|
||||
organization: "ООО Альтернатива Плюс"
|
||||
}
|
||||
],
|
||||
{
|
||||
userMessage:
|
||||
"Есть ли документально подтвержденная цепочка: поставщик Гамма-мебель, ООО -> товар Шкаф картотечный 1000*400*2100 -> покупатель Департамент капитального ремонта города Москвы?"
|
||||
}
|
||||
);
|
||||
|
||||
expect(reply.text.split("\n")[0]).toContain("полностью не подтверждена");
|
||||
expect(reply.text.split("\n")[0]).toContain("Гамма-мебель, ООО");
|
||||
expect(reply.text.split("\n")[0]).toContain("Департамент капитального ремонта города Москвы");
|
||||
expect(reply.text).toContain("ЭталонМебель");
|
||||
});
|
||||
});
|
||||
|
||||
describe("address compose stage utf8 headers", () => {
|
||||
@@ -5377,6 +5454,35 @@ it("routes old purchase residue questions to aging-by-purchase-date", () => {
|
||||
expect(reply.text).not.toContain("Контур:");
|
||||
});
|
||||
|
||||
it("answers unresolved supplier-link stock questions without asking for a supplier", () => {
|
||||
const reply = composeFactualReply(
|
||||
"inventory_supplier_stock_overlap_as_of_date",
|
||||
[
|
||||
{
|
||||
period: "2021-09-30T00:00:00Z",
|
||||
registrator: "Поступление товаров и услуг 00000000999 от 30.09.2021 0:00:00",
|
||||
account_dt: "41.01",
|
||||
account_kt: "60.01",
|
||||
amount: 1200,
|
||||
analytics: ["Товар без поставщика", "Основной склад"],
|
||||
item: "Товар без поставщика",
|
||||
warehouse: "Основной склад",
|
||||
organization: 'ООО "Альтернатива Плюс"'
|
||||
}
|
||||
],
|
||||
{
|
||||
asOfDate: "2021-09-30",
|
||||
userMessage: "Какие товары сейчас висят в остатке без понятной привязки к поставщику",
|
||||
useRubCurrency: true
|
||||
}
|
||||
);
|
||||
|
||||
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
||||
expect(reply.text.split("\n")[0]).toContain("без явно выделенного поставщика");
|
||||
expect(reply.text).toContain("Позиции без явно выделенного поставщика:");
|
||||
expect(reply.text).not.toContain("Уточните");
|
||||
});
|
||||
|
||||
it("routes inventory provenance questions to a dedicated intent", () => {
|
||||
const result = resolveAddressIntent("От какого поставщика куплен товар Шкаф картоотечный?");
|
||||
expect(result.intent).toBe("inventory_purchase_provenance_for_item");
|
||||
@@ -5416,6 +5522,11 @@ it("routes old purchase residue questions to aging-by-purchase-date", () => {
|
||||
expect(result.intent).toBe("inventory_sale_trace_for_item");
|
||||
});
|
||||
|
||||
it("routes passive buyer wording 'кому был продан товар' to inventory sale trace intent", () => {
|
||||
const result = resolveAddressIntent("Кому был продан товар Шкаф картотечный 1000*400*2100?");
|
||||
expect(result.intent).toBe("inventory_sale_trace_for_item");
|
||||
});
|
||||
|
||||
it("routes colloquial buyer wording with 'впарили' to inventory sale trace intent", () => {
|
||||
const result = resolveAddressIntent("Кому мы впарили этот товар Шкаф картотечный?");
|
||||
expect(result.intent).toBe("inventory_sale_trace_for_item");
|
||||
|
||||
@@ -43,6 +43,31 @@ describe("assistant capability runtime binding adapter", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("allows supplier-overlap inventory aggregate questions without a supplier anchor", () => {
|
||||
const binding = resolveAssistantCapabilityRuntimeBinding({
|
||||
addressDebug: {
|
||||
capability_id: "inventory_inventory_supplier_stock_overlap_as_of_date",
|
||||
detected_intent: "inventory_supplier_stock_overlap_as_of_date",
|
||||
detected_mode: "address_query",
|
||||
capability_layer: "compute",
|
||||
capability_route_mode: "exact",
|
||||
extracted_filters: {
|
||||
warehouse: "Основной склад",
|
||||
as_of_date: "2021-09-30"
|
||||
},
|
||||
rows_matched: 500,
|
||||
route_expectation_status: "matched"
|
||||
},
|
||||
groundingStatus: "grounded",
|
||||
replyType: "factual"
|
||||
});
|
||||
|
||||
expect(binding.binding_status).toBe("bound");
|
||||
expect(binding.required_anchors).toEqual([]);
|
||||
expect(binding.missing_anchors).toEqual([]);
|
||||
expect(binding.violations).toEqual([]);
|
||||
});
|
||||
|
||||
it("binds selected-object follow-ups through item anchor when focus object is implicit", () => {
|
||||
const binding = resolveAssistantCapabilityRuntimeBinding({
|
||||
addressDebug: {
|
||||
|
||||
@@ -1008,6 +1008,45 @@ describe("assistant orchestration contract", () => {
|
||||
expect(decision.orchestrationContract?.semantic_route_arbitration?.strict_deep_investigation_bypass_allowed).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps selected-object purchase-to-sale chain wording in address lane even when LLM predecompose calls it deep", () => {
|
||||
const question =
|
||||
'По выбранному объекту "Шкаф картотечный 1000*400*2100": через какие документы прошел путь товара: закупка -> склад -> продажа';
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: question,
|
||||
effectiveAddressUserMessage: question,
|
||||
followupContext: null,
|
||||
llmPreDecomposeMeta: {
|
||||
applied: true,
|
||||
llmCanonicalCandidateDetected: true,
|
||||
predecomposeContract: {
|
||||
mode: "deep_analysis",
|
||||
mode_confidence: "high",
|
||||
intent: "list_documents_by_counterparty",
|
||||
intent_confidence: "high"
|
||||
},
|
||||
semanticExtractionContract: {
|
||||
valid: true,
|
||||
apply_canonical_recommended: true,
|
||||
reason_codes: ["deep_investigation_signal_detected"],
|
||||
guard_hints: {
|
||||
deep_investigation_signal_detected: true
|
||||
},
|
||||
extraction: {
|
||||
query_shape: "DOCUMENT_LIST",
|
||||
aggregation_profile: "list_lookup"
|
||||
}
|
||||
}
|
||||
} as any,
|
||||
useMock: false
|
||||
});
|
||||
|
||||
expect(decision.runAddressLane).toBe(true);
|
||||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||||
expect(decision.livingMode).toBe("address_data");
|
||||
expect(decision.orchestrationContract?.deep_analysis_signal_fallback_to_deep).toBe(false);
|
||||
expect(decision.orchestrationContract?.address_intent).toBe("inventory_purchase_to_sale_chain");
|
||||
});
|
||||
|
||||
it("keeps 'a na tekushuyu datu' follow-up in address lane when previous VAT context exists", () => {
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "а на текущую дату",
|
||||
|
||||
@@ -135,6 +135,43 @@ describe("assistant MCP discovery response policy", () => {
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_not_discovery_ready_address_candidate");
|
||||
});
|
||||
|
||||
it("keeps exact matched inventory address replies over stale metadata discovery candidates", () => {
|
||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply: "По товару Шкаф картотечный 1000*400*2100 цепочка поставки и продажи подтверждена.",
|
||||
currentReplySource: "address_query_runtime_v1",
|
||||
currentReplyType: "factual",
|
||||
addressRuntimeMeta: {
|
||||
detected_intent: "inventory_purchase_to_sale_chain",
|
||||
selected_recipe: "address_inventory_purchase_to_sale_chain_v1",
|
||||
mcp_call_status: "matched_non_empty",
|
||||
truth_mode: "confirmed",
|
||||
capability_binding_status: "bound",
|
||||
capability_binding_violations: [],
|
||||
answer_shape_contract: {
|
||||
reply_type: "factual",
|
||||
capability_contract_id: "inventory_inventory_purchase_to_sale_chain"
|
||||
},
|
||||
assistant_mcp_discovery_entry_point_v1: entryPoint({
|
||||
turn_input: {
|
||||
adapter_status: "ready",
|
||||
should_run_discovery: true,
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "metadata",
|
||||
asked_action_family: "inspect_documents",
|
||||
metadata_scope_hint: "склад"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.applied).toBe(false);
|
||||
expect(result.decision).toBe("keep_current_reply");
|
||||
expect(result.reply_text).toContain("цепочка поставки и продажи подтверждена");
|
||||
expect(result.reason_codes).toContain("mcp_discovery_response_policy_keep_exact_matched_factual_address_reply");
|
||||
expect(result.reason_codes).toContain("mcp_discovery_response_policy_semantic_conflict_allows_candidate_override");
|
||||
});
|
||||
|
||||
it("keeps aligned factual address lane answers when the exact lane already matched the same semantic intent", () => {
|
||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply: "ИП Калинин Н.М. | сумма: 216600 | операций: 2",
|
||||
|
||||
Reference in New Issue
Block a user