Починить проверку цепочки поставщик товар покупатель
This commit is contained in:
parent
584edb7e5b
commit
325fcf50d1
|
|
@ -1372,6 +1372,8 @@ function extractInventorySupplierAnchor(text) {
|
||||||
}
|
}
|
||||||
const candidate = cleanupAnchorValue(cleanupAnchorValue(String(match[1]))
|
const candidate = cleanupAnchorValue(cleanupAnchorValue(String(match[1]))
|
||||||
.replace(/\s+(?:сейчас|на\s+склад(?:е|у|ом)?|на\s+дату|по\s+состоянию\s+на\s+дату|котор(?:ый|ые|ых)|куплен(?:ы|а|о)?|были|был|лежат|лежит|еще|ещё|организац\w*|компани\w*).*$/iu, "")
|
.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, ""));
|
.replace(/\s*(?:->|=>|→)\s*(?:товар|позици|номенклатур|item|product|sku)[\s\S]*$/iu, ""));
|
||||||
if (!candidate ||
|
if (!candidate ||
|
||||||
isLowQualityCounterpartyAnchorValue(candidate) ||
|
isLowQualityCounterpartyAnchorValue(candidate) ||
|
||||||
|
|
|
||||||
|
|
@ -1576,6 +1576,45 @@ function isQuestionFragmentPartyAnchor(value) {
|
||||||
return /(?:^| )(?:что|кто|какие|какой|сколько|сейчас)(?: |$)/iu.test(normalized) ||
|
return /(?:^| )(?:что|кто|какие|какой|сколько|сейчас)(?: |$)/iu.test(normalized) ||
|
||||||
/(?:на складе|нам должен|кому мы должны|по этой компании|по этой же компании|по ней)/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) {
|
function applyPreExecutionOrganizationScopeGrounding(input) {
|
||||||
const activeOrganization = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(input.activeOrganization ?? null);
|
const activeOrganization = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(input.activeOrganization ?? null);
|
||||||
const candidateOrganizations = (0, assistantOrganizationMatcher_1.mergeKnownOrganizations)([
|
const candidateOrganizations = (0, assistantOrganizationMatcher_1.mergeKnownOrganizations)([
|
||||||
|
|
@ -2988,14 +3027,65 @@ class AddressQueryService {
|
||||||
const knownOrganizations = (0, assistantOrganizationMatcher_1.mergeKnownOrganizations)(options.knownOrganizations ?? []);
|
const knownOrganizations = (0, assistantOrganizationMatcher_1.mergeKnownOrganizations)(options.knownOrganizations ?? []);
|
||||||
const activeOrganization = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(options.activeOrganization ?? null);
|
const activeOrganization = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(options.activeOrganization ?? null);
|
||||||
const previousOrganizationFromContext = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(followupContext?.previous_filters?.organization ?? null);
|
const previousOrganizationFromContext = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(followupContext?.previous_filters?.organization ?? null);
|
||||||
const chainCounterpartyAnchor = toNonEmptyFilterValue(filters.extracted_filters.counterparty);
|
let chainCounterpartyAnchor = toNonEmptyFilterValue(filters.extracted_filters.counterparty);
|
||||||
const chainOrganizationAnchor = toNonEmptyFilterValue(filters.extracted_filters.organization);
|
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" &&
|
if (intent.intent === "inventory_purchase_to_sale_chain" &&
|
||||||
chainCounterpartyAnchor &&
|
chainCounterpartyAnchor &&
|
||||||
chainOrganizationAnchor &&
|
chainOrganizationAnchor &&
|
||||||
sameOrganizationEntityReference(chainOrganizationAnchor, chainCounterpartyAnchor)) {
|
sameOrganizationEntityReference(chainOrganizationAnchor, chainCounterpartyAnchor)) {
|
||||||
delete filters.extracted_filters.organization;
|
delete filters.extracted_filters.organization;
|
||||||
const restoredOrganization = activeOrganization ?? previousOrganizationFromContext;
|
const restoredOrganization = normalizeInventoryChainOrganizationFilterValue(activeOrganization ?? previousOrganizationFromContext);
|
||||||
if (restoredOrganization && !sameOrganizationEntityReference(restoredOrganization, chainCounterpartyAnchor)) {
|
if (restoredOrganization && !sameOrganizationEntityReference(restoredOrganization, chainCounterpartyAnchor)) {
|
||||||
filters.extracted_filters.organization = restoredOrganization;
|
filters.extracted_filters.organization = restoredOrganization;
|
||||||
if (!filters.warnings.includes("organization_restored_from_inventory_chain_context")) {
|
if (!filters.warnings.includes("organization_restored_from_inventory_chain_context")) {
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,10 @@ function applyAddressLlmSemanticHintsToExtraction(extraction, semanticHintsInput
|
||||||
}
|
}
|
||||||
semanticFrame.selected_object_scope_detected = true;
|
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;
|
extractedFilters.organization = scopeTargetText;
|
||||||
pushWarning(warnings, "organization_from_llm_semantics");
|
pushWarning(warnings, "organization_from_llm_semantics");
|
||||||
if (toNonEmptyString(extractedFilters.warehouse)) {
|
if (toNonEmptyString(extractedFilters.warehouse)) {
|
||||||
|
|
@ -125,6 +128,9 @@ function applyAddressLlmSemanticHintsToExtraction(extraction, semanticHintsInput
|
||||||
semanticFrame.anchor_kind = "organization";
|
semanticFrame.anchor_kind = "organization";
|
||||||
semanticFrame.anchor_value = scopeTargetText;
|
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) {
|
if (semanticHints.scope_target_kind === "warehouse" && scopeTargetText) {
|
||||||
extractedFilters.warehouse = scopeTargetText;
|
extractedFilters.warehouse = scopeTargetText;
|
||||||
pushWarning(warnings, "warehouse_from_llm_semantics");
|
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,
|
/\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, "")
|
.replace(/\s*(?:->|=>|→)\s*(?:товар|позици|номенклатур|item|product|sku)[\s\S]*$/iu, "")
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
|
|
|
||||||
|
|
@ -1939,6 +1939,48 @@ function isQuestionFragmentPartyAnchor(value: string | null | undefined): boolea
|
||||||
/(?:на складе|нам должен|кому мы должны|по этой компании|по этой же компании|по ней)/iu.test(normalized);
|
/(?:на складе|нам должен|кому мы должны|по этой компании|по этой же компании|по ней)/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: {
|
function applyPreExecutionOrganizationScopeGrounding(input: {
|
||||||
userMessage: string;
|
userMessage: string;
|
||||||
filters: AddressFilterSet;
|
filters: AddressFilterSet;
|
||||||
|
|
@ -3716,8 +3758,66 @@ export class AddressQueryService {
|
||||||
const knownOrganizations = mergeKnownOrganizations(options.knownOrganizations ?? []);
|
const knownOrganizations = mergeKnownOrganizations(options.knownOrganizations ?? []);
|
||||||
const activeOrganization = normalizeOrganizationScopeValue(options.activeOrganization ?? null);
|
const activeOrganization = normalizeOrganizationScopeValue(options.activeOrganization ?? null);
|
||||||
const previousOrganizationFromContext = normalizeOrganizationScopeValue(followupContext?.previous_filters?.organization ?? null);
|
const previousOrganizationFromContext = normalizeOrganizationScopeValue(followupContext?.previous_filters?.organization ?? null);
|
||||||
const chainCounterpartyAnchor = toNonEmptyFilterValue(filters.extracted_filters.counterparty);
|
let chainCounterpartyAnchor = toNonEmptyFilterValue(filters.extracted_filters.counterparty);
|
||||||
const chainOrganizationAnchor = toNonEmptyFilterValue(filters.extracted_filters.organization);
|
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 (
|
if (
|
||||||
intent.intent === "inventory_purchase_to_sale_chain" &&
|
intent.intent === "inventory_purchase_to_sale_chain" &&
|
||||||
chainCounterpartyAnchor &&
|
chainCounterpartyAnchor &&
|
||||||
|
|
@ -3725,7 +3825,9 @@ export class AddressQueryService {
|
||||||
sameOrganizationEntityReference(chainOrganizationAnchor, chainCounterpartyAnchor)
|
sameOrganizationEntityReference(chainOrganizationAnchor, chainCounterpartyAnchor)
|
||||||
) {
|
) {
|
||||||
delete filters.extracted_filters.organization;
|
delete filters.extracted_filters.organization;
|
||||||
const restoredOrganization = activeOrganization ?? previousOrganizationFromContext;
|
const restoredOrganization = normalizeInventoryChainOrganizationFilterValue(
|
||||||
|
activeOrganization ?? previousOrganizationFromContext
|
||||||
|
);
|
||||||
if (restoredOrganization && !sameOrganizationEntityReference(restoredOrganization, chainCounterpartyAnchor)) {
|
if (restoredOrganization && !sameOrganizationEntityReference(restoredOrganization, chainCounterpartyAnchor)) {
|
||||||
filters.extracted_filters.organization = restoredOrganization;
|
filters.extracted_filters.organization = restoredOrganization;
|
||||||
if (!filters.warnings.includes("organization_restored_from_inventory_chain_context")) {
|
if (!filters.warnings.includes("organization_restored_from_inventory_chain_context")) {
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,15 @@ export function applyAddressLlmSemanticHintsToExtraction(
|
||||||
semanticFrame.selected_object_scope_detected = true;
|
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;
|
extractedFilters.organization = scopeTargetText;
|
||||||
pushWarning(warnings, "organization_from_llm_semantics");
|
pushWarning(warnings, "organization_from_llm_semantics");
|
||||||
if (toNonEmptyString(extractedFilters.warehouse)) {
|
if (toNonEmptyString(extractedFilters.warehouse)) {
|
||||||
|
|
@ -155,6 +163,8 @@ export function applyAddressLlmSemanticHintsToExtraction(
|
||||||
semanticFrame.scope_kind = "explicit_anchor";
|
semanticFrame.scope_kind = "explicit_anchor";
|
||||||
semanticFrame.anchor_kind = "organization";
|
semanticFrame.anchor_kind = "organization";
|
||||||
semanticFrame.anchor_value = scopeTargetText;
|
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) {
|
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?.response_type).toBe("FACTUAL_SUMMARY");
|
||||||
expect(result?.debug.detected_intent).toBe("inventory_purchase_to_sale_chain");
|
expect(result?.debug.detected_intent).toBe("inventory_purchase_to_sale_chain");
|
||||||
expect(result?.debug.extracted_filters?.organization).toBe("ООО Альтернатива Плюс");
|
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(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 ?? "")).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 () => {
|
it.skip("keeps the full selected item when sale trace is asked in canonical wording after provenance", async () => {
|
||||||
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||||
fetched_rows: 1,
|
fetched_rows: 1,
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,35 @@ describe("address query shape classifier", () => {
|
||||||
expect(extraction.warnings).toContain("supplier_anchor_derived_for_inventory_documentary_chain");
|
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", () => {
|
it("cuts inventory item anchor before purchase-doc residue tail", () => {
|
||||||
const filters = extractAddressFilters(
|
const filters = extractAddressFilters(
|
||||||
"По каким документам был куплен товар Диван трехместный для остатка на складе Основной склад",
|
"По каким документам был куплен товар Диван трехместный для остатка на складе Основной склад",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue