АРЧ - Ассистент: отделить meta-followup по прошлому ответу от повторного запуска address lane

This commit is contained in:
dctouch 2026-04-15 12:38:44 +03:00
parent 8866176be6
commit 70cc5a99f1
61 changed files with 4023 additions and 298 deletions

View File

@ -952,6 +952,23 @@ function isTemporalWarehousePhrase(candidate) {
.trim(); .trim();
return /^(?:в|на)\s+(?:январ(?:е|ь)|феврал(?:е|ь)|март(?:е)?|апрел(?:е|ь)|ма(?:й|е)|июн(?:е|ь)|июл(?:е|ь)|август(?:е)?|сентябр(?:е|ь)|октябр(?:е|ь)|ноябр(?:е|ь)|декабр(?:е|ь))(?:\s+\d{4}(?:\s+г(?:\.|ода)?)?)?$/iu.test(normalized); return /^(?:в|на)\s+(?:январ(?:е|ь)|феврал(?:е|ь)|март(?:е)?|апрел(?:е|ь)|ма(?:й|е)|июн(?:е|ь)|июл(?:е|ь)|август(?:е)?|сентябр(?:е|ь)|октябр(?:е|ь)|ноябр(?:е|ь)|декабр(?:е|ь))(?:\s+\d{4}(?:\s+г(?:\.|ода)?)?)?$/iu.test(normalized);
} }
function normalizeSemanticAnchorCandidate(value) {
return cleanupAnchorValue(value)
.toLowerCase()
.replace(/С/g, "Рµ")
.replace(/\s+/g, " ")
.trim();
}
function hasImplicitSelfScopeSignal(text) {
return /(?:^|[\s,.;:!?()\-])(?:у\s+нас|у\s+себя|у\s+меня|наш(?:ем|ей|его|их|а|е)?|сво(?:ем|ей|его|их|я|е)?)(?=$|[\s,.;:!?()\-])/iu.test(String(text ?? ""));
}
function isImplicitSelfScopeWarehouseAnchor(candidate) {
const normalized = normalizeSemanticAnchorCandidate(candidate);
return /^(?:у\s+нас|у\s+себя|у\s+меня|наш(?:ем|ей|его|их|а|е)?|сво(?:ем|ей|его|их|я|е)?)$/iu.test(normalized);
}
function hasSelectedObjectScopeSignal(text) {
return /(?:по\s+выбранному\s+объекту|selected\s+object)/iu.test(String(text ?? ""));
}
function extractInventoryWarehouseAnchor(text) { function extractInventoryWarehouseAnchor(text) {
const patterns = [ const patterns = [
/(?:на|по)\s+склад(?:е|у|ом)?\s+[«"']?([^\r\n,.;:!?]+?)(?:[»"']|(?=\s+(?:на|по|за|с|в)\b|[?]|$))/iu, /(?:на|по)\s+склад(?:е|у|ом)?\s+[«"']?([^\r\n,.;:!?]+?)(?:[»"']|(?=\s+(?:на|по|за|с|в)\b|[?]|$))/iu,
@ -967,6 +984,7 @@ function extractInventoryWarehouseAnchor(text) {
if (!candidate || if (!candidate ||
candidate.includes("->") || candidate.includes("->") ||
candidate.includes("=>") || candidate.includes("=>") ||
isImplicitSelfScopeWarehouseAnchor(candidate) ||
normalizedCandidate.startsWith("по состоянию") || normalizedCandidate.startsWith("по состоянию") ||
isTemporalWarehousePhrase(candidate) || isTemporalWarehousePhrase(candidate) ||
/^(?:сейчас|на|дату|дате|остаток|остатки)$/iu.test(candidate)) { /^(?:сейчас|на|дату|дате|остаток|остатки)$/iu.test(candidate)) {
@ -1076,6 +1094,95 @@ function shouldDefaultAsOfDateToToday(intent) {
intent === "receivables_confirmed_as_of_date" || intent === "receivables_confirmed_as_of_date" ||
intent === "vat_payable_confirmed_as_of_date"); intent === "vat_payable_confirmed_as_of_date");
} }
function resolveSemanticDateScopeKind(filters, warnings) {
if (warnings.includes("as_of_date_defaulted_today")) {
return "implicit_current";
}
if ((typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0) ||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0) ||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0)) {
return "explicit";
}
return "none";
}
function resolveSemanticDateBasisHint(filters, warnings) {
if (warnings.includes("as_of_date_defaulted_today")) {
return "implicit_current_snapshot";
}
const hasAsOfDate = typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0;
const hasPeriodFrom = typeof filters.period_from === "string" && filters.period_from.trim().length > 0;
const hasPeriodTo = typeof filters.period_to === "string" && filters.period_to.trim().length > 0;
if (hasPeriodFrom && hasPeriodTo) {
return "period_range";
}
if (hasAsOfDate) {
return "explicit_as_of_date";
}
if (hasPeriodTo) {
return "period_end";
}
if (hasPeriodFrom) {
return "period_range";
}
return null;
}
function buildSemanticFrame(text, filters, warnings) {
const selfScopeDetected = hasImplicitSelfScopeSignal(text);
const selectedObjectScopeDetected = hasSelectedObjectScopeSignal(text);
const itemAnchor = typeof filters.item === "string" && filters.item.trim().length > 0 ? filters.item.trim() : null;
const warehouseAnchor = typeof filters.warehouse === "string" && filters.warehouse.trim().length > 0 ? filters.warehouse.trim() : null;
const counterpartyAnchor = typeof filters.counterparty === "string" && filters.counterparty.trim().length > 0 ? filters.counterparty.trim() : null;
const contractAnchor = typeof filters.contract === "string" && filters.contract.trim().length > 0 ? filters.contract.trim() : null;
const organizationAnchor = typeof filters.organization === "string" && filters.organization.trim().length > 0 ? filters.organization.trim() : null;
if (selectedObjectScopeDetected && itemAnchor) {
return {
scope_kind: "selected_object_scope",
anchor_kind: "item",
anchor_value: itemAnchor,
date_scope_kind: resolveSemanticDateScopeKind(filters, warnings),
date_basis_hint: resolveSemanticDateBasisHint(filters, warnings),
self_scope_detected: selfScopeDetected,
selected_object_scope_detected: true
};
}
if (selfScopeDetected && !warehouseAnchor) {
return {
scope_kind: "implicit_self_scope",
anchor_kind: "self_scope",
anchor_value: null,
date_scope_kind: resolveSemanticDateScopeKind(filters, warnings),
date_basis_hint: resolveSemanticDateBasisHint(filters, warnings),
self_scope_detected: true,
selected_object_scope_detected: selectedObjectScopeDetected
};
}
const explicitAnchor = itemAnchor ??
warehouseAnchor ??
counterpartyAnchor ??
contractAnchor ??
organizationAnchor ??
null;
const anchorKind = itemAnchor
? "item"
: warehouseAnchor
? "warehouse"
: counterpartyAnchor
? "counterparty"
: contractAnchor
? "contract"
: organizationAnchor
? "organization"
: "none";
return {
scope_kind: explicitAnchor ? "explicit_anchor" : "none",
anchor_kind: anchorKind,
anchor_value: explicitAnchor,
date_scope_kind: resolveSemanticDateScopeKind(filters, warnings),
date_basis_hint: resolveSemanticDateBasisHint(filters, warnings),
self_scope_detected: selfScopeDetected,
selected_object_scope_detected: selectedObjectScopeDetected
};
}
function extractAddressFilters(userMessage, intent) { function extractAddressFilters(userMessage, intent) {
const rawText = String(userMessage ?? "").trim(); const rawText = String(userMessage ?? "").trim();
const text = normalizeMojibakeString(rawText); const text = normalizeMojibakeString(rawText);
@ -1130,6 +1237,10 @@ function extractAddressFilters(userMessage, intent) {
if (warehouseAnchor) { if (warehouseAnchor) {
filters.warehouse = warehouseAnchor; filters.warehouse = warehouseAnchor;
} }
else if ((intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date") &&
hasImplicitSelfScopeSignal(text)) {
warnings.push("warehouse_self_scope_detected");
}
if (intent === "inventory_supplier_stock_overlap_as_of_date") { if (intent === "inventory_supplier_stock_overlap_as_of_date") {
const supplierAnchor = asksForInventorySupplierIdentity(text) ? undefined : extractInventorySupplierAnchor(text); const supplierAnchor = asksForInventorySupplierIdentity(text) ? undefined : extractInventorySupplierAnchor(text);
if (supplierAnchor) { if (supplierAnchor) {
@ -1311,9 +1422,11 @@ function extractAddressFilters(userMessage, intent) {
const value = filters[key]; const value = filters[key];
return value === undefined || value === null || String(value).trim() === ""; return value === undefined || value === null || String(value).trim() === "";
}); });
const semanticFrame = buildSemanticFrame(text, filters, warnings);
return { return {
extracted_filters: filters, extracted_filters: filters,
missing_required_filters: missingRequiredFilters, missing_required_filters: missingRequiredFilters,
warnings warnings,
semantic_frame: semanticFrame
}; };
} }

View File

@ -1309,7 +1309,8 @@ function hasInventoryAsOfCue(text) {
return /(?:сейчас|текущ|на\s+дату|по\s+состоянию|срез|на\s+конец|date|as\s+of|current|now|today)/iu.test(text); return /(?:сейчас|текущ|на\s+дату|по\s+состоянию|срез|на\s+конец|date|as\s+of|current|now|today)/iu.test(text);
} }
function hasInventoryOnHandSignal(text) { function hasInventoryOnHandSignal(text) {
const hasColloquialStockSnapshotCue = /(?:что|ч[её])\s+(?:у\s+нас\s+)?на\s+склад(?:е|у|ом)(?=$|[\s,.;:!?])/iu.test(text); const hasColloquialStockSnapshotCue = /(?:что|ч[еёо])\s+(?:у\s+нас\s+)?на\s+склад(?:е|у|ом|ах)(?=$|[\s,.;:!?])/iu.test(text);
const hasStockStateCue = /(?:(?:что|ч[еёо])\s+там\s+на\s+склад(?:е|у|ом|ах)|(?:что|ч[еёо]).*происход(?:ит|ило|ящее).*(?:на\s+)?склад(?:е|у|ом|ах)|происход(?:ит|ило|ящее)\s+на\s+склад(?:е|у|ом|ах)|ситуац(?:ия|ии)\s+на\s+склад(?:е|у|ом|ах)|обстановк(?:а|и)\s+на\s+склад(?:е|у|ом|ах)|what(?:'s| is)?\s+(?:there\s+)?(?:on|in)\s+(?:the\s+)?(?:warehouse|stock)|what(?:'s| is)?\s+happening\s+(?:on|in)\s+(?:the\s+)?(?:warehouse|stock))/iu.test(text);
const hasAccount41Anchor = hasInventoryAccount41Anchor(text); const hasAccount41Anchor = hasInventoryAccount41Anchor(text);
const hasStockLexeme = /(?:склад(?:е|у|ом|ы|ов)?|warehouse|stock(?:room)?|inventory|on[\s-]?hand)/iu.test(text); const hasStockLexeme = /(?:склад(?:е|у|ом|ы|ов)?|warehouse|stock(?:room)?|inventory|on[\s-]?hand)/iu.test(text);
if (!hasStockLexeme && !hasAccount41Anchor) { if (!hasStockLexeme && !hasAccount41Anchor) {
@ -1323,13 +1324,13 @@ function hasInventoryOnHandSignal(text) {
return false; return false;
} }
const hasGoodsLexeme = /(?:товар(?:ы|ов|ом|а|ные)?|номенклатур|материал(?:ы|ов|а|ам)?|item(?:s)?|sku|product(?:s)?)/iu.test(text); const hasGoodsLexeme = /(?:товар(?:ы|ов|ом|а|ные)?|номенклатур|материал(?:ы|ов|а|ам)?|item(?:s)?|sku|product(?:s)?)/iu.test(text);
const hasBalanceLexeme = /(?:леж(?:ит|ат)|есть|числ(?:ит(?:ся|сь)|ятся)|остат(?:ок|ки)|срез|на\s+дат|по\s+состоянию|на\s+конец|today|now|current|as\s+of)/iu.test(text); const hasBalanceLexeme = /(?:леж(?:ит|ат)|есть|числ(?:ит(?:ся|сь)|ятся)|остат(?:ок|ки)|срез|на\s+дат|по\s+состоянию|на\s+конец|происход(?:ит|ило|ящее)|ситуац(?:ия|ии)|обстановк(?:а|и)|today|now|current|as\s+of)/iu.test(text);
const hasRequestCue = /(?:покажи|показать|выведи|дай|какие|что|какой|сколько|show|list|which|what)/iu.test(text); const hasRequestCue = /(?:покажи|показать|выведи|дай|какие|что|ч[еёо]|какой|сколько|проверь|проверить|чекни|check|show|list|which|what)/iu.test(text);
if (hasAccount41Anchor && (hasGoodsLexeme || hasBalanceLexeme || hasRequestCue || hasInventoryAsOfCue(text))) { if (hasAccount41Anchor && (hasGoodsLexeme || hasBalanceLexeme || hasRequestCue || hasInventoryAsOfCue(text))) {
return true; return true;
} }
return (hasGoodsLexeme || hasBalanceLexeme || hasColloquialStockSnapshotCue) && return (hasGoodsLexeme || hasBalanceLexeme || hasColloquialStockSnapshotCue || hasStockStateCue) &&
(hasRequestCue || hasBalanceLexeme || hasColloquialStockSnapshotCue); (hasRequestCue || hasBalanceLexeme || hasColloquialStockSnapshotCue || hasStockStateCue);
} }
function hasInventoryProvenanceSignal(text) { function hasInventoryProvenanceSignal(text) {
return /(?:поставщик|закупк|происхожд|откуда|когда был куплен|активная закупк|purchase provenance|purchase date|supplier provenance|stock overlap)/iu.test(text); return /(?:поставщик|закупк|происхожд|откуда|когда был куплен|активная закупк|purchase provenance|purchase date|supplier provenance|stock overlap)/iu.test(text);

View File

@ -14,6 +14,14 @@ const ADDRESS_ACTION_TOKENS = [
"покажи", "покажи",
"покаж", "покаж",
"показ", "показ",
"проверь",
"провер",
"чекни",
"чекн",
"глянь",
"глян",
"посмотри",
"смотри",
"список", "список",
"найди", "найди",
"найд", "найд",

View File

@ -9,6 +9,7 @@ const resolveStage_1 = require("./address_runtime/resolveStage");
const composeStage_1 = require("./address_runtime/composeStage"); const composeStage_1 = require("./address_runtime/composeStage");
const addressCapabilityPolicy_1 = require("./addressCapabilityPolicy"); const addressCapabilityPolicy_1 = require("./addressCapabilityPolicy");
const addressRouteExpectations_1 = require("./addressRouteExpectations"); const addressRouteExpectations_1 = require("./addressRouteExpectations");
const assistantOrganizationMatcher_1 = require("./assistantOrganizationMatcher");
const ACCOUNT_SCOPE_FIELDS_CHECKED = ["account_dt", "account_kt", "registrator", "analytics"]; const ACCOUNT_SCOPE_FIELDS_CHECKED = ["account_dt", "account_kt", "registrator", "analytics"];
const ACCOUNT_SCOPE_MATCH_STRATEGY = "account_code_regex_plus_alias_map_v1"; const ACCOUNT_SCOPE_MATCH_STRATEGY = "account_code_regex_plus_alias_map_v1";
const ADDRESS_ANCHOR_RECOVERY_LIMIT = 1000; const ADDRESS_ANCHOR_RECOVERY_LIMIT = 1000;
@ -1183,6 +1184,43 @@ function isCounterpartyRiskIntent(intent) {
intent === "list_open_contracts" || intent === "list_open_contracts" ||
intent === "open_items_by_counterparty_or_contract"); intent === "open_items_by_counterparty_or_contract");
} }
function sameNormalizedOrganizationScope(left, right) {
return (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeSearchText)(left ?? "") === (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeSearchText)(right ?? "");
}
function applyPreExecutionOrganizationScopeGrounding(input) {
const activeOrganization = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(input.activeOrganization ?? null);
const candidateOrganizations = (0, assistantOrganizationMatcher_1.mergeKnownOrganizations)([
...(Array.isArray(input.knownOrganizations) ? input.knownOrganizations : []),
activeOrganization
]);
const resolvedOrganizationFromMessage = (0, assistantOrganizationMatcher_1.resolveOrganizationSelectionFromMessage)(input.userMessage, candidateOrganizations);
if (!input.filters.organization &&
input.semanticFrame?.scope_kind === "implicit_self_scope" &&
activeOrganization) {
input.filters.organization = activeOrganization;
if (!input.warnings.includes("organization_from_active_scope")) {
input.warnings.push("organization_from_active_scope");
}
if (!input.baseReasons.includes("organization_from_active_scope")) {
input.baseReasons.push("organization_from_active_scope");
}
}
if (resolvedOrganizationFromMessage &&
(!input.filters.organization || input.semanticFrame?.anchor_kind === "organization") &&
!sameNormalizedOrganizationScope(input.filters.organization ?? null, resolvedOrganizationFromMessage)) {
input.filters.organization = resolvedOrganizationFromMessage;
if (!input.warnings.includes("organization_grounded_from_scope_candidates")) {
input.warnings.push("organization_grounded_from_scope_candidates");
}
if (!input.baseReasons.includes("organization_grounded_from_scope_candidates")) {
input.baseReasons.push("organization_grounded_from_scope_candidates");
}
if (input.semanticFrame?.anchor_kind === "organization") {
input.semanticFrame.anchor_value = resolvedOrganizationFromMessage;
}
}
return resolvedOrganizationFromMessage;
}
function isHeuristicCandidatesIntent(intent) { function isHeuristicCandidatesIntent(intent) {
return (intent === "list_receivables_counterparties" || return (intent === "list_receivables_counterparties" ||
intent === "list_payables_counterparties" || intent === "list_payables_counterparties" ||
@ -1203,7 +1241,10 @@ function isConfirmedBalanceIntent(intent) {
intent === "vat_payable_confirmed_as_of_date" || intent === "vat_payable_confirmed_as_of_date" ||
intent === "vat_liability_confirmed_for_tax_period"); intent === "vat_liability_confirmed_for_tax_period");
} }
function resolveAsOfDateBasis(filters) { function resolveAsOfDateBasis(filters, semanticFrame) {
if (semanticFrame?.date_basis_hint) {
return semanticFrame.date_basis_hint;
}
const asOfDate = normalizeAnalysisDateHint(filters.as_of_date); const asOfDate = normalizeAnalysisDateHint(filters.as_of_date);
if (asOfDate) { if (asOfDate) {
return "explicit_as_of_date"; return "explicit_as_of_date";
@ -1239,7 +1280,7 @@ function deriveAddressEvidenceStrength(input) {
} }
return undefined; return undefined;
} }
function resolveRequestedResultMode(intent, filters) { function resolveRequestedResultMode(intent, filters, semanticFrame) {
if (isConfirmedBalanceIntent(intent)) { if (isConfirmedBalanceIntent(intent)) {
return "confirmed_balance"; return "confirmed_balance";
} }
@ -1247,8 +1288,11 @@ function resolveRequestedResultMode(intent, filters) {
return "heuristic_candidates"; return "heuristic_candidates";
} }
if (isHeuristicCandidatesIntent(intent)) { if (isHeuristicCandidatesIntent(intent)) {
const asOfDateBasis = resolveAsOfDateBasis(filters); const asOfDateBasis = resolveAsOfDateBasis(filters, semanticFrame);
if (asOfDateBasis === "explicit_as_of_date" || asOfDateBasis === "period_end" || asOfDateBasis === "period_range") { if (asOfDateBasis === "explicit_as_of_date" ||
asOfDateBasis === "period_end" ||
asOfDateBasis === "period_range" ||
asOfDateBasis === "implicit_current_snapshot") {
return "confirmed_balance"; return "confirmed_balance";
} }
return "heuristic_candidates"; return "heuristic_candidates";
@ -1256,8 +1300,8 @@ function resolveRequestedResultMode(intent, filters) {
return undefined; return undefined;
} }
function deriveAddressResultSemantics(input) { function deriveAddressResultSemantics(input) {
const asOfDateBasis = resolveAsOfDateBasis(input.filters); const asOfDateBasis = resolveAsOfDateBasis(input.filters, input.semanticFrame);
const requestedResultMode = resolveRequestedResultMode(input.intent, input.filters); const requestedResultMode = resolveRequestedResultMode(input.intent, input.filters, input.semanticFrame);
if (isHeuristicCandidatesIntent(input.intent)) { if (isHeuristicCandidatesIntent(input.intent)) {
return { return {
requested_result_mode: requestedResultMode, requested_result_mode: requestedResultMode,
@ -1542,6 +1586,9 @@ function shouldBoostAutoBroadenedLimit(intent) {
intent === "inventory_purchase_to_sale_chain" || intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date"); intent === "inventory_aging_by_purchase_date");
} }
function shouldClearAsOfDateForHistoryRecovery(intent) {
return intent === "inventory_purchase_provenance_for_item" || intent === "inventory_purchase_documents_for_item";
}
function invertSort(sort) { function invertSort(sort) {
return sort === "period_asc" ? "period_desc" : "period_asc"; return sort === "period_asc" ? "period_desc" : "period_asc";
} }
@ -2097,10 +2144,11 @@ function buildLimitedExecutionResult(input) {
intent: input.intent.intent, intent: input.intent.intent,
selectedRecipe: input.selectedRecipe, selectedRecipe: input.selectedRecipe,
filters: input.filters, filters: input.filters,
semanticFrame: input.semanticFrame,
responseType: "LIMITED_WITH_REASON", responseType: "LIMITED_WITH_REASON",
rowsMatched: input.rowsMatched rowsMatched: input.rowsMatched
}); });
const requestedResultMode = resolveRequestedResultMode(input.intent.intent, input.filters); const requestedResultMode = resolveRequestedResultMode(input.intent.intent, input.filters, input.semanticFrame);
const reasonsWithConfirmedFallback = withConfirmedBalanceFallbackReason(input.reasons, requestedResultMode, undefined, resultSemantics.result_mode); const reasonsWithConfirmedFallback = withConfirmedBalanceFallbackReason(input.reasons, requestedResultMode, undefined, resultSemantics.result_mode);
const exactLimitedReason = input.intent.intent === "inventory_on_hand_as_of_date" const exactLimitedReason = input.intent.intent === "inventory_on_hand_as_of_date"
? "exact_inventory_mode_limited_response" ? "exact_inventory_mode_limited_response"
@ -2172,6 +2220,7 @@ function buildLimitedExecutionResult(input) {
account_scope_drop_reason: accountScopeAudit.accountScopeDropReason, account_scope_drop_reason: accountScopeAudit.accountScopeDropReason,
runtime_readiness: runtimeReadinessForLimitedCategory(input.category), runtime_readiness: runtimeReadinessForLimitedCategory(input.category),
limited_reason_category: input.category, limited_reason_category: input.category,
semantic_frame: input.semanticFrame ?? null,
response_type: "LIMITED_WITH_REASON", response_type: "LIMITED_WITH_REASON",
capability_id: input.capabilityAudit?.capabilityId ?? null, capability_id: input.capabilityAudit?.capabilityId ?? null,
capability_layer: input.capabilityAudit?.layer ?? null, capability_layer: input.capabilityAudit?.layer ?? null,
@ -2198,11 +2247,12 @@ class AddressQueryService {
return null; return null;
} }
const followupContext = options.followupContext ?? null; const followupContext = options.followupContext ?? null;
const decompose = (0, decomposeStage_1.runAddressDecomposeStage)(userMessage, followupContext); const decompose = (0, decomposeStage_1.runAddressDecomposeStage)(userMessage, followupContext, options.llmSemanticHints ?? null);
if (!decompose) { if (!decompose) {
return null; return null;
} }
const { mode, shape, intent, filters } = decompose; const { mode, shape, intent, filters } = decompose;
const semanticFrame = filters.semantic_frame ?? null;
const baseReasons = [...decompose.baseReasons]; const baseReasons = [...decompose.baseReasons];
const analysisDate = normalizeAnalysisDateHint(options.analysisDateHint); const analysisDate = normalizeAnalysisDateHint(options.analysisDateHint);
if (analysisDate) { if (analysisDate) {
@ -2218,7 +2268,16 @@ class AddressQueryService {
baseReasons.push("as_of_date_from_analysis_context"); baseReasons.push("as_of_date_from_analysis_context");
} }
} }
const requestedResultMode = resolveRequestedResultMode(intent.intent, filters.extracted_filters); const resolvedOrganizationFromMessage = applyPreExecutionOrganizationScopeGrounding({
userMessage,
filters: filters.extracted_filters,
semanticFrame,
warnings: filters.warnings,
baseReasons,
activeOrganization: options.activeOrganization ?? null,
knownOrganizations: options.knownOrganizations ?? []
});
const requestedResultMode = resolveRequestedResultMode(intent.intent, filters.extracted_filters, semanticFrame);
const confirmedBalancePayablesIntent = (intent.intent === "list_payables_counterparties" || intent.intent === "payables_confirmed_as_of_date") && const confirmedBalancePayablesIntent = (intent.intent === "list_payables_counterparties" || intent.intent === "payables_confirmed_as_of_date") &&
requestedResultMode === "confirmed_balance"; requestedResultMode === "confirmed_balance";
const confirmedBalanceReceivablesIntent = intent.intent === "receivables_confirmed_as_of_date" && requestedResultMode === "confirmed_balance"; const confirmedBalanceReceivablesIntent = intent.intent === "receivables_confirmed_as_of_date" && requestedResultMode === "confirmed_balance";
@ -2236,7 +2295,7 @@ class AddressQueryService {
const inventoryConfirmedExecution = confirmedBalanceInventoryIntent const inventoryConfirmedExecution = confirmedBalanceInventoryIntent
? resolveExecutionFiltersForConfirmedBalance(filters.extracted_filters, analysisDate) ? resolveExecutionFiltersForConfirmedBalance(filters.extracted_filters, analysisDate)
: null; : null;
const executionFilters = inventoryConfirmedExecution?.executionFilters ?? let executionFilters = inventoryConfirmedExecution?.executionFilters ??
payablesConfirmedExecution?.executionFilters ?? payablesConfirmedExecution?.executionFilters ??
receivablesConfirmedExecution?.executionFilters ?? receivablesConfirmedExecution?.executionFilters ??
vatPayableConfirmedExecution?.executionFilters ?? vatPayableConfirmedExecution?.executionFilters ??
@ -2303,6 +2362,7 @@ class AddressQueryService {
...baseReasons, ...baseReasons,
config_1.FEATURE_ASSISTANT_CAPABILITY_ROUTE_GUARD_V1 ? "capability_route_guard_blocked" : "capability_route_guard_skipped" config_1.FEATURE_ASSISTANT_CAPABILITY_ROUTE_GUARD_V1 ? "capability_route_guard_blocked" : "capability_route_guard_skipped"
], ],
semanticFrame,
capabilityAudit, capabilityAudit,
shadowRouteAudit shadowRouteAudit
}); });
@ -2384,6 +2444,7 @@ class AddressQueryService {
nextStep: "могу проверить близкие сценарии: документы/платежи по контрагенту, договоры или остаток по счету", nextStep: "могу проверить близкие сценарии: документы/платежи по контрагенту, договоры или остаток по счету",
limitations: ["intent_not_supported_in_v1"], limitations: ["intent_not_supported_in_v1"],
reasons: baseReasons, reasons: baseReasons,
semanticFrame,
capabilityAudit, capabilityAudit,
shadowRouteAudit shadowRouteAudit
}); });
@ -2405,6 +2466,7 @@ class AddressQueryService {
nextStep: "можно выбрать близкий поддерживаемый сценарий или переключить запрос в режим расширенной проверки", nextStep: "можно выбрать близкий поддерживаемый сценарий или переключить запрос в режим расширенной проверки",
limitations: ["recipe_not_available"], limitations: ["recipe_not_available"],
reasons: [...baseReasons, ...recipeSelection.selection_reason], reasons: [...baseReasons, ...recipeSelection.selection_reason],
semanticFrame,
capabilityAudit, capabilityAudit,
shadowRouteAudit shadowRouteAudit
}); });
@ -2426,6 +2488,7 @@ class AddressQueryService {
nextStep: `уточните: ${recipeSelection.missing_required_filters.join(", ")}`, nextStep: `уточните: ${recipeSelection.missing_required_filters.join(", ")}`,
limitations: ["missing_required_filters"], limitations: ["missing_required_filters"],
reasons: [...baseReasons, ...recipeSelection.selection_reason], reasons: [...baseReasons, ...recipeSelection.selection_reason],
semanticFrame,
capabilityAudit, capabilityAudit,
shadowRouteAudit shadowRouteAudit
}); });
@ -2447,6 +2510,7 @@ class AddressQueryService {
nextStep: "включите FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1", nextStep: "включите FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1",
limitations: ["address_live_lane_disabled"], limitations: ["address_live_lane_disabled"],
reasons: baseReasons, reasons: baseReasons,
semanticFrame,
capabilityAudit, capabilityAudit,
shadowRouteAudit shadowRouteAudit
}); });
@ -2621,6 +2685,7 @@ class AddressQueryService {
nextStep: mcp.error, nextStep: mcp.error,
limitations: ["mcp_call_failed"], limitations: ["mcp_call_failed"],
reasons: [...baseReasons, mcp.error], reasons: [...baseReasons, mcp.error],
semanticFrame,
capabilityAudit, capabilityAudit,
shadowRouteAudit shadowRouteAudit
}); });
@ -2633,7 +2698,7 @@ class AddressQueryService {
scopedRows.length === 0; scopedRows.length === 0;
const normalizedRows = accountScopeFallbackApplied ? normalizedRawRows : scopedRows; const normalizedRows = accountScopeFallbackApplied ? normalizedRawRows : scopedRows;
anchor = (0, resolveStage_1.refineAnchorFromRows)(anchor, normalizedRows); anchor = (0, resolveStage_1.refineAnchorFromRows)(anchor, normalizedRows);
const filtersForMatching = anchor.anchor_type === "counterparty" && anchor.anchor_value_resolved let filtersForMatching = anchor.anchor_type === "counterparty" && anchor.anchor_value_resolved
? { ...executionFilters, counterparty: anchor.anchor_value_resolved } ? { ...executionFilters, counterparty: anchor.anchor_value_resolved }
: 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 }
@ -2645,11 +2710,55 @@ class AddressQueryService {
rowsBeforeScope: normalizedRawRows.length, rowsBeforeScope: normalizedRawRows.length,
rowsAfterScope: normalizedRows.length rowsAfterScope: normalizedRows.length
}); });
const anchorFilter = applyAddressFilters(normalizedRows, filtersForMatching); let anchorFilter = applyAddressFilters(normalizedRows, filtersForMatching);
const filterByAnchors = anchorFilter.rows; let filterByAnchors = anchorFilter.rows;
const filteredRowsBeforeFutureGuard = applyIntentSpecificFilter(intent.intent, filterByAnchors); let filteredRowsBeforeFutureGuard = applyIntentSpecificFilter(intent.intent, filterByAnchors);
const filteredRowsFutureGuard = applyFutureDatedRowsGuard(filteredRowsBeforeFutureGuard, intent.intent, futureGuardReferenceDate); let filteredRowsFutureGuard = applyFutureDatedRowsGuard(filteredRowsBeforeFutureGuard, intent.intent, futureGuardReferenceDate);
const filteredRows = filteredRowsFutureGuard.rows; let filteredRows = filteredRowsFutureGuard.rows;
let organizationWarehouseRecoveryApplied = false;
if (filteredRows.length === 0 &&
anchorFilter.mismatchReason === "warehouse_anchor_not_matched_in_materialized_rows" &&
resolvedOrganizationFromMessage) {
filters.extracted_filters = {
...filters.extracted_filters,
organization: resolvedOrganizationFromMessage
};
delete filters.extracted_filters.warehouse;
executionFilters = {
...executionFilters,
organization: resolvedOrganizationFromMessage
};
delete executionFilters.warehouse;
filtersForMatching = {
...filtersForMatching,
organization: resolvedOrganizationFromMessage
};
delete filtersForMatching.warehouse;
anchor = {
...anchor,
anchor_type: "organization",
anchor_value_raw: anchor.anchor_value_raw,
anchor_value_resolved: resolvedOrganizationFromMessage,
resolver_confidence: "medium"
};
if (semanticFrame) {
semanticFrame.scope_kind = "explicit_anchor";
semanticFrame.anchor_kind = "organization";
semanticFrame.anchor_value = resolvedOrganizationFromMessage;
}
if (!filters.warnings.includes("warehouse_anchor_regrounded_to_organization_scope")) {
filters.warnings.push("warehouse_anchor_regrounded_to_organization_scope");
}
if (!baseReasons.includes("warehouse_anchor_regrounded_to_organization_scope")) {
baseReasons.push("warehouse_anchor_regrounded_to_organization_scope");
}
anchorFilter = applyAddressFilters(normalizedRows, filtersForMatching);
filterByAnchors = anchorFilter.rows;
filteredRowsBeforeFutureGuard = applyIntentSpecificFilter(intent.intent, filterByAnchors);
filteredRowsFutureGuard = applyFutureDatedRowsGuard(filteredRowsBeforeFutureGuard, intent.intent, futureGuardReferenceDate);
filteredRows = filteredRowsFutureGuard.rows;
organizationWarehouseRecoveryApplied = filteredRows.length > 0;
}
if (filteredRowsFutureGuard.droppedCount > 0) { if (filteredRowsFutureGuard.droppedCount > 0) {
if (!filters.warnings.includes("future_rows_excluded_from_response")) { if (!filters.warnings.includes("future_rows_excluded_from_response")) {
filters.warnings.push("future_rows_excluded_from_response"); filters.warnings.push("future_rows_excluded_from_response");
@ -2675,6 +2784,11 @@ class AddressQueryService {
: matchFailureStage === "materialized_but_filtered_out_by_recipe" : matchFailureStage === "materialized_but_filtered_out_by_recipe"
? "rows_filtered_out_by_intent_recipe_after_anchor_match" ? "rows_filtered_out_by_intent_recipe_after_anchor_match"
: null; : null;
if (organizationWarehouseRecoveryApplied) {
if (!baseReasons.includes("organization_scope_live_grounding_recovered_rows")) {
baseReasons.push("organization_scope_live_grounding_recovered_rows");
}
}
if (filteredRows.length === 0 && intent.intent === "list_documents_by_contract" && filterByAnchors.length > 0) { if (filteredRows.length === 0 && intent.intent === "list_documents_by_contract" && filterByAnchors.length > 0) {
const recoveredBankRows = applyIntentSpecificFilter("bank_operations_by_contract", filterByAnchors); const recoveredBankRows = applyIntentSpecificFilter("bank_operations_by_contract", filterByAnchors);
const recoveredRows = recoveredBankRows.length > 0 ? recoveredBankRows : filterByAnchors; const recoveredRows = recoveredBankRows.length > 0 ? recoveredBankRows : filterByAnchors;
@ -2732,6 +2846,7 @@ class AddressQueryService {
intent: intent.intent, intent: intent.intent,
selectedRecipe: effectiveRecipeId, selectedRecipe: effectiveRecipeId,
filters: filters.extracted_filters, filters: filters.extracted_filters,
semanticFrame,
responseType: factual.responseType, responseType: factual.responseType,
rowsMatched: recoveredRows.length rowsMatched: recoveredRows.length
}), factual.semantics), }), factual.semantics),
@ -2855,6 +2970,7 @@ class AddressQueryService {
intent: intent.intent, intent: intent.intent,
selectedRecipe: expandedSelection.selected_recipe.recipe_id, selectedRecipe: expandedSelection.selected_recipe.recipe_id,
filters: filters.extracted_filters, filters: filters.extracted_filters,
semanticFrame,
responseType: expandedFactual.responseType, responseType: expandedFactual.responseType,
rowsMatched: expandedFilteredRows.length rowsMatched: expandedFilteredRows.length
}), expandedFactual.semantics), }), expandedFactual.semantics),
@ -2870,8 +2986,13 @@ class AddressQueryService {
} }
if (filteredRows.length === 0 && canAutoBroadenPeriodWindow(intent.intent, filters.extracted_filters)) { if (filteredRows.length === 0 && canAutoBroadenPeriodWindow(intent.intent, filters.extracted_filters)) {
const autoBroadenedFilters = { ...filters.extracted_filters }; const autoBroadenedFilters = { ...filters.extracted_filters };
const broadenedAdjustments = [];
delete autoBroadenedFilters.period_from; delete autoBroadenedFilters.period_from;
delete autoBroadenedFilters.period_to; delete autoBroadenedFilters.period_to;
if (stageStatus === "no_raw_rows" && shouldClearAsOfDateForHistoryRecovery(intent.intent) && toNonEmptyFilterValue(autoBroadenedFilters.as_of_date)) {
delete autoBroadenedFilters.as_of_date;
broadenedAdjustments.push("as_of_date_cleared_for_history_recovery");
}
if (shouldBoostAutoBroadenedLimit(intent.intent)) { if (shouldBoostAutoBroadenedLimit(intent.intent)) {
autoBroadenedFilters.limit = Math.max(ADDRESS_ANCHOR_RECOVERY_LIMIT, typeof autoBroadenedFilters.limit === "number" && Number.isFinite(autoBroadenedFilters.limit) autoBroadenedFilters.limit = Math.max(ADDRESS_ANCHOR_RECOVERY_LIMIT, typeof autoBroadenedFilters.limit === "number" && Number.isFinite(autoBroadenedFilters.limit)
? Math.max(1, Math.trunc(autoBroadenedFilters.limit)) ? Math.max(1, Math.trunc(autoBroadenedFilters.limit))
@ -2930,12 +3051,17 @@ class AddressQueryService {
const observedWindow = deriveObservedPeriodWindow(broadenedFilteredRows); const observedWindow = deriveObservedPeriodWindow(broadenedFilteredRows);
const broadenedPrefix = composeAutoBroadenedPeriodPrefix(filters.extracted_filters, observedWindow); const broadenedPrefix = composeAutoBroadenedPeriodPrefix(filters.extracted_filters, observedWindow);
const broadenedFactual = (0, composeStage_1.composeFactualReply)(intent.intent, broadenedFilteredRows, composeOptionsFromFilters(autoBroadenedFilters)); const broadenedFactual = (0, composeStage_1.composeFactualReply)(intent.intent, broadenedFilteredRows, composeOptionsFromFilters(autoBroadenedFilters));
const broadenedLimitations = [...filters.warnings, "period_window_auto_broadened_to_available_data"]; const broadenedLimitations = [
const broadenedReasons = [...baseReasons, "period_window_auto_broadened_to_available_data"]; ...filters.warnings,
...broadenedAdjustments,
"period_window_auto_broadened_to_available_data"
];
const broadenedReasons = [...baseReasons, ...broadenedAdjustments, "period_window_auto_broadened_to_available_data"];
const broadenedResultSemantics = mergeAddressResultSemantics(deriveAddressResultSemantics({ const broadenedResultSemantics = mergeAddressResultSemantics(deriveAddressResultSemantics({
intent: intent.intent, intent: intent.intent,
selectedRecipe: broadenedSelection.selected_recipe.recipe_id, selectedRecipe: broadenedSelection.selected_recipe.recipe_id,
filters: filters.extracted_filters, filters: filters.extracted_filters,
semanticFrame,
responseType: broadenedFactual.responseType, responseType: broadenedFactual.responseType,
rowsMatched: broadenedFilteredRows.length rowsMatched: broadenedFilteredRows.length
}), broadenedFactual.semantics); }), broadenedFactual.semantics);
@ -3000,6 +3126,7 @@ class AddressQueryService {
route_expectation_expected_selected_recipes: broadenedRouteExpectationAudit.expectedSelectedRecipes, route_expectation_expected_selected_recipes: broadenedRouteExpectationAudit.expectedSelectedRecipes,
route_expectation_expected_requested_result_modes: broadenedRouteExpectationAudit.expectedRequestedResultModes, route_expectation_expected_requested_result_modes: broadenedRouteExpectationAudit.expectedRequestedResultModes,
route_expectation_expected_result_modes: broadenedRouteExpectationAudit.expectedResultModes, route_expectation_expected_result_modes: broadenedRouteExpectationAudit.expectedResultModes,
semantic_frame: semanticFrame,
...broadenedResultSemantics, ...broadenedResultSemantics,
limitations: broadenedLimitations, limitations: broadenedLimitations,
reasons: withConfirmedBalanceFallbackReason(broadenedReasons, requestedResultMode, broadenedFactual.semantics) reasons: withConfirmedBalanceFallbackReason(broadenedReasons, requestedResultMode, broadenedFactual.semantics)
@ -3124,6 +3251,7 @@ class AddressQueryService {
intent: intent.intent, intent: intent.intent,
selectedRecipe: historicalSelection.selected_recipe.recipe_id, selectedRecipe: historicalSelection.selected_recipe.recipe_id,
filters: filters.extracted_filters, filters: filters.extracted_filters,
semanticFrame,
responseType: historicalFactual.responseType, responseType: historicalFactual.responseType,
rowsMatched: historicalFilteredRows.length rowsMatched: historicalFilteredRows.length
}), historicalFactual.semantics), }), historicalFactual.semantics),
@ -3195,6 +3323,7 @@ class AddressQueryService {
intent: intent.intent, intent: intent.intent,
selectedRecipe: effectiveRecipeId, selectedRecipe: effectiveRecipeId,
filters: filters.extracted_filters, filters: filters.extracted_filters,
semanticFrame,
responseType: fallbackFactual.responseType, responseType: fallbackFactual.responseType,
rowsMatched: documentBankFallbackRows.length rowsMatched: documentBankFallbackRows.length
}), fallbackFactual.semantics), }), fallbackFactual.semantics),
@ -3318,6 +3447,7 @@ class AddressQueryService {
nextStep, nextStep,
limitations, limitations,
reasons: baseReasons, reasons: baseReasons,
semanticFrame,
capabilityAudit, capabilityAudit,
shadowRouteAudit shadowRouteAudit
}); });
@ -3351,6 +3481,7 @@ class AddressQueryService {
intent: composeIntent, intent: composeIntent,
selectedRecipe: effectiveRecipeId, selectedRecipe: effectiveRecipeId,
filters: filters.extracted_filters, filters: filters.extracted_filters,
semanticFrame,
responseType: factual.responseType, responseType: factual.responseType,
rowsMatched: filteredRows.length rowsMatched: filteredRows.length
}), factual.semantics); }), factual.semantics);
@ -3388,6 +3519,7 @@ class AddressQueryService {
nextStep: "проверьте intent/recipe mapping или отключите FEATURE_ASSISTANT_ROUTE_EXPECTATION_HARD_GUARD_V1 для безопасного rollout", nextStep: "проверьте intent/recipe mapping или отключите FEATURE_ASSISTANT_ROUTE_EXPECTATION_HARD_GUARD_V1 для безопасного rollout",
limitations: ["route_expectation_mismatch_guard_blocked"], limitations: ["route_expectation_mismatch_guard_blocked"],
reasons: [...baseReasons, `route_expectation_mismatch:${finalRouteExpectationAudit.reason}`], reasons: [...baseReasons, `route_expectation_mismatch:${finalRouteExpectationAudit.reason}`],
semanticFrame,
capabilityAudit, capabilityAudit,
shadowRouteAudit, shadowRouteAudit,
routeExpectationAudit: finalRouteExpectationAudit routeExpectationAudit: finalRouteExpectationAudit
@ -3437,6 +3569,7 @@ class AddressQueryService {
: "specify as_of_date/counterparty or enable detailed settlement registers for exact confirmed balance", : "specify as_of_date/counterparty or enable detailed settlement registers for exact confirmed balance",
limitations: [`exact_${exactModeName}_mode_unconfirmed_output_blocked`], limitations: [`exact_${exactModeName}_mode_unconfirmed_output_blocked`],
reasons: [...baseReasons, `exact_${exactModeName}_mode_unconfirmed_output_blocked`], reasons: [...baseReasons, `exact_${exactModeName}_mode_unconfirmed_output_blocked`],
semanticFrame,
capabilityAudit, capabilityAudit,
shadowRouteAudit, shadowRouteAudit,
routeExpectationAudit: finalRouteExpectationAudit routeExpectationAudit: finalRouteExpectationAudit
@ -3500,6 +3633,7 @@ class AddressQueryService {
route_expectation_expected_selected_recipes: finalRouteExpectationAudit.expectedSelectedRecipes, route_expectation_expected_selected_recipes: finalRouteExpectationAudit.expectedSelectedRecipes,
route_expectation_expected_requested_result_modes: finalRouteExpectationAudit.expectedRequestedResultModes, route_expectation_expected_requested_result_modes: finalRouteExpectationAudit.expectedRequestedResultModes,
route_expectation_expected_result_modes: finalRouteExpectationAudit.expectedResultModes, route_expectation_expected_result_modes: finalRouteExpectationAudit.expectedResultModes,
semantic_frame: semanticFrame,
...factualResultSemantics, ...factualResultSemantics,
limitations: factualLimitations, limitations: factualLimitations,
reasons: withConfirmedBalanceFallbackReason(reasonsWithRouteExpectation, requestedResultMode, factual.semantics, factualResultSemantics.result_mode) reasons: withConfirmedBalanceFallbackReason(reasonsWithRouteExpectation, requestedResultMode, factual.semantics, factualResultSemantics.result_mode)

View File

@ -6,6 +6,7 @@ const addressQueryClassifier_1 = require("../addressQueryClassifier");
const addressQueryShapeClassifier_1 = require("../addressQueryShapeClassifier"); const addressQueryShapeClassifier_1 = require("../addressQueryShapeClassifier");
const addressIntentResolver_1 = require("../addressIntentResolver"); const addressIntentResolver_1 = require("../addressIntentResolver");
const addressFilterExtractor_1 = require("../addressFilterExtractor"); const addressFilterExtractor_1 = require("../addressFilterExtractor");
const semanticHintOverlay_1 = require("./semanticHintOverlay");
function hasExplicitPeriodWindow(filters) { 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));
@ -253,6 +254,144 @@ function isInventoryIntent(intent) {
intent === "inventory_purchase_to_sale_chain" || intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date"); intent === "inventory_aging_by_purchase_date");
} }
function isInventoryRootFrameIntent(intent) {
return intent === "inventory_on_hand_as_of_date";
}
function isInventoryDrilldownFrameIntent(intent) {
return (intent === "inventory_purchase_provenance_for_item" ||
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date");
}
function buildInventoryRootFollowupContext(followupContext) {
if (!followupContext || !followupContext.root_intent || !followupContext.root_filters) {
return followupContext;
}
return {
...followupContext,
previous_intent: followupContext.root_intent,
previous_filters: { ...followupContext.root_filters },
previous_anchor_type: followupContext.root_anchor_type ?? followupContext.previous_anchor_type,
previous_anchor_value: followupContext.root_anchor_value ?? followupContext.previous_anchor_value,
current_frame_kind: "inventory_root"
};
}
function getTokenCount(text) {
return String(text ?? "")
.trim()
.split(/\s+/)
.filter(Boolean).length;
}
function resolveMonthNumberFromText(text) {
const normalized = String(text ?? "").toLowerCase();
if (!normalized) {
return null;
}
if (/январ|january|jan/iu.test(normalized))
return 1;
if (/феврал|february|feb/iu.test(normalized))
return 2;
if (/март|march|mar/iu.test(normalized))
return 3;
if (/апрел|april|apr/iu.test(normalized))
return 4;
if (/(?:^|[\s,.;:!?()\-])ма(?:й|е|я)(?=$|[\s,.;:!?()\-])|may/iu.test(normalized))
return 5;
if (/июн|june|jun/iu.test(normalized))
return 6;
if (/июл|july|jul/iu.test(normalized))
return 7;
if (/август|august|aug/iu.test(normalized))
return 8;
if (/сентябр|september|sep/iu.test(normalized))
return 9;
if (/октябр|october|oct/iu.test(normalized))
return 10;
if (/ноябр|november|nov/iu.test(normalized))
return 11;
if (/декабр|december|dec/iu.test(normalized))
return 12;
return null;
}
function resolveYearFromFilters(filters) {
const candidates = [
toNonEmptyString(filters?.as_of_date),
toNonEmptyString(filters?.period_to),
toNonEmptyString(filters?.period_from)
];
for (const candidate of candidates) {
const match = candidate?.match(/\b((?:19|20)\d{2})\b/u);
if (match) {
const year = Number(match[1]);
if (Number.isFinite(year)) {
return year;
}
}
}
return null;
}
function hasRelativeYearHint(text) {
return /(?:эт(?:от|ого)(?:\s+же)?\s+год|этого\s+же\s+года|того\s+же\s+года|this\s+year|same\s+year|that\s+year)/iu.test(String(text ?? ""));
}
function resolveRelativeMonthPeriodFromInventoryRoot(userMessage, followupContext) {
if (!followupContext || !isInventoryRootFrameIntent(followupContext.root_intent)) {
return null;
}
const month = resolveMonthNumberFromText(userMessage);
if (!month) {
return null;
}
const normalized = String(userMessage ?? "");
if (hasExplicitPeriodLiteral(normalized) || hasExplicitCurrentDateHint(normalized)) {
return null;
}
const shortTemporalPatch = getTokenCount(normalized) <= 8 || hasRelativeYearHint(normalized);
if (!shortTemporalPatch) {
return null;
}
const year = resolveYearFromFilters(followupContext.root_filters);
if (!year) {
return null;
}
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
const periodFrom = `${year}-${String(month).padStart(2, "0")}-01`;
const periodTo = `${year}-${String(month).padStart(2, "0")}-${String(lastDay).padStart(2, "0")}`;
return {
period_from: periodFrom,
period_to: periodTo,
as_of_date: periodTo
};
}
function shouldRestoreInventoryRootFrame(userMessage, intent, extractedFilters, followupContext) {
if (!followupContext || !isInventoryRootFrameIntent(followupContext.root_intent)) {
return false;
}
const currentFrameKind = followupContext.current_frame_kind ?? null;
const previousIntent = followupContext.previous_intent;
const comingFromInventoryDrilldown = currentFrameKind === "inventory_drilldown" || isInventoryDrilldownFrameIntent(previousIntent);
if (!comingFromInventoryDrilldown) {
return false;
}
const normalized = String(userMessage ?? "");
if (hasSelectedObjectInventorySignal(normalized) ||
hasInventorySupplierFollowupCue(normalized) ||
hasInventoryPurchaseDocumentsFollowupCue(normalized) ||
hasInventoryPurchaseDateFollowupCue(normalized) ||
hasBareInventoryPurchaseDateFollowupCue(normalized) ||
hasInventorySaleFollowupCue(normalized) ||
hasInventoryPurchaseToSaleChainFollowupCue(normalized)) {
return false;
}
if (intent === "inventory_on_hand_as_of_date") {
return true;
}
const hasTemporalPatch = hasExplicitPeriodWindow(extractedFilters) ||
Boolean(toNonEmptyString(extractedFilters.as_of_date)) ||
hasExplicitPeriodLiteral(normalized) ||
Boolean(resolveRelativeMonthPeriodFromInventoryRoot(normalized, followupContext));
return hasTemporalPatch;
}
function hasSelectedObjectInventorySignal(text) { function hasSelectedObjectInventorySignal(text) {
return /(?:по\s+выбранному\s+объекту|for\s+selected\s+object)/iu.test(String(text ?? "")); return /(?:по\s+выбранному\s+объекту|for\s+selected\s+object)/iu.test(String(text ?? ""));
} }
@ -350,6 +489,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
const previousAsOfDate = toNonEmptyString(previous.as_of_date); const previousAsOfDate = toNonEmptyString(previous.as_of_date);
const previousPeriodFrom = toNonEmptyString(previous.period_from); const previousPeriodFrom = toNonEmptyString(previous.period_from);
const previousPeriodTo = toNonEmptyString(previous.period_to); const previousPeriodTo = toNonEmptyString(previous.period_to);
const relativeMonthFromInventoryRoot = resolveRelativeMonthPeriodFromInventoryRoot(userMessage, followupContext);
const allTimeRequested = hasAllTimeHint(userMessage); const allTimeRequested = hasAllTimeHint(userMessage);
const sameDateRequested = hasSameDateHint(userMessage); const sameDateRequested = hasSameDateHint(userMessage);
if (!toNonEmptyString(merged.organization) && previousOrganization) { if (!toNonEmptyString(merged.organization) && previousOrganization) {
@ -516,6 +656,13 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
reasons.push("as_of_date_from_open_items_followup_context"); reasons.push("as_of_date_from_open_items_followup_context");
} }
} }
if (relativeMonthFromInventoryRoot &&
(intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date")) {
merged.period_from = relativeMonthFromInventoryRoot.period_from;
merged.period_to = relativeMonthFromInventoryRoot.period_to;
merged.as_of_date = relativeMonthFromInventoryRoot.as_of_date;
reasons.push("period_derived_from_inventory_root_frame_year");
}
if (intent === "inventory_aging_by_purchase_date") { if (intent === "inventory_aging_by_purchase_date") {
const explicitItemMention = /(?:^|[\s,.;:!?()\-\u2014])(?:товар(?:у|а|ом)?|позици(?:и|я|ю)|item|row|line)(?=$|[\s,.;:!?()\-\u2014])/iu.test(String(userMessage ?? "")); const explicitItemMention = /(?:^|[\s,.;:!?()\-\u2014])(?:товар(?:у|а|ом)?|позици(?:и|я|ю)|item|row|line)(?=$|[\s,.;:!?()\-\u2014])/iu.test(String(userMessage ?? ""));
if (toNonEmptyString(merged.item) && !explicitItemMention) { if (toNonEmptyString(merged.item) && !explicitItemMention) {
@ -822,7 +969,7 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
reasons: [...detectedIntent.reasons, "intent_from_followup_context"] reasons: [...detectedIntent.reasons, "intent_from_followup_context"]
}; };
} }
function runAddressDecomposeStage(userMessage, followupContext) { function runAddressDecomposeStage(userMessage, followupContext, llmSemanticHints = null) {
const detectedMode = (0, addressQueryClassifier_1.detectAddressQuestionMode)(userMessage); const detectedMode = (0, addressQueryClassifier_1.detectAddressQuestionMode)(userMessage);
const shape = (0, addressQueryShapeClassifier_1.classifyAddressQueryShape)(userMessage); const shape = (0, addressQueryShapeClassifier_1.classifyAddressQueryShape)(userMessage);
const allowExplainAsFollowup = shape.shape === "EXPLAIN_OR_REASON" && const allowExplainAsFollowup = shape.shape === "EXPLAIN_OR_REASON" &&
@ -850,17 +997,29 @@ function runAddressDecomposeStage(userMessage, followupContext) {
if (mode.mode !== "address_query") { if (mode.mode !== "address_query") {
return null; return null;
} }
const intent = deriveIntentWithFollowupContext(detectedIntent, userMessage, followupContext); let effectiveFollowupContext = followupContext;
const extractedFilters = (0, addressFilterExtractor_1.extractAddressFilters)(userMessage, intent.intent); let intent = deriveIntentWithFollowupContext(detectedIntent, userMessage, effectiveFollowupContext);
const followupMerged = mergeFollowupFilters(extractedFilters.extracted_filters, intent.intent, userMessage, followupContext); let extractedFilters = (0, semanticHintOverlay_1.applyAddressLlmSemanticHintsToExtraction)((0, addressFilterExtractor_1.extractAddressFilters)(userMessage, intent.intent), llmSemanticHints);
if (shouldRestoreInventoryRootFrame(userMessage, intent.intent, extractedFilters.extracted_filters, effectiveFollowupContext)) {
effectiveFollowupContext = buildInventoryRootFollowupContext(effectiveFollowupContext);
intent = {
intent: effectiveFollowupContext?.root_intent ?? "inventory_on_hand_as_of_date",
confidence: "low",
reasons: [...intent.reasons, "intent_restored_to_inventory_root_frame"]
};
extractedFilters = (0, semanticHintOverlay_1.applyAddressLlmSemanticHintsToExtraction)((0, addressFilterExtractor_1.extractAddressFilters)(userMessage, intent.intent), llmSemanticHints);
}
const followupMerged = mergeFollowupFilters(extractedFilters.extracted_filters, intent.intent, userMessage, effectiveFollowupContext);
const filters = { const filters = {
extracted_filters: followupMerged.filters, extracted_filters: followupMerged.filters,
missing_required_filters: resolveMissingRequiredFilters(intent.intent, followupMerged.filters), missing_required_filters: resolveMissingRequiredFilters(intent.intent, followupMerged.filters),
warnings: [...new Set([...extractedFilters.warnings, ...followupMerged.reasons])] warnings: [...new Set([...extractedFilters.warnings, ...followupMerged.reasons])],
semantic_frame: extractedFilters.semantic_frame
}; };
const followupContextApplied = Boolean(followupContext) && const followupContextApplied = Boolean(effectiveFollowupContext) &&
(mode.reasons.includes("address_mode_from_followup_context") || (mode.reasons.includes("address_mode_from_followup_context") ||
intent.reasons.includes("intent_from_followup_context") || intent.reasons.includes("intent_from_followup_context") ||
intent.reasons.includes("intent_restored_to_inventory_root_frame") ||
followupMerged.reasons.length > 0); followupMerged.reasons.length > 0);
const baseReasons = [ const baseReasons = [
...mode.reasons, ...mode.reasons,

View File

@ -6,10 +6,11 @@ const addressQueryClassifier_1 = require("../addressQueryClassifier");
const addressQueryShapeClassifier_1 = require("../addressQueryShapeClassifier"); const addressQueryShapeClassifier_1 = require("../addressQueryShapeClassifier");
const addressIntentResolver_1 = require("../addressIntentResolver"); const addressIntentResolver_1 = require("../addressIntentResolver");
const addressFilterExtractor_1 = require("../addressFilterExtractor"); const addressFilterExtractor_1 = require("../addressFilterExtractor");
const ADDRESS_SEMANTIC_DATA_SIGNAL_PATTERN = /(?:\u0434\u043e\u043a|\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442|\u0441\u0447(?:\u0435|\u0451)\u0442|\u0441\u0430\u043b\u044c\u0434\u043e|\u043e\u0431\u043e\u0440\u043e\u0442|\u043f\u043b\u0430\u0442(?:\u0435|\u0451)\u0436|\u043e\u043f\u0435\u0440\u0430\u0446|\u043f\u0435\u0440\u0438\u043e\u0434|\u0433\u043e\u0434|counterparty|contract|document|account|balance|turnover|operations?|doki|doky|dokument|dogovor|kontragent|schet|saldo|platezh|oplata)/iu; const semanticHintOverlay_1 = require("./semanticHintOverlay");
const ADDRESS_SEMANTIC_DATA_SIGNAL_PATTERN = /(?:\u0434\u043e\u043a|\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442|\u0441\u0447(?:\u0435|\u0451)\u0442|\u0441\u0430\u043b\u044c\u0434\u043e|\u043e\u0431\u043e\u0440\u043e\u0442|\u043f\u043b\u0430\u0442(?:\u0435|\u0451)\u0436|\u043e\u043f\u0435\u0440\u0430\u0446|\u043f\u0435\u0440\u0438\u043e\u0434|\u0433\u043e\u0434|\u0441\u043a\u043b\u0430\u0434|\u0442\u043e\u0432\u0430\u0440|\u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442\u0443\u0440|counterparty|contract|document|account|balance|turnover|operations?|warehouse|stock|inventory|item|goods|doki|doky|dokument|dogovor|kontragent|schet|saldo|platezh|oplata)/iu;
const ADDRESS_SEMANTIC_ENTITY_SIGNAL_PATTERN = /(?:\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u043a\u043e\u043d\u0442\u043e\u0440|customer|supplier|counterparty|company|vendor|client)/iu; const ADDRESS_SEMANTIC_ENTITY_SIGNAL_PATTERN = /(?:\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u043a\u043e\u043d\u0442\u043e\u0440|customer|supplier|counterparty|company|vendor|client)/iu;
const ADDRESS_SEMANTIC_SCOPE_META_PATTERN = /(?:\u043a\u0430\u043a\u0430\u044f\s+\u0431\u0430\u0437\u0430|\u0431\u0430\u0437\u0430\s+\u043a\u0430\u043a\u043e\u0439\s+\u043a\u043e\u043d\u0442\u043e\u0440|\u043f\u043e\s+\u043a\u0430\u043a\u0438\u043c\s+\u043a\u043e\u043d\u0442\u043e\u0440|which\s+company\s+base|which\s+tenant|data\s+scope)/iu; const ADDRESS_SEMANTIC_SCOPE_META_PATTERN = /(?:\u043a\u0430\u043a\u0430\u044f\s+\u0431\u0430\u0437\u0430|\u0431\u0430\u0437\u0430\s+\u043a\u0430\u043a\u043e\u0439\s+\u043a\u043e\u043d\u0442\u043e\u0440|\u043f\u043e\s+\u043a\u0430\u043a\u0438\u043c\s+\u043a\u043e\u043d\u0442\u043e\u0440|which\s+company\s+base|which\s+tenant|data\s+scope)/iu;
const ADDRESS_SEMANTIC_DEEP_INVESTIGATION_PATTERN = /(?:\u043f\u0440\u043e\u0432\u0435\u0440(?:\u044c|\u0438\u0442\u044c)|\u0440\u0430\u0437\u0431\u0435\u0440(?:\u0438|\u0430\u0442\u044c)|\u043f\u043e\u0447\u0435\u043c\u0443|\u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c|\u0440\u0430\u0437\u0440\u044b\u0432|\u0445\u0432\u043e\u0441\u0442|root\s*cause|trace\s*chain|state\s+transition)/iu; const ADDRESS_SEMANTIC_DEEP_INVESTIGATION_PATTERN = /(?:\u043f\u043e\u0447\u0435\u043c\u0443|\u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c|\u0440\u0430\u0437\u0440\u044b\u0432|\u0445\u0432\u043e\u0441\u0442|root\s*cause|trace\s*chain|state\s+transition|\u043f\u0440\u043e\u0432\u0435\u0440(?:\u044c|\u0438\u0442\u044c).*(?:\u0445\u0432\u043e\u0441\u0442|\u0440\u0430\u0437\u0440\u044b\u0432|\u0437\u0430\u043a\u0440\u044b\u0442|\u0446\u0435\u043f\u043e\u0447|\u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c|\u043e\u0448\u0438\u0431|\u0430\u043d\u043e\u043c\u0430\u043b|\u0440\u0438\u0441\u043a|\u0441\u0432\u0435\u0440\u043a)|\u0440\u0430\u0437\u0431\u0435\u0440(?:\u0438|\u0430\u0442\u044c).*(?:\u043f\u043e\u0447\u0435\u043c\u0443|\u0445\u0432\u043e\u0441\u0442|\u0440\u0430\u0437\u0440\u044b\u0432|\u0437\u0430\u043a\u0440\u044b\u0442|\u0446\u0435\u043f\u043e\u0447|\u043e\u0448\u0438\u0431|\u0430\u043d\u043e\u043c\u0430\u043b|\u0440\u0438\u0441\u043a))/iu;
function normalizeCompact(value) { function normalizeCompact(value) {
return String(value ?? "") return String(value ?? "")
.toLowerCase() .toLowerCase()
@ -127,8 +128,17 @@ function buildAddressLlmPredecomposeContractV1(input) {
const mode = (0, addressQueryClassifier_1.detectAddressQuestionMode)(canonicalMessage); const mode = (0, addressQueryClassifier_1.detectAddressQuestionMode)(canonicalMessage);
const shape = (0, addressQueryShapeClassifier_1.classifyAddressQueryShape)(canonicalMessage); const shape = (0, addressQueryShapeClassifier_1.classifyAddressQueryShape)(canonicalMessage);
const intent = (0, addressIntentResolver_1.resolveAddressIntent)(canonicalMessage); const intent = (0, addressIntentResolver_1.resolveAddressIntent)(canonicalMessage);
const extraction = (0, addressFilterExtractor_1.extractAddressFilters)(canonicalMessage, intent.intent); const extraction = (0, semanticHintOverlay_1.applyAddressLlmSemanticHintsToExtraction)((0, addressFilterExtractor_1.extractAddressFilters)(canonicalMessage, intent.intent), input.semanticHints ?? null);
const filters = extraction.extracted_filters; const filters = extraction.extracted_filters;
const semanticFrame = extraction.semantic_frame ?? {
scope_kind: "none",
anchor_kind: "none",
anchor_value: null,
date_scope_kind: "none",
date_basis_hint: null,
self_scope_detected: false,
selected_object_scope_detected: false
};
const periodScope = inferPeriodScope(filters, canonicalMessage); const periodScope = inferPeriodScope(filters, canonicalMessage);
return { return {
schema_version: "address_llm_predecompose_contract_v1", schema_version: "address_llm_predecompose_contract_v1",
@ -153,8 +163,9 @@ function buildAddressLlmPredecomposeContractV1(input) {
period_from: toNonEmptyString(filters.period_from), period_from: toNonEmptyString(filters.period_from),
period_to: toNonEmptyString(filters.period_to), period_to: toNonEmptyString(filters.period_to),
as_of_date: toNonEmptyString(filters.as_of_date), as_of_date: toNonEmptyString(filters.as_of_date),
has_explicit_period: Boolean(toNonEmptyString(filters.as_of_date) || toNonEmptyString(filters.period_from) || toNonEmptyString(filters.period_to)) has_explicit_period: semanticFrame.date_scope_kind === "explicit"
}, },
semantics: semanticFrame,
aggregation_profile: inferAggregationProfile(intent.intent, shape.shape) aggregation_profile: inferAggregationProfile(intent.intent, shape.shape)
}; };
} }
@ -238,6 +249,7 @@ function buildAddressSemanticExtractionContractV1(input) {
as_of_date: predecomposeContract.period.as_of_date, as_of_date: predecomposeContract.period.as_of_date,
has_explicit_period: predecomposeContract.period.has_explicit_period has_explicit_period: predecomposeContract.period.has_explicit_period
}, },
semantics: predecomposeContract.semantics,
guard_hints: { guard_hints: {
source_data_signal_detected: sourceDataSignal, source_data_signal_detected: sourceDataSignal,
canonical_data_signal_detected: canonicalDataSignal, canonical_data_signal_detected: canonicalDataSignal,

View File

@ -143,6 +143,7 @@ function resolvePrimaryAnchor(intent, filters) {
const contract = typeof filters.contract === "string" ? filters.contract.trim() : ""; const contract = typeof filters.contract === "string" ? filters.contract.trim() : "";
const item = typeof filters.item === "string" ? filters.item.trim() : ""; const item = typeof filters.item === "string" ? filters.item.trim() : "";
const warehouse = typeof filters.warehouse === "string" ? filters.warehouse.trim() : ""; const warehouse = typeof filters.warehouse === "string" ? filters.warehouse.trim() : "";
const organization = typeof filters.organization === "string" ? filters.organization.trim() : "";
const documentRef = typeof filters.document_ref === "string" ? filters.document_ref.trim() : ""; const documentRef = typeof filters.document_ref === "string" ? filters.document_ref.trim() : "";
if (intent === "account_balance_snapshot" || intent === "documents_forming_balance") { if (intent === "account_balance_snapshot" || intent === "documents_forming_balance") {
if (account) { if (account) {
@ -218,6 +219,15 @@ function resolvePrimaryAnchor(intent, filters) {
ambiguity_count: 0 ambiguity_count: 0
}; };
} }
if (organization) {
return {
anchor_type: "organization",
anchor_value_raw: organization,
anchor_value_resolved: organization,
resolver_confidence: "medium",
ambiguity_count: 0
};
}
if (documentRef) { if (documentRef) {
return { return {
anchor_type: "document_ref", anchor_type: "document_ref",
@ -242,15 +252,24 @@ function refineAnchorFromRows(anchor, rows) {
if (anchor.anchor_type !== "counterparty" && if (anchor.anchor_type !== "counterparty" &&
anchor.anchor_type !== "contract" && anchor.anchor_type !== "contract" &&
anchor.anchor_type !== "item" && anchor.anchor_type !== "item" &&
anchor.anchor_type !== "warehouse") { anchor.anchor_type !== "warehouse" &&
anchor.anchor_type !== "organization") {
return anchor; return anchor;
} }
const needleRaw = String(anchor.anchor_value_raw ?? "").trim(); const needleRaw = String(anchor.anchor_value_raw ?? "").trim();
if (!needleRaw) { if (!needleRaw) {
return anchor; return anchor;
} }
const searchableRows = anchor.anchor_type === "item" || anchor.anchor_type === "warehouse" const searchableRows = anchor.anchor_type === "item" || anchor.anchor_type === "warehouse" || anchor.anchor_type === "organization"
? rows.flatMap((row) => [row.registrator, row.item ?? "", row.warehouse ?? "", row.account_dt ?? "", row.account_kt ?? "", ...row.analytics]) ? rows.flatMap((row) => [
row.registrator,
row.item ?? "",
row.warehouse ?? "",
row.organization ?? "",
row.account_dt ?? "",
row.account_kt ?? "",
...row.analytics
])
: rows.flatMap((row) => row.analytics); : rows.flatMap((row) => row.analytics);
const candidates = uniqueStrings(searchableRows const candidates = uniqueStrings(searchableRows
.map((value) => value.trim()) .map((value) => value.trim())

View File

@ -0,0 +1,138 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.normalizeAddressLlmSemanticHints = normalizeAddressLlmSemanticHints;
exports.applyAddressLlmSemanticHintsToExtraction = applyAddressLlmSemanticHintsToExtraction;
function toNonEmptyString(value) {
if (value === null || value === undefined) {
return null;
}
const normalized = String(value).trim();
return normalized.length > 0 ? normalized : null;
}
function normalizeToken(value) {
return String(value ?? "")
.trim()
.toLowerCase()
.replace(/\s+/g, "_");
}
function normalizeAddressLlmSemanticHints(value) {
if (!value || typeof value !== "object") {
return null;
}
const source = value;
const scopeToken = normalizeToken(source.scope_target_kind);
const dateToken = normalizeToken(source.date_scope_kind);
const scopeTargetKind = scopeToken === "self_scope" ||
scopeToken === "selected_object" ||
scopeToken === "organization" ||
scopeToken === "warehouse" ||
scopeToken === "counterparty" ||
scopeToken === "contract" ||
scopeToken === "item"
? scopeToken
: "none";
const dateScopeKind = dateToken === "explicit" || dateToken === "implicit_current" ? dateToken : "missing";
return {
scope_target_kind: scopeTargetKind,
scope_target_text: toNonEmptyString(source.scope_target_text),
date_scope_kind: dateScopeKind,
self_scope_detected: source.self_scope_detected === true || scopeTargetKind === "self_scope",
selected_object_scope_detected: source.selected_object_scope_detected === true || scopeTargetKind === "selected_object"
};
}
function defaultSemanticFrame(extraction) {
return (extraction.semantic_frame ?? {
scope_kind: "none",
anchor_kind: "none",
anchor_value: null,
date_scope_kind: "none",
date_basis_hint: null,
self_scope_detected: false,
selected_object_scope_detected: false
});
}
function pushWarning(warnings, value) {
if (!warnings.includes(value)) {
warnings.push(value);
}
}
function applyDateScopeHint(frame, dateScopeKind) {
if (dateScopeKind === "explicit") {
frame.date_scope_kind = "explicit";
return;
}
if (dateScopeKind === "implicit_current" && frame.date_scope_kind !== "explicit") {
frame.date_scope_kind = "implicit_current";
frame.date_basis_hint = "implicit_current_snapshot";
}
}
function applyAddressLlmSemanticHintsToExtraction(extraction, semanticHintsInput) {
const semanticHints = normalizeAddressLlmSemanticHints(semanticHintsInput);
if (!semanticHints) {
return extraction;
}
const extractedFilters = { ...(extraction.extracted_filters ?? {}) };
const warnings = [...(Array.isArray(extraction.warnings) ? extraction.warnings : [])];
const semanticFrame = { ...defaultSemanticFrame(extraction) };
const scopeTargetText = semanticHints.scope_target_text;
applyDateScopeHint(semanticFrame, semanticHints.date_scope_kind);
if (semanticHints.self_scope_detected) {
semanticFrame.scope_kind = "implicit_self_scope";
semanticFrame.anchor_kind = "self_scope";
semanticFrame.anchor_value = null;
semanticFrame.self_scope_detected = true;
}
if (semanticHints.selected_object_scope_detected) {
if (semanticFrame.scope_kind === "none") {
semanticFrame.scope_kind = "selected_object_scope";
semanticFrame.anchor_kind = "selected_object";
semanticFrame.anchor_value = null;
}
semanticFrame.selected_object_scope_detected = true;
}
if (semanticHints.scope_target_kind === "organization" && scopeTargetText) {
extractedFilters.organization = scopeTargetText;
pushWarning(warnings, "organization_from_llm_semantics");
if (toNonEmptyString(extractedFilters.warehouse)) {
delete extractedFilters.warehouse;
pushWarning(warnings, "warehouse_cleared_by_llm_organization_semantics");
}
semanticFrame.scope_kind = "explicit_anchor";
semanticFrame.anchor_kind = "organization";
semanticFrame.anchor_value = scopeTargetText;
}
if (semanticHints.scope_target_kind === "warehouse" && scopeTargetText) {
extractedFilters.warehouse = scopeTargetText;
pushWarning(warnings, "warehouse_from_llm_semantics");
semanticFrame.scope_kind = "explicit_anchor";
semanticFrame.anchor_kind = "warehouse";
semanticFrame.anchor_value = scopeTargetText;
}
if (semanticHints.scope_target_kind === "counterparty" && scopeTargetText) {
extractedFilters.counterparty = scopeTargetText;
pushWarning(warnings, "counterparty_from_llm_semantics");
semanticFrame.scope_kind = "explicit_anchor";
semanticFrame.anchor_kind = "counterparty";
semanticFrame.anchor_value = scopeTargetText;
}
if (semanticHints.scope_target_kind === "contract" && scopeTargetText) {
extractedFilters.contract = scopeTargetText;
pushWarning(warnings, "contract_from_llm_semantics");
semanticFrame.scope_kind = "explicit_anchor";
semanticFrame.anchor_kind = "contract";
semanticFrame.anchor_value = scopeTargetText;
}
if (semanticHints.scope_target_kind === "item" && scopeTargetText) {
extractedFilters.item = scopeTargetText;
pushWarning(warnings, "item_from_llm_semantics");
semanticFrame.scope_kind = "explicit_anchor";
semanticFrame.anchor_kind = "item";
semanticFrame.anchor_value = scopeTargetText;
}
return {
...extraction,
extracted_filters: extractedFilters,
warnings,
semantic_frame: semanticFrame
};
}

View File

@ -93,11 +93,13 @@ async function runAssistantAddressAttemptRuntime(input) {
defaultApiKey: input.defaultApiKey defaultApiKey: input.defaultApiKey
})); }));
}; };
const runAddressLaneAttempt = async (messageUsed, carryMeta, analysisDateHint) => runAddressLaneAttemptRuntimeSafe((0, assistantAddressLaneAttemptInputBuilder_1.buildAssistantAddressLaneAttemptRuntimeInput)({ const runAddressLaneAttempt = async (messageUsed, carryMeta, analysisDateHint, llmSemanticHints = null) => runAddressLaneAttemptRuntimeSafe((0, assistantAddressLaneAttemptInputBuilder_1.buildAssistantAddressLaneAttemptRuntimeInput)({
messageUsed, messageUsed,
carryMeta, carryMeta,
analysisDateHint, analysisDateHint,
llmSemanticHints,
activeOrganization: input.sessionScope.activeOrganization, activeOrganization: input.sessionScope.activeOrganization,
knownOrganizations: input.sessionScope.knownOrganizations,
mergeFollowupContextWithOrganizationScope: input.mergeFollowupContextWithOrganizationScope, mergeFollowupContextWithOrganizationScope: input.mergeFollowupContextWithOrganizationScope,
runAddressQueryTryHandle: input.runAddressQueryTryHandle runAddressQueryTryHandle: input.runAddressQueryTryHandle
})); }));

View File

@ -6,7 +6,9 @@ function buildAssistantAddressLaneAttemptRuntimeInput(input) {
messageUsed: input.messageUsed, messageUsed: input.messageUsed,
carryMeta: input.carryMeta, carryMeta: input.carryMeta,
analysisDateHint: input.analysisDateHint, analysisDateHint: input.analysisDateHint,
llmSemanticHints: input.llmSemanticHints,
activeOrganization: input.activeOrganization, activeOrganization: input.activeOrganization,
knownOrganizations: input.knownOrganizations,
mergeFollowupContextWithOrganizationScope: input.mergeFollowupContextWithOrganizationScope, mergeFollowupContextWithOrganizationScope: input.mergeFollowupContextWithOrganizationScope,
runAddressQueryTryHandle: input.runAddressQueryTryHandle runAddressQueryTryHandle: input.runAddressQueryTryHandle
}; };

View File

@ -8,13 +8,20 @@ function resolveAssistantAddressLaneAttemptFollowupContext(carryMeta) {
: null; : null;
} }
function buildAssistantAddressLaneAttemptQueryOptions(input) { function buildAssistantAddressLaneAttemptQueryOptions(input) {
const base = {
analysisDateHint: input.analysisDateHint
};
if (input.scopedFollowupContext) { if (input.scopedFollowupContext) {
return { base.followupContext = input.scopedFollowupContext;
followupContext: input.scopedFollowupContext,
analysisDateHint: input.analysisDateHint
};
} }
return { if (input.llmSemanticHints) {
analysisDateHint: input.analysisDateHint base.llmSemanticHints = input.llmSemanticHints;
}; }
if (input.activeOrganization) {
base.activeOrganization = input.activeOrganization;
}
if (input.knownOrganizations.length > 0) {
base.knownOrganizations = input.knownOrganizations;
}
return base;
} }

View File

@ -7,6 +7,9 @@ async function runAssistantAddressLaneAttemptRuntime(input) {
const scopedFollowupContext = input.mergeFollowupContextWithOrganizationScope(followupContext, input.activeOrganization); const scopedFollowupContext = input.mergeFollowupContextWithOrganizationScope(followupContext, input.activeOrganization);
return input.runAddressQueryTryHandle(input.messageUsed, (0, assistantAddressLaneAttemptQueryOptionsBuilder_1.buildAssistantAddressLaneAttemptQueryOptions)({ return input.runAddressQueryTryHandle(input.messageUsed, (0, assistantAddressLaneAttemptQueryOptionsBuilder_1.buildAssistantAddressLaneAttemptQueryOptions)({
analysisDateHint: input.analysisDateHint, analysisDateHint: input.analysisDateHint,
scopedFollowupContext scopedFollowupContext,
llmSemanticHints: input.llmSemanticHints ?? null,
activeOrganization: input.activeOrganization,
knownOrganizations: input.knownOrganizations
})); }));
} }

View File

@ -157,12 +157,30 @@ function runAssistantAddressLaneResponseRuntime(input) {
: null; : null;
const debugActiveOrganization = input.toNonEmptyString(debugFilters?.organization) ?? const debugActiveOrganization = input.toNonEmptyString(debugFilters?.organization) ??
input.toNonEmptyString(input.activeOrganization); input.toNonEmptyString(input.activeOrganization);
const followupContextSource = input.carryoverMeta?.followupContext && typeof input.carryoverMeta.followupContext === "object"
? input.carryoverMeta.followupContext
: null;
if (debugKnownOrganizations.length > 0) { if (debugKnownOrganizations.length > 0) {
debug.assistant_known_organizations = debugKnownOrganizations; debug.assistant_known_organizations = debugKnownOrganizations;
} }
if (debugActiveOrganization) { if (debugActiveOrganization) {
debug.assistant_active_organization = debugActiveOrganization; debug.assistant_active_organization = debugActiveOrganization;
} }
const rootIntent = input.toNonEmptyString(followupContextSource?.root_intent);
const currentFrameKind = input.toNonEmptyString(followupContextSource?.current_frame_kind);
const rootFilters = followupContextSource?.root_filters && typeof followupContextSource.root_filters === "object"
? followupContextSource.root_filters
: null;
if (rootIntent || currentFrameKind) {
debug.address_root_frame_context = {
root_intent: rootIntent,
current_frame_kind: currentFrameKind,
organization: input.toNonEmptyString(rootFilters?.organization),
as_of_date: input.toNonEmptyString(rootFilters?.as_of_date),
period_from: input.toNonEmptyString(rootFilters?.period_from),
period_to: input.toNonEmptyString(rootFilters?.period_to)
};
}
const finalization = finalizeAddressTurnSafe({ const finalization = finalizeAddressTurnSafe({
sessionId: input.sessionId, sessionId: input.sessionId,
userMessage: input.userMessage, userMessage: input.userMessage,

View File

@ -40,7 +40,7 @@ async function runAssistantAddressLaneRuntime(input) {
return { action: "continue" }; return { action: "continue" };
}; };
if (input.shouldPreferContextualLane) { if (input.shouldPreferContextualLane) {
const contextualAddressLane = await input.runAddressLaneAttempt(input.addressInputMessage, input.carryover); const contextualAddressLane = await input.runAddressLaneAttempt(input.addressInputMessage, input.carryover, input.llmSemanticHints ?? null);
const decision = evaluateAddressLane(contextualAddressLane, input.addressInputMessage, input.carryover); const decision = evaluateAddressLane(contextualAddressLane, input.addressInputMessage, input.carryover);
if (decision.action === "return") { if (decision.action === "return") {
return { return {
@ -50,7 +50,7 @@ async function runAssistantAddressLaneRuntime(input) {
}; };
} }
} }
const primaryAddressLane = await input.runAddressLaneAttempt(input.addressInputMessage, null); const primaryAddressLane = await input.runAddressLaneAttempt(input.addressInputMessage, null, input.llmSemanticHints ?? null);
const primaryDecision = evaluateAddressLane(primaryAddressLane, input.addressInputMessage, null); const primaryDecision = evaluateAddressLane(primaryAddressLane, input.addressInputMessage, null);
if (primaryDecision.action === "return") { if (primaryDecision.action === "return") {
return { return {
@ -60,7 +60,7 @@ async function runAssistantAddressLaneRuntime(input) {
}; };
} }
if (!input.shouldPreferContextualLane && input.carryover?.followupContext) { if (!input.shouldPreferContextualLane && input.carryover?.followupContext) {
const contextualAddressLane = await input.runAddressLaneAttempt(input.addressInputMessage, input.carryover); const contextualAddressLane = await input.runAddressLaneAttempt(input.addressInputMessage, input.carryover, input.llmSemanticHints ?? null);
const contextualDecision = evaluateAddressLane(contextualAddressLane, input.addressInputMessage, input.carryover); const contextualDecision = evaluateAddressLane(contextualAddressLane, input.addressInputMessage, input.carryover);
if (contextualDecision.action === "return") { if (contextualDecision.action === "return") {
return { return {
@ -78,7 +78,7 @@ async function runAssistantAddressLaneRuntime(input) {
retryAudit.retry_message = input.userMessage; retryAudit.retry_message = input.userMessage;
if (input.carryover?.followupContext) { if (input.carryover?.followupContext) {
retryAudit.retry_used_followup_context = true; retryAudit.retry_used_followup_context = true;
const rawContextualLane = await input.runAddressLaneAttempt(input.userMessage, input.carryover); const rawContextualLane = await input.runAddressLaneAttempt(input.userMessage, input.carryover, input.llmSemanticHints ?? null);
const rawContextualDecision = evaluateAddressLane(rawContextualLane, input.userMessage, input.carryover); const rawContextualDecision = evaluateAddressLane(rawContextualLane, input.userMessage, input.carryover);
if (rawContextualDecision.action === "return") { if (rawContextualDecision.action === "return") {
retryAudit.retry_result_category = limitedCategory(rawContextualDecision.selection.addressLane); retryAudit.retry_result_category = limitedCategory(rawContextualDecision.selection.addressLane);
@ -89,7 +89,7 @@ async function runAssistantAddressLaneRuntime(input) {
}; };
} }
} }
const rawPrimaryLane = await input.runAddressLaneAttempt(input.userMessage, null); const rawPrimaryLane = await input.runAddressLaneAttempt(input.userMessage, null, input.llmSemanticHints ?? null);
retryAudit.retry_result_category = limitedCategory(rawPrimaryLane); retryAudit.retry_result_category = limitedCategory(rawPrimaryLane);
const rawPrimaryDecision = evaluateAddressLane(rawPrimaryLane, input.userMessage, null); const rawPrimaryDecision = evaluateAddressLane(rawPrimaryLane, input.userMessage, null);
if (rawPrimaryDecision.action === "return") { if (rawPrimaryDecision.action === "return") {

View File

@ -1,6 +1,41 @@
"use strict"; "use strict";
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.buildAssistantAddressOrchestrationRuntime = buildAssistantAddressOrchestrationRuntime; exports.buildAssistantAddressOrchestrationRuntime = buildAssistantAddressOrchestrationRuntime;
function hasSelectedObjectInventorySignal(text) {
return /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(String(text ?? ""));
}
function hasSelectedObjectInventoryActionCue(text) {
return /(?:кому[\s\S]{0,80}продал[аи]?|кому[\s\S]{0,80}реализова[нлт][а-я]*|кому\s+был\s+продан|кто[\s\S]{0,40}купил|кто\s+это\s+поставил|кто\s+поставил|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+мы\s+купили|где\s+куплено|по\s+каким\s+документам|какими\s+документами|покажи\s+документы|документы\s+закупки|buyer|sale\s+trace|supplier|vendor|purchase\s+documents|purchase[\s-]?to[\s-]?sale|old\s+purchase|aged\s+stock)/iu.test(String(text ?? ""));
}
function isGenericCanonicalDriftIntent(intent) {
return (intent === "open_items_by_counterparty_or_contract" ||
intent === "list_documents_by_counterparty" ||
intent === "list_documents_by_contract" ||
intent === "bank_operations_by_counterparty" ||
intent === "bank_operations_by_contract" ||
intent === "documents_forming_balance");
}
function shouldPreferRawFollowupMessage(userMessage, addressInputMessage, carryover, addressPreDecompose, toNonEmptyString) {
if (!carryover?.followupContext || typeof carryover.followupContext !== "object") {
return false;
}
const rawMessage = toNonEmptyString(userMessage);
const canonicalMessage = toNonEmptyString(addressInputMessage);
if (!rawMessage || !canonicalMessage || rawMessage === canonicalMessage) {
return false;
}
const predecomposeContract = addressPreDecompose?.predecomposeContract && typeof addressPreDecompose.predecomposeContract === "object"
? addressPreDecompose.predecomposeContract
: null;
const mode = toNonEmptyString(predecomposeContract?.mode) ?? "unknown";
const intent = toNonEmptyString(predecomposeContract?.intent) ?? "unknown";
if (mode === "unsupported" && intent === "unknown") {
return true;
}
return (hasSelectedObjectInventorySignal(rawMessage) &&
hasSelectedObjectInventoryActionCue(rawMessage) &&
isGenericCanonicalDriftIntent(intent));
}
function fallbackAddressPreDecompose(userMessage, llmProvider, buildAddressLlmPredecomposeContractV1, sanitizeAddressMessageForFallback) { function fallbackAddressPreDecompose(userMessage, llmProvider, buildAddressLlmPredecomposeContractV1, sanitizeAddressMessageForFallback) {
const provider = llmProvider === "local" ? "local" : llmProvider === "openai" ? "openai" : null; const provider = llmProvider === "local" ? "local" : llmProvider === "openai" ? "openai" : null;
return { return {
@ -22,11 +57,26 @@ function fallbackAddressPreDecompose(userMessage, llmProvider, buildAddressLlmPr
}; };
} }
async function buildAssistantAddressOrchestrationRuntime(input) { async function buildAssistantAddressOrchestrationRuntime(input) {
const addressPreDecompose = input.featureAddressLlmPredecomposeV1 const initialAddressPreDecompose = input.featureAddressLlmPredecomposeV1
? await input.runAddressLlmPreDecompose() ? await input.runAddressLlmPreDecompose()
: fallbackAddressPreDecompose(input.userMessage, input.llmProvider, input.buildAddressLlmPredecomposeContractV1, input.sanitizeAddressMessageForFallback); : fallbackAddressPreDecompose(input.userMessage, input.llmProvider, input.buildAddressLlmPredecomposeContractV1, input.sanitizeAddressMessageForFallback);
const addressInputMessage = input.toNonEmptyString(addressPreDecompose?.effectiveMessage) ?? input.userMessage; let addressPreDecompose = initialAddressPreDecompose;
const carryover = input.resolveAddressFollowupCarryoverContext(input.userMessage, input.sessionItems, addressInputMessage, addressPreDecompose); let addressInputMessage = input.toNonEmptyString(addressPreDecompose?.effectiveMessage) ?? input.userMessage;
let carryover = input.resolveAddressFollowupCarryoverContext(input.userMessage, input.sessionItems, addressInputMessage, addressPreDecompose);
if (shouldPreferRawFollowupMessage(input.userMessage, addressInputMessage, carryover, addressPreDecompose, input.toNonEmptyString)) {
addressInputMessage = input.userMessage;
addressPreDecompose = {
...addressPreDecompose,
applied: false,
effectiveMessage: input.userMessage,
reason: "followup_raw_message_preferred_over_llm_rewrite",
predecomposeContract: input.buildAddressLlmPredecomposeContractV1({
sourceMessage: input.userMessage,
canonicalMessage: input.userMessage
})
};
carryover = input.resolveAddressFollowupCarryoverContext(input.userMessage, input.sessionItems, addressInputMessage, addressPreDecompose);
}
const followupContext = carryover?.followupContext ?? null; const followupContext = carryover?.followupContext ?? null;
const orchestrationDecision = input.resolveAssistantOrchestrationDecision({ const orchestrationDecision = input.resolveAssistantOrchestrationDecision({
rawUserMessage: input.userMessage, rawUserMessage: input.userMessage,

View File

@ -62,9 +62,12 @@ async function runAssistantAddressRuntime(input) {
userMessage: input.userMessage, userMessage: input.userMessage,
addressInputMessage, addressInputMessage,
carryover, carryover,
llmSemanticHints: addressRuntimeMeta && typeof addressRuntimeMeta === "object"
? addressRuntimeMeta.semanticHints ?? null
: null,
shouldPreferContextualLane, shouldPreferContextualLane,
canRetryWithRawUserMessage, canRetryWithRawUserMessage,
runAddressLaneAttempt: (messageUsed, carryMeta) => input.runAddressLaneAttempt(messageUsed, carryMeta, analysisDateHint), runAddressLaneAttempt: (messageUsed, carryMeta, llmSemanticHints = null) => input.runAddressLaneAttempt(messageUsed, carryMeta, analysisDateHint, llmSemanticHints),
isRetryableAddressLimitedResult: input.isRetryableAddressLimitedResult isRetryableAddressLimitedResult: input.isRetryableAddressLimitedResult
}); });
if (addressLaneRuntime.handled && addressLaneRuntime.selection) { if (addressLaneRuntime.handled && addressLaneRuntime.selection) {

View File

@ -0,0 +1,209 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.normalizeOrganizationScopeValue = normalizeOrganizationScopeValue;
exports.normalizeOrganizationScopeSearchText = normalizeOrganizationScopeSearchText;
exports.scoreOrganizationMentionInMessage = scoreOrganizationMentionInMessage;
exports.mergeKnownOrganizations = mergeKnownOrganizations;
exports.resolveOrganizationSelectionFromMessage = resolveOrganizationSelectionFromMessage;
const ORGANIZATION_SCOPE_STOPWORDS = new Set([
"ооо",
"зао",
"оао",
"пао",
"ао",
"ип",
"llc",
"inc",
"ltd",
"corp",
"group",
"company",
"co",
"the",
"and",
"org",
"organization",
"компания",
"организация",
"контора",
"фирма",
"база",
"по",
"в",
"во",
"на",
"для",
"из",
"у",
"к",
"от",
"это",
"эта",
"этой",
"этот",
"сегодня",
"сейчас",
"текущая",
"текущей",
"наш",
"наша",
"нашей",
"нашу",
"наши"
]);
function normalizeScopeLabel(value) {
return String(value ?? "")
.replace(/[“”«»]/g, '"')
.replace(/\s+/g, " ")
.trim();
}
function normalizeScopeKey(value) {
return normalizeScopeLabel(value).toLowerCase().replace(/ё/g, "е");
}
function normalizeOrganizationScopeValue(value) {
const normalized = normalizeScopeLabel(value);
if (!normalized) {
return null;
}
let unwrapped = normalized.replace(/^\\+|\\+$/g, "").trim();
if ((unwrapped.startsWith('"') && unwrapped.endsWith('"')) ||
(unwrapped.startsWith("'") && unwrapped.endsWith("'"))) {
unwrapped = unwrapped.slice(1, -1).trim();
}
return unwrapped.length > 0 ? unwrapped : null;
}
function normalizeOrganizationScopeSearchText(value) {
return normalizeScopeKey(value)
.replace(/[^\p{L}\p{N}]+/gu, " ")
.replace(/\s+/g, " ")
.trim();
}
function tokenizeOrganizationScope(value) {
const normalized = normalizeOrganizationScopeSearchText(value);
if (!normalized) {
return [];
}
return normalized
.split(" ")
.map((token) => token.trim())
.filter((token) => token.length >= 3 && !ORGANIZATION_SCOPE_STOPWORDS.has(token));
}
function organizationTokenVariants(token) {
const source = String(token ?? "").trim().toLowerCase();
if (!source) {
return [];
}
const variants = new Set([source]);
const withoutLongEnding = source.replace(/(?:ами|ями|ого|ему|ому|ыми|ими|иях|ях|ах|ей|ой|ом|ем|ам|ям|ую|юю|ая|яя|ое|ее|ые|ие|ов|ев|ий|ый|ой)$/iu, "");
if (withoutLongEnding.length >= 4) {
variants.add(withoutLongEnding);
}
const withoutShortEnding = source.replace(/[аеёиоуыэюя]$/iu, "");
if (withoutShortEnding.length >= 4) {
variants.add(withoutShortEnding);
}
return Array.from(variants);
}
function scoreOrganizationMentionInMessage(message, organization) {
const messageNorm = normalizeOrganizationScopeSearchText(message);
const organizationNorm = normalizeOrganizationScopeSearchText(organization);
if (!messageNorm || !organizationNorm) {
return 0;
}
if (messageNorm.includes(organizationNorm)) {
return 10_000 + organizationNorm.length;
}
const organizationTokens = tokenizeOrganizationScope(organizationNorm);
const messageTokens = tokenizeOrganizationScope(messageNorm);
if (organizationTokens.length === 0 || messageTokens.length === 0) {
return 0;
}
let matchedTokens = 0;
let score = 0;
for (const token of organizationTokens) {
const variants = organizationTokenVariants(token);
let matched = false;
let variantScore = 0;
for (const variant of variants) {
if (!variant) {
continue;
}
if (messageNorm.includes(variant)) {
matched = true;
variantScore = Math.max(variantScore, variant.length * 5);
continue;
}
const fuzzyMatched = messageTokens.some((messageToken) => {
if (messageToken === variant) {
return true;
}
if (messageToken.length >= 5 && variant.length >= 5) {
return messageToken.startsWith(variant) || variant.startsWith(messageToken);
}
return false;
});
if (fuzzyMatched) {
matched = true;
variantScore = Math.max(variantScore, Math.max(20, variant.length * 3));
}
}
if (matched) {
matchedTokens += 1;
score += variantScore > 0 ? variantScore : 10;
}
}
if (matchedTokens === 0) {
return 0;
}
if (matchedTokens === organizationTokens.length) {
score += 400;
}
else {
score += matchedTokens * 50;
}
return score;
}
function mergeKnownOrganizations(values, limit = 50) {
const dedup = new Map();
for (const raw of Array.isArray(values) ? values : []) {
const normalized = normalizeOrganizationScopeValue(raw);
if (!normalized) {
continue;
}
const key = normalizeOrganizationScopeSearchText(normalized);
if (!key || dedup.has(key)) {
continue;
}
dedup.set(key, normalized);
}
return Array.from(dedup.values()).slice(0, limit);
}
function resolveOrganizationSelectionFromMessage(userMessage, knownOrganizations) {
const known = mergeKnownOrganizations(Array.isArray(knownOrganizations) ? knownOrganizations : []);
if (!userMessage || known.length === 0) {
return null;
}
const messageNorm = normalizeOrganizationScopeSearchText(userMessage);
if (!messageNorm) {
return null;
}
const scored = known
.map((organization) => ({
organization,
score: scoreOrganizationMentionInMessage(messageNorm, organization)
}))
.filter((item) => item.score > 0)
.sort((a, b) => b.score - a.score || a.organization.length - b.organization.length);
if (scored.length === 0) {
return null;
}
const best = scored[0];
const second = scored[1];
if (best.score < 90) {
return null;
}
if (second && second.score === best.score) {
return null;
}
return best.organization;
}

View File

@ -2496,6 +2496,62 @@ function findRecentAddressFilterValue(items, key) {
} }
return null; return null;
} }
function isInventoryRootFrameIntent(intent) {
return intent === "inventory_on_hand_as_of_date";
}
function isInventoryDrilldownFrameIntent(intent) {
return intent === "inventory_purchase_provenance_for_item" ||
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date";
}
function extractAddressCarryoverAnchor(addressDebug) {
if (!isAddressLaneDebugPayload(addressDebug)) {
return {
anchorType: null,
anchorValue: null
};
}
return {
anchorType: toNonEmptyString(addressDebug.anchor_type),
anchorValue: toNonEmptyString(addressDebug.anchor_value_resolved) ??
toNonEmptyString(addressDebug.anchor_value_raw) ??
readAddressInventoryItemFilter(addressDebug) ??
readAddressFilterString(addressDebug, "counterparty") ??
readAddressFilterString(addressDebug, "contract") ??
readAddressFilterString(addressDebug, "account")
};
}
function findRecentInventoryRootFrame(items) {
for (let index = items.length - 1; index >= 0; index -= 1) {
const item = items[index];
if (!item || item.role !== "assistant" || !item.debug) {
continue;
}
const debug = item.debug;
if (!isAddressLaneDebugPayload(debug)) {
continue;
}
const detectedIntent = toNonEmptyString(debug.detected_intent);
if (!isInventoryRootFrameIntent(detectedIntent)) {
continue;
}
const anchor = extractAddressCarryoverAnchor(debug);
const filtersRaw = debug.extracted_filters;
const filters = filtersRaw && typeof filtersRaw === "object"
? { ...filtersRaw }
: {};
return {
intent: detectedIntent,
filters,
anchorType: anchor.anchorType,
anchorValue: anchor.anchorValue,
messageId: toNonEmptyString(item.message_id)
};
}
return null;
}
const ADDRESS_FOLLOWUP_OFFER_BY_INTENT = { const ADDRESS_FOLLOWUP_OFFER_BY_INTENT = {
list_documents_by_counterparty: ["bank_operations_by_counterparty", "list_contracts_by_counterparty"], list_documents_by_counterparty: ["bank_operations_by_counterparty", "list_contracts_by_counterparty"],
bank_operations_by_counterparty: ["list_documents_by_counterparty", "list_contracts_by_counterparty"], bank_operations_by_counterparty: ["list_documents_by_counterparty", "list_contracts_by_counterparty"],
@ -2798,6 +2854,14 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
readAddressFilterString(previousAddressDebug, "counterparty") ?? readAddressFilterString(previousAddressDebug, "counterparty") ??
readAddressFilterString(previousAddressDebug, "account") ?? readAddressFilterString(previousAddressDebug, "account") ??
readAddressFilterString(previousAddressDebug, "contract"); readAddressFilterString(previousAddressDebug, "contract");
const inventoryRootFrame = findRecentInventoryRootFrame(items);
const currentFrameKind = inventoryRootFrame
? isInventoryDrilldownFrameIntent(sourceIntent)
? "inventory_drilldown"
: isInventoryRootFrameIntent(sourceIntent)
? "inventory_root"
: "generic"
: null;
let resolvedCounterpartyFromDisplay = false; let resolvedCounterpartyFromDisplay = false;
const previousFiltersRaw = previousAddressDebug.extracted_filters; const previousFiltersRaw = previousAddressDebug.extracted_filters;
const previousFilters = previousFiltersRaw && typeof previousFiltersRaw === "object" const previousFilters = previousFiltersRaw && typeof previousFiltersRaw === "object"
@ -2857,7 +2921,12 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
previous_filters: previousFilters, previous_filters: previousFilters,
previous_anchor_type: previousAnchorType ?? undefined, previous_anchor_type: previousAnchorType ?? undefined,
previous_anchor_value: previousAnchor, previous_anchor_value: previousAnchor,
resolved_counterparty_from_display: resolvedCounterpartyFromDisplay || undefined resolved_counterparty_from_display: resolvedCounterpartyFromDisplay || undefined,
root_intent: inventoryRootFrame?.intent ?? undefined,
root_filters: inventoryRootFrame?.filters ?? undefined,
root_anchor_type: inventoryRootFrame?.anchorType ?? undefined,
root_anchor_value: inventoryRootFrame?.anchorValue ?? undefined,
current_frame_kind: currentFrameKind ?? undefined
}, },
previousAddressIntent: previousIntent, previousAddressIntent: previousIntent,
previousAddressAnchor: previousAnchor, previousAddressAnchor: previousAnchor,
@ -2933,19 +3002,32 @@ function isAddressLlmPreDecomposeCandidate(userMessage) {
} }
return /(?:\bдок\b|доки|документ|контрагент|договор|остаток|сч(?:е|ё)т|сальдо|банк|выписк|платеж|оплат|поступлен|поступлени|списан|реализац|сверк|взаиморасч|кто\s+должен|show|list|documents?|counterparty|contract|account|balance|bank\s+operations?|doki|dokument(?:y|ov|am|a)?|platezh|oplata|schet|saldo)/i.test(text); return /(?:\bдок\b|доки|документ|контрагент|договор|остаток|сч(?:е|ё)т|сальдо|банк|выписк|платеж|оплат|поступлен|поступлени|списан|реализац|сверк|взаиморасч|кто\s+должен|show|list|documents?|counterparty|contract|account|balance|bank\s+operations?|doki|dokument(?:y|ov|am|a)?|platezh|oplata|schet|saldo)/i.test(text);
} }
function extractAddressQuestionFromNormalized(normalized) { function normalizeAddressSemanticHintsFromFragment(fragment) {
if (!normalized || typeof normalized !== "object") { if (!fragment || typeof fragment !== "object") {
return null; return null;
} }
const source = normalized; const hints = fragment.semantic_hints;
const fragments = Array.isArray(source.fragments) ? source.fragments : []; if (!hints || typeof hints !== "object") {
for (const item of fragments) { return null;
}
const scopeTargetKind = toNonEmptyString(hints.scope_target_kind);
const dateScopeKind = toNonEmptyString(hints.date_scope_kind);
return {
scope_target_kind: scopeTargetKind ?? "none",
scope_target_text: toNonEmptyString(hints.scope_target_text),
date_scope_kind: dateScopeKind ?? "missing",
self_scope_detected: hints.self_scope_detected === true || scopeTargetKind === "self_scope",
selected_object_scope_detected: hints.selected_object_scope_detected === true || scopeTargetKind === "selected_object"
};
}
function extractAddressPredecomposeCandidateFromFragments(fragments) {
for (const item of Array.isArray(fragments) ? fragments : []) {
if (!item || typeof item !== "object") { if (!item || typeof item !== "object") {
continue; continue;
} }
const fragment = item; const fragment = item;
const domainRelevance = String(fragment.domain_relevance ?? "").trim().toLowerCase(); const domainRelevance = String(fragment.domain_relevance ?? "").trim().toLowerCase();
if (domainRelevance === "out_of_scope") { if (domainRelevance === "out_of_scope" || domainRelevance === "offtopic") {
continue; continue;
} }
const normalizedText = toNonEmptyString(fragment.normalized_fragment_text); const normalizedText = toNonEmptyString(fragment.normalized_fragment_text);
@ -2955,11 +3037,20 @@ function extractAddressQuestionFromNormalized(normalized) {
continue; continue;
} }
if (candidate.length >= 3 && candidate.length <= 500) { if (candidate.length >= 3 && candidate.length <= 500) {
return candidate; return {
candidate,
semanticHints: normalizeAddressSemanticHintsFromFragment(fragment)
};
} }
} }
return null; return null;
} }
function extractAddressPredecomposeCandidateFromNormalized(normalized) {
if (!normalized || typeof normalized !== "object") {
return null;
}
return extractAddressPredecomposeCandidateFromFragments(normalized.fragments);
}
function stripMarkdownJsonFence(text) { function stripMarkdownJsonFence(text) {
return String(text ?? "") return String(text ?? "")
.trim() .trim()
@ -3037,7 +3128,7 @@ function extractOutputTextFromRawNormalizerOutput(raw) {
} }
return null; return null;
} }
function extractAddressQuestionFromRawNormalizerOutput(rawModelOutput) { function extractAddressPredecomposeCandidateFromRawNormalizerOutput(rawModelOutput) {
const outputText = extractOutputTextFromRawNormalizerOutput(rawModelOutput); const outputText = extractOutputTextFromRawNormalizerOutput(rawModelOutput);
if (!outputText) { if (!outputText) {
return null; return null;
@ -3046,31 +3137,7 @@ function extractAddressQuestionFromRawNormalizerOutput(rawModelOutput) {
if (!parsed || typeof parsed !== "object") { if (!parsed || typeof parsed !== "object") {
return null; return null;
} }
const source = parsed; return extractAddressPredecomposeCandidateFromFragments(parsed.fragments);
const fragments = Array.isArray(source.fragments) ? source.fragments : [];
for (const item of fragments) {
if (!item || typeof item !== "object") {
continue;
}
const fragment = item;
const domainRelevance = fragment.domain_relevance;
if (typeof domainRelevance === "string" && domainRelevance.trim().toLowerCase() === "out_of_scope") {
continue;
}
if (domainRelevance === false) {
continue;
}
const normalizedText = toNonEmptyString(fragment.normalized_fragment_text);
const rawText = toNonEmptyString(fragment.raw_fragment_text);
const candidate = selectPreferredAddressFragmentCandidate(rawText ?? "", normalizedText ?? "");
if (!candidate) {
continue;
}
if (candidate.length >= 3 && candidate.length <= 500) {
return candidate;
}
}
return null;
} }
const ADDRESS_PREDECOMPOSE_LOW_QUALITY_COUNTERPARTY_TOKENS = new Set([ const ADDRESS_PREDECOMPOSE_LOW_QUALITY_COUNTERPARTY_TOKENS = new Set([
"есть", "есть",
@ -3310,7 +3377,8 @@ function attachAddressPredecomposeContract(meta, sourceMessage) {
const canonicalMessage = toNonEmptyString(meta?.effectiveMessage) ?? String(sourceMessage ?? ""); const canonicalMessage = toNonEmptyString(meta?.effectiveMessage) ?? String(sourceMessage ?? "");
const predecomposeContract = (0, predecomposeContract_1.buildAddressLlmPredecomposeContractV1)({ const predecomposeContract = (0, predecomposeContract_1.buildAddressLlmPredecomposeContractV1)({
sourceMessage: String(sourceMessage ?? ""), sourceMessage: String(sourceMessage ?? ""),
canonicalMessage canonicalMessage,
semanticHints: meta?.semanticHints ?? null
}); });
const semanticExtractionContract = (0, predecomposeContract_1.buildAddressSemanticExtractionContractV1)({ const semanticExtractionContract = (0, predecomposeContract_1.buildAddressSemanticExtractionContractV1)({
sourceMessage: String(sourceMessage ?? ""), sourceMessage: String(sourceMessage ?? ""),
@ -3375,9 +3443,10 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
}; };
try { try {
const normalized = await normalizerService.normalize(normalizePayload); const normalized = await normalizerService.normalize(normalizePayload);
const candidateFromNormalized = extractAddressQuestionFromNormalized(normalized?.normalized); const candidateFromNormalized = extractAddressPredecomposeCandidateFromNormalized(normalized?.normalized);
const candidateFromRaw = candidateFromNormalized ? null : extractAddressQuestionFromRawNormalizerOutput(normalized?.raw_model_output); const candidateFromRaw = candidateFromNormalized ? null : extractAddressPredecomposeCandidateFromRawNormalizerOutput(normalized?.raw_model_output);
const candidate = candidateFromNormalized ?? candidateFromRaw; const candidateMeta = candidateFromNormalized ?? candidateFromRaw;
const candidate = candidateMeta?.candidate ?? null;
if (!candidate) { if (!candidate) {
if (fallbackCandidate) { if (fallbackCandidate) {
const fallbackCompact = compactWhitespace(String(fallbackCandidate.candidate ?? "").toLowerCase()); const fallbackCompact = compactWhitespace(String(fallbackCandidate.candidate ?? "").toLowerCase());
@ -3391,7 +3460,8 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
traceId: normalized?.trace_id ?? null, traceId: normalized?.trace_id ?? null,
effectiveMessage: fallbackCandidate.candidate, effectiveMessage: fallbackCandidate.candidate,
reason: "fallback_rule_applied_after_llm", reason: "fallback_rule_applied_after_llm",
fallbackRuleHit: fallbackCandidate.rule fallbackRuleHit: fallbackCandidate.rule,
semanticHints: null
}, userMessage); }, userMessage);
} }
} }
@ -3399,7 +3469,8 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
...baseMeta, ...baseMeta,
attempted: true, attempted: true,
traceId: normalized?.trace_id ?? null, traceId: normalized?.trace_id ?? null,
reason: normalized?.ok ? "no_usable_fragment" : "normalize_failed" reason: normalized?.ok ? "no_usable_fragment" : "normalize_failed",
semanticHints: null
}, userMessage); }, userMessage);
} }
const repairedSourceMessage = repairAddressMojibake(userMessage); const repairedSourceMessage = repairAddressMojibake(userMessage);
@ -3418,7 +3489,8 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
effectiveMessage: userMessage, effectiveMessage: userMessage,
reason: "normalized_fragment_rejected_diagnostic_rewrite", reason: "normalized_fragment_rejected_diagnostic_rewrite",
fallbackRuleHit: null, fallbackRuleHit: null,
sanitizedUserMessage sanitizedUserMessage,
semanticHints: candidateMeta?.semanticHints ?? null
}, userMessage); }, userMessage);
} }
const intentConflict = sourceIntentKnown && const intentConflict = sourceIntentKnown &&
@ -3440,7 +3512,8 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
? "normalized_fragment_rejected_intent_drop" ? "normalized_fragment_rejected_intent_drop"
: "normalized_fragment_rejected_intent_conflict", : "normalized_fragment_rejected_intent_conflict",
fallbackRuleHit: null, fallbackRuleHit: null,
sanitizedUserMessage sanitizedUserMessage,
semanticHints: candidateMeta?.semanticHints ?? null
}, userMessage); }, userMessage);
} }
const sourceHasExplicitDrilldownSignal = hasPredecomposeExplicitDrilldownSignal(repairedSourceMessage || userMessage); const sourceHasExplicitDrilldownSignal = hasPredecomposeExplicitDrilldownSignal(repairedSourceMessage || userMessage);
@ -3461,7 +3534,8 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
effectiveMessage: userMessage, effectiveMessage: userMessage,
reason: "normalized_fragment_rejected_followup_intent_injection", reason: "normalized_fragment_rejected_followup_intent_injection",
fallbackRuleHit: null, fallbackRuleHit: null,
sanitizedUserMessage sanitizedUserMessage,
semanticHints: candidateMeta?.semanticHints ?? null
}, userMessage); }, userMessage);
} }
const sourceHasSelectedObjectInventoryFollowup = hasSelectedObjectInventoryFollowupSignalForPredecompose(repairedSourceMessage || userMessage); const sourceHasSelectedObjectInventoryFollowup = hasSelectedObjectInventoryFollowupSignalForPredecompose(repairedSourceMessage || userMessage);
@ -3481,7 +3555,8 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
effectiveMessage: userMessage, effectiveMessage: userMessage,
reason: "normalized_fragment_rejected_selected_object_context_loss", reason: "normalized_fragment_rejected_selected_object_context_loss",
fallbackRuleHit: null, fallbackRuleHit: null,
sanitizedUserMessage sanitizedUserMessage,
semanticHints: candidateMeta?.semanticHints ?? null
}, userMessage); }, userMessage);
} }
const sourceAnchorQuality = evaluateAddressAnchorQuality(repairedSourceMessage || userMessage); const sourceAnchorQuality = evaluateAddressAnchorQuality(repairedSourceMessage || userMessage);
@ -3507,7 +3582,8 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
effectiveMessage: userMessage, effectiveMessage: userMessage,
reason: "normalized_fragment_rejected_anchor_substitution", reason: "normalized_fragment_rejected_anchor_substitution",
fallbackRuleHit: null, fallbackRuleHit: null,
sanitizedUserMessage sanitizedUserMessage,
semanticHints: candidateMeta?.semanticHints ?? null
}, userMessage); }, userMessage);
} }
const anchorDegradedByCandidate = sameIntentForAnchorSafety && const anchorDegradedByCandidate = sameIntentForAnchorSafety &&
@ -3524,7 +3600,8 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
effectiveMessage: userMessage, effectiveMessage: userMessage,
reason: "normalized_fragment_rejected_anchor_degradation", reason: "normalized_fragment_rejected_anchor_degradation",
fallbackRuleHit: null, fallbackRuleHit: null,
sanitizedUserMessage sanitizedUserMessage,
semanticHints: candidateMeta?.semanticHints ?? null
}, userMessage); }, userMessage);
} }
if (fallbackCandidate) { if (fallbackCandidate) {
@ -3543,19 +3620,25 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
effectiveMessage: fallbackCandidate.candidate, effectiveMessage: fallbackCandidate.candidate,
reason: "fallback_rule_preferred_over_llm_candidate_anchor_quality", reason: "fallback_rule_preferred_over_llm_candidate_anchor_quality",
fallbackRuleHit: fallbackCandidate.rule, fallbackRuleHit: fallbackCandidate.rule,
sanitizedUserMessage sanitizedUserMessage,
semanticHints: candidateMeta?.semanticHints ?? null
}, userMessage); }, userMessage);
} }
} }
const semanticContractForCandidate = (0, predecomposeContract_1.buildAddressSemanticExtractionContractV1)({ const semanticContractForCandidate = (0, predecomposeContract_1.buildAddressSemanticExtractionContractV1)({
sourceMessage: String(userMessage ?? ""), sourceMessage: String(userMessage ?? ""),
canonicalMessage: candidate canonicalMessage: candidate,
predecomposeContract: (0, predecomposeContract_1.buildAddressLlmPredecomposeContractV1)({
sourceMessage: String(userMessage ?? ""),
canonicalMessage: candidate,
semanticHints: candidateMeta?.semanticHints ?? null
})
}); });
if (!semanticContractForCandidate.apply_canonical_recommended) { if (!semanticContractForCandidate.apply_canonical_recommended) {
const sourceDataSignalDetected = Boolean(semanticContractForCandidate?.guard_hints?.source_data_signal_detected); const sourceDataSignalDetected = Boolean(semanticContractForCandidate?.guard_hints?.source_data_signal_detected);
const rawFragmentCandidatePreferred = Boolean(sourceDataSignalDetected && const rawFragmentCandidatePreferred = Boolean(sourceDataSignalDetected &&
candidateFromNormalized && candidateFromNormalized &&
candidateFromNormalized === candidate && candidateFromNormalized.candidate === candidate &&
toNonEmptyString(candidate)); toNonEmptyString(candidate));
if (rawFragmentCandidatePreferred) { if (rawFragmentCandidatePreferred) {
return attachAddressPredecomposeContract({ return attachAddressPredecomposeContract({
@ -3567,7 +3650,8 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
effectiveMessage: candidate, effectiveMessage: candidate,
reason: "normalized_fragment_semantic_guard_raw_fragment_preferred", reason: "normalized_fragment_semantic_guard_raw_fragment_preferred",
fallbackRuleHit: null, fallbackRuleHit: null,
sanitizedUserMessage sanitizedUserMessage,
semanticHints: candidateMeta?.semanticHints ?? null
}, userMessage); }, userMessage);
} }
if (fallbackCandidate) { if (fallbackCandidate) {
@ -3588,7 +3672,8 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
effectiveMessage: String(fallbackCandidate.candidate ?? ""), effectiveMessage: String(fallbackCandidate.candidate ?? ""),
reason: "fallback_rule_preferred_over_llm_candidate_semantic_guard", reason: "fallback_rule_preferred_over_llm_candidate_semantic_guard",
fallbackRuleHit: fallbackCandidate.rule, fallbackRuleHit: fallbackCandidate.rule,
sanitizedUserMessage sanitizedUserMessage,
semanticHints: candidateMeta?.semanticHints ?? null
}, userMessage); }, userMessage);
} }
} }
@ -3601,7 +3686,8 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
effectiveMessage: userMessage, effectiveMessage: userMessage,
reason: "normalized_fragment_rejected_semantic_guard", reason: "normalized_fragment_rejected_semantic_guard",
fallbackRuleHit: null, fallbackRuleHit: null,
sanitizedUserMessage sanitizedUserMessage,
semanticHints: candidateMeta?.semanticHints ?? null
}, userMessage); }, userMessage);
} }
const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase()); const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase());
@ -3628,7 +3714,8 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
reason, reason,
llmCanonicalCandidateDetected: true, llmCanonicalCandidateDetected: true,
fallbackRuleHit: null, fallbackRuleHit: null,
sanitizedUserMessage sanitizedUserMessage,
semanticHints: candidateMeta?.semanticHints ?? null
}, userMessage); }, userMessage);
} }
catch (error) { catch (error) {
@ -3975,7 +4062,11 @@ function resolveAssistantOrchestrationDecision(input) {
hasOpenContractsAddressSignal(repairedEffectiveAddressUserMessage); hasOpenContractsAddressSignal(repairedEffectiveAddressUserMessage);
const modeSample = repairedEffectiveAddressUserMessage || effectiveAddressUserMessage; const modeSample = repairedEffectiveAddressUserMessage || effectiveAddressUserMessage;
const modeDetection = (0, addressQueryClassifier_1.detectAddressQuestionMode)(modeSample); const modeDetection = (0, addressQueryClassifier_1.detectAddressQuestionMode)(modeSample);
const modeDetectionRaw = (0, addressQueryClassifier_1.detectAddressQuestionMode)(repairedRawUserMessage || rawUserMessage);
const resolvedModeDetection = modeDetection.mode === "address_query" ? modeDetection : modeDetectionRaw;
const intentResolution = (0, addressIntentResolver_1.resolveAddressIntent)(modeSample); const intentResolution = (0, addressIntentResolver_1.resolveAddressIntent)(modeSample);
const intentResolutionRaw = (0, addressIntentResolver_1.resolveAddressIntent)(repairedRawUserMessage || rawUserMessage);
const resolvedIntentResolution = intentResolution.intent !== "unknown" ? intentResolution : intentResolutionRaw;
const llmContractIntent = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent); const llmContractIntent = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
const llmPreDecomposeReason = toNonEmptyString(llmPreDecomposeMeta?.reason); const llmPreDecomposeReason = toNonEmptyString(llmPreDecomposeMeta?.reason);
const llmRuntimeUnavailableDetected = Boolean(llmPreDecomposeReason && const llmRuntimeUnavailableDetected = Boolean(llmPreDecomposeReason &&
@ -3993,10 +4084,10 @@ function resolveAssistantOrchestrationDecision(input) {
hasStrictDeepInvestigationCue(repairedRawUserMessage) || hasStrictDeepInvestigationCue(repairedRawUserMessage) ||
hasStrictDeepInvestigationCue(effectiveAddressUserMessage) || hasStrictDeepInvestigationCue(effectiveAddressUserMessage) ||
hasStrictDeepInvestigationCue(repairedEffectiveAddressUserMessage); hasStrictDeepInvestigationCue(repairedEffectiveAddressUserMessage);
const strictDeepInvestigationBypassAllowed = shouldBypassStrictDeepInvestigationCueForAddressIntent(intentResolution.intent) || const strictDeepInvestigationBypassAllowed = shouldBypassStrictDeepInvestigationCueForAddressIntent(resolvedIntentResolution.intent) ||
shouldBypassStrictDeepInvestigationCueForAddressIntent(llmContractIntent); shouldBypassStrictDeepInvestigationCueForAddressIntent(llmContractIntent);
const keepAddressLaneByIntent = semanticApplyCanonicalRecommended && const keepAddressLaneByIntent = semanticApplyCanonicalRecommended &&
Boolean((intentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(intentResolution.intent)) || Boolean((resolvedIntentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(resolvedIntentResolution.intent)) ||
(llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)) || (llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)) ||
openContractsAddressSignal) && openContractsAddressSignal) &&
(!strictDeepInvestigationCueDetected || strictDeepInvestigationBypassAllowed); (!strictDeepInvestigationCueDetected || strictDeepInvestigationBypassAllowed);
@ -4037,8 +4128,8 @@ function resolveAssistantOrchestrationDecision(input) {
!capabilityMetaQuery && !capabilityMetaQuery &&
!dataRetrievalSignal && !dataRetrievalSignal &&
!effectiveAddressFollowupSignal && !effectiveAddressFollowupSignal &&
modeDetection.mode === "unsupported" && resolvedModeDetection.mode === "unsupported" &&
intentResolution.intent === "unknown"); resolvedIntentResolution.intent === "unknown");
const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate && const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate &&
deterministicNonDomainGuard && deterministicNonDomainGuard &&
(llmFirstUnsupportedCandidate || llmContractMode === null) && (llmFirstUnsupportedCandidate || llmContractMode === null) &&
@ -4058,10 +4149,10 @@ function resolveAssistantOrchestrationDecision(input) {
orchestrationContract: { orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1", schema_version: "assistant_orchestration_contract_v1",
hard_meta_mode: "data_scope", hard_meta_mode: "data_scope",
address_mode: modeDetection.mode, address_mode: resolvedModeDetection.mode,
address_mode_confidence: modeDetection.confidence, address_mode_confidence: resolvedModeDetection.confidence,
address_intent: intentResolution.intent, address_intent: resolvedIntentResolution.intent,
address_intent_confidence: intentResolution.confidence, address_intent_confidence: resolvedIntentResolution.confidence,
strong_data_signal_detected: strongDataSignal, strong_data_signal_detected: strongDataSignal,
data_retrieval_signal_detected: dataRetrievalSignal, data_retrieval_signal_detected: dataRetrievalSignal,
followup_context_detected: Boolean(followupContext), followup_context_detected: Boolean(followupContext),
@ -4086,10 +4177,10 @@ function resolveAssistantOrchestrationDecision(input) {
orchestrationContract: { orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1", schema_version: "assistant_orchestration_contract_v1",
hard_meta_mode: "capability", hard_meta_mode: "capability",
address_mode: modeDetection.mode, address_mode: resolvedModeDetection.mode,
address_mode_confidence: modeDetection.confidence, address_mode_confidence: resolvedModeDetection.confidence,
address_intent: intentResolution.intent, address_intent: resolvedIntentResolution.intent,
address_intent_confidence: intentResolution.confidence, address_intent_confidence: resolvedIntentResolution.confidence,
strong_data_signal_detected: strongDataSignal, strong_data_signal_detected: strongDataSignal,
data_retrieval_signal_detected: dataRetrievalSignal, data_retrieval_signal_detected: dataRetrievalSignal,
followup_context_detected: Boolean(followupContext), followup_context_detected: Boolean(followupContext),
@ -4114,10 +4205,10 @@ function resolveAssistantOrchestrationDecision(input) {
orchestrationContract: { orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1", schema_version: "assistant_orchestration_contract_v1",
hard_meta_mode: "non_domain", hard_meta_mode: "non_domain",
address_mode: modeDetection.mode, address_mode: resolvedModeDetection.mode,
address_mode_confidence: modeDetection.confidence, address_mode_confidence: resolvedModeDetection.confidence,
address_intent: intentResolution.intent, address_intent: resolvedIntentResolution.intent,
address_intent_confidence: intentResolution.confidence, address_intent_confidence: resolvedIntentResolution.confidence,
strong_data_signal_detected: strongDataSignal, strong_data_signal_detected: strongDataSignal,
data_retrieval_signal_detected: dataRetrievalSignal, data_retrieval_signal_detected: dataRetrievalSignal,
followup_context_detected: Boolean(followupContext), followup_context_detected: Boolean(followupContext),
@ -4153,7 +4244,7 @@ function resolveAssistantOrchestrationDecision(input) {
hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) || hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) ||
hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage)); hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage));
const supportedAddressIntentDetected = (!strictDeepInvestigationCueDetected || strictDeepInvestigationBypassAllowed) && const supportedAddressIntentDetected = (!strictDeepInvestigationCueDetected || strictDeepInvestigationBypassAllowed) &&
Boolean((intentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(intentResolution.intent)) || Boolean((resolvedIntentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(resolvedIntentResolution.intent)) ||
(llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)) || (llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)) ||
openContractsAddressSignal); openContractsAddressSignal);
const semanticGuardHints = semanticExtractionContract?.guard_hints && const semanticGuardHints = semanticExtractionContract?.guard_hints &&
@ -4173,7 +4264,7 @@ function resolveAssistantOrchestrationDecision(input) {
semanticAggregateShapeDetected || semanticAggregateShapeDetected ||
semanticDeepInvestigationHintDetected || semanticDeepInvestigationHintDetected ||
!semanticApplyCanonicalRecommended)); !semanticApplyCanonicalRecommended));
const unsupportedIntentOrMode = (modeDetection.mode !== "address_query" && intentResolution.intent === "unknown") || const unsupportedIntentOrMode = (resolvedModeDetection.mode !== "address_query" && resolvedIntentResolution.intent === "unknown") ||
llmContractMode === "unsupported"; llmContractMode === "unsupported";
const unsupportedAddressIntentFallbackToDeep = Boolean(baseToolGate?.runAddressLane && const unsupportedAddressIntentFallbackToDeep = Boolean(baseToolGate?.runAddressLane &&
!llmRuntimeUnavailableDetected && !llmRuntimeUnavailableDetected &&
@ -4293,10 +4384,10 @@ function resolveAssistantOrchestrationDecision(input) {
orchestrationContract: { orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1", schema_version: "assistant_orchestration_contract_v1",
hard_meta_mode: null, hard_meta_mode: null,
address_mode: modeDetection.mode, address_mode: resolvedModeDetection.mode,
address_mode_confidence: modeDetection.confidence, address_mode_confidence: resolvedModeDetection.confidence,
address_intent: intentResolution.intent, address_intent: resolvedIntentResolution.intent,
address_intent_confidence: intentResolution.confidence, address_intent_confidence: resolvedIntentResolution.confidence,
strong_data_signal_detected: strongDataSignal, strong_data_signal_detected: strongDataSignal,
data_retrieval_signal_detected: dataRetrievalSignal, data_retrieval_signal_detected: dataRetrievalSignal,
semantic_contract_valid: semanticContractValid, semantic_contract_valid: semanticContractValid,

View File

@ -300,6 +300,71 @@ function coerceFlags(value, fallback) {
mentions_period_close_context: pick("mentions_period_close_context", ["period_close_context"]) mentions_period_close_context: pick("mentions_period_close_context", ["period_close_context"])
}; };
} }
function inferSemanticHints(rawText, timeScope) {
return {
scope_target_kind: "none",
scope_target_text: null,
date_scope_kind: timeScope.type === "explicit" ? "explicit" : "missing",
self_scope_detected: false,
selected_object_scope_detected: /(?:по\s+выбранному\s+объекту|selected\s+object)/iu.test(String(rawText ?? ""))
};
}
function coerceSemanticScopeTargetKind(value) {
const token = normalizeToken(value);
if (token === "none" ||
token === "self_scope" ||
token === "selected_object" ||
token === "organization" ||
token === "warehouse" ||
token === "counterparty" ||
token === "contract" ||
token === "item") {
return token;
}
if (["organization_scope", "company_scope", "org_scope", "company", "organization_anchor"].includes(token)) {
return "organization";
}
if (["warehouse_scope", "stock_scope", "warehouse_anchor"].includes(token)) {
return "warehouse";
}
if (["own_company_scope", "implicit_self_scope", "our_scope"].includes(token)) {
return "self_scope";
}
if (["selected_object_scope", "selected_object_anchor"].includes(token)) {
return "selected_object";
}
return "none";
}
function coerceSemanticDateScopeKind(value) {
const token = normalizeToken(value);
if (token === "explicit" || token === "implicit_current" || token === "missing") {
return token;
}
if (["implicit_current_snapshot", "current", "today", "default_current"].includes(token)) {
return "implicit_current";
}
return "missing";
}
function coerceSemanticHints(value, rawText, timeScope) {
const fallback = inferSemanticHints(rawText, timeScope);
if (!value || typeof value !== "object") {
return fallback;
}
const source = value;
return {
scope_target_kind: coerceSemanticScopeTargetKind(source.scope_target_kind ?? source.anchor_kind ?? source.scope_kind),
scope_target_text: toOptionalString(source.scope_target_text ??
source.anchor_value ??
source.organization ??
source.warehouse ??
source.counterparty ??
source.contract ??
source.item) ?? fallback.scope_target_text,
date_scope_kind: coerceSemanticDateScopeKind(source.date_scope_kind ?? source.date_scope ?? source.time_scope_kind),
self_scope_detected: coerceBoolean(source.self_scope_detected, fallback.self_scope_detected),
selected_object_scope_detected: coerceBoolean(source.selected_object_scope_detected, fallback.selected_object_scope_detected)
};
}
function mapCandidateLabel(value) { function mapCandidateLabel(value) {
const token = normalizeToken(value); const token = normalizeToken(value);
if (CANDIDATE_LABEL_VALUES.includes(token)) { if (CANDIDATE_LABEL_VALUES.includes(token)) {
@ -359,6 +424,7 @@ function coerceFragmentV2(rawFragment, index, userMessage) {
const accountHints = coerceStringArray(source.account_hints); const accountHints = coerceStringArray(source.account_hints);
const documentHints = coerceStringArray(source.document_hints); const documentHints = coerceStringArray(source.document_hints);
const registerHints = coerceStringArray(source.register_hints); const registerHints = coerceStringArray(source.register_hints);
const timeScope = coerceTimeScope(source.time_scope, rawText, base.time_scope);
return { return {
fragment_id: coerceFragmentId(source.fragment_id, index, base.fragment_id), fragment_id: coerceFragmentId(source.fragment_id, index, base.fragment_id),
raw_fragment_text: rawText, raw_fragment_text: rawText,
@ -369,8 +435,9 @@ function coerceFragmentV2(rawFragment, index, userMessage) {
account_hints: accountHints.length > 0 ? accountHints : base.account_hints, account_hints: accountHints.length > 0 ? accountHints : base.account_hints,
document_hints: documentHints.length > 0 ? documentHints : base.document_hints, document_hints: documentHints.length > 0 ? documentHints : base.document_hints,
register_hints: registerHints.length > 0 ? registerHints : base.register_hints, register_hints: registerHints.length > 0 ? registerHints : base.register_hints,
time_scope: coerceTimeScope(source.time_scope, rawText, base.time_scope), time_scope: timeScope,
flags, flags,
semantic_hints: coerceSemanticHints(source.semantic_hints, rawText, timeScope),
candidate_labels: coerceCandidateLabels(source.candidate_labels, flags, domainRelevance, base.candidate_labels), candidate_labels: coerceCandidateLabels(source.candidate_labels, flags, domainRelevance, base.candidate_labels),
confidence: coerceConfidence(source.confidence, base.confidence) confidence: coerceConfidence(source.confidence, base.confidence)
}; };
@ -811,6 +878,7 @@ function buildFragmentV2(rawText, index) {
else if (flags.asks_for_exact_object_trace || flags.asks_for_ranking_or_top) { else if (flags.asks_for_exact_object_trace || flags.asks_for_ranking_or_top) {
confidence = "high"; confidence = "high";
} }
const timeScope = inferTimeScope(text);
return { return {
fragment_id: `F${index + 1}`, fragment_id: `F${index + 1}`,
raw_fragment_text: text, raw_fragment_text: text,
@ -821,8 +889,9 @@ function buildFragmentV2(rawText, index) {
account_hints: extractAccounts(text), account_hints: extractAccounts(text),
document_hints: Array.from(new Set(Array.from(lower.matchAll(/(документ|реализац|поступлен|платеж|выписк|акт сверк)/g)).map((item) => item[0]))), document_hints: Array.from(new Set(Array.from(lower.matchAll(/(документ|реализац|поступлен|платеж|выписк|акт сверк)/g)).map((item) => item[0]))),
register_hints: Array.from(new Set(Array.from(lower.matchAll(/(регистр|движен|остатк|сальдо)/g)).map((item) => item[0]))), register_hints: Array.from(new Set(Array.from(lower.matchAll(/(регистр|движен|остатк|сальдо)/g)).map((item) => item[0]))),
time_scope: inferTimeScope(text), time_scope: timeScope,
flags, flags,
semantic_hints: inferSemanticHints(text, timeScope),
candidate_labels: candidateLabels, candidate_labels: candidateLabels,
confidence confidence
}; };

View File

@ -50,6 +50,7 @@
"register_hints", "register_hints",
"time_scope", "time_scope",
"flags", "flags",
"semantic_hints",
"candidate_labels", "candidate_labels",
"confidence" "confidence"
], ],
@ -134,6 +135,41 @@
"mentions_period_close_context": { "type": "boolean" } "mentions_period_close_context": { "type": "boolean" }
} }
}, },
"semantic_hints": {
"type": "object",
"additionalProperties": false,
"required": [
"scope_target_kind",
"scope_target_text",
"date_scope_kind",
"self_scope_detected",
"selected_object_scope_detected"
],
"properties": {
"scope_target_kind": {
"type": "string",
"enum": [
"none",
"self_scope",
"selected_object",
"organization",
"warehouse",
"counterparty",
"contract",
"item"
]
},
"scope_target_text": {
"type": ["string", "null"]
},
"date_scope_kind": {
"type": "string",
"enum": ["explicit", "implicit_current", "missing"]
},
"self_scope_detected": { "type": "boolean" },
"selected_object_scope_detected": { "type": "boolean" }
}
},
"candidate_labels": { "candidate_labels": {
"type": "array", "type": "array",
"items": { "items": {

View File

@ -50,6 +50,7 @@
"register_hints", "register_hints",
"time_scope", "time_scope",
"flags", "flags",
"semantic_hints",
"candidate_labels", "candidate_labels",
"confidence", "confidence",
"execution_readiness", "execution_readiness",
@ -120,6 +121,41 @@
"mentions_period_close_context": { "type": "boolean" } "mentions_period_close_context": { "type": "boolean" }
} }
}, },
"semantic_hints": {
"type": "object",
"additionalProperties": false,
"required": [
"scope_target_kind",
"scope_target_text",
"date_scope_kind",
"self_scope_detected",
"selected_object_scope_detected"
],
"properties": {
"scope_target_kind": {
"type": "string",
"enum": [
"none",
"self_scope",
"selected_object",
"organization",
"warehouse",
"counterparty",
"contract",
"item"
]
},
"scope_target_text": {
"type": ["string", "null"]
},
"date_scope_kind": {
"type": "string",
"enum": ["explicit", "implicit_current", "missing"]
},
"self_scope_detected": { "type": "boolean" },
"selected_object_scope_detected": { "type": "boolean" }
}
},
"candidate_labels": { "candidate_labels": {
"type": "array", "type": "array",
"items": { "items": {
@ -180,4 +216,3 @@
} }
} }
} }

View File

@ -50,6 +50,7 @@
"register_hints", "register_hints",
"time_scope", "time_scope",
"flags", "flags",
"semantic_hints",
"candidate_labels", "candidate_labels",
"confidence", "confidence",
"execution_readiness", "execution_readiness",
@ -122,6 +123,41 @@
"mentions_period_close_context": { "type": "boolean" } "mentions_period_close_context": { "type": "boolean" }
} }
}, },
"semantic_hints": {
"type": "object",
"additionalProperties": false,
"required": [
"scope_target_kind",
"scope_target_text",
"date_scope_kind",
"self_scope_detected",
"selected_object_scope_detected"
],
"properties": {
"scope_target_kind": {
"type": "string",
"enum": [
"none",
"self_scope",
"selected_object",
"organization",
"warehouse",
"counterparty",
"contract",
"item"
]
},
"scope_target_text": {
"type": ["string", "null"]
},
"date_scope_kind": {
"type": "string",
"enum": ["explicit", "implicit_current", "missing"]
},
"self_scope_detected": { "type": "boolean" },
"selected_object_scope_detected": { "type": "boolean" }
}
},
"candidate_labels": { "candidate_labels": {
"type": "array", "type": "array",
"items": { "items": {

View File

@ -1,4 +1,4 @@
import type { AddressFilterExtraction, AddressFilterSet, AddressIntent } from "../types/addressQuery"; import type { AddressFilterExtraction, AddressFilterSet, AddressIntent, AddressSemanticFrame } from "../types/addressQuery";
import iconv from "iconv-lite"; import iconv from "iconv-lite";
const ACCOUNT_PATTERN = /(?:сч[её]т|счет|account)[^0-9]{0,12}(\d{2}(?:[.,]\d{1,2})?)/i; const ACCOUNT_PATTERN = /(?:сч[её]т|счет|account)[^0-9]{0,12}(\d{2}(?:[.,]\d{1,2})?)/i;
@ -1088,6 +1088,29 @@ function isTemporalWarehousePhrase(candidate: string): boolean {
); );
} }
function normalizeSemanticAnchorCandidate(value: string): string {
return cleanupAnchorValue(value)
.toLowerCase()
.replace(/С/g, "Рµ")
.replace(/\s+/g, " ")
.trim();
}
function hasImplicitSelfScopeSignal(text: string): boolean {
return /(?:^|[\s,.;:!?()\-])(?:у\s+нас|у\s+себя|у\s+меня|наш(?:ем|ей|его|их|а|е)?|сво(?:ем|ей|его|их|я|е)?)(?=$|[\s,.;:!?()\-])/iu.test(
String(text ?? "")
);
}
function isImplicitSelfScopeWarehouseAnchor(candidate: string): boolean {
const normalized = normalizeSemanticAnchorCandidate(candidate);
return /^(?:у\s+нас|у\s+себя|у\s+меня|наш(?:ем|ей|его|их|а|е)?|сво(?:ем|ей|его|их|я|е)?)$/iu.test(normalized);
}
function hasSelectedObjectScopeSignal(text: string): boolean {
return /(?:по\s+выбранному\s+объекту|selected\s+object)/iu.test(String(text ?? ""));
}
function extractInventoryWarehouseAnchor(text: string): string | undefined { function extractInventoryWarehouseAnchor(text: string): string | undefined {
const patterns = [ const patterns = [
/(?:на|по)\s+склад(?:е|у|ом)?\s+[«"']?([^\r\n,.;:!?]+?)(?:[»"']|(?=\s+(?:на|по|за|с|в)\b|[?]|$))/iu, /(?:на|по)\s+склад(?:е|у|ом)?\s+[«"']?([^\r\n,.;:!?]+?)(?:[»"']|(?=\s+(?:на|по|за|с|в)\b|[?]|$))/iu,
@ -1109,6 +1132,7 @@ function extractInventoryWarehouseAnchor(text: string): string | undefined {
!candidate || !candidate ||
candidate.includes("->") || candidate.includes("->") ||
candidate.includes("=>") || candidate.includes("=>") ||
isImplicitSelfScopeWarehouseAnchor(candidate) ||
normalizedCandidate.startsWith("по состоянию") || normalizedCandidate.startsWith("по состоянию") ||
isTemporalWarehousePhrase(candidate) || isTemporalWarehousePhrase(candidate) ||
/^(?:сейчас|на|дату|дате|остаток|остатки)$/iu.test(candidate) /^(?:сейчас|на|дату|дате|остаток|остатки)$/iu.test(candidate)
@ -1244,6 +1268,114 @@ function shouldDefaultAsOfDateToToday(intent: AddressIntent): boolean {
); );
} }
function resolveSemanticDateScopeKind(
filters: AddressFilterSet,
warnings: string[]
): AddressSemanticFrame["date_scope_kind"] {
if (warnings.includes("as_of_date_defaulted_today")) {
return "implicit_current";
}
if (
(typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0) ||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0) ||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0)
) {
return "explicit";
}
return "none";
}
function resolveSemanticDateBasisHint(filters: AddressFilterSet, warnings: string[]): AddressSemanticFrame["date_basis_hint"] {
if (warnings.includes("as_of_date_defaulted_today")) {
return "implicit_current_snapshot";
}
const hasAsOfDate = typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0;
const hasPeriodFrom = typeof filters.period_from === "string" && filters.period_from.trim().length > 0;
const hasPeriodTo = typeof filters.period_to === "string" && filters.period_to.trim().length > 0;
if (hasPeriodFrom && hasPeriodTo) {
return "period_range";
}
if (hasAsOfDate) {
return "explicit_as_of_date";
}
if (hasPeriodTo) {
return "period_end";
}
if (hasPeriodFrom) {
return "period_range";
}
return null;
}
function buildSemanticFrame(
text: string,
filters: AddressFilterSet,
warnings: string[]
): AddressSemanticFrame {
const selfScopeDetected = hasImplicitSelfScopeSignal(text);
const selectedObjectScopeDetected = hasSelectedObjectScopeSignal(text);
const itemAnchor = typeof filters.item === "string" && filters.item.trim().length > 0 ? filters.item.trim() : null;
const warehouseAnchor = typeof filters.warehouse === "string" && filters.warehouse.trim().length > 0 ? filters.warehouse.trim() : null;
const counterpartyAnchor =
typeof filters.counterparty === "string" && filters.counterparty.trim().length > 0 ? filters.counterparty.trim() : null;
const contractAnchor = typeof filters.contract === "string" && filters.contract.trim().length > 0 ? filters.contract.trim() : null;
const organizationAnchor =
typeof filters.organization === "string" && filters.organization.trim().length > 0 ? filters.organization.trim() : null;
if (selectedObjectScopeDetected && itemAnchor) {
return {
scope_kind: "selected_object_scope",
anchor_kind: "item",
anchor_value: itemAnchor,
date_scope_kind: resolveSemanticDateScopeKind(filters, warnings),
date_basis_hint: resolveSemanticDateBasisHint(filters, warnings),
self_scope_detected: selfScopeDetected,
selected_object_scope_detected: true
};
}
if (selfScopeDetected && !warehouseAnchor) {
return {
scope_kind: "implicit_self_scope",
anchor_kind: "self_scope",
anchor_value: null,
date_scope_kind: resolveSemanticDateScopeKind(filters, warnings),
date_basis_hint: resolveSemanticDateBasisHint(filters, warnings),
self_scope_detected: true,
selected_object_scope_detected: selectedObjectScopeDetected
};
}
const explicitAnchor =
itemAnchor ??
warehouseAnchor ??
counterpartyAnchor ??
contractAnchor ??
organizationAnchor ??
null;
const anchorKind: AddressSemanticFrame["anchor_kind"] = itemAnchor
? "item"
: warehouseAnchor
? "warehouse"
: counterpartyAnchor
? "counterparty"
: contractAnchor
? "contract"
: organizationAnchor
? "organization"
: "none";
return {
scope_kind: explicitAnchor ? "explicit_anchor" : "none",
anchor_kind: anchorKind,
anchor_value: explicitAnchor,
date_scope_kind: resolveSemanticDateScopeKind(filters, warnings),
date_basis_hint: resolveSemanticDateBasisHint(filters, warnings),
self_scope_detected: selfScopeDetected,
selected_object_scope_detected: selectedObjectScopeDetected
};
}
export function extractAddressFilters(userMessage: string, intent: AddressIntent): AddressFilterExtraction { export function extractAddressFilters(userMessage: string, intent: AddressIntent): AddressFilterExtraction {
const rawText = String(userMessage ?? "").trim(); const rawText = String(userMessage ?? "").trim();
const text = normalizeMojibakeString(rawText); const text = normalizeMojibakeString(rawText);
@ -1302,6 +1434,11 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
const warehouseAnchor = extractInventoryWarehouseAnchor(text); const warehouseAnchor = extractInventoryWarehouseAnchor(text);
if (warehouseAnchor) { if (warehouseAnchor) {
filters.warehouse = warehouseAnchor; filters.warehouse = warehouseAnchor;
} else if (
(intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date") &&
hasImplicitSelfScopeSignal(text)
) {
warnings.push("warehouse_self_scope_detected");
} }
if (intent === "inventory_supplier_stock_overlap_as_of_date") { if (intent === "inventory_supplier_stock_overlap_as_of_date") {
@ -1511,10 +1648,12 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
const value = filters[key]; const value = filters[key];
return value === undefined || value === null || String(value).trim() === ""; return value === undefined || value === null || String(value).trim() === "";
}); });
const semanticFrame = buildSemanticFrame(text, filters, warnings);
return { return {
extracted_filters: filters, extracted_filters: filters,
missing_required_filters: missingRequiredFilters, missing_required_filters: missingRequiredFilters,
warnings warnings,
semantic_frame: semanticFrame
}; };
} }

View File

@ -1553,7 +1553,10 @@ function hasInventoryAsOfCue(text: string): boolean {
} }
function hasInventoryOnHandSignal(text: string): boolean { function hasInventoryOnHandSignal(text: string): boolean {
const hasColloquialStockSnapshotCue = /(?:что|ч[её])\s+(?:у\s+нас\s+)?на\s+склад(?:е|у|ом)(?=$|[\s,.;:!?])/iu.test( const hasColloquialStockSnapshotCue = /(?:что|ч[еёо])\s+(?:у\s+нас\s+)?на\s+склад(?:е|у|ом|ах)(?=$|[\s,.;:!?])/iu.test(
text
);
const hasStockStateCue = /(?:(?:что|ч[еёо])\s+там\s+на\s+склад(?:е|у|ом|ах)|(?:что|ч[еёо]).*происход(?:ит|ило|ящее).*(?:на\s+)?склад(?:е|у|ом|ах)|происход(?:ит|ило|ящее)\s+на\s+склад(?:е|у|ом|ах)|ситуац(?:ия|ии)\s+на\s+склад(?:е|у|ом|ах)|обстановк(?:а|и)\s+на\s+склад(?:е|у|ом|ах)|what(?:'s| is)?\s+(?:there\s+)?(?:on|in)\s+(?:the\s+)?(?:warehouse|stock)|what(?:'s| is)?\s+happening\s+(?:on|in)\s+(?:the\s+)?(?:warehouse|stock))/iu.test(
text text
); );
const hasAccount41Anchor = hasInventoryAccount41Anchor(text); const hasAccount41Anchor = hasInventoryAccount41Anchor(text);
@ -1574,15 +1577,18 @@ function hasInventoryOnHandSignal(text: string): boolean {
const hasGoodsLexeme = const hasGoodsLexeme =
/(?:товар(?:ы|ов|ом|а|ные)?|номенклатур|материал(?:ы|ов|а|ам)?|item(?:s)?|sku|product(?:s)?)/iu.test(text); /(?:товар(?:ы|ов|ом|а|ные)?|номенклатур|материал(?:ы|ов|а|ам)?|item(?:s)?|sku|product(?:s)?)/iu.test(text);
const hasBalanceLexeme = const hasBalanceLexeme =
/(?:леж(?:ит|ат)|есть|числ(?:ит(?:ся|сь)|ятся)|остат(?:ок|ки)|срез|на\s+дат|по\s+состоянию|на\s+конец|today|now|current|as\s+of)/iu.test( /(?:леж(?:ит|ат)|есть|числ(?:ит(?:ся|сь)|ятся)|остат(?:ок|ки)|срез|на\s+дат|по\s+состоянию|на\s+конец|происход(?:ит|ило|ящее)|ситуац(?:ия|ии)|обстановк(?:а|и)|today|now|current|as\s+of)/iu.test(
text
);
const hasRequestCue =
/(?:покажи|показать|выведи|дай|какие|что|ч[еёо]|какой|сколько|проверь|проверить|чекни|check|show|list|which|what)/iu.test(
text text
); );
const hasRequestCue = /(?:покажи|показать|выведи|дай|какие|что|какой|сколько|show|list|which|what)/iu.test(text);
if (hasAccount41Anchor && (hasGoodsLexeme || hasBalanceLexeme || hasRequestCue || hasInventoryAsOfCue(text))) { if (hasAccount41Anchor && (hasGoodsLexeme || hasBalanceLexeme || hasRequestCue || hasInventoryAsOfCue(text))) {
return true; return true;
} }
return (hasGoodsLexeme || hasBalanceLexeme || hasColloquialStockSnapshotCue) && return (hasGoodsLexeme || hasBalanceLexeme || hasColloquialStockSnapshotCue || hasStockStateCue) &&
(hasRequestCue || hasBalanceLexeme || hasColloquialStockSnapshotCue); (hasRequestCue || hasBalanceLexeme || hasColloquialStockSnapshotCue || hasStockStateCue);
} }
function hasInventoryProvenanceSignal(text: string): boolean { function hasInventoryProvenanceSignal(text: string): boolean {

View File

@ -13,6 +13,14 @@ const ADDRESS_ACTION_TOKENS = [
"покажи", "покажи",
"покаж", "покаж",
"показ", "показ",
"проверь",
"провер",
"чекни",
"чекн",
"глянь",
"глян",
"посмотри",
"смотри",
"список", "список",
"найди", "найди",
"найд", "найд",

View File

@ -14,13 +14,15 @@ import type {
AddressExecutionResult, AddressExecutionResult,
AddressFilterSet, AddressFilterSet,
AddressIntent, AddressIntent,
AddressLlmSemanticHints,
AddressLimitedReasonCategory, AddressLimitedReasonCategory,
AddressMatchFailureStage, AddressMatchFailureStage,
AddressMcpCallStatus, AddressMcpCallStatus,
AddressQueryShapeDetection, AddressQueryShapeDetection,
AddressResultMode, AddressResultMode,
AddressResponseType, AddressResponseType,
AddressRuntimeReadiness AddressRuntimeReadiness,
AddressSemanticFrame
} from "../types/addressQuery"; } from "../types/addressQuery";
import { import {
buildAddressRecipePlan, buildAddressRecipePlan,
@ -47,6 +49,12 @@ import {
resolveShadowRouteIntent resolveShadowRouteIntent
} from "./addressCapabilityPolicy"; } from "./addressCapabilityPolicy";
import { evaluateAddressRouteExpectation, type AddressRouteExpectationAudit } from "./addressRouteExpectations"; import { evaluateAddressRouteExpectation, type AddressRouteExpectationAudit } from "./addressRouteExpectations";
import {
mergeKnownOrganizations,
normalizeOrganizationScopeSearchText,
normalizeOrganizationScopeValue,
resolveOrganizationSelectionFromMessage
} from "./assistantOrganizationMatcher";
interface NormalizedAddressRow { interface NormalizedAddressRow {
period: string | null; period: string | null;
@ -64,6 +72,9 @@ interface NormalizedAddressRow {
interface AddressTryHandleOptions { interface AddressTryHandleOptions {
followupContext?: AddressFollowupContext | null; followupContext?: AddressFollowupContext | null;
analysisDateHint?: string | null; analysisDateHint?: string | null;
llmSemanticHints?: AddressLlmSemanticHints | null;
activeOrganization?: string | null;
knownOrganizations?: string[];
} }
interface AddressCapabilityAudit { interface AddressCapabilityAudit {
@ -1446,6 +1457,60 @@ function isCounterpartyRiskIntent(intent: AddressIntent): boolean {
); );
} }
function sameNormalizedOrganizationScope(left: string | null | undefined, right: string | null | undefined): boolean {
return normalizeOrganizationScopeSearchText(left ?? "") === normalizeOrganizationScopeSearchText(right ?? "");
}
function applyPreExecutionOrganizationScopeGrounding(input: {
userMessage: string;
filters: AddressFilterSet;
semanticFrame: AddressSemanticFrame | null;
warnings: string[];
baseReasons: string[];
activeOrganization?: string | null;
knownOrganizations?: string[];
}): string | null {
const activeOrganization = normalizeOrganizationScopeValue(input.activeOrganization ?? null);
const candidateOrganizations = mergeKnownOrganizations([
...(Array.isArray(input.knownOrganizations) ? input.knownOrganizations : []),
activeOrganization
]);
const resolvedOrganizationFromMessage = resolveOrganizationSelectionFromMessage(input.userMessage, candidateOrganizations);
if (
!input.filters.organization &&
input.semanticFrame?.scope_kind === "implicit_self_scope" &&
activeOrganization
) {
input.filters.organization = activeOrganization;
if (!input.warnings.includes("organization_from_active_scope")) {
input.warnings.push("organization_from_active_scope");
}
if (!input.baseReasons.includes("organization_from_active_scope")) {
input.baseReasons.push("organization_from_active_scope");
}
}
if (
resolvedOrganizationFromMessage &&
(!input.filters.organization || input.semanticFrame?.anchor_kind === "organization") &&
!sameNormalizedOrganizationScope(input.filters.organization ?? null, resolvedOrganizationFromMessage)
) {
input.filters.organization = resolvedOrganizationFromMessage;
if (!input.warnings.includes("organization_grounded_from_scope_candidates")) {
input.warnings.push("organization_grounded_from_scope_candidates");
}
if (!input.baseReasons.includes("organization_grounded_from_scope_candidates")) {
input.baseReasons.push("organization_grounded_from_scope_candidates");
}
if (input.semanticFrame?.anchor_kind === "organization") {
input.semanticFrame.anchor_value = resolvedOrganizationFromMessage;
}
}
return resolvedOrganizationFromMessage;
}
function isHeuristicCandidatesIntent(intent: AddressIntent): boolean { function isHeuristicCandidatesIntent(intent: AddressIntent): boolean {
return ( return (
intent === "list_receivables_counterparties" || intent === "list_receivables_counterparties" ||
@ -1472,7 +1537,10 @@ function isConfirmedBalanceIntent(intent: AddressIntent): boolean {
); );
} }
function resolveAsOfDateBasis(filters: AddressFilterSet): AddressAsOfDateBasis | null { function resolveAsOfDateBasis(filters: AddressFilterSet, semanticFrame?: AddressSemanticFrame | null): AddressAsOfDateBasis | null {
if (semanticFrame?.date_basis_hint) {
return semanticFrame.date_basis_hint;
}
const asOfDate = normalizeAnalysisDateHint(filters.as_of_date); const asOfDate = normalizeAnalysisDateHint(filters.as_of_date);
if (asOfDate) { if (asOfDate) {
return "explicit_as_of_date"; return "explicit_as_of_date";
@ -1515,7 +1583,11 @@ function deriveAddressEvidenceStrength(input: {
return undefined; return undefined;
} }
function resolveRequestedResultMode(intent: AddressIntent, filters: AddressFilterSet): AddressResultMode | undefined { function resolveRequestedResultMode(
intent: AddressIntent,
filters: AddressFilterSet,
semanticFrame?: AddressSemanticFrame | null
): AddressResultMode | undefined {
if (isConfirmedBalanceIntent(intent)) { if (isConfirmedBalanceIntent(intent)) {
return "confirmed_balance"; return "confirmed_balance";
} }
@ -1523,8 +1595,13 @@ function resolveRequestedResultMode(intent: AddressIntent, filters: AddressFilte
return "heuristic_candidates"; return "heuristic_candidates";
} }
if (isHeuristicCandidatesIntent(intent)) { if (isHeuristicCandidatesIntent(intent)) {
const asOfDateBasis = resolveAsOfDateBasis(filters); const asOfDateBasis = resolveAsOfDateBasis(filters, semanticFrame);
if (asOfDateBasis === "explicit_as_of_date" || asOfDateBasis === "period_end" || asOfDateBasis === "period_range") { if (
asOfDateBasis === "explicit_as_of_date" ||
asOfDateBasis === "period_end" ||
asOfDateBasis === "period_range" ||
asOfDateBasis === "implicit_current_snapshot"
) {
return "confirmed_balance"; return "confirmed_balance";
} }
return "heuristic_candidates"; return "heuristic_candidates";
@ -1536,6 +1613,7 @@ function deriveAddressResultSemantics(input: {
intent: AddressIntent; intent: AddressIntent;
selectedRecipe: string | null; selectedRecipe: string | null;
filters: AddressFilterSet; filters: AddressFilterSet;
semanticFrame?: AddressSemanticFrame | null;
responseType: AddressResponseType; responseType: AddressResponseType;
rowsMatched: number; rowsMatched: number;
}): { }): {
@ -1545,8 +1623,8 @@ function deriveAddressResultSemantics(input: {
balance_confirmed?: boolean; balance_confirmed?: boolean;
as_of_date_basis?: AddressAsOfDateBasis | null; as_of_date_basis?: AddressAsOfDateBasis | null;
} { } {
const asOfDateBasis = resolveAsOfDateBasis(input.filters); const asOfDateBasis = resolveAsOfDateBasis(input.filters, input.semanticFrame);
const requestedResultMode = resolveRequestedResultMode(input.intent, input.filters); const requestedResultMode = resolveRequestedResultMode(input.intent, input.filters, input.semanticFrame);
if (isHeuristicCandidatesIntent(input.intent)) { if (isHeuristicCandidatesIntent(input.intent)) {
return { return {
requested_result_mode: requestedResultMode, requested_result_mode: requestedResultMode,
@ -1897,6 +1975,10 @@ function shouldBoostAutoBroadenedLimit(intent: AddressIntent): boolean {
); );
} }
function shouldClearAsOfDateForHistoryRecovery(intent: AddressIntent): boolean {
return intent === "inventory_purchase_provenance_for_item" || intent === "inventory_purchase_documents_for_item";
}
function invertSort(sort: AddressFilterSet["sort"]): AddressFilterSet["sort"] { function invertSort(sort: AddressFilterSet["sort"]): AddressFilterSet["sort"] {
return sort === "period_asc" ? "period_desc" : "period_asc"; return sort === "period_asc" ? "period_desc" : "period_asc";
} }
@ -2609,16 +2691,18 @@ function buildLimitedExecutionResult(input: {
capabilityAudit?: AddressCapabilityAudit; capabilityAudit?: AddressCapabilityAudit;
shadowRouteAudit?: AddressShadowRouteAudit; shadowRouteAudit?: AddressShadowRouteAudit;
routeExpectationAudit?: AddressRouteExpectationAuditState; routeExpectationAudit?: AddressRouteExpectationAuditState;
semanticFrame?: AddressSemanticFrame | null;
}): AddressExecutionResult { }): AddressExecutionResult {
const accountScopeAudit = input.accountScopeAudit ?? buildDefaultAccountScopeAudit(input.filters); const accountScopeAudit = input.accountScopeAudit ?? buildDefaultAccountScopeAudit(input.filters);
const resultSemantics = deriveAddressResultSemantics({ const resultSemantics = deriveAddressResultSemantics({
intent: input.intent.intent, intent: input.intent.intent,
selectedRecipe: input.selectedRecipe, selectedRecipe: input.selectedRecipe,
filters: input.filters, filters: input.filters,
semanticFrame: input.semanticFrame,
responseType: "LIMITED_WITH_REASON", responseType: "LIMITED_WITH_REASON",
rowsMatched: input.rowsMatched rowsMatched: input.rowsMatched
}); });
const requestedResultMode = resolveRequestedResultMode(input.intent.intent, input.filters); const requestedResultMode = resolveRequestedResultMode(input.intent.intent, input.filters, input.semanticFrame);
const reasonsWithConfirmedFallback = withConfirmedBalanceFallbackReason( const reasonsWithConfirmedFallback = withConfirmedBalanceFallbackReason(
input.reasons, input.reasons,
requestedResultMode, requestedResultMode,
@ -2698,6 +2782,7 @@ function buildLimitedExecutionResult(input: {
account_scope_drop_reason: accountScopeAudit.accountScopeDropReason, account_scope_drop_reason: accountScopeAudit.accountScopeDropReason,
runtime_readiness: runtimeReadinessForLimitedCategory(input.category), runtime_readiness: runtimeReadinessForLimitedCategory(input.category),
limited_reason_category: input.category, limited_reason_category: input.category,
semantic_frame: input.semanticFrame ?? null,
response_type: "LIMITED_WITH_REASON", response_type: "LIMITED_WITH_REASON",
capability_id: input.capabilityAudit?.capabilityId ?? null, capability_id: input.capabilityAudit?.capabilityId ?? null,
capability_layer: input.capabilityAudit?.layer ?? null, capability_layer: input.capabilityAudit?.layer ?? null,
@ -2726,11 +2811,12 @@ export class AddressQueryService {
} }
const followupContext = options.followupContext ?? null; const followupContext = options.followupContext ?? null;
const decompose = runAddressDecomposeStage(userMessage, followupContext); const decompose = runAddressDecomposeStage(userMessage, followupContext, options.llmSemanticHints ?? null);
if (!decompose) { if (!decompose) {
return null; return null;
} }
const { mode, shape, intent, filters } = decompose; const { mode, shape, intent, filters } = decompose;
const semanticFrame = filters.semantic_frame ?? null;
const baseReasons = [...decompose.baseReasons]; const baseReasons = [...decompose.baseReasons];
const analysisDate = normalizeAnalysisDateHint(options.analysisDateHint); const analysisDate = normalizeAnalysisDateHint(options.analysisDateHint);
if (analysisDate) { if (analysisDate) {
@ -2748,7 +2834,16 @@ export class AddressQueryService {
baseReasons.push("as_of_date_from_analysis_context"); baseReasons.push("as_of_date_from_analysis_context");
} }
} }
const requestedResultMode = resolveRequestedResultMode(intent.intent, filters.extracted_filters); const resolvedOrganizationFromMessage = applyPreExecutionOrganizationScopeGrounding({
userMessage,
filters: filters.extracted_filters,
semanticFrame,
warnings: filters.warnings,
baseReasons,
activeOrganization: options.activeOrganization ?? null,
knownOrganizations: options.knownOrganizations ?? []
});
const requestedResultMode = resolveRequestedResultMode(intent.intent, filters.extracted_filters, semanticFrame);
const confirmedBalancePayablesIntent = const confirmedBalancePayablesIntent =
(intent.intent === "list_payables_counterparties" || intent.intent === "payables_confirmed_as_of_date") && (intent.intent === "list_payables_counterparties" || intent.intent === "payables_confirmed_as_of_date") &&
requestedResultMode === "confirmed_balance"; requestedResultMode === "confirmed_balance";
@ -2771,7 +2866,7 @@ export class AddressQueryService {
const inventoryConfirmedExecution = confirmedBalanceInventoryIntent const inventoryConfirmedExecution = confirmedBalanceInventoryIntent
? resolveExecutionFiltersForConfirmedBalance(filters.extracted_filters, analysisDate) ? resolveExecutionFiltersForConfirmedBalance(filters.extracted_filters, analysisDate)
: null; : null;
const executionFilters = let executionFilters =
inventoryConfirmedExecution?.executionFilters ?? inventoryConfirmedExecution?.executionFilters ??
payablesConfirmedExecution?.executionFilters ?? payablesConfirmedExecution?.executionFilters ??
receivablesConfirmedExecution?.executionFilters ?? receivablesConfirmedExecution?.executionFilters ??
@ -2847,6 +2942,7 @@ export class AddressQueryService {
...baseReasons, ...baseReasons,
FEATURE_ASSISTANT_CAPABILITY_ROUTE_GUARD_V1 ? "capability_route_guard_blocked" : "capability_route_guard_skipped" FEATURE_ASSISTANT_CAPABILITY_ROUTE_GUARD_V1 ? "capability_route_guard_blocked" : "capability_route_guard_skipped"
], ],
semanticFrame,
capabilityAudit, capabilityAudit,
shadowRouteAudit shadowRouteAudit
}); });
@ -2949,6 +3045,7 @@ export class AddressQueryService {
nextStep: "могу проверить близкие сценарии: документы/платежи по контрагенту, договоры или остаток по счету", nextStep: "могу проверить близкие сценарии: документы/платежи по контрагенту, договоры или остаток по счету",
limitations: ["intent_not_supported_in_v1"], limitations: ["intent_not_supported_in_v1"],
reasons: baseReasons, reasons: baseReasons,
semanticFrame,
capabilityAudit, capabilityAudit,
shadowRouteAudit shadowRouteAudit
}); });
@ -2971,6 +3068,7 @@ export class AddressQueryService {
nextStep: "можно выбрать близкий поддерживаемый сценарий или переключить запрос в режим расширенной проверки", nextStep: "можно выбрать близкий поддерживаемый сценарий или переключить запрос в режим расширенной проверки",
limitations: ["recipe_not_available"], limitations: ["recipe_not_available"],
reasons: [...baseReasons, ...recipeSelection.selection_reason], reasons: [...baseReasons, ...recipeSelection.selection_reason],
semanticFrame,
capabilityAudit, capabilityAudit,
shadowRouteAudit shadowRouteAudit
}); });
@ -2993,6 +3091,7 @@ export class AddressQueryService {
nextStep: `уточните: ${recipeSelection.missing_required_filters.join(", ")}`, nextStep: `уточните: ${recipeSelection.missing_required_filters.join(", ")}`,
limitations: ["missing_required_filters"], limitations: ["missing_required_filters"],
reasons: [...baseReasons, ...recipeSelection.selection_reason], reasons: [...baseReasons, ...recipeSelection.selection_reason],
semanticFrame,
capabilityAudit, capabilityAudit,
shadowRouteAudit shadowRouteAudit
}); });
@ -3015,6 +3114,7 @@ export class AddressQueryService {
nextStep: "включите FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1", nextStep: "включите FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1",
limitations: ["address_live_lane_disabled"], limitations: ["address_live_lane_disabled"],
reasons: baseReasons, reasons: baseReasons,
semanticFrame,
capabilityAudit, capabilityAudit,
shadowRouteAudit shadowRouteAudit
}); });
@ -3200,6 +3300,7 @@ export class AddressQueryService {
nextStep: mcp.error, nextStep: mcp.error,
limitations: ["mcp_call_failed"], limitations: ["mcp_call_failed"],
reasons: [...baseReasons, mcp.error], reasons: [...baseReasons, mcp.error],
semanticFrame,
capabilityAudit, capabilityAudit,
shadowRouteAudit shadowRouteAudit
}); });
@ -3214,7 +3315,7 @@ export class AddressQueryService {
scopedRows.length === 0; scopedRows.length === 0;
const normalizedRows = accountScopeFallbackApplied ? normalizedRawRows : scopedRows; const normalizedRows = accountScopeFallbackApplied ? normalizedRawRows : scopedRows;
anchor = refineAnchorFromRows(anchor, normalizedRows); anchor = refineAnchorFromRows(anchor, normalizedRows);
const filtersForMatching: AddressFilterSet = let filtersForMatching: AddressFilterSet =
anchor.anchor_type === "counterparty" && anchor.anchor_value_resolved anchor.anchor_type === "counterparty" && anchor.anchor_value_resolved
? { ...executionFilters, counterparty: anchor.anchor_value_resolved } ? { ...executionFilters, counterparty: anchor.anchor_value_resolved }
: anchor.anchor_type === "contract" && anchor.anchor_value_resolved : anchor.anchor_type === "contract" && anchor.anchor_value_resolved
@ -3227,15 +3328,65 @@ export class AddressQueryService {
rowsBeforeScope: normalizedRawRows.length, rowsBeforeScope: normalizedRawRows.length,
rowsAfterScope: normalizedRows.length rowsAfterScope: normalizedRows.length
}); });
const anchorFilter = applyAddressFilters(normalizedRows, filtersForMatching); let anchorFilter = applyAddressFilters(normalizedRows, filtersForMatching);
const filterByAnchors = anchorFilter.rows; let filterByAnchors = anchorFilter.rows;
const filteredRowsBeforeFutureGuard = applyIntentSpecificFilter(intent.intent, filterByAnchors); let filteredRowsBeforeFutureGuard = applyIntentSpecificFilter(intent.intent, filterByAnchors);
const filteredRowsFutureGuard = applyFutureDatedRowsGuard( let filteredRowsFutureGuard = applyFutureDatedRowsGuard(
filteredRowsBeforeFutureGuard, filteredRowsBeforeFutureGuard,
intent.intent, intent.intent,
futureGuardReferenceDate futureGuardReferenceDate
); );
const filteredRows = filteredRowsFutureGuard.rows; let filteredRows = filteredRowsFutureGuard.rows;
let organizationWarehouseRecoveryApplied = false;
if (
filteredRows.length === 0 &&
anchorFilter.mismatchReason === "warehouse_anchor_not_matched_in_materialized_rows" &&
resolvedOrganizationFromMessage
) {
filters.extracted_filters = {
...filters.extracted_filters,
organization: resolvedOrganizationFromMessage
};
delete filters.extracted_filters.warehouse;
executionFilters = {
...executionFilters,
organization: resolvedOrganizationFromMessage
};
delete executionFilters.warehouse;
filtersForMatching = {
...filtersForMatching,
organization: resolvedOrganizationFromMessage
};
delete filtersForMatching.warehouse;
anchor = {
...anchor,
anchor_type: "organization",
anchor_value_raw: anchor.anchor_value_raw,
anchor_value_resolved: resolvedOrganizationFromMessage,
resolver_confidence: "medium"
};
if (semanticFrame) {
semanticFrame.scope_kind = "explicit_anchor";
semanticFrame.anchor_kind = "organization";
semanticFrame.anchor_value = resolvedOrganizationFromMessage;
}
if (!filters.warnings.includes("warehouse_anchor_regrounded_to_organization_scope")) {
filters.warnings.push("warehouse_anchor_regrounded_to_organization_scope");
}
if (!baseReasons.includes("warehouse_anchor_regrounded_to_organization_scope")) {
baseReasons.push("warehouse_anchor_regrounded_to_organization_scope");
}
anchorFilter = applyAddressFilters(normalizedRows, filtersForMatching);
filterByAnchors = anchorFilter.rows;
filteredRowsBeforeFutureGuard = applyIntentSpecificFilter(intent.intent, filterByAnchors);
filteredRowsFutureGuard = applyFutureDatedRowsGuard(
filteredRowsBeforeFutureGuard,
intent.intent,
futureGuardReferenceDate
);
filteredRows = filteredRowsFutureGuard.rows;
organizationWarehouseRecoveryApplied = filteredRows.length > 0;
}
if (filteredRowsFutureGuard.droppedCount > 0) { if (filteredRowsFutureGuard.droppedCount > 0) {
if (!filters.warnings.includes("future_rows_excluded_from_response")) { if (!filters.warnings.includes("future_rows_excluded_from_response")) {
filters.warnings.push("future_rows_excluded_from_response"); filters.warnings.push("future_rows_excluded_from_response");
@ -3263,6 +3414,11 @@ export class AddressQueryService {
: matchFailureStage === "materialized_but_filtered_out_by_recipe" : matchFailureStage === "materialized_but_filtered_out_by_recipe"
? "rows_filtered_out_by_intent_recipe_after_anchor_match" ? "rows_filtered_out_by_intent_recipe_after_anchor_match"
: null; : null;
if (organizationWarehouseRecoveryApplied) {
if (!baseReasons.includes("organization_scope_live_grounding_recovered_rows")) {
baseReasons.push("organization_scope_live_grounding_recovered_rows");
}
}
if (filteredRows.length === 0 && intent.intent === "list_documents_by_contract" && filterByAnchors.length > 0) { if (filteredRows.length === 0 && intent.intent === "list_documents_by_contract" && filterByAnchors.length > 0) {
const recoveredBankRows = applyIntentSpecificFilter("bank_operations_by_contract", filterByAnchors); const recoveredBankRows = applyIntentSpecificFilter("bank_operations_by_contract", filterByAnchors);
@ -3324,6 +3480,7 @@ export class AddressQueryService {
intent: intent.intent, intent: intent.intent,
selectedRecipe: effectiveRecipeId, selectedRecipe: effectiveRecipeId,
filters: filters.extracted_filters, filters: filters.extracted_filters,
semanticFrame,
responseType: factual.responseType, responseType: factual.responseType,
rowsMatched: recoveredRows.length rowsMatched: recoveredRows.length
}), }),
@ -3472,6 +3629,7 @@ export class AddressQueryService {
intent: intent.intent, intent: intent.intent,
selectedRecipe: expandedSelection.selected_recipe.recipe_id, selectedRecipe: expandedSelection.selected_recipe.recipe_id,
filters: filters.extracted_filters, filters: filters.extracted_filters,
semanticFrame,
responseType: expandedFactual.responseType, responseType: expandedFactual.responseType,
rowsMatched: expandedFilteredRows.length rowsMatched: expandedFilteredRows.length
}), }),
@ -3494,8 +3652,13 @@ export class AddressQueryService {
if (filteredRows.length === 0 && canAutoBroadenPeriodWindow(intent.intent, filters.extracted_filters)) { if (filteredRows.length === 0 && canAutoBroadenPeriodWindow(intent.intent, filters.extracted_filters)) {
const autoBroadenedFilters: AddressFilterSet = { ...filters.extracted_filters }; const autoBroadenedFilters: AddressFilterSet = { ...filters.extracted_filters };
const broadenedAdjustments: string[] = [];
delete autoBroadenedFilters.period_from; delete autoBroadenedFilters.period_from;
delete autoBroadenedFilters.period_to; delete autoBroadenedFilters.period_to;
if (stageStatus === "no_raw_rows" && shouldClearAsOfDateForHistoryRecovery(intent.intent) && toNonEmptyFilterValue(autoBroadenedFilters.as_of_date)) {
delete autoBroadenedFilters.as_of_date;
broadenedAdjustments.push("as_of_date_cleared_for_history_recovery");
}
if (shouldBoostAutoBroadenedLimit(intent.intent)) { if (shouldBoostAutoBroadenedLimit(intent.intent)) {
autoBroadenedFilters.limit = Math.max( autoBroadenedFilters.limit = Math.max(
ADDRESS_ANCHOR_RECOVERY_LIMIT, ADDRESS_ANCHOR_RECOVERY_LIMIT,
@ -3571,13 +3734,18 @@ export class AddressQueryService {
broadenedFilteredRows, broadenedFilteredRows,
composeOptionsFromFilters(autoBroadenedFilters) composeOptionsFromFilters(autoBroadenedFilters)
); );
const broadenedLimitations = [...filters.warnings, "period_window_auto_broadened_to_available_data"]; const broadenedLimitations = [
const broadenedReasons = [...baseReasons, "period_window_auto_broadened_to_available_data"]; ...filters.warnings,
...broadenedAdjustments,
"period_window_auto_broadened_to_available_data"
];
const broadenedReasons = [...baseReasons, ...broadenedAdjustments, "period_window_auto_broadened_to_available_data"];
const broadenedResultSemantics = mergeAddressResultSemantics( const broadenedResultSemantics = mergeAddressResultSemantics(
deriveAddressResultSemantics({ deriveAddressResultSemantics({
intent: intent.intent, intent: intent.intent,
selectedRecipe: broadenedSelection.selected_recipe.recipe_id, selectedRecipe: broadenedSelection.selected_recipe.recipe_id,
filters: filters.extracted_filters, filters: filters.extracted_filters,
semanticFrame,
responseType: broadenedFactual.responseType, responseType: broadenedFactual.responseType,
rowsMatched: broadenedFilteredRows.length rowsMatched: broadenedFilteredRows.length
}), }),
@ -3645,6 +3813,7 @@ export class AddressQueryService {
route_expectation_expected_requested_result_modes: route_expectation_expected_requested_result_modes:
broadenedRouteExpectationAudit.expectedRequestedResultModes, broadenedRouteExpectationAudit.expectedRequestedResultModes,
route_expectation_expected_result_modes: broadenedRouteExpectationAudit.expectedResultModes, route_expectation_expected_result_modes: broadenedRouteExpectationAudit.expectedResultModes,
semantic_frame: semanticFrame,
...broadenedResultSemantics, ...broadenedResultSemantics,
limitations: broadenedLimitations, limitations: broadenedLimitations,
reasons: withConfirmedBalanceFallbackReason( reasons: withConfirmedBalanceFallbackReason(
@ -3793,6 +3962,7 @@ export class AddressQueryService {
intent: intent.intent, intent: intent.intent,
selectedRecipe: historicalSelection.selected_recipe.recipe_id, selectedRecipe: historicalSelection.selected_recipe.recipe_id,
filters: filters.extracted_filters, filters: filters.extracted_filters,
semanticFrame,
responseType: historicalFactual.responseType, responseType: historicalFactual.responseType,
rowsMatched: historicalFilteredRows.length rowsMatched: historicalFilteredRows.length
}), }),
@ -3879,6 +4049,7 @@ export class AddressQueryService {
intent: intent.intent, intent: intent.intent,
selectedRecipe: effectiveRecipeId, selectedRecipe: effectiveRecipeId,
filters: filters.extracted_filters, filters: filters.extracted_filters,
semanticFrame,
responseType: fallbackFactual.responseType, responseType: fallbackFactual.responseType,
rowsMatched: documentBankFallbackRows.length rowsMatched: documentBankFallbackRows.length
}), }),
@ -4016,6 +4187,7 @@ export class AddressQueryService {
nextStep, nextStep,
limitations, limitations,
reasons: baseReasons, reasons: baseReasons,
semanticFrame,
capabilityAudit, capabilityAudit,
shadowRouteAudit shadowRouteAudit
}); });
@ -4059,6 +4231,7 @@ export class AddressQueryService {
intent: composeIntent, intent: composeIntent,
selectedRecipe: effectiveRecipeId, selectedRecipe: effectiveRecipeId,
filters: filters.extracted_filters, filters: filters.extracted_filters,
semanticFrame,
responseType: factual.responseType, responseType: factual.responseType,
rowsMatched: filteredRows.length rowsMatched: filteredRows.length
}), }),
@ -4098,6 +4271,7 @@ export class AddressQueryService {
nextStep: "проверьте intent/recipe mapping или отключите FEATURE_ASSISTANT_ROUTE_EXPECTATION_HARD_GUARD_V1 для безопасного rollout", nextStep: "проверьте intent/recipe mapping или отключите FEATURE_ASSISTANT_ROUTE_EXPECTATION_HARD_GUARD_V1 для безопасного rollout",
limitations: ["route_expectation_mismatch_guard_blocked"], limitations: ["route_expectation_mismatch_guard_blocked"],
reasons: [...baseReasons, `route_expectation_mismatch:${finalRouteExpectationAudit.reason}`], reasons: [...baseReasons, `route_expectation_mismatch:${finalRouteExpectationAudit.reason}`],
semanticFrame,
capabilityAudit, capabilityAudit,
shadowRouteAudit, shadowRouteAudit,
routeExpectationAudit: finalRouteExpectationAudit routeExpectationAudit: finalRouteExpectationAudit
@ -4150,6 +4324,7 @@ export class AddressQueryService {
: "specify as_of_date/counterparty or enable detailed settlement registers for exact confirmed balance", : "specify as_of_date/counterparty or enable detailed settlement registers for exact confirmed balance",
limitations: [`exact_${exactModeName}_mode_unconfirmed_output_blocked`], limitations: [`exact_${exactModeName}_mode_unconfirmed_output_blocked`],
reasons: [...baseReasons, `exact_${exactModeName}_mode_unconfirmed_output_blocked`], reasons: [...baseReasons, `exact_${exactModeName}_mode_unconfirmed_output_blocked`],
semanticFrame,
capabilityAudit, capabilityAudit,
shadowRouteAudit, shadowRouteAudit,
routeExpectationAudit: finalRouteExpectationAudit routeExpectationAudit: finalRouteExpectationAudit
@ -4214,6 +4389,7 @@ export class AddressQueryService {
route_expectation_expected_selected_recipes: finalRouteExpectationAudit.expectedSelectedRecipes, route_expectation_expected_selected_recipes: finalRouteExpectationAudit.expectedSelectedRecipes,
route_expectation_expected_requested_result_modes: finalRouteExpectationAudit.expectedRequestedResultModes, route_expectation_expected_requested_result_modes: finalRouteExpectationAudit.expectedRequestedResultModes,
route_expectation_expected_result_modes: finalRouteExpectationAudit.expectedResultModes, route_expectation_expected_result_modes: finalRouteExpectationAudit.expectedResultModes,
semantic_frame: semanticFrame,
...factualResultSemantics, ...factualResultSemantics,
limitations: factualLimitations, limitations: factualLimitations,
reasons: withConfirmedBalanceFallbackReason( reasons: withConfirmedBalanceFallbackReason(

View File

@ -3,19 +3,45 @@
AddressIntent, AddressIntent,
AddressIntentResolution, AddressIntentResolution,
AddressModeDetection, AddressModeDetection,
AddressQueryShapeDetection AddressQueryShapeDetection,
AddressSemanticFrame
} from "../../types/addressQuery"; } from "../../types/addressQuery";
import { detectAddressQuestionMode } from "../addressQueryClassifier"; import { detectAddressQuestionMode } from "../addressQueryClassifier";
import { classifyAddressQueryShape } from "../addressQueryShapeClassifier"; import { classifyAddressQueryShape } from "../addressQueryShapeClassifier";
import { resolveAddressIntent } from "../addressIntentResolver"; import { resolveAddressIntent } from "../addressIntentResolver";
import { extractAddressFilters } from "../addressFilterExtractor"; import { extractAddressFilters } from "../addressFilterExtractor";
import { applyAddressLlmSemanticHintsToExtraction } from "./semanticHintOverlay";
import type { AddressLlmSemanticHints } from "../../types/addressQuery";
export interface AddressFollowupContext { export interface AddressFollowupContext {
previous_intent?: AddressIntent; previous_intent?: AddressIntent;
previous_filters?: AddressFilterSet; previous_filters?: AddressFilterSet;
previous_anchor_type?: "account" | "counterparty" | "contract" | "document_ref" | "item" | "warehouse" | "unknown" | null; previous_anchor_type?:
| "account"
| "counterparty"
| "contract"
| "document_ref"
| "item"
| "organization"
| "warehouse"
| "unknown"
| null;
previous_anchor_value?: string | null; previous_anchor_value?: string | null;
resolved_counterparty_from_display?: boolean; resolved_counterparty_from_display?: boolean;
root_intent?: AddressIntent;
root_filters?: AddressFilterSet;
root_anchor_type?:
| "account"
| "counterparty"
| "contract"
| "document_ref"
| "item"
| "organization"
| "warehouse"
| "unknown"
| null;
root_anchor_value?: string | null;
current_frame_kind?: "generic" | "inventory_root" | "inventory_drilldown";
} }
export interface AddressDecomposeStageResult { export interface AddressDecomposeStageResult {
@ -26,6 +52,7 @@ export interface AddressDecomposeStageResult {
extracted_filters: AddressFilterSet; extracted_filters: AddressFilterSet;
missing_required_filters: string[]; missing_required_filters: string[];
warnings: string[]; warnings: string[];
semantic_frame?: AddressSemanticFrame;
}; };
baseReasons: string[]; baseReasons: string[];
} }
@ -318,6 +345,159 @@ function isInventoryIntent(intent: AddressIntent | undefined): boolean {
); );
} }
function isInventoryRootFrameIntent(intent: AddressIntent | undefined): boolean {
return intent === "inventory_on_hand_as_of_date";
}
function isInventoryDrilldownFrameIntent(intent: AddressIntent | undefined): boolean {
return (
intent === "inventory_purchase_provenance_for_item" ||
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date"
);
}
function buildInventoryRootFollowupContext(
followupContext: AddressFollowupContext | null
): AddressFollowupContext | null {
if (!followupContext || !followupContext.root_intent || !followupContext.root_filters) {
return followupContext;
}
return {
...followupContext,
previous_intent: followupContext.root_intent,
previous_filters: { ...followupContext.root_filters },
previous_anchor_type: followupContext.root_anchor_type ?? followupContext.previous_anchor_type,
previous_anchor_value: followupContext.root_anchor_value ?? followupContext.previous_anchor_value,
current_frame_kind: "inventory_root"
};
}
function getTokenCount(text: string): number {
return String(text ?? "")
.trim()
.split(/\s+/)
.filter(Boolean).length;
}
function resolveMonthNumberFromText(text: string): number | null {
const normalized = String(text ?? "").toLowerCase();
if (!normalized) {
return null;
}
if (/январ|january|jan/iu.test(normalized)) return 1;
if (/феврал|february|feb/iu.test(normalized)) return 2;
if (/март|march|mar/iu.test(normalized)) return 3;
if (/апрел|april|apr/iu.test(normalized)) return 4;
if (/(?:^|[\s,.;:!?()\-])ма(?:й|е|я)(?=$|[\s,.;:!?()\-])|may/iu.test(normalized)) return 5;
if (/июн|june|jun/iu.test(normalized)) return 6;
if (/июл|july|jul/iu.test(normalized)) return 7;
if (/август|august|aug/iu.test(normalized)) return 8;
if (/сентябр|september|sep/iu.test(normalized)) return 9;
if (/октябр|october|oct/iu.test(normalized)) return 10;
if (/ноябр|november|nov/iu.test(normalized)) return 11;
if (/декабр|december|dec/iu.test(normalized)) return 12;
return null;
}
function resolveYearFromFilters(filters: AddressFilterSet | null | undefined): number | null {
const candidates = [
toNonEmptyString(filters?.as_of_date),
toNonEmptyString(filters?.period_to),
toNonEmptyString(filters?.period_from)
];
for (const candidate of candidates) {
const match = candidate?.match(/\b((?:19|20)\d{2})\b/u);
if (match) {
const year = Number(match[1]);
if (Number.isFinite(year)) {
return year;
}
}
}
return null;
}
function hasRelativeYearHint(text: string): boolean {
return /(?:эт(?:от|ого)(?:\s+же)?\s+год|этого\s+же\s+года|того\s+же\s+года|this\s+year|same\s+year|that\s+year)/iu.test(
String(text ?? "")
);
}
function resolveRelativeMonthPeriodFromInventoryRoot(
userMessage: string,
followupContext: AddressFollowupContext | null
): { period_from: string; period_to: string; as_of_date: string } | null {
if (!followupContext || !isInventoryRootFrameIntent(followupContext.root_intent)) {
return null;
}
const month = resolveMonthNumberFromText(userMessage);
if (!month) {
return null;
}
const normalized = String(userMessage ?? "");
if (hasExplicitPeriodLiteral(normalized) || hasExplicitCurrentDateHint(normalized)) {
return null;
}
const shortTemporalPatch = getTokenCount(normalized) <= 8 || hasRelativeYearHint(normalized);
if (!shortTemporalPatch) {
return null;
}
const year = resolveYearFromFilters(followupContext.root_filters);
if (!year) {
return null;
}
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
const periodFrom = `${year}-${String(month).padStart(2, "0")}-01`;
const periodTo = `${year}-${String(month).padStart(2, "0")}-${String(lastDay).padStart(2, "0")}`;
return {
period_from: periodFrom,
period_to: periodTo,
as_of_date: periodTo
};
}
function shouldRestoreInventoryRootFrame(
userMessage: string,
intent: AddressIntent,
extractedFilters: AddressFilterSet,
followupContext: AddressFollowupContext | null
): boolean {
if (!followupContext || !isInventoryRootFrameIntent(followupContext.root_intent)) {
return false;
}
const currentFrameKind = followupContext.current_frame_kind ?? null;
const previousIntent = followupContext.previous_intent;
const comingFromInventoryDrilldown =
currentFrameKind === "inventory_drilldown" || isInventoryDrilldownFrameIntent(previousIntent);
if (!comingFromInventoryDrilldown) {
return false;
}
const normalized = String(userMessage ?? "");
if (
hasSelectedObjectInventorySignal(normalized) ||
hasInventorySupplierFollowupCue(normalized) ||
hasInventoryPurchaseDocumentsFollowupCue(normalized) ||
hasInventoryPurchaseDateFollowupCue(normalized) ||
hasBareInventoryPurchaseDateFollowupCue(normalized) ||
hasInventorySaleFollowupCue(normalized) ||
hasInventoryPurchaseToSaleChainFollowupCue(normalized)
) {
return false;
}
if (intent === "inventory_on_hand_as_of_date") {
return true;
}
const hasTemporalPatch =
hasExplicitPeriodWindow(extractedFilters) ||
Boolean(toNonEmptyString(extractedFilters.as_of_date)) ||
hasExplicitPeriodLiteral(normalized) ||
Boolean(resolveRelativeMonthPeriodFromInventoryRoot(normalized, followupContext));
return hasTemporalPatch;
}
function hasSelectedObjectInventorySignal(text: string): boolean { function hasSelectedObjectInventorySignal(text: string): boolean {
return /(?:по\s+выбранному\s+объекту|for\s+selected\s+object)/iu.test(String(text ?? "")); return /(?:по\s+выбранному\s+объекту|for\s+selected\s+object)/iu.test(String(text ?? ""));
} }
@ -456,6 +636,7 @@ function mergeFollowupFilters(
const previousAsOfDate = toNonEmptyString(previous.as_of_date); const previousAsOfDate = toNonEmptyString(previous.as_of_date);
const previousPeriodFrom = toNonEmptyString(previous.period_from); const previousPeriodFrom = toNonEmptyString(previous.period_from);
const previousPeriodTo = toNonEmptyString(previous.period_to); const previousPeriodTo = toNonEmptyString(previous.period_to);
const relativeMonthFromInventoryRoot = resolveRelativeMonthPeriodFromInventoryRoot(userMessage, followupContext);
const allTimeRequested = hasAllTimeHint(userMessage); const allTimeRequested = hasAllTimeHint(userMessage);
const sameDateRequested = hasSameDateHint(userMessage); const sameDateRequested = hasSameDateHint(userMessage);
if (!toNonEmptyString(merged.organization) && previousOrganization) { if (!toNonEmptyString(merged.organization) && previousOrganization) {
@ -648,6 +829,15 @@ function mergeFollowupFilters(
reasons.push("as_of_date_from_open_items_followup_context"); reasons.push("as_of_date_from_open_items_followup_context");
} }
} }
if (
relativeMonthFromInventoryRoot &&
(intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date")
) {
merged.period_from = relativeMonthFromInventoryRoot.period_from;
merged.period_to = relativeMonthFromInventoryRoot.period_to;
merged.as_of_date = relativeMonthFromInventoryRoot.as_of_date;
reasons.push("period_derived_from_inventory_root_frame_year");
}
if (intent === "inventory_aging_by_purchase_date") { if (intent === "inventory_aging_by_purchase_date") {
const explicitItemMention = /(?:^|[\s,.;:!?()\-\u2014])(?:товар(?:у|а|ом)?|позици(?:и|я|ю)|item|row|line)(?=$|[\s,.;:!?()\-\u2014])/iu.test( const explicitItemMention = /(?:^|[\s,.;:!?()\-\u2014])(?:товар(?:у|а|ом)?|позици(?:и|я|ю)|item|row|line)(?=$|[\s,.;:!?()\-\u2014])/iu.test(
String(userMessage ?? "") String(userMessage ?? "")
@ -1016,7 +1206,8 @@ function deriveIntentWithFollowupContext(
export function runAddressDecomposeStage( export function runAddressDecomposeStage(
userMessage: string, userMessage: string,
followupContext: AddressFollowupContext | null followupContext: AddressFollowupContext | null,
llmSemanticHints: AddressLlmSemanticHints | null = null
): AddressDecomposeStageResult | null { ): AddressDecomposeStageResult | null {
const detectedMode = detectAddressQuestionMode(userMessage); const detectedMode = detectAddressQuestionMode(userMessage);
const shape = classifyAddressQueryShape(userMessage); const shape = classifyAddressQueryShape(userMessage);
@ -1047,18 +1238,48 @@ export function runAddressDecomposeStage(
if (mode.mode !== "address_query") { if (mode.mode !== "address_query") {
return null; return null;
} }
const intent = deriveIntentWithFollowupContext(detectedIntent, userMessage, followupContext); let effectiveFollowupContext = followupContext;
const extractedFilters = extractAddressFilters(userMessage, intent.intent); let intent = deriveIntentWithFollowupContext(detectedIntent, userMessage, effectiveFollowupContext);
const followupMerged = mergeFollowupFilters(extractedFilters.extracted_filters, intent.intent, userMessage, followupContext); let extractedFilters = applyAddressLlmSemanticHintsToExtraction(
extractAddressFilters(userMessage, intent.intent),
llmSemanticHints
);
if (
shouldRestoreInventoryRootFrame(
userMessage,
intent.intent,
extractedFilters.extracted_filters,
effectiveFollowupContext
)
) {
effectiveFollowupContext = buildInventoryRootFollowupContext(effectiveFollowupContext);
intent = {
intent: effectiveFollowupContext?.root_intent ?? "inventory_on_hand_as_of_date",
confidence: "low",
reasons: [...intent.reasons, "intent_restored_to_inventory_root_frame"]
};
extractedFilters = applyAddressLlmSemanticHintsToExtraction(
extractAddressFilters(userMessage, intent.intent),
llmSemanticHints
);
}
const followupMerged = mergeFollowupFilters(
extractedFilters.extracted_filters,
intent.intent,
userMessage,
effectiveFollowupContext
);
const filters = { const filters = {
extracted_filters: followupMerged.filters, extracted_filters: followupMerged.filters,
missing_required_filters: resolveMissingRequiredFilters(intent.intent, followupMerged.filters), missing_required_filters: resolveMissingRequiredFilters(intent.intent, followupMerged.filters),
warnings: [...new Set([...extractedFilters.warnings, ...followupMerged.reasons])] warnings: [...new Set([...extractedFilters.warnings, ...followupMerged.reasons])],
semantic_frame: extractedFilters.semantic_frame
}; };
const followupContextApplied = const followupContextApplied =
Boolean(followupContext) && Boolean(effectiveFollowupContext) &&
(mode.reasons.includes("address_mode_from_followup_context") || (mode.reasons.includes("address_mode_from_followup_context") ||
intent.reasons.includes("intent_from_followup_context") || intent.reasons.includes("intent_from_followup_context") ||
intent.reasons.includes("intent_restored_to_inventory_root_frame") ||
followupMerged.reasons.length > 0); followupMerged.reasons.length > 0);
const baseReasons = [ const baseReasons = [
...mode.reasons, ...mode.reasons,

View File

@ -1,8 +1,16 @@
import type { AddressFilterSet, AddressIntent, AddressQuestionMode, AddressQueryShape } from "../../types/addressQuery"; import type {
AddressLlmSemanticHints,
AddressFilterSet,
AddressIntent,
AddressQuestionMode,
AddressQueryShape,
AddressSemanticFrame
} from "../../types/addressQuery";
import { detectAddressQuestionMode } from "../addressQueryClassifier"; import { detectAddressQuestionMode } from "../addressQueryClassifier";
import { classifyAddressQueryShape } from "../addressQueryShapeClassifier"; import { classifyAddressQueryShape } from "../addressQueryShapeClassifier";
import { resolveAddressIntent } from "../addressIntentResolver"; import { resolveAddressIntent } from "../addressIntentResolver";
import { extractAddressFilters } from "../addressFilterExtractor"; import { extractAddressFilters } from "../addressFilterExtractor";
import { applyAddressLlmSemanticHintsToExtraction } from "./semanticHintOverlay";
export type AddressPredecomposePeriodScope = "all_time" | "year" | "range" | "as_of" | "unspecified"; export type AddressPredecomposePeriodScope = "all_time" | "year" | "range" | "as_of" | "unspecified";
@ -40,6 +48,7 @@ export interface AddressLlmPredecomposeContractV1 {
as_of_date: string | null; as_of_date: string | null;
has_explicit_period: boolean; has_explicit_period: boolean;
}; };
semantics: AddressSemanticFrame;
aggregation_profile: AddressPredecomposeAggregationProfile; aggregation_profile: AddressPredecomposeAggregationProfile;
} }
@ -59,6 +68,7 @@ export interface AddressSemanticExtractionContractV1 {
}; };
entities: AddressLlmPredecomposeContractV1["entities"]; entities: AddressLlmPredecomposeContractV1["entities"];
period: AddressLlmPredecomposeContractV1["period"]; period: AddressLlmPredecomposeContractV1["period"];
semantics: AddressLlmPredecomposeContractV1["semantics"];
guard_hints: { guard_hints: {
source_data_signal_detected: boolean; source_data_signal_detected: boolean;
canonical_data_signal_detected: boolean; canonical_data_signal_detected: boolean;
@ -75,7 +85,7 @@ export interface AddressSemanticExtractionContractV1 {
} }
const ADDRESS_SEMANTIC_DATA_SIGNAL_PATTERN = const ADDRESS_SEMANTIC_DATA_SIGNAL_PATTERN =
/(?:\u0434\u043e\u043a|\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442|\u0441\u0447(?:\u0435|\u0451)\u0442|\u0441\u0430\u043b\u044c\u0434\u043e|\u043e\u0431\u043e\u0440\u043e\u0442|\u043f\u043b\u0430\u0442(?:\u0435|\u0451)\u0436|\u043e\u043f\u0435\u0440\u0430\u0446|\u043f\u0435\u0440\u0438\u043e\u0434|\u0433\u043e\u0434|counterparty|contract|document|account|balance|turnover|operations?|doki|doky|dokument|dogovor|kontragent|schet|saldo|platezh|oplata)/iu; /(?:\u0434\u043e\u043a|\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442|\u0441\u0447(?:\u0435|\u0451)\u0442|\u0441\u0430\u043b\u044c\u0434\u043e|\u043e\u0431\u043e\u0440\u043e\u0442|\u043f\u043b\u0430\u0442(?:\u0435|\u0451)\u0436|\u043e\u043f\u0435\u0440\u0430\u0446|\u043f\u0435\u0440\u0438\u043e\u0434|\u0433\u043e\u0434|\u0441\u043a\u043b\u0430\u0434|\u0442\u043e\u0432\u0430\u0440|\u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442\u0443\u0440|counterparty|contract|document|account|balance|turnover|operations?|warehouse|stock|inventory|item|goods|doki|doky|dokument|dogovor|kontragent|schet|saldo|platezh|oplata)/iu;
const ADDRESS_SEMANTIC_ENTITY_SIGNAL_PATTERN = const ADDRESS_SEMANTIC_ENTITY_SIGNAL_PATTERN =
/(?:\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u043a\u043e\u043d\u0442\u043e\u0440|customer|supplier|counterparty|company|vendor|client)/iu; /(?:\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u043a\u043e\u043d\u0442\u043e\u0440|customer|supplier|counterparty|company|vendor|client)/iu;
@ -84,7 +94,7 @@ const ADDRESS_SEMANTIC_SCOPE_META_PATTERN =
/(?:\u043a\u0430\u043a\u0430\u044f\s+\u0431\u0430\u0437\u0430|\u0431\u0430\u0437\u0430\s+\u043a\u0430\u043a\u043e\u0439\s+\u043a\u043e\u043d\u0442\u043e\u0440|\u043f\u043e\s+\u043a\u0430\u043a\u0438\u043c\s+\u043a\u043e\u043d\u0442\u043e\u0440|which\s+company\s+base|which\s+tenant|data\s+scope)/iu; /(?:\u043a\u0430\u043a\u0430\u044f\s+\u0431\u0430\u0437\u0430|\u0431\u0430\u0437\u0430\s+\u043a\u0430\u043a\u043e\u0439\s+\u043a\u043e\u043d\u0442\u043e\u0440|\u043f\u043e\s+\u043a\u0430\u043a\u0438\u043c\s+\u043a\u043e\u043d\u0442\u043e\u0440|which\s+company\s+base|which\s+tenant|data\s+scope)/iu;
const ADDRESS_SEMANTIC_DEEP_INVESTIGATION_PATTERN = const ADDRESS_SEMANTIC_DEEP_INVESTIGATION_PATTERN =
/(?:\u043f\u0440\u043e\u0432\u0435\u0440(?:\u044c|\u0438\u0442\u044c)|\u0440\u0430\u0437\u0431\u0435\u0440(?:\u0438|\u0430\u0442\u044c)|\u043f\u043e\u0447\u0435\u043c\u0443|\u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c|\u0440\u0430\u0437\u0440\u044b\u0432|\u0445\u0432\u043e\u0441\u0442|root\s*cause|trace\s*chain|state\s+transition)/iu; /(?:\u043f\u043e\u0447\u0435\u043c\u0443|\u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c|\u0440\u0430\u0437\u0440\u044b\u0432|\u0445\u0432\u043e\u0441\u0442|root\s*cause|trace\s*chain|state\s+transition|\u043f\u0440\u043e\u0432\u0435\u0440(?:\u044c|\u0438\u0442\u044c).*(?:\u0445\u0432\u043e\u0441\u0442|\u0440\u0430\u0437\u0440\u044b\u0432|\u0437\u0430\u043a\u0440\u044b\u0442|\u0446\u0435\u043f\u043e\u0447|\u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c|\u043e\u0448\u0438\u0431|\u0430\u043d\u043e\u043c\u0430\u043b|\u0440\u0438\u0441\u043a|\u0441\u0432\u0435\u0440\u043a)|\u0440\u0430\u0437\u0431\u0435\u0440(?:\u0438|\u0430\u0442\u044c).*(?:\u043f\u043e\u0447\u0435\u043c\u0443|\u0445\u0432\u043e\u0441\u0442|\u0440\u0430\u0437\u0440\u044b\u0432|\u0437\u0430\u043a\u0440\u044b\u0442|\u0446\u0435\u043f\u043e\u0447|\u043e\u0448\u0438\u0431|\u0430\u043d\u043e\u043c\u0430\u043b|\u0440\u0438\u0441\u043a))/iu;
function normalizeCompact(value: unknown): string { function normalizeCompact(value: unknown): string {
return String(value ?? "") return String(value ?? "")
@ -232,6 +242,7 @@ function inferAggregationProfile(intent: AddressIntent, shape: AddressQueryShape
export function buildAddressLlmPredecomposeContractV1(input: { export function buildAddressLlmPredecomposeContractV1(input: {
sourceMessage: string; sourceMessage: string;
canonicalMessage: string; canonicalMessage: string;
semanticHints?: AddressLlmSemanticHints | null;
}): AddressLlmPredecomposeContractV1 { }): AddressLlmPredecomposeContractV1 {
const sourceMessage = String(input.sourceMessage ?? "").trim(); const sourceMessage = String(input.sourceMessage ?? "").trim();
const canonicalMessage = String(input.canonicalMessage ?? "").trim() || sourceMessage; const canonicalMessage = String(input.canonicalMessage ?? "").trim() || sourceMessage;
@ -239,8 +250,20 @@ export function buildAddressLlmPredecomposeContractV1(input: {
const mode = detectAddressQuestionMode(canonicalMessage); const mode = detectAddressQuestionMode(canonicalMessage);
const shape = classifyAddressQueryShape(canonicalMessage); const shape = classifyAddressQueryShape(canonicalMessage);
const intent = resolveAddressIntent(canonicalMessage); const intent = resolveAddressIntent(canonicalMessage);
const extraction = extractAddressFilters(canonicalMessage, intent.intent); const extraction = applyAddressLlmSemanticHintsToExtraction(
extractAddressFilters(canonicalMessage, intent.intent),
input.semanticHints ?? null
);
const filters = extraction.extracted_filters; const filters = extraction.extracted_filters;
const semanticFrame: AddressSemanticFrame = extraction.semantic_frame ?? {
scope_kind: "none",
anchor_kind: "none",
anchor_value: null,
date_scope_kind: "none",
date_basis_hint: null,
self_scope_detected: false,
selected_object_scope_detected: false
};
const periodScope = inferPeriodScope(filters, canonicalMessage); const periodScope = inferPeriodScope(filters, canonicalMessage);
return { return {
@ -266,10 +289,9 @@ export function buildAddressLlmPredecomposeContractV1(input: {
period_from: toNonEmptyString(filters.period_from), period_from: toNonEmptyString(filters.period_from),
period_to: toNonEmptyString(filters.period_to), period_to: toNonEmptyString(filters.period_to),
as_of_date: toNonEmptyString(filters.as_of_date), as_of_date: toNonEmptyString(filters.as_of_date),
has_explicit_period: Boolean( has_explicit_period: semanticFrame.date_scope_kind === "explicit"
toNonEmptyString(filters.as_of_date) || toNonEmptyString(filters.period_from) || toNonEmptyString(filters.period_to)
)
}, },
semantics: semanticFrame,
aggregation_profile: inferAggregationProfile(intent.intent, shape.shape) aggregation_profile: inferAggregationProfile(intent.intent, shape.shape)
}; };
} }
@ -370,6 +392,7 @@ export function buildAddressSemanticExtractionContractV1(input: {
as_of_date: predecomposeContract.period.as_of_date, as_of_date: predecomposeContract.period.as_of_date,
has_explicit_period: predecomposeContract.period.has_explicit_period has_explicit_period: predecomposeContract.period.has_explicit_period
}, },
semantics: predecomposeContract.semantics,
guard_hints: { guard_hints: {
source_data_signal_detected: sourceDataSignal, source_data_signal_detected: sourceDataSignal,
canonical_data_signal_detected: canonicalDataSignal, canonical_data_signal_detected: canonicalDataSignal,

View File

@ -16,7 +16,16 @@ const PARTY_ANCHOR_STOPWORDS = new Set([
]); ]);
export interface AnchorResolutionDebug { export interface AnchorResolutionDebug {
anchor_type: "account" | "counterparty" | "contract" | "document_ref" | "item" | "warehouse" | "unknown" | null; anchor_type:
| "account"
| "counterparty"
| "contract"
| "document_ref"
| "item"
| "warehouse"
| "organization"
| "unknown"
| null;
anchor_value_raw: string | null; anchor_value_raw: string | null;
anchor_value_resolved: string | null; anchor_value_resolved: string | null;
resolver_confidence: "high" | "medium" | "low" | null; resolver_confidence: "high" | "medium" | "low" | null;
@ -30,6 +39,7 @@ export interface ResolveStageRow {
analytics: string[]; analytics: string[];
item?: string | null; item?: string | null;
warehouse?: string | null; warehouse?: string | null;
organization?: string | null;
} }
function transliterateCyrillicToLatin(value: string): string { function transliterateCyrillicToLatin(value: string): string {
@ -175,6 +185,7 @@ export function resolvePrimaryAnchor(intent: AddressIntent, filters: AddressFilt
const contract = typeof filters.contract === "string" ? filters.contract.trim() : ""; const contract = typeof filters.contract === "string" ? filters.contract.trim() : "";
const item = typeof filters.item === "string" ? filters.item.trim() : ""; const item = typeof filters.item === "string" ? filters.item.trim() : "";
const warehouse = typeof filters.warehouse === "string" ? filters.warehouse.trim() : ""; const warehouse = typeof filters.warehouse === "string" ? filters.warehouse.trim() : "";
const organization = typeof filters.organization === "string" ? filters.organization.trim() : "";
const documentRef = typeof filters.document_ref === "string" ? filters.document_ref.trim() : ""; const documentRef = typeof filters.document_ref === "string" ? filters.document_ref.trim() : "";
if (intent === "account_balance_snapshot" || intent === "documents_forming_balance") { if (intent === "account_balance_snapshot" || intent === "documents_forming_balance") {
@ -260,6 +271,16 @@ export function resolvePrimaryAnchor(intent: AddressIntent, filters: AddressFilt
}; };
} }
if (organization) {
return {
anchor_type: "organization",
anchor_value_raw: organization,
anchor_value_resolved: organization,
resolver_confidence: "medium",
ambiguity_count: 0
};
}
if (documentRef) { if (documentRef) {
return { return {
anchor_type: "document_ref", anchor_type: "document_ref",
@ -287,7 +308,8 @@ export function refineAnchorFromRows(anchor: AnchorResolutionDebug, rows: Resolv
anchor.anchor_type !== "counterparty" && anchor.anchor_type !== "counterparty" &&
anchor.anchor_type !== "contract" && anchor.anchor_type !== "contract" &&
anchor.anchor_type !== "item" && anchor.anchor_type !== "item" &&
anchor.anchor_type !== "warehouse" anchor.anchor_type !== "warehouse" &&
anchor.anchor_type !== "organization"
) { ) {
return anchor; return anchor;
} }
@ -296,8 +318,16 @@ export function refineAnchorFromRows(anchor: AnchorResolutionDebug, rows: Resolv
return anchor; return anchor;
} }
const searchableRows = const searchableRows =
anchor.anchor_type === "item" || anchor.anchor_type === "warehouse" anchor.anchor_type === "item" || anchor.anchor_type === "warehouse" || anchor.anchor_type === "organization"
? rows.flatMap((row) => [row.registrator, row.item ?? "", row.warehouse ?? "", row.account_dt ?? "", row.account_kt ?? "", ...row.analytics]) ? rows.flatMap((row) => [
row.registrator,
row.item ?? "",
row.warehouse ?? "",
row.organization ?? "",
row.account_dt ?? "",
row.account_kt ?? "",
...row.analytics
])
: rows.flatMap((row) => row.analytics); : rows.flatMap((row) => row.analytics);
const candidates = uniqueStrings( const candidates = uniqueStrings(
searchableRows searchableRows

View File

@ -0,0 +1,168 @@
import type {
AddressAsOfDateBasis,
AddressFilterExtraction,
AddressLlmSemanticHints,
AddressSemanticFrame
} from "../../types/addressQuery";
function toNonEmptyString(value: unknown): string | null {
if (value === null || value === undefined) {
return null;
}
const normalized = String(value).trim();
return normalized.length > 0 ? normalized : null;
}
function normalizeToken(value: unknown): string {
return String(value ?? "")
.trim()
.toLowerCase()
.replace(/\s+/g, "_");
}
export function normalizeAddressLlmSemanticHints(value: unknown): AddressLlmSemanticHints | null {
if (!value || typeof value !== "object") {
return null;
}
const source = value as Record<string, unknown>;
const scopeToken = normalizeToken(source.scope_target_kind);
const dateToken = normalizeToken(source.date_scope_kind);
const scopeTargetKind: AddressLlmSemanticHints["scope_target_kind"] =
scopeToken === "self_scope" ||
scopeToken === "selected_object" ||
scopeToken === "organization" ||
scopeToken === "warehouse" ||
scopeToken === "counterparty" ||
scopeToken === "contract" ||
scopeToken === "item"
? (scopeToken as AddressLlmSemanticHints["scope_target_kind"])
: "none";
const dateScopeKind: AddressLlmSemanticHints["date_scope_kind"] =
dateToken === "explicit" || dateToken === "implicit_current" ? (dateToken as AddressLlmSemanticHints["date_scope_kind"]) : "missing";
return {
scope_target_kind: scopeTargetKind,
scope_target_text: toNonEmptyString(source.scope_target_text),
date_scope_kind: dateScopeKind,
self_scope_detected: source.self_scope_detected === true || scopeTargetKind === "self_scope",
selected_object_scope_detected:
source.selected_object_scope_detected === true || scopeTargetKind === "selected_object"
};
}
function defaultSemanticFrame(extraction: AddressFilterExtraction): AddressSemanticFrame {
return (
extraction.semantic_frame ?? {
scope_kind: "none",
anchor_kind: "none",
anchor_value: null,
date_scope_kind: "none",
date_basis_hint: null,
self_scope_detected: false,
selected_object_scope_detected: false
}
);
}
function pushWarning(warnings: string[], value: string): void {
if (!warnings.includes(value)) {
warnings.push(value);
}
}
function applyDateScopeHint(frame: AddressSemanticFrame, dateScopeKind: AddressLlmSemanticHints["date_scope_kind"]): void {
if (dateScopeKind === "explicit") {
frame.date_scope_kind = "explicit";
return;
}
if (dateScopeKind === "implicit_current" && frame.date_scope_kind !== "explicit") {
frame.date_scope_kind = "implicit_current";
frame.date_basis_hint = "implicit_current_snapshot" satisfies AddressAsOfDateBasis;
}
}
export function applyAddressLlmSemanticHintsToExtraction(
extraction: AddressFilterExtraction,
semanticHintsInput: unknown
): AddressFilterExtraction {
const semanticHints = normalizeAddressLlmSemanticHints(semanticHintsInput);
if (!semanticHints) {
return extraction;
}
const extractedFilters = { ...(extraction.extracted_filters ?? {}) };
const warnings = [...(Array.isArray(extraction.warnings) ? extraction.warnings : [])];
const semanticFrame = { ...defaultSemanticFrame(extraction) };
const scopeTargetText = semanticHints.scope_target_text;
applyDateScopeHint(semanticFrame, semanticHints.date_scope_kind);
if (semanticHints.self_scope_detected) {
semanticFrame.scope_kind = "implicit_self_scope";
semanticFrame.anchor_kind = "self_scope";
semanticFrame.anchor_value = null;
semanticFrame.self_scope_detected = true;
}
if (semanticHints.selected_object_scope_detected) {
if (semanticFrame.scope_kind === "none") {
semanticFrame.scope_kind = "selected_object_scope";
semanticFrame.anchor_kind = "selected_object";
semanticFrame.anchor_value = null;
}
semanticFrame.selected_object_scope_detected = true;
}
if (semanticHints.scope_target_kind === "organization" && scopeTargetText) {
extractedFilters.organization = scopeTargetText;
pushWarning(warnings, "organization_from_llm_semantics");
if (toNonEmptyString(extractedFilters.warehouse)) {
delete extractedFilters.warehouse;
pushWarning(warnings, "warehouse_cleared_by_llm_organization_semantics");
}
semanticFrame.scope_kind = "explicit_anchor";
semanticFrame.anchor_kind = "organization";
semanticFrame.anchor_value = scopeTargetText;
}
if (semanticHints.scope_target_kind === "warehouse" && scopeTargetText) {
extractedFilters.warehouse = scopeTargetText;
pushWarning(warnings, "warehouse_from_llm_semantics");
semanticFrame.scope_kind = "explicit_anchor";
semanticFrame.anchor_kind = "warehouse";
semanticFrame.anchor_value = scopeTargetText;
}
if (semanticHints.scope_target_kind === "counterparty" && scopeTargetText) {
extractedFilters.counterparty = scopeTargetText;
pushWarning(warnings, "counterparty_from_llm_semantics");
semanticFrame.scope_kind = "explicit_anchor";
semanticFrame.anchor_kind = "counterparty";
semanticFrame.anchor_value = scopeTargetText;
}
if (semanticHints.scope_target_kind === "contract" && scopeTargetText) {
extractedFilters.contract = scopeTargetText;
pushWarning(warnings, "contract_from_llm_semantics");
semanticFrame.scope_kind = "explicit_anchor";
semanticFrame.anchor_kind = "contract";
semanticFrame.anchor_value = scopeTargetText;
}
if (semanticHints.scope_target_kind === "item" && scopeTargetText) {
extractedFilters.item = scopeTargetText;
pushWarning(warnings, "item_from_llm_semantics");
semanticFrame.scope_kind = "explicit_anchor";
semanticFrame.anchor_kind = "item";
semanticFrame.anchor_value = scopeTargetText;
}
return {
...extraction,
extracted_filters: extractedFilters,
warnings,
semantic_frame: semanticFrame
};
}

View File

@ -205,14 +205,17 @@ export async function runAssistantAddressAttemptRuntime<ResponseType = unknown>(
const runAddressLaneAttempt: RunAssistantAddressRuntimeInput<ResponseType>["runAddressLaneAttempt"] = async ( const runAddressLaneAttempt: RunAssistantAddressRuntimeInput<ResponseType>["runAddressLaneAttempt"] = async (
messageUsed, messageUsed,
carryMeta, carryMeta,
analysisDateHint analysisDateHint,
llmSemanticHints = null
) => ) =>
runAddressLaneAttemptRuntimeSafe( runAddressLaneAttemptRuntimeSafe(
buildAssistantAddressLaneAttemptRuntimeInput({ buildAssistantAddressLaneAttemptRuntimeInput({
messageUsed, messageUsed,
carryMeta, carryMeta,
analysisDateHint, analysisDateHint,
llmSemanticHints,
activeOrganization: input.sessionScope.activeOrganization, activeOrganization: input.sessionScope.activeOrganization,
knownOrganizations: input.sessionScope.knownOrganizations,
mergeFollowupContextWithOrganizationScope: input.mergeFollowupContextWithOrganizationScope, mergeFollowupContextWithOrganizationScope: input.mergeFollowupContextWithOrganizationScope,
runAddressQueryTryHandle: input.runAddressQueryTryHandle runAddressQueryTryHandle: input.runAddressQueryTryHandle
}) })

View File

@ -4,7 +4,9 @@ export interface BuildAssistantAddressLaneAttemptRuntimeInputInput {
messageUsed: RunAssistantAddressLaneAttemptRuntimeInput["messageUsed"]; messageUsed: RunAssistantAddressLaneAttemptRuntimeInput["messageUsed"];
carryMeta: RunAssistantAddressLaneAttemptRuntimeInput["carryMeta"]; carryMeta: RunAssistantAddressLaneAttemptRuntimeInput["carryMeta"];
analysisDateHint: RunAssistantAddressLaneAttemptRuntimeInput["analysisDateHint"]; analysisDateHint: RunAssistantAddressLaneAttemptRuntimeInput["analysisDateHint"];
llmSemanticHints: RunAssistantAddressLaneAttemptRuntimeInput["llmSemanticHints"];
activeOrganization: RunAssistantAddressLaneAttemptRuntimeInput["activeOrganization"]; activeOrganization: RunAssistantAddressLaneAttemptRuntimeInput["activeOrganization"];
knownOrganizations: RunAssistantAddressLaneAttemptRuntimeInput["knownOrganizations"];
mergeFollowupContextWithOrganizationScope: mergeFollowupContextWithOrganizationScope:
RunAssistantAddressLaneAttemptRuntimeInput["mergeFollowupContextWithOrganizationScope"]; RunAssistantAddressLaneAttemptRuntimeInput["mergeFollowupContextWithOrganizationScope"];
runAddressQueryTryHandle: RunAssistantAddressLaneAttemptRuntimeInput["runAddressQueryTryHandle"]; runAddressQueryTryHandle: RunAssistantAddressLaneAttemptRuntimeInput["runAddressQueryTryHandle"];
@ -17,7 +19,9 @@ export function buildAssistantAddressLaneAttemptRuntimeInput(
messageUsed: input.messageUsed, messageUsed: input.messageUsed,
carryMeta: input.carryMeta, carryMeta: input.carryMeta,
analysisDateHint: input.analysisDateHint, analysisDateHint: input.analysisDateHint,
llmSemanticHints: input.llmSemanticHints,
activeOrganization: input.activeOrganization, activeOrganization: input.activeOrganization,
knownOrganizations: input.knownOrganizations,
mergeFollowupContextWithOrganizationScope: input.mergeFollowupContextWithOrganizationScope, mergeFollowupContextWithOrganizationScope: input.mergeFollowupContextWithOrganizationScope,
runAddressQueryTryHandle: input.runAddressQueryTryHandle runAddressQueryTryHandle: input.runAddressQueryTryHandle
}; };

View File

@ -11,18 +11,28 @@ export function resolveAssistantAddressLaneAttemptFollowupContext(
export interface BuildAssistantAddressLaneAttemptQueryOptionsInput { export interface BuildAssistantAddressLaneAttemptQueryOptionsInput {
analysisDateHint: RunAssistantAddressLaneAttemptRuntimeInput["analysisDateHint"]; analysisDateHint: RunAssistantAddressLaneAttemptRuntimeInput["analysisDateHint"];
scopedFollowupContext: Record<string, unknown> | null; scopedFollowupContext: Record<string, unknown> | null;
llmSemanticHints: RunAssistantAddressLaneAttemptRuntimeInput["llmSemanticHints"];
activeOrganization: RunAssistantAddressLaneAttemptRuntimeInput["activeOrganization"];
knownOrganizations: RunAssistantAddressLaneAttemptRuntimeInput["knownOrganizations"];
} }
export function buildAssistantAddressLaneAttemptQueryOptions( export function buildAssistantAddressLaneAttemptQueryOptions(
input: BuildAssistantAddressLaneAttemptQueryOptionsInput input: BuildAssistantAddressLaneAttemptQueryOptionsInput
): Parameters<RunAssistantAddressLaneAttemptRuntimeInput["runAddressQueryTryHandle"]>[1] { ): Parameters<RunAssistantAddressLaneAttemptRuntimeInput["runAddressQueryTryHandle"]>[1] {
const base = {
analysisDateHint: input.analysisDateHint
} as Parameters<RunAssistantAddressLaneAttemptRuntimeInput["runAddressQueryTryHandle"]>[1];
if (input.scopedFollowupContext) { if (input.scopedFollowupContext) {
return { base.followupContext = input.scopedFollowupContext;
followupContext: input.scopedFollowupContext,
analysisDateHint: input.analysisDateHint
};
} }
return { if (input.llmSemanticHints) {
analysisDateHint: input.analysisDateHint base.llmSemanticHints = input.llmSemanticHints;
}; }
if (input.activeOrganization) {
base.activeOrganization = input.activeOrganization;
}
if (input.knownOrganizations.length > 0) {
base.knownOrganizations = input.knownOrganizations;
}
return base;
} }

View File

@ -9,7 +9,9 @@ export interface RunAssistantAddressLaneAttemptRuntimeInput {
messageUsed: string; messageUsed: string;
carryMeta: AssistantAddressCarryoverLike | null; carryMeta: AssistantAddressCarryoverLike | null;
analysisDateHint: string | null; analysisDateHint: string | null;
llmSemanticHints?: Record<string, unknown> | null;
activeOrganization: string | null; activeOrganization: string | null;
knownOrganizations: string[];
mergeFollowupContextWithOrganizationScope: ( mergeFollowupContextWithOrganizationScope: (
followupContext: Record<string, unknown> | null, followupContext: Record<string, unknown> | null,
organization: string | null organization: string | null
@ -19,6 +21,9 @@ export interface RunAssistantAddressLaneAttemptRuntimeInput {
options: { options: {
followupContext?: Record<string, unknown>; followupContext?: Record<string, unknown>;
analysisDateHint?: string | null; analysisDateHint?: string | null;
llmSemanticHints?: Record<string, unknown> | null;
activeOrganization?: string | null;
knownOrganizations?: string[];
} }
) => Promise<AssistantAddressLaneLike | null>; ) => Promise<AssistantAddressLaneLike | null>;
} }
@ -35,7 +40,10 @@ export async function runAssistantAddressLaneAttemptRuntime(
input.messageUsed, input.messageUsed,
buildAssistantAddressLaneAttemptQueryOptions({ buildAssistantAddressLaneAttemptQueryOptions({
analysisDateHint: input.analysisDateHint, analysisDateHint: input.analysisDateHint,
scopedFollowupContext scopedFollowupContext,
llmSemanticHints: input.llmSemanticHints ?? null,
activeOrganization: input.activeOrganization,
knownOrganizations: input.knownOrganizations
}) })
); );
} }

View File

@ -214,12 +214,32 @@ export function runAssistantAddressLaneResponseRuntime<ResponseType = AssistantM
const debugActiveOrganization = const debugActiveOrganization =
input.toNonEmptyString(debugFilters?.organization) ?? input.toNonEmptyString(debugFilters?.organization) ??
input.toNonEmptyString(input.activeOrganization); input.toNonEmptyString(input.activeOrganization);
const followupContextSource =
input.carryoverMeta?.followupContext && typeof input.carryoverMeta.followupContext === "object"
? (input.carryoverMeta.followupContext as Record<string, unknown>)
: null;
if (debugKnownOrganizations.length > 0) { if (debugKnownOrganizations.length > 0) {
debug.assistant_known_organizations = debugKnownOrganizations; debug.assistant_known_organizations = debugKnownOrganizations;
} }
if (debugActiveOrganization) { if (debugActiveOrganization) {
debug.assistant_active_organization = debugActiveOrganization; debug.assistant_active_organization = debugActiveOrganization;
} }
const rootIntent = input.toNonEmptyString(followupContextSource?.root_intent);
const currentFrameKind = input.toNonEmptyString(followupContextSource?.current_frame_kind);
const rootFilters =
followupContextSource?.root_filters && typeof followupContextSource.root_filters === "object"
? (followupContextSource.root_filters as Record<string, unknown>)
: null;
if (rootIntent || currentFrameKind) {
debug.address_root_frame_context = {
root_intent: rootIntent,
current_frame_kind: currentFrameKind,
organization: input.toNonEmptyString(rootFilters?.organization),
as_of_date: input.toNonEmptyString(rootFilters?.as_of_date),
period_from: input.toNonEmptyString(rootFilters?.period_from),
period_to: input.toNonEmptyString(rootFilters?.period_to)
};
}
const finalization = finalizeAddressTurnSafe({ const finalization = finalizeAddressTurnSafe({
sessionId: input.sessionId, sessionId: input.sessionId,
userMessage: input.userMessage, userMessage: input.userMessage,

View File

@ -31,11 +31,13 @@ export interface RunAssistantAddressLaneRuntimeInput {
userMessage: string; userMessage: string;
addressInputMessage: string; addressInputMessage: string;
carryover: AssistantAddressFollowupCarryoverLike | null; carryover: AssistantAddressFollowupCarryoverLike | null;
llmSemanticHints?: Record<string, unknown> | null;
shouldPreferContextualLane: boolean; shouldPreferContextualLane: boolean;
canRetryWithRawUserMessage: boolean; canRetryWithRawUserMessage: boolean;
runAddressLaneAttempt: ( runAddressLaneAttempt: (
messageUsed: string, messageUsed: string,
carryMeta: AssistantAddressFollowupCarryoverLike | null carryMeta: AssistantAddressFollowupCarryoverLike | null,
llmSemanticHints?: Record<string, unknown> | null
) => Promise<AssistantAddressLaneLike | null>; ) => Promise<AssistantAddressLaneLike | null>;
isRetryableAddressLimitedResult: (addressLane: AssistantAddressLaneLike | null | undefined) => boolean; isRetryableAddressLimitedResult: (addressLane: AssistantAddressLaneLike | null | undefined) => boolean;
} }
@ -95,7 +97,11 @@ export async function runAssistantAddressLaneRuntime(
}; };
if (input.shouldPreferContextualLane) { if (input.shouldPreferContextualLane) {
const contextualAddressLane = await input.runAddressLaneAttempt(input.addressInputMessage, input.carryover); const contextualAddressLane = await input.runAddressLaneAttempt(
input.addressInputMessage,
input.carryover,
input.llmSemanticHints ?? null
);
const decision = evaluateAddressLane(contextualAddressLane, input.addressInputMessage, input.carryover); const decision = evaluateAddressLane(contextualAddressLane, input.addressInputMessage, input.carryover);
if (decision.action === "return") { if (decision.action === "return") {
return { return {
@ -106,7 +112,11 @@ export async function runAssistantAddressLaneRuntime(
} }
} }
const primaryAddressLane = await input.runAddressLaneAttempt(input.addressInputMessage, null); const primaryAddressLane = await input.runAddressLaneAttempt(
input.addressInputMessage,
null,
input.llmSemanticHints ?? null
);
const primaryDecision = evaluateAddressLane(primaryAddressLane, input.addressInputMessage, null); const primaryDecision = evaluateAddressLane(primaryAddressLane, input.addressInputMessage, null);
if (primaryDecision.action === "return") { if (primaryDecision.action === "return") {
return { return {
@ -117,7 +127,11 @@ export async function runAssistantAddressLaneRuntime(
} }
if (!input.shouldPreferContextualLane && input.carryover?.followupContext) { if (!input.shouldPreferContextualLane && input.carryover?.followupContext) {
const contextualAddressLane = await input.runAddressLaneAttempt(input.addressInputMessage, input.carryover); const contextualAddressLane = await input.runAddressLaneAttempt(
input.addressInputMessage,
input.carryover,
input.llmSemanticHints ?? null
);
const contextualDecision = evaluateAddressLane(contextualAddressLane, input.addressInputMessage, input.carryover); const contextualDecision = evaluateAddressLane(contextualAddressLane, input.addressInputMessage, input.carryover);
if (contextualDecision.action === "return") { if (contextualDecision.action === "return") {
return { return {
@ -139,7 +153,11 @@ export async function runAssistantAddressLaneRuntime(
if (input.carryover?.followupContext) { if (input.carryover?.followupContext) {
retryAudit.retry_used_followup_context = true; retryAudit.retry_used_followup_context = true;
const rawContextualLane = await input.runAddressLaneAttempt(input.userMessage, input.carryover); const rawContextualLane = await input.runAddressLaneAttempt(
input.userMessage,
input.carryover,
input.llmSemanticHints ?? null
);
const rawContextualDecision = evaluateAddressLane(rawContextualLane, input.userMessage, input.carryover); const rawContextualDecision = evaluateAddressLane(rawContextualLane, input.userMessage, input.carryover);
if (rawContextualDecision.action === "return") { if (rawContextualDecision.action === "return") {
retryAudit.retry_result_category = limitedCategory(rawContextualDecision.selection.addressLane); retryAudit.retry_result_category = limitedCategory(rawContextualDecision.selection.addressLane);
@ -151,7 +169,7 @@ export async function runAssistantAddressLaneRuntime(
} }
} }
const rawPrimaryLane = await input.runAddressLaneAttempt(input.userMessage, null); const rawPrimaryLane = await input.runAddressLaneAttempt(input.userMessage, null, input.llmSemanticHints ?? null);
retryAudit.retry_result_category = limitedCategory(rawPrimaryLane); retryAudit.retry_result_category = limitedCategory(rawPrimaryLane);
const rawPrimaryDecision = evaluateAddressLane(rawPrimaryLane, input.userMessage, null); const rawPrimaryDecision = evaluateAddressLane(rawPrimaryLane, input.userMessage, null);
if (rawPrimaryDecision.action === "return") { if (rawPrimaryDecision.action === "return") {

View File

@ -50,6 +50,64 @@ export interface BuildAssistantAddressOrchestrationRuntimeOutput {
}; };
} }
function hasSelectedObjectInventorySignal(text: string | null): boolean {
return /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(
String(text ?? "")
);
}
function hasSelectedObjectInventoryActionCue(text: string | null): boolean {
return /(?:кому[\s\S]{0,80}продал[аи]?|кому[\s\S]{0,80}реализова[нлт][а-я]*|кому\s+был\s+продан|кто[\s\S]{0,40}купил|кто\s+это\s+поставил|кто\s+поставил|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+мы\s+купили|где\s+куплено|по\s+каким\s+документам|какими\s+документами|покажи\s+документы|документы\s+закупки|buyer|sale\s+trace|supplier|vendor|purchase\s+documents|purchase[\s-]?to[\s-]?sale|old\s+purchase|aged\s+stock)/iu.test(
String(text ?? "")
);
}
function isGenericCanonicalDriftIntent(intent: string | null): boolean {
return (
intent === "open_items_by_counterparty_or_contract" ||
intent === "list_documents_by_counterparty" ||
intent === "list_documents_by_contract" ||
intent === "bank_operations_by_counterparty" ||
intent === "bank_operations_by_contract" ||
intent === "documents_forming_balance"
);
}
function shouldPreferRawFollowupMessage(
userMessage: string,
addressInputMessage: string,
carryover: AssistantAddressCarryoverLike | null,
addressPreDecompose: Record<string, unknown>,
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]
): boolean {
if (!carryover?.followupContext || typeof carryover.followupContext !== "object") {
return false;
}
const rawMessage = toNonEmptyString(userMessage);
const canonicalMessage = toNonEmptyString(addressInputMessage);
if (!rawMessage || !canonicalMessage || rawMessage === canonicalMessage) {
return false;
}
const predecomposeContract =
addressPreDecompose?.predecomposeContract && typeof addressPreDecompose.predecomposeContract === "object"
? (addressPreDecompose.predecomposeContract as Record<string, unknown>)
: null;
const mode = toNonEmptyString(predecomposeContract?.mode) ?? "unknown";
const intent = toNonEmptyString(predecomposeContract?.intent) ?? "unknown";
if (mode === "unsupported" && intent === "unknown") {
return true;
}
return (
hasSelectedObjectInventorySignal(rawMessage) &&
hasSelectedObjectInventoryActionCue(rawMessage) &&
isGenericCanonicalDriftIntent(intent)
);
}
function fallbackAddressPreDecompose( function fallbackAddressPreDecompose(
userMessage: string, userMessage: string,
llmProvider: unknown, llmProvider: unknown,
@ -80,7 +138,7 @@ function fallbackAddressPreDecompose(
export async function buildAssistantAddressOrchestrationRuntime( export async function buildAssistantAddressOrchestrationRuntime(
input: BuildAssistantAddressOrchestrationRuntimeInput input: BuildAssistantAddressOrchestrationRuntimeInput
): Promise<BuildAssistantAddressOrchestrationRuntimeOutput> { ): Promise<BuildAssistantAddressOrchestrationRuntimeOutput> {
const addressPreDecompose = input.featureAddressLlmPredecomposeV1 const initialAddressPreDecompose = input.featureAddressLlmPredecomposeV1
? await input.runAddressLlmPreDecompose() ? await input.runAddressLlmPreDecompose()
: fallbackAddressPreDecompose( : fallbackAddressPreDecompose(
input.userMessage, input.userMessage,
@ -89,14 +147,43 @@ export async function buildAssistantAddressOrchestrationRuntime(
input.sanitizeAddressMessageForFallback input.sanitizeAddressMessageForFallback
); );
const addressInputMessage = let addressPreDecompose = initialAddressPreDecompose;
let addressInputMessage =
input.toNonEmptyString(addressPreDecompose?.effectiveMessage) ?? input.userMessage; input.toNonEmptyString(addressPreDecompose?.effectiveMessage) ?? input.userMessage;
const carryover = input.resolveAddressFollowupCarryoverContext( let carryover = input.resolveAddressFollowupCarryoverContext(
input.userMessage, input.userMessage,
input.sessionItems, input.sessionItems,
addressInputMessage, addressInputMessage,
addressPreDecompose addressPreDecompose
); );
if (
shouldPreferRawFollowupMessage(
input.userMessage,
addressInputMessage,
carryover,
addressPreDecompose,
input.toNonEmptyString
)
) {
addressInputMessage = input.userMessage;
addressPreDecompose = {
...addressPreDecompose,
applied: false,
effectiveMessage: input.userMessage,
reason: "followup_raw_message_preferred_over_llm_rewrite",
predecomposeContract: input.buildAddressLlmPredecomposeContractV1({
sourceMessage: input.userMessage,
canonicalMessage: input.userMessage
})
};
carryover = input.resolveAddressFollowupCarryoverContext(
input.userMessage,
input.sessionItems,
addressInputMessage,
addressPreDecompose
);
}
const followupContext = carryover?.followupContext ?? null; const followupContext = carryover?.followupContext ?? null;
const orchestrationDecision = input.resolveAssistantOrchestrationDecision({ const orchestrationDecision = input.resolveAssistantOrchestrationDecision({
rawUserMessage: input.userMessage, rawUserMessage: input.userMessage,

View File

@ -35,7 +35,8 @@ export interface RunAssistantAddressRuntimeInput<ResponseType = unknown> {
runAddressLaneAttempt: ( runAddressLaneAttempt: (
messageUsed: string, messageUsed: string,
carryMeta: AssistantAddressCarryoverLike | null, carryMeta: AssistantAddressCarryoverLike | null,
analysisDateHint: string | null analysisDateHint: string | null,
llmSemanticHints?: Record<string, unknown> | null
) => Promise<AssistantAddressLaneLike | null>; ) => Promise<AssistantAddressLaneLike | null>;
isRetryableAddressLimitedResult: (addressLane: AssistantAddressLaneLike | null | undefined) => boolean; isRetryableAddressLimitedResult: (addressLane: AssistantAddressLaneLike | null | undefined) => boolean;
finalizeAddressLaneResponse: ( finalizeAddressLaneResponse: (
@ -78,7 +79,8 @@ export interface RunAssistantAddressRuntimeInput<ResponseType = unknown> {
canRetryWithRawUserMessage: boolean; canRetryWithRawUserMessage: boolean;
runAddressLaneAttempt: ( runAddressLaneAttempt: (
messageUsed: string, messageUsed: string,
carryMeta: AssistantAddressCarryoverLike | null carryMeta: AssistantAddressCarryoverLike | null,
llmSemanticHints?: Record<string, unknown> | null
) => Promise<AssistantAddressLaneLike | null>; ) => Promise<AssistantAddressLaneLike | null>;
isRetryableAddressLimitedResult: (addressLane: AssistantAddressLaneLike | null | undefined) => boolean; isRetryableAddressLimitedResult: (addressLane: AssistantAddressLaneLike | null | undefined) => boolean;
} }
@ -157,10 +159,14 @@ export async function runAssistantAddressRuntime<ResponseType = unknown>(
userMessage: input.userMessage, userMessage: input.userMessage,
addressInputMessage, addressInputMessage,
carryover, carryover,
llmSemanticHints:
addressRuntimeMeta && typeof addressRuntimeMeta === "object"
? ((addressRuntimeMeta as { semanticHints?: unknown }).semanticHints as Record<string, unknown> | null) ?? null
: null,
shouldPreferContextualLane, shouldPreferContextualLane,
canRetryWithRawUserMessage, canRetryWithRawUserMessage,
runAddressLaneAttempt: (messageUsed, carryMeta) => runAddressLaneAttempt: (messageUsed, carryMeta, llmSemanticHints = null) =>
input.runAddressLaneAttempt(messageUsed, carryMeta, analysisDateHint), input.runAddressLaneAttempt(messageUsed, carryMeta, analysisDateHint, llmSemanticHints),
isRetryableAddressLimitedResult: input.isRetryableAddressLimitedResult isRetryableAddressLimitedResult: input.isRetryableAddressLimitedResult
}); });
if (addressLaneRuntime.handled && addressLaneRuntime.selection) { if (addressLaneRuntime.handled && addressLaneRuntime.selection) {

View File

@ -0,0 +1,221 @@
const ORGANIZATION_SCOPE_STOPWORDS = new Set([
"ооо",
"зао",
"оао",
"пао",
"ао",
"ип",
"llc",
"inc",
"ltd",
"corp",
"group",
"company",
"co",
"the",
"and",
"org",
"organization",
"компания",
"организация",
"контора",
"фирма",
"база",
"по",
"в",
"во",
"на",
"для",
"из",
"у",
"к",
"от",
"это",
"эта",
"этой",
"этот",
"сегодня",
"сейчас",
"текущая",
"текущей",
"наш",
"наша",
"нашей",
"нашу",
"наши"
]);
function normalizeScopeLabel(value: unknown): string {
return String(value ?? "")
.replace(/[“”«»]/g, '"')
.replace(/\s+/g, " ")
.trim();
}
function normalizeScopeKey(value: unknown): string {
return normalizeScopeLabel(value).toLowerCase().replace(/ё/g, "е");
}
export function normalizeOrganizationScopeValue(value: unknown): string | null {
const normalized = normalizeScopeLabel(value);
if (!normalized) {
return null;
}
let unwrapped = normalized.replace(/^\\+|\\+$/g, "").trim();
if (
(unwrapped.startsWith('"') && unwrapped.endsWith('"')) ||
(unwrapped.startsWith("'") && unwrapped.endsWith("'"))
) {
unwrapped = unwrapped.slice(1, -1).trim();
}
return unwrapped.length > 0 ? unwrapped : null;
}
export function normalizeOrganizationScopeSearchText(value: unknown): string {
return normalizeScopeKey(value)
.replace(/[^\p{L}\p{N}]+/gu, " ")
.replace(/\s+/g, " ")
.trim();
}
function tokenizeOrganizationScope(value: unknown): string[] {
const normalized = normalizeOrganizationScopeSearchText(value);
if (!normalized) {
return [];
}
return normalized
.split(" ")
.map((token) => token.trim())
.filter((token) => token.length >= 3 && !ORGANIZATION_SCOPE_STOPWORDS.has(token));
}
function organizationTokenVariants(token: string): string[] {
const source = String(token ?? "").trim().toLowerCase();
if (!source) {
return [];
}
const variants = new Set([source]);
const withoutLongEnding = source.replace(
/(?:ами|ями|ого|ему|ому|ыми|ими|иях|ях|ах|ей|ой|ом|ем|ам|ям|ую|юю|ая|яя|ое|ее|ые|ие|ов|ев|ий|ый|ой)$/iu,
""
);
if (withoutLongEnding.length >= 4) {
variants.add(withoutLongEnding);
}
const withoutShortEnding = source.replace(/[аеёиоуыэюя]$/iu, "");
if (withoutShortEnding.length >= 4) {
variants.add(withoutShortEnding);
}
return Array.from(variants);
}
export function scoreOrganizationMentionInMessage(message: unknown, organization: unknown): number {
const messageNorm = normalizeOrganizationScopeSearchText(message);
const organizationNorm = normalizeOrganizationScopeSearchText(organization);
if (!messageNorm || !organizationNorm) {
return 0;
}
if (messageNorm.includes(organizationNorm)) {
return 10_000 + organizationNorm.length;
}
const organizationTokens = tokenizeOrganizationScope(organizationNorm);
const messageTokens = tokenizeOrganizationScope(messageNorm);
if (organizationTokens.length === 0 || messageTokens.length === 0) {
return 0;
}
let matchedTokens = 0;
let score = 0;
for (const token of organizationTokens) {
const variants = organizationTokenVariants(token);
let matched = false;
let variantScore = 0;
for (const variant of variants) {
if (!variant) {
continue;
}
if (messageNorm.includes(variant)) {
matched = true;
variantScore = Math.max(variantScore, variant.length * 5);
continue;
}
const fuzzyMatched = messageTokens.some((messageToken) => {
if (messageToken === variant) {
return true;
}
if (messageToken.length >= 5 && variant.length >= 5) {
return messageToken.startsWith(variant) || variant.startsWith(messageToken);
}
return false;
});
if (fuzzyMatched) {
matched = true;
variantScore = Math.max(variantScore, Math.max(20, variant.length * 3));
}
}
if (matched) {
matchedTokens += 1;
score += variantScore > 0 ? variantScore : 10;
}
}
if (matchedTokens === 0) {
return 0;
}
if (matchedTokens === organizationTokens.length) {
score += 400;
} else {
score += matchedTokens * 50;
}
return score;
}
export function mergeKnownOrganizations(values: unknown[], limit = 50): string[] {
const dedup = new Map<string, string>();
for (const raw of Array.isArray(values) ? values : []) {
const normalized = normalizeOrganizationScopeValue(raw);
if (!normalized) {
continue;
}
const key = normalizeOrganizationScopeSearchText(normalized);
if (!key || dedup.has(key)) {
continue;
}
dedup.set(key, normalized);
}
return Array.from(dedup.values()).slice(0, limit);
}
export function resolveOrganizationSelectionFromMessage(
userMessage: string,
knownOrganizations: unknown[]
): string | null {
const known = mergeKnownOrganizations(Array.isArray(knownOrganizations) ? knownOrganizations : []);
if (!userMessage || known.length === 0) {
return null;
}
const messageNorm = normalizeOrganizationScopeSearchText(userMessage);
if (!messageNorm) {
return null;
}
const scored = known
.map((organization) => ({
organization,
score: scoreOrganizationMentionInMessage(messageNorm, organization)
}))
.filter((item) => item.score > 0)
.sort((a, b) => b.score - a.score || a.organization.length - b.organization.length);
if (scored.length === 0) {
return null;
}
const best = scored[0];
const second = scored[1];
if (best.score < 90) {
return null;
}
if (second && second.score === best.score) {
return null;
}
return best.organization;
}

View File

@ -2453,6 +2453,62 @@ function findRecentAddressFilterValue(items, key) {
} }
return null; return null;
} }
function isInventoryRootFrameIntent(intent) {
return intent === "inventory_on_hand_as_of_date";
}
function isInventoryDrilldownFrameIntent(intent) {
return intent === "inventory_purchase_provenance_for_item" ||
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date";
}
function extractAddressCarryoverAnchor(addressDebug) {
if (!isAddressLaneDebugPayload(addressDebug)) {
return {
anchorType: null,
anchorValue: null
};
}
return {
anchorType: toNonEmptyString(addressDebug.anchor_type),
anchorValue: toNonEmptyString(addressDebug.anchor_value_resolved) ??
toNonEmptyString(addressDebug.anchor_value_raw) ??
readAddressInventoryItemFilter(addressDebug) ??
readAddressFilterString(addressDebug, "counterparty") ??
readAddressFilterString(addressDebug, "contract") ??
readAddressFilterString(addressDebug, "account")
};
}
function findRecentInventoryRootFrame(items) {
for (let index = items.length - 1; index >= 0; index -= 1) {
const item = items[index];
if (!item || item.role !== "assistant" || !item.debug) {
continue;
}
const debug = item.debug;
if (!isAddressLaneDebugPayload(debug)) {
continue;
}
const detectedIntent = toNonEmptyString(debug.detected_intent);
if (!isInventoryRootFrameIntent(detectedIntent)) {
continue;
}
const anchor = extractAddressCarryoverAnchor(debug);
const filtersRaw = debug.extracted_filters;
const filters = filtersRaw && typeof filtersRaw === "object"
? { ...filtersRaw }
: {};
return {
intent: detectedIntent,
filters,
anchorType: anchor.anchorType,
anchorValue: anchor.anchorValue,
messageId: toNonEmptyString(item.message_id)
};
}
return null;
}
const ADDRESS_FOLLOWUP_OFFER_BY_INTENT = { const ADDRESS_FOLLOWUP_OFFER_BY_INTENT = {
list_documents_by_counterparty: ["bank_operations_by_counterparty", "list_contracts_by_counterparty"], list_documents_by_counterparty: ["bank_operations_by_counterparty", "list_contracts_by_counterparty"],
bank_operations_by_counterparty: ["list_documents_by_counterparty", "list_contracts_by_counterparty"], bank_operations_by_counterparty: ["list_documents_by_counterparty", "list_contracts_by_counterparty"],
@ -2755,6 +2811,14 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
readAddressFilterString(previousAddressDebug, "counterparty") ?? readAddressFilterString(previousAddressDebug, "counterparty") ??
readAddressFilterString(previousAddressDebug, "account") ?? readAddressFilterString(previousAddressDebug, "account") ??
readAddressFilterString(previousAddressDebug, "contract"); readAddressFilterString(previousAddressDebug, "contract");
const inventoryRootFrame = findRecentInventoryRootFrame(items);
const currentFrameKind = inventoryRootFrame
? isInventoryDrilldownFrameIntent(sourceIntent)
? "inventory_drilldown"
: isInventoryRootFrameIntent(sourceIntent)
? "inventory_root"
: "generic"
: null;
let resolvedCounterpartyFromDisplay = false; let resolvedCounterpartyFromDisplay = false;
const previousFiltersRaw = previousAddressDebug.extracted_filters; const previousFiltersRaw = previousAddressDebug.extracted_filters;
const previousFilters = previousFiltersRaw && typeof previousFiltersRaw === "object" const previousFilters = previousFiltersRaw && typeof previousFiltersRaw === "object"
@ -2814,7 +2878,12 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
previous_filters: previousFilters, previous_filters: previousFilters,
previous_anchor_type: previousAnchorType ?? undefined, previous_anchor_type: previousAnchorType ?? undefined,
previous_anchor_value: previousAnchor, previous_anchor_value: previousAnchor,
resolved_counterparty_from_display: resolvedCounterpartyFromDisplay || undefined resolved_counterparty_from_display: resolvedCounterpartyFromDisplay || undefined,
root_intent: inventoryRootFrame?.intent ?? undefined,
root_filters: inventoryRootFrame?.filters ?? undefined,
root_anchor_type: inventoryRootFrame?.anchorType ?? undefined,
root_anchor_value: inventoryRootFrame?.anchorValue ?? undefined,
current_frame_kind: currentFrameKind ?? undefined
}, },
previousAddressIntent: previousIntent, previousAddressIntent: previousIntent,
previousAddressAnchor: previousAnchor, previousAddressAnchor: previousAnchor,
@ -2890,19 +2959,32 @@ function isAddressLlmPreDecomposeCandidate(userMessage) {
} }
return /(?:\bдок\b|доки|документ|контрагент|договор|остаток|сч(?:е|ё)т|сальдо|банк|выписк|платеж|оплат|поступлен|поступлени|списан|реализац|сверк|взаиморасч|кто\s+должен|show|list|documents?|counterparty|contract|account|balance|bank\s+operations?|doki|dokument(?:y|ov|am|a)?|platezh|oplata|schet|saldo)/i.test(text); return /(?:\bдок\b|доки|документ|контрагент|договор|остаток|сч(?:е|ё)т|сальдо|банк|выписк|платеж|оплат|поступлен|поступлени|списан|реализац|сверк|взаиморасч|кто\s+должен|show|list|documents?|counterparty|contract|account|balance|bank\s+operations?|doki|dokument(?:y|ov|am|a)?|platezh|oplata|schet|saldo)/i.test(text);
} }
function extractAddressQuestionFromNormalized(normalized) { function normalizeAddressSemanticHintsFromFragment(fragment) {
if (!normalized || typeof normalized !== "object") { if (!fragment || typeof fragment !== "object") {
return null; return null;
} }
const source = normalized; const hints = fragment.semantic_hints;
const fragments = Array.isArray(source.fragments) ? source.fragments : []; if (!hints || typeof hints !== "object") {
for (const item of fragments) { return null;
}
const scopeTargetKind = toNonEmptyString(hints.scope_target_kind);
const dateScopeKind = toNonEmptyString(hints.date_scope_kind);
return {
scope_target_kind: scopeTargetKind ?? "none",
scope_target_text: toNonEmptyString(hints.scope_target_text),
date_scope_kind: dateScopeKind ?? "missing",
self_scope_detected: hints.self_scope_detected === true || scopeTargetKind === "self_scope",
selected_object_scope_detected: hints.selected_object_scope_detected === true || scopeTargetKind === "selected_object"
};
}
function extractAddressPredecomposeCandidateFromFragments(fragments) {
for (const item of Array.isArray(fragments) ? fragments : []) {
if (!item || typeof item !== "object") { if (!item || typeof item !== "object") {
continue; continue;
} }
const fragment = item; const fragment = item;
const domainRelevance = String(fragment.domain_relevance ?? "").trim().toLowerCase(); const domainRelevance = String(fragment.domain_relevance ?? "").trim().toLowerCase();
if (domainRelevance === "out_of_scope") { if (domainRelevance === "out_of_scope" || domainRelevance === "offtopic") {
continue; continue;
} }
const normalizedText = toNonEmptyString(fragment.normalized_fragment_text); const normalizedText = toNonEmptyString(fragment.normalized_fragment_text);
@ -2912,11 +2994,20 @@ function extractAddressQuestionFromNormalized(normalized) {
continue; continue;
} }
if (candidate.length >= 3 && candidate.length <= 500) { if (candidate.length >= 3 && candidate.length <= 500) {
return candidate; return {
candidate,
semanticHints: normalizeAddressSemanticHintsFromFragment(fragment)
};
} }
} }
return null; return null;
} }
function extractAddressPredecomposeCandidateFromNormalized(normalized) {
if (!normalized || typeof normalized !== "object") {
return null;
}
return extractAddressPredecomposeCandidateFromFragments(normalized.fragments);
}
function stripMarkdownJsonFence(text) { function stripMarkdownJsonFence(text) {
return String(text ?? "") return String(text ?? "")
.trim() .trim()
@ -2994,7 +3085,7 @@ function extractOutputTextFromRawNormalizerOutput(raw) {
} }
return null; return null;
} }
function extractAddressQuestionFromRawNormalizerOutput(rawModelOutput) { function extractAddressPredecomposeCandidateFromRawNormalizerOutput(rawModelOutput) {
const outputText = extractOutputTextFromRawNormalizerOutput(rawModelOutput); const outputText = extractOutputTextFromRawNormalizerOutput(rawModelOutput);
if (!outputText) { if (!outputText) {
return null; return null;
@ -3003,31 +3094,7 @@ function extractAddressQuestionFromRawNormalizerOutput(rawModelOutput) {
if (!parsed || typeof parsed !== "object") { if (!parsed || typeof parsed !== "object") {
return null; return null;
} }
const source = parsed; return extractAddressPredecomposeCandidateFromFragments(parsed.fragments);
const fragments = Array.isArray(source.fragments) ? source.fragments : [];
for (const item of fragments) {
if (!item || typeof item !== "object") {
continue;
}
const fragment = item;
const domainRelevance = fragment.domain_relevance;
if (typeof domainRelevance === "string" && domainRelevance.trim().toLowerCase() === "out_of_scope") {
continue;
}
if (domainRelevance === false) {
continue;
}
const normalizedText = toNonEmptyString(fragment.normalized_fragment_text);
const rawText = toNonEmptyString(fragment.raw_fragment_text);
const candidate = selectPreferredAddressFragmentCandidate(rawText ?? "", normalizedText ?? "");
if (!candidate) {
continue;
}
if (candidate.length >= 3 && candidate.length <= 500) {
return candidate;
}
}
return null;
} }
const ADDRESS_PREDECOMPOSE_LOW_QUALITY_COUNTERPARTY_TOKENS = new Set([ const ADDRESS_PREDECOMPOSE_LOW_QUALITY_COUNTERPARTY_TOKENS = new Set([
"есть", "есть",
@ -3267,7 +3334,8 @@ function attachAddressPredecomposeContract(meta, sourceMessage) {
const canonicalMessage = toNonEmptyString(meta?.effectiveMessage) ?? String(sourceMessage ?? ""); const canonicalMessage = toNonEmptyString(meta?.effectiveMessage) ?? String(sourceMessage ?? "");
const predecomposeContract = (0, predecomposeContract_1.buildAddressLlmPredecomposeContractV1)({ const predecomposeContract = (0, predecomposeContract_1.buildAddressLlmPredecomposeContractV1)({
sourceMessage: String(sourceMessage ?? ""), sourceMessage: String(sourceMessage ?? ""),
canonicalMessage canonicalMessage,
semanticHints: meta?.semanticHints ?? null
}); });
const semanticExtractionContract = (0, predecomposeContract_1.buildAddressSemanticExtractionContractV1)({ const semanticExtractionContract = (0, predecomposeContract_1.buildAddressSemanticExtractionContractV1)({
sourceMessage: String(sourceMessage ?? ""), sourceMessage: String(sourceMessage ?? ""),
@ -3332,9 +3400,10 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
}; };
try { try {
const normalized = await normalizerService.normalize(normalizePayload); const normalized = await normalizerService.normalize(normalizePayload);
const candidateFromNormalized = extractAddressQuestionFromNormalized(normalized?.normalized); const candidateFromNormalized = extractAddressPredecomposeCandidateFromNormalized(normalized?.normalized);
const candidateFromRaw = candidateFromNormalized ? null : extractAddressQuestionFromRawNormalizerOutput(normalized?.raw_model_output); const candidateFromRaw = candidateFromNormalized ? null : extractAddressPredecomposeCandidateFromRawNormalizerOutput(normalized?.raw_model_output);
const candidate = candidateFromNormalized ?? candidateFromRaw; const candidateMeta = candidateFromNormalized ?? candidateFromRaw;
const candidate = candidateMeta?.candidate ?? null;
if (!candidate) { if (!candidate) {
if (fallbackCandidate) { if (fallbackCandidate) {
const fallbackCompact = compactWhitespace(String(fallbackCandidate.candidate ?? "").toLowerCase()); const fallbackCompact = compactWhitespace(String(fallbackCandidate.candidate ?? "").toLowerCase());
@ -3348,7 +3417,8 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
traceId: normalized?.trace_id ?? null, traceId: normalized?.trace_id ?? null,
effectiveMessage: fallbackCandidate.candidate, effectiveMessage: fallbackCandidate.candidate,
reason: "fallback_rule_applied_after_llm", reason: "fallback_rule_applied_after_llm",
fallbackRuleHit: fallbackCandidate.rule fallbackRuleHit: fallbackCandidate.rule,
semanticHints: null
}, userMessage); }, userMessage);
} }
} }
@ -3356,7 +3426,8 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
...baseMeta, ...baseMeta,
attempted: true, attempted: true,
traceId: normalized?.trace_id ?? null, traceId: normalized?.trace_id ?? null,
reason: normalized?.ok ? "no_usable_fragment" : "normalize_failed" reason: normalized?.ok ? "no_usable_fragment" : "normalize_failed",
semanticHints: null
}, userMessage); }, userMessage);
} }
const repairedSourceMessage = repairAddressMojibake(userMessage); const repairedSourceMessage = repairAddressMojibake(userMessage);
@ -3375,7 +3446,8 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
effectiveMessage: userMessage, effectiveMessage: userMessage,
reason: "normalized_fragment_rejected_diagnostic_rewrite", reason: "normalized_fragment_rejected_diagnostic_rewrite",
fallbackRuleHit: null, fallbackRuleHit: null,
sanitizedUserMessage sanitizedUserMessage,
semanticHints: candidateMeta?.semanticHints ?? null
}, userMessage); }, userMessage);
} }
const intentConflict = sourceIntentKnown && const intentConflict = sourceIntentKnown &&
@ -3397,7 +3469,8 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
? "normalized_fragment_rejected_intent_drop" ? "normalized_fragment_rejected_intent_drop"
: "normalized_fragment_rejected_intent_conflict", : "normalized_fragment_rejected_intent_conflict",
fallbackRuleHit: null, fallbackRuleHit: null,
sanitizedUserMessage sanitizedUserMessage,
semanticHints: candidateMeta?.semanticHints ?? null
}, userMessage); }, userMessage);
} }
const sourceHasExplicitDrilldownSignal = hasPredecomposeExplicitDrilldownSignal(repairedSourceMessage || userMessage); const sourceHasExplicitDrilldownSignal = hasPredecomposeExplicitDrilldownSignal(repairedSourceMessage || userMessage);
@ -3418,7 +3491,8 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
effectiveMessage: userMessage, effectiveMessage: userMessage,
reason: "normalized_fragment_rejected_followup_intent_injection", reason: "normalized_fragment_rejected_followup_intent_injection",
fallbackRuleHit: null, fallbackRuleHit: null,
sanitizedUserMessage sanitizedUserMessage,
semanticHints: candidateMeta?.semanticHints ?? null
}, userMessage); }, userMessage);
} }
const sourceHasSelectedObjectInventoryFollowup = hasSelectedObjectInventoryFollowupSignalForPredecompose(repairedSourceMessage || userMessage); const sourceHasSelectedObjectInventoryFollowup = hasSelectedObjectInventoryFollowupSignalForPredecompose(repairedSourceMessage || userMessage);
@ -3438,7 +3512,8 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
effectiveMessage: userMessage, effectiveMessage: userMessage,
reason: "normalized_fragment_rejected_selected_object_context_loss", reason: "normalized_fragment_rejected_selected_object_context_loss",
fallbackRuleHit: null, fallbackRuleHit: null,
sanitizedUserMessage sanitizedUserMessage,
semanticHints: candidateMeta?.semanticHints ?? null
}, userMessage); }, userMessage);
} }
const sourceAnchorQuality = evaluateAddressAnchorQuality(repairedSourceMessage || userMessage); const sourceAnchorQuality = evaluateAddressAnchorQuality(repairedSourceMessage || userMessage);
@ -3464,7 +3539,8 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
effectiveMessage: userMessage, effectiveMessage: userMessage,
reason: "normalized_fragment_rejected_anchor_substitution", reason: "normalized_fragment_rejected_anchor_substitution",
fallbackRuleHit: null, fallbackRuleHit: null,
sanitizedUserMessage sanitizedUserMessage,
semanticHints: candidateMeta?.semanticHints ?? null
}, userMessage); }, userMessage);
} }
const anchorDegradedByCandidate = sameIntentForAnchorSafety && const anchorDegradedByCandidate = sameIntentForAnchorSafety &&
@ -3481,7 +3557,8 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
effectiveMessage: userMessage, effectiveMessage: userMessage,
reason: "normalized_fragment_rejected_anchor_degradation", reason: "normalized_fragment_rejected_anchor_degradation",
fallbackRuleHit: null, fallbackRuleHit: null,
sanitizedUserMessage sanitizedUserMessage,
semanticHints: candidateMeta?.semanticHints ?? null
}, userMessage); }, userMessage);
} }
if (fallbackCandidate) { if (fallbackCandidate) {
@ -3500,19 +3577,25 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
effectiveMessage: fallbackCandidate.candidate, effectiveMessage: fallbackCandidate.candidate,
reason: "fallback_rule_preferred_over_llm_candidate_anchor_quality", reason: "fallback_rule_preferred_over_llm_candidate_anchor_quality",
fallbackRuleHit: fallbackCandidate.rule, fallbackRuleHit: fallbackCandidate.rule,
sanitizedUserMessage sanitizedUserMessage,
semanticHints: candidateMeta?.semanticHints ?? null
}, userMessage); }, userMessage);
} }
} }
const semanticContractForCandidate = (0, predecomposeContract_1.buildAddressSemanticExtractionContractV1)({ const semanticContractForCandidate = (0, predecomposeContract_1.buildAddressSemanticExtractionContractV1)({
sourceMessage: String(userMessage ?? ""), sourceMessage: String(userMessage ?? ""),
canonicalMessage: candidate canonicalMessage: candidate,
predecomposeContract: (0, predecomposeContract_1.buildAddressLlmPredecomposeContractV1)({
sourceMessage: String(userMessage ?? ""),
canonicalMessage: candidate,
semanticHints: candidateMeta?.semanticHints ?? null
})
}); });
if (!semanticContractForCandidate.apply_canonical_recommended) { if (!semanticContractForCandidate.apply_canonical_recommended) {
const sourceDataSignalDetected = Boolean(semanticContractForCandidate?.guard_hints?.source_data_signal_detected); const sourceDataSignalDetected = Boolean(semanticContractForCandidate?.guard_hints?.source_data_signal_detected);
const rawFragmentCandidatePreferred = Boolean(sourceDataSignalDetected && const rawFragmentCandidatePreferred = Boolean(sourceDataSignalDetected &&
candidateFromNormalized && candidateFromNormalized &&
candidateFromNormalized === candidate && candidateFromNormalized.candidate === candidate &&
toNonEmptyString(candidate)); toNonEmptyString(candidate));
if (rawFragmentCandidatePreferred) { if (rawFragmentCandidatePreferred) {
return attachAddressPredecomposeContract({ return attachAddressPredecomposeContract({
@ -3524,7 +3607,8 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
effectiveMessage: candidate, effectiveMessage: candidate,
reason: "normalized_fragment_semantic_guard_raw_fragment_preferred", reason: "normalized_fragment_semantic_guard_raw_fragment_preferred",
fallbackRuleHit: null, fallbackRuleHit: null,
sanitizedUserMessage sanitizedUserMessage,
semanticHints: candidateMeta?.semanticHints ?? null
}, userMessage); }, userMessage);
} }
if (fallbackCandidate) { if (fallbackCandidate) {
@ -3545,7 +3629,8 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
effectiveMessage: String(fallbackCandidate.candidate ?? ""), effectiveMessage: String(fallbackCandidate.candidate ?? ""),
reason: "fallback_rule_preferred_over_llm_candidate_semantic_guard", reason: "fallback_rule_preferred_over_llm_candidate_semantic_guard",
fallbackRuleHit: fallbackCandidate.rule, fallbackRuleHit: fallbackCandidate.rule,
sanitizedUserMessage sanitizedUserMessage,
semanticHints: candidateMeta?.semanticHints ?? null
}, userMessage); }, userMessage);
} }
} }
@ -3558,7 +3643,8 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
effectiveMessage: userMessage, effectiveMessage: userMessage,
reason: "normalized_fragment_rejected_semantic_guard", reason: "normalized_fragment_rejected_semantic_guard",
fallbackRuleHit: null, fallbackRuleHit: null,
sanitizedUserMessage sanitizedUserMessage,
semanticHints: candidateMeta?.semanticHints ?? null
}, userMessage); }, userMessage);
} }
const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase()); const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase());
@ -3585,7 +3671,8 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
reason, reason,
llmCanonicalCandidateDetected: true, llmCanonicalCandidateDetected: true,
fallbackRuleHit: null, fallbackRuleHit: null,
sanitizedUserMessage sanitizedUserMessage,
semanticHints: candidateMeta?.semanticHints ?? null
}, userMessage); }, userMessage);
} }
catch (error) { catch (error) {
@ -3933,7 +4020,11 @@ export function resolveAssistantOrchestrationDecision(input) {
hasOpenContractsAddressSignal(repairedEffectiveAddressUserMessage); hasOpenContractsAddressSignal(repairedEffectiveAddressUserMessage);
const modeSample = repairedEffectiveAddressUserMessage || effectiveAddressUserMessage; const modeSample = repairedEffectiveAddressUserMessage || effectiveAddressUserMessage;
const modeDetection = (0, addressQueryClassifier_1.detectAddressQuestionMode)(modeSample); const modeDetection = (0, addressQueryClassifier_1.detectAddressQuestionMode)(modeSample);
const modeDetectionRaw = (0, addressQueryClassifier_1.detectAddressQuestionMode)(repairedRawUserMessage || rawUserMessage);
const resolvedModeDetection = modeDetection.mode === "address_query" ? modeDetection : modeDetectionRaw;
const intentResolution = (0, addressIntentResolver_1.resolveAddressIntent)(modeSample); const intentResolution = (0, addressIntentResolver_1.resolveAddressIntent)(modeSample);
const intentResolutionRaw = (0, addressIntentResolver_1.resolveAddressIntent)(repairedRawUserMessage || rawUserMessage);
const resolvedIntentResolution = intentResolution.intent !== "unknown" ? intentResolution : intentResolutionRaw;
const llmContractIntent = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent); const llmContractIntent = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
const llmPreDecomposeReason = toNonEmptyString(llmPreDecomposeMeta?.reason); const llmPreDecomposeReason = toNonEmptyString(llmPreDecomposeMeta?.reason);
const llmRuntimeUnavailableDetected = Boolean(llmPreDecomposeReason && const llmRuntimeUnavailableDetected = Boolean(llmPreDecomposeReason &&
@ -3951,10 +4042,10 @@ export function resolveAssistantOrchestrationDecision(input) {
hasStrictDeepInvestigationCue(repairedRawUserMessage) || hasStrictDeepInvestigationCue(repairedRawUserMessage) ||
hasStrictDeepInvestigationCue(effectiveAddressUserMessage) || hasStrictDeepInvestigationCue(effectiveAddressUserMessage) ||
hasStrictDeepInvestigationCue(repairedEffectiveAddressUserMessage); hasStrictDeepInvestigationCue(repairedEffectiveAddressUserMessage);
const strictDeepInvestigationBypassAllowed = shouldBypassStrictDeepInvestigationCueForAddressIntent(intentResolution.intent) || const strictDeepInvestigationBypassAllowed = shouldBypassStrictDeepInvestigationCueForAddressIntent(resolvedIntentResolution.intent) ||
shouldBypassStrictDeepInvestigationCueForAddressIntent(llmContractIntent); shouldBypassStrictDeepInvestigationCueForAddressIntent(llmContractIntent);
const keepAddressLaneByIntent = semanticApplyCanonicalRecommended && const keepAddressLaneByIntent = semanticApplyCanonicalRecommended &&
Boolean((intentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(intentResolution.intent)) || Boolean((resolvedIntentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(resolvedIntentResolution.intent)) ||
(llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)) || (llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)) ||
openContractsAddressSignal) && openContractsAddressSignal) &&
(!strictDeepInvestigationCueDetected || strictDeepInvestigationBypassAllowed); (!strictDeepInvestigationCueDetected || strictDeepInvestigationBypassAllowed);
@ -3995,8 +4086,8 @@ export function resolveAssistantOrchestrationDecision(input) {
!capabilityMetaQuery && !capabilityMetaQuery &&
!dataRetrievalSignal && !dataRetrievalSignal &&
!effectiveAddressFollowupSignal && !effectiveAddressFollowupSignal &&
modeDetection.mode === "unsupported" && resolvedModeDetection.mode === "unsupported" &&
intentResolution.intent === "unknown"); resolvedIntentResolution.intent === "unknown");
const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate && const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate &&
deterministicNonDomainGuard && deterministicNonDomainGuard &&
(llmFirstUnsupportedCandidate || llmContractMode === null) && (llmFirstUnsupportedCandidate || llmContractMode === null) &&
@ -4016,10 +4107,10 @@ export function resolveAssistantOrchestrationDecision(input) {
orchestrationContract: { orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1", schema_version: "assistant_orchestration_contract_v1",
hard_meta_mode: "data_scope", hard_meta_mode: "data_scope",
address_mode: modeDetection.mode, address_mode: resolvedModeDetection.mode,
address_mode_confidence: modeDetection.confidence, address_mode_confidence: resolvedModeDetection.confidence,
address_intent: intentResolution.intent, address_intent: resolvedIntentResolution.intent,
address_intent_confidence: intentResolution.confidence, address_intent_confidence: resolvedIntentResolution.confidence,
strong_data_signal_detected: strongDataSignal, strong_data_signal_detected: strongDataSignal,
data_retrieval_signal_detected: dataRetrievalSignal, data_retrieval_signal_detected: dataRetrievalSignal,
followup_context_detected: Boolean(followupContext), followup_context_detected: Boolean(followupContext),
@ -4044,10 +4135,10 @@ export function resolveAssistantOrchestrationDecision(input) {
orchestrationContract: { orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1", schema_version: "assistant_orchestration_contract_v1",
hard_meta_mode: "capability", hard_meta_mode: "capability",
address_mode: modeDetection.mode, address_mode: resolvedModeDetection.mode,
address_mode_confidence: modeDetection.confidence, address_mode_confidence: resolvedModeDetection.confidence,
address_intent: intentResolution.intent, address_intent: resolvedIntentResolution.intent,
address_intent_confidence: intentResolution.confidence, address_intent_confidence: resolvedIntentResolution.confidence,
strong_data_signal_detected: strongDataSignal, strong_data_signal_detected: strongDataSignal,
data_retrieval_signal_detected: dataRetrievalSignal, data_retrieval_signal_detected: dataRetrievalSignal,
followup_context_detected: Boolean(followupContext), followup_context_detected: Boolean(followupContext),
@ -4072,10 +4163,10 @@ export function resolveAssistantOrchestrationDecision(input) {
orchestrationContract: { orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1", schema_version: "assistant_orchestration_contract_v1",
hard_meta_mode: "non_domain", hard_meta_mode: "non_domain",
address_mode: modeDetection.mode, address_mode: resolvedModeDetection.mode,
address_mode_confidence: modeDetection.confidence, address_mode_confidence: resolvedModeDetection.confidence,
address_intent: intentResolution.intent, address_intent: resolvedIntentResolution.intent,
address_intent_confidence: intentResolution.confidence, address_intent_confidence: resolvedIntentResolution.confidence,
strong_data_signal_detected: strongDataSignal, strong_data_signal_detected: strongDataSignal,
data_retrieval_signal_detected: dataRetrievalSignal, data_retrieval_signal_detected: dataRetrievalSignal,
followup_context_detected: Boolean(followupContext), followup_context_detected: Boolean(followupContext),
@ -4111,7 +4202,7 @@ export function resolveAssistantOrchestrationDecision(input) {
hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) || hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) ||
hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage)); hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage));
const supportedAddressIntentDetected = (!strictDeepInvestigationCueDetected || strictDeepInvestigationBypassAllowed) && const supportedAddressIntentDetected = (!strictDeepInvestigationCueDetected || strictDeepInvestigationBypassAllowed) &&
Boolean((intentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(intentResolution.intent)) || Boolean((resolvedIntentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(resolvedIntentResolution.intent)) ||
(llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)) || (llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)) ||
openContractsAddressSignal); openContractsAddressSignal);
const semanticGuardHints = semanticExtractionContract?.guard_hints && const semanticGuardHints = semanticExtractionContract?.guard_hints &&
@ -4131,7 +4222,7 @@ export function resolveAssistantOrchestrationDecision(input) {
semanticAggregateShapeDetected || semanticAggregateShapeDetected ||
semanticDeepInvestigationHintDetected || semanticDeepInvestigationHintDetected ||
!semanticApplyCanonicalRecommended)); !semanticApplyCanonicalRecommended));
const unsupportedIntentOrMode = (modeDetection.mode !== "address_query" && intentResolution.intent === "unknown") || const unsupportedIntentOrMode = (resolvedModeDetection.mode !== "address_query" && resolvedIntentResolution.intent === "unknown") ||
llmContractMode === "unsupported"; llmContractMode === "unsupported";
const unsupportedAddressIntentFallbackToDeep = Boolean(baseToolGate?.runAddressLane && const unsupportedAddressIntentFallbackToDeep = Boolean(baseToolGate?.runAddressLane &&
!llmRuntimeUnavailableDetected && !llmRuntimeUnavailableDetected &&
@ -4251,10 +4342,10 @@ export function resolveAssistantOrchestrationDecision(input) {
orchestrationContract: { orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1", schema_version: "assistant_orchestration_contract_v1",
hard_meta_mode: null, hard_meta_mode: null,
address_mode: modeDetection.mode, address_mode: resolvedModeDetection.mode,
address_mode_confidence: modeDetection.confidence, address_mode_confidence: resolvedModeDetection.confidence,
address_intent: intentResolution.intent, address_intent: resolvedIntentResolution.intent,
address_intent_confidence: intentResolution.confidence, address_intent_confidence: resolvedIntentResolution.confidence,
strong_data_signal_detected: strongDataSignal, strong_data_signal_detected: strongDataSignal,
data_retrieval_signal_detected: dataRetrievalSignal, data_retrieval_signal_detected: dataRetrievalSignal,
semantic_contract_valid: semanticContractValid, semantic_contract_valid: semanticContractValid,

View File

@ -14,6 +14,7 @@ import type {
NormalizedFragmentV2, NormalizedFragmentV2,
NormalizedFragmentV2_0_1, NormalizedFragmentV2_0_1,
NormalizedFragmentV2_0_2, NormalizedFragmentV2_0_2,
NormalizedFragmentSemanticHints,
NormalizedPayload, NormalizedPayload,
NormalizedQueryV1, NormalizedQueryV1,
NormalizedQueryV2, NormalizedQueryV2,
@ -352,6 +353,90 @@ function coerceFlags(
}; };
} }
function inferSemanticHints(
rawText: string,
timeScope: NormalizedFragmentV2["time_scope"]
): NormalizedFragmentSemanticHints {
return {
scope_target_kind: "none",
scope_target_text: null,
date_scope_kind: timeScope.type === "explicit" ? "explicit" : "missing",
self_scope_detected: false,
selected_object_scope_detected: /(?:по\s+выбранному\s+объекту|selected\s+object)/iu.test(String(rawText ?? ""))
};
}
function coerceSemanticScopeTargetKind(value: unknown): NormalizedFragmentSemanticHints["scope_target_kind"] {
const token = normalizeToken(value);
if (
token === "none" ||
token === "self_scope" ||
token === "selected_object" ||
token === "organization" ||
token === "warehouse" ||
token === "counterparty" ||
token === "contract" ||
token === "item"
) {
return token;
}
if (["organization_scope", "company_scope", "org_scope", "company", "organization_anchor"].includes(token)) {
return "organization";
}
if (["warehouse_scope", "stock_scope", "warehouse_anchor"].includes(token)) {
return "warehouse";
}
if (["own_company_scope", "implicit_self_scope", "our_scope"].includes(token)) {
return "self_scope";
}
if (["selected_object_scope", "selected_object_anchor"].includes(token)) {
return "selected_object";
}
return "none";
}
function coerceSemanticDateScopeKind(value: unknown): NormalizedFragmentSemanticHints["date_scope_kind"] {
const token = normalizeToken(value);
if (token === "explicit" || token === "implicit_current" || token === "missing") {
return token;
}
if (["implicit_current_snapshot", "current", "today", "default_current"].includes(token)) {
return "implicit_current";
}
return "missing";
}
function coerceSemanticHints(
value: unknown,
rawText: string,
timeScope: NormalizedFragmentV2["time_scope"]
): NormalizedFragmentSemanticHints {
const fallback = inferSemanticHints(rawText, timeScope);
if (!value || typeof value !== "object") {
return fallback;
}
const source = value as Record<string, unknown>;
return {
scope_target_kind: coerceSemanticScopeTargetKind(source.scope_target_kind ?? source.anchor_kind ?? source.scope_kind),
scope_target_text:
toOptionalString(
source.scope_target_text ??
source.anchor_value ??
source.organization ??
source.warehouse ??
source.counterparty ??
source.contract ??
source.item
) ?? fallback.scope_target_text,
date_scope_kind: coerceSemanticDateScopeKind(source.date_scope_kind ?? source.date_scope ?? source.time_scope_kind),
self_scope_detected: coerceBoolean(source.self_scope_detected, fallback.self_scope_detected),
selected_object_scope_detected: coerceBoolean(
source.selected_object_scope_detected,
fallback.selected_object_scope_detected
)
};
}
function mapCandidateLabel(value: string): NormalizedFragmentV2["candidate_labels"][number] | null { function mapCandidateLabel(value: string): NormalizedFragmentV2["candidate_labels"][number] | null {
const token = normalizeToken(value); const token = normalizeToken(value);
if (CANDIDATE_LABEL_VALUES.includes(token as NormalizedFragmentV2["candidate_labels"][number])) { if (CANDIDATE_LABEL_VALUES.includes(token as NormalizedFragmentV2["candidate_labels"][number])) {
@ -421,6 +506,7 @@ function coerceFragmentV2(rawFragment: unknown, index: number, userMessage: stri
const accountHints = coerceStringArray(source.account_hints); const accountHints = coerceStringArray(source.account_hints);
const documentHints = coerceStringArray(source.document_hints); const documentHints = coerceStringArray(source.document_hints);
const registerHints = coerceStringArray(source.register_hints); const registerHints = coerceStringArray(source.register_hints);
const timeScope = coerceTimeScope(source.time_scope, rawText, base.time_scope);
return { return {
fragment_id: coerceFragmentId(source.fragment_id, index, base.fragment_id), fragment_id: coerceFragmentId(source.fragment_id, index, base.fragment_id),
@ -432,8 +518,9 @@ function coerceFragmentV2(rawFragment: unknown, index: number, userMessage: stri
account_hints: accountHints.length > 0 ? accountHints : base.account_hints, account_hints: accountHints.length > 0 ? accountHints : base.account_hints,
document_hints: documentHints.length > 0 ? documentHints : base.document_hints, document_hints: documentHints.length > 0 ? documentHints : base.document_hints,
register_hints: registerHints.length > 0 ? registerHints : base.register_hints, register_hints: registerHints.length > 0 ? registerHints : base.register_hints,
time_scope: coerceTimeScope(source.time_scope, rawText, base.time_scope), time_scope: timeScope,
flags, flags,
semantic_hints: coerceSemanticHints(source.semantic_hints, rawText, timeScope),
candidate_labels: coerceCandidateLabels(source.candidate_labels, flags, domainRelevance, base.candidate_labels), candidate_labels: coerceCandidateLabels(source.candidate_labels, flags, domainRelevance, base.candidate_labels),
confidence: coerceConfidence(source.confidence, base.confidence) confidence: coerceConfidence(source.confidence, base.confidence)
}; };
@ -923,6 +1010,7 @@ function buildFragmentV2(rawText: string, index: number): NormalizedFragmentV2 |
} else if (flags.asks_for_exact_object_trace || flags.asks_for_ranking_or_top) { } else if (flags.asks_for_exact_object_trace || flags.asks_for_ranking_or_top) {
confidence = "high"; confidence = "high";
} }
const timeScope = inferTimeScope(text);
return { return {
fragment_id: `F${index + 1}`, fragment_id: `F${index + 1}`,
@ -940,8 +1028,9 @@ function buildFragmentV2(rawText: string, index: number): NormalizedFragmentV2 |
account_hints: extractAccounts(text), account_hints: extractAccounts(text),
document_hints: Array.from(new Set(Array.from(lower.matchAll(/(документ|реализац|поступлен|платеж|выписк|акт сверк)/g)).map((item) => item[0]))), document_hints: Array.from(new Set(Array.from(lower.matchAll(/(документ|реализац|поступлен|платеж|выписк|акт сверк)/g)).map((item) => item[0]))),
register_hints: Array.from(new Set(Array.from(lower.matchAll(/(регистр|движен|остатк|сальдо)/g)).map((item) => item[0]))), register_hints: Array.from(new Set(Array.from(lower.matchAll(/(регистр|движен|остатк|сальдо)/g)).map((item) => item[0]))),
time_scope: inferTimeScope(text), time_scope: timeScope,
flags, flags,
semantic_hints: inferSemanticHints(text, timeScope),
candidate_labels: candidateLabels, candidate_labels: candidateLabels,
confidence confidence
}; };

View File

@ -38,11 +38,40 @@ export type AddressIntent =
export type AddressResponseType = "FACTUAL_LIST" | "FACTUAL_SUMMARY" | "LIMITED_WITH_REASON"; export type AddressResponseType = "FACTUAL_LIST" | "FACTUAL_SUMMARY" | "LIMITED_WITH_REASON";
export type AddressResultMode = "heuristic_candidates" | "confirmed_balance"; export type AddressResultMode = "heuristic_candidates" | "confirmed_balance";
export type AddressEvidenceStrength = "weak" | "medium" | "strong"; export type AddressEvidenceStrength = "weak" | "medium" | "strong";
export type AddressAsOfDateBasis = "period_end" | "explicit_as_of_date" | "period_range"; export type AddressAsOfDateBasis = "period_end" | "explicit_as_of_date" | "period_range" | "implicit_current_snapshot";
export type AddressCapabilityLayer = "compute" | "navigation" | "conversational"; export type AddressCapabilityLayer = "compute" | "navigation" | "conversational";
export type AddressCapabilityRouteMode = "exact" | "heuristic"; export type AddressCapabilityRouteMode = "exact" | "heuristic";
export type AddressShadowRouteStatus = "skipped" | "planned" | "unavailable"; export type AddressShadowRouteStatus = "skipped" | "planned" | "unavailable";
export type AddressRouteExpectationStatus = "matched" | "mismatch" | "not_found"; export type AddressRouteExpectationStatus = "matched" | "mismatch" | "not_found";
export type AddressSemanticScopeKind = "none" | "explicit_anchor" | "implicit_self_scope" | "selected_object_scope";
export type AddressSemanticAnchorKind =
| "none"
| "warehouse"
| "organization"
| "counterparty"
| "contract"
| "item"
| "self_scope"
| "selected_object";
export type AddressSemanticDateScopeKind = "none" | "explicit" | "implicit_current";
export interface AddressLlmSemanticHints {
scope_target_kind: Exclude<AddressSemanticAnchorKind, "none"> | "none";
scope_target_text: string | null;
date_scope_kind: Exclude<AddressSemanticDateScopeKind, "none"> | "missing";
self_scope_detected: boolean;
selected_object_scope_detected: boolean;
}
export interface AddressSemanticFrame {
scope_kind: AddressSemanticScopeKind;
anchor_kind: AddressSemanticAnchorKind;
anchor_value: string | null;
date_scope_kind: AddressSemanticDateScopeKind;
date_basis_hint: AddressAsOfDateBasis | null;
self_scope_detected: boolean;
selected_object_scope_detected: boolean;
}
export type AddressQueryShape = export type AddressQueryShape =
| "AGGREGATE_LOOKUP" | "AGGREGATE_LOOKUP"
@ -124,6 +153,7 @@ export interface AddressFilterExtraction {
extracted_filters: AddressFilterSet; extracted_filters: AddressFilterSet;
missing_required_filters: string[]; missing_required_filters: string[];
warnings: string[]; warnings: string[];
semantic_frame?: AddressSemanticFrame;
} }
export interface AddressRecipeDefinition { export interface AddressRecipeDefinition {
@ -188,7 +218,16 @@ export interface AddressExecutionDebug {
mcp_call_status_legacy: Exclude<AddressMcpCallStatus, "materialized_but_not_anchor_matched" | "materialized_but_filtered_out_by_recipe">; mcp_call_status_legacy: Exclude<AddressMcpCallStatus, "materialized_but_not_anchor_matched" | "materialized_but_filtered_out_by_recipe">;
account_scope_mode: AddressAccountScopeMode; account_scope_mode: AddressAccountScopeMode;
account_scope_fallback_applied: boolean; account_scope_fallback_applied: boolean;
anchor_type: "account" | "counterparty" | "contract" | "document_ref" | "item" | "warehouse" | "unknown" | null; anchor_type:
| "account"
| "counterparty"
| "contract"
| "document_ref"
| "item"
| "organization"
| "warehouse"
| "unknown"
| null;
anchor_value_raw: string | null; anchor_value_raw: string | null;
anchor_value_resolved: string | null; anchor_value_resolved: string | null;
resolver_confidence: "high" | "medium" | "low" | null; resolver_confidence: "high" | "medium" | "low" | null;
@ -221,6 +260,7 @@ export interface AddressExecutionDebug {
| "rows_remaining_after_scope_filter"; | "rows_remaining_after_scope_filter";
runtime_readiness: AddressRuntimeReadiness; runtime_readiness: AddressRuntimeReadiness;
limited_reason_category: AddressLimitedReasonCategory | null; limited_reason_category: AddressLimitedReasonCategory | null;
semantic_frame?: AddressSemanticFrame | null;
response_type: AddressResponseType; response_type: AddressResponseType;
requested_result_mode?: AddressResultMode; requested_result_mode?: AddressResultMode;
result_mode?: AddressResultMode; result_mode?: AddressResultMode;

View File

@ -86,6 +86,24 @@ export type SoftAssumption =
| "problem_scan_mode_enabled"; | "problem_scan_mode_enabled";
export type RouteStatus = "routed" | "no_route"; export type RouteStatus = "routed" | "no_route";
export type NoRouteReason = "out_of_scope" | "insufficient_specificity" | "missing_mapping" | "unsupported_fragment_type"; export type NoRouteReason = "out_of_scope" | "insufficient_specificity" | "missing_mapping" | "unsupported_fragment_type";
export type FragmentScopeTargetKind =
| "none"
| "self_scope"
| "selected_object"
| "organization"
| "warehouse"
| "counterparty"
| "contract"
| "item";
export type FragmentDateScopeKind = "explicit" | "implicit_current" | "missing";
export interface NormalizedFragmentSemanticHints {
scope_target_kind: FragmentScopeTargetKind;
scope_target_text: string | null;
date_scope_kind: FragmentDateScopeKind;
self_scope_detected: boolean;
selected_object_scope_detected: boolean;
}
export interface NormalizedFragmentV2 { export interface NormalizedFragmentV2 {
fragment_id: string; fragment_id: string;
@ -113,6 +131,7 @@ export interface NormalizedFragmentV2 {
asks_for_evidence: boolean; asks_for_evidence: boolean;
mentions_period_close_context: boolean; mentions_period_close_context: boolean;
}; };
semantic_hints: NormalizedFragmentSemanticHints;
candidate_labels: IntentClass[]; candidate_labels: IntentClass[];
confidence: ConfidenceLevel; confidence: ConfidenceLevel;
} }

View File

@ -0,0 +1,183 @@
import { afterEach, describe, expect, it, vi } from "vitest";
const { executeAddressMcpQueryMock } = vi.hoisted(() => ({
executeAddressMcpQueryMock: vi.fn()
}));
vi.mock("../src/services/addressMcpClient", async () => {
const actual = await vi.importActual<typeof import("../src/services/addressMcpClient")>(
"../src/services/addressMcpClient"
);
return {
...actual,
executeAddressMcpQuery: executeAddressMcpQueryMock
};
});
import { AddressQueryService } from "../src/services/addressQueryService";
afterEach(() => {
executeAddressMcpQueryMock.mockReset();
vi.restoreAllMocks();
});
describe("implicit organization stock scope", () => {
it("uses llm semantic hints to ground informal organization wording without turning it into warehouse anchor", async () => {
executeAddressMcpQueryMock.mockResolvedValueOnce({
fetched_rows: 1,
matched_rows: 1,
raw_rows: [
{
Period: "2026-04-15T23:59:59Z",
Registrator: "Остатки товаров на складах",
AccountDt: "41.01",
AccountKt: "00.00",
Amount: 148261.67,
Quantity: 22,
SubcontoDt1: "Модуль прямоугольый 1400*110*750",
Warehouse: "Основной склад",
Organization: 'ООО "Альтернатива Плюс"'
}
],
rows: [],
error: null
});
const service = new AddressQueryService();
const result = await service.tryHandle("что на складе конторы альтернатива", {
llmSemanticHints: {
scope_target_kind: "organization",
scope_target_text: "Альтернатива",
date_scope_kind: "implicit_current",
self_scope_detected: false,
selected_object_scope_detected: false
}
});
expect(result?.handled).toBe(true);
expect(result?.reply_type).toBe("factual");
expect(result?.response_type).toBe("FACTUAL_LIST");
expect(result?.debug.detected_intent).toBe("inventory_on_hand_as_of_date");
expect(result?.debug.selected_recipe).toBe("address_inventory_on_hand_as_of_date_v1");
expect(result?.debug.mcp_call_status).toBe("matched_non_empty");
expect(result?.debug.extracted_filters?.organization).toBe("Альтернатива");
expect(result?.debug.extracted_filters?.warehouse).toBeUndefined();
expect(result?.debug.semantic_frame?.scope_kind).toBe("explicit_anchor");
expect(result?.debug.semantic_frame?.anchor_kind).toBe("organization");
expect(result?.debug.semantic_frame?.anchor_value).toBe("Альтернатива");
expect(result?.debug.as_of_date_basis).toBe("implicit_current_snapshot");
expect(String(result?.reply_text ?? "")).toContain("Модуль прямоугольый 1400*110*750");
});
it("re-grounds warehouse-like informal company wording to live organization candidate set", async () => {
executeAddressMcpQueryMock.mockResolvedValueOnce({
fetched_rows: 1,
matched_rows: 1,
raw_rows: [
{
Period: "2026-04-15T23:59:59Z",
Registrator: "Остатки товаров на складах",
AccountDt: "41.01",
AccountKt: "00.00",
Amount: 833.33,
Quantity: 1,
SubcontoDt1: "Четки Пост (84*117)",
Warehouse: "Основной склад",
Organization: "ООО КОТ ССЫТ ВО ДВОРЕ"
}
],
rows: [],
error: null
});
const service = new AddressQueryService();
const result = await service.tryHandle("что на складе конторы ссыт кот", {
activeOrganization: "ООО КОТ ССЫТ ВО ДВОРЕ",
knownOrganizations: ["ООО КОТ ССЫТ ВО ДВОРЕ", "ООО Альтернатива Плюс"]
});
expect(result?.handled).toBe(true);
expect(result?.reply_type).toBe("factual");
expect(result?.debug.extracted_filters?.organization).toBe("ООО КОТ ССЫТ ВО ДВОРЕ");
expect(result?.debug.extracted_filters?.warehouse).toBeUndefined();
expect(result?.debug.anchor_type).toBe("organization");
expect(result?.debug.reasons).toContain("warehouse_anchor_regrounded_to_organization_scope");
expect(result?.debug.reasons).toContain("organization_scope_live_grounding_recovered_rows");
expect(String(result?.reply_text ?? "")).toContain("Четки Пост (84*117)");
});
it("handles slang stock-state wording as current inventory snapshot for grounded organization scope", async () => {
executeAddressMcpQueryMock.mockResolvedValueOnce({
fetched_rows: 1,
matched_rows: 1,
raw_rows: [
{
Period: "2026-04-15T23:59:59Z",
Registrator: "Остатки товаров на складах",
AccountDt: "41.01",
AccountKt: "00.00",
Amount: 34490,
Quantity: 1,
SubcontoDt1: "Диван трехместный",
Warehouse: "Основной склад",
Organization: "ООО Альтернатива Плюс"
}
],
rows: [],
error: null
});
const service = new AddressQueryService();
const result = await service.tryHandle("чекни плиз чо там на складе альтернативы происходит", {
activeOrganization: "ООО Альтернатива Плюс",
knownOrganizations: ["ООО Альтернатива Плюс", "ООО Лайсвуд"]
});
expect(result?.handled).toBe(true);
expect(result?.reply_type).toBe("factual");
expect(result?.debug.detected_intent).toBe("inventory_on_hand_as_of_date");
expect(result?.debug.selected_recipe).toBe("address_inventory_on_hand_as_of_date_v1");
expect(result?.debug.extracted_filters?.organization).toBe("ООО Альтернатива Плюс");
expect(result?.debug.extracted_filters?.warehouse).toBeUndefined();
expect(result?.debug.as_of_date_basis).toBe("implicit_current_snapshot");
expect(String(result?.reply_text ?? "")).toContain("Диван трехместный");
});
it("handles short colloquial stock query as current inventory snapshot for grounded organization scope", async () => {
executeAddressMcpQueryMock.mockResolvedValueOnce({
fetched_rows: 1,
matched_rows: 1,
raw_rows: [
{
Period: "2026-04-15T23:59:59Z",
Registrator: "Остатки товаров на складах",
AccountDt: "41.01",
AccountKt: "00.00",
Amount: 6490,
Quantity: 1,
SubcontoDt1: "Пуф арий",
Warehouse: "Основной склад",
Organization: "ООО Альтернатива Плюс"
}
],
rows: [],
error: null
});
const service = new AddressQueryService();
const result = await service.tryHandle("че на складах альтернативы", {
activeOrganization: "ООО Альтернатива Плюс",
knownOrganizations: ["ООО Альтернатива Плюс", "ООО Лайсвуд"]
});
expect(result?.handled).toBe(true);
expect(result?.reply_type).toBe("factual");
expect(result?.debug.detected_intent).toBe("inventory_on_hand_as_of_date");
expect(result?.debug.selected_recipe).toBe("address_inventory_on_hand_as_of_date_v1");
expect(result?.debug.extracted_filters?.organization).toBe("ООО Альтернатива Плюс");
expect(result?.debug.extracted_filters?.warehouse).toBeUndefined();
expect(result?.debug.anchor_type).toBe("organization");
expect(result?.debug.as_of_date_basis).toBe("implicit_current_snapshot");
expect(String(result?.reply_text ?? "")).toContain("Пуф арий");
});
});

View File

@ -0,0 +1,98 @@
import { afterEach, describe, expect, it, vi } from "vitest";
const { executeAddressMcpQueryMock } = vi.hoisted(() => ({
executeAddressMcpQueryMock: vi.fn()
}));
vi.mock("../src/services/addressMcpClient", async () => {
const actual = await vi.importActual<typeof import("../src/services/addressMcpClient")>(
"../src/services/addressMcpClient"
);
return {
...actual,
executeAddressMcpQuery: executeAddressMcpQueryMock
};
});
import { AddressQueryService } from "../src/services/addressQueryService";
afterEach(() => {
executeAddressMcpQueryMock.mockReset();
vi.restoreAllMocks();
});
describe("implicit self-scope stock snapshot", () => {
it("does not turn 'у нас' into a literal warehouse anchor", async () => {
executeAddressMcpQueryMock.mockResolvedValueOnce({
fetched_rows: 1,
matched_rows: 1,
raw_rows: [
{
Period: "2026-04-15T23:59:59Z",
Registrator: "Остатки товаров на складах",
AccountDt: "41.01",
AccountKt: "00.00",
Amount: 498472.5,
Quantity: 3,
SubcontoDt1: "Конструкция трансформер рабочей станции 1300*900*2000",
Warehouse: "Основной склад",
Organization: 'ООО "Альтернатива Плюс"'
}
],
rows: [],
error: null
});
const service = new AddressQueryService();
const result = await service.tryHandle("что на складе у нас");
expect(result?.handled).toBe(true);
expect(result?.reply_type).toBe("factual");
expect(result?.response_type).toBe("FACTUAL_LIST");
expect(result?.debug.detected_intent).toBe("inventory_on_hand_as_of_date");
expect(result?.debug.selected_recipe).toBe("address_inventory_on_hand_as_of_date_v1");
expect(result?.debug.mcp_call_status).toBe("matched_non_empty");
expect(result?.debug.extracted_filters?.warehouse).toBeUndefined();
expect(result?.debug.as_of_date_basis).toBe("implicit_current_snapshot");
expect(result?.debug.semantic_frame?.scope_kind).toBe("implicit_self_scope");
expect(result?.debug.semantic_frame?.anchor_kind).toBe("self_scope");
expect(result?.debug.semantic_frame?.date_scope_kind).toBe("implicit_current");
expect(String(result?.reply_text ?? "")).toContain("Конструкция трансформер рабочей станции 1300*900*2000");
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
});
it("grounds implicit self-scope to active organization when one is in focus", async () => {
executeAddressMcpQueryMock.mockResolvedValueOnce({
fetched_rows: 1,
matched_rows: 1,
raw_rows: [
{
Period: "2026-04-15T23:59:59Z",
Registrator: "Остатки товаров на складах",
AccountDt: "41.01",
AccountKt: "00.00",
Amount: 34490,
Quantity: 1,
SubcontoDt1: "Диван трехместный",
Warehouse: "Основной склад",
Organization: 'ООО "Альтернатива Плюс"'
}
],
rows: [],
error: null
});
const service = new AddressQueryService();
const result = await service.tryHandle("что на складе у нас", {
activeOrganization: "ООО Альтернатива Плюс",
knownOrganizations: ["ООО Альтернатива Плюс", "ООО Лайсвуд"]
});
expect(result?.handled).toBe(true);
expect(result?.debug.extracted_filters?.organization).toBe("ООО Альтернатива Плюс");
expect(result?.debug.semantic_frame?.scope_kind).toBe("implicit_self_scope");
expect(result?.debug.semantic_frame?.anchor_kind).toBe("self_scope");
expect(result?.debug.reasons).toContain("organization_from_active_scope");
expect(String(result?.reply_text ?? "")).toContain("Диван трехместный");
});
});

View File

@ -0,0 +1,76 @@
import { describe, expect, it } from "vitest";
import { runAddressDecomposeStage } from "../src/services/address_runtime/decomposeStage";
describe("inventory root frame follow-up", () => {
it("restores the root inventory frame for a temporal patch after drilldown", () => {
const result = runAddressDecomposeStage("а на май 2020", {
previous_intent: "inventory_purchase_provenance_for_item",
previous_filters: {
item: "Кресло орион",
organization: "альтернатива",
counterparty: "альтернатива",
as_of_date: "2020-03-31",
period_from: "2020-03-01",
period_to: "2020-03-31"
},
previous_anchor_type: "item",
previous_anchor_value: "Кресло орион",
root_intent: "inventory_on_hand_as_of_date",
root_filters: {
organization: "альтернатива",
counterparty: "альтернатива",
as_of_date: "2020-03-31",
period_from: "2020-03-01",
period_to: "2020-03-31"
},
root_anchor_type: "organization",
root_anchor_value: "ООО \\Альтернатива Плюс\\",
current_frame_kind: "inventory_drilldown"
});
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("inventory_on_hand_as_of_date");
expect(result?.baseReasons).toContain("intent_restored_to_inventory_root_frame");
expect(result?.filters.extracted_filters.item).toBeUndefined();
expect(result?.filters.extracted_filters.organization).toBe("альтернатива");
expect(result?.filters.extracted_filters.counterparty).toBe("альтернатива");
expect(result?.filters.extracted_filters.period_from).toBe("2020-05-01");
expect(result?.filters.extracted_filters.period_to).toBe("2020-05-31");
expect(result?.filters.extracted_filters.as_of_date).toBe("2020-05-31");
});
it("derives a relative month from the root frame year", () => {
const result = runAddressDecomposeStage("а на май этого же года", {
previous_intent: "inventory_purchase_provenance_for_item",
previous_filters: {
item: "Кресло орион",
organization: "альтернатива",
counterparty: "альтернатива",
as_of_date: "2020-03-31",
period_from: "2020-03-01",
period_to: "2020-03-31"
},
previous_anchor_type: "item",
previous_anchor_value: "Кресло орион",
root_intent: "inventory_on_hand_as_of_date",
root_filters: {
organization: "альтернатива",
counterparty: "альтернатива",
as_of_date: "2020-03-31",
period_from: "2020-03-01",
period_to: "2020-03-31"
},
root_anchor_type: "organization",
root_anchor_value: "ООО \\Альтернатива Плюс\\",
current_frame_kind: "inventory_drilldown"
});
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("inventory_on_hand_as_of_date");
expect(result?.filters.extracted_filters.period_from).toBe("2020-05-01");
expect(result?.filters.extracted_filters.period_to).toBe("2020-05-31");
expect(result?.filters.extracted_filters.as_of_date).toBe("2020-05-31");
expect(result?.baseReasons).toContain("period_derived_from_inventory_root_frame_year");
});
});

View File

@ -521,4 +521,64 @@ describe("inventory selected-object follow-up", () => {
expect(result?.debug.rows_matched).toBeGreaterThan(0); expect(result?.debug.rows_matched).toBeGreaterThan(0);
expect(String(result?.reply_text ?? "")).not.toContain("совпадений не нашлось"); expect(String(result?.reply_text ?? "")).not.toContain("совпадений не нашлось");
}); });
it("clears carried as-of date during history recovery for selected-object provenance after dated stock slice", async () => {
executeAddressMcpQueryMock
.mockResolvedValueOnce({
fetched_rows: 0,
matched_rows: 0,
raw_rows: [],
rows: [],
error: null
})
.mockResolvedValueOnce({
fetched_rows: 1,
matched_rows: 1,
raw_rows: [
{
Period: "2020-06-18T00:00:00Z",
Registrator: "Поступление товаров и услуг 00000000101 от 18.06.2020 0:00:00",
AccountDt: "41.01",
AccountKt: "60.01",
Amount: 13490,
SubcontoDt1: "Кресло орион",
SubcontoDt3: "Основной склад",
SubcontoKt1: "ООО \\Гамма-мебель\\",
SubcontoKt2: "Договор поставки № 11 от 15.06.2020",
Organization: "ООО \\Альтернатива Плюс\\"
}
],
rows: [],
error: null
});
const service = new AddressQueryService();
const result = await service.tryHandle('По выбранному объекту "Кресло орион": кто поставил это?', {
followupContext: {
previous_intent: "inventory_on_hand_as_of_date",
previous_filters: {
as_of_date: "2020-03-31",
period_from: "2020-03-01",
period_to: "2020-03-31",
organization: "ООО \\Альтернатива Плюс\\"
},
previous_anchor_type: "counterparty",
previous_anchor_value: "ООО \\Альтернатива Плюс\\"
}
});
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.extracted_filters?.item).toBe("Кресло орион");
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-03-31");
expect(result?.debug.extracted_filters?.period_from).toBe("2020-03-01");
expect(result?.debug.extracted_filters?.period_to).toBe("2020-03-31");
expect(result?.debug.reasons).toContain("as_of_date_cleared_for_history_recovery");
expect(result?.debug.reasons).toContain("period_window_auto_broadened_to_available_data");
expect(result?.debug.limitations).toContain("as_of_date_cleared_for_history_recovery");
expect(result?.debug.limitations).toContain("period_window_auto_broadened_to_available_data");
expect(String(result?.reply_text ?? "")).toContain("ООО \\Гамма-мебель\\");
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(2);
});
}); });

View File

@ -23,4 +23,15 @@ describe("inventory warehouse anchor extraction", () => {
expect(filters.as_of_date).toBe("2019-03-31"); expect(filters.as_of_date).toBe("2019-03-31");
expect(filters.warehouse).toBeUndefined(); expect(filters.warehouse).toBeUndefined();
}); });
it("treats 'у нас' as implicit self-scope instead of literal warehouse anchor", () => {
const result = extractAddressFilters("что на складе у нас", "inventory_on_hand_as_of_date");
expect(result.extracted_filters.warehouse).toBeUndefined();
expect(result.warnings).toContain("warehouse_self_scope_detected");
expect(result.semantic_frame?.scope_kind).toBe("implicit_self_scope");
expect(result.semantic_frame?.anchor_kind).toBe("self_scope");
expect(result.semantic_frame?.anchor_value).toBeNull();
expect(result.semantic_frame?.date_scope_kind).toBe("implicit_current");
expect(result.semantic_frame?.date_basis_hint).toBe("implicit_current_snapshot");
});
}); });

View File

@ -193,4 +193,42 @@ describe("assistant address attempt runtime adapter", () => {
}) })
); );
}); });
it("forwards llm semantic hints from address runtime into lane attempt runtime input", async () => {
const runAddressLaneAttemptRuntime = vi.fn(async () => ({
response_type: "READY"
}));
const runAddressRuntime = vi.fn(async (input: any) => {
await input.runAddressLaneAttempt("что на складе конторы альтернатива", null, null, {
scope_target_kind: "organization",
scope_target_text: "Альтернатива",
date_scope_kind: "implicit_current",
self_scope_detected: false,
selected_object_scope_detected: false
});
return {
handled: false,
response: null,
addressRuntimeMetaForDeep: null
};
});
await runAssistantAddressAttemptRuntime(
buildInput({
runAddressRuntime,
runAddressLaneAttemptRuntime
})
);
expect(runAddressLaneAttemptRuntime).toHaveBeenCalledWith(
expect.objectContaining({
messageUsed: "что на складе конторы альтернатива",
llmSemanticHints: expect.objectContaining({
scope_target_kind: "organization",
scope_target_text: "Альтернатива"
})
})
);
});
}); });

View File

@ -475,6 +475,10 @@ describe("assistant address follow-up carryover", () => {
expect(calls[1].options?.followupContext?.previous_filters?.as_of_date).toBe("2020-06-30"); expect(calls[1].options?.followupContext?.previous_filters?.as_of_date).toBe("2020-06-30");
expect(calls[1].options?.followupContext?.previous_filters?.period_from).toBe("2020-06-01"); expect(calls[1].options?.followupContext?.previous_filters?.period_from).toBe("2020-06-01");
expect(calls[1].options?.followupContext?.previous_filters?.period_to).toBe("2020-06-30"); expect(calls[1].options?.followupContext?.previous_filters?.period_to).toBe("2020-06-30");
expect(calls[1].options?.followupContext?.root_intent).toBe("inventory_on_hand_as_of_date");
expect(calls[1].options?.followupContext?.root_filters?.organization).toBe("ООО \\Альтернатива Плюс\\");
expect(calls[1].options?.followupContext?.root_filters?.as_of_date).toBe("2020-06-30");
expect(calls[1].options?.followupContext?.current_frame_kind).toBe("inventory_root");
expect(calls[1].options?.followupContext?.previous_filters?.warehouse).toBe("Основной склад"); expect(calls[1].options?.followupContext?.previous_filters?.warehouse).toBe("Основной склад");
expect(normalizerService.normalize).not.toHaveBeenCalled(); expect(normalizerService.normalize).not.toHaveBeenCalled();
}); });

View File

@ -11,6 +11,7 @@ function buildInput(overrides: Record<string, unknown> = {}) {
carryMeta: { followupContext: { previous_intent: "docs_by_counterparty" } }, carryMeta: { followupContext: { previous_intent: "docs_by_counterparty" } },
analysisDateHint: "2020-08-31", analysisDateHint: "2020-08-31",
activeOrganization: "Org A", activeOrganization: "Org A",
knownOrganizations: ["Org A", "Org B"],
mergeFollowupContextWithOrganizationScope, mergeFollowupContextWithOrganizationScope,
runAddressQueryTryHandle, runAddressQueryTryHandle,
...overrides ...overrides
@ -24,6 +25,7 @@ describe("assistant address lane attempt input builder", () => {
expect(runtimeInput.messageUsed).toBe("Show overdue docs"); expect(runtimeInput.messageUsed).toBe("Show overdue docs");
expect(runtimeInput.analysisDateHint).toBe("2020-08-31"); expect(runtimeInput.analysisDateHint).toBe("2020-08-31");
expect(runtimeInput.activeOrganization).toBe("Org A"); expect(runtimeInput.activeOrganization).toBe("Org A");
expect(runtimeInput.knownOrganizations).toEqual(["Org A", "Org B"]);
expect(runtimeInput.carryMeta).toEqual({ expect(runtimeInput.carryMeta).toEqual({
followupContext: { previous_intent: "docs_by_counterparty" } followupContext: { previous_intent: "docs_by_counterparty" }
}); });
@ -37,6 +39,7 @@ describe("assistant address lane attempt input builder", () => {
carryMeta: null, carryMeta: null,
analysisDateHint: null, analysisDateHint: null,
activeOrganization: null, activeOrganization: null,
knownOrganizations: [],
mergeFollowupContextWithOrganizationScope, mergeFollowupContextWithOrganizationScope,
runAddressQueryTryHandle runAddressQueryTryHandle
}) })

View File

@ -23,7 +23,10 @@ describe("assistant address lane attempt query options builder", () => {
scopedFollowupContext: { scopedFollowupContext: {
previous_intent: "docs_by_counterparty", previous_intent: "docs_by_counterparty",
active_organization: "Org A" active_organization: "Org A"
} },
activeOrganization: "Org A",
knownOrganizations: ["Org A", "Org B"],
llmSemanticHints: null
}); });
expect(options).toEqual({ expect(options).toEqual({
@ -31,14 +34,19 @@ describe("assistant address lane attempt query options builder", () => {
previous_intent: "docs_by_counterparty", previous_intent: "docs_by_counterparty",
active_organization: "Org A" active_organization: "Org A"
}, },
analysisDateHint: "2020-07-31" analysisDateHint: "2020-07-31",
activeOrganization: "Org A",
knownOrganizations: ["Org A", "Org B"]
}); });
}); });
it("builds query options with only analysis date when scoped context is missing", () => { it("builds query options with only analysis date when scoped context is missing", () => {
const options = buildAssistantAddressLaneAttemptQueryOptions({ const options = buildAssistantAddressLaneAttemptQueryOptions({
analysisDateHint: null, analysisDateHint: null,
scopedFollowupContext: null scopedFollowupContext: null,
activeOrganization: null,
knownOrganizations: [],
llmSemanticHints: null
}); });
expect(options).toEqual({ expect(options).toEqual({

View File

@ -15,6 +15,7 @@ describe("assistant address lane attempt runtime adapter", () => {
}, },
analysisDateHint: "2020-07-31", analysisDateHint: "2020-07-31",
activeOrganization: "ООО Тест", activeOrganization: "ООО Тест",
knownOrganizations: ["ООО Тест", "ООО Лютик"],
mergeFollowupContextWithOrganizationScope: () => ({ mergeFollowupContextWithOrganizationScope: () => ({
previous_intent: "docs_by_counterparty", previous_intent: "docs_by_counterparty",
active_organization: "ООО Тест" active_organization: "ООО Тест"
@ -27,7 +28,9 @@ describe("assistant address lane attempt runtime adapter", () => {
previous_intent: "docs_by_counterparty", previous_intent: "docs_by_counterparty",
active_organization: "ООО Тест" active_organization: "ООО Тест"
}, },
analysisDateHint: "2020-07-31" analysisDateHint: "2020-07-31",
activeOrganization: "ООО Тест",
knownOrganizations: ["ООО Тест", "ООО Лютик"]
}); });
expect(result).toEqual({ expect(result).toEqual({
response_type: "READY" response_type: "READY"
@ -41,6 +44,7 @@ describe("assistant address lane attempt runtime adapter", () => {
carryMeta: null, carryMeta: null,
analysisDateHint: null, analysisDateHint: null,
activeOrganization: null, activeOrganization: null,
knownOrganizations: [],
mergeFollowupContextWithOrganizationScope: () => null, mergeFollowupContextWithOrganizationScope: () => null,
runAddressQueryTryHandle runAddressQueryTryHandle
}); });
@ -49,4 +53,36 @@ describe("assistant address lane attempt runtime adapter", () => {
analysisDateHint: null analysisDateHint: null
}); });
}); });
it("forwards llm semantic hints into query options", async () => {
const runAddressQueryTryHandle = vi.fn(async () => ({
response_type: "READY"
}));
await runAssistantAddressLaneAttemptRuntime({
messageUsed: "что на складе конторы альтернатива",
carryMeta: null,
analysisDateHint: null,
llmSemanticHints: {
scope_target_kind: "organization",
scope_target_text: "Альтернатива",
date_scope_kind: "implicit_current",
self_scope_detected: false,
selected_object_scope_detected: false
},
activeOrganization: null,
knownOrganizations: ["ООО Альтернатива Плюс"],
mergeFollowupContextWithOrganizationScope: () => null,
runAddressQueryTryHandle
});
expect(runAddressQueryTryHandle).toHaveBeenCalledWith("что на складе конторы альтернатива", {
analysisDateHint: null,
knownOrganizations: ["ООО Альтернатива Плюс"],
llmSemanticHints: expect.objectContaining({
scope_target_kind: "organization",
scope_target_text: "Альтернатива"
})
});
});
}); });

View File

@ -164,6 +164,59 @@ describe("assistant address llm pre-decompose candidate preference", () => {
const addressQueryService = { const addressQueryService = {
tryHandle: vi.fn(async (message: string) => { tryHandle: vi.fn(async (message: string) => {
calls.push({ message }); calls.push({ message });
if (message === "получить остатки по складу для организации 'альтернатива'") {
return {
handled: true,
reply_text: `handled: ${message}`,
reply_type: "factual",
response_type: "FACTUAL_LIST",
debug: {
detected_mode: "address_query",
detected_mode_confidence: "high",
query_shape: "UNKNOWN",
query_shape_confidence: "low",
detected_intent: "inventory_on_hand_as_of_date",
detected_intent_confidence: "high",
extracted_filters: {
sort: "period_desc",
organization: "альтернатива",
counterparty: "альтернатива",
as_of_date: "2026-04-15"
},
missing_required_filters: [],
selected_recipe: "address_inventory_on_hand_as_of_date_v1",
mcp_call_status_legacy: "matched_non_empty",
account_scope_mode: "strict",
account_scope_fallback_applied: false,
anchor_type: "counterparty",
anchor_value_raw: "альтернатива",
anchor_value_resolved: "ООО \\Альтернатива Плюс\\",
resolver_confidence: "medium",
ambiguity_count: 0,
match_failure_stage: "none",
match_failure_reason: null,
mcp_call_status: "matched_non_empty",
rows_fetched: 1,
raw_rows_received: 1,
rows_after_account_scope: 1,
rows_after_recipe_filter: 1,
rows_materialized: 1,
rows_matched: 1,
raw_row_keys_sample: [],
materialization_drop_reason: "none",
account_token_raw: null,
account_token_normalized: null,
account_scope_fields_checked: ["account_dt", "account_kt", "registrator", "analytics"],
account_scope_match_strategy: "account_code_regex_plus_alias_map_v1",
account_scope_drop_reason: "not_applicable",
runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS",
limited_reason_category: null,
response_type: "FACTUAL_LIST",
limitations: [],
reasons: ["inventory_on_hand_signal_detected"]
}
};
}
return buildAddressLaneResult(message); return buildAddressLaneResult(message);
}) })
} as any; } as any;
@ -449,6 +502,177 @@ describe("assistant address llm pre-decompose candidate preference", () => {
]).toContain(response.debug?.llm_decomposition_reason); ]).toContain(response.debug?.llm_decomposition_reason);
}); });
it("prefers raw selected-object sale follow-up when llm rewrite drifts into generic open-items intent", async () => {
const calls: Array<{ message: string }> = [];
const addressQueryService = {
tryHandle: vi.fn(async (message: string) => {
calls.push({ message });
return buildAddressLaneResult(message);
})
} as any;
const normalizerService = {
normalize: vi.fn(async (payload: any) => {
if (payload?.userQuestion === "какие остатки по складу у альтернативы") {
return {
trace_id: "norm-predecompose-root-stock",
ok: true,
normalized: {
schema_version: "normalized_query_v2_0_2",
user_message_raw: "какие остатки по складу у альтернативы",
message_in_scope: true,
scope_confidence: "medium",
contains_multiple_tasks: false,
fragments: [
{
fragment_id: "F1",
raw_fragment_text: "какие остатки по складу у альтернативы",
normalized_fragment_text: "получить остатки по складу для организации 'альтернатива'",
domain_relevance: "in_scope",
business_scope: "company_specific_accounting",
entity_hints: [],
account_hints: [],
document_hints: [],
register_hints: [],
time_scope: {
type: "missing",
value: null,
confidence: "low"
},
flags: {
has_multi_entity_scope: false,
asks_for_chain_explanation: false,
asks_for_ranking_or_top: false,
asks_for_period_summary: false,
asks_for_rule_check: false,
asks_for_anomaly_scan: false,
asks_for_exact_object_trace: false,
asks_for_evidence: false,
mentions_period_close_context: false
},
candidate_labels: ["simple_factual"],
confidence: "medium",
execution_readiness: "executable",
clarification_reason: null,
soft_assumption_used: [],
route_status: "routed",
no_route_reason: null
}
],
discarded_fragments: [],
global_notes: {
needs_clarification: false,
clarification_reason: null
}
},
raw_model_output: null,
validation: { passed: true, errors: [] },
usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 },
latency_ms: 10,
prompt_version: "normalizer_v2_0_2",
schema_version: "v2_0_2",
request_count_for_case: 1
};
}
return {
trace_id: "norm-predecompose-selected-object-sale-drift",
ok: true,
normalized: {
schema_version: "normalized_query_v2_0_2",
user_message_raw:
'По выбранному объекту "Рабочая станция универсального специалиста (индивидуальное изготовление)": кому мы это продали в итоге',
message_in_scope: true,
scope_confidence: "medium",
contains_multiple_tasks: false,
fragments: [
{
fragment_id: "F1",
raw_fragment_text:
'По выбранному объекту "Рабочая станция универсального специалиста (индивидуальное изготовление)": кому мы это продали в итоге',
normalized_fragment_text:
"Определить контрагента, которому была реализована позиция «Рабочая станция универсального специалиста (индивидуальное изготовление)» по выбранному объекту",
domain_relevance: "in_scope",
business_scope: "company_specific_accounting",
entity_hints: [],
account_hints: [],
document_hints: [],
register_hints: [],
time_scope: {
type: "missing",
value: null,
confidence: "low"
},
flags: {
has_multi_entity_scope: false,
asks_for_chain_explanation: false,
asks_for_ranking_or_top: false,
asks_for_period_summary: false,
asks_for_rule_check: false,
asks_for_anomaly_scan: false,
asks_for_exact_object_trace: true,
asks_for_evidence: false,
mentions_period_close_context: false
},
candidate_labels: ["simple_factual"],
confidence: "medium",
execution_readiness: "executable",
clarification_reason: null,
soft_assumption_used: [],
route_status: "routed",
no_route_reason: null
}
],
discarded_fragments: [],
global_notes: {
needs_clarification: false,
clarification_reason: null
}
},
raw_model_output: null,
validation: { passed: true, errors: [] },
usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 },
latency_ms: 10,
prompt_version: "normalizer_v2_0_2",
schema_version: "v2_0_2",
request_count_for_case: 1
};
})
} as any;
const sessions = new AssistantSessionStore();
const service = new AssistantService(
normalizerService,
sessions as any,
{} as any,
{ persistSession: vi.fn() } as any,
addressQueryService
);
const sessionId = `asst-predecompose-selected-object-sale-${Date.now()}`;
await service.handleMessage({
session_id: sessionId,
user_message: "какие остатки по складу у альтернативы",
llmProvider: "local",
useMock: false
} as any);
const response = await service.handleMessage({
session_id: sessionId,
user_message:
'По выбранному объекту "Рабочая станция универсального специалиста (индивидуальное изготовление)": кому мы это продали в итоге',
llmProvider: "local",
useMock: false
} as any);
expect(response.ok).toBe(true);
expect(response.reply_type).toBe("factual");
expect(calls).toHaveLength(2);
expect(calls[1].message).toBe(
'По выбранному объекту "Рабочая станция универсального специалиста (индивидуальное изготовление)": кому мы это продали в итоге'
);
expect(response.debug?.llm_decomposition_reason).toBe("followup_raw_message_preferred_over_llm_rewrite");
});
it("does not treat service verb as counterparty anchor when llm rewrites noisy bank phrase", async () => { it("does not treat service verb as counterparty anchor when llm rewrites noisy bank phrase", async () => {
const calls: Array<{ message: string }> = []; const calls: Array<{ message: string }> = [];
const addressQueryService = { const addressQueryService = {

View File

@ -99,5 +99,142 @@ describe("assistant address orchestration runtime adapter", () => {
expect(output.livingModeDecision.mode).toBe("chat"); expect(output.livingModeDecision.mode).toBe("chat");
expect(output.addressRuntimeMeta.toolGateDecision).toBe("skip_address_lane"); expect(output.addressRuntimeMeta.toolGateDecision).toBe("skip_address_lane");
}); });
it("prefers raw short follow-up over unsupported llm rewrite when carryover context exists", async () => {
const resolveAddressFollowupCarryoverContext = vi.fn(() => ({
followupContext: {
previous_intent: "inventory_on_hand_as_of_date",
previous_filters: {
organization: "ООО \\Альтернатива Плюс\\",
as_of_date: "2026-04-15"
}
}
}));
const resolveAssistantOrchestrationDecision = vi.fn(() => ({
runAddressLane: true,
livingMode: "address_data",
livingReason: "address_lane_triggered",
toolGateDecision: "run_address_lane",
toolGateReason: "followup_context_detected",
orchestrationContract: { schema_version: "assistant_orchestration_contract_v1" }
}));
const buildAddressLlmPredecomposeContractV1 = vi.fn(({ sourceMessage, canonicalMessage }: { sourceMessage: string; canonicalMessage: string }) => ({
schema_version: "address_llm_predecompose_contract_v1",
source_message: sourceMessage,
canonical_message: canonicalMessage,
mode: canonicalMessage === sourceMessage ? "address_query" : "unsupported",
intent: canonicalMessage === sourceMessage ? "inventory_on_hand_as_of_date" : "unknown"
}));
const output = await buildAssistantAddressOrchestrationRuntime(
buildInput({
userMessage: "ахуен а на март 2020",
runAddressLlmPreDecompose: vi.fn(async () => ({
attempted: true,
applied: true,
effectiveMessage: "что не так в бухгалтерии за март 2020 года?",
reason: "normalized_fragment_applied",
predecomposeContract: {
mode: "unsupported",
intent: "unknown"
}
})),
buildAddressLlmPredecomposeContractV1,
resolveAddressFollowupCarryoverContext,
resolveAssistantOrchestrationDecision
})
);
expect(output.addressInputMessage).toBe("ахуен а на март 2020");
expect(output.addressPreDecompose.applied).toBe(false);
expect(output.addressPreDecompose.reason).toBe("followup_raw_message_preferred_over_llm_rewrite");
expect(output.addressPreDecompose.predecomposeContract).toEqual(
expect.objectContaining({
canonical_message: "ахуен а на март 2020",
mode: "address_query",
intent: "inventory_on_hand_as_of_date"
})
);
expect(buildAddressLlmPredecomposeContractV1).toHaveBeenCalledWith({
sourceMessage: "ахуен а на март 2020",
canonicalMessage: "ахуен а на март 2020"
});
expect(resolveAddressFollowupCarryoverContext).toHaveBeenCalledTimes(2);
expect(resolveAssistantOrchestrationDecision).toHaveBeenCalledWith(
expect.objectContaining({
rawUserMessage: "ахуен а на март 2020",
effectiveAddressUserMessage: "ахуен а на март 2020"
})
);
}); });
it("prefers raw selected-object inventory action over generic canonical drift intent", async () => {
const resolveAddressFollowupCarryoverContext = vi.fn(() => ({
followupContext: {
previous_intent: "inventory_on_hand_as_of_date",
previous_filters: {
organization: "ООО \\Альтернатива Плюс\\",
as_of_date: "2016-06-30",
period_from: "2016-06-01",
period_to: "2016-06-30"
}
}
}));
const resolveAssistantOrchestrationDecision = vi.fn(() => ({
runAddressLane: true,
livingMode: "address_data",
livingReason: "address_lane_triggered",
toolGateDecision: "run_address_lane",
toolGateReason: "address_mode_classifier_detected",
orchestrationContract: { schema_version: "assistant_orchestration_contract_v1" }
}));
const buildAddressLlmPredecomposeContractV1 = vi.fn(({ sourceMessage, canonicalMessage }: { sourceMessage: string; canonicalMessage: string }) => ({
schema_version: "address_llm_predecompose_contract_v1",
source_message: sourceMessage,
canonical_message: canonicalMessage,
mode: "address_query",
intent: "unknown"
}));
const rawMessage =
'По выбранному объекту "Рабочая станция универсального специалиста (индивидуальное изготовление)": кому мы это продали в итоге';
const output = await buildAssistantAddressOrchestrationRuntime(
buildInput({
userMessage: rawMessage,
runAddressLlmPreDecompose: vi.fn(async () => ({
attempted: true,
applied: true,
effectiveMessage:
"Определить контрагента, которому была реализована позиция «Рабочая станция универсального специалиста (индивидуальное изготовление)» по выбранному объекту",
reason: "normalized_fragment_applied",
predecomposeContract: {
mode: "address_query",
intent: "open_items_by_counterparty_or_contract",
semantics: {
selected_object_scope_detected: true
}
}
})),
buildAddressLlmPredecomposeContractV1,
resolveAddressFollowupCarryoverContext,
resolveAssistantOrchestrationDecision
})
);
expect(output.addressInputMessage).toBe(rawMessage);
expect(output.addressPreDecompose.applied).toBe(false);
expect(output.addressPreDecompose.reason).toBe("followup_raw_message_preferred_over_llm_rewrite");
expect(buildAddressLlmPredecomposeContractV1).toHaveBeenCalledWith({
sourceMessage: rawMessage,
canonicalMessage: rawMessage
});
expect(resolveAddressFollowupCarryoverContext).toHaveBeenCalledTimes(2);
expect(resolveAssistantOrchestrationDecision).toHaveBeenCalledWith(
expect.objectContaining({
rawUserMessage: rawMessage,
effectiveAddressUserMessage: rawMessage
})
);
});
});

View File

@ -173,7 +173,7 @@ describe("assistant address runtime adapter", () => {
runAddressLaneRuntime runAddressLaneRuntime
}); });
expect(runAddressLaneAttempt).toHaveBeenCalledWith("canon", null, "2020-07-31"); expect(runAddressLaneAttempt).toHaveBeenCalledWith("canon", null, "2020-07-31", null);
expect(finalizeAddressLaneResponse).toHaveBeenCalledWith( expect(finalizeAddressLaneResponse).toHaveBeenCalledWith(
{ handled: true }, { handled: true },
"canon", "canon",
@ -193,4 +193,87 @@ describe("assistant address runtime adapter", () => {
} }
}); });
}); });
it("passes llm semantic hints from orchestration metadata into lane attempts", async () => {
const runAddressLaneAttempt = vi.fn(async () => ({
handled: true
}));
const result = await runAssistantAddressRuntime({
featureAssistantAddressQueryV1: true,
sessionId: "asst-4",
userMessage: "что на складе конторы альтернатива",
sessionItems: [],
llmProvider: "local",
useMock: false,
featureAddressLlmPredecomposeV1: true,
runAddressLlmPreDecompose: async () => ({}),
buildAddressLlmPredecomposeContractV1: () => ({}),
sanitizeAddressMessageForFallback: (value) => value,
toNonEmptyString: (value) => (typeof value === "string" && value.trim() ? value.trim() : null),
resolveAddressFollowupCarryoverContext: () => null,
resolveAssistantOrchestrationDecision: () => ({}),
buildAddressDialogContinuationContractV2: () => ({}),
runtimeAnalysisContextAsOfDate: null,
payloadContextPeriodHint: null,
compactWhitespace: (value) => value.replace(/\s+/g, " ").trim(),
runAddressLaneAttempt,
isRetryableAddressLimitedResult: () => false,
finalizeAddressLaneResponse: () => ({ ok: "address" }),
tryHandleLivingChat: async () => null,
logEvent: () => {},
nowIso: () => "2026-04-10T00:00:00.000Z",
runAddressOrchestrationRuntime: async () => ({
addressPreDecompose: {},
addressInputMessage: "что на складе конторы альтернатива",
carryover: null,
orchestrationDecision: { runAddressLane: true },
addressRuntimeMeta: {
attempted: true,
semanticHints: {
scope_target_kind: "organization",
scope_target_text: "Альтернатива",
date_scope_kind: "implicit_current",
self_scope_detected: false,
selected_object_scope_detected: false
}
},
livingModeDecision: { mode: "address_data", reason: "address_lane_triggered" }
}),
runAddressToolGateRuntime: async () => ({
handled: false,
response: null
}),
runAddressLaneRuntime: async (input) => {
const addressLane = await input.runAddressLaneAttempt(input.addressInputMessage, null, input.llmSemanticHints ?? null);
return {
handled: true,
selection: {
addressLane: addressLane ?? { handled: true },
messageUsed: input.addressInputMessage,
carryMeta: null
},
retryAudit: {
attempted: false,
reason: null,
initial_limited_category: null,
retry_message: null,
retry_used_followup_context: false,
retry_result_category: null
}
};
}
});
expect(runAddressLaneAttempt).toHaveBeenCalledWith(
"что на складе конторы альтернатива",
null,
null,
expect.objectContaining({
scope_target_kind: "organization",
scope_target_text: "Альтернатива"
})
);
expect(result.handled).toBe(true);
});
}); });

View File

@ -1,5 +1,9 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { resolveAssistantOrchestrationDecision, resolveLivingAssistantModeDecision } from "../src/services/assistantService"; import { resolveAssistantOrchestrationDecision, resolveLivingAssistantModeDecision } from "../src/services/assistantService";
import {
buildAddressLlmPredecomposeContractV1,
buildAddressSemanticExtractionContractV1
} from "../src/services/address_runtime/predecomposeContract";
describe("assistant living router mode decision", () => { describe("assistant living router mode decision", () => {
it("returns address_data when address lane already triggered", () => { it("returns address_data when address lane already triggered", () => {
@ -471,7 +475,9 @@ describe("assistant orchestration contract", () => {
expect(decision.livingMode).toBe("address_data"); expect(decision.livingMode).toBe("address_data");
expect(decision.toolGateDecision).toBe("run_address_lane"); expect(decision.toolGateDecision).toBe("run_address_lane");
expect(["address_signal_detected", "address_intent_resolver_detected"]).toContain(String(decision.toolGateReason)); expect(["address_signal_detected", "address_intent_resolver_detected", "address_mode_classifier_detected"]).toContain(
String(decision.toolGateReason)
);
expect(decision.livingReason).toBe("address_lane_triggered"); expect(decision.livingReason).toBe("address_lane_triggered");
}); });
@ -772,6 +778,95 @@ describe("assistant orchestration contract", () => {
expect(decision.livingReason).toBe("address_lane_triggered"); expect(decision.livingReason).toBe("address_lane_triggered");
}); });
it("keeps slang stock-state query with organization scope in address lane instead of deep fallback", () => {
const rawUserMessage = "чекни плиз чо там на складе альтернативы происходит";
const effectiveAddressUserMessage = "проверь, что происходит на складе у компании 'альтернатива'";
const predecomposeContract = buildAddressLlmPredecomposeContractV1({
sourceMessage: rawUserMessage,
canonicalMessage: effectiveAddressUserMessage,
semanticHints: {
scope_target_kind: "organization",
scope_target_text: "альтернатива",
date_scope_kind: "implicit_current",
self_scope_detected: false,
selected_object_scope_detected: false
}
});
const semanticExtractionContract = buildAddressSemanticExtractionContractV1({
sourceMessage: rawUserMessage,
canonicalMessage: effectiveAddressUserMessage,
predecomposeContract
});
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage,
effectiveAddressUserMessage,
followupContext: null,
llmPreDecomposeMeta: {
applied: true,
llmCanonicalCandidateDetected: true,
predecomposeContract,
semanticExtractionContract
} as any,
useMock: false
});
expect(decision.runAddressLane).toBe(true);
expect(decision.toolGateDecision).toBe("run_address_lane");
expect(decision.livingMode).toBe("address_data");
expect(decision.livingReason).toBe("address_lane_triggered");
expect(decision.orchestrationContract?.unsupported_address_intent_fallback_to_deep).toBe(false);
expect(decision.orchestrationContract?.deep_analysis_signal_fallback_to_deep).toBe(false);
expect(decision.orchestrationContract?.semantic_route_arbitration?.supported_address_intent_detected).toBe(true);
});
it("keeps short colloquial stock query with organization scope in address lane instead of chat fallback", () => {
const rawUserMessage = "че на складах альтернативы";
const effectiveAddressUserMessage = "что находится на складах у компании 'альтернатива'";
const predecomposeContract = buildAddressLlmPredecomposeContractV1({
sourceMessage: rawUserMessage,
canonicalMessage: effectiveAddressUserMessage,
semanticHints: {
scope_target_kind: "organization",
scope_target_text: "альтернатива",
date_scope_kind: "implicit_current",
self_scope_detected: false,
selected_object_scope_detected: false
}
});
const semanticExtractionContract = buildAddressSemanticExtractionContractV1({
sourceMessage: rawUserMessage,
canonicalMessage: effectiveAddressUserMessage,
predecomposeContract
});
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage,
effectiveAddressUserMessage,
followupContext: null,
llmPreDecomposeMeta: {
applied: true,
llmCanonicalCandidateDetected: true,
predecomposeContract,
semanticExtractionContract
} as any,
useMock: false
});
expect(decision.runAddressLane).toBe(true);
expect(decision.toolGateDecision).toBe("run_address_lane");
expect([
"address_intent_resolver_detected",
"address_mode_classifier_detected",
"llm_canonical_data_signal_detected",
"address_signal_detected"
]).toContain(
String(decision.toolGateReason)
);
expect(decision.livingMode).toBe("address_data");
expect(decision.livingReason).toBe("address_lane_triggered");
});
it("keeps open-contracts request in address lane even with stale deep followup context when LLM contract is absent", () => { it("keeps open-contracts request in address lane even with stale deep followup context when LLM contract is absent", () => {
const decision = resolveAssistantOrchestrationDecision({ const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "Покажи незакрытые договоры на 2020-12-31", rawUserMessage: "Покажи незакрытые договоры на 2020-12-31",

View File

@ -0,0 +1,37 @@
import { describe, expect, it } from "vitest";
import {
mergeKnownOrganizations,
normalizeOrganizationScopeSearchText,
resolveOrganizationSelectionFromMessage,
scoreOrganizationMentionInMessage
} from "../src/services/assistantOrganizationMatcher";
describe("assistant organization matcher", () => {
it("deduplicates known organizations by normalized search key", () => {
expect(
mergeKnownOrganizations([
'ООО "Альтернатива Плюс"',
"ооо альтернатива плюс",
"ООО Лайсвуд"
])
).toEqual(['ООО "Альтернатива Плюс"', "ООО Лайсвуд"]);
});
it("matches incomplete or reordered organization mention against live candidates", () => {
const resolved = resolveOrganizationSelectionFromMessage("дай что сегодня на складе в конторе ссыт кот", [
"ООО КОТ ССЫТ ВО ДВОРЕ",
"ООО Альтернатива Плюс"
]);
expect(resolved).toBe("ООО КОТ ССЫТ ВО ДВОРЕ");
});
it("scores direct and fuzzy token overlap above ambiguity threshold", () => {
const score = scoreOrganizationMentionInMessage(
normalizeOrganizationScopeSearchText("что на складе конторы альтернатива"),
'ООО "Альтернатива Плюс"'
);
expect(score).toBeGreaterThanOrEqual(90);
});
});

View File

@ -58,5 +58,71 @@ describe("address semantic extraction contract", () => {
expect(semantic.apply_canonical_recommended).toBe(true); expect(semantic.apply_canonical_recommended).toBe(true);
expect(["high", "medium"]).toContain(semantic.quality); expect(["high", "medium"]).toContain(semantic.quality);
}); });
it("marks self-scope stock snapshot wording as implicit current scope, not explicit date", () => {
const sourceMessage = "что на складе у нас";
const predecomposeContract = buildAddressLlmPredecomposeContractV1({
sourceMessage,
canonicalMessage: sourceMessage
}); });
expect(predecomposeContract.intent).toBe("inventory_on_hand_as_of_date");
expect(predecomposeContract.period.has_explicit_period).toBe(false);
expect(predecomposeContract.semantics.scope_kind).toBe("implicit_self_scope");
expect(predecomposeContract.semantics.anchor_kind).toBe("self_scope");
expect(predecomposeContract.semantics.date_scope_kind).toBe("implicit_current");
expect(predecomposeContract.semantics.date_basis_hint).toBe("implicit_current_snapshot");
});
it("accepts llm semantic hints for organization-scoped informal warehouse wording", () => {
const sourceMessage = "что на складе конторы альтернатива";
const predecomposeContract = buildAddressLlmPredecomposeContractV1({
sourceMessage,
canonicalMessage: sourceMessage,
semanticHints: {
scope_target_kind: "organization",
scope_target_text: "Альтернатива",
date_scope_kind: "implicit_current",
self_scope_detected: false,
selected_object_scope_detected: false
}
});
expect(predecomposeContract.intent).toBe("inventory_on_hand_as_of_date");
expect(predecomposeContract.entities.organization).toBe("Альтернатива");
expect(predecomposeContract.entities.counterparty).toBeNull();
expect(predecomposeContract.semantics.scope_kind).toBe("explicit_anchor");
expect(predecomposeContract.semantics.anchor_kind).toBe("organization");
expect(predecomposeContract.semantics.anchor_value).toBe("Альтернатива");
expect(predecomposeContract.period.has_explicit_period).toBe(false);
expect(predecomposeContract.semantics.date_scope_kind).toBe("implicit_current");
});
it("keeps slang stock-state rewrite as address snapshot instead of deep investigation", () => {
const sourceMessage = "чекни плиз чо там на складе альтернативы происходит";
const canonicalMessage = "проверь, что происходит на складе у компании 'альтернатива'";
const predecomposeContract = buildAddressLlmPredecomposeContractV1({
sourceMessage,
canonicalMessage,
semanticHints: {
scope_target_kind: "organization",
scope_target_text: "альтернатива",
date_scope_kind: "implicit_current",
self_scope_detected: false,
selected_object_scope_detected: false
}
});
const semantic = buildAddressSemanticExtractionContractV1({
sourceMessage,
canonicalMessage,
predecomposeContract
});
expect(predecomposeContract.mode).toBe("address_query");
expect(predecomposeContract.intent).toBe("inventory_on_hand_as_of_date");
expect(predecomposeContract.entities.organization).toBe("альтернатива");
expect(semantic.guard_hints.deep_investigation_signal_detected).toBe(false);
expect(semantic.guard_hints.canonical_data_signal_detected).toBe(true);
expect(semantic.valid).toBe(true);
expect(semantic.apply_canonical_recommended).toBe(true);
});
});

View File

@ -13,7 +13,8 @@ Core behavior (v2.0.2):
- soft_assumption_used - soft_assumption_used
- route_status - route_status
- no_route_reason - no_route_reason
5. Clarification must be rare and justified. 5. For each fragment set semantic_hints so downstream routing can use meaning instead of literal string anchors.
6. Clarification must be rare and justified.
Execution-state policy: Execution-state policy:
- Every in-scope fragment must produce a consistent execution state. - Every in-scope fragment must produce a consistent execution state.
@ -53,6 +54,7 @@ Fragment required fields:
- register_hints - register_hints
- time_scope - time_scope
- flags - flags
- semantic_hints
- candidate_labels - candidate_labels
- confidence - confidence
- execution_readiness - execution_readiness
@ -66,6 +68,27 @@ Soft assumptions (`soft_assumption_used`) allowed values:
- company_scope_defaulted - company_scope_defaulted
- problem_scan_mode_enabled - problem_scan_mode_enabled
semantic_hints fields:
- scope_target_kind: none | self_scope | selected_object | organization | warehouse | counterparty | contract | item
- scope_target_text: short user-facing mention when scope_target_kind is organization/warehouse/counterparty/contract/item
- date_scope_kind: explicit | implicit_current | missing
- self_scope_detected: true when wording means "our own scope" or "this connected company"
- selected_object_scope_detected: true when wording refers to currently selected object/item
Semantic-hints policy:
- Use semantic_hints to preserve meaning of colloquial or elliptical wording.
- Do not convert vague possessive wording into a fake literal anchor.
- If user means "our company / our connected base / current selected scope", prefer self_scope_detected=true and scope_target_kind=self_scope.
- If user refers to a company or organization colloquially, prefer scope_target_kind=organization, not warehouse.
- If user refers to the selected row/object/item, prefer selected_object_scope_detected=true and scope_target_kind=selected_object or item when item text is explicit.
- Do not invent exact database names. Use short text from the user in scope_target_text.
Examples:
- "что на складе у нас" -> semantic_hints.scope_target_kind=self_scope; self_scope_detected=true; date_scope_kind=implicit_current
- "что на складе конторы альтернатива" -> semantic_hints.scope_target_kind=organization; scope_target_text="альтернатива"; date_scope_kind=implicit_current
- "по выбранному объекту ... кто поставщик" -> semantic_hints.scope_target_kind=selected_object; selected_object_scope_detected=true
- "по ней какие документы" -> semantic_hints.scope_target_kind=selected_object; selected_object_scope_detected=true
Global notes: Global notes:
- global_notes.needs_clarification should be true only when execution is truly blocked for all in-scope fragments. - global_notes.needs_clarification should be true only when execution is truly blocked for all in-scope fragments.
- global_notes.clarification_reason must explain the blocker. - global_notes.clarification_reason must explain the blocker.

View File

@ -23,3 +23,14 @@
Важное правило: Важное правило:
Если в одном вопросе есть и риск-лексика, и цепочка document/payment/posting, не понижать задачу до чистого `store_feature_risk`. Если в одном вопросе есть и риск-лексика, и цепочка document/payment/posting, не понижать задачу до чистого `store_feature_risk`.
Приоритет у causal cross-entity семантики. Приоритет у causal cross-entity семантики.
Неформальные scope-формулировки:
- "у нас", "у себя", "по нашей базе", "в нашей конторе" обычно означают self/company scope, а не буквальный якорь склада;
- "контора альтернатива", "альтернатива", "по фирме альтернатива" обычно означают organization scope, а не склад;
- "по выбранному объекту", "по ней", "по этой позиции", "по этому товару" обычно означают selected object scope.
Для semantic_hints:
- если речь про текущую подключенную компанию/нашу базу -> scope_target_kind=self_scope;
- если речь про организацию/фирму/контору -> scope_target_kind=organization;
- если речь про выбранную позицию/объект -> scope_target_kind=selected_object;
- для складских snapshot-вопросов без даты обычно date_scope_kind=implicit_current.