Починить проверку цепочки поставщик товар покупатель
This commit is contained in:
parent
584edb7e5b
commit
325fcf50d1
|
|
@ -1372,6 +1372,8 @@ function extractInventorySupplierAnchor(text) {
|
|||
}
|
||||
const candidate = cleanupAnchorValue(cleanupAnchorValue(String(match[1]))
|
||||
.replace(/\s+(?:сейчас|на\s+склад(?:е|у|ом)?|на\s+дату|по\s+состоянию\s+на\s+дату|котор(?:ый|ые|ых)|куплен(?:ы|а|о)?|были|был|лежат|лежит|еще|ещё|организац\w*|компани\w*).*$/iu, "")
|
||||
.replace(/\s+к\s+покупател\p{L}*[\s\S]*$/iu, "")
|
||||
.replace(/\s+через\s+товар\p{L}*[\s\S]*$/iu, "")
|
||||
.replace(/\s*(?:->|=>|→)\s*(?:товар|позици|номенклатур|item|product|sku)[\s\S]*$/iu, ""));
|
||||
if (!candidate ||
|
||||
isLowQualityCounterpartyAnchorValue(candidate) ||
|
||||
|
|
|
|||
|
|
@ -1576,6 +1576,45 @@ function isQuestionFragmentPartyAnchor(value) {
|
|||
return /(?:^| )(?:что|кто|какие|какой|сколько|сейчас)(?: |$)/iu.test(normalized) ||
|
||||
/(?:на складе|нам должен|кому мы должны|по этой компании|по этой же компании|по ней)/iu.test(normalized);
|
||||
}
|
||||
function extractInventoryDocumentaryChainBuyerAnchor(value) {
|
||||
const source = String(value ?? "");
|
||||
const patterns = [
|
||||
/(?:->|=>|→)\s*(?:покупател\p{L}*|buyer)\s+([^\r\n?]+?)(?=$|[?])/iu,
|
||||
/(?:^|[\s,.;:!?])(?:к\s+покупател\p{L}*|покупател\p{L}*|buyer)\s+(.+?)(?=\s+через\s+(?:товар|item|product|sku)\b|$|[?])/iu
|
||||
];
|
||||
for (const pattern of patterns) {
|
||||
const match = source.match(pattern);
|
||||
const candidate = String(match?.[1] ?? "")
|
||||
.replace(/\s*(?:->|=>|→).+$/u, "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
if (candidate) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function trimInventoryDocumentaryChainSupplierAnchor(value) {
|
||||
const source = String(value ?? "").trim();
|
||||
if (!source) {
|
||||
return null;
|
||||
}
|
||||
const candidate = source
|
||||
.replace(/\s+к\s+покупател\p{L}*[\s\S]*$/iu, "")
|
||||
.replace(/\s+через\s+товар\p{L}*[\s\S]*$/iu, "")
|
||||
.replace(/\s*(?:->|=>|→)\s*(?:товар|позици|номенклатур|item|product|sku)[\s\S]*$/iu, "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
return candidate && candidate !== source ? candidate : null;
|
||||
}
|
||||
function normalizeInventoryChainOrganizationFilterValue(value) {
|
||||
const normalized = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(value);
|
||||
const cleaned = String(normalized ?? "")
|
||||
.replace(/[\\"]/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
return cleaned || null;
|
||||
}
|
||||
function applyPreExecutionOrganizationScopeGrounding(input) {
|
||||
const activeOrganization = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(input.activeOrganization ?? null);
|
||||
const candidateOrganizations = (0, assistantOrganizationMatcher_1.mergeKnownOrganizations)([
|
||||
|
|
@ -2988,14 +3027,65 @@ class AddressQueryService {
|
|||
const knownOrganizations = (0, assistantOrganizationMatcher_1.mergeKnownOrganizations)(options.knownOrganizations ?? []);
|
||||
const activeOrganization = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(options.activeOrganization ?? null);
|
||||
const previousOrganizationFromContext = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(followupContext?.previous_filters?.organization ?? null);
|
||||
const chainCounterpartyAnchor = toNonEmptyFilterValue(filters.extracted_filters.counterparty);
|
||||
const chainOrganizationAnchor = toNonEmptyFilterValue(filters.extracted_filters.organization);
|
||||
let chainCounterpartyAnchor = toNonEmptyFilterValue(filters.extracted_filters.counterparty);
|
||||
let chainOrganizationAnchor = toNonEmptyFilterValue(filters.extracted_filters.organization);
|
||||
if (intent.intent === "inventory_purchase_to_sale_chain") {
|
||||
const trimmedChainCounterparty = trimInventoryDocumentaryChainSupplierAnchor(chainCounterpartyAnchor);
|
||||
if (trimmedChainCounterparty) {
|
||||
filters.extracted_filters.counterparty = trimmedChainCounterparty;
|
||||
chainCounterpartyAnchor = trimmedChainCounterparty;
|
||||
if (!filters.warnings.includes("inventory_chain_supplier_anchor_trimmed_for_verification")) {
|
||||
filters.warnings.push("inventory_chain_supplier_anchor_trimmed_for_verification");
|
||||
}
|
||||
if (!baseReasons.includes("inventory_chain_supplier_anchor_trimmed_for_verification")) {
|
||||
baseReasons.push("inventory_chain_supplier_anchor_trimmed_for_verification");
|
||||
}
|
||||
}
|
||||
const buyerAnchor = extractInventoryDocumentaryChainBuyerAnchor(options.rawUserMessage ?? userMessage);
|
||||
const organizationLooksLikeBuyer = Boolean(buyerAnchor) &&
|
||||
Boolean(chainOrganizationAnchor) &&
|
||||
sameOrganizationEntityReference(chainOrganizationAnchor, buyerAnchor);
|
||||
const trustedOrganization = knownOrganizations.length === 1
|
||||
? knownOrganizations[0]
|
||||
: previousOrganizationFromContext && !sameOrganizationEntityReference(previousOrganizationFromContext, buyerAnchor)
|
||||
? previousOrganizationFromContext
|
||||
: null;
|
||||
if (trustedOrganization &&
|
||||
(!chainOrganizationAnchor || organizationLooksLikeBuyer) &&
|
||||
!sameOrganizationEntityReference(trustedOrganization, chainCounterpartyAnchor)) {
|
||||
const restoredOrganizationForFilter = normalizeInventoryChainOrganizationFilterValue(trustedOrganization) ?? trustedOrganization;
|
||||
filters.extracted_filters.organization = restoredOrganizationForFilter;
|
||||
chainOrganizationAnchor = restoredOrganizationForFilter;
|
||||
const reason = knownOrganizations.length === 1
|
||||
? "organization_restored_from_known_scope_for_inventory_chain"
|
||||
: "organization_restored_from_inventory_chain_context";
|
||||
if (!filters.warnings.includes(reason)) {
|
||||
filters.warnings.push(reason);
|
||||
}
|
||||
if (!baseReasons.includes(reason)) {
|
||||
baseReasons.push(reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (intent.intent === "inventory_purchase_to_sale_chain") {
|
||||
const normalizedChainOrganization = normalizeInventoryChainOrganizationFilterValue(filters.extracted_filters.organization);
|
||||
if (normalizedChainOrganization && normalizedChainOrganization !== filters.extracted_filters.organization) {
|
||||
filters.extracted_filters.organization = normalizedChainOrganization;
|
||||
chainOrganizationAnchor = normalizedChainOrganization;
|
||||
if (!filters.warnings.includes("organization_scope_normalized_for_inventory_chain")) {
|
||||
filters.warnings.push("organization_scope_normalized_for_inventory_chain");
|
||||
}
|
||||
if (!baseReasons.includes("organization_scope_normalized_for_inventory_chain")) {
|
||||
baseReasons.push("organization_scope_normalized_for_inventory_chain");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (intent.intent === "inventory_purchase_to_sale_chain" &&
|
||||
chainCounterpartyAnchor &&
|
||||
chainOrganizationAnchor &&
|
||||
sameOrganizationEntityReference(chainOrganizationAnchor, chainCounterpartyAnchor)) {
|
||||
delete filters.extracted_filters.organization;
|
||||
const restoredOrganization = activeOrganization ?? previousOrganizationFromContext;
|
||||
const restoredOrganization = normalizeInventoryChainOrganizationFilterValue(activeOrganization ?? previousOrganizationFromContext);
|
||||
if (restoredOrganization && !sameOrganizationEntityReference(restoredOrganization, chainCounterpartyAnchor)) {
|
||||
filters.extracted_filters.organization = restoredOrganization;
|
||||
if (!filters.warnings.includes("organization_restored_from_inventory_chain_context")) {
|
||||
|
|
|
|||
|
|
@ -114,7 +114,10 @@ function applyAddressLlmSemanticHintsToExtraction(extraction, semanticHintsInput
|
|||
}
|
||||
semanticFrame.selected_object_scope_detected = true;
|
||||
}
|
||||
if (semanticHints.scope_target_kind === "organization" && scopeTargetText) {
|
||||
const protectsInventoryDocumentaryChainSupplierAnchor = warnings.includes("supplier_anchor_derived_for_inventory_documentary_chain");
|
||||
if (semanticHints.scope_target_kind === "organization" &&
|
||||
scopeTargetText &&
|
||||
!protectsInventoryDocumentaryChainSupplierAnchor) {
|
||||
extractedFilters.organization = scopeTargetText;
|
||||
pushWarning(warnings, "organization_from_llm_semantics");
|
||||
if (toNonEmptyString(extractedFilters.warehouse)) {
|
||||
|
|
@ -125,6 +128,9 @@ function applyAddressLlmSemanticHintsToExtraction(extraction, semanticHintsInput
|
|||
semanticFrame.anchor_kind = "organization";
|
||||
semanticFrame.anchor_value = scopeTargetText;
|
||||
}
|
||||
else if (semanticHints.scope_target_kind === "organization" && scopeTargetText) {
|
||||
pushWarning(warnings, "organization_llm_semantics_ignored_for_inventory_documentary_chain");
|
||||
}
|
||||
if (semanticHints.scope_target_kind === "warehouse" && scopeTargetText) {
|
||||
extractedFilters.warehouse = scopeTargetText;
|
||||
pushWarning(warnings, "warehouse_from_llm_semantics");
|
||||
|
|
|
|||
|
|
@ -1583,6 +1583,8 @@ function extractInventorySupplierAnchor(text: string): string | undefined {
|
|||
/\s+(?:сейчас|на\s+склад(?:е|у|ом)?|на\s+дату|по\s+состоянию\s+на\s+дату|котор(?:ый|ые|ых)|куплен(?:ы|а|о)?|были|был|лежат|лежит|еще|ещё|организац\w*|компани\w*).*$/iu,
|
||||
""
|
||||
)
|
||||
.replace(/\s+к\s+покупател\p{L}*[\s\S]*$/iu, "")
|
||||
.replace(/\s+через\s+товар\p{L}*[\s\S]*$/iu, "")
|
||||
.replace(/\s*(?:->|=>|→)\s*(?:товар|позици|номенклатур|item|product|sku)[\s\S]*$/iu, "")
|
||||
);
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -1939,6 +1939,48 @@ function isQuestionFragmentPartyAnchor(value: string | null | undefined): boolea
|
|||
/(?:на складе|нам должен|кому мы должны|по этой компании|по этой же компании|по ней)/iu.test(normalized);
|
||||
}
|
||||
|
||||
function extractInventoryDocumentaryChainBuyerAnchor(value: string | null | undefined): string | null {
|
||||
const source = String(value ?? "");
|
||||
const patterns = [
|
||||
/(?:->|=>|→)\s*(?:покупател\p{L}*|buyer)\s+([^\r\n?]+?)(?=$|[?])/iu,
|
||||
/(?:^|[\s,.;:!?])(?:к\s+покупател\p{L}*|покупател\p{L}*|buyer)\s+(.+?)(?=\s+через\s+(?:товар|item|product|sku)\b|$|[?])/iu
|
||||
];
|
||||
for (const pattern of patterns) {
|
||||
const match = source.match(pattern);
|
||||
const candidate = String(match?.[1] ?? "")
|
||||
.replace(/\s*(?:->|=>|→).+$/u, "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
if (candidate) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function trimInventoryDocumentaryChainSupplierAnchor(value: string | null | undefined): string | null {
|
||||
const source = String(value ?? "").trim();
|
||||
if (!source) {
|
||||
return null;
|
||||
}
|
||||
const candidate = source
|
||||
.replace(/\s+к\s+покупател\p{L}*[\s\S]*$/iu, "")
|
||||
.replace(/\s+через\s+товар\p{L}*[\s\S]*$/iu, "")
|
||||
.replace(/\s*(?:->|=>|→)\s*(?:товар|позици|номенклатур|item|product|sku)[\s\S]*$/iu, "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
return candidate && candidate !== source ? candidate : null;
|
||||
}
|
||||
|
||||
function normalizeInventoryChainOrganizationFilterValue(value: string | null | undefined): string | null {
|
||||
const normalized = normalizeOrganizationScopeValue(value);
|
||||
const cleaned = String(normalized ?? "")
|
||||
.replace(/[\\"]/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
return cleaned || null;
|
||||
}
|
||||
|
||||
function applyPreExecutionOrganizationScopeGrounding(input: {
|
||||
userMessage: string;
|
||||
filters: AddressFilterSet;
|
||||
|
|
@ -3716,8 +3758,66 @@ export class AddressQueryService {
|
|||
const knownOrganizations = mergeKnownOrganizations(options.knownOrganizations ?? []);
|
||||
const activeOrganization = normalizeOrganizationScopeValue(options.activeOrganization ?? null);
|
||||
const previousOrganizationFromContext = normalizeOrganizationScopeValue(followupContext?.previous_filters?.organization ?? null);
|
||||
const chainCounterpartyAnchor = toNonEmptyFilterValue(filters.extracted_filters.counterparty);
|
||||
const chainOrganizationAnchor = toNonEmptyFilterValue(filters.extracted_filters.organization);
|
||||
let chainCounterpartyAnchor = toNonEmptyFilterValue(filters.extracted_filters.counterparty);
|
||||
let chainOrganizationAnchor = toNonEmptyFilterValue(filters.extracted_filters.organization);
|
||||
if (intent.intent === "inventory_purchase_to_sale_chain") {
|
||||
const trimmedChainCounterparty = trimInventoryDocumentaryChainSupplierAnchor(chainCounterpartyAnchor);
|
||||
if (trimmedChainCounterparty) {
|
||||
filters.extracted_filters.counterparty = trimmedChainCounterparty;
|
||||
chainCounterpartyAnchor = trimmedChainCounterparty;
|
||||
if (!filters.warnings.includes("inventory_chain_supplier_anchor_trimmed_for_verification")) {
|
||||
filters.warnings.push("inventory_chain_supplier_anchor_trimmed_for_verification");
|
||||
}
|
||||
if (!baseReasons.includes("inventory_chain_supplier_anchor_trimmed_for_verification")) {
|
||||
baseReasons.push("inventory_chain_supplier_anchor_trimmed_for_verification");
|
||||
}
|
||||
}
|
||||
const buyerAnchor = extractInventoryDocumentaryChainBuyerAnchor(options.rawUserMessage ?? userMessage);
|
||||
const organizationLooksLikeBuyer =
|
||||
Boolean(buyerAnchor) &&
|
||||
Boolean(chainOrganizationAnchor) &&
|
||||
sameOrganizationEntityReference(chainOrganizationAnchor, buyerAnchor);
|
||||
const trustedOrganization =
|
||||
knownOrganizations.length === 1
|
||||
? knownOrganizations[0]
|
||||
: previousOrganizationFromContext && !sameOrganizationEntityReference(previousOrganizationFromContext, buyerAnchor)
|
||||
? previousOrganizationFromContext
|
||||
: null;
|
||||
if (
|
||||
trustedOrganization &&
|
||||
(!chainOrganizationAnchor || organizationLooksLikeBuyer) &&
|
||||
!sameOrganizationEntityReference(trustedOrganization, chainCounterpartyAnchor)
|
||||
) {
|
||||
const restoredOrganizationForFilter =
|
||||
normalizeInventoryChainOrganizationFilterValue(trustedOrganization) ?? trustedOrganization;
|
||||
filters.extracted_filters.organization = restoredOrganizationForFilter;
|
||||
chainOrganizationAnchor = restoredOrganizationForFilter;
|
||||
const reason = knownOrganizations.length === 1
|
||||
? "organization_restored_from_known_scope_for_inventory_chain"
|
||||
: "organization_restored_from_inventory_chain_context";
|
||||
if (!filters.warnings.includes(reason)) {
|
||||
filters.warnings.push(reason);
|
||||
}
|
||||
if (!baseReasons.includes(reason)) {
|
||||
baseReasons.push(reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (intent.intent === "inventory_purchase_to_sale_chain") {
|
||||
const normalizedChainOrganization = normalizeInventoryChainOrganizationFilterValue(
|
||||
filters.extracted_filters.organization
|
||||
);
|
||||
if (normalizedChainOrganization && normalizedChainOrganization !== filters.extracted_filters.organization) {
|
||||
filters.extracted_filters.organization = normalizedChainOrganization;
|
||||
chainOrganizationAnchor = normalizedChainOrganization;
|
||||
if (!filters.warnings.includes("organization_scope_normalized_for_inventory_chain")) {
|
||||
filters.warnings.push("organization_scope_normalized_for_inventory_chain");
|
||||
}
|
||||
if (!baseReasons.includes("organization_scope_normalized_for_inventory_chain")) {
|
||||
baseReasons.push("organization_scope_normalized_for_inventory_chain");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
intent.intent === "inventory_purchase_to_sale_chain" &&
|
||||
chainCounterpartyAnchor &&
|
||||
|
|
@ -3725,7 +3825,9 @@ export class AddressQueryService {
|
|||
sameOrganizationEntityReference(chainOrganizationAnchor, chainCounterpartyAnchor)
|
||||
) {
|
||||
delete filters.extracted_filters.organization;
|
||||
const restoredOrganization = activeOrganization ?? previousOrganizationFromContext;
|
||||
const restoredOrganization = normalizeInventoryChainOrganizationFilterValue(
|
||||
activeOrganization ?? previousOrganizationFromContext
|
||||
);
|
||||
if (restoredOrganization && !sameOrganizationEntityReference(restoredOrganization, chainCounterpartyAnchor)) {
|
||||
filters.extracted_filters.organization = restoredOrganization;
|
||||
if (!filters.warnings.includes("organization_restored_from_inventory_chain_context")) {
|
||||
|
|
|
|||
|
|
@ -145,7 +145,15 @@ export function applyAddressLlmSemanticHintsToExtraction(
|
|||
semanticFrame.selected_object_scope_detected = true;
|
||||
}
|
||||
|
||||
if (semanticHints.scope_target_kind === "organization" && scopeTargetText) {
|
||||
const protectsInventoryDocumentaryChainSupplierAnchor = warnings.includes(
|
||||
"supplier_anchor_derived_for_inventory_documentary_chain"
|
||||
);
|
||||
|
||||
if (
|
||||
semanticHints.scope_target_kind === "organization" &&
|
||||
scopeTargetText &&
|
||||
!protectsInventoryDocumentaryChainSupplierAnchor
|
||||
) {
|
||||
extractedFilters.organization = scopeTargetText;
|
||||
pushWarning(warnings, "organization_from_llm_semantics");
|
||||
if (toNonEmptyString(extractedFilters.warehouse)) {
|
||||
|
|
@ -155,6 +163,8 @@ export function applyAddressLlmSemanticHintsToExtraction(
|
|||
semanticFrame.scope_kind = "explicit_anchor";
|
||||
semanticFrame.anchor_kind = "organization";
|
||||
semanticFrame.anchor_value = scopeTargetText;
|
||||
} else if (semanticHints.scope_target_kind === "organization" && scopeTargetText) {
|
||||
pushWarning(warnings, "organization_llm_semantics_ignored_for_inventory_documentary_chain");
|
||||
}
|
||||
|
||||
if (semanticHints.scope_target_kind === "warehouse" && scopeTargetText) {
|
||||
|
|
|
|||
|
|
@ -1147,12 +1147,84 @@ describe("inventory selected-object follow-up", () => {
|
|||
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("organization_scope_normalized_for_inventory_chain");
|
||||
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("restores known organization when live chain semantics misclassify buyer as active organization", 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: "Департамент капитального ремонта города Москвы",
|
||||
knownOrganizations: ["ООО \"Альтернатива Плюс\""],
|
||||
analysisDateHint: "2020-03-31",
|
||||
llmSemanticHints: {
|
||||
scope_target_kind: "organization",
|
||||
scope_target_text: "Департамент капитального ремонта города Москвы",
|
||||
date_scope_kind: "missing",
|
||||
self_scope_detected: false,
|
||||
selected_object_scope_detected: false
|
||||
},
|
||||
followupContext: {
|
||||
previous_intent: "inventory_purchase_to_sale_chain",
|
||||
previous_filters: {
|
||||
as_of_date: "2020-03-31",
|
||||
item: "Шкаф картотечный 1000*400*2100",
|
||||
organization: "ООО \"Альтернатива Плюс\""
|
||||
},
|
||||
previous_anchor_type: "item",
|
||||
previous_anchor_value: "Шкаф картотечный 1000*400*2100"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
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?.counterparty).toBe("Гамма-мебель, ООО");
|
||||
expect(result?.debug.extracted_filters?.organization).toBe("ООО Альтернатива Плюс");
|
||||
expect(result?.debug.reasons).toContain("organization_scope_normalized_for_inventory_chain");
|
||||
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 ?? "").split("\n")[0]).toContain("Гамма-мебель, ООО");
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -186,6 +186,35 @@ describe("address query shape classifier", () => {
|
|||
expect(extraction.warnings).toContain("supplier_anchor_derived_for_inventory_documentary_chain");
|
||||
});
|
||||
|
||||
it("extracts supplier anchor from normalized supplier-buyer chain without swallowing buyer and item 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("ignores buyer-as-organization llm semantics for supplier-item-buyer inventory chain", () => {
|
||||
const result = runAddressDecomposeStage(
|
||||
"Проверить наличие документально подтвержденной цепочки поставки от поставщика Гамма-мебель, ООО к покупателю Департамент капитального ремонта города Москвы через товар Шкаф картотечный 1000*400*2100",
|
||||
null,
|
||||
{
|
||||
scope_target_kind: "organization",
|
||||
scope_target_text: "Департамент капитального ремонта города Москвы",
|
||||
date_scope_kind: "missing",
|
||||
self_scope_detected: false,
|
||||
selected_object_scope_detected: false
|
||||
}
|
||||
);
|
||||
|
||||
expect(result?.filters.extracted_filters.counterparty).toBe("Гамма-мебель, ООО");
|
||||
expect(result?.filters.extracted_filters.organization).toBeUndefined();
|
||||
expect(result?.filters.warnings).toContain("organization_llm_semantics_ignored_for_inventory_documentary_chain");
|
||||
});
|
||||
|
||||
it("cuts inventory item anchor before purchase-doc residue tail", () => {
|
||||
const filters = extractAddressFilters(
|
||||
"По каким документам был куплен товар Диван трехместный для остатка на складе Основной склад",
|
||||
|
|
|
|||
Loading…
Reference in New Issue