Закрепить selected-object закупки в 1С ассистенте
This commit is contained in:
@@ -58,6 +58,15 @@ describe("addressInventoryIntentSignals", () => {
|
||||
expect(result.reasons).toContain("inventory_purchase_date_signal_detected");
|
||||
});
|
||||
|
||||
it("routes selected-object purchase confirmation wording to item purchase documents before generic document drilldown", () => {
|
||||
const result = resolveAddressIntent(
|
||||
'По выбранному объекту "Флаг геральдический. 100*150 см. Ткань Т101 полиэфирный шелк": каким документом подтверждена закупка?'
|
||||
);
|
||||
|
||||
expect(result.intent).toBe("inventory_purchase_documents_for_item");
|
||||
expect(result.reasons).toContain("inventory_selected_object_purchase_documents_signal_detected");
|
||||
});
|
||||
|
||||
it("classifies direct Russian purchase-date wording with an explicit item", () => {
|
||||
const result = resolveAddressIntent("Когда был куплен товар Столешница 600*3050*26 дуб ниагара");
|
||||
|
||||
|
||||
+29
@@ -49,6 +49,35 @@ describe("inventory profitability selected-object regressions", () => {
|
||||
expect(result?.filters.extracted_filters.period_to).toBe("2020-05-31");
|
||||
});
|
||||
|
||||
it("routes UTF-8 selected-object purchase confirmation wording to item purchase documents", () => {
|
||||
const item = "Флаг геральдический. 100*150 см. Ткань Т101 полиэфирный шелк";
|
||||
const result = runAddressDecomposeStage(
|
||||
`По выбранному объекту "${item}": каким документом подтверждена закупка?`,
|
||||
{
|
||||
previous_intent: "inventory_margin_ranking_for_nomenclature",
|
||||
target_intent: "inventory_margin_ranking_for_nomenclature",
|
||||
root_intent: "inventory_margin_ranking_for_nomenclature",
|
||||
previous_filters: {
|
||||
item,
|
||||
organization: 'ООО "Альтернатива Плюс"',
|
||||
as_of_date: "2017-12-31",
|
||||
period_from: "2017-01-01",
|
||||
period_to: "2017-12-31"
|
||||
},
|
||||
previous_anchor_type: "item",
|
||||
previous_anchor_value: item,
|
||||
current_frame_kind: "inventory_drilldown"
|
||||
}
|
||||
);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.intent.intent).toBe("inventory_purchase_documents_for_item");
|
||||
expect(result?.filters.extracted_filters.item).toBe(item);
|
||||
expect(result?.filters.extracted_filters.as_of_date).toBe("2017-12-31");
|
||||
expect(result?.filters.extracted_filters.counterparty).toBeUndefined();
|
||||
expect(result?.intent.reasons).toContain("inventory_selected_object_purchase_documents_signal_detected");
|
||||
});
|
||||
|
||||
it("answers selected-object profitability with a bounded document spread instead of a recipe gap", async () => {
|
||||
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||
fetched_rows: 2,
|
||||
|
||||
@@ -557,6 +557,59 @@ describe("inventory selected-object follow-up", () => {
|
||||
expect(String(result?.reply_text ?? "")).toContain("Поступление товаров и услуг 00000000077");
|
||||
});
|
||||
|
||||
it("handles UTF-8 UI selected-object purchase confirmation as an item document drilldown", async () => {
|
||||
const item = "Флаг геральдический. 100*150 см. Ткань Т101 полиэфирный шелк";
|
||||
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||
fetched_rows: 1,
|
||||
matched_rows: 1,
|
||||
raw_rows: [
|
||||
{
|
||||
Period: "2017-06-01T12:00:09Z",
|
||||
Registrator: "Поступление товаров и услуг 00000000016 от 01.06.2017 12:00:09",
|
||||
AccountDt: "41.01",
|
||||
AccountKt: "60.01",
|
||||
Amount: 8520,
|
||||
Quantity: 1,
|
||||
SubcontoDt1: item,
|
||||
SubcontoDt3: "Основной склад",
|
||||
Counterparty: "ПрофТренд,ООО",
|
||||
Organization: 'ООО "Альтернатива Плюс"'
|
||||
}
|
||||
],
|
||||
rows: [],
|
||||
error: null
|
||||
});
|
||||
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle(`По выбранному объекту "${item}": каким документом подтверждена закупка?`, {
|
||||
followupContext: {
|
||||
previous_intent: "inventory_margin_ranking_for_nomenclature",
|
||||
target_intent: "inventory_margin_ranking_for_nomenclature",
|
||||
root_intent: "inventory_margin_ranking_for_nomenclature",
|
||||
previous_filters: {
|
||||
item,
|
||||
organization: 'ООО "Альтернатива Плюс"',
|
||||
as_of_date: "2017-12-31",
|
||||
period_from: "2017-01-01",
|
||||
period_to: "2017-12-31"
|
||||
},
|
||||
previous_anchor_type: "item",
|
||||
previous_anchor_value: item,
|
||||
current_frame_kind: "inventory_drilldown"
|
||||
}
|
||||
});
|
||||
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.response_type).toBe("FACTUAL_LIST");
|
||||
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.capability_id).toBe("inventory_inventory_purchase_documents_for_item");
|
||||
expect(result?.debug.extracted_filters?.item).toBe(item);
|
||||
expect(result?.debug.extracted_filters?.counterparty).toBeUndefined();
|
||||
expect(String(result?.reply_text ?? "")).toContain("Поступление товаров и услуг 00000000016");
|
||||
expect(String(result?.reply_text ?? "")).toContain("ПрофТренд,ООО");
|
||||
});
|
||||
|
||||
it("does not let carried counterparty scope steal selected-item document follow-up into open items", async () => {
|
||||
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||
fetched_rows: 1,
|
||||
|
||||
@@ -701,4 +701,56 @@ describe("address reply builders regressions", () => {
|
||||
expect(firstLine).toContain("Самая маржинальная позиция");
|
||||
expect(result?.text).not.toContain("амортизац");
|
||||
});
|
||||
|
||||
it("keeps supplier provenance evidence focused on document identity instead of surfacing ambiguous amount fields", () => {
|
||||
const result = composeInventoryReply(
|
||||
"inventory_purchase_provenance_for_item",
|
||||
[
|
||||
{
|
||||
amount: 8520,
|
||||
item: "Флаг геральдический",
|
||||
period: "2017-06-01",
|
||||
registrator: "Поступление товаров и услуг 00000000016",
|
||||
counterparty: "ПрофТренд,ООО"
|
||||
} as any
|
||||
],
|
||||
{
|
||||
userMessage: "А по этой позиции кто поставщик?",
|
||||
asOfDate: "2017-12-31"
|
||||
},
|
||||
{
|
||||
resolvePayablesAsOfDate: () => "2017-12-31",
|
||||
buildInventoryOnHandAggregate: () => [],
|
||||
uniqueStrings: (values: string[]) => Array.from(new Set(values)),
|
||||
formatDateRu: (value: string) => value,
|
||||
formatNumberWithDots: (value: number, fractionDigits = 0) => value.toFixed(fractionDigits),
|
||||
formatMoneyRub: (value: number) => `${value} ₽`,
|
||||
isInventoryPurchaseMovement: () => true,
|
||||
summarizeInventoryTraceRows: (rows: any[]) => ({
|
||||
item: rows[0]?.item ?? null,
|
||||
warehouses: [],
|
||||
organizations: [],
|
||||
counterparties: ["ПрофТренд,ООО"],
|
||||
documents: ["Поступление товаров и услуг 00000000016"],
|
||||
firstPeriod: "2017-06-01",
|
||||
lastPeriod: "2017-06-01",
|
||||
totalAmount: 8520
|
||||
}),
|
||||
formatInventoryTraceRows: () => [
|
||||
"1. Поступление товаров и услуг 00000000016 | дата: 2017-06-01 | сумма: 8.520,00 ₽ | товар: Флаг геральдический | контрагент: ПрофТренд,ООО"
|
||||
],
|
||||
hasInventoryPurchaseDateActionFocus: () => false,
|
||||
inventoryTraceDateLabel: (value: string | null) => String(value ?? ""),
|
||||
extractInventoryCounterpartyCandidates: () => ["ПрофТренд,ООО"],
|
||||
buildInventoryAgingByItemAggregate: () => [],
|
||||
formatInventoryAgingRows: () => [],
|
||||
isInventorySaleMovement: () => false
|
||||
}
|
||||
);
|
||||
|
||||
expect(result?.text.split("\n")[0]).toContain("ПрофТренд,ООО");
|
||||
expect(result?.text).toContain("Поступление товаров и услуг 00000000016");
|
||||
expect(result?.text).not.toContain("сумма:");
|
||||
expect(result?.text).not.toContain("8.520,00 ₽");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -502,6 +502,115 @@ describe("assistant address llm pre-decompose candidate preference", () => {
|
||||
]).toContain(response.debug?.llm_decomposition_reason);
|
||||
});
|
||||
|
||||
it("rejects UTF-8 selected-object purchase document rewrite into a generic counterparty document query", async () => {
|
||||
const calls: Array<{ message: string }> = [];
|
||||
const addressQueryService = {
|
||||
tryHandle: vi.fn(async (message: string) => {
|
||||
calls.push({ message });
|
||||
return buildAddressLaneResult(message);
|
||||
})
|
||||
} as any;
|
||||
|
||||
const sourceMessage =
|
||||
'По выбранному объекту "Флаг геральдический. 100*150 см. Ткань Т101 полиэфирный шелк": каким документом подтверждена закупка?';
|
||||
const selectedItem = "Флаг геральдический. 100*150 см. Ткань Т101 полиэфирный шелк";
|
||||
|
||||
const normalizerService = {
|
||||
normalize: vi.fn(async () => ({
|
||||
trace_id: "norm-predecompose-selected-object-counterparty-degradation",
|
||||
ok: true,
|
||||
normalized: {
|
||||
schema_version: "normalized_query_v2_0_2",
|
||||
user_message_raw: sourceMessage,
|
||||
message_in_scope: true,
|
||||
scope_confidence: "medium",
|
||||
contains_multiple_tasks: false,
|
||||
fragments: [
|
||||
{
|
||||
fragment_id: "F1",
|
||||
raw_fragment_text: sourceMessage,
|
||||
normalized_fragment_text: "документы по контрагенту выбранному",
|
||||
semantic_hints: {
|
||||
scope_target_kind: "selected_object",
|
||||
scope_target_text: selectedItem,
|
||||
date_scope_kind: "missing",
|
||||
self_scope_detected: false,
|
||||
selected_object_scope_detected: true
|
||||
},
|
||||
domain_relevance: "in_scope",
|
||||
business_scope: "company_specific_accounting",
|
||||
entity_hints: [],
|
||||
account_hints: [],
|
||||
document_hints: [],
|
||||
register_hints: [],
|
||||
time_scope: {
|
||||
type: "missing",
|
||||
value: null,
|
||||
confidence: "low"
|
||||
},
|
||||
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: true,
|
||||
asks_for_evidence: true,
|
||||
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-selected-object-doc-${Date.now()}`,
|
||||
user_message: sourceMessage,
|
||||
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).toBe(sourceMessage);
|
||||
expect([
|
||||
"normalized_fragment_rejected_selected_object_context_loss",
|
||||
"normalized_fragment_rejected_selected_object_counterparty_degradation"
|
||||
]).toContain(response.debug?.llm_decomposition_reason);
|
||||
expect(response.debug?.llm_predecompose_contract?.canonical_message).toBe(sourceMessage);
|
||||
});
|
||||
|
||||
it("accepts exact selected-object sale rewrite when llm candidate stays on the same item", async () => {
|
||||
const calls: Array<{ message: string }> = [];
|
||||
const addressQueryService = {
|
||||
@@ -1437,7 +1546,7 @@ describe("assistant address llm pre-decompose candidate preference", () => {
|
||||
} as any);
|
||||
|
||||
expect(response.ok).toBe(true);
|
||||
expect(response.reply_type).toBe("factual");
|
||||
expect(["factual", "partial_coverage"]).toContain(response.reply_type);
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0].message).toBe("документы по контрагенту свк за 2020 год");
|
||||
expect(response.debug?.llm_decomposition_reason).toBe("fallback_rule_applied_after_llm");
|
||||
|
||||
Reference in New Issue
Block a user