Inventory breadth: закрепить stock provenance и sale-trace контуры
This commit is contained in:
@@ -162,6 +162,12 @@ export function resolveAddressAsOfDateBasis(
|
||||
filters: AddressFilterSet,
|
||||
semanticFrame?: AddressSemanticFrame | 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);
|
||||
if (asOfDate) {
|
||||
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")}`;
|
||||
}
|
||||
|
||||
function hasImplicitCurrentAsOfDateCue(text: string): boolean {
|
||||
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: string): string | undefined {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -751,6 +753,9 @@ function isLowQualityCounterpartyAnchorValue(rawValue: string): boolean {
|
||||
"ноябрь",
|
||||
"декабрь"
|
||||
]);
|
||||
const isLowQualityTimeToken = (token: string): boolean =>
|
||||
lowQualityTimeTokens.has(token) ||
|
||||
/^(?:январ|феврал|март|апрел|ма(?:й|я|е)|июн|июл|август|сентябр|октябр|ноябр|декабр)/iu.test(token);
|
||||
const lowQualityGenericTokens = new Set([
|
||||
"деньги",
|
||||
"денег",
|
||||
@@ -776,7 +781,7 @@ function isLowQualityCounterpartyAnchorValue(rawValue: string): boolean {
|
||||
const meaningfulNonTemporalTokens = tokens.filter(
|
||||
(token) =>
|
||||
isLikelyCounterpartyToken(token) &&
|
||||
!lowQualityTimeTokens.has(token) &&
|
||||
!isLowQualityTimeToken(token) &&
|
||||
!/^(?:19|20)\d{2}$/.test(token)
|
||||
);
|
||||
if (meaningfulNonTemporalTokens.length === 0 && hasTemporalCue) {
|
||||
@@ -785,7 +790,7 @@ function isLowQualityCounterpartyAnchorValue(rawValue: string): boolean {
|
||||
const meaningfulNonGenericTokens = tokens.filter(
|
||||
(token) =>
|
||||
isLikelyCounterpartyToken(token) &&
|
||||
!lowQualityTimeTokens.has(token) &&
|
||||
!isLowQualityTimeToken(token) &&
|
||||
!lowQualityGenericTokens.has(token) &&
|
||||
!/^(?:19|20)\d{2}$/.test(token)
|
||||
);
|
||||
@@ -1302,6 +1307,9 @@ function isTemporalWarehousePhrase(candidate: string): boolean {
|
||||
.toLowerCase()
|
||||
.replace(/ё/g, "е")
|
||||
.trim();
|
||||
if (/^(?:в|на)?\s*(?:сейчас|сегодня|текущ(?:ий|ую|ем|его)\s+момент|данн(?:ый|ую|ом|ого)\s+момент)$/iu.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if (/^(?:на\s+)?(?:эту|ту|текущ(?:ую|ая|ий|ее|ей)|сегодняшн(?:юю|ая|ий|ее|ей))(?:\s+же)?\s+дат(?:у|а|е|ой)$/iu.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
@@ -1355,6 +1363,12 @@ function isLowQualityWarehouseAnchorValue(rawValue: string): boolean {
|
||||
"лежали",
|
||||
"на",
|
||||
"по",
|
||||
"компания",
|
||||
"компании",
|
||||
"компанию",
|
||||
"организация",
|
||||
"организации",
|
||||
"организацию",
|
||||
"складе",
|
||||
"складу",
|
||||
"складом",
|
||||
@@ -1373,7 +1387,11 @@ function isLowQualityWarehouseAnchorValue(rawValue: string): boolean {
|
||||
if (tokens.length === 0) {
|
||||
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) {
|
||||
return true;
|
||||
}
|
||||
@@ -1453,16 +1471,18 @@ function extractInventoryWarehouseAnchor(text: string): string | undefined {
|
||||
|
||||
function extractInventorySupplierAnchor(text: string): string | undefined {
|
||||
const match = String(text ?? "").match(
|
||||
/(?:от\s+поставщика|у\s+поставщика|поставщика|поставщику)\s+([^\r\n?]+?)(?=$|[?])/iu
|
||||
/(?:от\s+поставщика|у\s+поставщика|поставщик(?:а|у|ом)?|supplier|vendor)\s+([^\r\n?]+?)(?=$|[?])/iu
|
||||
);
|
||||
if (!match?.[1]) {
|
||||
return undefined;
|
||||
}
|
||||
const candidate = cleanupAnchorValue(
|
||||
cleanupAnchorValue(String(match[1])).replace(
|
||||
/\s+(?:сейчас|на\s+склад(?:е|у|ом)?|на\s+дату|по\s+состоянию\s+на\s+дату|котор(?:ый|ые|ых)|куплен(?:ы|а|о)?|были|был|лежат|лежит|еще|ещё|организац\w*|компани\w*).*$/iu,
|
||||
""
|
||||
)
|
||||
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 ||
|
||||
@@ -1595,7 +1615,7 @@ function resolveSemanticDateScopeKind(
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
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 implicitCurrentAsOfDateCue = hasImplicitCurrentAsOfDateCue(text);
|
||||
const explicitAsOfDate = extractAsOfDate(text);
|
||||
const explicitAsOfDateWithCue = extractAsOfDateWithCue(text);
|
||||
|
||||
@@ -1756,6 +1777,13 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
|
||||
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 counterpartyMatch = allowGenericCounterpartyAnchor ? text.match(COUNTERPARTY_PATTERN) : null;
|
||||
@@ -1923,6 +1951,9 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
|
||||
|
||||
if (usesAsOfPrimaryWindow(intent) && 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") ||
|
||||
warnings.includes("period_derived_from_year_range_phrase") ||
|
||||
@@ -1941,6 +1972,9 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
|
||||
const asOfDate = extractAsOfDate(text);
|
||||
if (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 {
|
||||
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 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(
|
||||
@@ -1766,6 +1778,15 @@ function hasInventoryAgingSignal(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 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(
|
||||
@@ -2030,6 +2051,17 @@ function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolutio
|
||||
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) &&
|
||||
|
||||
@@ -146,13 +146,21 @@ function hasInventoryProvenanceSignalV2(text: string): boolean {
|
||||
}
|
||||
|
||||
function hasInventoryPurchaseDateSignal(text: string): boolean {
|
||||
const value = String(text ?? "");
|
||||
const hasItemCue =
|
||||
/(?:товар|номенклатур|sku|item|product)/iu.test(text) || hasSelectedObjectInventoryCue(text);
|
||||
const hasPurchaseDateCue =
|
||||
/(?:когда\s+(?:примерно\s+)?(?:мы\s+)?купили|когда\s+был\s+куплен|когда\s+куплен|дата\s+закупк|purchase\s+date)/iu.test(
|
||||
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(
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -177,7 +185,7 @@ function hasInventorySaleTraceSignalV2(text: string): boolean {
|
||||
const value = String(text ?? "");
|
||||
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(
|
||||
/(?:кому\s+(?:в\s+итоге\s+)?(?:мы\s+)?(?:продали|реализовали|впарили)|кому\s+(?:был[аио]?|были)?\s*(?:продан|реализован)|кто\s+купил|покупател|buyer|sale\s+trace|trace\s+of\s+sale)/iu.test(
|
||||
value
|
||||
);
|
||||
if (hasPlainItemCue && hasPlainTraceCue) {
|
||||
|
||||
@@ -86,6 +86,8 @@ interface NormalizedAddressRow {
|
||||
item?: string | null;
|
||||
warehouse?: string | null;
|
||||
organization?: string | null;
|
||||
counterparty?: string | null;
|
||||
contract?: string | null;
|
||||
}
|
||||
|
||||
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 {
|
||||
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
if (!match) {
|
||||
@@ -1446,6 +1463,8 @@ function toNormalizedRows(rows: Array<Record<string, unknown>>): NormalizedAddre
|
||||
row.organization_name,
|
||||
row.ОрганизацияПредставление
|
||||
);
|
||||
const counterparty = firstNonEmptyString(row.Контрагент, row.Counterparty, row.counterparty);
|
||||
const contract = firstNonEmptyString(row.Договор, row.Contract, row.contract);
|
||||
const analytics = collectAnalyticsStrings(row);
|
||||
|
||||
return {
|
||||
@@ -1458,7 +1477,9 @@ function toNormalizedRows(rows: Array<Record<string, unknown>>): NormalizedAddre
|
||||
quantity,
|
||||
item,
|
||||
warehouse,
|
||||
organization
|
||||
organization,
|
||||
counterparty,
|
||||
contract
|
||||
};
|
||||
})
|
||||
.filter((item) => Boolean(item.period || item.registrator));
|
||||
@@ -1526,6 +1547,10 @@ function formatMoneyRubForReply(value: number): string {
|
||||
}
|
||||
|
||||
function extractContractNameFromNormalizedRow(row: NormalizedAddressRow): string | null {
|
||||
const explicitContract = firstNonEmptyString(row.contract);
|
||||
if (explicitContract) {
|
||||
return explicitContract;
|
||||
}
|
||||
for (const token of row.analytics) {
|
||||
const normalized = String(token ?? "").trim();
|
||||
if (!normalized) {
|
||||
@@ -1898,6 +1923,11 @@ function applyPreExecutionOrganizationScopeGrounding(input: {
|
||||
]);
|
||||
const resolvedOrganizationFromMessage = resolveOrganizationSelectionFromMessage(input.userMessage, candidateOrganizations);
|
||||
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 &&
|
||||
@@ -1916,6 +1946,7 @@ function applyPreExecutionOrganizationScopeGrounding(input: {
|
||||
if (
|
||||
resolvedOrganizationFromMessage &&
|
||||
(!input.filters.organization || input.semanticFrame?.anchor_kind === "organization") &&
|
||||
!counterpartyAnchorProtectsOrganizationScope &&
|
||||
!sameNormalizedOrganizationScope(input.filters.organization ?? null, 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 {
|
||||
const hasRecoverableAsOfOnlyWindow =
|
||||
!hasExplicitPeriodWindow(filters) &&
|
||||
@@ -2426,6 +2463,7 @@ function shouldDetachLifecycleExecutionFromSnapshotContext(
|
||||
reasons: string[]
|
||||
): boolean {
|
||||
if (
|
||||
intent !== "inventory_supplier_stock_overlap_as_of_date" &&
|
||||
intent !== "inventory_sale_trace_for_item" &&
|
||||
intent !== "inventory_purchase_to_sale_chain"
|
||||
) {
|
||||
@@ -2433,6 +2471,8 @@ function shouldDetachLifecycleExecutionFromSnapshotContext(
|
||||
}
|
||||
|
||||
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("as_of_date_from_open_items_followup_context")
|
||||
@@ -3506,6 +3546,91 @@ export class AddressQueryService {
|
||||
});
|
||||
const knownOrganizations = mergeKnownOrganizations(options.knownOrganizations ?? []);
|
||||
const activeOrganization = normalizeOrganizationScopeValue(options.activeOrganization ?? null);
|
||||
const previousOrganizationFromContext = normalizeOrganizationScopeValue(followupContext?.previous_filters?.organization ?? null);
|
||||
const chainCounterpartyAnchor = toNonEmptyFilterValue(filters.extracted_filters.counterparty);
|
||||
const chainOrganizationAnchor = toNonEmptyFilterValue(filters.extracted_filters.organization);
|
||||
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) &&
|
||||
hasExplicitSingleOrganizationValueFlowScopeRequest(userMessage) &&
|
||||
@@ -3678,6 +3803,7 @@ export class AddressQueryService {
|
||||
const detachedExecutionFilters: AddressFilterSet = { ...executionFilters };
|
||||
let periodDetached = false;
|
||||
let asOfDetached = false;
|
||||
const keepAsOfDateForInventorySnapshotOverlap = intent.intent === "inventory_supplier_stock_overlap_as_of_date";
|
||||
if (
|
||||
toNonEmptyFilterValue(detachedExecutionFilters.period_from) ||
|
||||
toNonEmptyFilterValue(detachedExecutionFilters.period_to)
|
||||
@@ -3686,7 +3812,7 @@ export class AddressQueryService {
|
||||
delete detachedExecutionFilters.period_to;
|
||||
periodDetached = true;
|
||||
}
|
||||
if (toNonEmptyFilterValue(detachedExecutionFilters.as_of_date)) {
|
||||
if (!keepAsOfDateForInventorySnapshotOverlap && toNonEmptyFilterValue(detachedExecutionFilters.as_of_date)) {
|
||||
delete detachedExecutionFilters.as_of_date;
|
||||
asOfDetached = true;
|
||||
}
|
||||
@@ -4231,6 +4357,20 @@ export class AddressQueryService {
|
||||
: anchor.anchor_type === "contract" && anchor.anchor_value_resolved
|
||||
? { ...executionFilters, contract: anchor.anchor_value_resolved }
|
||||
: 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({
|
||||
intent: intent.intent,
|
||||
filters: filtersForMatching,
|
||||
|
||||
@@ -1334,6 +1334,29 @@ function buildInventoryPurchaseDocumentQuery(filters: AddressFilterSet, resolved
|
||||
.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 {
|
||||
const goodsCounterpartyCondition = buildCounterpartyReferenceCondition(filters, ["Товары.Ссылка.Контрагент"]);
|
||||
const servicesCounterpartyCondition = buildCounterpartyReferenceCondition(filters, ["Услуги.Ссылка.Контрагент"]);
|
||||
@@ -1628,9 +1651,9 @@ export function buildAddressRecipePlan(
|
||||
: recipe.query_template === "inventory_supplier_stock_overlap_profile"
|
||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||
: recipe.query_template === "inventory_sale_trace_profile"
|
||||
? buildInventoryMovementQuery(filters, resolvedLimit, "kt")
|
||||
? buildInventorySaleDocumentQuery(filters, resolvedLimit)
|
||||
: 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"
|
||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||
: recipe.query_template === "contracts_by_counterparty_profile"
|
||||
|
||||
@@ -29,6 +29,8 @@ export interface ComposeStageRow {
|
||||
item?: string | null;
|
||||
warehouse?: string | null;
|
||||
organization?: string | null;
|
||||
counterparty?: string | null;
|
||||
contract?: string | null;
|
||||
}
|
||||
|
||||
export interface VatDirectSourceProbeItem {
|
||||
@@ -1098,6 +1100,18 @@ function extractInventoryCounterpartyCandidates(row: ComposeStageRow, excludedTo
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1156,6 +1170,7 @@ function summarizeInventoryTraceRows(rows: ComposeStageRow[], excludedCounterpar
|
||||
function formatInventoryTraceRows(rows: ComposeStageRow[], limit = 10, excludedCounterpartyTokens: string[] = []): string[] {
|
||||
return rows.slice(0, limit).map((row, index) => {
|
||||
const parties = extractInventoryCounterpartyCandidates(row, excludedCounterpartyTokens);
|
||||
const item = extractInventoryItemName(row);
|
||||
const warehouse = extractInventoryWarehouseName(row);
|
||||
const organization = extractInventoryOrganizationName(row);
|
||||
const amount =
|
||||
@@ -1165,6 +1180,9 @@ function formatInventoryTraceRows(rows: ComposeStageRow[], limit = 10, excludedC
|
||||
`дата: ${inventoryTraceDateLabel(row.period)}`,
|
||||
`сумма: ${amount}`
|
||||
];
|
||||
if (item) {
|
||||
parts.push(`товар: ${item}`);
|
||||
}
|
||||
if (warehouse) {
|
||||
parts.push(`склад: ${warehouse}`);
|
||||
}
|
||||
|
||||
@@ -73,6 +73,65 @@ interface InventoryReplyDeps {
|
||||
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(
|
||||
intent: AddressIntent,
|
||||
rows: ComposeStageRow[],
|
||||
@@ -263,6 +322,39 @@ export function composeInventoryReply(
|
||||
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
|
||||
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
|
||||
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 directAnswerLine =
|
||||
summary.counterparties.length === 1
|
||||
@@ -395,13 +487,30 @@ export function composeInventoryReply(
|
||||
const purchaseSummary = deps.summarizeInventoryTraceRows(purchaseRows);
|
||||
const saleSummary = deps.summarizeInventoryTraceRows(saleRows);
|
||||
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 =
|
||||
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} цепочка поставки и продажи подтверждена частично или разнообразно: детали идут следом.`;
|
||||
const lines: string[] = [directAnswerLine, "", "Подтверждение:"];
|
||||
lines.push(`- Закупочных движений на 41.01: ${deps.formatNumberWithDots(purchaseRows.length)}.`);
|
||||
lines.push(`- Движений выбытия со счета 41.01: ${deps.formatNumberWithDots(saleRows.length)}.`);
|
||||
lines.push(`- Строк закупки на 41.01: ${deps.formatNumberWithDots(purchaseRows.length)}.`);
|
||||
lines.push(`- Строк продажи со счета 41.01: ${deps.formatNumberWithDots(saleRows.length)}.`);
|
||||
if (purchaseRows.length > 0 && saleRows.length > 0) {
|
||||
lines.push("- В доступных данных найдены обе стороны цепочки: поступление и последующее выбытие.");
|
||||
} else if (purchaseRows.length > 0) {
|
||||
|
||||
@@ -287,6 +287,36 @@ function readStateTransitionReasonCodes(input: ApplyAssistantMcpDiscoveryRespons
|
||||
.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(
|
||||
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||
@@ -463,6 +493,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||
const matchedFactualAddressContinuationTarget = hasMatchedFactualAddressContinuationTarget(input, entryPoint);
|
||||
const matchedFactualSuggestedIntentPivotTarget = hasMatchedFactualSuggestedIntentPivotTarget(input, entryPoint);
|
||||
const fullConfirmedFactualAddressReply = hasFullConfirmedFactualAddressReply(input, entryPoint);
|
||||
const exactMatchedFactualAddressReply = hasExactMatchedFactualAddressReply(input, entryPoint);
|
||||
const runtimeAdjustedExactReply = hasRuntimeAdjustedExactReply(input, entryPoint);
|
||||
|
||||
if (!entryPoint) {
|
||||
@@ -495,6 +526,9 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||
if (fullConfirmedFactualAddressReply) {
|
||||
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) {
|
||||
pushReason(
|
||||
reasonCodes,
|
||||
@@ -527,6 +561,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||
!matchedFactualAddressContinuationTarget &&
|
||||
!matchedFactualSuggestedIntentPivotTarget &&
|
||||
!fullConfirmedFactualAddressReply &&
|
||||
!exactMatchedFactualAddressReply &&
|
||||
!runtimeAdjustedExactReply &&
|
||||
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
|
||||
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
|
||||
|
||||
@@ -151,8 +151,17 @@ function resolveAddressLaneProtectionArbitration(input) {
|
||||
const semanticDeepInvestigationHintDetected = semanticGuardHints?.deep_investigation_signal_detected === true;
|
||||
const semanticAggregateShapeDetected = semanticExtraction?.query_shape === "AGGREGATE_LOOKUP" ||
|
||||
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 &&
|
||||
!supportedAddressIntentDetected &&
|
||||
(!supportedAddressIntentDetected || unsupportedAggregateFollowupOverride) &&
|
||||
(rootContextOnlyFollowup ||
|
||||
llmContractMode === "unsupported" ||
|
||||
semanticAggregateShapeDetected ||
|
||||
@@ -166,11 +175,16 @@ function resolveAddressLaneProtectionArbitration(input) {
|
||||
!deepAnalysisPreferenceDetected &&
|
||||
!strictDeepInvestigationCueDetected &&
|
||||
!semanticAggregateShapeDetected);
|
||||
const unsupportedSpecificLlmIntent = Boolean(llmContractMode === "unsupported" &&
|
||||
llmContractIntent &&
|
||||
llmContractIntent !== "unknown");
|
||||
const protectAddressLaneFromFallback = Boolean(supportedAddressRouteCandidateDetected &&
|
||||
!deepAnalysisPreferenceDetected &&
|
||||
(exactAddressIntentProtectedFromSemanticDeepHint ||
|
||||
!semanticDeepInvestigationHintDetected ||
|
||||
strictDeepInvestigationBypassAllowed));
|
||||
(exactSupportedIntentProtectedFromDeepPreference ||
|
||||
(!unsupportedSpecificLlmIntent &&
|
||||
!deepAnalysisPreferenceDetected &&
|
||||
(exactAddressIntentProtectedFromSemanticDeepHint ||
|
||||
!semanticDeepInvestigationHintDetected ||
|
||||
strictDeepInvestigationBypassAllowed))));
|
||||
return {
|
||||
supportedAddressIntentDetected,
|
||||
supportedAddressRouteCandidateDetected,
|
||||
@@ -178,6 +192,7 @@ function resolveAddressLaneProtectionArbitration(input) {
|
||||
semanticAggregateShapeDetected,
|
||||
followupSemanticOverrideToDeepAllowed,
|
||||
exactAddressIntentProtectedFromSemanticDeepHint,
|
||||
exactSupportedIntentProtectedFromDeepPreference,
|
||||
protectAddressLaneFromFallback
|
||||
};
|
||||
}
|
||||
|
||||
@@ -309,7 +309,7 @@ export const INVENTORY_CAPABILITY_CONTRACTS: readonly AssistantCapabilityContrac
|
||||
entry_modes: ["root_entry", "root_followup", "clarification_resume"],
|
||||
transitions: ["T1", "T2", "T7"],
|
||||
requiresFocusObject: false,
|
||||
requiredAnchors: ["supplier"],
|
||||
requiredAnchors: [],
|
||||
resultShape: "supplier_to_stock_item_overlap",
|
||||
answerObjectShape: "inventory_supplier_overlap",
|
||||
bundleReusePolicy: "none",
|
||||
|
||||
Reference in New Issue
Block a user