Inventory breadth: закрепить stock provenance и sale-trace контуры
This commit is contained in:
parent
87c440d6fb
commit
b12f370784
|
|
@ -771,9 +771,7 @@
|
||||||
},
|
},
|
||||||
"expected_result_mode": "confirmed_balance",
|
"expected_result_mode": "confirmed_balance",
|
||||||
"required_filters": {
|
"required_filters": {
|
||||||
"as_of_date": "2021-09-30",
|
"as_of_date": "2021-09-30"
|
||||||
"period_from": "2021-09-01",
|
|
||||||
"period_to": "2021-09-30"
|
|
||||||
},
|
},
|
||||||
"invariant_severity": {
|
"invariant_severity": {
|
||||||
"wrong_as_of_date": "P0",
|
"wrong_as_of_date": "P0",
|
||||||
|
|
@ -794,15 +792,11 @@
|
||||||
"source": "binding_target_date_historical"
|
"source": "binding_target_date_historical"
|
||||||
},
|
},
|
||||||
"expected_capability": "confirmed_inventory_on_hand_as_of_date",
|
"expected_capability": "confirmed_inventory_on_hand_as_of_date",
|
||||||
"analysis_context": {
|
|
||||||
"as_of_date": "2021-09-30",
|
|
||||||
"source": "binding_target_date_current"
|
|
||||||
},
|
|
||||||
"expected_result_mode": "confirmed_balance",
|
"expected_result_mode": "confirmed_balance",
|
||||||
"required_filters": {
|
"required_filters": {
|
||||||
"as_of_date": "2021-09-30",
|
"as_of_date": "2019-03-31",
|
||||||
"period_from": "2021-09-01",
|
"period_from": "2019-03-01",
|
||||||
"period_to": "2021-09-30"
|
"period_to": "2019-03-31"
|
||||||
},
|
},
|
||||||
"invariant_severity": {
|
"invariant_severity": {
|
||||||
"wrong_as_of_date": "P0",
|
"wrong_as_of_date": "P0",
|
||||||
|
|
@ -844,8 +838,7 @@
|
||||||
},
|
},
|
||||||
"required_filters": {
|
"required_filters": {
|
||||||
"as_of_date": "2021-09-30",
|
"as_of_date": "2021-09-30",
|
||||||
"period_from": "2021-09-01",
|
"account": "41"
|
||||||
"period_to": "2021-09-30"
|
|
||||||
},
|
},
|
||||||
"invariant_severity": {
|
"invariant_severity": {
|
||||||
"wrong_as_of_date": "P0",
|
"wrong_as_of_date": "P0",
|
||||||
|
|
@ -1074,9 +1067,7 @@
|
||||||
"organization_scope"
|
"organization_scope"
|
||||||
],
|
],
|
||||||
"required_filters": {
|
"required_filters": {
|
||||||
"as_of_date": "2019-03-31",
|
"as_of_date": "2019-03-31"
|
||||||
"period_from": "2019-03-01",
|
|
||||||
"period_to": "2019-03-31"
|
|
||||||
},
|
},
|
||||||
"invariant_severity": {
|
"invariant_severity": {
|
||||||
"wrong_as_of_date": "P0",
|
"wrong_as_of_date": "P0",
|
||||||
|
|
@ -1222,9 +1213,24 @@
|
||||||
"step_01_snapshot_historical",
|
"step_01_snapshot_historical",
|
||||||
"step_02_selected_item_supplier_ui"
|
"step_02_selected_item_supplier_ui"
|
||||||
],
|
],
|
||||||
|
"analysis_context": {
|
||||||
|
"as_of_date": "2019-03-31",
|
||||||
|
"source": "binding_target_date_historical"
|
||||||
|
},
|
||||||
|
"expected_capability": "inventory_purchase_provenance_for_item",
|
||||||
"required_state_objects": [
|
"required_state_objects": [
|
||||||
"focus_object"
|
"focus_object"
|
||||||
],
|
],
|
||||||
|
"required_filters": {
|
||||||
|
"as_of_date": "2019-03-31"
|
||||||
|
},
|
||||||
|
"required_carryover_invariants": [
|
||||||
|
"selected_object",
|
||||||
|
"focus_object",
|
||||||
|
"date_scope",
|
||||||
|
"reusable_bundle",
|
||||||
|
"followup_action_resolution"
|
||||||
|
],
|
||||||
"forbidden_capabilities": [
|
"forbidden_capabilities": [
|
||||||
"confirmed_inventory_on_hand_as_of_date"
|
"confirmed_inventory_on_hand_as_of_date"
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,10 @@ function isConfirmedBalanceIntent(intent) {
|
||||||
intent === "vat_liability_confirmed_for_tax_period");
|
intent === "vat_liability_confirmed_for_tax_period");
|
||||||
}
|
}
|
||||||
function resolveAddressAsOfDateBasis(filters, semanticFrame) {
|
function resolveAddressAsOfDateBasis(filters, semanticFrame) {
|
||||||
|
if (semanticFrame?.date_scope_kind === "implicit_current" &&
|
||||||
|
semanticFrame.date_basis_hint === "implicit_current_snapshot") {
|
||||||
|
return "implicit_current_snapshot";
|
||||||
|
}
|
||||||
const asOfDate = normalizeIsoDateHint(filters.as_of_date);
|
const asOfDate = normalizeIsoDateHint(filters.as_of_date);
|
||||||
if (asOfDate) {
|
if (asOfDate) {
|
||||||
return "explicit_as_of_date";
|
return "explicit_as_of_date";
|
||||||
|
|
|
||||||
|
|
@ -166,8 +166,11 @@ function toIsoDate(year, month, day) {
|
||||||
}
|
}
|
||||||
return `${String(year).padStart(4, "0")}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
return `${String(year).padStart(4, "0")}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
||||||
}
|
}
|
||||||
|
function hasImplicitCurrentAsOfDateCue(text) {
|
||||||
|
return /\b(сегодня|на\s+сегодня|на\s+текущ(?:ую|ая|ий|ее|ей|ем|его)\s+дат(?:у|а|е|ой|ою)|на\s+сегодняшн(?:юю|ий|ей|ем|его)\s+дат(?:у|а|е|ой|ою)|на\s+текущ(?:ий|ую)\s+момент|today|as\s+of\s+today|current\s+date|as\s+of\s+current\s+date)\b/i.test(text);
|
||||||
|
}
|
||||||
function extractAsOfDate(text) {
|
function extractAsOfDate(text) {
|
||||||
if (/\b(сегодня|на\s+сегодня|на\s+текущ(?:ую|ая|ий|ее|ей|ем|его)\s+дат(?:у|а|е|ой|ою)|на\s+сегодняшн(?:юю|ий|ей|ем|его)\s+дат(?:у|а|е|ой|ою)|на\s+текущ(?:ий|ую)\s+момент|today|as\s+of\s+today|current\s+date|as\s+of\s+current\s+date)\b/i.test(text)) {
|
if (hasImplicitCurrentAsOfDateCue(text)) {
|
||||||
return new Date().toISOString().slice(0, 10);
|
return new Date().toISOString().slice(0, 10);
|
||||||
}
|
}
|
||||||
const ymd = text.match(DATE_YMD_PATTERN);
|
const ymd = text.match(DATE_YMD_PATTERN);
|
||||||
|
|
@ -657,6 +660,8 @@ function isLowQualityCounterpartyAnchorValue(rawValue) {
|
||||||
"ноябрь",
|
"ноябрь",
|
||||||
"декабрь"
|
"декабрь"
|
||||||
]);
|
]);
|
||||||
|
const isLowQualityTimeToken = (token) => lowQualityTimeTokens.has(token) ||
|
||||||
|
/^(?:январ|феврал|март|апрел|ма(?:й|я|е)|июн|июл|август|сентябр|октябр|ноябр|декабр)/iu.test(token);
|
||||||
const lowQualityGenericTokens = new Set([
|
const lowQualityGenericTokens = new Set([
|
||||||
"деньги",
|
"деньги",
|
||||||
"денег",
|
"денег",
|
||||||
|
|
@ -680,13 +685,13 @@ function isLowQualityCounterpartyAnchorValue(rawValue) {
|
||||||
"целом"
|
"целом"
|
||||||
]);
|
]);
|
||||||
const meaningfulNonTemporalTokens = tokens.filter((token) => isLikelyCounterpartyToken(token) &&
|
const meaningfulNonTemporalTokens = tokens.filter((token) => isLikelyCounterpartyToken(token) &&
|
||||||
!lowQualityTimeTokens.has(token) &&
|
!isLowQualityTimeToken(token) &&
|
||||||
!/^(?:19|20)\d{2}$/.test(token));
|
!/^(?:19|20)\d{2}$/.test(token));
|
||||||
if (meaningfulNonTemporalTokens.length === 0 && hasTemporalCue) {
|
if (meaningfulNonTemporalTokens.length === 0 && hasTemporalCue) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const meaningfulNonGenericTokens = tokens.filter((token) => isLikelyCounterpartyToken(token) &&
|
const meaningfulNonGenericTokens = tokens.filter((token) => isLikelyCounterpartyToken(token) &&
|
||||||
!lowQualityTimeTokens.has(token) &&
|
!isLowQualityTimeToken(token) &&
|
||||||
!lowQualityGenericTokens.has(token) &&
|
!lowQualityGenericTokens.has(token) &&
|
||||||
!/^(?:19|20)\d{2}$/.test(token));
|
!/^(?:19|20)\d{2}$/.test(token));
|
||||||
if (meaningfulNonGenericTokens.length === 0 && (hasTemporalCue || paymentCue)) {
|
if (meaningfulNonGenericTokens.length === 0 && (hasTemporalCue || paymentCue)) {
|
||||||
|
|
@ -1133,6 +1138,9 @@ function isTemporalWarehousePhrase(candidate) {
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/ё/g, "е")
|
.replace(/ё/g, "е")
|
||||||
.trim();
|
.trim();
|
||||||
|
if (/^(?:в|на)?\s*(?:сейчас|сегодня|текущ(?:ий|ую|ем|его)\s+момент|данн(?:ый|ую|ом|ого)\s+момент)$/iu.test(normalized)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (/^(?:на\s+)?(?:эту|ту|текущ(?:ую|ая|ий|ее|ей)|сегодняшн(?:юю|ая|ий|ее|ей))(?:\s+же)?\s+дат(?:у|а|е|ой)$/iu.test(normalized)) {
|
if (/^(?:на\s+)?(?:эту|ту|текущ(?:ую|ая|ий|ее|ей)|сегодняшн(?:юю|ая|ий|ее|ей))(?:\s+же)?\s+дат(?:у|а|е|ой)$/iu.test(normalized)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -1177,6 +1185,12 @@ function isLowQualityWarehouseAnchorValue(rawValue) {
|
||||||
"лежали",
|
"лежали",
|
||||||
"на",
|
"на",
|
||||||
"по",
|
"по",
|
||||||
|
"компания",
|
||||||
|
"компании",
|
||||||
|
"компанию",
|
||||||
|
"организация",
|
||||||
|
"организации",
|
||||||
|
"организацию",
|
||||||
"складе",
|
"складе",
|
||||||
"складу",
|
"складу",
|
||||||
"складом",
|
"складом",
|
||||||
|
|
@ -1195,7 +1209,10 @@ function isLowQualityWarehouseAnchorValue(rawValue) {
|
||||||
if (tokens.length === 0) {
|
if (tokens.length === 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const meaningfulTokens = tokens.filter((token) => !lowQualityTokens.has(token) && token.length > 1);
|
const isLowQualityWarehouseToken = (token) => lowQualityTokens.has(token) ||
|
||||||
|
/^(?:19|20)\d{2}$/.test(token) ||
|
||||||
|
/^(?:январ|феврал|март|апрел|ма(?:й|я|е)|июн|июл|август|сентябр|октябр|ноябр|декабр)/iu.test(token);
|
||||||
|
const meaningfulTokens = tokens.filter((token) => !isLowQualityWarehouseToken(token) && token.length > 1);
|
||||||
if (meaningfulTokens.length === 0) {
|
if (meaningfulTokens.length === 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -1256,11 +1273,13 @@ function extractInventoryWarehouseAnchor(text) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
function extractInventorySupplierAnchor(text) {
|
function extractInventorySupplierAnchor(text) {
|
||||||
const match = String(text ?? "").match(/(?:от\s+поставщика|у\s+поставщика|поставщика|поставщику)\s+([^\r\n?]+?)(?=$|[?])/iu);
|
const match = String(text ?? "").match(/(?:от\s+поставщика|у\s+поставщика|поставщик(?:а|у|ом)?|supplier|vendor)\s+([^\r\n?]+?)(?=$|[?])/iu);
|
||||||
if (!match?.[1]) {
|
if (!match?.[1]) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const candidate = cleanupAnchorValue(cleanupAnchorValue(String(match[1])).replace(/\s+(?:сейчас|на\s+склад(?:е|у|ом)?|на\s+дату|по\s+состоянию\s+на\s+дату|котор(?:ый|ые|ых)|куплен(?:ы|а|о)?|были|был|лежат|лежит|еще|ещё|организац\w*|компани\w*).*$/iu, ""));
|
const candidate = cleanupAnchorValue(cleanupAnchorValue(String(match[1]))
|
||||||
|
.replace(/\s+(?:сейчас|на\s+склад(?:е|у|ом)?|на\s+дату|по\s+состоянию\s+на\s+дату|котор(?:ый|ые|ых)|куплен(?:ы|а|о)?|были|был|лежат|лежит|еще|ещё|организац\w*|компани\w*).*$/iu, "")
|
||||||
|
.replace(/\s*(?:->|=>|→)\s*(?:товар|позици|номенклатур|item|product|sku)[\s\S]*$/iu, ""));
|
||||||
if (!candidate ||
|
if (!candidate ||
|
||||||
isLowQualityCounterpartyAnchorValue(candidate) ||
|
isLowQualityCounterpartyAnchorValue(candidate) ||
|
||||||
/^(?:были|был|куплен|куплены|которые|который|которых|сейчас|лежат|лежит)\b/iu.test(candidate)) {
|
/^(?:были|был|куплен|куплены|которые|который|которых|сейчас|лежат|лежит)\b/iu.test(candidate)) {
|
||||||
|
|
@ -1369,7 +1388,7 @@ function resolveSemanticDateScopeKind(filters, warnings) {
|
||||||
return "none";
|
return "none";
|
||||||
}
|
}
|
||||||
function resolveSemanticDateBasisHint(filters, warnings) {
|
function resolveSemanticDateBasisHint(filters, warnings) {
|
||||||
if (warnings.includes("as_of_date_defaulted_today")) {
|
if (warnings.includes("as_of_date_defaulted_today") || warnings.includes("as_of_date_from_implicit_current_phrase")) {
|
||||||
return "implicit_current_snapshot";
|
return "implicit_current_snapshot";
|
||||||
}
|
}
|
||||||
const hasAsOfDate = typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0;
|
const hasAsOfDate = typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0;
|
||||||
|
|
@ -1470,6 +1489,7 @@ function extractAddressFilters(userMessage, intent) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const warnings = [];
|
const warnings = [];
|
||||||
|
const implicitCurrentAsOfDateCue = hasImplicitCurrentAsOfDateCue(text);
|
||||||
const explicitAsOfDate = extractAsOfDate(text);
|
const explicitAsOfDate = extractAsOfDate(text);
|
||||||
const explicitAsOfDateWithCue = extractAsOfDateWithCue(text);
|
const explicitAsOfDateWithCue = extractAsOfDateWithCue(text);
|
||||||
const accountMatch = text.match(ACCOUNT_REVERSE_PATTERN) ?? text.match(ACCOUNT_PATTERN);
|
const accountMatch = text.match(ACCOUNT_REVERSE_PATTERN) ?? text.match(ACCOUNT_PATTERN);
|
||||||
|
|
@ -1510,6 +1530,13 @@ function extractAddressFilters(userMessage, intent) {
|
||||||
filters.counterparty = supplierAnchor;
|
filters.counterparty = supplierAnchor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (intent === "inventory_purchase_to_sale_chain") {
|
||||||
|
const supplierAnchor = asksForInventorySupplierIdentity(text) ? undefined : extractInventorySupplierAnchor(text);
|
||||||
|
if (supplierAnchor) {
|
||||||
|
filters.counterparty = supplierAnchor;
|
||||||
|
warnings.push("supplier_anchor_derived_for_inventory_documentary_chain");
|
||||||
|
}
|
||||||
|
}
|
||||||
const allowGenericCounterpartyAnchor = !isInventoryTraceIntent(intent);
|
const allowGenericCounterpartyAnchor = !isInventoryTraceIntent(intent);
|
||||||
const counterpartyMatch = allowGenericCounterpartyAnchor ? text.match(COUNTERPARTY_PATTERN) : null;
|
const counterpartyMatch = allowGenericCounterpartyAnchor ? text.match(COUNTERPARTY_PATTERN) : null;
|
||||||
if (counterpartyMatch && !filters.counterparty) {
|
if (counterpartyMatch && !filters.counterparty) {
|
||||||
|
|
@ -1655,6 +1682,9 @@ function extractAddressFilters(userMessage, intent) {
|
||||||
}
|
}
|
||||||
if (usesAsOfPrimaryWindow(intent) && explicitAsOfDate) {
|
if (usesAsOfPrimaryWindow(intent) && explicitAsOfDate) {
|
||||||
filters.as_of_date = explicitAsOfDate;
|
filters.as_of_date = explicitAsOfDate;
|
||||||
|
if (implicitCurrentAsOfDateCue && !warnings.includes("as_of_date_from_implicit_current_phrase")) {
|
||||||
|
warnings.push("as_of_date_from_implicit_current_phrase");
|
||||||
|
}
|
||||||
const periodWasDerivedHeuristically = warnings.includes("period_derived_from_month_phrase") ||
|
const periodWasDerivedHeuristically = warnings.includes("period_derived_from_month_phrase") ||
|
||||||
warnings.includes("period_derived_from_year_range_phrase") ||
|
warnings.includes("period_derived_from_year_range_phrase") ||
|
||||||
warnings.includes("period_derived_from_year_phrase");
|
warnings.includes("period_derived_from_year_phrase");
|
||||||
|
|
@ -1670,6 +1700,9 @@ function extractAddressFilters(userMessage, intent) {
|
||||||
const asOfDate = extractAsOfDate(text);
|
const asOfDate = extractAsOfDate(text);
|
||||||
if (asOfDate) {
|
if (asOfDate) {
|
||||||
filters.as_of_date = asOfDate;
|
filters.as_of_date = asOfDate;
|
||||||
|
if (implicitCurrentAsOfDateCue && !warnings.includes("as_of_date_from_implicit_current_phrase")) {
|
||||||
|
warnings.push("as_of_date_from_implicit_current_phrase");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// For counterparty document/bank lists we keep period open by default (all-time over available data)
|
// For counterparty document/bank lists we keep period open by default (all-time over available data)
|
||||||
|
|
|
||||||
|
|
@ -1416,6 +1416,12 @@ function hasInventoryPurchaseDocumentsSignalV2(text) {
|
||||||
return hasItemCue && hasPurchaseDocCue;
|
return hasItemCue && hasPurchaseDocCue;
|
||||||
}
|
}
|
||||||
function hasInventorySaleTraceSignalV2(text) {
|
function hasInventorySaleTraceSignalV2(text) {
|
||||||
|
const value = String(text ?? "");
|
||||||
|
const hasPlainItemCue = /(?:\u0442\u043e\u0432\u0430\u0440|\u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442\u0443\u0440|\u043f\u043e\u0437\u0438\u0446|\u043f\u0440\u043e\u0434\u0443\u043a\u0446\u0438|sku|item|product)/iu.test(value);
|
||||||
|
const hasPlainTraceCue = /(?:\u043a\u043e\u043c\u0443\s+(?:\u0432\s+\u0438\u0442\u043e\u0433\u0435\s+)?(?:\u043c\u044b\s+)?(?:\u043f\u0440\u043e\u0434\u0430\u043b\u0438|\u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043b\u0438|\u0432\u043f\u0430\u0440\u0438\u043b\u0438)|\u043a\u043e\u043c\u0443\s+(?:\u0431\u044b\u043b[\u0430\u0438\u043e]?|\u0431\u044b\u043b\u0438)?\s*(?:\u043f\u0440\u043e\u0434\u0430\u043d|\u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d)|\u043a\u0442\u043e\s+\u043a\u0443\u043f\u0438\u043b|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b|buyer|sale\s+trace|trace\s+of\s+sale)/iu.test(value);
|
||||||
|
if (hasPlainItemCue && (hasPlainTraceCue || (0, inventoryLifecycleCueHelpers_1.hasInventorySaleCue)(value))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const hasItemCue = /(?:товар|номенклатур|sku|item|product|позици(?:я|ю|и)|продукци(?:я|ю|и))/iu.test(text);
|
const hasItemCue = /(?:товар|номенклатур|sku|item|product|позици(?:я|ю|и)|продукци(?:я|ю|и))/iu.test(text);
|
||||||
const hasTraceCue = /(?:кому\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали|кому\s+был\s+продан|куда\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали(?:\s+(?:это|его|товар|позицию))?|куда\s+(?:была\s+)?реализована\s+(?:позиция|номенклатура|продукция)|кто\s+купил|buyer|sale\s+trace|trace\s+of\s+sale|через\s+какие\s+документы\s+прош[её]л\s+путь\s+товара|закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*warehouse\s*->\s*sale|purchase\s*->\s*stock\s*->\s*sale)/iu.test(text);
|
const hasTraceCue = /(?:кому\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали|кому\s+был\s+продан|куда\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали(?:\s+(?:это|его|товар|позицию))?|куда\s+(?:была\s+)?реализована\s+(?:позиция|номенклатура|продукция)|кто\s+купил|buyer|sale\s+trace|trace\s+of\s+sale|через\s+какие\s+документы\s+прош[её]л\s+путь\s+товара|закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*warehouse\s*->\s*sale|purchase\s*->\s*stock\s*->\s*sale)/iu.test(text);
|
||||||
return hasItemCue && hasTraceCue;
|
return hasItemCue && hasTraceCue;
|
||||||
|
|
@ -1435,6 +1441,12 @@ function hasInventoryAgingSignal(text) {
|
||||||
return hasAgingCue || (hasResidueCue && /(?:давно\s+куплен|давно\s+приобретен|задолго\s+до)/iu.test(text));
|
return hasAgingCue || (hasResidueCue && /(?:давно\s+куплен|давно\s+приобретен|задолго\s+до)/iu.test(text));
|
||||||
}
|
}
|
||||||
function hasInventoryPurchaseToSaleChainSignal(text) {
|
function hasInventoryPurchaseToSaleChainSignal(text) {
|
||||||
|
const value = String(text ?? "");
|
||||||
|
const hasPlainItemCue = /(?:товар|номенклатур|позици|sku|item|product)/iu.test(value);
|
||||||
|
const hasPlainChainCue = /(?:закупк[а-яё]*\s*->\s*склад\s*->\s*продаж|закупк[а-яё]*[\s\S]{0,80}склад[\s\S]{0,80}продаж|через\s+какие\s+документы\s+прош[её]л\s+путь|путь\s+товар[а-яё]*[\s\S]{0,80}закуп|цепочк[а-яё]*\s+движен|документально\s+подтвержденн[а-яё]*\s+цепочк|supplier\s*->\s*item\s*->\s*(?:buyer|customer)|supplier\s+to\s+buyer|supplier\s+to\s+item\s+to\s+buyer|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*(?:warehouse|stock)\s*->\s*sale)/iu.test(value) || value.includes("->");
|
||||||
|
if (hasPlainItemCue && hasPlainChainCue) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text);
|
const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text);
|
||||||
const hasChainCue = /(?:закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*(?:warehouse|stock)\s*->\s*sale|закупка\s*->\s*склад\s*->\s*продажа|цепочк[аи]\s+движен|документально\s+подтвержденн\w+\s+цепочк|supplier\s*->\s*item\s*->\s*(?:buyer|customer)|supplier\s+to\s+buyer|supplier\s+to\s+item\s+to\s+buyer)/iu.test(text) || text.includes("->");
|
const hasChainCue = /(?:закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*(?:warehouse|stock)\s*->\s*sale|закупка\s*->\s*склад\s*->\s*продажа|цепочк[аи]\s+движен|документально\s+подтвержденн\w+\s+цепочк|supplier\s*->\s*item\s*->\s*(?:buyer|customer)|supplier\s+to\s+buyer|supplier\s+to\s+item\s+to\s+buyer)/iu.test(text) || text.includes("->");
|
||||||
return hasItemCue && hasChainCue;
|
return hasItemCue && hasChainCue;
|
||||||
|
|
@ -1603,6 +1615,10 @@ function resolveUnicodeAddressIntentBridge(text) {
|
||||||
]).has(byAnchorToken);
|
]).has(byAnchorToken);
|
||||||
const hasMoneyCue = /(?:деньг|денег|выручк|доход|оборот|заработ|прин[её]с|чек|ликвидн|revenue|turnover|money)/iu.test(normalized);
|
const hasMoneyCue = /(?:деньг|денег|выручк|доход|оборот|заработ|прин[её]с|чек|ликвидн|revenue|turnover|money)/iu.test(normalized);
|
||||||
const hasRankingCue = /(?:топ|ранк|сам(?:ый|ая|ое|ые)|больше\s+всего|наибольш|крупн|жирн|max|top|rank)/iu.test(normalized);
|
const hasRankingCue = /(?:топ|ранк|сам(?:ый|ая|ое|ые)|больше\s+всего|наибольш|крупн|жирн|max|top|rank)/iu.test(normalized);
|
||||||
|
const hasInventoryPurchaseToSaleDocumentChainCue = /(?:закупк[а-яё]*[\s\S]{0,80}склад[\s\S]{0,80}продаж|путь\s+товар[а-яё]*[\s\S]{0,80}закуп|purchase\s*->\s*(?:warehouse|stock)\s*->\s*sale|->\s*(?:склад|warehouse|stock)\s*->\s*(?:продаж|sale))/iu.test(normalized) && /(?:товар|позици|номенклатур|sku|item|product)/iu.test(normalized);
|
||||||
|
if (hasInventoryPurchaseToSaleDocumentChainCue) {
|
||||||
|
return unicodeBridgeResolution("inventory_purchase_to_sale_chain", "high", "unicode_inventory_purchase_to_sale_chain_bridge_signal_detected");
|
||||||
|
}
|
||||||
const hasOpenItemsAccountCue = /(?:хвост|долг|незакрыт|вис)/iu.test(normalized) &&
|
const hasOpenItemsAccountCue = /(?:хвост|долг|незакрыт|вис)/iu.test(normalized) &&
|
||||||
/(?:сч(?:е|ё)т(?:а|у|ом|е|ов)?\s*(?:№|#)?\s*(?:60|62|76)(?:[.,]\d{1,2})?|\b(?:60|62|76)(?:[.,]\d{1,2})?\b\s*сч(?:е|ё)т)/iu.test(normalized);
|
/(?:сч(?:е|ё)т(?:а|у|ом|е|ов)?\s*(?:№|#)?\s*(?:60|62|76)(?:[.,]\d{1,2})?|\b(?:60|62|76)(?:[.,]\d{1,2})?\b\s*сч(?:е|ё)т)/iu.test(normalized);
|
||||||
if (hasOpenItemsAccountCue) {
|
if (hasOpenItemsAccountCue) {
|
||||||
|
|
|
||||||
|
|
@ -89,9 +89,13 @@ function hasInventoryProvenanceSignalV2(text) {
|
||||||
return hasItemCue && hasSupplierCue && hasPurchaseCue;
|
return hasItemCue && hasSupplierCue && hasPurchaseCue;
|
||||||
}
|
}
|
||||||
function hasInventoryPurchaseDateSignal(text) {
|
function hasInventoryPurchaseDateSignal(text) {
|
||||||
const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text) || hasSelectedObjectInventoryCue(text);
|
const value = String(text ?? "");
|
||||||
const hasPurchaseDateCue = /(?:когда\s+(?:примерно\s+)?(?:мы\s+)?купили|когда\s+был\s+куплен|когда\s+куплен|дата\s+закупк|purchase\s+date)/iu.test(text) ||
|
const hasItemCue = /(?:\u0442\u043e\u0432\u0430\u0440|\u043f\u043e\u0437\u0438\u0446|\u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442\u0443\u0440|sku|item|product)/iu.test(value) ||
|
||||||
/(?:когда\s+был(?:а|и|о)?\s+закупк\w*|когда\s+закупк\w*)/iu.test(text);
|
/(?:товар|номенклатур|sku|item|product)/iu.test(value) ||
|
||||||
|
hasSelectedObjectInventoryCue(value);
|
||||||
|
const hasPurchaseDateCue = /(?:\u043a\u043e\u0433\u0434\u0430\s+(?:\u043f\u0440\u0438\u043c\u0435\u0440\u043d\u043e\s+)?(?:\u043c\u044b\s+)?\u043a\u0443\u043f\u0438\u043b\u0438|\u043a\u043e\u0433\u0434\u0430\s+\u0431\u044b\u043b(?:\u0430|\u0438|\u043e)?\s+\u043a\u0443\u043f\u043b\u0435\u043d|\u043a\u043e\u0433\u0434\u0430\s+\u043a\u0443\u043f\u043b\u0435\u043d|\u0434\u0430\u0442\u0430\s+\u0437\u0430\u043a\u0443\u043f\u043a|purchase\s+date)/iu.test(value) ||
|
||||||
|
/(?:когда\s+(?:примерно\s+)?(?:мы\s+)?купили|когда\s+был\s+куплен|когда\s+куплен|дата\s+закупк|purchase\s+date)/iu.test(value) ||
|
||||||
|
/(?:когда\s+был(?:а|и|о)?\s+закупк\w*|когда\s+закупк\w*)/iu.test(value);
|
||||||
return hasItemCue && hasPurchaseDateCue;
|
return hasItemCue && hasPurchaseDateCue;
|
||||||
}
|
}
|
||||||
function hasInventoryPurchaseDocumentsSignalV2(text) {
|
function hasInventoryPurchaseDocumentsSignalV2(text) {
|
||||||
|
|
@ -108,7 +112,7 @@ function hasInventoryPurchaseDocumentsSignalV2(text) {
|
||||||
function hasInventorySaleTraceSignalV2(text) {
|
function hasInventorySaleTraceSignalV2(text) {
|
||||||
const value = String(text ?? "");
|
const value = String(text ?? "");
|
||||||
const hasPlainItemCue = /(?:товар|номенклатур|позици|продукци|sku|item|product)/iu.test(value);
|
const hasPlainItemCue = /(?:товар|номенклатур|позици|продукци|sku|item|product)/iu.test(value);
|
||||||
const hasPlainTraceCue = /(?:кому\s+(?:в\s+итоге\s+)?(?:мы\s+)?(?:продали|реализовали|впарили)|кому\s+(?:был[аио]?|были)?\s*реализован|кто\s+купил|покупател|buyer|sale\s+trace|trace\s+of\s+sale)/iu.test(value);
|
const hasPlainTraceCue = /(?:кому\s+(?:в\s+итоге\s+)?(?:мы\s+)?(?:продали|реализовали|впарили)|кому\s+(?:был[аио]?|были)?\s*(?:продан|реализован)|кто\s+купил|покупател|buyer|sale\s+trace|trace\s+of\s+sale)/iu.test(value);
|
||||||
if (hasPlainItemCue && hasPlainTraceCue) {
|
if (hasPlainItemCue && hasPlainTraceCue) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -211,6 +211,20 @@ function deriveTaxQuarterWindowForDate(value) {
|
||||||
period_to: `${year}-${String(quarterEndMonth).padStart(2, "0")}-${String(quarterEndDay).padStart(2, "0")}`
|
period_to: `${year}-${String(quarterEndMonth).padStart(2, "0")}-${String(quarterEndDay).padStart(2, "0")}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function deriveMonthWindowForDate(value) {
|
||||||
|
const isoDate = normalizeIsoDateForQuery(value);
|
||||||
|
if (!isoDate) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const match = isoDate.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
period_from: `${match[1]}-${match[2]}-01`,
|
||||||
|
period_to: isoDate
|
||||||
|
};
|
||||||
|
}
|
||||||
function toDateTimeExprForQuery(isoDate) {
|
function toDateTimeExprForQuery(isoDate) {
|
||||||
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
|
|
@ -1173,6 +1187,8 @@ function toNormalizedRows(rows) {
|
||||||
const item = resolveInventoryItemFromRawRow(row, accountDt, accountKt);
|
const item = resolveInventoryItemFromRawRow(row, accountDt, accountKt);
|
||||||
const warehouse = firstNonEmptyString(row.Склад, row.Warehouse, row.warehouse, row.СкладПредставление);
|
const warehouse = firstNonEmptyString(row.Склад, row.Warehouse, row.warehouse, row.СкладПредставление);
|
||||||
const organization = firstNonEmptyString(row.Организация, row.Organization, row.organization, row.organization_name, row.ОрганизацияПредставление);
|
const organization = firstNonEmptyString(row.Организация, row.Organization, row.organization, row.organization_name, row.ОрганизацияПредставление);
|
||||||
|
const counterparty = firstNonEmptyString(row.Контрагент, row.Counterparty, row.counterparty);
|
||||||
|
const contract = firstNonEmptyString(row.Договор, row.Contract, row.contract);
|
||||||
const analytics = collectAnalyticsStrings(row);
|
const analytics = collectAnalyticsStrings(row);
|
||||||
return {
|
return {
|
||||||
period,
|
period,
|
||||||
|
|
@ -1184,7 +1200,9 @@ function toNormalizedRows(rows) {
|
||||||
quantity,
|
quantity,
|
||||||
item,
|
item,
|
||||||
warehouse,
|
warehouse,
|
||||||
organization
|
organization,
|
||||||
|
counterparty,
|
||||||
|
contract
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((item) => Boolean(item.period || item.registrator));
|
.filter((item) => Boolean(item.period || item.registrator));
|
||||||
|
|
@ -1235,6 +1253,10 @@ function formatMoneyRubForReply(value) {
|
||||||
}).format(value)} ₽`;
|
}).format(value)} ₽`;
|
||||||
}
|
}
|
||||||
function extractContractNameFromNormalizedRow(row) {
|
function extractContractNameFromNormalizedRow(row) {
|
||||||
|
const explicitContract = firstNonEmptyString(row.contract);
|
||||||
|
if (explicitContract) {
|
||||||
|
return explicitContract;
|
||||||
|
}
|
||||||
for (const token of row.analytics) {
|
for (const token of row.analytics) {
|
||||||
const normalized = String(token ?? "").trim();
|
const normalized = String(token ?? "").trim();
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
|
|
@ -1539,6 +1561,10 @@ function applyPreExecutionOrganizationScopeGrounding(input) {
|
||||||
]);
|
]);
|
||||||
const resolvedOrganizationFromMessage = (0, assistantOrganizationMatcher_1.resolveOrganizationSelectionFromMessage)(input.userMessage, candidateOrganizations);
|
const resolvedOrganizationFromMessage = (0, assistantOrganizationMatcher_1.resolveOrganizationSelectionFromMessage)(input.userMessage, candidateOrganizations);
|
||||||
const referentialOrganizationScopeDetected = hasReferentialOrganizationScopeSignal(input.userMessage);
|
const referentialOrganizationScopeDetected = hasReferentialOrganizationScopeSignal(input.userMessage);
|
||||||
|
const counterpartyAnchorProtectsOrganizationScope = input.semanticFrame?.anchor_kind === "counterparty" &&
|
||||||
|
typeof input.filters.counterparty === "string" &&
|
||||||
|
input.filters.counterparty.trim().length > 0 &&
|
||||||
|
!referentialOrganizationScopeDetected;
|
||||||
if (!input.filters.organization &&
|
if (!input.filters.organization &&
|
||||||
input.semanticFrame?.scope_kind === "implicit_self_scope" &&
|
input.semanticFrame?.scope_kind === "implicit_self_scope" &&
|
||||||
activeOrganization) {
|
activeOrganization) {
|
||||||
|
|
@ -1552,6 +1578,7 @@ function applyPreExecutionOrganizationScopeGrounding(input) {
|
||||||
}
|
}
|
||||||
if (resolvedOrganizationFromMessage &&
|
if (resolvedOrganizationFromMessage &&
|
||||||
(!input.filters.organization || input.semanticFrame?.anchor_kind === "organization") &&
|
(!input.filters.organization || input.semanticFrame?.anchor_kind === "organization") &&
|
||||||
|
!counterpartyAnchorProtectsOrganizationScope &&
|
||||||
!sameNormalizedOrganizationScope(input.filters.organization ?? null, resolvedOrganizationFromMessage)) {
|
!sameNormalizedOrganizationScope(input.filters.organization ?? null, resolvedOrganizationFromMessage)) {
|
||||||
input.filters.organization = resolvedOrganizationFromMessage;
|
input.filters.organization = resolvedOrganizationFromMessage;
|
||||||
if (!input.warnings.includes("organization_grounded_from_scope_candidates")) {
|
if (!input.warnings.includes("organization_grounded_from_scope_candidates")) {
|
||||||
|
|
@ -1921,6 +1948,9 @@ function hasExplicitPeriodWindow(filters) {
|
||||||
return ((typeof filters.period_from === "string" && filters.period_from.trim().length > 0) ||
|
return ((typeof filters.period_from === "string" && filters.period_from.trim().length > 0) ||
|
||||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0));
|
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0));
|
||||||
}
|
}
|
||||||
|
function asksForUnresolvedInventorySupplierLink(userMessage) {
|
||||||
|
return /(?:\u0431\u0435\u0437\s+\u043f\u043e\u043d\u044f\u0442\u043d[^\s]*\s+\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0431\u0435\u0437\s+(?:\u044f\u0432\u043d[^\s]*\s+)?\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043d\u0435\s+\u0438\u043c\u0435\u044e\u0442\s+\u044f\u0432\u043d[^\s]*\s+\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043d\u0435\u0442\s+\u044f\u0432\u043d[^\s]*\s+\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|unresolved\s+supplier\s+link)/iu.test(String(userMessage ?? ""));
|
||||||
|
}
|
||||||
function canAutoBroadenPeriodWindow(intent, filters) {
|
function canAutoBroadenPeriodWindow(intent, filters) {
|
||||||
const hasRecoverableAsOfOnlyWindow = !hasExplicitPeriodWindow(filters) &&
|
const hasRecoverableAsOfOnlyWindow = !hasExplicitPeriodWindow(filters) &&
|
||||||
typeof filters.as_of_date === "string" &&
|
typeof filters.as_of_date === "string" &&
|
||||||
|
|
@ -1956,11 +1986,14 @@ function shouldClearAsOfDateForHistoryRecovery(intent) {
|
||||||
intent === "inventory_purchase_to_sale_chain");
|
intent === "inventory_purchase_to_sale_chain");
|
||||||
}
|
}
|
||||||
function shouldDetachLifecycleExecutionFromSnapshotContext(intent, reasons) {
|
function shouldDetachLifecycleExecutionFromSnapshotContext(intent, reasons) {
|
||||||
if (intent !== "inventory_sale_trace_for_item" &&
|
if (intent !== "inventory_supplier_stock_overlap_as_of_date" &&
|
||||||
|
intent !== "inventory_sale_trace_for_item" &&
|
||||||
intent !== "inventory_purchase_to_sale_chain") {
|
intent !== "inventory_purchase_to_sale_chain") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return (reasons.includes("as_of_date_from_followup_context") ||
|
return (reasons.includes("period_window_semantic_from_inventory_snapshot_context") ||
|
||||||
|
reasons.includes("period_window_semantic_from_inventory_as_of_month") ||
|
||||||
|
reasons.includes("as_of_date_from_followup_context") ||
|
||||||
reasons.includes("period_from_followup_context") ||
|
reasons.includes("period_from_followup_context") ||
|
||||||
reasons.includes("as_of_date_from_open_items_followup_context"));
|
reasons.includes("as_of_date_from_open_items_followup_context"));
|
||||||
}
|
}
|
||||||
|
|
@ -2820,6 +2853,84 @@ 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 chainCounterpartyAnchor = toNonEmptyFilterValue(filters.extracted_filters.counterparty);
|
||||||
|
const chainOrganizationAnchor = toNonEmptyFilterValue(filters.extracted_filters.organization);
|
||||||
|
if (intent.intent === "inventory_purchase_to_sale_chain" &&
|
||||||
|
chainCounterpartyAnchor &&
|
||||||
|
chainOrganizationAnchor &&
|
||||||
|
sameOrganizationEntityReference(chainOrganizationAnchor, chainCounterpartyAnchor)) {
|
||||||
|
delete filters.extracted_filters.organization;
|
||||||
|
const restoredOrganization = activeOrganization ?? previousOrganizationFromContext;
|
||||||
|
if (restoredOrganization && !sameOrganizationEntityReference(restoredOrganization, chainCounterpartyAnchor)) {
|
||||||
|
filters.extracted_filters.organization = restoredOrganization;
|
||||||
|
if (!filters.warnings.includes("organization_restored_from_inventory_chain_context")) {
|
||||||
|
filters.warnings.push("organization_restored_from_inventory_chain_context");
|
||||||
|
}
|
||||||
|
if (!baseReasons.includes("organization_restored_from_inventory_chain_context")) {
|
||||||
|
baseReasons.push("organization_restored_from_inventory_chain_context");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (!filters.warnings.includes("organization_cleared_from_inventory_chain_counterparty_anchor")) {
|
||||||
|
filters.warnings.push("organization_cleared_from_inventory_chain_counterparty_anchor");
|
||||||
|
}
|
||||||
|
if (!baseReasons.includes("organization_cleared_from_inventory_chain_counterparty_anchor")) {
|
||||||
|
baseReasons.push("organization_cleared_from_inventory_chain_counterparty_anchor");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (intent.intent === "inventory_supplier_stock_overlap_as_of_date" &&
|
||||||
|
(followupContext?.root_filters || followupContext?.previous_filters) &&
|
||||||
|
!toNonEmptyFilterValue(filters.extracted_filters.period_from) &&
|
||||||
|
!toNonEmptyFilterValue(filters.extracted_filters.period_to) &&
|
||||||
|
!/(?:за\s+вс[её]\s+время|за\s+любой\s+период|all[\s-]?time|all\s+periods?)/iu.test(userMessage)) {
|
||||||
|
const snapshotContextFilters = followupContext?.root_filters &&
|
||||||
|
(toNonEmptyFilterValue(followupContext.root_filters.period_from) ||
|
||||||
|
toNonEmptyFilterValue(followupContext.root_filters.period_to))
|
||||||
|
? followupContext.root_filters
|
||||||
|
: followupContext?.previous_filters;
|
||||||
|
const previousPeriodFrom = toNonEmptyFilterValue(snapshotContextFilters?.period_from);
|
||||||
|
const previousPeriodTo = toNonEmptyFilterValue(snapshotContextFilters?.period_to);
|
||||||
|
if (previousPeriodFrom || previousPeriodTo) {
|
||||||
|
filters.extracted_filters = {
|
||||||
|
...filters.extracted_filters,
|
||||||
|
...(previousPeriodFrom ? { period_from: previousPeriodFrom } : {}),
|
||||||
|
...(previousPeriodTo ? { period_to: previousPeriodTo } : {})
|
||||||
|
};
|
||||||
|
if (!toNonEmptyFilterValue(filters.extracted_filters.as_of_date)) {
|
||||||
|
const inheritedAsOfDate = toNonEmptyFilterValue(snapshotContextFilters?.as_of_date) ?? previousPeriodTo ?? previousPeriodFrom;
|
||||||
|
if (inheritedAsOfDate) {
|
||||||
|
filters.extracted_filters.as_of_date = inheritedAsOfDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!filters.warnings.includes("period_window_semantic_from_inventory_snapshot_context")) {
|
||||||
|
filters.warnings.push("period_window_semantic_from_inventory_snapshot_context");
|
||||||
|
}
|
||||||
|
if (!baseReasons.includes("period_window_semantic_from_inventory_snapshot_context")) {
|
||||||
|
baseReasons.push("period_window_semantic_from_inventory_snapshot_context");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (intent.intent === "inventory_supplier_stock_overlap_as_of_date" &&
|
||||||
|
!toNonEmptyFilterValue(filters.extracted_filters.period_from) &&
|
||||||
|
!toNonEmptyFilterValue(filters.extracted_filters.period_to) &&
|
||||||
|
asksForUnresolvedInventorySupplierLink(userMessage) &&
|
||||||
|
!/(?:за\s+вс[её]\s+время|за\s+любой\s+период|all[\s-]?time|all\s+periods?)/iu.test(userMessage)) {
|
||||||
|
const monthWindow = deriveMonthWindowForDate(filters.extracted_filters.as_of_date);
|
||||||
|
if (monthWindow) {
|
||||||
|
filters.extracted_filters = {
|
||||||
|
...filters.extracted_filters,
|
||||||
|
...monthWindow
|
||||||
|
};
|
||||||
|
if (!filters.warnings.includes("period_window_semantic_from_inventory_as_of_month")) {
|
||||||
|
filters.warnings.push("period_window_semantic_from_inventory_as_of_month");
|
||||||
|
}
|
||||||
|
if (!baseReasons.includes("period_window_semantic_from_inventory_as_of_month")) {
|
||||||
|
baseReasons.push("period_window_semantic_from_inventory_as_of_month");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (isOrganizationScopedValueFlowIntent(intent.intent) &&
|
if (isOrganizationScopedValueFlowIntent(intent.intent) &&
|
||||||
hasExplicitSingleOrganizationValueFlowScopeRequest(userMessage) &&
|
hasExplicitSingleOrganizationValueFlowScopeRequest(userMessage) &&
|
||||||
!resolvedOrganizationFromMessage) {
|
!resolvedOrganizationFromMessage) {
|
||||||
|
|
@ -2969,13 +3080,14 @@ class AddressQueryService {
|
||||||
const detachedExecutionFilters = { ...executionFilters };
|
const detachedExecutionFilters = { ...executionFilters };
|
||||||
let periodDetached = false;
|
let periodDetached = false;
|
||||||
let asOfDetached = false;
|
let asOfDetached = false;
|
||||||
|
const keepAsOfDateForInventorySnapshotOverlap = intent.intent === "inventory_supplier_stock_overlap_as_of_date";
|
||||||
if (toNonEmptyFilterValue(detachedExecutionFilters.period_from) ||
|
if (toNonEmptyFilterValue(detachedExecutionFilters.period_from) ||
|
||||||
toNonEmptyFilterValue(detachedExecutionFilters.period_to)) {
|
toNonEmptyFilterValue(detachedExecutionFilters.period_to)) {
|
||||||
delete detachedExecutionFilters.period_from;
|
delete detachedExecutionFilters.period_from;
|
||||||
delete detachedExecutionFilters.period_to;
|
delete detachedExecutionFilters.period_to;
|
||||||
periodDetached = true;
|
periodDetached = true;
|
||||||
}
|
}
|
||||||
if (toNonEmptyFilterValue(detachedExecutionFilters.as_of_date)) {
|
if (!keepAsOfDateForInventorySnapshotOverlap && toNonEmptyFilterValue(detachedExecutionFilters.as_of_date)) {
|
||||||
delete detachedExecutionFilters.as_of_date;
|
delete detachedExecutionFilters.as_of_date;
|
||||||
asOfDetached = true;
|
asOfDetached = true;
|
||||||
}
|
}
|
||||||
|
|
@ -3452,6 +3564,18 @@ class AddressQueryService {
|
||||||
: anchor.anchor_type === "contract" && anchor.anchor_value_resolved
|
: anchor.anchor_type === "contract" && anchor.anchor_value_resolved
|
||||||
? { ...executionFilters, contract: anchor.anchor_value_resolved }
|
? { ...executionFilters, contract: anchor.anchor_value_resolved }
|
||||||
: executionFilters;
|
: executionFilters;
|
||||||
|
if (intent.intent === "inventory_purchase_to_sale_chain" &&
|
||||||
|
toNonEmptyFilterValue(filtersForMatching.item) &&
|
||||||
|
toNonEmptyFilterValue(filtersForMatching.counterparty)) {
|
||||||
|
filtersForMatching = { ...filtersForMatching };
|
||||||
|
delete filtersForMatching.counterparty;
|
||||||
|
if (!filters.warnings.includes("inventory_chain_counterparty_anchor_kept_for_verification")) {
|
||||||
|
filters.warnings.push("inventory_chain_counterparty_anchor_kept_for_verification");
|
||||||
|
}
|
||||||
|
if (!baseReasons.includes("inventory_chain_counterparty_anchor_kept_for_verification")) {
|
||||||
|
baseReasons.push("inventory_chain_counterparty_anchor_kept_for_verification");
|
||||||
|
}
|
||||||
|
}
|
||||||
const accountScopeAudit = buildAccountScopeAudit({
|
const accountScopeAudit = buildAccountScopeAudit({
|
||||||
intent: intent.intent,
|
intent: intent.intent,
|
||||||
filters: filtersForMatching,
|
filters: filtersForMatching,
|
||||||
|
|
|
||||||
|
|
@ -1235,6 +1235,26 @@ function buildInventoryPurchaseDocumentQuery(filters, resolvedLimit) {
|
||||||
.replace("__WHERE_CLAUSE__", buildWhereClause(filters, "Товары.Ссылка.Дата", ['Товары.Ссылка.Проведен = ИСТИНА', itemCondition].filter((item) => Boolean(item))))
|
.replace("__WHERE_CLAUSE__", buildWhereClause(filters, "Товары.Ссылка.Дата", ['Товары.Ссылка.Проведен = ИСТИНА', itemCondition].filter((item) => Boolean(item))))
|
||||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||||
}
|
}
|
||||||
|
function stripTrailingOrderBy(query) {
|
||||||
|
return String(query ?? "").replace(/\r?\nУПОРЯДОЧИТЬ ПО[\s\S]*$/u, "").trimEnd();
|
||||||
|
}
|
||||||
|
function removeTopLimit(query) {
|
||||||
|
return String(query ?? "").replace(/ВЫБРАТЬ ПЕРВЫЕ\s+\d+/u, "ВЫБРАТЬ");
|
||||||
|
}
|
||||||
|
function buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit) {
|
||||||
|
const purchaseQuery = removeTopLimit(stripTrailingOrderBy(buildInventoryPurchaseDocumentQuery(filters, resolvedLimit)));
|
||||||
|
const saleQuery = removeTopLimit(stripTrailingOrderBy(buildInventorySaleDocumentQuery(filters, resolvedLimit)));
|
||||||
|
return [
|
||||||
|
purchaseQuery,
|
||||||
|
"",
|
||||||
|
"ОБЪЕДИНИТЬ ВСЕ",
|
||||||
|
"",
|
||||||
|
saleQuery,
|
||||||
|
"",
|
||||||
|
"УПОРЯДОЧИТЬ ПО",
|
||||||
|
` Период ${resolveOrderDirection(filters.sort)}`
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
function buildCounterpartyPurchaseDocumentQuery(filters, resolvedLimit) {
|
function buildCounterpartyPurchaseDocumentQuery(filters, resolvedLimit) {
|
||||||
const goodsCounterpartyCondition = buildCounterpartyReferenceCondition(filters, ["Товары.Ссылка.Контрагент"]);
|
const goodsCounterpartyCondition = buildCounterpartyReferenceCondition(filters, ["Товары.Ссылка.Контрагент"]);
|
||||||
const servicesCounterpartyCondition = buildCounterpartyReferenceCondition(filters, ["Услуги.Ссылка.Контрагент"]);
|
const servicesCounterpartyCondition = buildCounterpartyReferenceCondition(filters, ["Услуги.Ссылка.Контрагент"]);
|
||||||
|
|
@ -1463,9 +1483,9 @@ function buildAddressRecipePlan(recipe, filters) {
|
||||||
: recipe.query_template === "inventory_supplier_stock_overlap_profile"
|
: recipe.query_template === "inventory_supplier_stock_overlap_profile"
|
||||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||||
: recipe.query_template === "inventory_sale_trace_profile"
|
: recipe.query_template === "inventory_sale_trace_profile"
|
||||||
? buildInventoryMovementQuery(filters, resolvedLimit, "kt")
|
? buildInventorySaleDocumentQuery(filters, resolvedLimit)
|
||||||
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
||||||
? buildInventoryMovementQuery(filters, resolvedLimit, "either")
|
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||||
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
||||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||||
: recipe.query_template === "contracts_by_counterparty_profile"
|
: recipe.query_template === "contracts_by_counterparty_profile"
|
||||||
|
|
|
||||||
|
|
@ -848,6 +848,16 @@ function extractInventoryCounterpartyCandidates(row, excludedTokens = []) {
|
||||||
}
|
}
|
||||||
candidates.push(normalized);
|
candidates.push(normalized);
|
||||||
}
|
}
|
||||||
|
const explicitCounterparty = normalizeCounterpartyDisplayLabel(row.counterparty);
|
||||||
|
const explicitComparable = normalizeEntityToken(explicitCounterparty);
|
||||||
|
if (explicitCounterparty &&
|
||||||
|
explicitComparable &&
|
||||||
|
explicitComparable !== itemToken &&
|
||||||
|
explicitComparable !== warehouseToken &&
|
||||||
|
explicitComparable !== organizationToken &&
|
||||||
|
!excludedComparableTokens.includes(explicitComparable)) {
|
||||||
|
candidates.unshift(explicitCounterparty);
|
||||||
|
}
|
||||||
return uniqueStrings(candidates);
|
return uniqueStrings(candidates);
|
||||||
}
|
}
|
||||||
function summarizeInventoryTraceRows(rows, excludedCounterpartyTokens = []) {
|
function summarizeInventoryTraceRows(rows, excludedCounterpartyTokens = []) {
|
||||||
|
|
@ -883,6 +893,7 @@ function summarizeInventoryTraceRows(rows, excludedCounterpartyTokens = []) {
|
||||||
function formatInventoryTraceRows(rows, limit = 10, excludedCounterpartyTokens = []) {
|
function formatInventoryTraceRows(rows, limit = 10, excludedCounterpartyTokens = []) {
|
||||||
return rows.slice(0, limit).map((row, index) => {
|
return rows.slice(0, limit).map((row, index) => {
|
||||||
const parties = extractInventoryCounterpartyCandidates(row, excludedCounterpartyTokens);
|
const parties = extractInventoryCounterpartyCandidates(row, excludedCounterpartyTokens);
|
||||||
|
const item = extractInventoryItemName(row);
|
||||||
const warehouse = extractInventoryWarehouseName(row);
|
const warehouse = extractInventoryWarehouseName(row);
|
||||||
const organization = extractInventoryOrganizationName(row);
|
const organization = extractInventoryOrganizationName(row);
|
||||||
const amount = typeof row.amount === "number" && Number.isFinite(row.amount) ? formatMoneyRub(row.amount) : "сумма не указана";
|
const amount = typeof row.amount === "number" && Number.isFinite(row.amount) ? formatMoneyRub(row.amount) : "сумма не указана";
|
||||||
|
|
@ -891,6 +902,9 @@ function formatInventoryTraceRows(rows, limit = 10, excludedCounterpartyTokens =
|
||||||
`дата: ${inventoryTraceDateLabel(row.period)}`,
|
`дата: ${inventoryTraceDateLabel(row.period)}`,
|
||||||
`сумма: ${amount}`
|
`сумма: ${amount}`
|
||||||
];
|
];
|
||||||
|
if (item) {
|
||||||
|
parts.push(`товар: ${item}`);
|
||||||
|
}
|
||||||
if (warehouse) {
|
if (warehouse) {
|
||||||
parts.push(`склад: ${warehouse}`);
|
parts.push(`склад: ${warehouse}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,59 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.composeInventoryReply = composeInventoryReply;
|
exports.composeInventoryReply = composeInventoryReply;
|
||||||
const replyContracts_1 = require("./replyContracts");
|
const replyContracts_1 = require("./replyContracts");
|
||||||
const inventoryReplyPresentation_1 = require("./inventoryReplyPresentation");
|
const inventoryReplyPresentation_1 = require("./inventoryReplyPresentation");
|
||||||
|
function cleanupInventoryRequestedParty(value) {
|
||||||
|
const cleaned = String(value ?? "")
|
||||||
|
.replace(/\s*(?:->|=>|→)\s*(?:товар|позици|номенклатур|покупател|buyer|customer|item|product|sku)[\s\S]*$/iu, "")
|
||||||
|
.replace(/\s+(?:на\s+дату|по\s+состоянию|за\s+период)\b[\s\S]*$/iu, "")
|
||||||
|
.replace(/[«»"]/gu, "")
|
||||||
|
.replace(/[.,;:\s]+$/u, "")
|
||||||
|
.trim();
|
||||||
|
return cleaned.length > 0 ? cleaned : null;
|
||||||
|
}
|
||||||
|
function extractRequestedInventoryParty(userMessage, role) {
|
||||||
|
const text = String(userMessage ?? "");
|
||||||
|
const patterns = role === "supplier"
|
||||||
|
? [
|
||||||
|
/(?:от\s+поставщика|у\s+поставщика|поставщик(?:а|у|ом)?|supplier|vendor)\s+([^\r\n?]+?)(?=$|[?]|(?:\s*(?:->|=>|→)\s*(?:товар|позици|номенклатур|item|product|sku|покупател|buyer|customer)))/iu
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
/(?:покупател(?:ь|я|ю|ем)?|buyer|customer|client)\s+([^\r\n?]+?)(?=$|[?]|(?:\s*(?:->|=>|→))|(?:\s+на\s+дату))/iu
|
||||||
|
];
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const match = text.match(pattern);
|
||||||
|
const candidate = match?.[1] ? cleanupInventoryRequestedParty(match[1]) : null;
|
||||||
|
if (candidate) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function inventoryPartyComparableTokens(value) {
|
||||||
|
const stopWords = new Set(["ооо", "ао", "пао", "зао", "ип", "llc", "ltd", "inc", "corp"]);
|
||||||
|
return String(value ?? "")
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ё/gu, "е")
|
||||||
|
.replace(/[^a-zа-я0-9]+/giu, " ")
|
||||||
|
.split(/\s+/u)
|
||||||
|
.map((token) => token.trim())
|
||||||
|
.filter((token) => token.length >= 3 && !stopWords.has(token));
|
||||||
|
}
|
||||||
|
function inventoryRequestedPartyMatches(requested, actualParties) {
|
||||||
|
if (!requested) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const requestedTokens = inventoryPartyComparableTokens(requested);
|
||||||
|
if (requestedTokens.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return actualParties.some((actual) => {
|
||||||
|
const actualTokens = inventoryPartyComparableTokens(actual);
|
||||||
|
return requestedTokens.every((token) => actualTokens.includes(token));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function inventoryPartyListOrUnknown(parties) {
|
||||||
|
return parties.length > 0 ? parties.slice(0, 4).join("; ") : "не выделен отдельным полем";
|
||||||
|
}
|
||||||
function composeInventoryReply(intent, rows, options, deps) {
|
function composeInventoryReply(intent, rows, options, deps) {
|
||||||
if (intent === "inventory_on_hand_as_of_date") {
|
if (intent === "inventory_on_hand_as_of_date") {
|
||||||
const asOfDate = deps.resolvePayablesAsOfDate(options);
|
const asOfDate = deps.resolvePayablesAsOfDate(options);
|
||||||
|
|
@ -163,6 +216,29 @@ function composeInventoryReply(intent, rows, options, deps) {
|
||||||
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
|
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
|
||||||
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
|
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
|
||||||
const unresolvedRows = purchaseRows.filter((row) => deps.extractInventoryCounterpartyCandidates(row).length === 0);
|
const unresolvedRows = purchaseRows.filter((row) => deps.extractInventoryCounterpartyCandidates(row).length === 0);
|
||||||
|
const unresolvedSupplierQuestion = /(?:\u0431\u0435\u0437\s+\u043f\u043e\u043d\u044f\u0442\u043d[^\s]*\s+\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0431\u0435\u0437\s+(?:\u044f\u0432\u043d[^\s]*\s+)?\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043d\u0435\s+\u0438\u043c\u0435\u044e\u0442\s+\u044f\u0432\u043d[^\s]*\s+\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043d\u0435\u0442\s+\u044f\u0432\u043d[^\s]*\s+\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|unresolved\s+supplier\s+link)/iu.test(String(options.userMessage ?? ""));
|
||||||
|
if (unresolvedSupplierQuestion) {
|
||||||
|
const directAnswerLine = unresolvedRows.length > 0
|
||||||
|
? `В текущем складском срезе найдено операций без явно выделенного поставщика: ${deps.formatNumberWithDots(unresolvedRows.length)}.`
|
||||||
|
: "В текущем складском срезе товары без явно выделенной привязки к поставщику в доступных данных не найдены.";
|
||||||
|
const lines = [directAnswerLine];
|
||||||
|
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Что проверили:", [
|
||||||
|
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||||
|
`Закупочных операций в выборке: ${deps.formatNumberWithDots(purchaseRows.length)}.`,
|
||||||
|
`Операций без явно выделенного поставщика: ${deps.formatNumberWithDots(unresolvedRows.length)}.`,
|
||||||
|
`Поставщиков, выделенных в остальных операциях: ${deps.formatNumberWithDots(summary.counterparties.length)}.`
|
||||||
|
]);
|
||||||
|
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Ограничения:", [
|
||||||
|
"Без партионного учета это проверка доступного закупочного следа по складскому срезу, а не юридическое доказательство владельца каждой партии."
|
||||||
|
]);
|
||||||
|
if (unresolvedRows.length > 0) {
|
||||||
|
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Позиции без явно выделенного поставщика:", deps.formatInventoryTraceRows(unresolvedRows, 12));
|
||||||
|
}
|
||||||
|
else if (summary.counterparties.length > 0) {
|
||||||
|
lines.push(`- В доступном закупочном следе встречаются поставщики: ${summary.counterparties.slice(0, 6).join("; ")}.`);
|
||||||
|
}
|
||||||
|
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(unresolvedRows.length > 0 ? "medium" : "strong", true));
|
||||||
|
}
|
||||||
const warehouseLabel = summary.warehouses[0] ?? "не указанного склада";
|
const warehouseLabel = summary.warehouses[0] ?? "не указанного склада";
|
||||||
const directAnswerLine = summary.counterparties.length === 1
|
const directAnswerLine = summary.counterparties.length === 1
|
||||||
? `По складскому остатку ${warehouseLabel} выявлен поставщик: ${summary.counterparties[0]}.`
|
? `По складскому остатку ${warehouseLabel} выявлен поставщик: ${summary.counterparties[0]}.`
|
||||||
|
|
@ -283,12 +359,25 @@ function composeInventoryReply(intent, rows, options, deps) {
|
||||||
const purchaseSummary = deps.summarizeInventoryTraceRows(purchaseRows);
|
const purchaseSummary = deps.summarizeInventoryTraceRows(purchaseRows);
|
||||||
const saleSummary = deps.summarizeInventoryTraceRows(saleRows);
|
const saleSummary = deps.summarizeInventoryTraceRows(saleRows);
|
||||||
const itemLabel = purchaseSummary.item ?? saleSummary.item ?? "товар не определен";
|
const itemLabel = purchaseSummary.item ?? saleSummary.item ?? "товар не определен";
|
||||||
const directAnswerLine = purchaseSummary.counterparties.length === 1 && saleSummary.counterparties.length === 1
|
const requestedSupplier = extractRequestedInventoryParty(options.userMessage, "supplier");
|
||||||
|
const requestedBuyer = extractRequestedInventoryParty(options.userMessage, "buyer");
|
||||||
|
const supplierMatches = inventoryRequestedPartyMatches(requestedSupplier, purchaseSummary.counterparties);
|
||||||
|
const buyerMatches = inventoryRequestedPartyMatches(requestedBuyer, saleSummary.counterparties);
|
||||||
|
const mismatchParts = [];
|
||||||
|
if (requestedSupplier && purchaseRows.length > 0 && !supplierMatches) {
|
||||||
|
mismatchParts.push(`запрошенный поставщик ${requestedSupplier} не совпал с найденным поставщиком: ${inventoryPartyListOrUnknown(purchaseSummary.counterparties)}`);
|
||||||
|
}
|
||||||
|
if (requestedBuyer && saleRows.length > 0 && !buyerMatches) {
|
||||||
|
mismatchParts.push(`запрошенный покупатель ${requestedBuyer} не совпал с найденным покупателем: ${inventoryPartyListOrUnknown(saleSummary.counterparties)}`);
|
||||||
|
}
|
||||||
|
const directAnswerLine = mismatchParts.length > 0
|
||||||
|
? `Запрошенная цепочка по товару ${itemLabel} полностью не подтверждена: ${mismatchParts.join("; ")}.`
|
||||||
|
: purchaseSummary.counterparties.length === 1 && saleSummary.counterparties.length === 1
|
||||||
? `По товару ${itemLabel} цепочка поставки и продажи связана с поставщиком ${purchaseSummary.counterparties[0]} и покупателем ${saleSummary.counterparties[0]}.`
|
? `По товару ${itemLabel} цепочка поставки и продажи связана с поставщиком ${purchaseSummary.counterparties[0]} и покупателем ${saleSummary.counterparties[0]}.`
|
||||||
: `По товару ${itemLabel} цепочка поставки и продажи подтверждена частично или разнообразно: детали идут следом.`;
|
: `По товару ${itemLabel} цепочка поставки и продажи подтверждена частично или разнообразно: детали идут следом.`;
|
||||||
const lines = [directAnswerLine, "", "Подтверждение:"];
|
const lines = [directAnswerLine, "", "Подтверждение:"];
|
||||||
lines.push(`- Закупочных движений на 41.01: ${deps.formatNumberWithDots(purchaseRows.length)}.`);
|
lines.push(`- Строк закупки на 41.01: ${deps.formatNumberWithDots(purchaseRows.length)}.`);
|
||||||
lines.push(`- Движений выбытия со счета 41.01: ${deps.formatNumberWithDots(saleRows.length)}.`);
|
lines.push(`- Строк продажи со счета 41.01: ${deps.formatNumberWithDots(saleRows.length)}.`);
|
||||||
if (purchaseRows.length > 0 && saleRows.length > 0) {
|
if (purchaseRows.length > 0 && saleRows.length > 0) {
|
||||||
lines.push("- В доступных данных найдены обе стороны цепочки: поступление и последующее выбытие.");
|
lines.push("- В доступных данных найдены обе стороны цепочки: поступление и последующее выбытие.");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,29 @@ function readStateTransitionReasonCodes(input) {
|
||||||
.map((item) => toNonEmptyString(item))
|
.map((item) => toNonEmptyString(item))
|
||||||
.filter((item) => Boolean(item));
|
.filter((item) => Boolean(item));
|
||||||
}
|
}
|
||||||
|
function readStringArray(value) {
|
||||||
|
return Array.isArray(value)
|
||||||
|
? value.map((item) => toNonEmptyString(item)).filter((item) => Boolean(item))
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
function hasExactMatchedFactualAddressReply(input, entryPoint) {
|
||||||
|
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!hasEffectivelyFactualAddressReply(input)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const mcpCallStatus = toNonEmptyString(input.addressRuntimeMeta?.mcp_call_status);
|
||||||
|
const truthMode = toNonEmptyString(input.addressRuntimeMeta?.truth_mode);
|
||||||
|
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
|
||||||
|
const bindingStatus = toNonEmptyString(input.addressRuntimeMeta?.capability_binding_status);
|
||||||
|
const bindingViolations = readStringArray(input.addressRuntimeMeta?.capability_binding_violations);
|
||||||
|
return Boolean(mcpCallStatus === "matched_non_empty" &&
|
||||||
|
truthMode === "confirmed" &&
|
||||||
|
selectedRecipe?.startsWith("address_") &&
|
||||||
|
(bindingStatus === "bound" || bindingStatus === "bound_with_limits") &&
|
||||||
|
bindingViolations.length === 0);
|
||||||
|
}
|
||||||
function hasRuntimeAdjustedExactReply(input, entryPoint) {
|
function hasRuntimeAdjustedExactReply(input, entryPoint) {
|
||||||
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -332,6 +355,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
||||||
const matchedFactualAddressContinuationTarget = hasMatchedFactualAddressContinuationTarget(input, entryPoint);
|
const matchedFactualAddressContinuationTarget = hasMatchedFactualAddressContinuationTarget(input, entryPoint);
|
||||||
const matchedFactualSuggestedIntentPivotTarget = hasMatchedFactualSuggestedIntentPivotTarget(input, entryPoint);
|
const matchedFactualSuggestedIntentPivotTarget = hasMatchedFactualSuggestedIntentPivotTarget(input, entryPoint);
|
||||||
const fullConfirmedFactualAddressReply = hasFullConfirmedFactualAddressReply(input, entryPoint);
|
const fullConfirmedFactualAddressReply = hasFullConfirmedFactualAddressReply(input, entryPoint);
|
||||||
|
const exactMatchedFactualAddressReply = hasExactMatchedFactualAddressReply(input, entryPoint);
|
||||||
const runtimeAdjustedExactReply = hasRuntimeAdjustedExactReply(input, entryPoint);
|
const runtimeAdjustedExactReply = hasRuntimeAdjustedExactReply(input, entryPoint);
|
||||||
if (!entryPoint) {
|
if (!entryPoint) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point");
|
||||||
|
|
@ -363,6 +387,9 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
||||||
if (fullConfirmedFactualAddressReply) {
|
if (fullConfirmedFactualAddressReply) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply");
|
||||||
}
|
}
|
||||||
|
if (exactMatchedFactualAddressReply) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_exact_matched_factual_address_reply");
|
||||||
|
}
|
||||||
if (runtimeAdjustedExactReply) {
|
if (runtimeAdjustedExactReply) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_runtime_adjusted_exact_reply_over_stale_discovery_turn_meaning");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_runtime_adjusted_exact_reply_over_stale_discovery_turn_meaning");
|
||||||
}
|
}
|
||||||
|
|
@ -387,6 +414,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
||||||
!matchedFactualAddressContinuationTarget &&
|
!matchedFactualAddressContinuationTarget &&
|
||||||
!matchedFactualSuggestedIntentPivotTarget &&
|
!matchedFactualSuggestedIntentPivotTarget &&
|
||||||
!fullConfirmedFactualAddressReply &&
|
!fullConfirmedFactualAddressReply &&
|
||||||
|
!exactMatchedFactualAddressReply &&
|
||||||
!runtimeAdjustedExactReply &&
|
!runtimeAdjustedExactReply &&
|
||||||
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
|
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
|
||||||
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
|
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
|
||||||
|
|
|
||||||
|
|
@ -112,8 +112,17 @@ function resolveAddressLaneProtectionArbitration(input) {
|
||||||
const semanticDeepInvestigationHintDetected = semanticGuardHints?.deep_investigation_signal_detected === true;
|
const semanticDeepInvestigationHintDetected = semanticGuardHints?.deep_investigation_signal_detected === true;
|
||||||
const semanticAggregateShapeDetected = semanticExtraction?.query_shape === "AGGREGATE_LOOKUP" ||
|
const semanticAggregateShapeDetected = semanticExtraction?.query_shape === "AGGREGATE_LOOKUP" ||
|
||||||
semanticExtraction?.aggregation_profile === "management_profile";
|
semanticExtraction?.aggregation_profile === "management_profile";
|
||||||
|
const exactSupportedIntentProtectedFromDeepPreference = Boolean(supportedAddressIntentDetected &&
|
||||||
|
resolvedIntent &&
|
||||||
|
ADDRESS_INTENTS_ALLOW_STRICT_DEEP_INVESTIGATION_BYPASS.has(resolvedIntent) &&
|
||||||
|
semanticApplyCanonicalRecommended &&
|
||||||
|
(!strictDeepInvestigationCueDetected || strictDeepInvestigationBypassAllowed));
|
||||||
|
const unsupportedAggregateFollowupOverride = Boolean(followupContext &&
|
||||||
|
llmContractMode === "unsupported" &&
|
||||||
|
(semanticAggregateShapeDetected || !semanticApplyCanonicalRecommended) &&
|
||||||
|
!exactSupportedIntentProtectedFromDeepPreference);
|
||||||
const followupSemanticOverrideToDeepAllowed = Boolean(followupContext &&
|
const followupSemanticOverrideToDeepAllowed = Boolean(followupContext &&
|
||||||
!supportedAddressIntentDetected &&
|
(!supportedAddressIntentDetected || unsupportedAggregateFollowupOverride) &&
|
||||||
(rootContextOnlyFollowup ||
|
(rootContextOnlyFollowup ||
|
||||||
llmContractMode === "unsupported" ||
|
llmContractMode === "unsupported" ||
|
||||||
semanticAggregateShapeDetected ||
|
semanticAggregateShapeDetected ||
|
||||||
|
|
@ -127,11 +136,16 @@ function resolveAddressLaneProtectionArbitration(input) {
|
||||||
!deepAnalysisPreferenceDetected &&
|
!deepAnalysisPreferenceDetected &&
|
||||||
!strictDeepInvestigationCueDetected &&
|
!strictDeepInvestigationCueDetected &&
|
||||||
!semanticAggregateShapeDetected);
|
!semanticAggregateShapeDetected);
|
||||||
|
const unsupportedSpecificLlmIntent = Boolean(llmContractMode === "unsupported" &&
|
||||||
|
llmContractIntent &&
|
||||||
|
llmContractIntent !== "unknown");
|
||||||
const protectAddressLaneFromFallback = Boolean(supportedAddressRouteCandidateDetected &&
|
const protectAddressLaneFromFallback = Boolean(supportedAddressRouteCandidateDetected &&
|
||||||
|
(exactSupportedIntentProtectedFromDeepPreference ||
|
||||||
|
(!unsupportedSpecificLlmIntent &&
|
||||||
!deepAnalysisPreferenceDetected &&
|
!deepAnalysisPreferenceDetected &&
|
||||||
(exactAddressIntentProtectedFromSemanticDeepHint ||
|
(exactAddressIntentProtectedFromSemanticDeepHint ||
|
||||||
!semanticDeepInvestigationHintDetected ||
|
!semanticDeepInvestigationHintDetected ||
|
||||||
strictDeepInvestigationBypassAllowed));
|
strictDeepInvestigationBypassAllowed))));
|
||||||
return {
|
return {
|
||||||
supportedAddressIntentDetected,
|
supportedAddressIntentDetected,
|
||||||
supportedAddressRouteCandidateDetected,
|
supportedAddressRouteCandidateDetected,
|
||||||
|
|
@ -139,6 +153,7 @@ function resolveAddressLaneProtectionArbitration(input) {
|
||||||
semanticAggregateShapeDetected,
|
semanticAggregateShapeDetected,
|
||||||
followupSemanticOverrideToDeepAllowed,
|
followupSemanticOverrideToDeepAllowed,
|
||||||
exactAddressIntentProtectedFromSemanticDeepHint,
|
exactAddressIntentProtectedFromSemanticDeepHint,
|
||||||
|
exactSupportedIntentProtectedFromDeepPreference,
|
||||||
protectAddressLaneFromFallback
|
protectAddressLaneFromFallback
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -282,7 +282,7 @@ exports.INVENTORY_CAPABILITY_CONTRACTS = [
|
||||||
entry_modes: ["root_entry", "root_followup", "clarification_resume"],
|
entry_modes: ["root_entry", "root_followup", "clarification_resume"],
|
||||||
transitions: ["T1", "T2", "T7"],
|
transitions: ["T1", "T2", "T7"],
|
||||||
requiresFocusObject: false,
|
requiresFocusObject: false,
|
||||||
requiredAnchors: ["supplier"],
|
requiredAnchors: [],
|
||||||
resultShape: "supplier_to_stock_item_overlap",
|
resultShape: "supplier_to_stock_item_overlap",
|
||||||
answerObjectShape: "inventory_supplier_overlap",
|
answerObjectShape: "inventory_supplier_overlap",
|
||||||
bundleReusePolicy: "none",
|
bundleReusePolicy: "none",
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,12 @@ export function resolveAddressAsOfDateBasis(
|
||||||
filters: AddressFilterSet,
|
filters: AddressFilterSet,
|
||||||
semanticFrame?: AddressSemanticFrame | null
|
semanticFrame?: AddressSemanticFrame | null
|
||||||
): AddressAsOfDateBasis | null {
|
): AddressAsOfDateBasis | null {
|
||||||
|
if (
|
||||||
|
semanticFrame?.date_scope_kind === "implicit_current" &&
|
||||||
|
semanticFrame.date_basis_hint === "implicit_current_snapshot"
|
||||||
|
) {
|
||||||
|
return "implicit_current_snapshot";
|
||||||
|
}
|
||||||
const asOfDate = normalizeIsoDateHint(filters.as_of_date);
|
const asOfDate = normalizeIsoDateHint(filters.as_of_date);
|
||||||
if (asOfDate) {
|
if (asOfDate) {
|
||||||
return "explicit_as_of_date";
|
return "explicit_as_of_date";
|
||||||
|
|
|
||||||
|
|
@ -181,12 +181,14 @@ function toIsoDate(year: number, month: number, day: number): string | null {
|
||||||
return `${String(year).padStart(4, "0")}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
return `${String(year).padStart(4, "0")}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractAsOfDate(text: string): string | undefined {
|
function hasImplicitCurrentAsOfDateCue(text: string): boolean {
|
||||||
if (
|
return /\b(сегодня|на\s+сегодня|на\s+текущ(?:ую|ая|ий|ее|ей|ем|его)\s+дат(?:у|а|е|ой|ою)|на\s+сегодняшн(?:юю|ий|ей|ем|его)\s+дат(?:у|а|е|ой|ою)|на\s+текущ(?:ий|ую)\s+момент|today|as\s+of\s+today|current\s+date|as\s+of\s+current\s+date)\b/i.test(
|
||||||
/\b(сегодня|на\s+сегодня|на\s+текущ(?:ую|ая|ий|ее|ей|ем|его)\s+дат(?:у|а|е|ой|ою)|на\s+сегодняшн(?:юю|ий|ей|ем|его)\s+дат(?:у|а|е|ой|ою)|на\s+текущ(?:ий|ую)\s+момент|today|as\s+of\s+today|current\s+date|as\s+of\s+current\s+date)\b/i.test(
|
|
||||||
text
|
text
|
||||||
)
|
);
|
||||||
) {
|
}
|
||||||
|
|
||||||
|
function extractAsOfDate(text: string): string | undefined {
|
||||||
|
if (hasImplicitCurrentAsOfDateCue(text)) {
|
||||||
return new Date().toISOString().slice(0, 10);
|
return new Date().toISOString().slice(0, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -751,6 +753,9 @@ function isLowQualityCounterpartyAnchorValue(rawValue: string): boolean {
|
||||||
"ноябрь",
|
"ноябрь",
|
||||||
"декабрь"
|
"декабрь"
|
||||||
]);
|
]);
|
||||||
|
const isLowQualityTimeToken = (token: string): boolean =>
|
||||||
|
lowQualityTimeTokens.has(token) ||
|
||||||
|
/^(?:январ|феврал|март|апрел|ма(?:й|я|е)|июн|июл|август|сентябр|октябр|ноябр|декабр)/iu.test(token);
|
||||||
const lowQualityGenericTokens = new Set([
|
const lowQualityGenericTokens = new Set([
|
||||||
"деньги",
|
"деньги",
|
||||||
"денег",
|
"денег",
|
||||||
|
|
@ -776,7 +781,7 @@ function isLowQualityCounterpartyAnchorValue(rawValue: string): boolean {
|
||||||
const meaningfulNonTemporalTokens = tokens.filter(
|
const meaningfulNonTemporalTokens = tokens.filter(
|
||||||
(token) =>
|
(token) =>
|
||||||
isLikelyCounterpartyToken(token) &&
|
isLikelyCounterpartyToken(token) &&
|
||||||
!lowQualityTimeTokens.has(token) &&
|
!isLowQualityTimeToken(token) &&
|
||||||
!/^(?:19|20)\d{2}$/.test(token)
|
!/^(?:19|20)\d{2}$/.test(token)
|
||||||
);
|
);
|
||||||
if (meaningfulNonTemporalTokens.length === 0 && hasTemporalCue) {
|
if (meaningfulNonTemporalTokens.length === 0 && hasTemporalCue) {
|
||||||
|
|
@ -785,7 +790,7 @@ function isLowQualityCounterpartyAnchorValue(rawValue: string): boolean {
|
||||||
const meaningfulNonGenericTokens = tokens.filter(
|
const meaningfulNonGenericTokens = tokens.filter(
|
||||||
(token) =>
|
(token) =>
|
||||||
isLikelyCounterpartyToken(token) &&
|
isLikelyCounterpartyToken(token) &&
|
||||||
!lowQualityTimeTokens.has(token) &&
|
!isLowQualityTimeToken(token) &&
|
||||||
!lowQualityGenericTokens.has(token) &&
|
!lowQualityGenericTokens.has(token) &&
|
||||||
!/^(?:19|20)\d{2}$/.test(token)
|
!/^(?:19|20)\d{2}$/.test(token)
|
||||||
);
|
);
|
||||||
|
|
@ -1302,6 +1307,9 @@ function isTemporalWarehousePhrase(candidate: string): boolean {
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/ё/g, "е")
|
.replace(/ё/g, "е")
|
||||||
.trim();
|
.trim();
|
||||||
|
if (/^(?:в|на)?\s*(?:сейчас|сегодня|текущ(?:ий|ую|ем|его)\s+момент|данн(?:ый|ую|ом|ого)\s+момент)$/iu.test(normalized)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (/^(?:на\s+)?(?:эту|ту|текущ(?:ую|ая|ий|ее|ей)|сегодняшн(?:юю|ая|ий|ее|ей))(?:\s+же)?\s+дат(?:у|а|е|ой)$/iu.test(normalized)) {
|
if (/^(?:на\s+)?(?:эту|ту|текущ(?:ую|ая|ий|ее|ей)|сегодняшн(?:юю|ая|ий|ее|ей))(?:\s+же)?\s+дат(?:у|а|е|ой)$/iu.test(normalized)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -1355,6 +1363,12 @@ function isLowQualityWarehouseAnchorValue(rawValue: string): boolean {
|
||||||
"лежали",
|
"лежали",
|
||||||
"на",
|
"на",
|
||||||
"по",
|
"по",
|
||||||
|
"компания",
|
||||||
|
"компании",
|
||||||
|
"компанию",
|
||||||
|
"организация",
|
||||||
|
"организации",
|
||||||
|
"организацию",
|
||||||
"складе",
|
"складе",
|
||||||
"складу",
|
"складу",
|
||||||
"складом",
|
"складом",
|
||||||
|
|
@ -1373,7 +1387,11 @@ function isLowQualityWarehouseAnchorValue(rawValue: string): boolean {
|
||||||
if (tokens.length === 0) {
|
if (tokens.length === 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const meaningfulTokens = tokens.filter((token) => !lowQualityTokens.has(token) && token.length > 1);
|
const isLowQualityWarehouseToken = (token: string): boolean =>
|
||||||
|
lowQualityTokens.has(token) ||
|
||||||
|
/^(?:19|20)\d{2}$/.test(token) ||
|
||||||
|
/^(?:январ|феврал|март|апрел|ма(?:й|я|е)|июн|июл|август|сентябр|октябр|ноябр|декабр)/iu.test(token);
|
||||||
|
const meaningfulTokens = tokens.filter((token) => !isLowQualityWarehouseToken(token) && token.length > 1);
|
||||||
if (meaningfulTokens.length === 0) {
|
if (meaningfulTokens.length === 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -1453,16 +1471,18 @@ function extractInventoryWarehouseAnchor(text: string): string | undefined {
|
||||||
|
|
||||||
function extractInventorySupplierAnchor(text: string): string | undefined {
|
function extractInventorySupplierAnchor(text: string): string | undefined {
|
||||||
const match = String(text ?? "").match(
|
const match = String(text ?? "").match(
|
||||||
/(?:от\s+поставщика|у\s+поставщика|поставщика|поставщику)\s+([^\r\n?]+?)(?=$|[?])/iu
|
/(?:от\s+поставщика|у\s+поставщика|поставщик(?:а|у|ом)?|supplier|vendor)\s+([^\r\n?]+?)(?=$|[?])/iu
|
||||||
);
|
);
|
||||||
if (!match?.[1]) {
|
if (!match?.[1]) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const candidate = cleanupAnchorValue(
|
const candidate = cleanupAnchorValue(
|
||||||
cleanupAnchorValue(String(match[1])).replace(
|
cleanupAnchorValue(String(match[1]))
|
||||||
|
.replace(
|
||||||
/\s+(?:сейчас|на\s+склад(?:е|у|ом)?|на\s+дату|по\s+состоянию\s+на\s+дату|котор(?:ый|ые|ых)|куплен(?:ы|а|о)?|были|был|лежат|лежит|еще|ещё|организац\w*|компани\w*).*$/iu,
|
/\s+(?:сейчас|на\s+склад(?:е|у|ом)?|на\s+дату|по\s+состоянию\s+на\s+дату|котор(?:ый|ые|ых)|куплен(?:ы|а|о)?|были|был|лежат|лежит|еще|ещё|организац\w*|компани\w*).*$/iu,
|
||||||
""
|
""
|
||||||
)
|
)
|
||||||
|
.replace(/\s*(?:->|=>|→)\s*(?:товар|позици|номенклатур|item|product|sku)[\s\S]*$/iu, "")
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
!candidate ||
|
!candidate ||
|
||||||
|
|
@ -1595,7 +1615,7 @@ function resolveSemanticDateScopeKind(
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveSemanticDateBasisHint(filters: AddressFilterSet, warnings: string[]): AddressSemanticFrame["date_basis_hint"] {
|
function resolveSemanticDateBasisHint(filters: AddressFilterSet, warnings: string[]): AddressSemanticFrame["date_basis_hint"] {
|
||||||
if (warnings.includes("as_of_date_defaulted_today")) {
|
if (warnings.includes("as_of_date_defaulted_today") || warnings.includes("as_of_date_from_implicit_current_phrase")) {
|
||||||
return "implicit_current_snapshot";
|
return "implicit_current_snapshot";
|
||||||
}
|
}
|
||||||
const hasAsOfDate = typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0;
|
const hasAsOfDate = typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0;
|
||||||
|
|
@ -1710,6 +1730,7 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const warnings: string[] = [];
|
const warnings: string[] = [];
|
||||||
|
const implicitCurrentAsOfDateCue = hasImplicitCurrentAsOfDateCue(text);
|
||||||
const explicitAsOfDate = extractAsOfDate(text);
|
const explicitAsOfDate = extractAsOfDate(text);
|
||||||
const explicitAsOfDateWithCue = extractAsOfDateWithCue(text);
|
const explicitAsOfDateWithCue = extractAsOfDateWithCue(text);
|
||||||
|
|
||||||
|
|
@ -1756,6 +1777,13 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
|
||||||
filters.counterparty = supplierAnchor;
|
filters.counterparty = supplierAnchor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (intent === "inventory_purchase_to_sale_chain") {
|
||||||
|
const supplierAnchor = asksForInventorySupplierIdentity(text) ? undefined : extractInventorySupplierAnchor(text);
|
||||||
|
if (supplierAnchor) {
|
||||||
|
filters.counterparty = supplierAnchor;
|
||||||
|
warnings.push("supplier_anchor_derived_for_inventory_documentary_chain");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const allowGenericCounterpartyAnchor = !isInventoryTraceIntent(intent);
|
const allowGenericCounterpartyAnchor = !isInventoryTraceIntent(intent);
|
||||||
const counterpartyMatch = allowGenericCounterpartyAnchor ? text.match(COUNTERPARTY_PATTERN) : null;
|
const counterpartyMatch = allowGenericCounterpartyAnchor ? text.match(COUNTERPARTY_PATTERN) : null;
|
||||||
|
|
@ -1923,6 +1951,9 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
|
||||||
|
|
||||||
if (usesAsOfPrimaryWindow(intent) && explicitAsOfDate) {
|
if (usesAsOfPrimaryWindow(intent) && explicitAsOfDate) {
|
||||||
filters.as_of_date = explicitAsOfDate;
|
filters.as_of_date = explicitAsOfDate;
|
||||||
|
if (implicitCurrentAsOfDateCue && !warnings.includes("as_of_date_from_implicit_current_phrase")) {
|
||||||
|
warnings.push("as_of_date_from_implicit_current_phrase");
|
||||||
|
}
|
||||||
const periodWasDerivedHeuristically =
|
const periodWasDerivedHeuristically =
|
||||||
warnings.includes("period_derived_from_month_phrase") ||
|
warnings.includes("period_derived_from_month_phrase") ||
|
||||||
warnings.includes("period_derived_from_year_range_phrase") ||
|
warnings.includes("period_derived_from_year_range_phrase") ||
|
||||||
|
|
@ -1941,6 +1972,9 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
|
||||||
const asOfDate = extractAsOfDate(text);
|
const asOfDate = extractAsOfDate(text);
|
||||||
if (asOfDate) {
|
if (asOfDate) {
|
||||||
filters.as_of_date = asOfDate;
|
filters.as_of_date = asOfDate;
|
||||||
|
if (implicitCurrentAsOfDateCue && !warnings.includes("as_of_date_from_implicit_current_phrase")) {
|
||||||
|
warnings.push("as_of_date_from_implicit_current_phrase");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1732,6 +1732,18 @@ function hasInventoryPurchaseDocumentsSignalV2(text: string): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasInventorySaleTraceSignalV2(text: string): boolean {
|
function hasInventorySaleTraceSignalV2(text: string): boolean {
|
||||||
|
const value = String(text ?? "");
|
||||||
|
const hasPlainItemCue =
|
||||||
|
/(?:\u0442\u043e\u0432\u0430\u0440|\u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442\u0443\u0440|\u043f\u043e\u0437\u0438\u0446|\u043f\u0440\u043e\u0434\u0443\u043a\u0446\u0438|sku|item|product)/iu.test(
|
||||||
|
value
|
||||||
|
);
|
||||||
|
const hasPlainTraceCue =
|
||||||
|
/(?:\u043a\u043e\u043c\u0443\s+(?:\u0432\s+\u0438\u0442\u043e\u0433\u0435\s+)?(?:\u043c\u044b\s+)?(?:\u043f\u0440\u043e\u0434\u0430\u043b\u0438|\u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043b\u0438|\u0432\u043f\u0430\u0440\u0438\u043b\u0438)|\u043a\u043e\u043c\u0443\s+(?:\u0431\u044b\u043b[\u0430\u0438\u043e]?|\u0431\u044b\u043b\u0438)?\s*(?:\u043f\u0440\u043e\u0434\u0430\u043d|\u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d)|\u043a\u0442\u043e\s+\u043a\u0443\u043f\u0438\u043b|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b|buyer|sale\s+trace|trace\s+of\s+sale)/iu.test(
|
||||||
|
value
|
||||||
|
);
|
||||||
|
if (hasPlainItemCue && (hasPlainTraceCue || hasInventorySaleCue(value))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const hasItemCue = /(?:товар|номенклатур|sku|item|product|позици(?:я|ю|и)|продукци(?:я|ю|и))/iu.test(text);
|
const hasItemCue = /(?:товар|номенклатур|sku|item|product|позици(?:я|ю|и)|продукци(?:я|ю|и))/iu.test(text);
|
||||||
const hasTraceCue =
|
const hasTraceCue =
|
||||||
/(?:кому\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали|кому\s+был\s+продан|куда\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали(?:\s+(?:это|его|товар|позицию))?|куда\s+(?:была\s+)?реализована\s+(?:позиция|номенклатура|продукция)|кто\s+купил|buyer|sale\s+trace|trace\s+of\s+sale|через\s+какие\s+документы\s+прош[её]л\s+путь\s+товара|закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*warehouse\s*->\s*sale|purchase\s*->\s*stock\s*->\s*sale)/iu.test(
|
/(?:кому\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали|кому\s+был\s+продан|куда\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали(?:\s+(?:это|его|товар|позицию))?|куда\s+(?:была\s+)?реализована\s+(?:позиция|номенклатура|продукция)|кто\s+купил|buyer|sale\s+trace|trace\s+of\s+sale|через\s+какие\s+документы\s+прош[её]л\s+путь\s+товара|закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*warehouse\s*->\s*sale|purchase\s*->\s*stock\s*->\s*sale)/iu.test(
|
||||||
|
|
@ -1766,6 +1778,15 @@ function hasInventoryAgingSignal(text: string): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasInventoryPurchaseToSaleChainSignal(text: string): boolean {
|
function hasInventoryPurchaseToSaleChainSignal(text: string): boolean {
|
||||||
|
const value = String(text ?? "");
|
||||||
|
const hasPlainItemCue = /(?:товар|номенклатур|позици|sku|item|product)/iu.test(value);
|
||||||
|
const hasPlainChainCue =
|
||||||
|
/(?:закупк[а-яё]*\s*->\s*склад\s*->\s*продаж|закупк[а-яё]*[\s\S]{0,80}склад[\s\S]{0,80}продаж|через\s+какие\s+документы\s+прош[её]л\s+путь|путь\s+товар[а-яё]*[\s\S]{0,80}закуп|цепочк[а-яё]*\s+движен|документально\s+подтвержденн[а-яё]*\s+цепочк|supplier\s*->\s*item\s*->\s*(?:buyer|customer)|supplier\s+to\s+buyer|supplier\s+to\s+item\s+to\s+buyer|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*(?:warehouse|stock)\s*->\s*sale)/iu.test(
|
||||||
|
value
|
||||||
|
) || value.includes("->");
|
||||||
|
if (hasPlainItemCue && hasPlainChainCue) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text);
|
const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text);
|
||||||
const hasChainCue =
|
const hasChainCue =
|
||||||
/(?:закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*(?:warehouse|stock)\s*->\s*sale|закупка\s*->\s*склад\s*->\s*продажа|цепочк[аи]\s+движен|документально\s+подтвержденн\w+\s+цепочк|supplier\s*->\s*item\s*->\s*(?:buyer|customer)|supplier\s+to\s+buyer|supplier\s+to\s+item\s+to\s+buyer)/iu.test(
|
/(?:закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*(?:warehouse|stock)\s*->\s*sale|закупка\s*->\s*склад\s*->\s*продажа|цепочк[аи]\s+движен|документально\s+подтвержденн\w+\s+цепочк|supplier\s*->\s*item\s*->\s*(?:buyer|customer)|supplier\s+to\s+buyer|supplier\s+to\s+item\s+to\s+buyer)/iu.test(
|
||||||
|
|
@ -2030,6 +2051,17 @@ function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolutio
|
||||||
const hasRankingCue = /(?:топ|ранк|сам(?:ый|ая|ое|ые)|больше\s+всего|наибольш|крупн|жирн|max|top|rank)/iu.test(
|
const hasRankingCue = /(?:топ|ранк|сам(?:ый|ая|ое|ые)|больше\s+всего|наибольш|крупн|жирн|max|top|rank)/iu.test(
|
||||||
normalized
|
normalized
|
||||||
);
|
);
|
||||||
|
const hasInventoryPurchaseToSaleDocumentChainCue =
|
||||||
|
/(?:закупк[а-яё]*[\s\S]{0,80}склад[\s\S]{0,80}продаж|путь\s+товар[а-яё]*[\s\S]{0,80}закуп|purchase\s*->\s*(?:warehouse|stock)\s*->\s*sale|->\s*(?:склад|warehouse|stock)\s*->\s*(?:продаж|sale))/iu.test(
|
||||||
|
normalized
|
||||||
|
) && /(?:товар|позици|номенклатур|sku|item|product)/iu.test(normalized);
|
||||||
|
if (hasInventoryPurchaseToSaleDocumentChainCue) {
|
||||||
|
return unicodeBridgeResolution(
|
||||||
|
"inventory_purchase_to_sale_chain",
|
||||||
|
"high",
|
||||||
|
"unicode_inventory_purchase_to_sale_chain_bridge_signal_detected"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const hasOpenItemsAccountCue =
|
const hasOpenItemsAccountCue =
|
||||||
/(?:хвост|долг|незакрыт|вис)/iu.test(normalized) &&
|
/(?:хвост|долг|незакрыт|вис)/iu.test(normalized) &&
|
||||||
|
|
|
||||||
|
|
@ -146,13 +146,21 @@ function hasInventoryProvenanceSignalV2(text: string): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasInventoryPurchaseDateSignal(text: string): boolean {
|
function hasInventoryPurchaseDateSignal(text: string): boolean {
|
||||||
|
const value = String(text ?? "");
|
||||||
const hasItemCue =
|
const hasItemCue =
|
||||||
/(?:товар|номенклатур|sku|item|product)/iu.test(text) || hasSelectedObjectInventoryCue(text);
|
/(?:\u0442\u043e\u0432\u0430\u0440|\u043f\u043e\u0437\u0438\u0446|\u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442\u0443\u0440|sku|item|product)/iu.test(
|
||||||
const hasPurchaseDateCue =
|
value
|
||||||
/(?:когда\s+(?:примерно\s+)?(?:мы\s+)?купили|когда\s+был\s+куплен|когда\s+куплен|дата\s+закупк|purchase\s+date)/iu.test(
|
|
||||||
text
|
|
||||||
) ||
|
) ||
|
||||||
/(?:когда\s+был(?:а|и|о)?\s+закупк\w*|когда\s+закупк\w*)/iu.test(text);
|
/(?:товар|номенклатур|sku|item|product)/iu.test(value) ||
|
||||||
|
hasSelectedObjectInventoryCue(value);
|
||||||
|
const hasPurchaseDateCue =
|
||||||
|
/(?:\u043a\u043e\u0433\u0434\u0430\s+(?:\u043f\u0440\u0438\u043c\u0435\u0440\u043d\u043e\s+)?(?:\u043c\u044b\s+)?\u043a\u0443\u043f\u0438\u043b\u0438|\u043a\u043e\u0433\u0434\u0430\s+\u0431\u044b\u043b(?:\u0430|\u0438|\u043e)?\s+\u043a\u0443\u043f\u043b\u0435\u043d|\u043a\u043e\u0433\u0434\u0430\s+\u043a\u0443\u043f\u043b\u0435\u043d|\u0434\u0430\u0442\u0430\s+\u0437\u0430\u043a\u0443\u043f\u043a|purchase\s+date)/iu.test(
|
||||||
|
value
|
||||||
|
) ||
|
||||||
|
/(?:когда\s+(?:примерно\s+)?(?:мы\s+)?купили|когда\s+был\s+куплен|когда\s+куплен|дата\s+закупк|purchase\s+date)/iu.test(
|
||||||
|
value
|
||||||
|
) ||
|
||||||
|
/(?:когда\s+был(?:а|и|о)?\s+закупк\w*|когда\s+закупк\w*)/iu.test(value);
|
||||||
return hasItemCue && hasPurchaseDateCue;
|
return hasItemCue && hasPurchaseDateCue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -177,7 +185,7 @@ function hasInventorySaleTraceSignalV2(text: string): boolean {
|
||||||
const value = String(text ?? "");
|
const value = String(text ?? "");
|
||||||
const hasPlainItemCue = /(?:товар|номенклатур|позици|продукци|sku|item|product)/iu.test(value);
|
const hasPlainItemCue = /(?:товар|номенклатур|позици|продукци|sku|item|product)/iu.test(value);
|
||||||
const hasPlainTraceCue =
|
const hasPlainTraceCue =
|
||||||
/(?:кому\s+(?:в\s+итоге\s+)?(?:мы\s+)?(?:продали|реализовали|впарили)|кому\s+(?:был[аио]?|были)?\s*реализован|кто\s+купил|покупател|buyer|sale\s+trace|trace\s+of\s+sale)/iu.test(
|
/(?:кому\s+(?:в\s+итоге\s+)?(?:мы\s+)?(?:продали|реализовали|впарили)|кому\s+(?:был[аио]?|были)?\s*(?:продан|реализован)|кто\s+купил|покупател|buyer|sale\s+trace|trace\s+of\s+sale)/iu.test(
|
||||||
value
|
value
|
||||||
);
|
);
|
||||||
if (hasPlainItemCue && hasPlainTraceCue) {
|
if (hasPlainItemCue && hasPlainTraceCue) {
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,8 @@ interface NormalizedAddressRow {
|
||||||
item?: string | null;
|
item?: string | null;
|
||||||
warehouse?: string | null;
|
warehouse?: string | null;
|
||||||
organization?: string | null;
|
organization?: string | null;
|
||||||
|
counterparty?: string | null;
|
||||||
|
contract?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AddressTryHandleOptions {
|
interface AddressTryHandleOptions {
|
||||||
|
|
@ -350,6 +352,21 @@ function deriveTaxQuarterWindowForDate(value: unknown): { period_from: string; p
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function deriveMonthWindowForDate(value: unknown): { period_from: string; period_to: string } | null {
|
||||||
|
const isoDate = normalizeIsoDateForQuery(value);
|
||||||
|
if (!isoDate) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const match = isoDate.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
period_from: `${match[1]}-${match[2]}-01`,
|
||||||
|
period_to: isoDate
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function toDateTimeExprForQuery(isoDate: string): string | null {
|
function toDateTimeExprForQuery(isoDate: string): string | null {
|
||||||
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
|
|
@ -1446,6 +1463,8 @@ function toNormalizedRows(rows: Array<Record<string, unknown>>): NormalizedAddre
|
||||||
row.organization_name,
|
row.organization_name,
|
||||||
row.ОрганизацияПредставление
|
row.ОрганизацияПредставление
|
||||||
);
|
);
|
||||||
|
const counterparty = firstNonEmptyString(row.Контрагент, row.Counterparty, row.counterparty);
|
||||||
|
const contract = firstNonEmptyString(row.Договор, row.Contract, row.contract);
|
||||||
const analytics = collectAnalyticsStrings(row);
|
const analytics = collectAnalyticsStrings(row);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -1458,7 +1477,9 @@ function toNormalizedRows(rows: Array<Record<string, unknown>>): NormalizedAddre
|
||||||
quantity,
|
quantity,
|
||||||
item,
|
item,
|
||||||
warehouse,
|
warehouse,
|
||||||
organization
|
organization,
|
||||||
|
counterparty,
|
||||||
|
contract
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((item) => Boolean(item.period || item.registrator));
|
.filter((item) => Boolean(item.period || item.registrator));
|
||||||
|
|
@ -1526,6 +1547,10 @@ function formatMoneyRubForReply(value: number): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractContractNameFromNormalizedRow(row: NormalizedAddressRow): string | null {
|
function extractContractNameFromNormalizedRow(row: NormalizedAddressRow): string | null {
|
||||||
|
const explicitContract = firstNonEmptyString(row.contract);
|
||||||
|
if (explicitContract) {
|
||||||
|
return explicitContract;
|
||||||
|
}
|
||||||
for (const token of row.analytics) {
|
for (const token of row.analytics) {
|
||||||
const normalized = String(token ?? "").trim();
|
const normalized = String(token ?? "").trim();
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
|
|
@ -1898,6 +1923,11 @@ function applyPreExecutionOrganizationScopeGrounding(input: {
|
||||||
]);
|
]);
|
||||||
const resolvedOrganizationFromMessage = resolveOrganizationSelectionFromMessage(input.userMessage, candidateOrganizations);
|
const resolvedOrganizationFromMessage = resolveOrganizationSelectionFromMessage(input.userMessage, candidateOrganizations);
|
||||||
const referentialOrganizationScopeDetected = hasReferentialOrganizationScopeSignal(input.userMessage);
|
const referentialOrganizationScopeDetected = hasReferentialOrganizationScopeSignal(input.userMessage);
|
||||||
|
const counterpartyAnchorProtectsOrganizationScope =
|
||||||
|
input.semanticFrame?.anchor_kind === "counterparty" &&
|
||||||
|
typeof input.filters.counterparty === "string" &&
|
||||||
|
input.filters.counterparty.trim().length > 0 &&
|
||||||
|
!referentialOrganizationScopeDetected;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!input.filters.organization &&
|
!input.filters.organization &&
|
||||||
|
|
@ -1916,6 +1946,7 @@ function applyPreExecutionOrganizationScopeGrounding(input: {
|
||||||
if (
|
if (
|
||||||
resolvedOrganizationFromMessage &&
|
resolvedOrganizationFromMessage &&
|
||||||
(!input.filters.organization || input.semanticFrame?.anchor_kind === "organization") &&
|
(!input.filters.organization || input.semanticFrame?.anchor_kind === "organization") &&
|
||||||
|
!counterpartyAnchorProtectsOrganizationScope &&
|
||||||
!sameNormalizedOrganizationScope(input.filters.organization ?? null, resolvedOrganizationFromMessage)
|
!sameNormalizedOrganizationScope(input.filters.organization ?? null, resolvedOrganizationFromMessage)
|
||||||
) {
|
) {
|
||||||
input.filters.organization = resolvedOrganizationFromMessage;
|
input.filters.organization = resolvedOrganizationFromMessage;
|
||||||
|
|
@ -2377,6 +2408,12 @@ function hasExplicitPeriodWindow(filters: AddressFilterSet): boolean {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function asksForUnresolvedInventorySupplierLink(userMessage: string | null | undefined): boolean {
|
||||||
|
return /(?:\u0431\u0435\u0437\s+\u043f\u043e\u043d\u044f\u0442\u043d[^\s]*\s+\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0431\u0435\u0437\s+(?:\u044f\u0432\u043d[^\s]*\s+)?\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043d\u0435\s+\u0438\u043c\u0435\u044e\u0442\s+\u044f\u0432\u043d[^\s]*\s+\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043d\u0435\u0442\s+\u044f\u0432\u043d[^\s]*\s+\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|unresolved\s+supplier\s+link)/iu.test(
|
||||||
|
String(userMessage ?? "")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function canAutoBroadenPeriodWindow(intent: AddressIntent, filters: AddressFilterSet): boolean {
|
function canAutoBroadenPeriodWindow(intent: AddressIntent, filters: AddressFilterSet): boolean {
|
||||||
const hasRecoverableAsOfOnlyWindow =
|
const hasRecoverableAsOfOnlyWindow =
|
||||||
!hasExplicitPeriodWindow(filters) &&
|
!hasExplicitPeriodWindow(filters) &&
|
||||||
|
|
@ -2426,6 +2463,7 @@ function shouldDetachLifecycleExecutionFromSnapshotContext(
|
||||||
reasons: string[]
|
reasons: string[]
|
||||||
): boolean {
|
): boolean {
|
||||||
if (
|
if (
|
||||||
|
intent !== "inventory_supplier_stock_overlap_as_of_date" &&
|
||||||
intent !== "inventory_sale_trace_for_item" &&
|
intent !== "inventory_sale_trace_for_item" &&
|
||||||
intent !== "inventory_purchase_to_sale_chain"
|
intent !== "inventory_purchase_to_sale_chain"
|
||||||
) {
|
) {
|
||||||
|
|
@ -2433,6 +2471,8 @@ function shouldDetachLifecycleExecutionFromSnapshotContext(
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
reasons.includes("period_window_semantic_from_inventory_snapshot_context") ||
|
||||||
|
reasons.includes("period_window_semantic_from_inventory_as_of_month") ||
|
||||||
reasons.includes("as_of_date_from_followup_context") ||
|
reasons.includes("as_of_date_from_followup_context") ||
|
||||||
reasons.includes("period_from_followup_context") ||
|
reasons.includes("period_from_followup_context") ||
|
||||||
reasons.includes("as_of_date_from_open_items_followup_context")
|
reasons.includes("as_of_date_from_open_items_followup_context")
|
||||||
|
|
@ -3506,6 +3546,91 @@ 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 chainCounterpartyAnchor = toNonEmptyFilterValue(filters.extracted_filters.counterparty);
|
||||||
|
const chainOrganizationAnchor = toNonEmptyFilterValue(filters.extracted_filters.organization);
|
||||||
|
if (
|
||||||
|
intent.intent === "inventory_purchase_to_sale_chain" &&
|
||||||
|
chainCounterpartyAnchor &&
|
||||||
|
chainOrganizationAnchor &&
|
||||||
|
sameOrganizationEntityReference(chainOrganizationAnchor, chainCounterpartyAnchor)
|
||||||
|
) {
|
||||||
|
delete filters.extracted_filters.organization;
|
||||||
|
const restoredOrganization = activeOrganization ?? previousOrganizationFromContext;
|
||||||
|
if (restoredOrganization && !sameOrganizationEntityReference(restoredOrganization, chainCounterpartyAnchor)) {
|
||||||
|
filters.extracted_filters.organization = restoredOrganization;
|
||||||
|
if (!filters.warnings.includes("organization_restored_from_inventory_chain_context")) {
|
||||||
|
filters.warnings.push("organization_restored_from_inventory_chain_context");
|
||||||
|
}
|
||||||
|
if (!baseReasons.includes("organization_restored_from_inventory_chain_context")) {
|
||||||
|
baseReasons.push("organization_restored_from_inventory_chain_context");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!filters.warnings.includes("organization_cleared_from_inventory_chain_counterparty_anchor")) {
|
||||||
|
filters.warnings.push("organization_cleared_from_inventory_chain_counterparty_anchor");
|
||||||
|
}
|
||||||
|
if (!baseReasons.includes("organization_cleared_from_inventory_chain_counterparty_anchor")) {
|
||||||
|
baseReasons.push("organization_cleared_from_inventory_chain_counterparty_anchor");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
intent.intent === "inventory_supplier_stock_overlap_as_of_date" &&
|
||||||
|
(followupContext?.root_filters || followupContext?.previous_filters) &&
|
||||||
|
!toNonEmptyFilterValue(filters.extracted_filters.period_from) &&
|
||||||
|
!toNonEmptyFilterValue(filters.extracted_filters.period_to) &&
|
||||||
|
!/(?:за\s+вс[её]\s+время|за\s+любой\s+период|all[\s-]?time|all\s+periods?)/iu.test(userMessage)
|
||||||
|
) {
|
||||||
|
const snapshotContextFilters =
|
||||||
|
followupContext?.root_filters &&
|
||||||
|
(toNonEmptyFilterValue(followupContext.root_filters.period_from) ||
|
||||||
|
toNonEmptyFilterValue(followupContext.root_filters.period_to))
|
||||||
|
? followupContext.root_filters
|
||||||
|
: followupContext?.previous_filters;
|
||||||
|
const previousPeriodFrom = toNonEmptyFilterValue(snapshotContextFilters?.period_from);
|
||||||
|
const previousPeriodTo = toNonEmptyFilterValue(snapshotContextFilters?.period_to);
|
||||||
|
if (previousPeriodFrom || previousPeriodTo) {
|
||||||
|
filters.extracted_filters = {
|
||||||
|
...filters.extracted_filters,
|
||||||
|
...(previousPeriodFrom ? { period_from: previousPeriodFrom } : {}),
|
||||||
|
...(previousPeriodTo ? { period_to: previousPeriodTo } : {})
|
||||||
|
};
|
||||||
|
if (!toNonEmptyFilterValue(filters.extracted_filters.as_of_date)) {
|
||||||
|
const inheritedAsOfDate =
|
||||||
|
toNonEmptyFilterValue(snapshotContextFilters?.as_of_date) ?? previousPeriodTo ?? previousPeriodFrom;
|
||||||
|
if (inheritedAsOfDate) {
|
||||||
|
filters.extracted_filters.as_of_date = inheritedAsOfDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!filters.warnings.includes("period_window_semantic_from_inventory_snapshot_context")) {
|
||||||
|
filters.warnings.push("period_window_semantic_from_inventory_snapshot_context");
|
||||||
|
}
|
||||||
|
if (!baseReasons.includes("period_window_semantic_from_inventory_snapshot_context")) {
|
||||||
|
baseReasons.push("period_window_semantic_from_inventory_snapshot_context");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
intent.intent === "inventory_supplier_stock_overlap_as_of_date" &&
|
||||||
|
!toNonEmptyFilterValue(filters.extracted_filters.period_from) &&
|
||||||
|
!toNonEmptyFilterValue(filters.extracted_filters.period_to) &&
|
||||||
|
asksForUnresolvedInventorySupplierLink(userMessage) &&
|
||||||
|
!/(?:за\s+вс[её]\s+время|за\s+любой\s+период|all[\s-]?time|all\s+periods?)/iu.test(userMessage)
|
||||||
|
) {
|
||||||
|
const monthWindow = deriveMonthWindowForDate(filters.extracted_filters.as_of_date);
|
||||||
|
if (monthWindow) {
|
||||||
|
filters.extracted_filters = {
|
||||||
|
...filters.extracted_filters,
|
||||||
|
...monthWindow
|
||||||
|
};
|
||||||
|
if (!filters.warnings.includes("period_window_semantic_from_inventory_as_of_month")) {
|
||||||
|
filters.warnings.push("period_window_semantic_from_inventory_as_of_month");
|
||||||
|
}
|
||||||
|
if (!baseReasons.includes("period_window_semantic_from_inventory_as_of_month")) {
|
||||||
|
baseReasons.push("period_window_semantic_from_inventory_as_of_month");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
isOrganizationScopedValueFlowIntent(intent.intent) &&
|
isOrganizationScopedValueFlowIntent(intent.intent) &&
|
||||||
hasExplicitSingleOrganizationValueFlowScopeRequest(userMessage) &&
|
hasExplicitSingleOrganizationValueFlowScopeRequest(userMessage) &&
|
||||||
|
|
@ -3678,6 +3803,7 @@ export class AddressQueryService {
|
||||||
const detachedExecutionFilters: AddressFilterSet = { ...executionFilters };
|
const detachedExecutionFilters: AddressFilterSet = { ...executionFilters };
|
||||||
let periodDetached = false;
|
let periodDetached = false;
|
||||||
let asOfDetached = false;
|
let asOfDetached = false;
|
||||||
|
const keepAsOfDateForInventorySnapshotOverlap = intent.intent === "inventory_supplier_stock_overlap_as_of_date";
|
||||||
if (
|
if (
|
||||||
toNonEmptyFilterValue(detachedExecutionFilters.period_from) ||
|
toNonEmptyFilterValue(detachedExecutionFilters.period_from) ||
|
||||||
toNonEmptyFilterValue(detachedExecutionFilters.period_to)
|
toNonEmptyFilterValue(detachedExecutionFilters.period_to)
|
||||||
|
|
@ -3686,7 +3812,7 @@ export class AddressQueryService {
|
||||||
delete detachedExecutionFilters.period_to;
|
delete detachedExecutionFilters.period_to;
|
||||||
periodDetached = true;
|
periodDetached = true;
|
||||||
}
|
}
|
||||||
if (toNonEmptyFilterValue(detachedExecutionFilters.as_of_date)) {
|
if (!keepAsOfDateForInventorySnapshotOverlap && toNonEmptyFilterValue(detachedExecutionFilters.as_of_date)) {
|
||||||
delete detachedExecutionFilters.as_of_date;
|
delete detachedExecutionFilters.as_of_date;
|
||||||
asOfDetached = true;
|
asOfDetached = true;
|
||||||
}
|
}
|
||||||
|
|
@ -4231,6 +4357,20 @@ export class AddressQueryService {
|
||||||
: anchor.anchor_type === "contract" && anchor.anchor_value_resolved
|
: anchor.anchor_type === "contract" && anchor.anchor_value_resolved
|
||||||
? { ...executionFilters, contract: anchor.anchor_value_resolved }
|
? { ...executionFilters, contract: anchor.anchor_value_resolved }
|
||||||
: executionFilters;
|
: executionFilters;
|
||||||
|
if (
|
||||||
|
intent.intent === "inventory_purchase_to_sale_chain" &&
|
||||||
|
toNonEmptyFilterValue(filtersForMatching.item) &&
|
||||||
|
toNonEmptyFilterValue(filtersForMatching.counterparty)
|
||||||
|
) {
|
||||||
|
filtersForMatching = { ...filtersForMatching };
|
||||||
|
delete filtersForMatching.counterparty;
|
||||||
|
if (!filters.warnings.includes("inventory_chain_counterparty_anchor_kept_for_verification")) {
|
||||||
|
filters.warnings.push("inventory_chain_counterparty_anchor_kept_for_verification");
|
||||||
|
}
|
||||||
|
if (!baseReasons.includes("inventory_chain_counterparty_anchor_kept_for_verification")) {
|
||||||
|
baseReasons.push("inventory_chain_counterparty_anchor_kept_for_verification");
|
||||||
|
}
|
||||||
|
}
|
||||||
const accountScopeAudit = buildAccountScopeAudit({
|
const accountScopeAudit = buildAccountScopeAudit({
|
||||||
intent: intent.intent,
|
intent: intent.intent,
|
||||||
filters: filtersForMatching,
|
filters: filtersForMatching,
|
||||||
|
|
|
||||||
|
|
@ -1334,6 +1334,29 @@ function buildInventoryPurchaseDocumentQuery(filters: AddressFilterSet, resolved
|
||||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stripTrailingOrderBy(query: string): string {
|
||||||
|
return String(query ?? "").replace(/\r?\nУПОРЯДОЧИТЬ ПО[\s\S]*$/u, "").trimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeTopLimit(query: string): string {
|
||||||
|
return String(query ?? "").replace(/ВЫБРАТЬ ПЕРВЫЕ\s+\d+/u, "ВЫБРАТЬ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildInventoryPurchaseToSaleDocumentQuery(filters: AddressFilterSet, resolvedLimit: number): string {
|
||||||
|
const purchaseQuery = removeTopLimit(stripTrailingOrderBy(buildInventoryPurchaseDocumentQuery(filters, resolvedLimit)));
|
||||||
|
const saleQuery = removeTopLimit(stripTrailingOrderBy(buildInventorySaleDocumentQuery(filters, resolvedLimit)));
|
||||||
|
return [
|
||||||
|
purchaseQuery,
|
||||||
|
"",
|
||||||
|
"ОБЪЕДИНИТЬ ВСЕ",
|
||||||
|
"",
|
||||||
|
saleQuery,
|
||||||
|
"",
|
||||||
|
"УПОРЯДОЧИТЬ ПО",
|
||||||
|
` Период ${resolveOrderDirection(filters.sort)}`
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
export function buildCounterpartyPurchaseDocumentQuery(filters: AddressFilterSet, resolvedLimit: number): string {
|
export function buildCounterpartyPurchaseDocumentQuery(filters: AddressFilterSet, resolvedLimit: number): string {
|
||||||
const goodsCounterpartyCondition = buildCounterpartyReferenceCondition(filters, ["Товары.Ссылка.Контрагент"]);
|
const goodsCounterpartyCondition = buildCounterpartyReferenceCondition(filters, ["Товары.Ссылка.Контрагент"]);
|
||||||
const servicesCounterpartyCondition = buildCounterpartyReferenceCondition(filters, ["Услуги.Ссылка.Контрагент"]);
|
const servicesCounterpartyCondition = buildCounterpartyReferenceCondition(filters, ["Услуги.Ссылка.Контрагент"]);
|
||||||
|
|
@ -1628,9 +1651,9 @@ export function buildAddressRecipePlan(
|
||||||
: recipe.query_template === "inventory_supplier_stock_overlap_profile"
|
: recipe.query_template === "inventory_supplier_stock_overlap_profile"
|
||||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||||
: recipe.query_template === "inventory_sale_trace_profile"
|
: recipe.query_template === "inventory_sale_trace_profile"
|
||||||
? buildInventoryMovementQuery(filters, resolvedLimit, "kt")
|
? buildInventorySaleDocumentQuery(filters, resolvedLimit)
|
||||||
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
||||||
? buildInventoryMovementQuery(filters, resolvedLimit, "either")
|
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||||
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
||||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||||
: recipe.query_template === "contracts_by_counterparty_profile"
|
: recipe.query_template === "contracts_by_counterparty_profile"
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ export interface ComposeStageRow {
|
||||||
item?: string | null;
|
item?: string | null;
|
||||||
warehouse?: string | null;
|
warehouse?: string | null;
|
||||||
organization?: string | null;
|
organization?: string | null;
|
||||||
|
counterparty?: string | null;
|
||||||
|
contract?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VatDirectSourceProbeItem {
|
export interface VatDirectSourceProbeItem {
|
||||||
|
|
@ -1098,6 +1100,18 @@ function extractInventoryCounterpartyCandidates(row: ComposeStageRow, excludedTo
|
||||||
}
|
}
|
||||||
candidates.push(normalized);
|
candidates.push(normalized);
|
||||||
}
|
}
|
||||||
|
const explicitCounterparty = normalizeCounterpartyDisplayLabel(row.counterparty);
|
||||||
|
const explicitComparable = normalizeEntityToken(explicitCounterparty);
|
||||||
|
if (
|
||||||
|
explicitCounterparty &&
|
||||||
|
explicitComparable &&
|
||||||
|
explicitComparable !== itemToken &&
|
||||||
|
explicitComparable !== warehouseToken &&
|
||||||
|
explicitComparable !== organizationToken &&
|
||||||
|
!excludedComparableTokens.includes(explicitComparable)
|
||||||
|
) {
|
||||||
|
candidates.unshift(explicitCounterparty);
|
||||||
|
}
|
||||||
return uniqueStrings(candidates);
|
return uniqueStrings(candidates);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1156,6 +1170,7 @@ function summarizeInventoryTraceRows(rows: ComposeStageRow[], excludedCounterpar
|
||||||
function formatInventoryTraceRows(rows: ComposeStageRow[], limit = 10, excludedCounterpartyTokens: string[] = []): string[] {
|
function formatInventoryTraceRows(rows: ComposeStageRow[], limit = 10, excludedCounterpartyTokens: string[] = []): string[] {
|
||||||
return rows.slice(0, limit).map((row, index) => {
|
return rows.slice(0, limit).map((row, index) => {
|
||||||
const parties = extractInventoryCounterpartyCandidates(row, excludedCounterpartyTokens);
|
const parties = extractInventoryCounterpartyCandidates(row, excludedCounterpartyTokens);
|
||||||
|
const item = extractInventoryItemName(row);
|
||||||
const warehouse = extractInventoryWarehouseName(row);
|
const warehouse = extractInventoryWarehouseName(row);
|
||||||
const organization = extractInventoryOrganizationName(row);
|
const organization = extractInventoryOrganizationName(row);
|
||||||
const amount =
|
const amount =
|
||||||
|
|
@ -1165,6 +1180,9 @@ function formatInventoryTraceRows(rows: ComposeStageRow[], limit = 10, excludedC
|
||||||
`дата: ${inventoryTraceDateLabel(row.period)}`,
|
`дата: ${inventoryTraceDateLabel(row.period)}`,
|
||||||
`сумма: ${amount}`
|
`сумма: ${amount}`
|
||||||
];
|
];
|
||||||
|
if (item) {
|
||||||
|
parts.push(`товар: ${item}`);
|
||||||
|
}
|
||||||
if (warehouse) {
|
if (warehouse) {
|
||||||
parts.push(`склад: ${warehouse}`);
|
parts.push(`склад: ${warehouse}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,65 @@ interface InventoryReplyDeps {
|
||||||
isInventorySaleMovement: (row: ComposeStageRow) => boolean;
|
isInventorySaleMovement: (row: ComposeStageRow) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cleanupInventoryRequestedParty(value: string): string | null {
|
||||||
|
const cleaned = String(value ?? "")
|
||||||
|
.replace(/\s*(?:->|=>|→)\s*(?:товар|позици|номенклатур|покупател|buyer|customer|item|product|sku)[\s\S]*$/iu, "")
|
||||||
|
.replace(/\s+(?:на\s+дату|по\s+состоянию|за\s+период)\b[\s\S]*$/iu, "")
|
||||||
|
.replace(/[«»"]/gu, "")
|
||||||
|
.replace(/[.,;:\s]+$/u, "")
|
||||||
|
.trim();
|
||||||
|
return cleaned.length > 0 ? cleaned : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractRequestedInventoryParty(userMessage: string | null | undefined, role: "supplier" | "buyer"): string | null {
|
||||||
|
const text = String(userMessage ?? "");
|
||||||
|
const patterns =
|
||||||
|
role === "supplier"
|
||||||
|
? [
|
||||||
|
/(?:от\s+поставщика|у\s+поставщика|поставщик(?:а|у|ом)?|supplier|vendor)\s+([^\r\n?]+?)(?=$|[?]|(?:\s*(?:->|=>|→)\s*(?:товар|позици|номенклатур|item|product|sku|покупател|buyer|customer)))/iu
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
/(?:покупател(?:ь|я|ю|ем)?|buyer|customer|client)\s+([^\r\n?]+?)(?=$|[?]|(?:\s*(?:->|=>|→))|(?:\s+на\s+дату))/iu
|
||||||
|
];
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const match = text.match(pattern);
|
||||||
|
const candidate = match?.[1] ? cleanupInventoryRequestedParty(match[1]) : null;
|
||||||
|
if (candidate) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function inventoryPartyComparableTokens(value: string | null | undefined): string[] {
|
||||||
|
const stopWords = new Set(["ооо", "ао", "пао", "зао", "ип", "llc", "ltd", "inc", "corp"]);
|
||||||
|
return String(value ?? "")
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ё/gu, "е")
|
||||||
|
.replace(/[^a-zа-я0-9]+/giu, " ")
|
||||||
|
.split(/\s+/u)
|
||||||
|
.map((token) => token.trim())
|
||||||
|
.filter((token) => token.length >= 3 && !stopWords.has(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
function inventoryRequestedPartyMatches(requested: string | null, actualParties: string[]): boolean {
|
||||||
|
if (!requested) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const requestedTokens = inventoryPartyComparableTokens(requested);
|
||||||
|
if (requestedTokens.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return actualParties.some((actual) => {
|
||||||
|
const actualTokens = inventoryPartyComparableTokens(actual);
|
||||||
|
return requestedTokens.every((token) => actualTokens.includes(token));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function inventoryPartyListOrUnknown(parties: string[]): string {
|
||||||
|
return parties.length > 0 ? parties.slice(0, 4).join("; ") : "не выделен отдельным полем";
|
||||||
|
}
|
||||||
|
|
||||||
export function composeInventoryReply(
|
export function composeInventoryReply(
|
||||||
intent: AddressIntent,
|
intent: AddressIntent,
|
||||||
rows: ComposeStageRow[],
|
rows: ComposeStageRow[],
|
||||||
|
|
@ -263,6 +322,39 @@ export function composeInventoryReply(
|
||||||
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
|
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
|
||||||
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
|
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
|
||||||
const unresolvedRows = purchaseRows.filter((row) => deps.extractInventoryCounterpartyCandidates(row).length === 0);
|
const unresolvedRows = purchaseRows.filter((row) => deps.extractInventoryCounterpartyCandidates(row).length === 0);
|
||||||
|
const unresolvedSupplierQuestion =
|
||||||
|
/(?:\u0431\u0435\u0437\s+\u043f\u043e\u043d\u044f\u0442\u043d[^\s]*\s+\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0431\u0435\u0437\s+(?:\u044f\u0432\u043d[^\s]*\s+)?\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043d\u0435\s+\u0438\u043c\u0435\u044e\u0442\s+\u044f\u0432\u043d[^\s]*\s+\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043d\u0435\u0442\s+\u044f\u0432\u043d[^\s]*\s+\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|unresolved\s+supplier\s+link)/iu.test(
|
||||||
|
String(options.userMessage ?? "")
|
||||||
|
);
|
||||||
|
if (unresolvedSupplierQuestion) {
|
||||||
|
const directAnswerLine =
|
||||||
|
unresolvedRows.length > 0
|
||||||
|
? `В текущем складском срезе найдено операций без явно выделенного поставщика: ${deps.formatNumberWithDots(unresolvedRows.length)}.`
|
||||||
|
: "В текущем складском срезе товары без явно выделенной привязки к поставщику в доступных данных не найдены.";
|
||||||
|
const lines: string[] = [directAnswerLine];
|
||||||
|
appendInventoryBulletSection(lines, "Что проверили:", [
|
||||||
|
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||||
|
`Закупочных операций в выборке: ${deps.formatNumberWithDots(purchaseRows.length)}.`,
|
||||||
|
`Операций без явно выделенного поставщика: ${deps.formatNumberWithDots(unresolvedRows.length)}.`,
|
||||||
|
`Поставщиков, выделенных в остальных операциях: ${deps.formatNumberWithDots(summary.counterparties.length)}.`
|
||||||
|
]);
|
||||||
|
appendInventoryBulletSection(lines, "Ограничения:", [
|
||||||
|
"Без партионного учета это проверка доступного закупочного следа по складскому срезу, а не юридическое доказательство владельца каждой партии."
|
||||||
|
]);
|
||||||
|
if (unresolvedRows.length > 0) {
|
||||||
|
appendInventorySection(
|
||||||
|
lines,
|
||||||
|
"Позиции без явно выделенного поставщика:",
|
||||||
|
deps.formatInventoryTraceRows(unresolvedRows, 12)
|
||||||
|
);
|
||||||
|
} else if (summary.counterparties.length > 0) {
|
||||||
|
lines.push(`- В доступном закупочном следе встречаются поставщики: ${summary.counterparties.slice(0, 6).join("; ")}.`);
|
||||||
|
}
|
||||||
|
return buildFactualSummaryReply(
|
||||||
|
lines,
|
||||||
|
buildConfirmedBalanceSemantics(unresolvedRows.length > 0 ? "medium" : "strong", true)
|
||||||
|
);
|
||||||
|
}
|
||||||
const warehouseLabel = summary.warehouses[0] ?? "не указанного склада";
|
const warehouseLabel = summary.warehouses[0] ?? "не указанного склада";
|
||||||
const directAnswerLine =
|
const directAnswerLine =
|
||||||
summary.counterparties.length === 1
|
summary.counterparties.length === 1
|
||||||
|
|
@ -395,13 +487,30 @@ export function composeInventoryReply(
|
||||||
const purchaseSummary = deps.summarizeInventoryTraceRows(purchaseRows);
|
const purchaseSummary = deps.summarizeInventoryTraceRows(purchaseRows);
|
||||||
const saleSummary = deps.summarizeInventoryTraceRows(saleRows);
|
const saleSummary = deps.summarizeInventoryTraceRows(saleRows);
|
||||||
const itemLabel = purchaseSummary.item ?? saleSummary.item ?? "товар не определен";
|
const itemLabel = purchaseSummary.item ?? saleSummary.item ?? "товар не определен";
|
||||||
|
const requestedSupplier = extractRequestedInventoryParty(options.userMessage, "supplier");
|
||||||
|
const requestedBuyer = extractRequestedInventoryParty(options.userMessage, "buyer");
|
||||||
|
const supplierMatches = inventoryRequestedPartyMatches(requestedSupplier, purchaseSummary.counterparties);
|
||||||
|
const buyerMatches = inventoryRequestedPartyMatches(requestedBuyer, saleSummary.counterparties);
|
||||||
|
const mismatchParts: string[] = [];
|
||||||
|
if (requestedSupplier && purchaseRows.length > 0 && !supplierMatches) {
|
||||||
|
mismatchParts.push(
|
||||||
|
`запрошенный поставщик ${requestedSupplier} не совпал с найденным поставщиком: ${inventoryPartyListOrUnknown(purchaseSummary.counterparties)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (requestedBuyer && saleRows.length > 0 && !buyerMatches) {
|
||||||
|
mismatchParts.push(
|
||||||
|
`запрошенный покупатель ${requestedBuyer} не совпал с найденным покупателем: ${inventoryPartyListOrUnknown(saleSummary.counterparties)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
const directAnswerLine =
|
const directAnswerLine =
|
||||||
purchaseSummary.counterparties.length === 1 && saleSummary.counterparties.length === 1
|
mismatchParts.length > 0
|
||||||
|
? `Запрошенная цепочка по товару ${itemLabel} полностью не подтверждена: ${mismatchParts.join("; ")}.`
|
||||||
|
: purchaseSummary.counterparties.length === 1 && saleSummary.counterparties.length === 1
|
||||||
? `По товару ${itemLabel} цепочка поставки и продажи связана с поставщиком ${purchaseSummary.counterparties[0]} и покупателем ${saleSummary.counterparties[0]}.`
|
? `По товару ${itemLabel} цепочка поставки и продажи связана с поставщиком ${purchaseSummary.counterparties[0]} и покупателем ${saleSummary.counterparties[0]}.`
|
||||||
: `По товару ${itemLabel} цепочка поставки и продажи подтверждена частично или разнообразно: детали идут следом.`;
|
: `По товару ${itemLabel} цепочка поставки и продажи подтверждена частично или разнообразно: детали идут следом.`;
|
||||||
const lines: string[] = [directAnswerLine, "", "Подтверждение:"];
|
const lines: string[] = [directAnswerLine, "", "Подтверждение:"];
|
||||||
lines.push(`- Закупочных движений на 41.01: ${deps.formatNumberWithDots(purchaseRows.length)}.`);
|
lines.push(`- Строк закупки на 41.01: ${deps.formatNumberWithDots(purchaseRows.length)}.`);
|
||||||
lines.push(`- Движений выбытия со счета 41.01: ${deps.formatNumberWithDots(saleRows.length)}.`);
|
lines.push(`- Строк продажи со счета 41.01: ${deps.formatNumberWithDots(saleRows.length)}.`);
|
||||||
if (purchaseRows.length > 0 && saleRows.length > 0) {
|
if (purchaseRows.length > 0 && saleRows.length > 0) {
|
||||||
lines.push("- В доступных данных найдены обе стороны цепочки: поступление и последующее выбытие.");
|
lines.push("- В доступных данных найдены обе стороны цепочки: поступление и последующее выбытие.");
|
||||||
} else if (purchaseRows.length > 0) {
|
} else if (purchaseRows.length > 0) {
|
||||||
|
|
|
||||||
|
|
@ -287,6 +287,36 @@ function readStateTransitionReasonCodes(input: ApplyAssistantMcpDiscoveryRespons
|
||||||
.filter((item): item is string => Boolean(item));
|
.filter((item): item is string => Boolean(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readStringArray(value: unknown): string[] {
|
||||||
|
return Array.isArray(value)
|
||||||
|
? value.map((item) => toNonEmptyString(item)).filter((item): item is string => Boolean(item))
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasExactMatchedFactualAddressReply(
|
||||||
|
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
||||||
|
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||||
|
): boolean {
|
||||||
|
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!hasEffectivelyFactualAddressReply(input)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const mcpCallStatus = toNonEmptyString(input.addressRuntimeMeta?.mcp_call_status);
|
||||||
|
const truthMode = toNonEmptyString(input.addressRuntimeMeta?.truth_mode);
|
||||||
|
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
|
||||||
|
const bindingStatus = toNonEmptyString(input.addressRuntimeMeta?.capability_binding_status);
|
||||||
|
const bindingViolations = readStringArray(input.addressRuntimeMeta?.capability_binding_violations);
|
||||||
|
return Boolean(
|
||||||
|
mcpCallStatus === "matched_non_empty" &&
|
||||||
|
truthMode === "confirmed" &&
|
||||||
|
selectedRecipe?.startsWith("address_") &&
|
||||||
|
(bindingStatus === "bound" || bindingStatus === "bound_with_limits") &&
|
||||||
|
bindingViolations.length === 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function hasRuntimeAdjustedExactReply(
|
function hasRuntimeAdjustedExactReply(
|
||||||
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
||||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||||
|
|
@ -463,6 +493,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||||
const matchedFactualAddressContinuationTarget = hasMatchedFactualAddressContinuationTarget(input, entryPoint);
|
const matchedFactualAddressContinuationTarget = hasMatchedFactualAddressContinuationTarget(input, entryPoint);
|
||||||
const matchedFactualSuggestedIntentPivotTarget = hasMatchedFactualSuggestedIntentPivotTarget(input, entryPoint);
|
const matchedFactualSuggestedIntentPivotTarget = hasMatchedFactualSuggestedIntentPivotTarget(input, entryPoint);
|
||||||
const fullConfirmedFactualAddressReply = hasFullConfirmedFactualAddressReply(input, entryPoint);
|
const fullConfirmedFactualAddressReply = hasFullConfirmedFactualAddressReply(input, entryPoint);
|
||||||
|
const exactMatchedFactualAddressReply = hasExactMatchedFactualAddressReply(input, entryPoint);
|
||||||
const runtimeAdjustedExactReply = hasRuntimeAdjustedExactReply(input, entryPoint);
|
const runtimeAdjustedExactReply = hasRuntimeAdjustedExactReply(input, entryPoint);
|
||||||
|
|
||||||
if (!entryPoint) {
|
if (!entryPoint) {
|
||||||
|
|
@ -495,6 +526,9 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||||
if (fullConfirmedFactualAddressReply) {
|
if (fullConfirmedFactualAddressReply) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply");
|
||||||
}
|
}
|
||||||
|
if (exactMatchedFactualAddressReply) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_exact_matched_factual_address_reply");
|
||||||
|
}
|
||||||
if (runtimeAdjustedExactReply) {
|
if (runtimeAdjustedExactReply) {
|
||||||
pushReason(
|
pushReason(
|
||||||
reasonCodes,
|
reasonCodes,
|
||||||
|
|
@ -527,6 +561,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||||
!matchedFactualAddressContinuationTarget &&
|
!matchedFactualAddressContinuationTarget &&
|
||||||
!matchedFactualSuggestedIntentPivotTarget &&
|
!matchedFactualSuggestedIntentPivotTarget &&
|
||||||
!fullConfirmedFactualAddressReply &&
|
!fullConfirmedFactualAddressReply &&
|
||||||
|
!exactMatchedFactualAddressReply &&
|
||||||
!runtimeAdjustedExactReply &&
|
!runtimeAdjustedExactReply &&
|
||||||
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
|
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
|
||||||
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
|
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
|
||||||
|
|
|
||||||
|
|
@ -151,8 +151,17 @@ function resolveAddressLaneProtectionArbitration(input) {
|
||||||
const semanticDeepInvestigationHintDetected = semanticGuardHints?.deep_investigation_signal_detected === true;
|
const semanticDeepInvestigationHintDetected = semanticGuardHints?.deep_investigation_signal_detected === true;
|
||||||
const semanticAggregateShapeDetected = semanticExtraction?.query_shape === "AGGREGATE_LOOKUP" ||
|
const semanticAggregateShapeDetected = semanticExtraction?.query_shape === "AGGREGATE_LOOKUP" ||
|
||||||
semanticExtraction?.aggregation_profile === "management_profile";
|
semanticExtraction?.aggregation_profile === "management_profile";
|
||||||
|
const exactSupportedIntentProtectedFromDeepPreference = Boolean(supportedAddressIntentDetected &&
|
||||||
|
resolvedIntent &&
|
||||||
|
ADDRESS_INTENTS_ALLOW_STRICT_DEEP_INVESTIGATION_BYPASS.has(resolvedIntent) &&
|
||||||
|
semanticApplyCanonicalRecommended &&
|
||||||
|
(!strictDeepInvestigationCueDetected || strictDeepInvestigationBypassAllowed));
|
||||||
|
const unsupportedAggregateFollowupOverride = Boolean(followupContext &&
|
||||||
|
llmContractMode === "unsupported" &&
|
||||||
|
(semanticAggregateShapeDetected || !semanticApplyCanonicalRecommended) &&
|
||||||
|
!exactSupportedIntentProtectedFromDeepPreference);
|
||||||
const followupSemanticOverrideToDeepAllowed = Boolean(followupContext &&
|
const followupSemanticOverrideToDeepAllowed = Boolean(followupContext &&
|
||||||
!supportedAddressIntentDetected &&
|
(!supportedAddressIntentDetected || unsupportedAggregateFollowupOverride) &&
|
||||||
(rootContextOnlyFollowup ||
|
(rootContextOnlyFollowup ||
|
||||||
llmContractMode === "unsupported" ||
|
llmContractMode === "unsupported" ||
|
||||||
semanticAggregateShapeDetected ||
|
semanticAggregateShapeDetected ||
|
||||||
|
|
@ -166,11 +175,16 @@ function resolveAddressLaneProtectionArbitration(input) {
|
||||||
!deepAnalysisPreferenceDetected &&
|
!deepAnalysisPreferenceDetected &&
|
||||||
!strictDeepInvestigationCueDetected &&
|
!strictDeepInvestigationCueDetected &&
|
||||||
!semanticAggregateShapeDetected);
|
!semanticAggregateShapeDetected);
|
||||||
|
const unsupportedSpecificLlmIntent = Boolean(llmContractMode === "unsupported" &&
|
||||||
|
llmContractIntent &&
|
||||||
|
llmContractIntent !== "unknown");
|
||||||
const protectAddressLaneFromFallback = Boolean(supportedAddressRouteCandidateDetected &&
|
const protectAddressLaneFromFallback = Boolean(supportedAddressRouteCandidateDetected &&
|
||||||
|
(exactSupportedIntentProtectedFromDeepPreference ||
|
||||||
|
(!unsupportedSpecificLlmIntent &&
|
||||||
!deepAnalysisPreferenceDetected &&
|
!deepAnalysisPreferenceDetected &&
|
||||||
(exactAddressIntentProtectedFromSemanticDeepHint ||
|
(exactAddressIntentProtectedFromSemanticDeepHint ||
|
||||||
!semanticDeepInvestigationHintDetected ||
|
!semanticDeepInvestigationHintDetected ||
|
||||||
strictDeepInvestigationBypassAllowed));
|
strictDeepInvestigationBypassAllowed))));
|
||||||
return {
|
return {
|
||||||
supportedAddressIntentDetected,
|
supportedAddressIntentDetected,
|
||||||
supportedAddressRouteCandidateDetected,
|
supportedAddressRouteCandidateDetected,
|
||||||
|
|
@ -178,6 +192,7 @@ function resolveAddressLaneProtectionArbitration(input) {
|
||||||
semanticAggregateShapeDetected,
|
semanticAggregateShapeDetected,
|
||||||
followupSemanticOverrideToDeepAllowed,
|
followupSemanticOverrideToDeepAllowed,
|
||||||
exactAddressIntentProtectedFromSemanticDeepHint,
|
exactAddressIntentProtectedFromSemanticDeepHint,
|
||||||
|
exactSupportedIntentProtectedFromDeepPreference,
|
||||||
protectAddressLaneFromFallback
|
protectAddressLaneFromFallback
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -309,7 +309,7 @@ export const INVENTORY_CAPABILITY_CONTRACTS: readonly AssistantCapabilityContrac
|
||||||
entry_modes: ["root_entry", "root_followup", "clarification_resume"],
|
entry_modes: ["root_entry", "root_followup", "clarification_resume"],
|
||||||
transitions: ["T1", "T2", "T7"],
|
transitions: ["T1", "T2", "T7"],
|
||||||
requiresFocusObject: false,
|
requiresFocusObject: false,
|
||||||
requiredAnchors: ["supplier"],
|
requiredAnchors: [],
|
||||||
resultShape: "supplier_to_stock_item_overlap",
|
resultShape: "supplier_to_stock_item_overlap",
|
||||||
answerObjectShape: "inventory_supplier_overlap",
|
answerObjectShape: "inventory_supplier_overlap",
|
||||||
bundleReusePolicy: "none",
|
bundleReusePolicy: "none",
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,31 @@ describe("address coverage evidence policy", () => {
|
||||||
expect(contract.as_of_date_basis).toBe("explicit_as_of_date");
|
expect(contract.as_of_date_basis).toBe("explicit_as_of_date");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps relative current snapshot basis even when it materializes to today's ISO date", () => {
|
||||||
|
const todayIso = new Date().toISOString().slice(0, 10);
|
||||||
|
const contract = resolveAddressCoverageEvidence({
|
||||||
|
intent: "inventory_on_hand_as_of_date",
|
||||||
|
selectedRecipe: "address_inventory_on_hand_as_of_date_v1",
|
||||||
|
filters: {
|
||||||
|
as_of_date: todayIso
|
||||||
|
},
|
||||||
|
semanticFrame: {
|
||||||
|
scope_kind: "implicit_self_scope",
|
||||||
|
anchor_kind: "self_scope",
|
||||||
|
anchor_value: null,
|
||||||
|
date_scope_kind: "implicit_current",
|
||||||
|
date_basis_hint: "implicit_current_snapshot",
|
||||||
|
self_scope_detected: true,
|
||||||
|
selected_object_scope_detected: false
|
||||||
|
},
|
||||||
|
responseType: "FACTUAL_LIST",
|
||||||
|
rowsMatched: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(contract.as_of_date_basis).toBe("implicit_current_snapshot");
|
||||||
|
expect(contract.reason_codes).toContain("as_of_date_basis_implicit_current_snapshot");
|
||||||
|
});
|
||||||
|
|
||||||
it("treats factual exact negatives as full confirmed-balance evidence instead of partial noise", () => {
|
it("treats factual exact negatives as full confirmed-balance evidence instead of partial noise", () => {
|
||||||
const contract = resolveAddressCoverageEvidence({
|
const contract = resolveAddressCoverageEvidence({
|
||||||
intent: "payables_confirmed_as_of_date",
|
intent: "payables_confirmed_as_of_date",
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,13 @@ describe("addressInventoryIntentSignals", () => {
|
||||||
expect(result.reasons).toContain("inventory_purchase_date_signal_detected");
|
expect(result.reasons).toContain("inventory_purchase_date_signal_detected");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("classifies direct Russian purchase-date wording with an explicit item", () => {
|
||||||
|
const result = resolveAddressIntent("Когда был куплен товар Столешница 600*3050*26 дуб ниагара");
|
||||||
|
|
||||||
|
expect(result.intent).toBe("inventory_purchase_provenance_for_item");
|
||||||
|
expect(result.reasons).toContain("inventory_purchase_date_signal_detected");
|
||||||
|
});
|
||||||
|
|
||||||
it("does not steal non-inventory open-items wording into the inventory owner", () => {
|
it("does not steal non-inventory open-items wording into the inventory owner", () => {
|
||||||
const result = resolveInventoryAddressIntent("хвосты покажи по счету 60 на август 2022");
|
const result = resolveInventoryAddressIntent("хвосты покажи по счету 60 на август 2022");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,52 @@ describe("inventory purchase-date selected-object follow-up", () => {
|
||||||
expect(String(result?.reply_text ?? "")).not.toContain("Блок 1");
|
expect(String(result?.reply_text ?? "")).not.toContain("Блок 1");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("routes direct canonical purchase-date wording with an explicit item through the exact provenance lane", async () => {
|
||||||
|
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||||
|
fetched_rows: 1,
|
||||||
|
matched_rows: 1,
|
||||||
|
raw_rows: [
|
||||||
|
{
|
||||||
|
Period: "2019-02-12T00:00:00Z",
|
||||||
|
Registrator: "Поступление товаров и услуг 00000000003 от 12.02.2019 0:00:00",
|
||||||
|
AccountDt: "41.01",
|
||||||
|
AccountKt: "60.01",
|
||||||
|
Amount: 3724.17,
|
||||||
|
SubcontoDt1: "Столешница 600*3050*26 дуб ниагара",
|
||||||
|
SubcontoDt3: "Основной склад",
|
||||||
|
SubcontoKt1: "Торговый дом \\Союз МСК\\",
|
||||||
|
SubcontoKt2: "Договор поставки № 12 от 01.02.2019",
|
||||||
|
Organization: "ООО \\Альтернатива Плюс\\"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
rows: [],
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const service = new AddressQueryService();
|
||||||
|
const result = await service.tryHandle("Когда был куплен товар Столешница 600*3050*26 дуб ниагара", {
|
||||||
|
followupContext: {
|
||||||
|
previous_intent: "inventory_purchase_provenance_for_item",
|
||||||
|
previous_filters: {
|
||||||
|
item: "Столешница 600*3050*26 дуб ниагара",
|
||||||
|
warehouse: "Основной склад",
|
||||||
|
as_of_date: "2019-03-31"
|
||||||
|
},
|
||||||
|
previous_anchor_type: "unknown",
|
||||||
|
previous_anchor_value: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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.selected_recipe).toBe("address_inventory_purchase_provenance_for_item_v1");
|
||||||
|
expect(result?.debug.extracted_filters?.item).toBe("Столешница 600*3050*26 дуб ниагара");
|
||||||
|
expect(result?.debug.extracted_filters?.as_of_date).toBe("2019-03-31");
|
||||||
|
expect(String(result?.reply_text ?? "").split("\n")[0]).toContain("12.02.2019");
|
||||||
|
expect(String(result?.reply_text ?? "")).not.toContain("Блок 1");
|
||||||
|
});
|
||||||
|
|
||||||
it("routes 'когда примерно мы купили' follow-up to compact purchase-date answer with the carried item", async () => {
|
it("routes 'когда примерно мы купили' follow-up to compact purchase-date answer with the carried item", async () => {
|
||||||
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||||
fetched_rows: 1,
|
fetched_rows: 1,
|
||||||
|
|
|
||||||
|
|
@ -87,14 +87,15 @@ describe("inventory sale trace movement route", () => {
|
||||||
|
|
||||||
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
||||||
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
|
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
|
||||||
expect(query).toContain("РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто КАК Движения");
|
expect(query).toContain("Документ.РеализацияТоваровУслуг.Товары КАК Товары");
|
||||||
expect(query).toContain('ПОДСТРОКА(ЕСТЬNULL(Движения.СчетКт.Код, ""), 1, 5) = "41.01"');
|
expect(query).toContain('"41.01" КАК СчетКт');
|
||||||
expect(query).toContain("Движения.СубконтоКт1 В (ВЫБРАТЬ Номенклатура.Ссылка");
|
expect(query).toContain("Товары.Номенклатура В (ВЫБРАТЬ Номенклатура.Ссылка");
|
||||||
expect(query).toContain(
|
expect(query).toContain(
|
||||||
'Номенклатура.Наименование = "Рабочая станция универсального специалиста (индивидуальное изготовление)"'
|
'Номенклатура.Наименование = "Рабочая станция универсального специалиста (индивидуальное изготовление)"'
|
||||||
);
|
);
|
||||||
|
expect(query).toContain("ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Контрагент) КАК Контрагент");
|
||||||
expect(query).not.toContain("2016-06-30");
|
expect(query).not.toContain("2016-06-30");
|
||||||
expect(query).not.toContain("2016-06-01");
|
expect(query).not.toContain("2016-06-01");
|
||||||
expect(query).not.toContain('ПРЕДСТАВЛЕНИЕ(Движения.Организация) = "ООО \\Альтернатива Плюс\\"');
|
expect(query).not.toContain('ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Организация) = "ООО \\Альтернатива Плюс\\"');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,9 @@ describe("inventory sale trace selected-object regressions", () => {
|
||||||
|
|
||||||
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
||||||
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
|
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
|
||||||
expect(query).toContain("РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто КАК Движения");
|
expect(query).toContain("Документ.РеализацияТоваровУслуг.Товары КАК Товары");
|
||||||
|
expect(query).toContain("ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Контрагент) КАК Контрагент");
|
||||||
|
expect(query).toContain("Товары.Номенклатура В (ВЫБРАТЬ Номенклатура.Ссылка");
|
||||||
expect(query).toContain('Номенклатура.Наименование = "Кромка с клеем 33 дуб ниагара 137 м"');
|
expect(query).toContain('Номенклатура.Наименование = "Кромка с клеем 33 дуб ниагара 137 м"');
|
||||||
expect(query).not.toContain('Номенклатура.Наименование = "Кромка"');
|
expect(query).not.toContain('Номенклатура.Наименование = "Кромка"');
|
||||||
});
|
});
|
||||||
|
|
@ -133,7 +135,9 @@ describe("inventory sale trace selected-object regressions", () => {
|
||||||
|
|
||||||
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
||||||
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
|
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
|
||||||
expect(query).toContain("РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто КАК Движения");
|
expect(query).toContain("Документ.РеализацияТоваровУслуг.Товары КАК Товары");
|
||||||
|
expect(query).toContain("ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Контрагент) КАК Контрагент");
|
||||||
|
expect(query).toContain("Товары.Номенклатура В (ВЫБРАТЬ Номенклатура.Ссылка");
|
||||||
expect(query).toContain('Номенклатура.Наименование = "Кромка с клеем 33 дуб ниагара 137 м"');
|
expect(query).toContain('Номенклатура.Наименование = "Кромка с клеем 33 дуб ниагара 137 м"');
|
||||||
expect(query).not.toContain('Номенклатура.Наименование = "Кромка"');
|
expect(query).not.toContain('Номенклатура.Наименование = "Кромка"');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -197,7 +197,8 @@ 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_provenance_for_item");
|
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
||||||
expect(result?.debug.extracted_filters?.item).toBe("Столешница 600*3050*26 дуб ниагара");
|
expect(result?.debug.extracted_filters?.item).toBe("Столешница 600*3050*26 дуб ниагара");
|
||||||
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
expect(result?.debug.extracted_filters?.as_of_date).toBe("2019-03-31");
|
||||||
|
expect(result?.debug.reasons).toContain("as_of_date_from_followup_context");
|
||||||
expect(String(result?.reply_text ?? "")).toContain("Торговый дом \\Союз МСК\\");
|
expect(String(result?.reply_text ?? "")).toContain("Торговый дом \\Союз МСК\\");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -246,7 +247,8 @@ 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_provenance_for_item");
|
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
||||||
expect(result?.debug.extracted_filters?.item).toBe("Столешница 600*3050*26 альмандин");
|
expect(result?.debug.extracted_filters?.item).toBe("Столешница 600*3050*26 альмандин");
|
||||||
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
expect(result?.debug.extracted_filters?.as_of_date).toBe("2021-03-31");
|
||||||
|
expect(result?.debug.reasons).toContain("as_of_date_from_followup_context");
|
||||||
expect(result?.debug.capability_id).toBe("inventory_inventory_purchase_provenance_for_item");
|
expect(result?.debug.capability_id).toBe("inventory_inventory_purchase_provenance_for_item");
|
||||||
expect(String(result?.reply_text ?? "")).toContain("Торговый дом \\Союз");
|
expect(String(result?.reply_text ?? "")).toContain("Торговый дом \\Союз");
|
||||||
});
|
});
|
||||||
|
|
@ -296,7 +298,8 @@ 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_provenance_for_item");
|
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
||||||
expect(result?.debug.extracted_filters?.item).toBe("Рабочая станция универсального специалиста (индивидуальное изготовление)");
|
expect(result?.debug.extracted_filters?.item).toBe("Рабочая станция универсального специалиста (индивидуальное изготовление)");
|
||||||
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
expect(result?.debug.extracted_filters?.as_of_date).toBe("2016-07-31");
|
||||||
|
expect(result?.debug.reasons).toContain("as_of_date_from_followup_context");
|
||||||
expect(String(result?.reply_text ?? "")).toContain("ООО \\Производство мебели\\");
|
expect(String(result?.reply_text ?? "")).toContain("ООО \\Производство мебели\\");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -346,9 +349,10 @@ describe("inventory selected-object follow-up", () => {
|
||||||
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
||||||
expect(result?.debug.selected_recipe).toBe("address_inventory_purchase_provenance_for_item_v1");
|
expect(result?.debug.selected_recipe).toBe("address_inventory_purchase_provenance_for_item_v1");
|
||||||
expect(result?.debug.extracted_filters?.item).toBe("Конструкция трансформер рабочей станции 1300*900*2000");
|
expect(result?.debug.extracted_filters?.item).toBe("Конструкция трансформер рабочей станции 1300*900*2000");
|
||||||
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-06-30");
|
||||||
expect(result?.debug.extracted_filters?.period_from).toBeUndefined();
|
expect(result?.debug.extracted_filters?.period_from).toBeUndefined();
|
||||||
expect(result?.debug.extracted_filters?.period_to).toBeUndefined();
|
expect(result?.debug.extracted_filters?.period_to).toBeUndefined();
|
||||||
|
expect(result?.debug.reasons).toContain("as_of_date_from_followup_context");
|
||||||
expect(result?.debug.capability_id).toBe("inventory_inventory_purchase_provenance_for_item");
|
expect(result?.debug.capability_id).toBe("inventory_inventory_purchase_provenance_for_item");
|
||||||
expect(result?.debug.capability_route_mode).toBe("exact");
|
expect(result?.debug.capability_route_mode).toBe("exact");
|
||||||
expect(String(result?.reply_text ?? "")).toContain("ООО \\Гамма-мебель\\");
|
expect(String(result?.reply_text ?? "")).toContain("ООО \\Гамма-мебель\\");
|
||||||
|
|
@ -399,7 +403,8 @@ 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_provenance_for_item");
|
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
||||||
expect(result?.debug.extracted_filters?.item).toBe("Рабочая станция универсального специалиста (индивидуальное изготовление)");
|
expect(result?.debug.extracted_filters?.item).toBe("Рабочая станция универсального специалиста (индивидуальное изготовление)");
|
||||||
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
expect(result?.debug.extracted_filters?.as_of_date).toBe("2016-05-31");
|
||||||
|
expect(result?.debug.reasons).toContain("as_of_date_from_followup_context");
|
||||||
expect(String(result?.reply_text ?? "")).toContain("ООО \\Производство мебели\\");
|
expect(String(result?.reply_text ?? "")).toContain("ООО \\Производство мебели\\");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -448,7 +453,8 @@ 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_provenance_for_item");
|
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
||||||
expect(result?.debug.extracted_filters?.item).toBe("Рабочая станция универсального специалиста (индивидуальное изготовление)");
|
expect(result?.debug.extracted_filters?.item).toBe("Рабочая станция универсального специалиста (индивидуальное изготовление)");
|
||||||
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
expect(result?.debug.extracted_filters?.as_of_date).toBe("2016-05-31");
|
||||||
|
expect(result?.debug.reasons).toContain("as_of_date_from_followup_context");
|
||||||
expect(String(result?.reply_text ?? "")).toContain("ООО \\Производство мебели\\");
|
expect(String(result?.reply_text ?? "")).toContain("ООО \\Производство мебели\\");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -498,7 +504,8 @@ describe("inventory selected-object follow-up", () => {
|
||||||
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
||||||
expect(result?.debug.selected_recipe).toBe("address_inventory_purchase_provenance_for_item_v1");
|
expect(result?.debug.selected_recipe).toBe("address_inventory_purchase_provenance_for_item_v1");
|
||||||
expect(result?.debug.extracted_filters?.item).toBe("Рабочая станция универсального специалиста (индивидуальное изготовление)");
|
expect(result?.debug.extracted_filters?.item).toBe("Рабочая станция универсального специалиста (индивидуальное изготовление)");
|
||||||
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
expect(result?.debug.extracted_filters?.as_of_date).toBe("2016-05-31");
|
||||||
|
expect(result?.debug.reasons).toContain("as_of_date_from_followup_context");
|
||||||
expect(String(result?.reply_text ?? "")).toContain("ООО \\Производство мебели\\");
|
expect(String(result?.reply_text ?? "")).toContain("ООО \\Производство мебели\\");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -545,7 +552,8 @@ describe("inventory selected-object follow-up", () => {
|
||||||
expect(result?.debug.detected_intent).toBe("inventory_purchase_documents_for_item");
|
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.selected_recipe).toBe("address_inventory_purchase_documents_for_item_v1");
|
||||||
expect(result?.debug.extracted_filters?.item).toBe("Столешница 600*3050*26 дуб ниагара");
|
expect(result?.debug.extracted_filters?.item).toBe("Столешница 600*3050*26 дуб ниагара");
|
||||||
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
expect(result?.debug.extracted_filters?.as_of_date).toBe("2019-03-31");
|
||||||
|
expect(result?.debug.reasons).toContain("as_of_date_from_followup_context");
|
||||||
expect(String(result?.reply_text ?? "")).toContain("Поступление товаров и услуг 00000000077");
|
expect(String(result?.reply_text ?? "")).toContain("Поступление товаров и услуг 00000000077");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -646,7 +654,7 @@ describe("inventory selected-object follow-up", () => {
|
||||||
expect(result?.debug.detected_intent).toBe("inventory_sale_trace_for_item");
|
expect(result?.debug.detected_intent).toBe("inventory_sale_trace_for_item");
|
||||||
expect(result?.debug.selected_recipe).toBe("address_inventory_sale_trace_for_item_v1");
|
expect(result?.debug.selected_recipe).toBe("address_inventory_sale_trace_for_item_v1");
|
||||||
expect(result?.debug.extracted_filters?.item).toBe("Четки Пост (84*117)");
|
expect(result?.debug.extracted_filters?.item).toBe("Четки Пост (84*117)");
|
||||||
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-03-31");
|
||||||
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("Документы выбытия");
|
||||||
});
|
});
|
||||||
|
|
@ -692,7 +700,7 @@ describe("inventory selected-object follow-up", () => {
|
||||||
expect(result?.debug.detected_intent).toBe("inventory_sale_trace_for_item");
|
expect(result?.debug.detected_intent).toBe("inventory_sale_trace_for_item");
|
||||||
expect(result?.debug.selected_recipe).toBe("address_inventory_sale_trace_for_item_v1");
|
expect(result?.debug.selected_recipe).toBe("address_inventory_sale_trace_for_item_v1");
|
||||||
expect(result?.debug.extracted_filters?.item).toBe("Пуф арий");
|
expect(result?.debug.extracted_filters?.item).toBe("Пуф арий");
|
||||||
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-05-31");
|
||||||
expect(result?.debug.reasons).toContain("inventory_selected_object_sale_trace_signal_detected");
|
expect(result?.debug.reasons).toContain("inventory_selected_object_sale_trace_signal_detected");
|
||||||
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("Документы выбытия");
|
||||||
|
|
@ -740,7 +748,7 @@ describe("inventory selected-object follow-up", () => {
|
||||||
expect(result?.debug.detected_intent).toBe("inventory_sale_trace_for_item");
|
expect(result?.debug.detected_intent).toBe("inventory_sale_trace_for_item");
|
||||||
expect(result?.debug.selected_recipe).toBe("address_inventory_sale_trace_for_item_v1");
|
expect(result?.debug.selected_recipe).toBe("address_inventory_sale_trace_for_item_v1");
|
||||||
expect(result?.debug.extracted_filters?.item).toBe("Кромка с клеем 33 альмандин 137 м");
|
expect(result?.debug.extracted_filters?.item).toBe("Кромка с клеем 33 альмандин 137 м");
|
||||||
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-03-31");
|
||||||
expect(String(result?.reply_text ?? "")).toContain("ООО \\Покупатель\\");
|
expect(String(result?.reply_text ?? "")).toContain("ООО \\Покупатель\\");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -787,7 +795,7 @@ describe("inventory selected-object follow-up", () => {
|
||||||
expect(result?.handled).toBe(true);
|
expect(result?.handled).toBe(true);
|
||||||
expect(result?.response_type).toBe("FACTUAL_LIST");
|
expect(result?.response_type).toBe("FACTUAL_LIST");
|
||||||
expect(result?.debug.detected_intent).toBe("inventory_sale_trace_for_item");
|
expect(result?.debug.detected_intent).toBe("inventory_sale_trace_for_item");
|
||||||
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
expect(result?.debug.extracted_filters?.as_of_date).toBe("2019-03-31");
|
||||||
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
||||||
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
|
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
|
||||||
expect(query).not.toContain("2019-03-31");
|
expect(query).not.toContain("2019-03-31");
|
||||||
|
|
@ -826,6 +834,164 @@ describe("inventory selected-object follow-up", () => {
|
||||||
expect(String(result?.reply_text ?? "")).not.toContain("совпадений не нашлось");
|
expect(String(result?.reply_text ?? "")).not.toContain("совпадений не нашлось");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps semantic stock period for unresolved supplier-link follow-up while querying purchase history up to as-of date", async () => {
|
||||||
|
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||||
|
fetched_rows: 1,
|
||||||
|
matched_rows: 1,
|
||||||
|
raw_rows: [
|
||||||
|
{
|
||||||
|
Period: "2020-06-04T00:00:00Z",
|
||||||
|
Registrator: "Поступление товаров и услуг 00000000012 от 04.06.2020 13:36:29",
|
||||||
|
AccountDt: "41.01",
|
||||||
|
AccountKt: "60.01",
|
||||||
|
Amount: 439000,
|
||||||
|
SubcontoDt1: "Кресло для посетителей Экокожа/хром Цвет - оранжевый",
|
||||||
|
SubcontoDt3: "Основной склад",
|
||||||
|
Organization: "ООО \\Альтернатива Плюс\\"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
rows: [],
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const service = new AddressQueryService();
|
||||||
|
const result = await service.tryHandle("Какие товары сейчас висят в остатке без понятной привязки к поставщику", {
|
||||||
|
analysisDateHint: "2021-09-30",
|
||||||
|
followupContext: {
|
||||||
|
previous_intent: "inventory_aging_by_purchase_date",
|
||||||
|
previous_filters: {
|
||||||
|
as_of_date: "2021-09-30",
|
||||||
|
organization: "ООО \\Альтернатива Плюс\\"
|
||||||
|
},
|
||||||
|
root_intent: "inventory_on_hand_as_of_date",
|
||||||
|
root_filters: {
|
||||||
|
period_from: "2021-09-01",
|
||||||
|
period_to: "2021-09-30",
|
||||||
|
as_of_date: "2021-09-30",
|
||||||
|
organization: "ООО \\Альтернатива Плюс\\"
|
||||||
|
},
|
||||||
|
previous_anchor_type: "unknown",
|
||||||
|
previous_anchor_value: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result?.handled).toBe(true);
|
||||||
|
expect(result?.debug.detected_intent).toBe("inventory_supplier_stock_overlap_as_of_date");
|
||||||
|
expect(result?.debug.extracted_filters?.period_from).toBe("2021-09-01");
|
||||||
|
expect(result?.debug.extracted_filters?.period_to).toBe("2021-09-30");
|
||||||
|
expect(result?.debug.extracted_filters?.as_of_date).toBe("2021-09-30");
|
||||||
|
expect(result?.debug.reasons).toContain("period_window_semantic_from_inventory_snapshot_context");
|
||||||
|
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
|
||||||
|
expect(query).not.toContain("ДАТАВРЕМЯ(2021, 9, 1");
|
||||||
|
expect(query).toContain("ДАТАВРЕМЯ(2021, 9, 30, 23, 59, 59)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("derives semantic stock month for unresolved supplier-link when dialog continuation starts a new topic", async () => {
|
||||||
|
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||||
|
fetched_rows: 1,
|
||||||
|
matched_rows: 1,
|
||||||
|
raw_rows: [
|
||||||
|
{
|
||||||
|
Period: "2020-06-04T00:00:00Z",
|
||||||
|
Registrator: "Purchase document 00000000012 from 2020-06-04",
|
||||||
|
AccountDt: "41.01",
|
||||||
|
AccountKt: "60.01",
|
||||||
|
Amount: 439000,
|
||||||
|
SubcontoDt1: "Office chair",
|
||||||
|
SubcontoDt3: "Main warehouse",
|
||||||
|
Organization: "OOO Test Org"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
rows: [],
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const service = new AddressQueryService();
|
||||||
|
const unresolvedSupplierLinkQuestion =
|
||||||
|
"\u041a\u0430\u043a\u0438\u0435 \u0442\u043e\u0432\u0430\u0440\u044b \u0441\u0435\u0439\u0447\u0430\u0441 \u0432\u0438\u0441\u044f\u0442 \u0432 \u043e\u0441\u0442\u0430\u0442\u043a\u0435 \u0431\u0435\u0437 \u043f\u043e\u043d\u044f\u0442\u043d\u043e\u0439 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438 \u043a \u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\u0443";
|
||||||
|
const result = await service.tryHandle(unresolvedSupplierLinkQuestion, {
|
||||||
|
analysisDateHint: "2021-09-30",
|
||||||
|
activeOrganization: "OOO Test Org"
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result?.handled).toBe(true);
|
||||||
|
expect(result?.debug.detected_intent).toBe("inventory_supplier_stock_overlap_as_of_date");
|
||||||
|
expect(result?.debug.extracted_filters?.period_from).toBe("2021-09-01");
|
||||||
|
expect(result?.debug.extracted_filters?.period_to).toBe("2021-09-30");
|
||||||
|
expect(result?.debug.extracted_filters?.as_of_date).toBe("2021-09-30");
|
||||||
|
expect(result?.debug.reasons).toContain("period_window_semantic_from_inventory_as_of_month");
|
||||||
|
expect(result?.debug.reasons).not.toContain("period_window_semantic_from_inventory_snapshot_context");
|
||||||
|
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
|
||||||
|
expect(query).not.toContain("(2021, 9, 1");
|
||||||
|
expect(query).toContain("(2021, 9, 30, 23, 59, 59)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats supplier in purchase-to-sale chain as verification party, not stale organization scope", 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: "ООО \\Альтернатива Плюс\\",
|
||||||
|
llmSemanticHints: {
|
||||||
|
scope_target_kind: "organization",
|
||||||
|
scope_target_text: "Гамма-мебель, ООО",
|
||||||
|
date_scope_kind: "explicit",
|
||||||
|
self_scope_detected: false,
|
||||||
|
selected_object_scope_detected: false
|
||||||
|
},
|
||||||
|
followupContext: {
|
||||||
|
previous_intent: "inventory_on_hand_as_of_date",
|
||||||
|
previous_filters: {
|
||||||
|
as_of_date: "2020-03-31",
|
||||||
|
organization: "ООО \\Альтернатива Плюс\\"
|
||||||
|
},
|
||||||
|
previous_anchor_type: "organization",
|
||||||
|
previous_anchor_value: "ООО \\Альтернатива Плюс\\"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
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?.organization).toBe("ООО Альтернатива Плюс");
|
||||||
|
expect(result?.debug.reasons).toContain("organization_restored_from_inventory_chain_context");
|
||||||
|
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.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,
|
||||||
|
|
@ -914,9 +1080,10 @@ 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_provenance_for_item");
|
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
||||||
expect(result?.debug.extracted_filters?.item).toBe("Кресло орион");
|
expect(result?.debug.extracted_filters?.item).toBe("Кресло орион");
|
||||||
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-03-31");
|
||||||
expect(result?.debug.extracted_filters?.period_from).toBeUndefined();
|
expect(result?.debug.extracted_filters?.period_from).toBeUndefined();
|
||||||
expect(result?.debug.extracted_filters?.period_to).toBeUndefined();
|
expect(result?.debug.extracted_filters?.period_to).toBeUndefined();
|
||||||
|
expect(result?.debug.reasons).toContain("as_of_date_from_followup_context");
|
||||||
expect(result?.debug.reasons ?? []).not.toContain("lifecycle_execution_detached_from_snapshot_date");
|
expect(result?.debug.reasons ?? []).not.toContain("lifecycle_execution_detached_from_snapshot_date");
|
||||||
expect(result?.debug.reasons ?? []).not.toContain("as_of_date_cleared_for_history_recovery");
|
expect(result?.debug.reasons ?? []).not.toContain("as_of_date_cleared_for_history_recovery");
|
||||||
expect(result?.debug.limitations ?? []).not.toContain("lifecycle_execution_detached_from_snapshot_date");
|
expect(result?.debug.limitations ?? []).not.toContain("lifecycle_execution_detached_from_snapshot_date");
|
||||||
|
|
|
||||||
|
|
@ -93,4 +93,26 @@ describe("inventory warehouse anchor extraction", () => {
|
||||||
|
|
||||||
expect(filters.warehouse).toBeUndefined();
|
expect(filters.warehouse).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not materialize current-moment canonical tail as warehouse anchor", () => {
|
||||||
|
const filters = extractAddressFilters(
|
||||||
|
"Какие товары находятся на складе в текущий момент",
|
||||||
|
"inventory_on_hand_as_of_date"
|
||||||
|
).extracted_filters;
|
||||||
|
|
||||||
|
expect(filters.warehouse).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not split organization-generic stock wording into stale warehouse and counterparty anchors", () => {
|
||||||
|
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();
|
||||||
|
expect(filters.counterparty).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,17 @@ describe("address query shape classifier", () => {
|
||||||
expect(filters.warehouse).toBeUndefined();
|
expect(filters.warehouse).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("extracts supplier anchor from supplier-item-buyer chain without swallowing the arrow 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("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(
|
||||||
"По каким документам был куплен товар Диван трехместный для остатка на складе Основной склад",
|
"По каким документам был куплен товар Диван трехместный для остатка на складе Основной склад",
|
||||||
|
|
@ -317,6 +328,33 @@ describe("address query shape classifier", () => {
|
||||||
expect(plan.query).toContain("41.01");
|
expect(plan.query).toContain("41.01");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("builds sale-trace query from sale document rows with explicit buyer fields", () => {
|
||||||
|
const selected = selectAddressRecipe("inventory_sale_trace_for_item", {
|
||||||
|
item: "Шкаф картотечный"
|
||||||
|
});
|
||||||
|
expect(selected.selected_recipe?.recipe_id).toBe("address_inventory_sale_trace_for_item_v1");
|
||||||
|
const plan = buildAddressRecipePlan(selected.selected_recipe!, {
|
||||||
|
item: "Шкаф картотечный"
|
||||||
|
});
|
||||||
|
expect(plan.query).toContain("Документ.РеализацияТоваровУслуг.Товары КАК Товары");
|
||||||
|
expect(plan.query).toContain("ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Контрагент) КАК Контрагент");
|
||||||
|
expect(plan.query).toContain("Товары.Номенклатура В (ВЫБРАТЬ Номенклатура.Ссылка");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds purchase-to-sale chain query as purchase and sale document union", () => {
|
||||||
|
const selected = selectAddressRecipe("inventory_purchase_to_sale_chain", {
|
||||||
|
item: "Шкаф картотечный"
|
||||||
|
});
|
||||||
|
expect(selected.selected_recipe?.recipe_id).toBe("address_inventory_purchase_to_sale_chain_v1");
|
||||||
|
const plan = buildAddressRecipePlan(selected.selected_recipe!, {
|
||||||
|
item: "Шкаф картотечный"
|
||||||
|
});
|
||||||
|
expect(plan.query).toContain("Документ.ПоступлениеТоваровУслуг.Товары КАК Товары");
|
||||||
|
expect(plan.query).toContain("ОБЪЕДИНИТЬ ВСЕ");
|
||||||
|
expect(plan.query).toContain("Документ.РеализацияТоваровУслуг.Товары КАК Товары");
|
||||||
|
expect(plan.query).toContain("ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Контрагент) КАК Контрагент");
|
||||||
|
});
|
||||||
|
|
||||||
it("renders inventory purchase documents from purchase-side 41.01 movements", () => {
|
it("renders inventory purchase documents from purchase-side 41.01 movements", () => {
|
||||||
const reply = composeFactualReply(
|
const reply = composeFactualReply(
|
||||||
"inventory_purchase_documents_for_item",
|
"inventory_purchase_documents_for_item",
|
||||||
|
|
@ -372,7 +410,7 @@ describe("address query shape classifier", () => {
|
||||||
expect(reply.semantics?.balance_confirmed).toBe(true);
|
expect(reply.semantics?.balance_confirmed).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders inventory sale trace from credit-side 41.01 movements", () => {
|
it("renders inventory sale trace from sale document rows", () => {
|
||||||
const reply = composeFactualReply(
|
const reply = composeFactualReply(
|
||||||
"inventory_sale_trace_for_item",
|
"inventory_sale_trace_for_item",
|
||||||
[
|
[
|
||||||
|
|
@ -400,7 +438,7 @@ describe("address query shape classifier", () => {
|
||||||
expect(reply.text).toContain("Департамент капитального ремонта города Москвы");
|
expect(reply.text).toContain("Департамент капитального ремонта города Москвы");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders purchase-to-sale chain from both sides of 41.01", () => {
|
it("renders purchase-to-sale chain from purchase and sale document rows", () => {
|
||||||
const reply = composeFactualReply(
|
const reply = composeFactualReply(
|
||||||
"inventory_purchase_to_sale_chain",
|
"inventory_purchase_to_sale_chain",
|
||||||
[
|
[
|
||||||
|
|
@ -437,6 +475,45 @@ describe("address query shape classifier", () => {
|
||||||
expect(reply.text).toContain("Реализация товаров и услуг 0007");
|
expect(reply.text).toContain("Реализация товаров и услуг 0007");
|
||||||
expect(reply.semantics?.result_mode).toBe("confirmed_balance");
|
expect(reply.semantics?.result_mode).toBe("confirmed_balance");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("states when requested supplier-to-buyer chain parties are not confirmed by item documents", () => {
|
||||||
|
const reply = composeFactualReply(
|
||||||
|
"inventory_purchase_to_sale_chain",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
period: "2019-12-10T00:00:00Z",
|
||||||
|
registrator: "Поступление товаров и услуг 00000000150 от 10.12.2019 0:00:00",
|
||||||
|
account_dt: "41.01",
|
||||||
|
account_kt: "",
|
||||||
|
amount: 712500,
|
||||||
|
analytics: ["Шкаф картотечный 1000*400*2100", "ЭталонМебель"],
|
||||||
|
item: "Шкаф картотечный 1000*400*2100",
|
||||||
|
counterparty: "ЭталонМебель",
|
||||||
|
organization: "ООО Альтернатива Плюс"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
period: "2020-04-01T00:00:00Z",
|
||||||
|
registrator: "Реализация товаров и услуг 00000000001 от 01.04.2020 0:00:00",
|
||||||
|
account_dt: "",
|
||||||
|
account_kt: "41.01",
|
||||||
|
amount: 855000,
|
||||||
|
analytics: ["Шкаф картотечный 1000*400*2100", "ЭталонМебель"],
|
||||||
|
item: "Шкаф картотечный 1000*400*2100",
|
||||||
|
counterparty: "ЭталонМебель",
|
||||||
|
organization: "ООО Альтернатива Плюс"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
userMessage:
|
||||||
|
"Есть ли документально подтвержденная цепочка: поставщик Гамма-мебель, ООО -> товар Шкаф картотечный 1000*400*2100 -> покупатель Департамент капитального ремонта города Москвы?"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(reply.text.split("\n")[0]).toContain("полностью не подтверждена");
|
||||||
|
expect(reply.text.split("\n")[0]).toContain("Гамма-мебель, ООО");
|
||||||
|
expect(reply.text.split("\n")[0]).toContain("Департамент капитального ремонта города Москвы");
|
||||||
|
expect(reply.text).toContain("ЭталонМебель");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("address compose stage utf8 headers", () => {
|
describe("address compose stage utf8 headers", () => {
|
||||||
|
|
@ -5377,6 +5454,35 @@ it("routes old purchase residue questions to aging-by-purchase-date", () => {
|
||||||
expect(reply.text).not.toContain("Контур:");
|
expect(reply.text).not.toContain("Контур:");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("answers unresolved supplier-link stock questions without asking for a supplier", () => {
|
||||||
|
const reply = composeFactualReply(
|
||||||
|
"inventory_supplier_stock_overlap_as_of_date",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
period: "2021-09-30T00:00:00Z",
|
||||||
|
registrator: "Поступление товаров и услуг 00000000999 от 30.09.2021 0:00:00",
|
||||||
|
account_dt: "41.01",
|
||||||
|
account_kt: "60.01",
|
||||||
|
amount: 1200,
|
||||||
|
analytics: ["Товар без поставщика", "Основной склад"],
|
||||||
|
item: "Товар без поставщика",
|
||||||
|
warehouse: "Основной склад",
|
||||||
|
organization: 'ООО "Альтернатива Плюс"'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
asOfDate: "2021-09-30",
|
||||||
|
userMessage: "Какие товары сейчас висят в остатке без понятной привязки к поставщику",
|
||||||
|
useRubCurrency: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
||||||
|
expect(reply.text.split("\n")[0]).toContain("без явно выделенного поставщика");
|
||||||
|
expect(reply.text).toContain("Позиции без явно выделенного поставщика:");
|
||||||
|
expect(reply.text).not.toContain("Уточните");
|
||||||
|
});
|
||||||
|
|
||||||
it("routes inventory provenance questions to a dedicated intent", () => {
|
it("routes inventory provenance questions to a dedicated intent", () => {
|
||||||
const result = resolveAddressIntent("От какого поставщика куплен товар Шкаф картоотечный?");
|
const result = resolveAddressIntent("От какого поставщика куплен товар Шкаф картоотечный?");
|
||||||
expect(result.intent).toBe("inventory_purchase_provenance_for_item");
|
expect(result.intent).toBe("inventory_purchase_provenance_for_item");
|
||||||
|
|
@ -5416,6 +5522,11 @@ it("routes old purchase residue questions to aging-by-purchase-date", () => {
|
||||||
expect(result.intent).toBe("inventory_sale_trace_for_item");
|
expect(result.intent).toBe("inventory_sale_trace_for_item");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("routes passive buyer wording 'кому был продан товар' to inventory sale trace intent", () => {
|
||||||
|
const result = resolveAddressIntent("Кому был продан товар Шкаф картотечный 1000*400*2100?");
|
||||||
|
expect(result.intent).toBe("inventory_sale_trace_for_item");
|
||||||
|
});
|
||||||
|
|
||||||
it("routes colloquial buyer wording with 'впарили' to inventory sale trace intent", () => {
|
it("routes colloquial buyer wording with 'впарили' to inventory sale trace intent", () => {
|
||||||
const result = resolveAddressIntent("Кому мы впарили этот товар Шкаф картотечный?");
|
const result = resolveAddressIntent("Кому мы впарили этот товар Шкаф картотечный?");
|
||||||
expect(result.intent).toBe("inventory_sale_trace_for_item");
|
expect(result.intent).toBe("inventory_sale_trace_for_item");
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,31 @@ describe("assistant capability runtime binding adapter", () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("allows supplier-overlap inventory aggregate questions without a supplier anchor", () => {
|
||||||
|
const binding = resolveAssistantCapabilityRuntimeBinding({
|
||||||
|
addressDebug: {
|
||||||
|
capability_id: "inventory_inventory_supplier_stock_overlap_as_of_date",
|
||||||
|
detected_intent: "inventory_supplier_stock_overlap_as_of_date",
|
||||||
|
detected_mode: "address_query",
|
||||||
|
capability_layer: "compute",
|
||||||
|
capability_route_mode: "exact",
|
||||||
|
extracted_filters: {
|
||||||
|
warehouse: "Основной склад",
|
||||||
|
as_of_date: "2021-09-30"
|
||||||
|
},
|
||||||
|
rows_matched: 500,
|
||||||
|
route_expectation_status: "matched"
|
||||||
|
},
|
||||||
|
groundingStatus: "grounded",
|
||||||
|
replyType: "factual"
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(binding.binding_status).toBe("bound");
|
||||||
|
expect(binding.required_anchors).toEqual([]);
|
||||||
|
expect(binding.missing_anchors).toEqual([]);
|
||||||
|
expect(binding.violations).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
it("binds selected-object follow-ups through item anchor when focus object is implicit", () => {
|
it("binds selected-object follow-ups through item anchor when focus object is implicit", () => {
|
||||||
const binding = resolveAssistantCapabilityRuntimeBinding({
|
const binding = resolveAssistantCapabilityRuntimeBinding({
|
||||||
addressDebug: {
|
addressDebug: {
|
||||||
|
|
|
||||||
|
|
@ -1008,6 +1008,45 @@ describe("assistant orchestration contract", () => {
|
||||||
expect(decision.orchestrationContract?.semantic_route_arbitration?.strict_deep_investigation_bypass_allowed).toBe(true);
|
expect(decision.orchestrationContract?.semantic_route_arbitration?.strict_deep_investigation_bypass_allowed).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps selected-object purchase-to-sale chain wording in address lane even when LLM predecompose calls it deep", () => {
|
||||||
|
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: "list_documents_by_counterparty",
|
||||||
|
intent_confidence: "high"
|
||||||
|
},
|
||||||
|
semanticExtractionContract: {
|
||||||
|
valid: true,
|
||||||
|
apply_canonical_recommended: true,
|
||||||
|
reason_codes: ["deep_investigation_signal_detected"],
|
||||||
|
guard_hints: {
|
||||||
|
deep_investigation_signal_detected: true
|
||||||
|
},
|
||||||
|
extraction: {
|
||||||
|
query_shape: "DOCUMENT_LIST",
|
||||||
|
aggregation_profile: "list_lookup"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as any,
|
||||||
|
useMock: false
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(decision.runAddressLane).toBe(true);
|
||||||
|
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||||||
|
expect(decision.livingMode).toBe("address_data");
|
||||||
|
expect(decision.orchestrationContract?.deep_analysis_signal_fallback_to_deep).toBe(false);
|
||||||
|
expect(decision.orchestrationContract?.address_intent).toBe("inventory_purchase_to_sale_chain");
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps 'a na tekushuyu datu' follow-up in address lane when previous VAT context exists", () => {
|
it("keeps 'a na tekushuyu datu' follow-up in address lane when previous VAT context exists", () => {
|
||||||
const decision = resolveAssistantOrchestrationDecision({
|
const decision = resolveAssistantOrchestrationDecision({
|
||||||
rawUserMessage: "а на текущую дату",
|
rawUserMessage: "а на текущую дату",
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,43 @@ describe("assistant MCP discovery response policy", () => {
|
||||||
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_not_discovery_ready_address_candidate");
|
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_not_discovery_ready_address_candidate");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps exact matched inventory address replies over stale metadata discovery candidates", () => {
|
||||||
|
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||||
|
currentReply: "По товару Шкаф картотечный 1000*400*2100 цепочка поставки и продажи подтверждена.",
|
||||||
|
currentReplySource: "address_query_runtime_v1",
|
||||||
|
currentReplyType: "factual",
|
||||||
|
addressRuntimeMeta: {
|
||||||
|
detected_intent: "inventory_purchase_to_sale_chain",
|
||||||
|
selected_recipe: "address_inventory_purchase_to_sale_chain_v1",
|
||||||
|
mcp_call_status: "matched_non_empty",
|
||||||
|
truth_mode: "confirmed",
|
||||||
|
capability_binding_status: "bound",
|
||||||
|
capability_binding_violations: [],
|
||||||
|
answer_shape_contract: {
|
||||||
|
reply_type: "factual",
|
||||||
|
capability_contract_id: "inventory_inventory_purchase_to_sale_chain"
|
||||||
|
},
|
||||||
|
assistant_mcp_discovery_entry_point_v1: entryPoint({
|
||||||
|
turn_input: {
|
||||||
|
adapter_status: "ready",
|
||||||
|
should_run_discovery: true,
|
||||||
|
turn_meaning_ref: {
|
||||||
|
asked_domain_family: "metadata",
|
||||||
|
asked_action_family: "inspect_documents",
|
||||||
|
metadata_scope_hint: "склад"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.applied).toBe(false);
|
||||||
|
expect(result.decision).toBe("keep_current_reply");
|
||||||
|
expect(result.reply_text).toContain("цепочка поставки и продажи подтверждена");
|
||||||
|
expect(result.reason_codes).toContain("mcp_discovery_response_policy_keep_exact_matched_factual_address_reply");
|
||||||
|
expect(result.reason_codes).toContain("mcp_discovery_response_policy_semantic_conflict_allows_candidate_override");
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps aligned factual address lane answers when the exact lane already matched the same semantic intent", () => {
|
it("keeps aligned factual address lane answers when the exact lane already matched the same semantic intent", () => {
|
||||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||||
currentReply: "ИП Калинин Н.М. | сумма: 216600 | операций: 2",
|
currentReply: "ИП Калинин Н.М. | сумма: 216600 | операций: 2",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue