ДОМЕНЫ - ВОПРОСЫ - СКЛАД - Починить selected-object follow-up по складу и включить разговорные/UI-варианты в обязательный domain-loop
This commit is contained in:
@@ -42,28 +42,73 @@ describe("address capability policy", () => {
|
||||
expect(isCapabilityRouteBlocked(decision)).toBe(false);
|
||||
});
|
||||
|
||||
it("marks inventory provenance intents as blocked exact-capability gaps", () => {
|
||||
it("enables inventory provenance intent as compute exact capability", () => {
|
||||
const decision = resolveAddressCapabilityRouteDecision("inventory_purchase_provenance_for_item");
|
||||
expect(decision.capability_id).toBe("inventory_inventory_purchase_provenance_for_item");
|
||||
expect(decision.capability_layer).toBe("compute");
|
||||
expect(decision.capability_route_mode).toBe("exact");
|
||||
expect(decision.capability_route_enabled).toBe(false);
|
||||
expect(decision.capability_route_reason).toBe("inventory_provenance_route_not_implemented");
|
||||
expect(isCapabilityRouteBlocked(decision)).toBe(true);
|
||||
expect(decision.capability_route_enabled).toBe(true);
|
||||
expect(decision.capability_route_reason).toBe("inventory_trace_route_enabled");
|
||||
expect(isCapabilityRouteBlocked(decision)).toBe(false);
|
||||
});
|
||||
|
||||
it("marks purchase-to-sale trace and aging intents as blocked exact-capability gaps", () => {
|
||||
it("enables inventory supplier overlap intent as compute exact capability", () => {
|
||||
const decision = resolveAddressCapabilityRouteDecision("inventory_supplier_stock_overlap_as_of_date");
|
||||
expect(decision.capability_id).toBe("inventory_inventory_supplier_stock_overlap_as_of_date");
|
||||
expect(decision.capability_layer).toBe("compute");
|
||||
expect(decision.capability_route_mode).toBe("exact");
|
||||
expect(decision.capability_route_enabled).toBe(true);
|
||||
expect(decision.capability_route_reason).toBe("inventory_supplier_stock_overlap_route_enabled");
|
||||
expect(isCapabilityRouteBlocked(decision)).toBe(false);
|
||||
});
|
||||
|
||||
it("enables purchase-document and sale-trace intents", () => {
|
||||
const purchaseDocsDecision = resolveAddressCapabilityRouteDecision("inventory_purchase_documents_for_item");
|
||||
expect(purchaseDocsDecision.capability_route_enabled).toBe(true);
|
||||
expect(purchaseDocsDecision.capability_route_reason).toBe("inventory_trace_route_enabled");
|
||||
|
||||
const saleTraceDecision = resolveAddressCapabilityRouteDecision("inventory_sale_trace_for_item");
|
||||
expect(saleTraceDecision.capability_route_enabled).toBe(true);
|
||||
expect(saleTraceDecision.capability_route_reason).toBe("inventory_trace_route_enabled");
|
||||
});
|
||||
|
||||
|
||||
it("keeps inventory purchase documents and sale chain on exact compute routes", () => {
|
||||
const purchaseDocsDecision = resolveAddressCapabilityRouteDecision("inventory_purchase_documents_for_item");
|
||||
expect(purchaseDocsDecision.capability_id).toBe("inventory_inventory_purchase_documents_for_item");
|
||||
expect(purchaseDocsDecision.capability_layer).toBe("compute");
|
||||
expect(purchaseDocsDecision.capability_route_mode).toBe("exact");
|
||||
|
||||
const chainDecision = resolveAddressCapabilityRouteDecision("inventory_purchase_to_sale_chain");
|
||||
expect(chainDecision.capability_id).toBe("inventory_inventory_purchase_to_sale_chain");
|
||||
expect(chainDecision.capability_layer).toBe("compute");
|
||||
expect(chainDecision.capability_route_mode).toBe("exact");
|
||||
});
|
||||
|
||||
it("enables purchase-to-sale trace and aging by purchase date", () => {
|
||||
const chainDecision = resolveAddressCapabilityRouteDecision("inventory_purchase_to_sale_chain");
|
||||
expect(chainDecision.capability_id).toBe("inventory_inventory_purchase_to_sale_chain");
|
||||
expect(chainDecision.capability_route_mode).toBe("exact");
|
||||
expect(chainDecision.capability_route_enabled).toBe(false);
|
||||
expect(isCapabilityRouteBlocked(chainDecision)).toBe(true);
|
||||
expect(chainDecision.capability_route_enabled).toBe(true);
|
||||
expect(chainDecision.capability_route_reason).toBe("inventory_purchase_to_sale_chain_route_enabled");
|
||||
expect(isCapabilityRouteBlocked(chainDecision)).toBe(false);
|
||||
|
||||
const agingDecision = resolveAddressCapabilityRouteDecision("inventory_aging_by_purchase_date");
|
||||
expect(agingDecision.capability_id).toBe("inventory_inventory_aging_by_purchase_date");
|
||||
expect(agingDecision.capability_route_mode).toBe("exact");
|
||||
expect(agingDecision.capability_route_enabled).toBe(false);
|
||||
expect(isCapabilityRouteBlocked(agingDecision)).toBe(true);
|
||||
expect(agingDecision.capability_route_enabled).toBe(true);
|
||||
expect(agingDecision.capability_route_reason).toBe("inventory_aging_route_enabled");
|
||||
expect(isCapabilityRouteBlocked(agingDecision)).toBe(false);
|
||||
});
|
||||
|
||||
it("enables aging by purchase date as an exact inventory capability", () => {
|
||||
const decision = resolveAddressCapabilityRouteDecision("inventory_aging_by_purchase_date");
|
||||
expect(decision.capability_id).toBe("inventory_inventory_aging_by_purchase_date");
|
||||
expect(decision.capability_layer).toBe("compute");
|
||||
expect(decision.capability_route_mode).toBe("exact");
|
||||
expect(decision.capability_route_enabled).toBe(true);
|
||||
expect(decision.capability_route_reason).toBe("inventory_aging_route_enabled");
|
||||
expect(isCapabilityRouteBlocked(decision)).toBe(false);
|
||||
});
|
||||
|
||||
it("maps document drilldown intent to navigation capability", () => {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { extractAddressFilters } from "../src/services/addressFilterExtractor";
|
||||
|
||||
describe("inventory supplier-item-buyer chain filter extraction", () => {
|
||||
it("trims buyer tail from item filter in documentary chain questions", () => {
|
||||
const filters = extractAddressFilters(
|
||||
"Есть ли документально подтвержденная цепочка: поставщик Гамма-мебель, ООО -> товар Шкаф картотечный 1000*400*2100 -> покупатель Департамент капитального ремонта города Москвы",
|
||||
"inventory_purchase_to_sale_chain"
|
||||
).extracted_filters;
|
||||
|
||||
expect(filters.item).toContain("1000*400*2100");
|
||||
expect(filters.item).not.toContain("покупатель");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { executeAddressMcpQueryMock } = vi.hoisted(() => ({
|
||||
executeAddressMcpQueryMock: vi.fn()
|
||||
}));
|
||||
|
||||
vi.mock("../src/services/addressMcpClient", async () => {
|
||||
const actual = await vi.importActual<typeof import("../src/services/addressMcpClient")>(
|
||||
"../src/services/addressMcpClient"
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
executeAddressMcpQuery: executeAddressMcpQueryMock
|
||||
};
|
||||
});
|
||||
|
||||
import { AddressQueryService } from "../src/services/addressQueryService";
|
||||
|
||||
afterEach(() => {
|
||||
executeAddressMcpQueryMock.mockReset();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("inventory selected-object follow-up", () => {
|
||||
it("auto-broadens dated stock follow-up window for inventory provenance", async () => {
|
||||
executeAddressMcpQueryMock
|
||||
.mockResolvedValueOnce({
|
||||
fetched_rows: 1,
|
||||
matched_rows: 1,
|
||||
raw_rows: [
|
||||
{
|
||||
Period: "2021-03-09T00:00:00Z",
|
||||
Registrator: "Поступление товаров и услуг 00000000001 от 09.03.2021 0:00:00",
|
||||
AccountDt: "41.01",
|
||||
AccountKt: "60.01",
|
||||
Amount: 442075,
|
||||
SubcontoDt1: "Рабочая станция универсального специалиста с угловым элементом.",
|
||||
SubcontoDt3: "Основной склад",
|
||||
SubcontoKt1: "АСТРА",
|
||||
SubcontoKt2: "А_03/2021 от 01.03.2021г.",
|
||||
Organization: "ООО \\Альтернатива Плюс\\"
|
||||
}
|
||||
],
|
||||
rows: [],
|
||||
error: null
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
fetched_rows: 2,
|
||||
matched_rows: 2,
|
||||
raw_rows: [
|
||||
{
|
||||
Period: "2020-02-11T00:00:00Z",
|
||||
Registrator: "Поступление товаров и услуг 00000000077 от 11.02.2020 0:00:00",
|
||||
AccountDt: "41.01",
|
||||
AccountKt: "60.01",
|
||||
Amount: 165.83,
|
||||
SubcontoDt1: "Кромка с клеем 33 альмандин 137 м",
|
||||
SubcontoDt3: "Основной склад",
|
||||
SubcontoKt1: "Торговый дом \\Союз МСК\\",
|
||||
SubcontoKt2: "Договор поставки № 12 от 01.02.2020",
|
||||
Organization: "ООО \\Альтернатива Плюс\\"
|
||||
},
|
||||
{
|
||||
Period: "2020-02-11T00:00:00Z",
|
||||
Registrator: "Поступление товаров и услуг 00000000077 от 11.02.2020 0:00:00",
|
||||
AccountDt: "41.01",
|
||||
AccountKt: "60.01",
|
||||
Amount: 165.83,
|
||||
SubcontoDt1: "Кромка с клеем 33 дуб ниагара 137 м",
|
||||
SubcontoDt3: "Основной склад",
|
||||
SubcontoKt1: "Торговый дом \\Союз МСК\\",
|
||||
SubcontoKt2: "Договор поставки № 12 от 01.02.2020",
|
||||
Organization: "ООО \\Альтернатива Плюс\\"
|
||||
}
|
||||
],
|
||||
rows: [],
|
||||
error: null
|
||||
});
|
||||
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle(
|
||||
'По выбранному объекту "Кромка с клеем 33 альмандин 137 м | склад: Основной склад | количество: 1,000 | стоимость: 165,83 ₽ | организация: ООО \\\\Альтернатива Плюс\\\\ | дата строки: 2021-03-31T23:59:59Z": От какого поставщика куплен товар'
|
||||
);
|
||||
|
||||
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.extracted_filters?.item).toBe("Кромка с клеем 33 альмандин 137 м");
|
||||
expect(result?.debug.reasons).toContain("period_window_auto_broadened_to_available_data");
|
||||
expect(result?.debug.limitations).toContain("period_window_auto_broadened_to_available_data");
|
||||
expect(String(result?.reply_text ?? "")).toContain("Торговый дом \\Союз МСК\\");
|
||||
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { extractAddressFilters } from "../src/services/addressFilterExtractor";
|
||||
|
||||
describe("inventory warehouse anchor extraction", () => {
|
||||
it("does not treat 'по состоянию ...' as warehouse name in stock snapshot questions", () => {
|
||||
const filters = extractAddressFilters(
|
||||
"Какие товары находятся на складе по состоянию на 15 марта 2020 года?",
|
||||
"inventory_on_hand_as_of_date"
|
||||
).extracted_filters;
|
||||
|
||||
expect(filters.as_of_date).toBe("2020-03-15");
|
||||
expect(filters.warehouse).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not treat month phrases as warehouse name in stock snapshot questions", () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -130,6 +130,222 @@ describe("address query shape classifier", () => {
|
||||
expect(result.mode).toBe("address_query");
|
||||
});
|
||||
|
||||
it("extracts item anchor for inventory provenance questions", () => {
|
||||
const filters = extractAddressFilters(
|
||||
"От какого поставщика куплен товар Шкаф картотечный?",
|
||||
"inventory_purchase_provenance_for_item"
|
||||
).extracted_filters;
|
||||
expect(filters.item).toBe("Шкаф картотечный");
|
||||
});
|
||||
|
||||
it("cuts inventory item anchor before current-stock tail", () => {
|
||||
const filters = extractAddressFilters(
|
||||
"От какого поставщика куплен товар Диван трехместный из текущего остатка на складе Основной склад",
|
||||
"inventory_purchase_provenance_for_item"
|
||||
).extracted_filters;
|
||||
expect(filters.item).toBe("Диван трехместный");
|
||||
expect(filters.warehouse).toBe("Основной склад");
|
||||
});
|
||||
|
||||
it("cuts inventory item anchor before chain suffix and ignores chain pseudo-warehouse", () => {
|
||||
const filters = extractAddressFilters(
|
||||
"Через какие документы прошел путь товара Шкаф картотечный 1000*400*2100: закупка -> склад -> продажа",
|
||||
"inventory_purchase_to_sale_chain"
|
||||
).extracted_filters;
|
||||
expect(filters.item).toBe("Шкаф картотечный 1000*400*2100");
|
||||
expect(filters.warehouse).toBeUndefined();
|
||||
});
|
||||
|
||||
it("cuts inventory item anchor before purchase-doc residue tail", () => {
|
||||
const filters = extractAddressFilters(
|
||||
"По каким документам был куплен товар Диван трехместный для остатка на складе Основной склад",
|
||||
"inventory_purchase_documents_for_item"
|
||||
).extracted_filters;
|
||||
expect(filters.item).toBe("Диван трехместный");
|
||||
expect(filters.warehouse).toBe("Основной склад");
|
||||
});
|
||||
|
||||
it("extracts item anchor from selected-object inventory row for provenance follow-up", () => {
|
||||
const filters = extractAddressFilters(
|
||||
'По выбранному объекту "Кромка с клеем 33 альмандин 137 м | склад: Основной склад | количество: 1,000 | стоимость: 165,83 ₽ | организация: ООО \\\\Альтернатива Плюс\\\\ | дата строки: 2021-03-31T23:59:59Z": От какого поставщика куплен товар',
|
||||
"inventory_purchase_provenance_for_item"
|
||||
).extracted_filters;
|
||||
expect(filters.item).toBe("Кромка с клеем 33 альмандин 137 м");
|
||||
});
|
||||
|
||||
it("keeps full supplier anchor with comma suffix for stock-overlap questions", () => {
|
||||
const filters = extractAddressFilters(
|
||||
"Какие товары от поставщика Гамма-мебель, ООО сейчас еще лежат на складе Основной склад?",
|
||||
"inventory_supplier_stock_overlap_as_of_date"
|
||||
).extracted_filters;
|
||||
expect(filters.counterparty).toBe("Гамма-мебель, ООО");
|
||||
});
|
||||
|
||||
it("does not capture organization wording as supplier anchor in overlap questions", () => {
|
||||
const filters = extractAddressFilters(
|
||||
"У какого поставщика были куплены товары, которые сейчас лежат на складе Основной склад организации ООО \\Альтернатива Плюс\\",
|
||||
"inventory_supplier_stock_overlap_as_of_date"
|
||||
).extracted_filters;
|
||||
expect(filters.counterparty).toBeUndefined();
|
||||
expect(filters.warehouse).toBe("Основной склад");
|
||||
});
|
||||
|
||||
it("extracts item anchor for inventory aging questions", () => {
|
||||
const filters = extractAddressFilters(
|
||||
"Относится ли товар Шкаф картотечный 1000*400*2100 в остатке на дату 2020-03-31 к старым закупкам",
|
||||
"inventory_aging_by_purchase_date"
|
||||
).extracted_filters;
|
||||
expect(filters.item).toBe("Шкаф картотечный 1000*400*2100");
|
||||
});
|
||||
|
||||
it("builds dedicated inventory purchase-documents query plan", () => {
|
||||
const selected = selectAddressRecipe("inventory_purchase_documents_for_item", {
|
||||
item: "Шкаф картотечный",
|
||||
as_of_date: "2020-03-31"
|
||||
});
|
||||
expect(selected.selected_recipe?.recipe_id).toBe("address_inventory_purchase_documents_for_item_v1");
|
||||
const plan = buildAddressRecipePlan(selected.selected_recipe!, {
|
||||
item: "Шкаф картотечный",
|
||||
as_of_date: "2020-03-31"
|
||||
});
|
||||
expect(plan.query).toContain("РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто");
|
||||
expect(plan.query).toContain("Движения.СчетДт");
|
||||
expect(plan.query).toContain("ПРЕДСТАВЛЕНИЕ(Движения.СубконтоДт1) КАК СубконтоДт1");
|
||||
expect(plan.query).toContain("41.01");
|
||||
});
|
||||
|
||||
it("builds overlap recipe for supplier-linked stock slice", () => {
|
||||
const selected = selectAddressRecipe("inventory_supplier_stock_overlap_as_of_date", {
|
||||
counterparty: "Гамма-мебель, ООО",
|
||||
warehouse: "Основной склад",
|
||||
as_of_date: "2020-03-31"
|
||||
});
|
||||
expect(selected.selected_recipe?.recipe_id).toBe("address_inventory_supplier_stock_overlap_as_of_date_v1");
|
||||
const plan = buildAddressRecipePlan(selected.selected_recipe!, {
|
||||
counterparty: "Гамма-мебель, ООО",
|
||||
warehouse: "Основной склад",
|
||||
as_of_date: "2020-03-31"
|
||||
});
|
||||
expect(plan.query).toContain("РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто");
|
||||
expect(plan.query).toContain("Движения.СчетДт");
|
||||
expect(plan.query).toContain("41.01");
|
||||
});
|
||||
|
||||
it("renders inventory purchase documents from purchase-side 41.01 movements", () => {
|
||||
const reply = composeFactualReply(
|
||||
"inventory_purchase_documents_for_item",
|
||||
[
|
||||
{
|
||||
period: "2020-03-15T12:00:00Z",
|
||||
registrator: "Поступление товаров и услуг 0001",
|
||||
account_dt: "41.01",
|
||||
account_kt: "60.01",
|
||||
amount: 150000,
|
||||
analytics: ["Шкаф картотечный", "Основной склад", "ООО Ромашка", "Гамма-мебель, ООО"],
|
||||
item: "Шкаф картотечный",
|
||||
warehouse: "Основной склад",
|
||||
organization: "ООО Ромашка"
|
||||
}
|
||||
],
|
||||
{
|
||||
asOfDate: "2020-03-31",
|
||||
useRubCurrency: true
|
||||
}
|
||||
);
|
||||
expect(reply.text).toContain("Шкаф картотечный");
|
||||
expect(reply.text).toContain("Поступление товаров и услуг 0001");
|
||||
expect(reply.semantics?.result_mode).toBe("confirmed_balance");
|
||||
});
|
||||
|
||||
it("renders inventory provenance summary from purchase-side 41.01 movements", () => {
|
||||
const reply = composeFactualReply(
|
||||
"inventory_purchase_provenance_for_item",
|
||||
[
|
||||
{
|
||||
period: "2020-03-15T12:00:00Z",
|
||||
registrator: "Поступление товаров и услуг 0001",
|
||||
account_dt: "41.01",
|
||||
account_kt: "60.01",
|
||||
amount: 150000,
|
||||
analytics: ["Шкаф картотечный", "Основной склад", "ООО Ромашка", "Гамма-мебель, ООО"],
|
||||
item: "Шкаф картотечный",
|
||||
warehouse: "Основной склад",
|
||||
organization: "ООО Ромашка"
|
||||
}
|
||||
],
|
||||
{
|
||||
asOfDate: "2020-03-31",
|
||||
useRubCurrency: true
|
||||
}
|
||||
);
|
||||
expect(reply.text).toContain("закупочный след");
|
||||
expect(reply.text).toContain("Гамма-мебель, ООО");
|
||||
expect(reply.semantics?.balance_confirmed).toBe(true);
|
||||
});
|
||||
|
||||
it("renders inventory sale trace from credit-side 41.01 movements", () => {
|
||||
const reply = composeFactualReply(
|
||||
"inventory_sale_trace_for_item",
|
||||
[
|
||||
{
|
||||
period: "2020-04-10T12:00:00Z",
|
||||
registrator: "Реализация товаров и услуг 0007",
|
||||
account_dt: "90.02",
|
||||
account_kt: "41.01",
|
||||
amount: 210000,
|
||||
analytics: ["Шкаф картотечный", "Основной склад", "ООО Ромашка", "Департамент капитального ремонта города Москвы"],
|
||||
item: "Шкаф картотечный",
|
||||
warehouse: "Основной склад",
|
||||
organization: "ООО Ромашка"
|
||||
}
|
||||
],
|
||||
{
|
||||
asOfDate: "2020-04-30",
|
||||
useRubCurrency: true
|
||||
}
|
||||
);
|
||||
expect(reply.text).toContain("след выбытия");
|
||||
expect(reply.text).toContain("Реализация товаров и услуг 0007");
|
||||
expect(reply.text).toContain("Департамент капитального ремонта города Москвы");
|
||||
});
|
||||
|
||||
it("renders purchase-to-sale chain from both sides of 41.01", () => {
|
||||
const reply = composeFactualReply(
|
||||
"inventory_purchase_to_sale_chain",
|
||||
[
|
||||
{
|
||||
period: "2020-03-15T12:00:00Z",
|
||||
registrator: "Поступление товаров и услуг 0001",
|
||||
account_dt: "41.01",
|
||||
account_kt: "60.01",
|
||||
amount: 150000,
|
||||
analytics: ["Шкаф картотечный", "Основной склад", "ООО Ромашка", "Гамма-мебель, ООО"],
|
||||
item: "Шкаф картотечный",
|
||||
warehouse: "Основной склад",
|
||||
organization: "ООО Ромашка"
|
||||
},
|
||||
{
|
||||
period: "2020-04-10T12:00:00Z",
|
||||
registrator: "Реализация товаров и услуг 0007",
|
||||
account_dt: "90.02",
|
||||
account_kt: "41.01",
|
||||
amount: 210000,
|
||||
analytics: ["Шкаф картотечный", "Основной склад", "ООО Ромашка", "Департамент капитального ремонта города Москвы"],
|
||||
item: "Шкаф картотечный",
|
||||
warehouse: "Основной склад",
|
||||
organization: "ООО Ромашка"
|
||||
}
|
||||
],
|
||||
{
|
||||
asOfDate: "2020-04-30",
|
||||
useRubCurrency: true
|
||||
}
|
||||
);
|
||||
expect(reply.text).toContain("документальная цепочка");
|
||||
expect(reply.text).toContain("Поступление товаров и услуг 0001");
|
||||
expect(reply.text).toContain("Реализация товаров и услуг 0007");
|
||||
expect(reply.semantics?.result_mode).toBe("confirmed_balance");
|
||||
});
|
||||
});
|
||||
|
||||
describe("address compose stage utf8 headers", () => {
|
||||
@@ -4090,6 +4306,39 @@ describe("address recipe catalog counterparty filtering", () => {
|
||||
expect(plan.query).toContain("ДоговорКонтрагента");
|
||||
});
|
||||
|
||||
it("keeps recipe-default limits for inventory exact intents", () => {
|
||||
const onHand = extractAddressFilters("Какие товары сейчас лежат на складе?", "inventory_on_hand_as_of_date");
|
||||
const provenance = extractAddressFilters(
|
||||
"От какого поставщика куплен товар Диван трехместный из текущего остатка на складе Основной склад?",
|
||||
"inventory_purchase_provenance_for_item"
|
||||
);
|
||||
const purchaseDocs = extractAddressFilters(
|
||||
"По каким документам был куплен товар Диван трехместный для остатка на складе Основной склад?",
|
||||
"inventory_purchase_documents_for_item"
|
||||
);
|
||||
const overlap = extractAddressFilters(
|
||||
"Какие товары от поставщика Гамма-мебель, ООО сейчас еще лежат на складе Основной склад?",
|
||||
"inventory_supplier_stock_overlap_as_of_date"
|
||||
);
|
||||
const saleTrace = extractAddressFilters("Кому был продан товар Шкаф картотечный 1000*400*2100?", "inventory_sale_trace_for_item");
|
||||
const chain = extractAddressFilters(
|
||||
"Через какие документы прошел путь товара Шкаф картотечный 1000*400*2100: закупка -> склад -> продажа?",
|
||||
"inventory_purchase_to_sale_chain"
|
||||
);
|
||||
const aging = extractAddressFilters(
|
||||
"Есть ли среди текущих остатков на складе Основной склад позиции, закупленные очень давно?",
|
||||
"inventory_aging_by_purchase_date"
|
||||
);
|
||||
|
||||
expect(onHand.extracted_filters.limit).toBeUndefined();
|
||||
expect(provenance.extracted_filters.limit).toBeUndefined();
|
||||
expect(purchaseDocs.extracted_filters.limit).toBeUndefined();
|
||||
expect(overlap.extracted_filters.limit).toBeUndefined();
|
||||
expect(saleTrace.extracted_filters.limit).toBeUndefined();
|
||||
expect(chain.extracted_filters.limit).toBeUndefined();
|
||||
expect(aging.extracted_filters.limit).toBeUndefined();
|
||||
});
|
||||
|
||||
it("selects customer value recipe and keeps top-20 default", () => {
|
||||
const selected = selectAddressRecipe("customer_revenue_and_payments", {});
|
||||
expect(selected.selected_recipe).toBeTruthy();
|
||||
@@ -4339,6 +4588,52 @@ describe("address recipe catalog counterparty filtering", () => {
|
||||
expect(result.confidence).toBe("high");
|
||||
});
|
||||
|
||||
it("detects colloquial warehouse snapshot wording as inventory-on-hand intent", () => {
|
||||
const result = resolveAddressIntent("что у нас на складе на март 2017");
|
||||
expect(result.intent).toBe("inventory_on_hand_as_of_date");
|
||||
expect(result.confidence).toBe("high");
|
||||
});
|
||||
|
||||
it("routes account 41 composition wording into inventory snapshot intent", () => {
|
||||
const result = resolveAddressIntent("Из каких товаров состоит остаток по 41 счету");
|
||||
expect(result.intent).toBe("inventory_on_hand_as_of_date");
|
||||
});
|
||||
|
||||
it("routes account 41 date snapshot wording into inventory snapshot intent", () => {
|
||||
const result = resolveAddressIntent("Какие товары числятся на 41 счете на дату 2020-03-31");
|
||||
expect(result.intent).toBe("inventory_on_hand_as_of_date");
|
||||
});
|
||||
|
||||
it("routes supplier stock overlap wording into overlap intent", () => {
|
||||
const result = resolveAddressIntent("Какие товары от поставщика Гамма-мебель, ООО сейчас еще лежат на складе Основной склад");
|
||||
expect(result.intent).toBe("inventory_supplier_stock_overlap_as_of_date");
|
||||
});
|
||||
|
||||
it("routes supplier to buyer chain wording into purchase-to-sale chain intent", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"Есть ли документально подтвержденная цепочка: поставщик Гамма-мебель, ООО -> товар Шкаф картотечный 1000*400*2100 -> покупатель Департамент капитального ремонта города Москвы"
|
||||
);
|
||||
expect(result.intent).toBe("inventory_purchase_to_sale_chain");
|
||||
});
|
||||
|
||||
it("routes supplier-to-buyer inventory chains as exact trace intents", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"Есть ли документально подтвержденная цепочка: поставщик Гамма-мебель, ООО -> товар Шкаф картотечный 1000*400*2100 -> покупатель Департамент капитального ремонта города Москвы"
|
||||
);
|
||||
|
||||
expect(result.intent).toBe("inventory_purchase_to_sale_chain");
|
||||
expect(result.confidence).toBe("medium");
|
||||
});
|
||||
|
||||
it("routes old purchase residue questions to aging-by-purchase-date", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"Относится ли товар Шкаф картотечный 1000*400*2100 в остатке на дату 2020-03-31 к старым закупкам"
|
||||
);
|
||||
|
||||
expect(result.intent).toBe("inventory_aging_by_purchase_date");
|
||||
expect(result.confidence).toBe("high");
|
||||
});
|
||||
|
||||
it("derives as_of_date for inventory-on-hand from explicit month window", () => {
|
||||
const filters = extractAddressFilters(
|
||||
"Какие товары лежат на складе на март 2020",
|
||||
@@ -4400,6 +4695,13 @@ describe("address recipe catalog counterparty filtering", () => {
|
||||
expect(result.intent).toBe("inventory_purchase_provenance_for_item");
|
||||
});
|
||||
|
||||
it("keeps direct item supplier questions in provenance intent even with current-stock tail", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"От какого поставщика куплен товар Диван трехместный из текущего остатка на складе Основной склад?"
|
||||
);
|
||||
expect(result.intent).toBe("inventory_purchase_provenance_for_item");
|
||||
});
|
||||
|
||||
it("keeps inventory supplier overlap questions out of on-hand routing", () => {
|
||||
const result = resolveAddressIntent("Какие товары от поставщика Альфа сейчас лежат на складе?");
|
||||
expect(result.intent).toBe("inventory_supplier_stock_overlap_as_of_date");
|
||||
@@ -4414,7 +4716,7 @@ describe("address recipe catalog counterparty filtering", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"Через какие документы прошел путь товара Шкаф картоотечный: закупка -> склад -> продажа?"
|
||||
);
|
||||
expect(result.intent).toBe("inventory_sale_trace_for_item");
|
||||
expect(result.intent).toBe("inventory_purchase_to_sale_chain");
|
||||
});
|
||||
|
||||
it("keeps inventory provenance wording out of inventory-on-hand routing", () => {
|
||||
@@ -4427,6 +4729,16 @@ describe("address recipe catalog counterparty filtering", () => {
|
||||
expect(result.intent).toBe("inventory_aging_by_purchase_date");
|
||||
});
|
||||
|
||||
it("keeps very old purchase wording out of on-hand routing", () => {
|
||||
const result = resolveAddressIntent("Относится ли товар к закупленным задолго до даты остатка на складе?");
|
||||
expect(result.intent).toBe("inventory_aging_by_purchase_date");
|
||||
});
|
||||
|
||||
it("routes old stock wording with residue anchor to aging intent", () => {
|
||||
const result = resolveAddressIntent("Это остаток по очень давно купленному товару?");
|
||||
expect(result.intent).toBe("inventory_aging_by_purchase_date");
|
||||
});
|
||||
|
||||
it("routes purchase-document trace wording to dedicated inventory intent", () => {
|
||||
const result = resolveAddressIntent("Какими документами был куплен товар Шкаф картоотечный?");
|
||||
expect(result.intent).toBe("inventory_purchase_documents_for_item");
|
||||
@@ -4436,9 +4748,33 @@ describe("address recipe catalog counterparty filtering", () => {
|
||||
expect(result.intent).toBe("inventory_aging_by_purchase_date");
|
||||
});
|
||||
|
||||
it("routes residue wording with explicit cut-off date into aging intent", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"Есть ли среди текущих остатков на складе Основной склад позиции, закупленные задолго до 2020-03-31?"
|
||||
);
|
||||
expect(result.intent).toBe("inventory_aging_by_purchase_date");
|
||||
});
|
||||
|
||||
it("keeps unresolved stock provenance wording out of open-items routing", () => {
|
||||
const result = resolveAddressIntent("Какие товары сейчас висят в остатке без понятной привязки к поставщику?");
|
||||
expect(result.intent).toBe("inventory_purchase_provenance_for_item");
|
||||
expect(result.intent).toBe("inventory_supplier_stock_overlap_as_of_date");
|
||||
});
|
||||
|
||||
it("routes documentary supplier-to-buyer chain wording into inventory chain intent", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"Есть ли документально подтвержденная цепочка: поставщик Гамма-мебель, ООО -> товар Шкаф картотечный 1000*400*2100 -> покупатель Департамент капитального ремонта города Москвы?"
|
||||
);
|
||||
expect(result.intent).toBe("inventory_purchase_to_sale_chain");
|
||||
});
|
||||
|
||||
it("routes explicit supplier-item-buyer chain wording into inventory chain intent", () => {
|
||||
const result = resolveAddressIntent("supplier -> item -> buyer");
|
||||
expect(result.intent).toBe("inventory_purchase_to_sale_chain");
|
||||
});
|
||||
|
||||
it("routes documented supplier-item-buyer chain wording into inventory chain intent", () => {
|
||||
const result = resolveAddressIntent("Документально подтвержденная цепочка: поставщик -> товар -> покупатель");
|
||||
expect(result.intent).toBe("inventory_purchase_to_sale_chain");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -254,6 +254,102 @@ describe("assistant address llm pre-decompose candidate preference", () => {
|
||||
expect(response.debug?.llm_decomposition_reason).not.toBe("normalized_fragment_applied");
|
||||
});
|
||||
|
||||
it("keeps colloquial warehouse snapshot questions in exact inventory contour when llm rewrites them abstractly", 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-inventory-colloquial",
|
||||
ok: true,
|
||||
normalized: {
|
||||
schema_version: "normalized_query_v2_0_2",
|
||||
user_message_raw: "что у нас на складе на март 2017",
|
||||
message_in_scope: true,
|
||||
scope_confidence: "medium",
|
||||
contains_multiple_tasks: false,
|
||||
fragments: [
|
||||
{
|
||||
fragment_id: "F1",
|
||||
raw_fragment_text: "что у нас на складе на март 2017",
|
||||
normalized_fragment_text: "информация о наличии товаров на складе за март 2017 года",
|
||||
domain_relevance: "in_scope",
|
||||
business_scope: "company_specific_accounting",
|
||||
entity_hints: [],
|
||||
account_hints: [],
|
||||
document_hints: [],
|
||||
register_hints: [],
|
||||
time_scope: {
|
||||
type: "explicit",
|
||||
value: "2017-03",
|
||||
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-inventory-colloquial-${Date.now()}`,
|
||||
user_message: "что у нас на складе на март 2017",
|
||||
llmProvider: "local",
|
||||
useMock: false
|
||||
} as any);
|
||||
|
||||
expect(response.ok).toBe(true);
|
||||
expect(response.reply_type).toBe("factual");
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0].message.toLowerCase()).toContain("складе");
|
||||
expect(response.debug?.tool_gate_decision).toBe("run_address_lane");
|
||||
expect(response.debug?.llm_predecompose_contract?.intent).toBe("inventory_on_hand_as_of_date");
|
||||
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 = {
|
||||
|
||||
@@ -264,6 +264,36 @@ describe("assistant orchestration contract", () => {
|
||||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||||
});
|
||||
|
||||
it("keeps inventory provenance and sale-trace queries in address lane", () => {
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "От какого поставщика куплен товар Диван трехместный из текущего остатка на складе Основной склад",
|
||||
effectiveAddressUserMessage: "От какого поставщика куплен товар Диван трехместный из текущего остатка на складе Основной склад",
|
||||
followupContext: {
|
||||
previous_intent: "inventory_purchase_provenance_for_item",
|
||||
previous_filters: {
|
||||
as_of_date: "2020-03-31"
|
||||
}
|
||||
},
|
||||
llmPreDecomposeMeta: {
|
||||
applied: true,
|
||||
llmCanonicalCandidateDetected: true,
|
||||
predecomposeContract: {
|
||||
mode: "address_query",
|
||||
mode_confidence: "high",
|
||||
intent: "inventory_purchase_provenance_for_item",
|
||||
intent_confidence: "high"
|
||||
}
|
||||
} as any,
|
||||
useMock: false
|
||||
});
|
||||
|
||||
expect(decision.runAddressLane).toBe(true);
|
||||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||||
expect(decision.toolGateReason).toBe("address_mode_classifier_detected");
|
||||
expect(decision.livingMode).toBe("address_data");
|
||||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||||
});
|
||||
|
||||
it("keeps customer-value ranking question in address lane even when LLM semantic guard rejects canonical rewrite", () => {
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "кто больше всего принес денег в 2020",
|
||||
@@ -558,6 +588,47 @@ describe("assistant orchestration contract", () => {
|
||||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||||
});
|
||||
|
||||
it("keeps documentary inventory chain verification in address lane for supported exact intent", () => {
|
||||
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: "inventory_purchase_to_sale_chain",
|
||||
intent_confidence: "medium"
|
||||
},
|
||||
semanticExtractionContract: {
|
||||
valid: true,
|
||||
apply_canonical_recommended: true,
|
||||
reason_codes: ["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?.deep_analysis_signal_fallback_to_deep).toBe(false);
|
||||
expect(decision.orchestrationContract?.semantic_route_arbitration?.supported_address_intent_detected).toBe(true);
|
||||
expect(decision.orchestrationContract?.semantic_route_arbitration?.strict_deep_investigation_bypass_allowed).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps 'a na tekushuyu datu' follow-up in address lane when previous VAT context exists", () => {
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "а на текущую дату",
|
||||
|
||||
Reference in New Issue
Block a user