Стабилизировать маржинальность номенклатуры
This commit is contained in:
@@ -104,6 +104,15 @@ describe("addressIntentResolver regression bridges", () => {
|
||||
|
||||
expect(result.intent).toBe("inventory_aging_by_purchase_date");
|
||||
});
|
||||
it("routes nomenclature margin ranking away from OS, bank, and settlement fallbacks", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"\u041a\u0430\u043a\u0430\u044f \u043d\u043e\u043c\u0435\u043a\u043b\u0430\u0442\u0443\u0440\u0430 \u0442\u043e\u0432\u0430\u0440\u0430 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u0430 \u0441 \u0432\u044b\u0441\u043e\u043a\u043e\u0439 \u043f\u0440\u0438\u0431\u044b\u043b\u044c\u044e \u043a\u0430\u043a\u0430\u044f \u0441 \u043d\u0438\u0437\u043a\u043e\u0439"
|
||||
);
|
||||
|
||||
expect(result.intent).toBe("inventory_margin_ranking_for_nomenclature");
|
||||
expect(result.reasons).toContain("unicode_nomenclature_margin_ranking_bridge_signal_detected");
|
||||
});
|
||||
|
||||
it("detects bare historical inventory root with explicit month-year", () => {
|
||||
const result = resolveAddressIntent("остатки на март 2016");
|
||||
|
||||
|
||||
+288
@@ -104,4 +104,292 @@ describe("inventory profitability selected-object regressions", () => {
|
||||
expect(reply).toContain("не чистая прибыль компании");
|
||||
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("asks for a period before ranking nomenclature by margin", async () => {
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle(
|
||||
"\u041a\u0430\u043a\u0430\u044f \u043d\u043e\u043c\u0435\u043a\u043b\u0430\u0442\u0443\u0440\u0430 \u0442\u043e\u0432\u0430\u0440\u0430 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u0430 \u0441 \u0432\u044b\u0441\u043e\u043a\u043e\u0439 \u043f\u0440\u0438\u0431\u044b\u043b\u044c\u044e \u043a\u0430\u043a\u0430\u044f \u0441 \u043d\u0438\u0437\u043a\u043e\u0439"
|
||||
);
|
||||
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.response_type).toBe("LIMITED_WITH_REASON");
|
||||
expect(result?.debug.detected_intent).toBe("inventory_margin_ranking_for_nomenclature");
|
||||
expect(result?.debug.selected_recipe).toBe("address_inventory_margin_ranking_for_nomenclature_v1");
|
||||
expect(result?.debug.capability_id).toBe("inventory_inventory_margin_ranking_for_nomenclature");
|
||||
expect(result?.debug.missing_required_filters).toEqual(["period_from", "period_to"]);
|
||||
const reply = String(result?.reply_text ?? "");
|
||||
expect(reply).toContain("\u043d\u0443\u0436\u0435\u043d \u043f\u0435\u0440\u0438\u043e\u0434");
|
||||
expect(reply).toContain("\u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438");
|
||||
expect(reply).not.toContain("\u041e\u0421");
|
||||
expect(reply).not.toContain("\u0430\u043c\u043e\u0440\u0442\u0438\u0437");
|
||||
expect(reply).not.toContain("\u0437\u0430\u043a\u0443\u043f\u043e\u0447\u043d\u044b\u0439/\u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u043d\u044b\u0439 \u0441\u043b\u0435\u0434");
|
||||
expect(reply).not.toContain("settlement");
|
||||
expect(executeAddressMcpQueryMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("gives a useful accounting limited answer when sales exist but cost is missing", async () => {
|
||||
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||
fetched_rows: 1,
|
||||
matched_rows: 1,
|
||||
raw_rows: [
|
||||
{
|
||||
Period: "2020-05-20T00:00:00Z",
|
||||
Registrator: "Sales document 1",
|
||||
AccountDt: "62.01",
|
||||
AccountKt: "41.01",
|
||||
Amount: 1500,
|
||||
Quantity: 10,
|
||||
SubcontoKt1: "Item A",
|
||||
Organization: "OOO Alternative Plus"
|
||||
}
|
||||
],
|
||||
rows: [],
|
||||
error: null
|
||||
});
|
||||
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle(
|
||||
"\u041a\u0430\u043a\u0430\u044f \u043d\u043e\u043c\u0435\u043a\u043b\u0430\u0442\u0443\u0440\u0430 \u0442\u043e\u0432\u0430\u0440\u0430 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u0430 \u0441 \u0432\u044b\u0441\u043e\u043a\u043e\u0439 \u043f\u0440\u0438\u0431\u044b\u043b\u044c\u044e \u043a\u0430\u043a\u0430\u044f \u0441 \u043d\u0438\u0437\u043a\u043e\u0439 \u0437\u0430 \u043c\u0430\u0439 2020"
|
||||
);
|
||||
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
|
||||
expect(result?.debug.detected_intent).toBe("inventory_margin_ranking_for_nomenclature");
|
||||
expect(result?.debug.mcp_call_status).toBe("matched_non_empty");
|
||||
const reply = String(result?.reply_text ?? "");
|
||||
expect(reply).toContain("\u0440\u0435\u0439\u0442\u0438\u043d\u0433 \u043f\u0440\u0438\u0431\u044b\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442\u0443\u0440\u044b \u043f\u043e\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043d\u0435\u043b\u044c\u0437\u044f");
|
||||
expect(reply).toContain("\u0415\u0441\u0442\u044c \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f");
|
||||
expect(reply).toContain("\u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u0438 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438");
|
||||
expect(reply).toContain("\u0447\u0435\u0441\u0442\u043d\u043e \u043f\u043e\u0441\u0447\u0438\u0442\u0430\u0442\u044c \u043d\u0435\u043b\u044c\u0437\u044f");
|
||||
expect(reply).toContain("\u0427\u0442\u043e \u043c\u043e\u0436\u043d\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0434\u0430\u043b\u044c\u0448\u0435");
|
||||
expect(reply).toContain("90.01 / 90.02");
|
||||
expect(reply).not.toContain("\u041e\u0421");
|
||||
expect(reply).not.toContain("\u0430\u043c\u043e\u0440\u0442\u0438\u0437");
|
||||
expect(reply).not.toContain("\u0437\u0430\u043a\u0443\u043f\u043e\u0447\u043d\u044b\u0439/\u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u043d\u044b\u0439 \u0441\u043b\u0435\u0434");
|
||||
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not offer to show realizations when only cost base exists in the period", async () => {
|
||||
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||
fetched_rows: 1,
|
||||
matched_rows: 1,
|
||||
raw_rows: [
|
||||
{
|
||||
Period: "2017-09-12T00:00:00Z",
|
||||
Registrator: "Purchase document 1",
|
||||
AccountDt: "41.01",
|
||||
AccountKt: "60.01",
|
||||
Amount: 700,
|
||||
Quantity: 2,
|
||||
SubcontoDt1: "Item C",
|
||||
Organization: "OOO Alternative Plus"
|
||||
}
|
||||
],
|
||||
rows: [],
|
||||
error: null
|
||||
});
|
||||
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle(
|
||||
"\u041a\u0430\u043a\u0430\u044f \u043d\u043e\u043c\u0435\u043a\u043b\u0430\u0442\u0443\u0440\u0430 \u0442\u043e\u0432\u0430\u0440\u0430 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u0430 \u0441 \u0432\u044b\u0441\u043e\u043a\u043e\u0439 \u043f\u0440\u0438\u0431\u044b\u043b\u044c\u044e \u043a\u0430\u043a\u0430\u044f \u0441 \u043d\u0438\u0437\u043a\u043e\u0439 \u0437\u0430 \u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044c 2017"
|
||||
);
|
||||
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
|
||||
const reply = String(result?.reply_text ?? "");
|
||||
expect(reply).toContain("\u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u043d\u0430\u044f \u0431\u0430\u0437\u0430");
|
||||
expect(reply).toContain("\u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u043f\u043e \u043d\u0435\u0439 \u0432 \u043f\u0435\u0440\u0438\u043e\u0434\u0435 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e");
|
||||
expect(reply).toContain("\u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u0435 \u0441\u0442\u0440\u043e\u043a\u0438 \u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u043d\u043e\u0439 \u0431\u0430\u0437\u044b");
|
||||
expect(reply).not.toContain("\u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u0435 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438");
|
||||
expect(reply).not.toContain("\u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u043d\u0430\u044f/\u0437\u0430\u043a\u0443\u043f\u043e\u0447\u043d\u0430\u044f");
|
||||
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("keeps cost-base line drilldown inside the nomenclature margin route", () => {
|
||||
const marginFollowupContext = {
|
||||
previous_intent: "inventory_margin_ranking_for_nomenclature" as const,
|
||||
target_intent: "inventory_margin_ranking_for_nomenclature" as const,
|
||||
root_intent: "inventory_margin_ranking_for_nomenclature" as const,
|
||||
previous_filters: {
|
||||
organization: "OOO Alternative Plus",
|
||||
period_from: "2017-09-01",
|
||||
period_to: "2017-09-30"
|
||||
},
|
||||
previous_anchor_type: "unknown" as const,
|
||||
previous_anchor_value: null
|
||||
};
|
||||
|
||||
const result = runAddressDecomposeStage(
|
||||
"\u043f\u043e\u043a\u0430\u0436\u0438 \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u0435 \u0441\u0442\u0440\u043e\u043a\u0438 \u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u043d\u043e\u0439 \u0431\u0430\u0437\u044b",
|
||||
marginFollowupContext
|
||||
);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.intent.intent).toBe("inventory_margin_ranking_for_nomenclature");
|
||||
expect(result?.filters.extracted_filters.period_from).toBe("2017-09-01");
|
||||
expect(result?.filters.extracted_filters.period_to).toBe("2017-09-30");
|
||||
expect(result?.filters.missing_required_filters).toEqual([]);
|
||||
expect(result?.intent.reasons).toContain("intent_adjusted_to_inventory_margin_ranking_followup_context");
|
||||
});
|
||||
|
||||
it("does not pivot margin follow-up account-41 correction into a balance snapshot", () => {
|
||||
const marginFollowupContext = {
|
||||
previous_intent: "inventory_margin_ranking_for_nomenclature" as const,
|
||||
target_intent: "inventory_margin_ranking_for_nomenclature" as const,
|
||||
root_intent: "inventory_margin_ranking_for_nomenclature" as const,
|
||||
previous_filters: {
|
||||
organization: "OOO Alternative Plus",
|
||||
period_from: "2017-09-01",
|
||||
period_to: "2017-09-30"
|
||||
},
|
||||
previous_anchor_type: "unknown" as const,
|
||||
previous_anchor_value: null
|
||||
};
|
||||
|
||||
for (const message of [
|
||||
"\u0430\u043d\u0430\u043b\u0438\u0437 \u043f\u043e 41 \u0441\u0447\u0435\u0442\u0443 \u0430 \u043d\u0435 01",
|
||||
"\u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0430\u043d\u0430\u043b\u0438\u0437 \u043f\u043e \u0441\u0447\u0435\u0442\u0443 41 \u0432\u043c\u0435\u0441\u0442\u043e \u0441\u0447\u0435\u0442\u0430 01"
|
||||
]) {
|
||||
const result = runAddressDecomposeStage(message, marginFollowupContext);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.intent.intent).toBe("inventory_margin_ranking_for_nomenclature");
|
||||
expect(result?.intent.intent).not.toBe("account_balance_snapshot");
|
||||
expect(result?.filters.extracted_filters.period_from).toBe("2017-09-01");
|
||||
expect(result?.filters.extracted_filters.period_to).toBe("2017-09-30");
|
||||
expect(result?.filters.missing_required_filters).toEqual([]);
|
||||
expect(result?.intent.reasons).toContain("intent_adjusted_to_inventory_margin_ranking_followup_context");
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps carried period when executing margin account-41 correction", async () => {
|
||||
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||
fetched_rows: 2,
|
||||
matched_rows: 2,
|
||||
raw_rows: [
|
||||
{
|
||||
Period: "2017-02-10T00:00:00Z",
|
||||
Registrator: "Purchase document 2017",
|
||||
AccountDt: "41.01",
|
||||
AccountKt: "60.01",
|
||||
Amount: 500,
|
||||
Quantity: 1,
|
||||
SubcontoDt1: "Item A",
|
||||
Organization: "OOO Alternative Plus"
|
||||
},
|
||||
{
|
||||
Period: "2017-03-10T00:00:00Z",
|
||||
Registrator: "Sales document 2017",
|
||||
AccountDt: "62.01",
|
||||
AccountKt: "41.01",
|
||||
Amount: 900,
|
||||
Quantity: 1,
|
||||
SubcontoKt1: "Item A",
|
||||
Organization: "OOO Alternative Plus"
|
||||
}
|
||||
],
|
||||
rows: [],
|
||||
error: null
|
||||
});
|
||||
const marginFollowupContext = {
|
||||
previous_intent: "inventory_margin_ranking_for_nomenclature" as const,
|
||||
target_intent: "inventory_margin_ranking_for_nomenclature" as const,
|
||||
root_intent: "inventory_margin_ranking_for_nomenclature" as const,
|
||||
previous_filters: {
|
||||
organization: "OOO Alternative Plus",
|
||||
period_from: "2017-01-01",
|
||||
period_to: "2017-12-31"
|
||||
},
|
||||
previous_anchor_type: "organization" as const,
|
||||
previous_anchor_value: "OOO Alternative Plus"
|
||||
};
|
||||
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle(
|
||||
"\u0430\u043d\u0430\u043b\u0438\u0437 \u043f\u043e 41 \u0441\u0447\u0435\u0442\u0443 \u0430 \u043d\u0435 01",
|
||||
{ followupContext: marginFollowupContext }
|
||||
);
|
||||
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.debug.detected_intent).toBe("inventory_margin_ranking_for_nomenclature");
|
||||
expect(result?.debug.selected_recipe).toBe("address_inventory_margin_ranking_for_nomenclature_v1");
|
||||
expect(result?.debug.capability_id).toBe("inventory_inventory_margin_ranking_for_nomenclature");
|
||||
expect(result?.debug.extracted_filters?.period_from).toBe("2017-01-01");
|
||||
expect(result?.debug.extracted_filters?.period_to).toBe("2017-12-31");
|
||||
expect(result?.debug.missing_required_filters).toEqual([]);
|
||||
expect(result?.debug.mcp_call_status).toBe("matched_non_empty");
|
||||
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("answers period-scoped nomenclature margin ranking with high and low gross-margin buckets", async () => {
|
||||
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||
fetched_rows: 4,
|
||||
matched_rows: 4,
|
||||
raw_rows: [
|
||||
{
|
||||
Period: "2020-01-10T00:00:00Z",
|
||||
Registrator: "Purchase document 1",
|
||||
AccountDt: "41.01",
|
||||
AccountKt: "60.01",
|
||||
Amount: 500,
|
||||
Quantity: 10,
|
||||
SubcontoDt1: "Item A",
|
||||
Organization: "OOO Alternative Plus"
|
||||
},
|
||||
{
|
||||
Period: "2020-02-10T00:00:00Z",
|
||||
Registrator: "Sales document 1",
|
||||
AccountDt: "62.01",
|
||||
AccountKt: "41.01",
|
||||
Amount: 1500,
|
||||
Quantity: 10,
|
||||
SubcontoKt1: "Item A",
|
||||
Organization: "OOO Alternative Plus"
|
||||
},
|
||||
{
|
||||
Period: "2020-03-10T00:00:00Z",
|
||||
Registrator: "Purchase document 2",
|
||||
AccountDt: "41.01",
|
||||
AccountKt: "60.01",
|
||||
Amount: 1000,
|
||||
Quantity: 5,
|
||||
SubcontoDt1: "Item B",
|
||||
Organization: "OOO Alternative Plus"
|
||||
},
|
||||
{
|
||||
Period: "2020-04-10T00:00:00Z",
|
||||
Registrator: "Sales document 2",
|
||||
AccountDt: "62.01",
|
||||
AccountKt: "41.01",
|
||||
Amount: 900,
|
||||
Quantity: 5,
|
||||
SubcontoKt1: "Item B",
|
||||
Organization: "OOO Alternative Plus"
|
||||
}
|
||||
],
|
||||
rows: [],
|
||||
error: null
|
||||
});
|
||||
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle(
|
||||
"\u041a\u0430\u043a\u0430\u044f \u043d\u043e\u043c\u0435\u043a\u043b\u0430\u0442\u0443\u0440\u0430 \u0442\u043e\u0432\u0430\u0440\u0430 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u0430 \u0441 \u0432\u044b\u0441\u043e\u043a\u043e\u0439 \u043f\u0440\u0438\u0431\u044b\u043b\u044c\u044e \u043a\u0430\u043a\u0430\u044f \u0441 \u043d\u0438\u0437\u043a\u043e\u0439 \u0437\u0430 2020 \u0433\u043e\u0434"
|
||||
);
|
||||
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
|
||||
expect(result?.debug.detected_intent).toBe("inventory_margin_ranking_for_nomenclature");
|
||||
expect(result?.debug.selected_recipe).toBe("address_inventory_margin_ranking_for_nomenclature_v1");
|
||||
expect(result?.debug.capability_id).toBe("inventory_inventory_margin_ranking_for_nomenclature");
|
||||
expect(result?.debug.mcp_call_status).toBe("matched_non_empty");
|
||||
const reply = String(result?.reply_text ?? "");
|
||||
expect(reply).toContain("\u0412\u044b\u0441\u043e\u043a\u0430\u044f \u0432\u0430\u043b\u043e\u0432\u0430\u044f \u043c\u0430\u0440\u0436\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u044c");
|
||||
expect(reply).toContain("\u041d\u0438\u0437\u043a\u0430\u044f \u0438\u043b\u0438 \u043e\u0442\u0440\u0438\u0446\u0430\u0442\u0435\u043b\u044c\u043d\u0430\u044f");
|
||||
expect(reply).toContain("Item A");
|
||||
expect(reply).toContain("Item B");
|
||||
expect(reply).toContain("\u043d\u0435 \u0447\u0438\u0441\u0442\u0430\u044f \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438");
|
||||
expect(reply).not.toContain("\u041e\u0421");
|
||||
expect(reply).not.toContain("\u0430\u043c\u043e\u0440\u0442\u0438\u0437");
|
||||
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -499,6 +499,70 @@ describe("assistant address orchestration runtime adapter", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("prefers raw margin-ranking account correction over account-balance canonical drift", async () => {
|
||||
const resolveAddressFollowupCarryoverContext = vi.fn(() => ({
|
||||
followupContext: {
|
||||
previous_intent: "inventory_margin_ranking_for_nomenclature",
|
||||
target_intent: "inventory_margin_ranking_for_nomenclature",
|
||||
root_intent: "inventory_margin_ranking_for_nomenclature",
|
||||
previous_filters: {
|
||||
period_from: "2017-01-01",
|
||||
period_to: "2017-12-31",
|
||||
organization: "OOO Alternative Plus"
|
||||
},
|
||||
previous_anchor_type: "organization",
|
||||
previous_anchor_value: "OOO Alternative Plus"
|
||||
}
|
||||
}));
|
||||
const resolveAssistantOrchestrationDecision = vi.fn(() => ({
|
||||
runAddressLane: true,
|
||||
livingMode: "address_data",
|
||||
livingReason: "address_lane_triggered",
|
||||
toolGateDecision: "run_address_lane",
|
||||
toolGateReason: "followup_context_detected",
|
||||
orchestrationContract: { schema_version: "assistant_orchestration_contract_v1" }
|
||||
}));
|
||||
const buildAddressLlmPredecomposeContractV1 = vi.fn(({ sourceMessage, canonicalMessage }: { sourceMessage: string; canonicalMessage: string }) => ({
|
||||
schema_version: "address_llm_predecompose_contract_v1",
|
||||
source_message: sourceMessage,
|
||||
canonical_message: canonicalMessage,
|
||||
mode: canonicalMessage === sourceMessage ? "unsupported" : "address_query",
|
||||
intent: canonicalMessage === sourceMessage ? "unknown" : "account_balance_snapshot"
|
||||
}));
|
||||
const rawMessage = "\u0430\u043d\u0430\u043b\u0438\u0437 \u043f\u043e 41 \u0441\u0447\u0435\u0442\u0443 \u0430 \u043d\u0435 01";
|
||||
|
||||
const output = await buildAssistantAddressOrchestrationRuntime(
|
||||
buildInput({
|
||||
userMessage: rawMessage,
|
||||
runAddressLlmPreDecompose: vi.fn(async () => ({
|
||||
attempted: true,
|
||||
applied: true,
|
||||
effectiveMessage:
|
||||
"\u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0430\u043d\u0430\u043b\u0438\u0437 \u043f\u043e \u0441\u0447\u0435\u0442\u0443 41 \u0432\u043c\u0435\u0441\u0442\u043e \u0441\u0447\u0435\u0442\u0430 01",
|
||||
reason: "normalized_fragment_applied",
|
||||
predecomposeContract: {
|
||||
mode: "address_query",
|
||||
intent: "account_balance_snapshot"
|
||||
}
|
||||
})),
|
||||
buildAddressLlmPredecomposeContractV1,
|
||||
resolveAddressFollowupCarryoverContext,
|
||||
resolveAssistantOrchestrationDecision
|
||||
})
|
||||
);
|
||||
|
||||
expect(output.addressInputMessage).toBe(rawMessage);
|
||||
expect(output.addressPreDecompose.applied).toBe(false);
|
||||
expect(output.addressPreDecompose.reason).toBe("followup_raw_message_preferred_over_llm_rewrite");
|
||||
expect(resolveAddressFollowupCarryoverContext).toHaveBeenCalledTimes(2);
|
||||
expect(resolveAssistantOrchestrationDecision).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
rawUserMessage: rawMessage,
|
||||
effectiveAddressUserMessage: rawMessage
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("prefers raw selected-object inventory action over generic canonical drift intent", async () => {
|
||||
const resolveAddressFollowupCarryoverContext = vi.fn(() => ({
|
||||
followupContext: {
|
||||
|
||||
@@ -66,6 +66,35 @@ describe("assistant capability binding response guard", () => {
|
||||
expect(output.audit.reason_codes).toContain("capability_binding_guard_clarification_reply");
|
||||
});
|
||||
|
||||
it("uses business clarification for nomenclature margin ranking period", () => {
|
||||
const output = applyAssistantCapabilityBindingResponseGuard({
|
||||
assistantReply: "unsafe answer",
|
||||
replyType: "partial_coverage",
|
||||
capabilityBinding: binding({
|
||||
capability_id: "inventory_inventory_margin_ranking_for_nomenclature",
|
||||
capability_contract_id: "inventory_inventory_margin_ranking_for_nomenclature",
|
||||
binding_status: "blocked",
|
||||
binding_action: "clarify",
|
||||
required_anchors: ["period"],
|
||||
provided_anchors: [],
|
||||
missing_anchors: ["period"],
|
||||
requires_focus_object: false,
|
||||
focus_object_binding_status: "not_required",
|
||||
result_shape: "nomenclature_margin_ranking",
|
||||
answer_object_shape: "inventory_margin_ranking",
|
||||
violations: ["required_anchor_missing"],
|
||||
reason_codes: ["required_anchor_missing"]
|
||||
})
|
||||
});
|
||||
|
||||
expect(output.replyType).toBe("partial_coverage");
|
||||
expect(output.assistantReply).toContain("Для рейтинга прибыльности номенклатуры нужен период");
|
||||
expect(output.assistantReply).toContain("выручку");
|
||||
expect(output.assistantReply).toContain("себестоимость реализации");
|
||||
expect(output.assistantReply).not.toContain("period");
|
||||
expect(output.audit.applied).toBe(true);
|
||||
});
|
||||
|
||||
it("turns blocked incompatible transitions into bounded replies", () => {
|
||||
const output = applyAssistantCapabilityBindingResponseGuard({
|
||||
assistantReply: "unsafe answer",
|
||||
|
||||
@@ -130,6 +130,34 @@ describe("assistant capability runtime binding adapter", () => {
|
||||
expect(binding.violations).toEqual([]);
|
||||
});
|
||||
|
||||
it("treats period_from and period_to as satisfying period anchor", () => {
|
||||
const binding = resolveAssistantCapabilityRuntimeBinding({
|
||||
addressDebug: {
|
||||
capability_id: "inventory_inventory_margin_ranking_for_nomenclature",
|
||||
detected_intent: "inventory_margin_ranking_for_nomenclature",
|
||||
detected_mode: "address_query",
|
||||
capability_layer: "compute",
|
||||
capability_route_mode: "exact",
|
||||
extracted_filters: {
|
||||
period_from: "2020-05-01",
|
||||
period_to: "2020-05-31",
|
||||
organization: "ООО Альтернатива Плюс"
|
||||
},
|
||||
rows_matched: 1,
|
||||
route_expectation_status: "matched"
|
||||
},
|
||||
groundingStatus: "grounded",
|
||||
replyType: "factual"
|
||||
});
|
||||
|
||||
expect(binding.binding_status).toBe("bound");
|
||||
expect(binding.binding_action).toBe("allow");
|
||||
expect(binding.required_anchors).toEqual(["period"]);
|
||||
expect(binding.provided_anchors).toEqual(expect.arrayContaining(["period_from", "period_to"]));
|
||||
expect(binding.missing_anchors).toEqual([]);
|
||||
expect(binding.violations).toEqual([]);
|
||||
});
|
||||
|
||||
it("blocks selected-object capabilities when required anchors are missing", () => {
|
||||
const binding = resolveAssistantCapabilityRuntimeBinding({
|
||||
addressDebug: {
|
||||
|
||||
Reference in New Issue
Block a user