АРЧ - Ассистент: отделить reference-срез от execution-окна в ответах по lifecycle follow-up

This commit is contained in:
dctouch 2026-04-15 14:50:16 +03:00
parent 70cc5a99f1
commit 7a6d8eb070
44 changed files with 2394 additions and 118 deletions

View File

@ -1352,6 +1352,10 @@ function hasSelectedObjectInventoryPurchaseDocumentsSignal(text) {
return (hasSelectedObjectInventoryCue(text) && return (hasSelectedObjectInventoryCue(text) &&
/(?:по\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|по\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|какими\s+документами\s+(?:был\s+)?куплен|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(text)); /(?:по\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|по\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|какими\s+документами\s+(?:был\s+)?куплен|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(text));
} }
function hasSelectedObjectInventorySaleTraceSignal(text) {
return (hasSelectedObjectInventoryCue(text) &&
/(?:кому\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали|кому\s+был\s+продан|куда\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|куда\s+(?:была\s+)?реализована\s+(?:позиция|номенклатура|продукция)|кто\s+купил|buyer|sale\s+trace|trace\s+of\s+sale)/iu.test(text));
}
function hasInventoryProvenanceSignalV2(text) { function hasInventoryProvenanceSignalV2(text) {
const hasItemCue = /(?:товар|номенклатур|sku|item|product|остат(?:ок|ки)|склад)/iu.test(text); const hasItemCue = /(?:товар|номенклатур|sku|item|product|остат(?:ок|ки)|склад)/iu.test(text);
const hasSupplierCue = /(?:от\s+какого\s+поставщика|у\s+какого\s+поставщика|от\s+кого\s+куплен|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|товар|позицию))?|где\s+куплено|кто\s+(?:нам\s+)?поставил|кем\s+поставлен|поставщик|supplier|vendor)/iu.test(text); const hasSupplierCue = /(?:от\s+какого\s+поставщика|у\s+какого\s+поставщика|от\s+кого\s+куплен|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|товар|позицию))?|где\s+куплено|кто\s+(?:нам\s+)?поставил|кем\s+поставлен|поставщик|supplier|vendor)/iu.test(text);
@ -1369,8 +1373,8 @@ function hasInventoryPurchaseDocumentsSignalV2(text) {
return hasItemCue && hasPurchaseDocCue; return hasItemCue && hasPurchaseDocCue;
} }
function hasInventorySaleTraceSignalV2(text) { function hasInventorySaleTraceSignalV2(text) {
const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text); const hasItemCue = /(?:товар|номенклатур|sku|item|product|позици(?:я|ю|и)|продукци(?:я|ю|и))/iu.test(text);
const hasTraceCue = /(?:кому\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали|кому\s+был\s+продан|кто\s+купил|buyer|sale\s+trace|trace\s+of\s+sale|через\s+какие\s+документы\s+прош[её]л\s+путь\s+товара|закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*warehouse\s*->\s*sale|purchase\s*->\s*stock\s*->\s*sale)/iu.test(text); const hasTraceCue = /(?:кому\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали|кому\s+был\s+продан|куда\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали(?:\s+(?:это|его|товар|позицию))?|куда\s+(?:была\s+)?реализована\s+(?:позиция|номенклатура|продукция)|кто\s+купил|buyer|sale\s+trace|trace\s+of\s+sale|через\s+какие\s+документы\s+прош[её]л\s+путь\s+товара|закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*warehouse\s*->\s*sale|purchase\s*->\s*stock\s*->\s*sale)/iu.test(text);
return hasItemCue && hasTraceCue; return hasItemCue && hasTraceCue;
} }
function hasInventorySupplierStockOverlapSignal(text) { function hasInventorySupplierStockOverlapSignal(text) {
@ -1588,6 +1592,13 @@ function resolveAddressIntent(userMessage) {
reasons: ["inventory_purchase_documents_signal_detected"] reasons: ["inventory_purchase_documents_signal_detected"]
}; };
} }
if (hasSelectedObjectInventorySaleTraceSignal(text)) {
return {
intent: "inventory_sale_trace_for_item",
confidence: "medium",
reasons: ["inventory_selected_object_sale_trace_signal_detected"]
};
}
if (hasInventorySaleTraceSignalV2(text)) { if (hasInventorySaleTraceSignalV2(text)) {
return { return {
intent: "inventory_sale_trace_for_item", intent: "inventory_sale_trace_for_item",

View File

@ -21,7 +21,14 @@ const DISPLAY_ENTITY_TYPE_BY_INTENT = {
list_documents_by_contract: "document_ref", list_documents_by_contract: "document_ref",
bank_operations_by_counterparty: "document_ref", bank_operations_by_counterparty: "document_ref",
bank_operations_by_contract: "document_ref", bank_operations_by_contract: "document_ref",
open_items_by_counterparty_or_contract: "counterparty" open_items_by_counterparty_or_contract: "counterparty",
inventory_on_hand_as_of_date: "item",
inventory_purchase_provenance_for_item: "item",
inventory_purchase_documents_for_item: "item",
inventory_supplier_stock_overlap_as_of_date: "item",
inventory_sale_trace_for_item: "item",
inventory_purchase_to_sale_chain: "item",
inventory_aging_by_purchase_date: "item"
}; };
const RESULT_SET_TYPE_BY_INTENT = { const RESULT_SET_TYPE_BY_INTENT = {
counterparty_activity_lifecycle: "counterparty_list", counterparty_activity_lifecycle: "counterparty_list",
@ -39,6 +46,13 @@ const RESULT_SET_TYPE_BY_INTENT = {
bank_operations_by_counterparty: "bank_operations_list", bank_operations_by_counterparty: "bank_operations_list",
bank_operations_by_contract: "bank_operations_list", bank_operations_by_contract: "bank_operations_list",
open_items_by_counterparty_or_contract: "open_items_list", open_items_by_counterparty_or_contract: "open_items_list",
inventory_on_hand_as_of_date: "inventory_snapshot",
inventory_purchase_provenance_for_item: "inventory_trace",
inventory_purchase_documents_for_item: "inventory_trace",
inventory_supplier_stock_overlap_as_of_date: "inventory_trace",
inventory_sale_trace_for_item: "inventory_trace",
inventory_purchase_to_sale_chain: "inventory_trace",
inventory_aging_by_purchase_date: "inventory_trace",
period_coverage_profile: "profile_summary", period_coverage_profile: "profile_summary",
document_type_and_account_section_profile: "profile_summary", document_type_and_account_section_profile: "profile_summary",
counterparty_population_and_roles: "profile_summary", counterparty_population_and_roles: "profile_summary",
@ -64,7 +78,13 @@ function toAddressFocusObjectType(value) {
if (!normalized) { if (!normalized) {
return "unknown"; return "unknown";
} }
if (normalized === "counterparty" || normalized === "contract" || normalized === "document_ref" || normalized === "account") { if (normalized === "counterparty" ||
normalized === "contract" ||
normalized === "document_ref" ||
normalized === "account" ||
normalized === "item" ||
normalized === "organization" ||
normalized === "warehouse") {
return normalized; return normalized;
} }
return "unknown"; return "unknown";
@ -127,6 +147,38 @@ function extractEntityRefsFromAssistantReply(replyText, intent, limit = MAX_ENTI
} }
return Array.from(dedup.values()); return Array.from(dedup.values());
} }
function extractOrganizationsFromAssistantReply(replyText, limit = 10) {
const dedup = new Map();
const lines = String(replyText ?? "").split(/\r?\n/);
for (const line of lines) {
const match = line.match(/(?:^|\|)\s*организац(?:ия|ии)\s*:\s*([^|]+)/iu);
if (!match) {
continue;
}
const organization = toNonEmptyString(match[1]);
if (!organization) {
continue;
}
const key = organization.toLowerCase();
if (!dedup.has(key)) {
dedup.set(key, organization);
}
if (dedup.size >= limit) {
break;
}
}
return Array.from(dedup.values());
}
function resolveDerivedOrganizationScope(debug, filters, replyText) {
const rootFrameContext = toObject(debug.address_root_frame_context) ?? {};
const candidates = [
toNonEmptyString(filters.organization),
toNonEmptyString(rootFrameContext.organization),
...extractOrganizationsFromAssistantReply(replyText)
].filter((value) => Boolean(value));
const dedup = Array.from(new Map(candidates.map((value) => [value.toLowerCase(), value])).values());
return dedup.length === 1 ? dedup[0] : null;
}
function cloneFocusObject(value) { function cloneFocusObject(value) {
if (!value) { if (!value) {
return null; return null;
@ -345,6 +397,13 @@ function evolveAddressNavigationStateWithAssistantItem(state, item, turnIndex) {
const resultSetId = `rs-${item.message_id}`; const resultSetId = `rs-${item.message_id}`;
const routeId = toNonEmptyString(debug.selected_recipe); const routeId = toNonEmptyString(debug.selected_recipe);
const filters = normalizeFilters(debug.extracted_filters); const filters = normalizeFilters(debug.extracted_filters);
const derivedOrganizationScope = resolveDerivedOrganizationScope(debug, filters, item.text);
const filtersWithDerivedScope = derivedOrganizationScope && !toNonEmptyString(filters.organization)
? {
...filters,
organization: derivedOrganizationScope
}
: filters;
const sourceRefs = routeId ? [routeId] : []; const sourceRefs = routeId ? [routeId] : [];
const entityRefs = extractEntityRefsFromAssistantReply(item.text, intent); const entityRefs = extractEntityRefsFromAssistantReply(item.text, intent);
const resultSet = { const resultSet = {
@ -352,7 +411,7 @@ function evolveAddressNavigationStateWithAssistantItem(state, item, turnIndex) {
type: inferResultSetType(intent), type: inferResultSetType(intent),
intent, intent,
route_id: routeId, route_id: routeId,
filters, filters: filtersWithDerivedScope,
source_refs: sourceRefs, source_refs: sourceRefs,
entity_refs: entityRefs, entity_refs: entityRefs,
created_from_turn: turnIndex, created_from_turn: turnIndex,
@ -371,11 +430,11 @@ function evolveAddressNavigationStateWithAssistantItem(state, item, turnIndex) {
created_at: createdAt created_at: createdAt
}; };
const normalizedDateScope = { const normalizedDateScope = {
as_of_date: toNonEmptyString(filters.as_of_date), as_of_date: toNonEmptyString(filtersWithDerivedScope.as_of_date),
period_from: toNonEmptyString(filters.period_from), period_from: toNonEmptyString(filtersWithDerivedScope.period_from),
period_to: toNonEmptyString(filters.period_to) period_to: toNonEmptyString(filtersWithDerivedScope.period_to)
}; };
const organizationScope = toNonEmptyString(filters.organization); const organizationScope = toNonEmptyString(filtersWithDerivedScope.organization);
const nextResultSets = capResultSets([...state.result_sets.filter((itemSet) => itemSet.result_set_id !== resultSetId), resultSet].sort((left, right) => left.created_from_turn - right.created_from_turn)); const nextResultSets = capResultSets([...state.result_sets.filter((itemSet) => itemSet.result_set_id !== resultSetId), resultSet].sort((left, right) => left.created_from_turn - right.created_from_turn));
const nextEvents = capNavigationEvents([...state.navigation_history, navigationEvent]); const nextEvents = capNavigationEvents([...state.navigation_history, navigationEvent]);
return { return {

View File

@ -1219,8 +1219,32 @@ function applyPreExecutionOrganizationScopeGrounding(input) {
input.semanticFrame.anchor_value = resolvedOrganizationFromMessage; input.semanticFrame.anchor_value = resolvedOrganizationFromMessage;
} }
} }
if (!input.filters.organization && !activeOrganization && !resolvedOrganizationFromMessage && candidateOrganizations.length === 1) {
input.filters.organization = candidateOrganizations[0];
if (!input.warnings.includes("organization_auto_selected_from_single_scope_candidate")) {
input.warnings.push("organization_auto_selected_from_single_scope_candidate");
}
if (!input.baseReasons.includes("organization_auto_selected_from_single_scope_candidate")) {
input.baseReasons.push("organization_auto_selected_from_single_scope_candidate");
}
if (input.semanticFrame?.anchor_kind === "organization") {
input.semanticFrame.anchor_value = candidateOrganizations[0];
}
}
return resolvedOrganizationFromMessage; return resolvedOrganizationFromMessage;
} }
function isOrganizationScopedInventoryIntent(intent) {
return (intent === "inventory_on_hand_as_of_date" ||
intent === "inventory_purchase_provenance_for_item" ||
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date");
}
function collectOrganizationCandidatesFromRows(rows) {
return (0, assistantOrganizationMatcher_1.mergeKnownOrganizations)(rows.map((row) => row.organization).filter((value) => Boolean(value)));
}
function isHeuristicCandidatesIntent(intent) { function isHeuristicCandidatesIntent(intent) {
return (intent === "list_receivables_counterparties" || return (intent === "list_receivables_counterparties" ||
intent === "list_payables_counterparties" || intent === "list_payables_counterparties" ||
@ -1587,7 +1611,21 @@ function shouldBoostAutoBroadenedLimit(intent) {
intent === "inventory_aging_by_purchase_date"); intent === "inventory_aging_by_purchase_date");
} }
function shouldClearAsOfDateForHistoryRecovery(intent) { function shouldClearAsOfDateForHistoryRecovery(intent) {
return intent === "inventory_purchase_provenance_for_item" || intent === "inventory_purchase_documents_for_item"; 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");
}
function shouldDetachLifecycleExecutionFromSnapshotContext(intent, reasons) {
if (intent !== "inventory_purchase_provenance_for_item" &&
intent !== "inventory_purchase_documents_for_item" &&
intent !== "inventory_sale_trace_for_item" &&
intent !== "inventory_purchase_to_sale_chain") {
return false;
}
return (reasons.includes("as_of_date_from_followup_context") ||
reasons.includes("period_from_followup_context") ||
reasons.includes("as_of_date_from_open_items_followup_context"));
} }
function invertSort(sort) { function invertSort(sort) {
return sort === "period_asc" ? "period_desc" : "period_asc"; return sort === "period_asc" ? "period_desc" : "period_asc";
@ -2241,6 +2279,48 @@ function buildLimitedExecutionResult(input) {
} }
}; };
} }
function composeOrganizationClarificationReply(organizations) {
const normalizedOrganizations = (0, assistantOrganizationMatcher_1.mergeKnownOrganizations)(organizations).slice(0, 10);
const lines = [
normalizedOrganizations.length > 1
? "Нужно уточнить организацию, чтобы не смешивать компании в одном ответе."
: "Нужно уточнить организацию, чтобы продолжить запрос.",
normalizedOrganizations.length > 0
? "Сейчас в доступном контуре вижу такие организации:"
: "Уточни, по какой организации продолжать."
];
for (const organization of normalizedOrganizations) {
lines.push(`- ${organization}`);
}
lines.push("Можешь ответить просто названием компании, и я продолжу этот же запрос.");
return lines.join("\n");
}
function buildOrganizationClarificationExecutionResult(input) {
const result = buildLimitedExecutionResult({
mode: input.mode,
shape: input.shape,
intent: input.intent,
filters: input.filters,
missingRequiredFilters: ["organization"],
selectedRecipe: null,
anchor: input.anchor,
mcpCallStatus: "skipped",
rowsFetched: 0,
rowsMatched: 0,
category: "missing_anchor",
reasonText: "не указана организация, а в доступном контуре найдено несколько компаний",
nextStep: "уточните организацию из списка, и я продолжу этот же запрос",
limitations: ["organization_clarification_required", "multiple_known_organizations_detected"],
reasons: [...input.reasons, "organization_clarification_required", "multiple_known_organizations_detected"],
semanticFrame: input.semanticFrame,
capabilityAudit: input.capabilityAudit,
shadowRouteAudit: input.shadowRouteAudit,
routeExpectationAudit: input.routeExpectationAudit
});
result.reply_text = composeOrganizationClarificationReply(input.organizations);
result.debug.organization_candidates = (0, assistantOrganizationMatcher_1.mergeKnownOrganizations)(input.organizations);
return result;
}
class AddressQueryService { class AddressQueryService {
async tryHandle(userMessage, options = {}) { async tryHandle(userMessage, options = {}) {
if (!config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1) { if (!config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1) {
@ -2277,6 +2357,29 @@ class AddressQueryService {
activeOrganization: options.activeOrganization ?? null, activeOrganization: options.activeOrganization ?? null,
knownOrganizations: options.knownOrganizations ?? [] knownOrganizations: options.knownOrganizations ?? []
}); });
const knownOrganizations = (0, assistantOrganizationMatcher_1.mergeKnownOrganizations)(options.knownOrganizations ?? []);
const activeOrganization = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(options.activeOrganization ?? null);
if (isOrganizationScopedInventoryIntent(intent.intent) &&
!toNonEmptyFilterValue(filters.extracted_filters.organization) &&
!activeOrganization &&
!resolvedOrganizationFromMessage &&
knownOrganizations.length > 1) {
return buildOrganizationClarificationExecutionResult({
mode,
shape,
intent,
filters: filters.extracted_filters,
organizations: knownOrganizations,
reasons: [...baseReasons, "organization_candidates_from_scope_context"],
semanticFrame,
capabilityAudit: buildCapabilityAudit(intent.intent),
shadowRouteAudit: buildShadowRouteAudit({
intent: intent.intent,
requestedResultMode: resolveRequestedResultMode(intent.intent, filters.extracted_filters, semanticFrame),
filters: filters.extracted_filters
})
});
}
const requestedResultMode = resolveRequestedResultMode(intent.intent, filters.extracted_filters, semanticFrame); 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";
@ -2336,6 +2439,44 @@ class AddressQueryService {
baseReasons.push("as_of_date_derived_for_inventory_on_hand"); baseReasons.push("as_of_date_derived_for_inventory_on_hand");
} }
} }
if (shouldDetachLifecycleExecutionFromSnapshotContext(intent.intent, baseReasons)) {
const detachedExecutionFilters = { ...executionFilters };
let periodDetached = false;
let asOfDetached = false;
if (toNonEmptyFilterValue(detachedExecutionFilters.period_from) ||
toNonEmptyFilterValue(detachedExecutionFilters.period_to)) {
delete detachedExecutionFilters.period_from;
delete detachedExecutionFilters.period_to;
periodDetached = true;
}
if (toNonEmptyFilterValue(detachedExecutionFilters.as_of_date)) {
delete detachedExecutionFilters.as_of_date;
asOfDetached = true;
}
if (periodDetached || asOfDetached) {
executionFilters = detachedExecutionFilters;
if (periodDetached && !filters.warnings.includes("period_window_detached_for_lifecycle_execution")) {
filters.warnings.push("period_window_detached_for_lifecycle_execution");
}
if (periodDetached && !baseReasons.includes("period_window_detached_for_lifecycle_execution")) {
baseReasons.push("period_window_detached_for_lifecycle_execution");
}
if ((periodDetached || asOfDetached) &&
!filters.warnings.includes("lifecycle_execution_detached_from_snapshot_date")) {
filters.warnings.push("lifecycle_execution_detached_from_snapshot_date");
}
if ((periodDetached || asOfDetached) &&
!baseReasons.includes("lifecycle_execution_detached_from_snapshot_date")) {
baseReasons.push("lifecycle_execution_detached_from_snapshot_date");
}
if (asOfDetached && !filters.warnings.includes("as_of_date_cleared_for_history_recovery")) {
filters.warnings.push("as_of_date_cleared_for_history_recovery");
}
if (asOfDetached && !baseReasons.includes("as_of_date_cleared_for_history_recovery")) {
baseReasons.push("as_of_date_cleared_for_history_recovery");
}
}
}
const capabilityDecision = (0, addressCapabilityPolicy_1.resolveAddressCapabilityRouteDecision)(intent.intent); const capabilityDecision = (0, addressCapabilityPolicy_1.resolveAddressCapabilityRouteDecision)(intent.intent);
const capabilityAudit = buildCapabilityAudit(intent.intent); const capabilityAudit = buildCapabilityAudit(intent.intent);
const shadowRouteAudit = buildShadowRouteAudit({ const shadowRouteAudit = buildShadowRouteAudit({
@ -2789,6 +2930,41 @@ class AddressQueryService {
baseReasons.push("organization_scope_live_grounding_recovered_rows"); baseReasons.push("organization_scope_live_grounding_recovered_rows");
} }
} }
if (filteredRows.length > 0 &&
isOrganizationScopedInventoryIntent(intent.intent) &&
!toNonEmptyFilterValue(filters.extracted_filters.organization)) {
const observedOrganizations = collectOrganizationCandidatesFromRows(filteredRows);
if (observedOrganizations.length === 1) {
filters.extracted_filters = {
...filters.extracted_filters,
organization: observedOrganizations[0]
};
executionFilters = {
...executionFilters,
organization: observedOrganizations[0]
};
if (!filters.warnings.includes("organization_grounded_from_observed_rows")) {
filters.warnings.push("organization_grounded_from_observed_rows");
}
if (!baseReasons.includes("organization_grounded_from_observed_rows")) {
baseReasons.push("organization_grounded_from_observed_rows");
}
}
else if (observedOrganizations.length > 1) {
return buildOrganizationClarificationExecutionResult({
mode,
shape,
intent,
filters: filters.extracted_filters,
anchor,
organizations: observedOrganizations,
reasons: [...baseReasons, "organization_candidates_from_observed_rows"],
semanticFrame,
capabilityAudit,
shadowRouteAudit
});
}
}
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;
@ -2989,7 +3165,7 @@ class AddressQueryService {
const broadenedAdjustments = []; 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)) { if (shouldClearAsOfDateForHistoryRecovery(intent.intent) && toNonEmptyFilterValue(autoBroadenedFilters.as_of_date)) {
delete autoBroadenedFilters.as_of_date; delete autoBroadenedFilters.as_of_date;
broadenedAdjustments.push("as_of_date_cleared_for_history_recovery"); broadenedAdjustments.push("as_of_date_cleared_for_history_recovery");
} }

View File

@ -34,7 +34,8 @@ const INVENTORY_MOVEMENTS_QUERY_TEMPLATE = `
ПРЕДСТАВЛЕНИЕ(Движения.СубконтоДт3) КАК СубконтоДт3, ПРЕДСТАВЛЕНИЕ(Движения.СубконтоДт3) КАК СубконтоДт3,
ПРЕДСТАВЛЕНИЕ(Движения.СубконтоКт1) КАК СубконтоКт1, ПРЕДСТАВЛЕНИЕ(Движения.СубконтоКт1) КАК СубконтоКт1,
ПРЕДСТАВЛЕНИЕ(Движения.СубконтоКт2) КАК СубконтоКт2, ПРЕДСТАВЛЕНИЕ(Движения.СубконтоКт2) КАК СубконтоКт2,
ПРЕДСТАВЛЕНИЕ(Движения.СубконтоКт3) КАК СубконтоКт3 ПРЕДСТАВЛЕНИЕ(Движения.СубконтоКт3) КАК СубконтоКт3,
ПРЕДСТАВЛЕНИЕ(Движения.Организация) КАК Организация
ИЗ ИЗ
РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто КАК Движения РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто КАК Движения
__WHERE_CLAUSE__ __WHERE_CLAUSE__
@ -934,6 +935,18 @@ function toDateTimeExpr(isoDate, endOfDay) {
const second = endOfDay ? 59 : 0; const second = endOfDay ? 59 : 0;
return `ДАТАВРЕМЯ(${year}, ${month}, ${day}, ${hour}, ${minute}, ${second})`; return `ДАТАВРЕМЯ(${year}, ${month}, ${day}, ${hour}, ${minute}, ${second})`;
} }
function toQueryStringLiteral(value) {
return String(value ?? "").replace(/"/g, '""');
}
function buildOrganizationPresentationCondition(filters, fieldPath) {
const organization = typeof filters.organization === "string" && filters.organization.trim().length > 0
? filters.organization.trim()
: "";
if (!organization) {
return null;
}
return `ПРЕДСТАВЛЕНИЕ(${fieldPath}) = "${toQueryStringLiteral(organization)}"`;
}
function buildWhereClause(filters, fieldPath, extraConditions = []) { function buildWhereClause(filters, fieldPath, extraConditions = []) {
const periodFromExpr = typeof filters.period_from === "string" && filters.period_from.trim().length > 0 const periodFromExpr = typeof filters.period_from === "string" && filters.period_from.trim().length > 0
? toDateTimeExpr(filters.period_from, false) ? toDateTimeExpr(filters.period_from, false)
@ -1074,9 +1087,10 @@ function buildInventoryMovementQuery(filters, resolvedLimit, side) {
: side === "kt" : side === "kt"
? creditPredicate ? creditPredicate
: `(${debitPredicate} ИЛИ ${creditPredicate})`; : `(${debitPredicate} ИЛИ ${creditPredicate})`;
const organizationCondition = buildOrganizationPresentationCondition(filters, "Движения.Организация");
return INVENTORY_MOVEMENTS_QUERY_TEMPLATE return INVENTORY_MOVEMENTS_QUERY_TEMPLATE
.replace("__LIMIT__", String(resolvedLimit)) .replace("__LIMIT__", String(resolvedLimit))
.replace("__WHERE_CLAUSE__", buildWhereClause(filters, "Движения.Период", [inventoryCondition])) .replace("__WHERE_CLAUSE__", buildWhereClause(filters, "Движения.Период", [inventoryCondition, organizationCondition].filter((item) => Boolean(item))))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort)); .replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
} }
function shouldBoostLimitForAllTimeCounterparty(filters) { function shouldBoostLimitForAllTimeCounterparty(filters) {

View File

@ -264,6 +264,12 @@ function isInventoryDrilldownFrameIntent(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 isInventoryLifecycleHistoryIntent(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");
}
function buildInventoryRootFollowupContext(followupContext) { function buildInventoryRootFollowupContext(followupContext) {
if (!followupContext || !followupContext.root_intent || !followupContext.root_filters) { if (!followupContext || !followupContext.root_intent || !followupContext.root_filters) {
return followupContext; return followupContext;
@ -412,7 +418,7 @@ function hasBareInventoryPurchaseDateFollowupCue(text) {
return /(?:^|\s)(?:когда|а\s+когда|ну\s+когда)(?=$|[\s,.;:!?])/iu.test(normalized) && normalized.split(/\s+/).filter(Boolean).length <= 3; return /(?:^|\s)(?:когда|а\s+когда|ну\s+когда)(?=$|[\s,.;:!?])/iu.test(normalized) && normalized.split(/\s+/).filter(Boolean).length <= 3;
} }
function hasInventorySaleFollowupCue(text) { function hasInventorySaleFollowupCue(text) {
return /(?:кому\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали|кому\s+был\s+продан|кому\s+дальше\s+продали|кто\s+купил|buyer|покупател)/iu.test(String(text ?? "")); return /(?:кому\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали|кому\s+был\s+продан|кому\s+дальше\s+продали|куда\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали|куда\s+ушла\s+позиция|куда\s+ушел\s+товар|кто\s+купил|buyer|покупател)/iu.test(String(text ?? ""));
} }
function hasInventoryPurchaseToSaleChainFollowupCue(text) { function hasInventoryPurchaseToSaleChainFollowupCue(text) {
return /(?:через\s+какие\s+документы\s+прош[её]л\s+путь\s+товара|закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*(?:warehouse|stock)\s*->\s*sale)/iu.test(String(text ?? "")); return /(?:через\s+какие\s+документы\s+прош[её]л\s+путь\s+товара|закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*(?:warehouse|stock)\s*->\s*sale)/iu.test(String(text ?? ""));
@ -618,13 +624,10 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
} }
} }
if (!sameDateRequested && if (!sameDateRequested &&
(intent === "inventory_purchase_provenance_for_item" || (intent === "inventory_aging_by_purchase_date" || isInventoryLifecycleHistoryIntent(intent)) &&
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date") &&
!hasExplicitPeriodLiteral(userMessage) && !hasExplicitPeriodLiteral(userMessage) &&
!hasExplicitCurrentDateHint(userMessage)) { !hasExplicitCurrentDateHint(userMessage)) {
if (intent === "inventory_aging_by_purchase_date") {
const inheritedAsOfDate = previousAsOfDate ?? previousPeriodTo ?? previousPeriodFrom; const inheritedAsOfDate = previousAsOfDate ?? previousPeriodTo ?? previousPeriodFrom;
const currentAsOfDate = toNonEmptyString(merged.as_of_date); const currentAsOfDate = toNonEmptyString(merged.as_of_date);
const todayIso = new Date().toISOString().slice(0, 10); const todayIso = new Date().toISOString().slice(0, 10);
@ -634,6 +637,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
reasons.push("as_of_date_from_followup_context"); reasons.push("as_of_date_from_followup_context");
} }
} }
}
if (!sameDateRequested && if (!sameDateRequested &&
(intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date") && (intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date") &&
!hasExplicitPeriodLiteral(userMessage) && !hasExplicitPeriodLiteral(userMessage) &&
@ -706,6 +710,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
const hasFollowupSignal = hasAddressFollowupContextSignal(userMessage); const hasFollowupSignal = hasAddressFollowupContextSignal(userMessage);
const hasExplicitPeriodInMessage = hasExplicitPeriodLiteral(userMessage); const hasExplicitPeriodInMessage = hasExplicitPeriodLiteral(userMessage);
const hasExplicitCurrentDateInMessage = hasExplicitCurrentDateHint(userMessage); const hasExplicitCurrentDateInMessage = hasExplicitCurrentDateHint(userMessage);
const inventoryLifecycleHistoryIntent = isInventoryLifecycleHistoryIntent(intent);
const asOfPrimaryIntent = intent === "account_balance_snapshot" || const asOfPrimaryIntent = intent === "account_balance_snapshot" ||
intent === "documents_forming_balance" || intent === "documents_forming_balance" ||
intent === "open_contracts_confirmed_as_of_date" || intent === "open_contracts_confirmed_as_of_date" ||
@ -739,7 +744,11 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
reasons.push("period_from_followup_context"); reasons.push("period_from_followup_context");
} }
} }
if (!currentHasPeriod && previousHasPeriod && hasFollowupSignal && !hasExplicitPeriodInMessage) { if (!currentHasPeriod &&
previousHasPeriod &&
hasFollowupSignal &&
!hasExplicitPeriodInMessage &&
!inventoryLifecycleHistoryIntent) {
if (previousPeriodFrom) { if (previousPeriodFrom) {
merged.period_from = previousPeriodFrom; merged.period_from = previousPeriodFrom;
} }

View File

@ -108,6 +108,8 @@ async function runAssistantAddressAttemptRuntime(input) {
sessionId: input.sessionId, sessionId: input.sessionId,
userMessage: input.userMessage, userMessage: input.userMessage,
sessionItems: input.sessionItems, sessionItems: input.sessionItems,
sessionAddressNavigationState: input.sessionAddressNavigationState,
sessionScope: input.sessionScope,
payload: input.payload, payload: input.payload,
featureAddressLlmPredecomposeV1: input.featureAddressLlmPredecomposeV1, featureAddressLlmPredecomposeV1: input.featureAddressLlmPredecomposeV1,
runAddressLlmPreDecompose: input.runAddressLlmPreDecompose, runAddressLlmPreDecompose: input.runAddressLlmPreDecompose,

View File

@ -151,7 +151,13 @@ function runAssistantAddressLaneResponseRuntime(input) {
if (followupOffer) { if (followupOffer) {
debug.address_followup_offer = followupOffer; debug.address_followup_offer = followupOffer;
} }
const debugKnownOrganizations = input.mergeKnownOrganizations(input.knownOrganizations); const laneOrganizationCandidates = Array.isArray(input.addressLane.debug?.organization_candidates)
? input.addressLane.debug.organization_candidates
: [];
const debugKnownOrganizations = input.mergeKnownOrganizations([
...input.knownOrganizations,
...laneOrganizationCandidates
]);
const debugFilters = debug?.extracted_filters && typeof debug.extracted_filters === "object" const debugFilters = debug?.extracted_filters && typeof debug.extracted_filters === "object"
? debug.extracted_filters ? debug.extracted_filters
: null; : null;

View File

@ -5,7 +5,7 @@ function hasSelectedObjectInventorySignal(text) {
return /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(String(text ?? "")); return /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(String(text ?? ""));
} }
function hasSelectedObjectInventoryActionCue(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 ?? "")); return /(?:кому[\s\S]{0,80}продал[аи]?|кому[\s\S]{0,80}реализова[нлт][а-я]*|кому\s+был\s+продан|куда[\s\S]{0,80}продал[аи]?|куда[\s\S]{0,80}реализова[нлт][а-я]*|кто[\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) { function isGenericCanonicalDriftIntent(intent) {
return (intent === "open_items_by_counterparty_or_contract" || return (intent === "open_items_by_counterparty_or_contract" ||
@ -62,7 +62,7 @@ async function buildAssistantAddressOrchestrationRuntime(input) {
: fallbackAddressPreDecompose(input.userMessage, input.llmProvider, input.buildAddressLlmPredecomposeContractV1, input.sanitizeAddressMessageForFallback); : fallbackAddressPreDecompose(input.userMessage, input.llmProvider, input.buildAddressLlmPredecomposeContractV1, input.sanitizeAddressMessageForFallback);
let addressPreDecompose = initialAddressPreDecompose; let addressPreDecompose = initialAddressPreDecompose;
let addressInputMessage = input.toNonEmptyString(addressPreDecompose?.effectiveMessage) ?? input.userMessage; let addressInputMessage = input.toNonEmptyString(addressPreDecompose?.effectiveMessage) ?? input.userMessage;
let carryover = input.resolveAddressFollowupCarryoverContext(input.userMessage, input.sessionItems, addressInputMessage, addressPreDecompose); let carryover = input.resolveAddressFollowupCarryoverContext(input.userMessage, input.sessionItems, addressInputMessage, addressPreDecompose, input.sessionAddressNavigationState);
if (shouldPreferRawFollowupMessage(input.userMessage, addressInputMessage, carryover, addressPreDecompose, input.toNonEmptyString)) { if (shouldPreferRawFollowupMessage(input.userMessage, addressInputMessage, carryover, addressPreDecompose, input.toNonEmptyString)) {
addressInputMessage = input.userMessage; addressInputMessage = input.userMessage;
addressPreDecompose = { addressPreDecompose = {
@ -75,7 +75,7 @@ async function buildAssistantAddressOrchestrationRuntime(input) {
canonicalMessage: input.userMessage canonicalMessage: input.userMessage
}) })
}; };
carryover = input.resolveAddressFollowupCarryoverContext(input.userMessage, input.sessionItems, addressInputMessage, addressPreDecompose); carryover = input.resolveAddressFollowupCarryoverContext(input.userMessage, input.sessionItems, addressInputMessage, addressPreDecompose, input.sessionAddressNavigationState);
} }
const followupContext = carryover?.followupContext ?? null; const followupContext = carryover?.followupContext ?? null;
const orchestrationDecision = input.resolveAssistantOrchestrationDecision({ const orchestrationDecision = input.resolveAssistantOrchestrationDecision({
@ -84,6 +84,7 @@ async function buildAssistantAddressOrchestrationRuntime(input) {
followupContext, followupContext,
llmPreDecomposeMeta: addressPreDecompose, llmPreDecomposeMeta: addressPreDecompose,
sessionItems: input.sessionItems, sessionItems: input.sessionItems,
sessionOrganizationScope: input.sessionOrganizationScope ?? null,
useMock: input.useMock useMock: input.useMock
}); });
const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(input.userMessage, addressInputMessage, carryover, addressPreDecompose); const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(input.userMessage, addressInputMessage, carryover, addressPreDecompose);

View File

@ -18,6 +18,8 @@ async function runAssistantAddressRuntime(input) {
const addressOrchestrationRuntime = await runAddressOrchestrationRuntimeSafe({ const addressOrchestrationRuntime = await runAddressOrchestrationRuntimeSafe({
userMessage: input.userMessage, userMessage: input.userMessage,
sessionItems: input.sessionItems, sessionItems: input.sessionItems,
sessionAddressNavigationState: input.sessionAddressNavigationState,
sessionOrganizationScope: input.sessionOrganizationScope ?? null,
llmProvider: input.llmProvider, llmProvider: input.llmProvider,
useMock: input.useMock, useMock: input.useMock,
featureAddressLlmPredecomposeV1: input.featureAddressLlmPredecomposeV1, featureAddressLlmPredecomposeV1: input.featureAddressLlmPredecomposeV1,

View File

@ -7,6 +7,8 @@ function buildAssistantAddressRuntimeInput(input) {
sessionId: input.sessionId, sessionId: input.sessionId,
userMessage: input.userMessage, userMessage: input.userMessage,
sessionItems: input.sessionItems, sessionItems: input.sessionItems,
sessionAddressNavigationState: input.sessionAddressNavigationState,
sessionOrganizationScope: input.sessionScope,
llmProvider: input.payload.llmProvider, llmProvider: input.payload.llmProvider,
useMock: Boolean(input.payload.useMock), useMock: Boolean(input.payload.useMock),
featureAddressLlmPredecomposeV1: input.featureAddressLlmPredecomposeV1, featureAddressLlmPredecomposeV1: input.featureAddressLlmPredecomposeV1,

View File

@ -1,6 +1,70 @@
"use strict"; "use strict";
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.runAssistantLivingChatRuntime = runAssistantLivingChatRuntime; exports.runAssistantLivingChatRuntime = runAssistantLivingChatRuntime;
function formatIsoDateForReply(value) {
const source = String(value ?? "").trim();
const match = source.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
return null;
}
return `${match[3]}.${match[2]}.${match[1]}`;
}
function findLastGroundedInventoryAddressDebug(items) {
if (!Array.isArray(items)) {
return null;
}
for (let index = items.length - 1; index >= 0; index -= 1) {
const item = items[index];
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
continue;
}
const debug = item.debug;
const answerGroundingCheck = debug.answer_grounding_check && typeof debug.answer_grounding_check === "object"
? debug.answer_grounding_check
: null;
const groundingStatus = String(answerGroundingCheck?.status ?? "");
const detectedIntent = String(debug.detected_intent ?? "");
const capabilityId = String(debug.capability_id ?? "");
const rootFrameContext = debug.address_root_frame_context && typeof debug.address_root_frame_context === "object"
? debug.address_root_frame_context
: null;
const rootIntent = String(rootFrameContext?.root_intent ?? "");
const isInventoryContext = detectedIntent === "inventory_on_hand_as_of_date" ||
capabilityId === "confirmed_inventory_on_hand_as_of_date" ||
rootIntent === "inventory_on_hand_as_of_date";
if (groundingStatus === "grounded" && isInventoryContext) {
return debug;
}
}
return null;
}
function buildInventoryHistoryCapabilityFollowupReply(input) {
const rootFrameContext = input.addressDebug?.address_root_frame_context && typeof input.addressDebug.address_root_frame_context === "object"
? input.addressDebug.address_root_frame_context
: null;
const extractedFilters = input.addressDebug?.extracted_filters && typeof input.addressDebug.extracted_filters === "object"
? input.addressDebug.extracted_filters
: null;
const organization = input.organization ??
input.toNonEmptyString(rootFrameContext?.organization) ??
input.toNonEmptyString(extractedFilters?.organization);
const lastAsOfDate = formatIsoDateForReply(rootFrameContext?.as_of_date) ??
formatIsoDateForReply(extractedFilters?.as_of_date);
const organizationPart = organization ? ` по компании «${organization}»` : "";
const referenceLine = lastAsOfDate
? `Да, могу. Сейчас мы уже смотрели складской срез${organizationPart} на ${lastAsOfDate}.`
: `Да, могу показать исторические данные${organizationPart} в этом же складском контуре.`;
return [
referenceLine,
`Могу показать исторические остатки${organizationPart} за нужный месяц, дату или год.`,
"Например:",
"- `на март 2020`",
"- `на июнь 2016`",
"- `за 2017 год`",
"- `сравни июнь 2016 с текущим срезом`",
"Если хочешь, сразу покажу нужный исторический период."
].join("\n");
}
async function runAssistantLivingChatRuntime(input) { async function runAssistantLivingChatRuntime(input) {
const userMessage = String(input.userMessage ?? ""); const userMessage = String(input.userMessage ?? "");
const dataScopeMetaQuery = input.hasAssistantDataScopeMetaQuestionSignal(userMessage); const dataScopeMetaQuery = input.hasAssistantDataScopeMetaQuestionSignal(userMessage);
@ -18,6 +82,10 @@ async function runAssistantLivingChatRuntime(input) {
let knownOrganizations = input.mergeKnownOrganizations(input.sessionScope.knownOrganizations ?? []); let knownOrganizations = input.mergeKnownOrganizations(input.sessionScope.knownOrganizations ?? []);
let selectedOrganization = input.toNonEmptyString(input.sessionScope.selectedOrganization); let selectedOrganization = input.toNonEmptyString(input.sessionScope.selectedOrganization);
let activeOrganization = input.toNonEmptyString(input.sessionScope.activeOrganization); let activeOrganization = input.toNonEmptyString(input.sessionScope.activeOrganization);
const contextualInventoryHistoryCapabilityFollowup = input.modeDecision?.reason === "inventory_history_capability_followup_detected";
const lastGroundedInventoryAddressDebug = contextualInventoryHistoryCapabilityFollowup
? findLastGroundedInventoryAddressDebug(input.sessionItems)
: null;
if (capabilityMetaQuery && (destructiveSignal || dangerSignal)) { if (capabilityMetaQuery && (destructiveSignal || dangerSignal)) {
chatText = input.buildAssistantSafetyRefusalReply(); chatText = input.buildAssistantSafetyRefusalReply();
livingChatSource = "deterministic_safety_refusal"; livingChatSource = "deterministic_safety_refusal";
@ -61,6 +129,16 @@ async function runAssistantLivingChatRuntime(input) {
chatText = input.buildAssistantOperationalBoundaryReply(); chatText = input.buildAssistantOperationalBoundaryReply();
livingChatSource = "deterministic_operational_boundary"; livingChatSource = "deterministic_operational_boundary";
} }
else if (contextualInventoryHistoryCapabilityFollowup) {
const scopedOrganization = selectedOrganization ?? activeOrganization ?? null;
chatText = buildInventoryHistoryCapabilityFollowupReply({
organization: scopedOrganization,
addressDebug: lastGroundedInventoryAddressDebug,
toNonEmptyString: input.toNonEmptyString
});
activeOrganization = scopedOrganization ?? activeOrganization;
livingChatSource = "deterministic_inventory_history_capability_contract";
}
else if (capabilityMetaQuery) { else if (capabilityMetaQuery) {
chatText = input.buildAssistantCapabilityContractReply(); chatText = input.buildAssistantCapabilityContractReply();
livingChatSource = "deterministic_capability_contract"; livingChatSource = "deterministic_capability_contract";

View File

@ -65,7 +65,7 @@ function normalizeOrganizationScopeValue(value) {
if (!normalized) { if (!normalized) {
return null; return null;
} }
let unwrapped = normalized.replace(/^\\+|\\+$/g, "").trim(); let unwrapped = normalized.trim();
if ((unwrapped.startsWith('"') && unwrapped.endsWith('"')) || if ((unwrapped.startsWith('"') && unwrapped.endsWith('"')) ||
(unwrapped.startsWith("'") && unwrapped.endsWith("'"))) { (unwrapped.startsWith("'") && unwrapped.endsWith("'"))) {
unwrapped = unwrapped.slice(1, -1).trim(); unwrapped = unwrapped.slice(1, -1).trim();

View File

@ -2,11 +2,41 @@
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveSessionOrganizationScopeContextRuntime = resolveSessionOrganizationScopeContextRuntime; exports.resolveSessionOrganizationScopeContextRuntime = resolveSessionOrganizationScopeContextRuntime;
exports.mergeFollowupContextWithOrganizationScopeRuntime = mergeFollowupContextWithOrganizationScopeRuntime; exports.mergeFollowupContextWithOrganizationScopeRuntime = mergeFollowupContextWithOrganizationScopeRuntime;
function extractOrganizationsFromNavigationState(addressNavigationState, normalizeOrganizationScopeValue) {
if (!addressNavigationState || typeof addressNavigationState !== "object") {
return [];
}
const collected = [];
const directOrganization = normalizeOrganizationScopeValue(addressNavigationState.session_context?.organization_scope);
if (directOrganization) {
collected.push(directOrganization);
}
for (const resultSet of Array.isArray(addressNavigationState.result_sets) ? addressNavigationState.result_sets : []) {
const scopedOrganization = normalizeOrganizationScopeValue(resultSet?.filters?.organization);
if (scopedOrganization) {
collected.push(scopedOrganization);
}
}
return Array.from(new Map(collected.map((value) => [value.toLowerCase(), value])).values());
}
function resolveActiveOrganizationFromNavigationState(addressNavigationState, normalizeOrganizationScopeValue) {
if (!addressNavigationState || typeof addressNavigationState !== "object") {
return null;
}
return normalizeOrganizationScopeValue(addressNavigationState.session_context?.organization_scope);
}
function resolveSessionOrganizationScopeContextRuntime(input) { function resolveSessionOrganizationScopeContextRuntime(input) {
const knownOrganizations = input.extractKnownOrganizationsFromHistory(input.items); const knownOrganizations = Array.from(new Map([
...extractOrganizationsFromNavigationState(input.addressNavigationState, input.normalizeOrganizationScopeValue),
...input.extractKnownOrganizationsFromHistory(input.items)
].map((value) => [String(value).toLowerCase(), value])).values());
const selectedOrganization = input.resolveOrganizationSelectionFromMessage(input.userMessage, knownOrganizations); const selectedOrganization = input.resolveOrganizationSelectionFromMessage(input.userMessage, knownOrganizations);
const lastActiveOrganization = input.findLastAssistantActiveOrganization(input.items); const lastActiveOrganization = input.findLastAssistantActiveOrganization(input.items);
const activeOrganization = selectedOrganization ?? input.normalizeOrganizationScopeValue(lastActiveOrganization); const navigationActiveOrganization = resolveActiveOrganizationFromNavigationState(input.addressNavigationState, input.normalizeOrganizationScopeValue);
const activeOrganization = selectedOrganization ??
navigationActiveOrganization ??
input.normalizeOrganizationScopeValue(lastActiveOrganization) ??
(knownOrganizations.length === 1 ? knownOrganizations[0] : null);
return { return {
knownOrganizations, knownOrganizations,
selectedOrganization, selectedOrganization,
@ -28,5 +58,15 @@ function mergeFollowupContextWithOrganizationScopeRuntime(input) {
previousFilters.organization = normalizedOrganization; previousFilters.organization = normalizedOrganization;
} }
base.previous_filters = previousFilters; base.previous_filters = previousFilters;
const rootFiltersRaw = base.root_filters;
const rootFilters = rootFiltersRaw && typeof rootFiltersRaw === "object"
? { ...rootFiltersRaw }
: {};
if (!input.toNonEmptyString(rootFilters.organization)) {
rootFilters.organization = normalizedOrganization;
}
if (Object.keys(rootFilters).length > 0) {
base.root_filters = rootFilters;
}
return base; return base;
} }

View File

@ -1473,6 +1473,7 @@ function buildAddressDebugPayload(addressDebug, llmPreDecomposeMeta = null) {
account_scope_drop_reason: addressDebug.account_scope_drop_reason, account_scope_drop_reason: addressDebug.account_scope_drop_reason,
runtime_readiness: addressDebug.runtime_readiness, runtime_readiness: addressDebug.runtime_readiness,
limited_reason_category: addressDebug.limited_reason_category, limited_reason_category: addressDebug.limited_reason_category,
organization_candidates: addressDebug.organization_candidates ?? undefined,
response_type: addressDebug.response_type, response_type: addressDebug.response_type,
requested_result_mode: addressDebug.requested_result_mode ?? undefined, requested_result_mode: addressDebug.requested_result_mode ?? undefined,
result_mode: addressDebug.result_mode ?? undefined, result_mode: addressDebug.result_mode ?? undefined,
@ -2790,9 +2791,18 @@ function resolveDebtRoleSwapFollowupIntent(userMessage, previousIntent) {
} }
return null; return null;
} }
function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMessage = null, llmPreDecomposeMeta = null) { function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMessage = null, llmPreDecomposeMeta = null, addressNavigationState = null) {
const previousAddressItem = findLastAddressAssistantItem(items); const previousAddressItem = findLastAddressAssistantItem(items);
const previousAddressDebug = previousAddressItem?.debug ?? null; const previousAddressDebug = previousAddressItem?.debug ?? null;
const lastOrganizationClarificationDebug = findLastOrganizationClarificationAddressDebug(items);
const organizationClarificationCandidates = Array.isArray(lastOrganizationClarificationDebug?.organization_candidates)
? mergeKnownOrganizations(lastOrganizationClarificationDebug.organization_candidates)
: [];
const organizationClarificationSelection = resolveOrganizationSelectionFromMessage(userMessage, organizationClarificationCandidates) ??
(toNonEmptyString(alternateMessage)
? resolveOrganizationSelectionFromMessage(String(alternateMessage ?? ""), organizationClarificationCandidates)
: null);
const hasOrganizationClarificationContinuation = Boolean(lastOrganizationClarificationDebug && organizationClarificationSelection);
const followupOffer = previousAddressDebug ? buildAddressFollowupOffer(previousAddressDebug) : null; const followupOffer = previousAddressDebug ? buildAddressFollowupOffer(previousAddressDebug) : null;
const hasImplicitContinuationSignal = Boolean(previousAddressDebug) && const hasImplicitContinuationSignal = Boolean(previousAddressDebug) &&
Boolean(followupOffer?.enabled) && Boolean(followupOffer?.enabled) &&
@ -2823,10 +2833,15 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
!hasPrimaryFollowupSignal && !hasPrimaryFollowupSignal &&
!hasAlternateFollowupSignal && !hasAlternateFollowupSignal &&
!hasImplicitContinuationSignal && !hasImplicitContinuationSignal &&
!hasOrganizationClarificationContinuation &&
!hasIndexReferenceSignal) { !hasIndexReferenceSignal) {
return null; return null;
} }
if (!hasPrimaryFollowupSignal && !hasAlternateFollowupSignal && !hasImplicitContinuationSignal && !hasIndexReferenceSignal) { if (!hasPrimaryFollowupSignal &&
!hasAlternateFollowupSignal &&
!hasImplicitContinuationSignal &&
!hasOrganizationClarificationContinuation &&
!hasIndexReferenceSignal) {
return null; return null;
} }
if (!previousAddressDebug) { if (!previousAddressDebug) {
@ -2854,7 +2869,45 @@ 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 navigationSessionContext = addressNavigationState && typeof addressNavigationState === "object"
? (addressNavigationState.session_context && typeof addressNavigationState.session_context === "object"
? addressNavigationState.session_context
: null)
: null;
const navigationDateScope = navigationSessionContext && typeof navigationSessionContext.date_scope === "object"
? navigationSessionContext.date_scope
: null;
const navigationOrganization = normalizeOrganizationScopeValue(navigationSessionContext?.organization_scope);
const navigationFocusObject = navigationSessionContext && typeof navigationSessionContext.active_focus_object === "object"
? navigationSessionContext.active_focus_object
: null;
const navigationFocusObjectType = toNonEmptyString(navigationFocusObject?.object_type);
const navigationFocusObjectLabel = toNonEmptyString(navigationFocusObject?.label);
const hasSelectedObjectInventorySignalPrimary = /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(String(userMessage ?? ""));
const hasSelectedObjectInventorySignalAlternate = toNonEmptyString(alternateMessage)
? /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(String(alternateMessage ?? ""))
: false;
let inventoryRootFrame = findRecentInventoryRootFrame(items);
if (inventoryRootFrame && navigationOrganization && !toNonEmptyString(inventoryRootFrame.filters?.organization)) {
inventoryRootFrame = {
...inventoryRootFrame,
filters: {
...(inventoryRootFrame.filters ?? {}),
organization: navigationOrganization
}
};
}
if (inventoryRootFrame && navigationDateScope) {
inventoryRootFrame = {
...inventoryRootFrame,
filters: {
...(inventoryRootFrame.filters ?? {}),
as_of_date: toNonEmptyString(inventoryRootFrame.filters?.as_of_date) ?? toNonEmptyString(navigationDateScope.as_of_date) ?? undefined,
period_from: toNonEmptyString(inventoryRootFrame.filters?.period_from) ?? toNonEmptyString(navigationDateScope.period_from) ?? undefined,
period_to: toNonEmptyString(inventoryRootFrame.filters?.period_to) ?? toNonEmptyString(navigationDateScope.period_to) ?? undefined
}
};
}
const currentFrameKind = inventoryRootFrame const currentFrameKind = inventoryRootFrame
? isInventoryDrilldownFrameIntent(sourceIntent) ? isInventoryDrilldownFrameIntent(sourceIntent)
? "inventory_drilldown" ? "inventory_drilldown"
@ -2885,6 +2938,21 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
previousFilters.organization = historicalOrganization; previousFilters.organization = historicalOrganization;
} }
} }
if (!toNonEmptyString(previousFilters.organization) && navigationOrganization) {
previousFilters.organization = navigationOrganization;
}
if (!toNonEmptyString(previousFilters.organization) && organizationClarificationSelection) {
previousFilters.organization = organizationClarificationSelection;
}
if (!toNonEmptyString(previousFilters.as_of_date) && toNonEmptyString(navigationDateScope?.as_of_date)) {
previousFilters.as_of_date = toNonEmptyString(navigationDateScope?.as_of_date);
}
if (!toNonEmptyString(previousFilters.period_from) && toNonEmptyString(navigationDateScope?.period_from)) {
previousFilters.period_from = toNonEmptyString(navigationDateScope?.period_from);
}
if (!toNonEmptyString(previousFilters.period_to) && toNonEmptyString(navigationDateScope?.period_to)) {
previousFilters.period_to = toNonEmptyString(navigationDateScope?.period_to);
}
const displayedEntityType = inferDisplayedEntityTypeFromIntent(sourceIntent); const displayedEntityType = inferDisplayedEntityTypeFromIntent(sourceIntent);
const displayedEntities = extractDisplayedAddressEntityCandidates(toNonEmptyString(previousAddressItem?.text) ?? "", displayedEntityType); const displayedEntities = extractDisplayedAddressEntityCandidates(toNonEmptyString(previousAddressItem?.text) ?? "", displayedEntityType);
const resolvedEntityFromFollowup = resolveDisplayedAddressEntityMention(userMessage, displayedEntities) ?? const resolvedEntityFromFollowup = resolveDisplayedAddressEntityMention(userMessage, displayedEntities) ??
@ -2912,6 +2980,36 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
followupSelectionMode = "carry_referenced_entity"; followupSelectionMode = "carry_referenced_entity";
} }
} }
if (!toNonEmptyString(previousFilters.item) &&
navigationFocusObjectType === "item" &&
navigationFocusObjectLabel &&
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
sourceIntentHint === "inventory_purchase_provenance_for_item" ||
sourceIntentHint === "inventory_purchase_documents_for_item" ||
sourceIntentHint === "inventory_sale_trace_for_item" ||
sourceIntentHint === "inventory_purchase_to_sale_chain" ||
sourceIntentHint === "inventory_aging_by_purchase_date" ||
hasSelectedObjectInventorySignalPrimary ||
hasSelectedObjectInventorySignalAlternate)) {
previousFilters.item = navigationFocusObjectLabel;
if (!previousAnchor) {
previousAnchorType = "item";
previousAnchor = navigationFocusObjectLabel;
}
}
if (organizationClarificationSelection && !previousAnchor) {
previousAnchorType = "organization";
previousAnchor = organizationClarificationSelection;
}
if (inventoryRootFrame && organizationClarificationSelection && !toNonEmptyString(inventoryRootFrame.filters?.organization)) {
inventoryRootFrame = {
...inventoryRootFrame,
filters: {
...(inventoryRootFrame.filters ?? {}),
organization: organizationClarificationSelection
}
};
}
if (!previousIntent && !previousAnchor && Object.keys(previousFilters).length === 0) { if (!previousIntent && !previousAnchor && Object.keys(previousFilters).length === 0) {
return null; return null;
} }
@ -4036,6 +4134,28 @@ function resolveAssistantOrchestrationDecision(input) {
const llmPreDecomposeMeta = input?.llmPreDecomposeMeta ?? null; const llmPreDecomposeMeta = input?.llmPreDecomposeMeta ?? null;
const useMock = Boolean(input?.useMock); const useMock = Boolean(input?.useMock);
const sessionItems = Array.isArray(input?.sessionItems) ? input.sessionItems : null; const sessionItems = Array.isArray(input?.sessionItems) ? input.sessionItems : null;
const sessionOrganizationScope = input?.sessionOrganizationScope && typeof input.sessionOrganizationScope === "object"
? input.sessionOrganizationScope
: null;
const lastGroundedAddressDebug = findLastGroundedAddressAnswerDebug(sessionItems);
const lastOrganizationClarificationDebug = findLastOrganizationClarificationAddressDebug(sessionItems);
const organizationClarificationCandidates = Array.isArray(lastOrganizationClarificationDebug?.organization_candidates)
? mergeKnownOrganizations([
...lastOrganizationClarificationDebug.organization_candidates,
...((Array.isArray(sessionOrganizationScope?.knownOrganizations)
? sessionOrganizationScope.knownOrganizations
: []))
])
: [];
const organizationClarificationSelectionFromScope = normalizeOrganizationScopeValue(sessionOrganizationScope?.selectedOrganization);
const organizationClarificationSelection = resolveOrganizationSelectionFromMessage(rawUserMessage, organizationClarificationCandidates) ??
resolveOrganizationSelectionFromMessage(repairedRawUserMessage, organizationClarificationCandidates) ??
resolveOrganizationSelectionFromMessage(effectiveAddressUserMessage, organizationClarificationCandidates) ??
resolveOrganizationSelectionFromMessage(repairedEffectiveAddressUserMessage, organizationClarificationCandidates) ??
(organizationClarificationSelectionFromScope &&
organizationClarificationCandidates.some((candidate) => normalizeOrganizationScopeValue(candidate) === organizationClarificationSelectionFromScope)
? organizationClarificationSelectionFromScope
: null);
const dataScopeMetaQuery = hasAssistantDataScopeMetaQuestionSignal(rawUserMessage) || const dataScopeMetaQuery = hasAssistantDataScopeMetaQuestionSignal(rawUserMessage) ||
hasAssistantDataScopeMetaQuestionSignal(repairedRawUserMessage) || hasAssistantDataScopeMetaQuestionSignal(repairedRawUserMessage) ||
hasAssistantDataScopeMetaQuestionSignal(effectiveAddressUserMessage) || hasAssistantDataScopeMetaQuestionSignal(effectiveAddressUserMessage) ||
@ -4123,6 +4243,12 @@ function resolveAssistantOrchestrationDecision(input) {
hasShortInventoryObjectFollowupSignal(repairedRawUserMessage) || hasShortInventoryObjectFollowupSignal(repairedRawUserMessage) ||
hasShortInventoryObjectFollowupSignal(effectiveAddressUserMessage) || hasShortInventoryObjectFollowupSignal(effectiveAddressUserMessage) ||
hasShortInventoryObjectFollowupSignal(repairedEffectiveAddressUserMessage))); hasShortInventoryObjectFollowupSignal(repairedEffectiveAddressUserMessage)));
const organizationClarificationContinuationDetected = Boolean(followupContext &&
lastOrganizationClarificationDebug &&
organizationClarificationSelection &&
!dataScopeMetaQuery &&
!capabilityMetaQuery &&
!dataRetrievalSignal);
const effectiveAddressFollowupSignal = explicitAddressFollowupSignal && !dangerOrCoercionSignal; const effectiveAddressFollowupSignal = explicitAddressFollowupSignal && !dangerOrCoercionSignal;
const deterministicNonDomainGuard = Boolean(!dataScopeMetaQuery && const deterministicNonDomainGuard = Boolean(!dataScopeMetaQuery &&
!capabilityMetaQuery && !capabilityMetaQuery &&
@ -4133,7 +4259,16 @@ function resolveAssistantOrchestrationDecision(input) {
const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate && const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate &&
deterministicNonDomainGuard && deterministicNonDomainGuard &&
(llmFirstUnsupportedCandidate || llmContractMode === null) && (llmFirstUnsupportedCandidate || llmContractMode === null) &&
!protectedInventoryShortFollowup); !protectedInventoryShortFollowup &&
!organizationClarificationContinuationDetected);
const contextualHistoricalCapabilityFollowupDetected = Boolean(capabilityMetaQuery &&
!dataScopeMetaQuery &&
!dataRetrievalSignal &&
(hasHistoricalCapabilityFollowupSignal(rawUserMessage) ||
hasHistoricalCapabilityFollowupSignal(repairedRawUserMessage) ||
hasHistoricalCapabilityFollowupSignal(effectiveAddressUserMessage) ||
hasHistoricalCapabilityFollowupSignal(repairedEffectiveAddressUserMessage)) &&
isGroundedInventoryContextDebug(lastGroundedAddressDebug));
const hardMetaMode = dataScopeMetaQuery const hardMetaMode = dataScopeMetaQuery
? "data_scope" ? "data_scope"
: capabilityMetaQuery && !dataRetrievalSignal : capabilityMetaQuery && !dataRetrievalSignal
@ -4168,6 +4303,34 @@ function resolveAssistantOrchestrationDecision(input) {
}; };
} }
if (hardMetaMode === "capability") { if (hardMetaMode === "capability") {
if (contextualHistoricalCapabilityFollowupDetected) {
return {
runAddressLane: false,
toolGateDecision: "skip_address_lane",
toolGateReason: "inventory_history_capability_followup_detected",
livingMode: "chat",
livingReason: "inventory_history_capability_followup_detected",
orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1",
hard_meta_mode: "capability",
address_mode: resolvedModeDetection.mode,
address_mode_confidence: resolvedModeDetection.confidence,
address_intent: resolvedIntentResolution.intent,
address_intent_confidence: resolvedIntentResolution.confidence,
strong_data_signal_detected: strongDataSignal,
data_retrieval_signal_detected: dataRetrievalSignal,
followup_context_detected: Boolean(followupContext || lastGroundedAddressDebug),
unsupported_address_intent_fallback_to_deep: false,
final_decision: {
run_address_lane: false,
tool_gate_decision: "skip_address_lane",
tool_gate_reason: "inventory_history_capability_followup_detected",
living_mode: "chat",
living_reason: "inventory_history_capability_followup_detected"
}
}
};
}
return { return {
runAddressLane: false, runAddressLane: false,
toolGateDecision: "skip_address_lane", toolGateDecision: "skip_address_lane",
@ -4223,6 +4386,10 @@ function resolveAssistantOrchestrationDecision(input) {
} }
}; };
} }
const metaAnswerFollowupSignal = hasMetaAnswerFollowupSignal(rawUserMessage) ||
hasMetaAnswerFollowupSignal(repairedRawUserMessage) ||
hasMetaAnswerFollowupSignal(effectiveAddressUserMessage) ||
hasMetaAnswerFollowupSignal(repairedEffectiveAddressUserMessage);
const baseToolGate = resolveAddressToolGateDecision(effectiveAddressUserMessage, followupContext, llmPreDecomposeMeta, rawUserMessage); const baseToolGate = resolveAddressToolGateDecision(effectiveAddressUserMessage, followupContext, llmPreDecomposeMeta, rawUserMessage);
const preserveAddressLaneSignal = Boolean((llmPreDecomposeMeta?.llmCanonicalCandidateDetected && const preserveAddressLaneSignal = Boolean((llmPreDecomposeMeta?.llmCanonicalCandidateDetected &&
llmPreDecomposeMeta?.applied && llmPreDecomposeMeta?.applied &&
@ -4317,6 +4484,19 @@ function resolveAssistantOrchestrationDecision(input) {
repairedEffectiveAddressUserMessage, repairedEffectiveAddressUserMessage,
sessionItems sessionItems
})); }));
const hasPriorAddressAnswerContext = Boolean(lastGroundedAddressDebug || toNonEmptyString(followupContext?.previous_intent));
const metaFollowupOverGroundedAnswer = Boolean(followupContext &&
hasPriorAddressAnswerContext &&
metaAnswerFollowupSignal &&
!dataScopeMetaQuery &&
!capabilityMetaQuery &&
!aggregateBusinessAnalyticsSignal &&
!dataRetrievalSignal &&
!strongDataSignal &&
resolvedModeDetection.mode !== "address_query" &&
resolvedIntentResolution.intent === "unknown" &&
(!llmContractIntent || llmContractIntent === "unknown") &&
llmContractMode !== "address_query");
let runAddressLane = Boolean(baseToolGate?.runAddressLane); let runAddressLane = Boolean(baseToolGate?.runAddressLane);
let toolGateDecision = String(baseToolGate?.decision ?? "skip_address_lane"); let toolGateDecision = String(baseToolGate?.decision ?? "skip_address_lane");
let toolGateReason = String(baseToolGate?.reason ?? "no_address_signal_after_l0"); let toolGateReason = String(baseToolGate?.reason ?? "no_address_signal_after_l0");
@ -4342,6 +4522,11 @@ function resolveAssistantOrchestrationDecision(input) {
toolGateDecision = "skip_address_lane"; toolGateDecision = "skip_address_lane";
toolGateReason = "deep_session_continuation_fallback_to_deep"; toolGateReason = "deep_session_continuation_fallback_to_deep";
} }
if (metaFollowupOverGroundedAnswer) {
runAddressLane = false;
toolGateDecision = "skip_address_lane";
toolGateReason = "meta_followup_over_grounded_answer";
}
let livingDecision = resolveLivingAssistantModeDecision({ let livingDecision = resolveLivingAssistantModeDecision({
userMessage: rawUserMessage, userMessage: rawUserMessage,
addressLaneTriggered: runAddressLane, addressLaneTriggered: runAddressLane,
@ -4375,6 +4560,12 @@ function resolveAssistantOrchestrationDecision(input) {
reason: "deep_session_continuation_fallback_to_deep" reason: "deep_session_continuation_fallback_to_deep"
}; };
} }
if (metaFollowupOverGroundedAnswer) {
livingDecision = {
mode: "chat",
reason: "meta_followup_over_grounded_answer"
};
}
return { return {
runAddressLane, runAddressLane,
toolGateDecision, toolGateDecision,
@ -4476,6 +4667,105 @@ function findLastAssistantLivingChatDebug(items) {
} }
return null; return null;
} }
function findLastGroundedAddressAnswerDebug(items) {
if (!Array.isArray(items)) {
return null;
}
for (let index = items.length - 1; index >= 0; index -= 1) {
const item = items[index];
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
continue;
}
const debug = item.debug;
if (debug.execution_lane !== "address_query") {
continue;
}
const groundingStatus = toNonEmptyString(debug.answer_grounding_check?.status);
if (groundingStatus === "grounded") {
return debug;
}
}
return null;
}
function findLastOrganizationClarificationAddressDebug(items) {
if (!Array.isArray(items)) {
return null;
}
for (let index = items.length - 1; index >= 0; index -= 1) {
const item = items[index];
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
continue;
}
const debug = item.debug;
if (debug.execution_lane !== "address_query" && debug.detected_mode !== "address_query") {
continue;
}
const limitedCategory = toNonEmptyString(debug.limited_reason_category);
const candidates = Array.isArray(debug.organization_candidates)
? mergeKnownOrganizations(debug.organization_candidates)
: [];
if (limitedCategory === "missing_anchor" && candidates.length > 0) {
return debug;
}
}
return null;
}
function hasMetaAnswerFollowupSignal(userMessage) {
const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase());
const repairedText = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
const samples = [rawText, repairedText]
.filter((item) => item.length > 0)
.map((item) => item.replace(/ё/g, "е"));
if (samples.length === 0) {
return false;
}
const hasReflectionCue = samples.some((sample) => sample.includes("дума") ||
sample.includes("скаж") ||
sample.includes("мнение") ||
sample.includes("как тебе") ||
sample.includes("норм") ||
sample.includes("стран") ||
sample.includes("логич") ||
sample.includes("смуща") ||
sample.includes("выгляд"));
const hasTopicPointerCue = samples.some((sample) => sample.includes("на эту тему") ||
sample.includes("по этому поводу") ||
sample.includes("об этом") ||
(sample.includes("это") && hasReferentialPointer(sample)));
if (!(hasReflectionCue && hasTopicPointerCue)) {
return false;
}
return !samples.some((sample) => hasAssistantDataScopeMetaQuestionSignal(sample) ||
shouldHandleAsAssistantCapabilityMetaQuery(sample) ||
hasDataRetrievalRequestSignal(sample) ||
hasStrongDataIntentSignal(sample));
}
function hasHistoricalCapabilityFollowupSignal(text) {
const repaired = repairAddressMojibake(String(text ?? ""));
const normalized = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е");
if (!normalized) {
return false;
}
const hasHistoryCue = /(?:историческ|история|архив|прошл(?:ый|ые|ую|ых)?|раньше|ретро|старые\s+данные)/iu.test(normalized);
if (!hasHistoryCue) {
return false;
}
return /(?:мож(?:ешь|ете|но)|уме(?:ешь|ете)|показ|вывед|дай|раскрой)/iu.test(normalized);
}
function isGroundedInventoryContextDebug(debug) {
if (!debug || typeof debug !== "object") {
return false;
}
const detectedIntent = toNonEmptyString(debug.detected_intent);
const capabilityId = toNonEmptyString(debug.capability_id);
const rootFrameContext = debug.address_root_frame_context && typeof debug.address_root_frame_context === "object"
? debug.address_root_frame_context
: null;
const rootIntent = toNonEmptyString(rootFrameContext?.root_intent);
return detectedIntent === "inventory_on_hand_as_of_date" ||
capabilityId === "confirmed_inventory_on_hand_as_of_date" ||
rootIntent === "inventory_on_hand_as_of_date";
}
function hasOrganizationFactFollowupSignal(userMessage, items) { function hasOrganizationFactFollowupSignal(userMessage, items) {
const repaired = repairAddressMojibake(String(userMessage ?? "")); const repaired = repairAddressMojibake(String(userMessage ?? ""));
const normalized = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е"); const normalized = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е");
@ -4764,7 +5054,6 @@ function normalizeOrganizationScopeValue(value) {
return null; return null;
} }
const unwrapped = normalized const unwrapped = normalized
.replace(/^\\+|\\+$/g, "")
.replace(/^"+|"+$/g, "") .replace(/^"+|"+$/g, "")
.replace(/^'+|'+$/g, "") .replace(/^'+|'+$/g, "")
.trim(); .trim();
@ -4905,8 +5194,34 @@ function mergeKnownOrganizations(values) {
} }
return Array.from(dedup.values()).slice(0, 20); return Array.from(dedup.values()).slice(0, 20);
} }
function extractKnownOrganizationsFromHistory(items) { function extractKnownOrganizationsFromNavigationState(addressNavigationState) {
if (!addressNavigationState || typeof addressNavigationState !== "object") {
return [];
}
const collected = []; const collected = [];
const sessionContext = addressNavigationState.session_context && typeof addressNavigationState.session_context === "object"
? addressNavigationState.session_context
: null;
const directOrganization = normalizeOrganizationScopeValue(sessionContext?.organization_scope);
if (directOrganization) {
collected.push(directOrganization);
}
const resultSets = Array.isArray(addressNavigationState.result_sets) ? addressNavigationState.result_sets : [];
for (const resultSet of resultSets) {
const filters = resultSet?.filters && typeof resultSet.filters === "object" ? resultSet.filters : null;
const scopedOrganization = normalizeOrganizationScopeValue(filters?.organization);
if (scopedOrganization) {
collected.push(scopedOrganization);
}
}
return mergeKnownOrganizations(collected);
}
function extractKnownOrganizationsFromHistory(items, addressNavigationState = null) {
const collected = [];
const navigationOrganizations = extractKnownOrganizationsFromNavigationState(addressNavigationState);
if (navigationOrganizations.length > 0) {
collected.push(...navigationOrganizations);
}
for (let index = (Array.isArray(items) ? items.length : 0) - 1; index >= 0; index -= 1) { for (let index = (Array.isArray(items) ? items.length : 0) - 1; index >= 0; index -= 1) {
const item = items[index]; const item = items[index];
if (!item || item.role !== "assistant") { if (!item || item.role !== "assistant") {
@ -4920,8 +5235,17 @@ function extractKnownOrganizationsFromHistory(items) {
const knownFromDebug = Array.isArray(debug.assistant_known_organizations) const knownFromDebug = Array.isArray(debug.assistant_known_organizations)
? debug.assistant_known_organizations ? debug.assistant_known_organizations
: []; : [];
if (directFromProbe.length > 0 || knownFromDebug.length > 0) { const directFromCandidates = Array.isArray(debug.organization_candidates)
collected.push(...directFromProbe, ...knownFromDebug); ? debug.organization_candidates
: [];
const directFromResolved = [
normalizeOrganizationScopeValue(debug.assistant_active_organization),
normalizeOrganizationScopeValue(debug.living_chat_selected_organization),
normalizeOrganizationScopeValue(debug.extracted_filters?.organization),
normalizeOrganizationScopeValue(debug.address_root_frame_context?.organization)
].filter(Boolean);
if (directFromProbe.length > 0 || knownFromDebug.length > 0 || directFromCandidates.length > 0 || directFromResolved.length > 0) {
collected.push(...directFromProbe, ...knownFromDebug, ...directFromCandidates, ...directFromResolved);
} }
} }
const parsedFromText = parseOrganizationsFromDataScopeAssistantText(item.text); const parsedFromText = parseOrganizationsFromDataScopeAssistantText(item.text);
@ -4934,7 +5258,16 @@ function extractKnownOrganizationsFromHistory(items) {
} }
return mergeKnownOrganizations(collected); return mergeKnownOrganizations(collected);
} }
function findLastAssistantActiveOrganization(items) { function findLastAssistantActiveOrganization(items, addressNavigationState = null) {
const sessionContext = addressNavigationState && typeof addressNavigationState === "object"
? (addressNavigationState.session_context && typeof addressNavigationState.session_context === "object"
? addressNavigationState.session_context
: null)
: null;
const navigationOrganization = normalizeOrganizationScopeValue(sessionContext?.organization_scope);
if (navigationOrganization) {
return navigationOrganization;
}
for (let index = (Array.isArray(items) ? items.length : 0) - 1; index >= 0; index -= 1) { for (let index = (Array.isArray(items) ? items.length : 0) - 1; index >= 0; index -= 1) {
const item = items[index]; const item = items[index];
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") { if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
@ -4980,10 +5313,11 @@ function resolveOrganizationSelectionFromMessage(userMessage, knownOrganizations
} }
return best.organization; return best.organization;
} }
function resolveSessionOrganizationScopeContext(userMessage, items) { function resolveSessionOrganizationScopeContext(userMessage, items, addressNavigationState = null) {
return (0, assistantOrganizationScopeRuntimeAdapter_1.resolveSessionOrganizationScopeContextRuntime)({ return (0, assistantOrganizationScopeRuntimeAdapter_1.resolveSessionOrganizationScopeContextRuntime)({
userMessage, userMessage,
items, items,
addressNavigationState,
extractKnownOrganizationsFromHistory, extractKnownOrganizationsFromHistory,
resolveOrganizationSelectionFromMessage, resolveOrganizationSelectionFromMessage,
findLastAssistantActiveOrganization, findLastAssistantActiveOrganization,
@ -4998,8 +5332,8 @@ function mergeFollowupContextWithOrganizationScope(followupContext, organization
toNonEmptyString toNonEmptyString
}); });
} }
function resolveSessionOrganizationScopeContextForTests(userMessage, items) { function resolveSessionOrganizationScopeContextForTests(userMessage, items, addressNavigationState = null) {
return resolveSessionOrganizationScopeContext(userMessage, items); return resolveSessionOrganizationScopeContext(userMessage, items, addressNavigationState);
} }
function normalizeGuidValue(value) { function normalizeGuidValue(value) {
const source = normalizeScopeLabel(value); const source = normalizeScopeLabel(value);

View File

@ -8,6 +8,7 @@ function buildAssistantTurnAttemptAddressRuntimeInput(input) {
sessionId: input.userTurn.sessionId, sessionId: input.userTurn.sessionId,
userMessage: input.userTurn.userMessage, userMessage: input.userTurn.userMessage,
sessionItems: input.userTurn.session.items, sessionItems: input.userTurn.session.items,
sessionAddressNavigationState: input.userTurn.session.address_navigation_state ?? null,
runtimeAnalysisContext: input.userTurn.runtimeAnalysisContext, runtimeAnalysisContext: input.userTurn.runtimeAnalysisContext,
sessionOrganizationScope: input.sessionOrganizationScope sessionOrganizationScope: input.sessionOrganizationScope
}; };

View File

@ -4,7 +4,7 @@ exports.runAssistantTurnAttemptRuntime = runAssistantTurnAttemptRuntime;
const assistantTurnAttemptInputBuilder_1 = require("./assistantTurnAttemptInputBuilder"); const assistantTurnAttemptInputBuilder_1 = require("./assistantTurnAttemptInputBuilder");
async function runAssistantTurnAttemptRuntime(input) { async function runAssistantTurnAttemptRuntime(input) {
const userTurn = input.runUserTurnBootstrapRuntime(input.payload); const userTurn = input.runUserTurnBootstrapRuntime(input.payload);
const sessionOrganizationScope = input.resolveSessionOrganizationScopeContext(userTurn.userMessage, userTurn.session.items); const sessionOrganizationScope = input.resolveSessionOrganizationScopeContext(userTurn.userMessage, userTurn.session.items, userTurn.session.address_navigation_state ?? null);
const addressRuntime = await input.runAddressAttemptRuntime((0, assistantTurnAttemptInputBuilder_1.buildAssistantTurnAttemptAddressRuntimeInput)({ const addressRuntime = await input.runAddressAttemptRuntime((0, assistantTurnAttemptInputBuilder_1.buildAssistantTurnAttemptAddressRuntimeInput)({
payload: input.payload, payload: input.payload,
userTurn, userTurn,

View File

@ -23,6 +23,7 @@ function buildAssistantAddressAttemptRuntimeInput(runtimeInput, deps) {
sessionId: runtimeInput.sessionId, sessionId: runtimeInput.sessionId,
userMessage: runtimeInput.userMessage, userMessage: runtimeInput.userMessage,
sessionItems: runtimeInput.sessionItems, sessionItems: runtimeInput.sessionItems,
sessionAddressNavigationState: runtimeInput.sessionAddressNavigationState,
payload: runtimeInput.payload, payload: runtimeInput.payload,
sessionScope: { sessionScope: {
knownOrganizations: runtimeInput.sessionOrganizationScope.knownOrganizations, knownOrganizations: runtimeInput.sessionOrganizationScope.knownOrganizations,

View File

@ -1633,6 +1633,15 @@ function hasSelectedObjectInventoryPurchaseDocumentsSignal(text: string): boolea
); );
} }
function hasSelectedObjectInventorySaleTraceSignal(text: string): boolean {
return (
hasSelectedObjectInventoryCue(text) &&
/(?:кому\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали|кому\s+был\s+продан|куда\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|куда\s+(?:была\s+)?реализована\s+(?:позиция|номенклатура|продукция)|кто\s+купил|buyer|sale\s+trace|trace\s+of\s+sale)/iu.test(
text
)
);
}
function hasInventoryProvenanceSignalV2(text: string): boolean { function hasInventoryProvenanceSignalV2(text: string): boolean {
const hasItemCue = /(?:товар|номенклатур|sku|item|product|остат(?:ок|ки)|склад)/iu.test(text); const hasItemCue = /(?:товар|номенклатур|sku|item|product|остат(?:ок|ки)|склад)/iu.test(text);
const hasSupplierCue = const hasSupplierCue =
@ -1663,9 +1672,9 @@ function hasInventoryPurchaseDocumentsSignalV2(text: string): boolean {
} }
function hasInventorySaleTraceSignalV2(text: string): boolean { function hasInventorySaleTraceSignalV2(text: string): boolean {
const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text); const hasItemCue = /(?:товар|номенклатур|sku|item|product|позици(?:я|ю|и)|продукци(?:я|ю|и))/iu.test(text);
const hasTraceCue = const hasTraceCue =
/(?:кому\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали|кому\s+был\s+продан|кто\s+купил|buyer|sale\s+trace|trace\s+of\s+sale|через\s+какие\s+документы\s+прош[её]л\s+путь\s+товара|закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*warehouse\s*->\s*sale|purchase\s*->\s*stock\s*->\s*sale)/iu.test( /(?:кому\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали|кому\s+был\s+продан|куда\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали(?:\s+(?:это|его|товар|позицию))?|куда\s+(?:была\s+)?реализована\s+(?:позиция|номенклатура|продукция)|кто\s+купил|buyer|sale\s+trace|trace\s+of\s+sale|через\s+какие\s+документы\s+прош[её]л\s+путь\s+товара|закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*warehouse\s*->\s*sale|purchase\s*->\s*stock\s*->\s*sale)/iu.test(
text text
); );
return hasItemCue && hasTraceCue; return hasItemCue && hasTraceCue;
@ -1944,6 +1953,14 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
}; };
} }
if (hasSelectedObjectInventorySaleTraceSignal(text)) {
return {
intent: "inventory_sale_trace_for_item",
confidence: "medium",
reasons: ["inventory_selected_object_sale_trace_signal_detected"]
};
}
if (hasInventorySaleTraceSignalV2(text)) { if (hasInventorySaleTraceSignalV2(text)) {
return { return {
intent: "inventory_sale_trace_for_item", intent: "inventory_sale_trace_for_item",

View File

@ -29,7 +29,14 @@ const DISPLAY_ENTITY_TYPE_BY_INTENT: Partial<Record<AddressIntent, AddressFocusO
list_documents_by_contract: "document_ref", list_documents_by_contract: "document_ref",
bank_operations_by_counterparty: "document_ref", bank_operations_by_counterparty: "document_ref",
bank_operations_by_contract: "document_ref", bank_operations_by_contract: "document_ref",
open_items_by_counterparty_or_contract: "counterparty" open_items_by_counterparty_or_contract: "counterparty",
inventory_on_hand_as_of_date: "item",
inventory_purchase_provenance_for_item: "item",
inventory_purchase_documents_for_item: "item",
inventory_supplier_stock_overlap_as_of_date: "item",
inventory_sale_trace_for_item: "item",
inventory_purchase_to_sale_chain: "item",
inventory_aging_by_purchase_date: "item"
}; };
const RESULT_SET_TYPE_BY_INTENT: Partial<Record<AddressIntent, AddressResultSetType>> = { const RESULT_SET_TYPE_BY_INTENT: Partial<Record<AddressIntent, AddressResultSetType>> = {
@ -48,6 +55,13 @@ const RESULT_SET_TYPE_BY_INTENT: Partial<Record<AddressIntent, AddressResultSetT
bank_operations_by_counterparty: "bank_operations_list", bank_operations_by_counterparty: "bank_operations_list",
bank_operations_by_contract: "bank_operations_list", bank_operations_by_contract: "bank_operations_list",
open_items_by_counterparty_or_contract: "open_items_list", open_items_by_counterparty_or_contract: "open_items_list",
inventory_on_hand_as_of_date: "inventory_snapshot",
inventory_purchase_provenance_for_item: "inventory_trace",
inventory_purchase_documents_for_item: "inventory_trace",
inventory_supplier_stock_overlap_as_of_date: "inventory_trace",
inventory_sale_trace_for_item: "inventory_trace",
inventory_purchase_to_sale_chain: "inventory_trace",
inventory_aging_by_purchase_date: "inventory_trace",
period_coverage_profile: "profile_summary", period_coverage_profile: "profile_summary",
document_type_and_account_section_profile: "profile_summary", document_type_and_account_section_profile: "profile_summary",
counterparty_population_and_roles: "profile_summary", counterparty_population_and_roles: "profile_summary",
@ -76,7 +90,15 @@ function toAddressFocusObjectType(value: unknown): AddressFocusObjectType {
if (!normalized) { if (!normalized) {
return "unknown"; return "unknown";
} }
if (normalized === "counterparty" || normalized === "contract" || normalized === "document_ref" || normalized === "account") { if (
normalized === "counterparty" ||
normalized === "contract" ||
normalized === "document_ref" ||
normalized === "account" ||
normalized === "item" ||
normalized === "organization" ||
normalized === "warehouse"
) {
return normalized; return normalized;
} }
return "unknown"; return "unknown";
@ -149,6 +171,44 @@ function extractEntityRefsFromAssistantReply(
return Array.from(dedup.values()); return Array.from(dedup.values());
} }
function extractOrganizationsFromAssistantReply(replyText: string, limit: number = 10): string[] {
const dedup = new Map<string, string>();
const lines = String(replyText ?? "").split(/\r?\n/);
for (const line of lines) {
const match = line.match(/(?:^|\|)\s*организац(?:ия|ии)\s*:\s*([^|]+)/iu);
if (!match) {
continue;
}
const organization = toNonEmptyString(match[1]);
if (!organization) {
continue;
}
const key = organization.toLowerCase();
if (!dedup.has(key)) {
dedup.set(key, organization);
}
if (dedup.size >= limit) {
break;
}
}
return Array.from(dedup.values());
}
function resolveDerivedOrganizationScope(
debug: Record<string, unknown>,
filters: Record<string, unknown>,
replyText: string
): string | null {
const rootFrameContext = toObject(debug.address_root_frame_context) ?? {};
const candidates = [
toNonEmptyString(filters.organization),
toNonEmptyString(rootFrameContext.organization),
...extractOrganizationsFromAssistantReply(replyText)
].filter((value): value is string => Boolean(value));
const dedup = Array.from(new Map(candidates.map((value) => [value.toLowerCase(), value])).values());
return dedup.length === 1 ? dedup[0] : null;
}
function cloneFocusObject(value: AddressFocusObject | null): AddressFocusObject | null { function cloneFocusObject(value: AddressFocusObject | null): AddressFocusObject | null {
if (!value) { if (!value) {
return null; return null;
@ -392,6 +452,14 @@ export function evolveAddressNavigationStateWithAssistantItem(
const resultSetId = `rs-${item.message_id}`; const resultSetId = `rs-${item.message_id}`;
const routeId = toNonEmptyString(debug.selected_recipe); const routeId = toNonEmptyString(debug.selected_recipe);
const filters = normalizeFilters(debug.extracted_filters); const filters = normalizeFilters(debug.extracted_filters);
const derivedOrganizationScope = resolveDerivedOrganizationScope(debug, filters, item.text);
const filtersWithDerivedScope =
derivedOrganizationScope && !toNonEmptyString(filters.organization)
? {
...filters,
organization: derivedOrganizationScope
}
: filters;
const sourceRefs = routeId ? [routeId] : []; const sourceRefs = routeId ? [routeId] : [];
const entityRefs = extractEntityRefsFromAssistantReply(item.text, intent); const entityRefs = extractEntityRefsFromAssistantReply(item.text, intent);
const resultSet: AddressResultSet = { const resultSet: AddressResultSet = {
@ -399,7 +467,7 @@ export function evolveAddressNavigationStateWithAssistantItem(
type: inferResultSetType(intent), type: inferResultSetType(intent),
intent, intent,
route_id: routeId, route_id: routeId,
filters, filters: filtersWithDerivedScope,
source_refs: sourceRefs, source_refs: sourceRefs,
entity_refs: entityRefs, entity_refs: entityRefs,
created_from_turn: turnIndex, created_from_turn: turnIndex,
@ -418,11 +486,11 @@ export function evolveAddressNavigationStateWithAssistantItem(
created_at: createdAt created_at: createdAt
}; };
const normalizedDateScope = { const normalizedDateScope = {
as_of_date: toNonEmptyString(filters.as_of_date), as_of_date: toNonEmptyString(filtersWithDerivedScope.as_of_date),
period_from: toNonEmptyString(filters.period_from), period_from: toNonEmptyString(filtersWithDerivedScope.period_from),
period_to: toNonEmptyString(filters.period_to) period_to: toNonEmptyString(filtersWithDerivedScope.period_to)
}; };
const organizationScope = toNonEmptyString(filters.organization); const organizationScope = toNonEmptyString(filtersWithDerivedScope.organization);
const nextResultSets = capResultSets( const nextResultSets = capResultSets(
[...state.result_sets.filter((itemSet) => itemSet.result_set_id !== resultSetId), resultSet].sort( [...state.result_sets.filter((itemSet) => itemSet.result_set_id !== resultSetId), resultSet].sort(
(left, right) => left.created_from_turn - right.created_from_turn (left, right) => left.created_from_turn - right.created_from_turn

View File

@ -18,11 +18,13 @@ import type {
AddressLimitedReasonCategory, AddressLimitedReasonCategory,
AddressMatchFailureStage, AddressMatchFailureStage,
AddressMcpCallStatus, AddressMcpCallStatus,
AddressModeDetection,
AddressQueryShapeDetection, AddressQueryShapeDetection,
AddressResultMode, AddressResultMode,
AddressResponseType, AddressResponseType,
AddressRuntimeReadiness, AddressRuntimeReadiness,
AddressSemanticFrame AddressSemanticFrame,
AddressIntentResolution
} from "../types/addressQuery"; } from "../types/addressQuery";
import { import {
buildAddressRecipePlan, buildAddressRecipePlan,
@ -1508,9 +1510,38 @@ function applyPreExecutionOrganizationScopeGrounding(input: {
} }
} }
if (!input.filters.organization && !activeOrganization && !resolvedOrganizationFromMessage && candidateOrganizations.length === 1) {
input.filters.organization = candidateOrganizations[0];
if (!input.warnings.includes("organization_auto_selected_from_single_scope_candidate")) {
input.warnings.push("organization_auto_selected_from_single_scope_candidate");
}
if (!input.baseReasons.includes("organization_auto_selected_from_single_scope_candidate")) {
input.baseReasons.push("organization_auto_selected_from_single_scope_candidate");
}
if (input.semanticFrame?.anchor_kind === "organization") {
input.semanticFrame.anchor_value = candidateOrganizations[0];
}
}
return resolvedOrganizationFromMessage; return resolvedOrganizationFromMessage;
} }
function isOrganizationScopedInventoryIntent(intent: AddressIntent): boolean {
return (
intent === "inventory_on_hand_as_of_date" ||
intent === "inventory_purchase_provenance_for_item" ||
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date"
);
}
function collectOrganizationCandidatesFromRows(rows: NormalizedAddressRow[]): string[] {
return mergeKnownOrganizations(rows.map((row) => row.organization).filter((value): value is string => Boolean(value)));
}
function isHeuristicCandidatesIntent(intent: AddressIntent): boolean { function isHeuristicCandidatesIntent(intent: AddressIntent): boolean {
return ( return (
intent === "list_receivables_counterparties" || intent === "list_receivables_counterparties" ||
@ -1976,7 +2007,32 @@ function shouldBoostAutoBroadenedLimit(intent: AddressIntent): boolean {
} }
function shouldClearAsOfDateForHistoryRecovery(intent: AddressIntent): boolean { function shouldClearAsOfDateForHistoryRecovery(intent: AddressIntent): boolean {
return intent === "inventory_purchase_provenance_for_item" || intent === "inventory_purchase_documents_for_item"; 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"
);
}
function shouldDetachLifecycleExecutionFromSnapshotContext(
intent: AddressIntent,
reasons: string[]
): boolean {
if (
intent !== "inventory_purchase_provenance_for_item" &&
intent !== "inventory_purchase_documents_for_item" &&
intent !== "inventory_sale_trace_for_item" &&
intent !== "inventory_purchase_to_sale_chain"
) {
return false;
}
return (
reasons.includes("as_of_date_from_followup_context") ||
reasons.includes("period_from_followup_context") ||
reasons.includes("as_of_date_from_open_items_followup_context")
);
} }
function invertSort(sort: AddressFilterSet["sort"]): AddressFilterSet["sort"] { function invertSort(sort: AddressFilterSet["sort"]): AddressFilterSet["sort"] {
@ -2804,6 +2860,62 @@ function buildLimitedExecutionResult(input: {
}; };
} }
function composeOrganizationClarificationReply(organizations: string[]): string {
const normalizedOrganizations = mergeKnownOrganizations(organizations).slice(0, 10);
const lines = [
normalizedOrganizations.length > 1
? "Нужно уточнить организацию, чтобы не смешивать компании в одном ответе."
: "Нужно уточнить организацию, чтобы продолжить запрос.",
normalizedOrganizations.length > 0
? "Сейчас в доступном контуре вижу такие организации:"
: "Уточни, по какой организации продолжать."
];
for (const organization of normalizedOrganizations) {
lines.push(`- ${organization}`);
}
lines.push("Можешь ответить просто названием компании, и я продолжу этот же запрос.");
return lines.join("\n");
}
function buildOrganizationClarificationExecutionResult(input: {
mode: AddressModeDetection;
shape: AddressQueryShapeDetection;
intent: AddressIntentResolution;
filters: AddressFilterSet;
anchor?: AnchorResolutionDebug;
organizations: string[];
reasons: string[];
semanticFrame?: AddressSemanticFrame | null;
capabilityAudit?: AddressCapabilityAudit;
shadowRouteAudit?: AddressShadowRouteAudit;
routeExpectationAudit?: AddressRouteExpectationAuditState;
}): AddressExecutionResult {
const result = buildLimitedExecutionResult({
mode: input.mode,
shape: input.shape,
intent: input.intent,
filters: input.filters,
missingRequiredFilters: ["organization"],
selectedRecipe: null,
anchor: input.anchor,
mcpCallStatus: "skipped",
rowsFetched: 0,
rowsMatched: 0,
category: "missing_anchor",
reasonText: "не указана организация, а в доступном контуре найдено несколько компаний",
nextStep: "уточните организацию из списка, и я продолжу этот же запрос",
limitations: ["organization_clarification_required", "multiple_known_organizations_detected"],
reasons: [...input.reasons, "organization_clarification_required", "multiple_known_organizations_detected"],
semanticFrame: input.semanticFrame,
capabilityAudit: input.capabilityAudit,
shadowRouteAudit: input.shadowRouteAudit,
routeExpectationAudit: input.routeExpectationAudit
});
result.reply_text = composeOrganizationClarificationReply(input.organizations);
result.debug.organization_candidates = mergeKnownOrganizations(input.organizations);
return result;
}
export class AddressQueryService { export class AddressQueryService {
public async tryHandle(userMessage: string, options: AddressTryHandleOptions = {}): Promise<AddressExecutionResult | null> { public async tryHandle(userMessage: string, options: AddressTryHandleOptions = {}): Promise<AddressExecutionResult | null> {
if (!FEATURE_ASSISTANT_ADDRESS_QUERY_V1) { if (!FEATURE_ASSISTANT_ADDRESS_QUERY_V1) {
@ -2843,6 +2955,31 @@ export class AddressQueryService {
activeOrganization: options.activeOrganization ?? null, activeOrganization: options.activeOrganization ?? null,
knownOrganizations: options.knownOrganizations ?? [] knownOrganizations: options.knownOrganizations ?? []
}); });
const knownOrganizations = mergeKnownOrganizations(options.knownOrganizations ?? []);
const activeOrganization = normalizeOrganizationScopeValue(options.activeOrganization ?? null);
if (
isOrganizationScopedInventoryIntent(intent.intent) &&
!toNonEmptyFilterValue(filters.extracted_filters.organization) &&
!activeOrganization &&
!resolvedOrganizationFromMessage &&
knownOrganizations.length > 1
) {
return buildOrganizationClarificationExecutionResult({
mode,
shape,
intent,
filters: filters.extracted_filters,
organizations: knownOrganizations,
reasons: [...baseReasons, "organization_candidates_from_scope_context"],
semanticFrame,
capabilityAudit: buildCapabilityAudit(intent.intent),
shadowRouteAudit: buildShadowRouteAudit({
intent: intent.intent,
requestedResultMode: resolveRequestedResultMode(intent.intent, filters.extracted_filters, semanticFrame),
filters: filters.extracted_filters
})
});
}
const requestedResultMode = resolveRequestedResultMode(intent.intent, filters.extracted_filters, semanticFrame); 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") &&
@ -2916,6 +3053,50 @@ export class AddressQueryService {
baseReasons.push("as_of_date_derived_for_inventory_on_hand"); baseReasons.push("as_of_date_derived_for_inventory_on_hand");
} }
} }
if (shouldDetachLifecycleExecutionFromSnapshotContext(intent.intent, baseReasons)) {
const detachedExecutionFilters: AddressFilterSet = { ...executionFilters };
let periodDetached = false;
let asOfDetached = false;
if (
toNonEmptyFilterValue(detachedExecutionFilters.period_from) ||
toNonEmptyFilterValue(detachedExecutionFilters.period_to)
) {
delete detachedExecutionFilters.period_from;
delete detachedExecutionFilters.period_to;
periodDetached = true;
}
if (toNonEmptyFilterValue(detachedExecutionFilters.as_of_date)) {
delete detachedExecutionFilters.as_of_date;
asOfDetached = true;
}
if (periodDetached || asOfDetached) {
executionFilters = detachedExecutionFilters;
if (periodDetached && !filters.warnings.includes("period_window_detached_for_lifecycle_execution")) {
filters.warnings.push("period_window_detached_for_lifecycle_execution");
}
if (periodDetached && !baseReasons.includes("period_window_detached_for_lifecycle_execution")) {
baseReasons.push("period_window_detached_for_lifecycle_execution");
}
if (
(periodDetached || asOfDetached) &&
!filters.warnings.includes("lifecycle_execution_detached_from_snapshot_date")
) {
filters.warnings.push("lifecycle_execution_detached_from_snapshot_date");
}
if (
(periodDetached || asOfDetached) &&
!baseReasons.includes("lifecycle_execution_detached_from_snapshot_date")
) {
baseReasons.push("lifecycle_execution_detached_from_snapshot_date");
}
if (asOfDetached && !filters.warnings.includes("as_of_date_cleared_for_history_recovery")) {
filters.warnings.push("as_of_date_cleared_for_history_recovery");
}
if (asOfDetached && !baseReasons.includes("as_of_date_cleared_for_history_recovery")) {
baseReasons.push("as_of_date_cleared_for_history_recovery");
}
}
}
const capabilityDecision = resolveAddressCapabilityRouteDecision(intent.intent); const capabilityDecision = resolveAddressCapabilityRouteDecision(intent.intent);
const capabilityAudit = buildCapabilityAudit(intent.intent); const capabilityAudit = buildCapabilityAudit(intent.intent);
const shadowRouteAudit = buildShadowRouteAudit({ const shadowRouteAudit = buildShadowRouteAudit({
@ -3419,6 +3600,42 @@ export class AddressQueryService {
baseReasons.push("organization_scope_live_grounding_recovered_rows"); baseReasons.push("organization_scope_live_grounding_recovered_rows");
} }
} }
if (
filteredRows.length > 0 &&
isOrganizationScopedInventoryIntent(intent.intent) &&
!toNonEmptyFilterValue(filters.extracted_filters.organization)
) {
const observedOrganizations = collectOrganizationCandidatesFromRows(filteredRows);
if (observedOrganizations.length === 1) {
filters.extracted_filters = {
...filters.extracted_filters,
organization: observedOrganizations[0]
};
executionFilters = {
...executionFilters,
organization: observedOrganizations[0]
};
if (!filters.warnings.includes("organization_grounded_from_observed_rows")) {
filters.warnings.push("organization_grounded_from_observed_rows");
}
if (!baseReasons.includes("organization_grounded_from_observed_rows")) {
baseReasons.push("organization_grounded_from_observed_rows");
}
} else if (observedOrganizations.length > 1) {
return buildOrganizationClarificationExecutionResult({
mode,
shape,
intent,
filters: filters.extracted_filters,
anchor,
organizations: observedOrganizations,
reasons: [...baseReasons, "organization_candidates_from_observed_rows"],
semanticFrame,
capabilityAudit,
shadowRouteAudit
});
}
}
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);
@ -3655,7 +3872,7 @@ export class AddressQueryService {
const broadenedAdjustments: string[] = []; 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)) { if (shouldClearAsOfDateForHistoryRecovery(intent.intent) && toNonEmptyFilterValue(autoBroadenedFilters.as_of_date)) {
delete autoBroadenedFilters.as_of_date; delete autoBroadenedFilters.as_of_date;
broadenedAdjustments.push("as_of_date_cleared_for_history_recovery"); broadenedAdjustments.push("as_of_date_cleared_for_history_recovery");
} }

View File

@ -38,7 +38,8 @@ const INVENTORY_MOVEMENTS_QUERY_TEMPLATE = `
ПРЕДСТАВЛЕНИЕ(Движения.СубконтоДт3) КАК СубконтоДт3, ПРЕДСТАВЛЕНИЕ(Движения.СубконтоДт3) КАК СубконтоДт3,
ПРЕДСТАВЛЕНИЕ(Движения.СубконтоКт1) КАК СубконтоКт1, ПРЕДСТАВЛЕНИЕ(Движения.СубконтоКт1) КАК СубконтоКт1,
ПРЕДСТАВЛЕНИЕ(Движения.СубконтоКт2) КАК СубконтоКт2, ПРЕДСТАВЛЕНИЕ(Движения.СубконтоКт2) КАК СубконтоКт2,
ПРЕДСТАВЛЕНИЕ(Движения.СубконтоКт3) КАК СубконтоКт3 ПРЕДСТАВЛЕНИЕ(Движения.СубконтоКт3) КАК СубконтоКт3,
ПРЕДСТАВЛЕНИЕ(Движения.Организация) КАК Организация
ИЗ ИЗ
РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто КАК Движения РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто КАК Движения
__WHERE_CLAUSE__ __WHERE_CLAUSE__
@ -967,6 +968,21 @@ function toDateTimeExpr(isoDate: string, endOfDay: boolean): string | null {
return `ДАТАВРЕМЯ(${year}, ${month}, ${day}, ${hour}, ${minute}, ${second})`; return `ДАТАВРЕМЯ(${year}, ${month}, ${day}, ${hour}, ${minute}, ${second})`;
} }
function toQueryStringLiteral(value: string): string {
return String(value ?? "").replace(/"/g, '""');
}
function buildOrganizationPresentationCondition(filters: AddressFilterSet, fieldPath: string): string | null {
const organization =
typeof filters.organization === "string" && filters.organization.trim().length > 0
? filters.organization.trim()
: "";
if (!organization) {
return null;
}
return `ПРЕДСТАВЛЕНИЕ(${fieldPath}) = "${toQueryStringLiteral(organization)}"`;
}
function buildWhereClause(filters: AddressFilterSet, fieldPath: string, extraConditions: string[] = []): string { function buildWhereClause(filters: AddressFilterSet, fieldPath: string, extraConditions: string[] = []): string {
const periodFromExpr = const periodFromExpr =
typeof filters.period_from === "string" && filters.period_from.trim().length > 0 typeof filters.period_from === "string" && filters.period_from.trim().length > 0
@ -1138,11 +1154,16 @@ function buildInventoryMovementQuery(
: side === "kt" : side === "kt"
? creditPredicate ? creditPredicate
: `(${debitPredicate} ИЛИ ${creditPredicate})`; : `(${debitPredicate} ИЛИ ${creditPredicate})`;
const organizationCondition = buildOrganizationPresentationCondition(filters, "Движения.Организация");
return INVENTORY_MOVEMENTS_QUERY_TEMPLATE return INVENTORY_MOVEMENTS_QUERY_TEMPLATE
.replace("__LIMIT__", String(resolvedLimit)) .replace("__LIMIT__", String(resolvedLimit))
.replace( .replace(
"__WHERE_CLAUSE__", "__WHERE_CLAUSE__",
buildWhereClause(filters, "Движения.Период", [inventoryCondition]) buildWhereClause(
filters,
"Движения.Период",
[inventoryCondition, organizationCondition].filter((item): item is string => Boolean(item))
)
) )
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort)); .replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
} }

View File

@ -359,6 +359,15 @@ function isInventoryDrilldownFrameIntent(intent: AddressIntent | undefined): boo
); );
} }
function isInventoryLifecycleHistoryIntent(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"
);
}
function buildInventoryRootFollowupContext( function buildInventoryRootFollowupContext(
followupContext: AddressFollowupContext | null followupContext: AddressFollowupContext | null
): AddressFollowupContext | null { ): AddressFollowupContext | null {
@ -529,7 +538,7 @@ function hasBareInventoryPurchaseDateFollowupCue(text: string): boolean {
} }
function hasInventorySaleFollowupCue(text: string): boolean { function hasInventorySaleFollowupCue(text: string): boolean {
return /(?:кому\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали|кому\s+был\s+продан|кому\s+дальше\s+продали|кто\s+купил|buyer|покупател)/iu.test( return /(?:кому\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали|кому\s+был\s+продан|кому\s+дальше\s+продали|куда\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали|куда\s+ушла\s+позиция|куда\s+ушел\s+товар|кто\s+купил|buyer|покупател)/iu.test(
String(text ?? "") String(text ?? "")
); );
} }
@ -786,14 +795,11 @@ function mergeFollowupFilters(
} }
if ( if (
!sameDateRequested && !sameDateRequested &&
(intent === "inventory_purchase_provenance_for_item" || (intent === "inventory_aging_by_purchase_date" || isInventoryLifecycleHistoryIntent(intent)) &&
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date") &&
!hasExplicitPeriodLiteral(userMessage) && !hasExplicitPeriodLiteral(userMessage) &&
!hasExplicitCurrentDateHint(userMessage) !hasExplicitCurrentDateHint(userMessage)
) { ) {
if (intent === "inventory_aging_by_purchase_date") {
const inheritedAsOfDate = previousAsOfDate ?? previousPeriodTo ?? previousPeriodFrom; const inheritedAsOfDate = previousAsOfDate ?? previousPeriodTo ?? previousPeriodFrom;
const currentAsOfDate = toNonEmptyString(merged.as_of_date); const currentAsOfDate = toNonEmptyString(merged.as_of_date);
const todayIso = new Date().toISOString().slice(0, 10); const todayIso = new Date().toISOString().slice(0, 10);
@ -803,6 +809,7 @@ function mergeFollowupFilters(
reasons.push("as_of_date_from_followup_context"); reasons.push("as_of_date_from_followup_context");
} }
} }
}
if ( if (
!sameDateRequested && !sameDateRequested &&
(intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date") && (intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date") &&
@ -890,6 +897,7 @@ function mergeFollowupFilters(
const hasFollowupSignal = hasAddressFollowupContextSignal(userMessage); const hasFollowupSignal = hasAddressFollowupContextSignal(userMessage);
const hasExplicitPeriodInMessage = hasExplicitPeriodLiteral(userMessage); const hasExplicitPeriodInMessage = hasExplicitPeriodLiteral(userMessage);
const hasExplicitCurrentDateInMessage = hasExplicitCurrentDateHint(userMessage); const hasExplicitCurrentDateInMessage = hasExplicitCurrentDateHint(userMessage);
const inventoryLifecycleHistoryIntent = isInventoryLifecycleHistoryIntent(intent);
const asOfPrimaryIntent = const asOfPrimaryIntent =
intent === "account_balance_snapshot" || intent === "account_balance_snapshot" ||
intent === "documents_forming_balance" || intent === "documents_forming_balance" ||
@ -928,7 +936,13 @@ function mergeFollowupFilters(
} }
} }
if (!currentHasPeriod && previousHasPeriod && hasFollowupSignal && !hasExplicitPeriodInMessage) { if (
!currentHasPeriod &&
previousHasPeriod &&
hasFollowupSignal &&
!hasExplicitPeriodInMessage &&
!inventoryLifecycleHistoryIntent
) {
if (previousPeriodFrom) { if (previousPeriodFrom) {
merged.period_from = previousPeriodFrom; merged.period_from = previousPeriodFrom;
} }

View File

@ -42,10 +42,16 @@ interface AddressSessionScope {
export interface RunAssistantAddressAttemptRuntimeInput<ResponseType = unknown> export interface RunAssistantAddressAttemptRuntimeInput<ResponseType = unknown>
extends Omit< extends Omit<
RunAssistantAddressRuntimeInput<ResponseType>, RunAssistantAddressRuntimeInput<ResponseType>,
"llmProvider" | "useMock" | "payloadContextPeriodHint" | "runAddressLaneAttempt" | "finalizeAddressLaneResponse" | "tryHandleLivingChat" | "llmProvider"
| "useMock"
| "payloadContextPeriodHint"
| "runAddressLaneAttempt"
| "finalizeAddressLaneResponse"
| "tryHandleLivingChat"
> { > {
payload: AddressAttemptPayload; payload: AddressAttemptPayload;
sessionScope: AddressSessionScope; sessionScope: AddressSessionScope;
sessionAddressNavigationState?: unknown;
mergeFollowupContextWithOrganizationScope: RunAssistantAddressLaneAttemptRuntimeInput["mergeFollowupContextWithOrganizationScope"]; mergeFollowupContextWithOrganizationScope: RunAssistantAddressLaneAttemptRuntimeInput["mergeFollowupContextWithOrganizationScope"];
runAddressQueryTryHandle: RunAssistantAddressLaneAttemptRuntimeInput["runAddressQueryTryHandle"]; runAddressQueryTryHandle: RunAssistantAddressLaneAttemptRuntimeInput["runAddressQueryTryHandle"];
mergeKnownOrganizations: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["mergeKnownOrganizations"]; mergeKnownOrganizations: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["mergeKnownOrganizations"];
@ -227,6 +233,8 @@ export async function runAssistantAddressAttemptRuntime<ResponseType = unknown>(
sessionId: input.sessionId, sessionId: input.sessionId,
userMessage: input.userMessage, userMessage: input.userMessage,
sessionItems: input.sessionItems, sessionItems: input.sessionItems,
sessionAddressNavigationState: input.sessionAddressNavigationState,
sessionScope: input.sessionScope,
payload: input.payload, payload: input.payload,
featureAddressLlmPredecomposeV1: input.featureAddressLlmPredecomposeV1, featureAddressLlmPredecomposeV1: input.featureAddressLlmPredecomposeV1,
runAddressLlmPreDecompose: input.runAddressLlmPreDecompose, runAddressLlmPreDecompose: input.runAddressLlmPreDecompose,

View File

@ -206,7 +206,13 @@ export function runAssistantAddressLaneResponseRuntime<ResponseType = AssistantM
if (followupOffer) { if (followupOffer) {
debug.address_followup_offer = followupOffer; debug.address_followup_offer = followupOffer;
} }
const debugKnownOrganizations = input.mergeKnownOrganizations(input.knownOrganizations); const laneOrganizationCandidates = Array.isArray(input.addressLane.debug?.organization_candidates)
? input.addressLane.debug.organization_candidates
: [];
const debugKnownOrganizations = input.mergeKnownOrganizations([
...input.knownOrganizations,
...laneOrganizationCandidates
]);
const debugFilters = const debugFilters =
debug?.extracted_filters && typeof debug.extracted_filters === "object" debug?.extracted_filters && typeof debug.extracted_filters === "object"
? (debug.extracted_filters as Record<string, unknown>) ? (debug.extracted_filters as Record<string, unknown>)

View File

@ -1,6 +1,12 @@
export interface BuildAssistantAddressOrchestrationRuntimeInput { export interface BuildAssistantAddressOrchestrationRuntimeInput {
userMessage: string; userMessage: string;
sessionItems: unknown[]; sessionItems: unknown[];
sessionAddressNavigationState?: unknown;
sessionOrganizationScope?: {
knownOrganizations?: unknown;
selectedOrganization?: unknown;
activeOrganization?: unknown;
} | null;
llmProvider: unknown; llmProvider: unknown;
useMock: boolean; useMock: boolean;
featureAddressLlmPredecomposeV1: boolean; featureAddressLlmPredecomposeV1: boolean;
@ -15,7 +21,8 @@ export interface BuildAssistantAddressOrchestrationRuntimeInput {
userMessage: string, userMessage: string,
sessionItems: unknown[], sessionItems: unknown[],
addressInputMessage: string, addressInputMessage: string,
addressPreDecompose: Record<string, unknown> addressPreDecompose: Record<string, unknown>,
sessionAddressNavigationState?: unknown
) => AssistantAddressCarryoverLike | null; ) => AssistantAddressCarryoverLike | null;
resolveAssistantOrchestrationDecision: (input: { resolveAssistantOrchestrationDecision: (input: {
rawUserMessage: string; rawUserMessage: string;
@ -23,6 +30,7 @@ export interface BuildAssistantAddressOrchestrationRuntimeInput {
followupContext: unknown; followupContext: unknown;
llmPreDecomposeMeta: Record<string, unknown>; llmPreDecomposeMeta: Record<string, unknown>;
sessionItems?: unknown[]; sessionItems?: unknown[];
sessionOrganizationScope?: unknown;
useMock: boolean; useMock: boolean;
}) => Record<string, unknown>; }) => Record<string, unknown>;
buildAddressDialogContinuationContractV2: ( buildAddressDialogContinuationContractV2: (
@ -57,7 +65,7 @@ function hasSelectedObjectInventorySignal(text: string | null): boolean {
} }
function hasSelectedObjectInventoryActionCue(text: string | null): boolean { 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( return /(?:кому[\s\S]{0,80}продал[аи]?|кому[\s\S]{0,80}реализова[нлт][а-я]*|кому\s+был\s+продан|куда[\s\S]{0,80}продал[аи]?|куда[\s\S]{0,80}реализова[нлт][а-я]*|кто[\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 ?? "") String(text ?? "")
); );
} }
@ -154,7 +162,8 @@ export async function buildAssistantAddressOrchestrationRuntime(
input.userMessage, input.userMessage,
input.sessionItems, input.sessionItems,
addressInputMessage, addressInputMessage,
addressPreDecompose addressPreDecompose,
input.sessionAddressNavigationState
); );
if ( if (
shouldPreferRawFollowupMessage( shouldPreferRawFollowupMessage(
@ -180,7 +189,8 @@ export async function buildAssistantAddressOrchestrationRuntime(
input.userMessage, input.userMessage,
input.sessionItems, input.sessionItems,
addressInputMessage, addressInputMessage,
addressPreDecompose addressPreDecompose,
input.sessionAddressNavigationState
); );
} }
@ -191,6 +201,7 @@ export async function buildAssistantAddressOrchestrationRuntime(
followupContext, followupContext,
llmPreDecomposeMeta: addressPreDecompose, llmPreDecomposeMeta: addressPreDecompose,
sessionItems: input.sessionItems, sessionItems: input.sessionItems,
sessionOrganizationScope: input.sessionOrganizationScope ?? null,
useMock: input.useMock useMock: input.useMock
}); });
const dialogContinuationContract = input.buildAddressDialogContinuationContractV2( const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(

View File

@ -19,6 +19,12 @@ export interface RunAssistantAddressRuntimeInput<ResponseType = unknown> {
sessionId: string; sessionId: string;
userMessage: string; userMessage: string;
sessionItems: unknown[]; sessionItems: unknown[];
sessionAddressNavigationState?: unknown;
sessionOrganizationScope?: {
knownOrganizations?: unknown;
selectedOrganization?: unknown;
activeOrganization?: unknown;
} | null;
llmProvider: unknown; llmProvider: unknown;
useMock: boolean; useMock: boolean;
featureAddressLlmPredecomposeV1: boolean; featureAddressLlmPredecomposeV1: boolean;
@ -112,6 +118,8 @@ export async function runAssistantAddressRuntime<ResponseType = unknown>(
const addressOrchestrationRuntime = await runAddressOrchestrationRuntimeSafe({ const addressOrchestrationRuntime = await runAddressOrchestrationRuntimeSafe({
userMessage: input.userMessage, userMessage: input.userMessage,
sessionItems: input.sessionItems, sessionItems: input.sessionItems,
sessionAddressNavigationState: input.sessionAddressNavigationState,
sessionOrganizationScope: input.sessionOrganizationScope ?? null,
llmProvider: input.llmProvider, llmProvider: input.llmProvider,
useMock: input.useMock, useMock: input.useMock,
featureAddressLlmPredecomposeV1: input.featureAddressLlmPredecomposeV1, featureAddressLlmPredecomposeV1: input.featureAddressLlmPredecomposeV1,

View File

@ -11,6 +11,11 @@ interface AssistantAddressAttemptPayloadLike {
export interface BuildAssistantAddressRuntimeInputInput<ResponseType = unknown> export interface BuildAssistantAddressRuntimeInputInput<ResponseType = unknown>
extends Omit<RunAssistantAddressRuntimeInput<ResponseType>, "llmProvider" | "useMock" | "payloadContextPeriodHint"> { extends Omit<RunAssistantAddressRuntimeInput<ResponseType>, "llmProvider" | "useMock" | "payloadContextPeriodHint"> {
payload: AssistantAddressAttemptPayloadLike; payload: AssistantAddressAttemptPayloadLike;
sessionScope?: {
knownOrganizations?: unknown;
selectedOrganization?: unknown;
activeOrganization?: unknown;
} | null;
} }
export function buildAssistantAddressRuntimeInput<ResponseType = unknown>( export function buildAssistantAddressRuntimeInput<ResponseType = unknown>(
@ -21,6 +26,8 @@ export function buildAssistantAddressRuntimeInput<ResponseType = unknown>(
sessionId: input.sessionId, sessionId: input.sessionId,
userMessage: input.userMessage, userMessage: input.userMessage,
sessionItems: input.sessionItems, sessionItems: input.sessionItems,
sessionAddressNavigationState: input.sessionAddressNavigationState,
sessionOrganizationScope: input.sessionScope,
llmProvider: input.payload.llmProvider, llmProvider: input.payload.llmProvider,
useMock: Boolean(input.payload.useMock), useMock: Boolean(input.payload.useMock),
featureAddressLlmPredecomposeV1: input.featureAddressLlmPredecomposeV1, featureAddressLlmPredecomposeV1: input.featureAddressLlmPredecomposeV1,

View File

@ -57,6 +57,84 @@ export interface AssistantLivingChatRuntimeOutput {
debug: Record<string, unknown> | null; debug: Record<string, unknown> | null;
} }
function formatIsoDateForReply(value: unknown): string | null {
const source = String(value ?? "").trim();
const match = source.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
return null;
}
return `${match[3]}.${match[2]}.${match[1]}`;
}
function findLastGroundedInventoryAddressDebug(items: unknown[]): Record<string, unknown> | null {
if (!Array.isArray(items)) {
return null;
}
for (let index = items.length - 1; index >= 0; index -= 1) {
const item = items[index] as { role?: string; debug?: Record<string, unknown> } | null;
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
continue;
}
const debug = item.debug;
const answerGroundingCheck =
debug.answer_grounding_check && typeof debug.answer_grounding_check === "object"
? (debug.answer_grounding_check as Record<string, unknown>)
: null;
const groundingStatus = String(answerGroundingCheck?.status ?? "");
const detectedIntent = String(debug.detected_intent ?? "");
const capabilityId = String(debug.capability_id ?? "");
const rootFrameContext =
debug.address_root_frame_context && typeof debug.address_root_frame_context === "object"
? (debug.address_root_frame_context as Record<string, unknown>)
: null;
const rootIntent = String(rootFrameContext?.root_intent ?? "");
const isInventoryContext =
detectedIntent === "inventory_on_hand_as_of_date" ||
capabilityId === "confirmed_inventory_on_hand_as_of_date" ||
rootIntent === "inventory_on_hand_as_of_date";
if (groundingStatus === "grounded" && isInventoryContext) {
return debug;
}
}
return null;
}
function buildInventoryHistoryCapabilityFollowupReply(input: {
organization: string | null;
addressDebug: Record<string, unknown> | null;
toNonEmptyString: (value: unknown) => string | null;
}): string {
const rootFrameContext =
input.addressDebug?.address_root_frame_context && typeof input.addressDebug.address_root_frame_context === "object"
? (input.addressDebug.address_root_frame_context as Record<string, unknown>)
: null;
const extractedFilters =
input.addressDebug?.extracted_filters && typeof input.addressDebug.extracted_filters === "object"
? (input.addressDebug.extracted_filters as Record<string, unknown>)
: null;
const organization =
input.organization ??
input.toNonEmptyString(rootFrameContext?.organization) ??
input.toNonEmptyString(extractedFilters?.organization);
const lastAsOfDate =
formatIsoDateForReply(rootFrameContext?.as_of_date) ??
formatIsoDateForReply(extractedFilters?.as_of_date);
const organizationPart = organization ? ` по компании «${organization}»` : "";
const referenceLine = lastAsOfDate
? `Да, могу. Сейчас мы уже смотрели складской срез${organizationPart} на ${lastAsOfDate}.`
: `Да, могу показать исторические данные${organizationPart} в этом же складском контуре.`;
return [
referenceLine,
`Могу показать исторические остатки${organizationPart} за нужный месяц, дату или год.`,
"Например:",
"- `на март 2020`",
"- `на июнь 2016`",
"- `за 2017 год`",
"- `сравни июнь 2016 с текущим срезом`",
"Если хочешь, сразу покажу нужный исторический период."
].join("\n");
}
export async function runAssistantLivingChatRuntime( export async function runAssistantLivingChatRuntime(
input: AssistantLivingChatRuntimeInput input: AssistantLivingChatRuntimeInput
): Promise<AssistantLivingChatRuntimeOutput> { ): Promise<AssistantLivingChatRuntimeOutput> {
@ -77,6 +155,11 @@ export async function runAssistantLivingChatRuntime(
let knownOrganizations = input.mergeKnownOrganizations(input.sessionScope.knownOrganizations ?? []); let knownOrganizations = input.mergeKnownOrganizations(input.sessionScope.knownOrganizations ?? []);
let selectedOrganization = input.toNonEmptyString(input.sessionScope.selectedOrganization); let selectedOrganization = input.toNonEmptyString(input.sessionScope.selectedOrganization);
let activeOrganization = input.toNonEmptyString(input.sessionScope.activeOrganization); let activeOrganization = input.toNonEmptyString(input.sessionScope.activeOrganization);
const contextualInventoryHistoryCapabilityFollowup =
input.modeDecision?.reason === "inventory_history_capability_followup_detected";
const lastGroundedInventoryAddressDebug = contextualInventoryHistoryCapabilityFollowup
? findLastGroundedInventoryAddressDebug(input.sessionItems)
: null;
if (capabilityMetaQuery && (destructiveSignal || dangerSignal)) { if (capabilityMetaQuery && (destructiveSignal || dangerSignal)) {
chatText = input.buildAssistantSafetyRefusalReply(); chatText = input.buildAssistantSafetyRefusalReply();
@ -119,6 +202,15 @@ export async function runAssistantLivingChatRuntime(
} else if (capabilityMetaQuery && operationalSignal && !input.hasAssistantCapabilityQuestionSignal(userMessage)) { } else if (capabilityMetaQuery && operationalSignal && !input.hasAssistantCapabilityQuestionSignal(userMessage)) {
chatText = input.buildAssistantOperationalBoundaryReply(); chatText = input.buildAssistantOperationalBoundaryReply();
livingChatSource = "deterministic_operational_boundary"; livingChatSource = "deterministic_operational_boundary";
} else if (contextualInventoryHistoryCapabilityFollowup) {
const scopedOrganization = selectedOrganization ?? activeOrganization ?? null;
chatText = buildInventoryHistoryCapabilityFollowupReply({
organization: scopedOrganization,
addressDebug: lastGroundedInventoryAddressDebug,
toNonEmptyString: input.toNonEmptyString
});
activeOrganization = scopedOrganization ?? activeOrganization;
livingChatSource = "deterministic_inventory_history_capability_contract";
} else if (capabilityMetaQuery) { } else if (capabilityMetaQuery) {
chatText = input.buildAssistantCapabilityContractReply(); chatText = input.buildAssistantCapabilityContractReply();
livingChatSource = "deterministic_capability_contract"; livingChatSource = "deterministic_capability_contract";

View File

@ -61,7 +61,7 @@ export function normalizeOrganizationScopeValue(value: unknown): string | null {
if (!normalized) { if (!normalized) {
return null; return null;
} }
let unwrapped = normalized.replace(/^\\+|\\+$/g, "").trim(); let unwrapped = normalized.trim();
if ( if (
(unwrapped.startsWith('"') && unwrapped.endsWith('"')) || (unwrapped.startsWith('"') && unwrapped.endsWith('"')) ||
(unwrapped.startsWith("'") && unwrapped.endsWith("'")) (unwrapped.startsWith("'") && unwrapped.endsWith("'"))

View File

@ -1,3 +1,5 @@
import type { AddressNavigationState } from "../types/addressNavigation";
export interface AssistantSessionOrganizationScopeContext { export interface AssistantSessionOrganizationScopeContext {
knownOrganizations: string[]; knownOrganizations: string[];
selectedOrganization: string | null; selectedOrganization: string | null;
@ -7,6 +9,7 @@ export interface AssistantSessionOrganizationScopeContext {
export interface ResolveSessionOrganizationScopeContextRuntimeInput<ItemType = unknown> { export interface ResolveSessionOrganizationScopeContextRuntimeInput<ItemType = unknown> {
userMessage: string; userMessage: string;
items: ItemType[]; items: ItemType[];
addressNavigationState?: AddressNavigationState | null;
extractKnownOrganizationsFromHistory: (items: ItemType[]) => string[]; extractKnownOrganizationsFromHistory: (items: ItemType[]) => string[];
resolveOrganizationSelectionFromMessage: (userMessage: string, knownOrganizations: string[]) => string | null; resolveOrganizationSelectionFromMessage: (userMessage: string, knownOrganizations: string[]) => string | null;
findLastAssistantActiveOrganization: (items: ItemType[]) => string | null; findLastAssistantActiveOrganization: (items: ItemType[]) => string | null;
@ -20,16 +23,62 @@ export interface MergeFollowupContextWithOrganizationScopeRuntimeInput {
toNonEmptyString: (value: unknown) => string | null; toNonEmptyString: (value: unknown) => string | null;
} }
function extractOrganizationsFromNavigationState(
addressNavigationState: AddressNavigationState | null | undefined,
normalizeOrganizationScopeValue: ResolveSessionOrganizationScopeContextRuntimeInput["normalizeOrganizationScopeValue"]
): string[] {
if (!addressNavigationState || typeof addressNavigationState !== "object") {
return [];
}
const collected: string[] = [];
const directOrganization = normalizeOrganizationScopeValue(addressNavigationState.session_context?.organization_scope);
if (directOrganization) {
collected.push(directOrganization);
}
for (const resultSet of Array.isArray(addressNavigationState.result_sets) ? addressNavigationState.result_sets : []) {
const scopedOrganization = normalizeOrganizationScopeValue(resultSet?.filters?.organization);
if (scopedOrganization) {
collected.push(scopedOrganization);
}
}
return Array.from(new Map(collected.map((value) => [value.toLowerCase(), value])).values());
}
function resolveActiveOrganizationFromNavigationState(
addressNavigationState: AddressNavigationState | null | undefined,
normalizeOrganizationScopeValue: ResolveSessionOrganizationScopeContextRuntimeInput["normalizeOrganizationScopeValue"]
): string | null {
if (!addressNavigationState || typeof addressNavigationState !== "object") {
return null;
}
return normalizeOrganizationScopeValue(addressNavigationState.session_context?.organization_scope);
}
export function resolveSessionOrganizationScopeContextRuntime<ItemType = unknown>( export function resolveSessionOrganizationScopeContextRuntime<ItemType = unknown>(
input: ResolveSessionOrganizationScopeContextRuntimeInput<ItemType> input: ResolveSessionOrganizationScopeContextRuntimeInput<ItemType>
): AssistantSessionOrganizationScopeContext { ): AssistantSessionOrganizationScopeContext {
const knownOrganizations = input.extractKnownOrganizationsFromHistory(input.items); const knownOrganizations = Array.from(
new Map(
[
...extractOrganizationsFromNavigationState(input.addressNavigationState, input.normalizeOrganizationScopeValue),
...input.extractKnownOrganizationsFromHistory(input.items)
].map((value) => [String(value).toLowerCase(), value])
).values()
);
const selectedOrganization = input.resolveOrganizationSelectionFromMessage( const selectedOrganization = input.resolveOrganizationSelectionFromMessage(
input.userMessage, input.userMessage,
knownOrganizations knownOrganizations
); );
const lastActiveOrganization = input.findLastAssistantActiveOrganization(input.items); const lastActiveOrganization = input.findLastAssistantActiveOrganization(input.items);
const activeOrganization = selectedOrganization ?? input.normalizeOrganizationScopeValue(lastActiveOrganization); const navigationActiveOrganization = resolveActiveOrganizationFromNavigationState(
input.addressNavigationState,
input.normalizeOrganizationScopeValue
);
const activeOrganization =
selectedOrganization ??
navigationActiveOrganization ??
input.normalizeOrganizationScopeValue(lastActiveOrganization) ??
(knownOrganizations.length === 1 ? knownOrganizations[0] : null);
return { return {
knownOrganizations, knownOrganizations,
@ -57,5 +106,16 @@ export function mergeFollowupContextWithOrganizationScopeRuntime(
previousFilters.organization = normalizedOrganization; previousFilters.organization = normalizedOrganization;
} }
base.previous_filters = previousFilters; base.previous_filters = previousFilters;
const rootFiltersRaw = base.root_filters;
const rootFilters =
rootFiltersRaw && typeof rootFiltersRaw === "object"
? { ...(rootFiltersRaw as Record<string, unknown>) }
: {};
if (!input.toNonEmptyString(rootFilters.organization)) {
rootFilters.organization = normalizedOrganization;
}
if (Object.keys(rootFilters).length > 0) {
base.root_filters = rootFilters;
}
return base; return base;
} }

View File

@ -20,6 +20,7 @@ import * as assistantAddressAttemptRuntimeAdapter_1 from "./assistantAddressAtte
import * as assistantCoverageGrounding_1 from "./assistantCoverageGrounding"; import * as assistantCoverageGrounding_1 from "./assistantCoverageGrounding";
import * as assistantDeepTurnAttemptRuntimeAdapter_1 from "./assistantDeepTurnAttemptRuntimeAdapter"; import * as assistantDeepTurnAttemptRuntimeAdapter_1 from "./assistantDeepTurnAttemptRuntimeAdapter";
import * as assistantOrganizationScopeRuntimeAdapter_1 from "./assistantOrganizationScopeRuntimeAdapter"; import * as assistantOrganizationScopeRuntimeAdapter_1 from "./assistantOrganizationScopeRuntimeAdapter";
import * as assistantOrganizationMatcher_1 from "./assistantOrganizationMatcher";
import * as assistantTurnAttemptRuntimeAdapter_1 from "./assistantTurnAttemptRuntimeAdapter"; import * as assistantTurnAttemptRuntimeAdapter_1 from "./assistantTurnAttemptRuntimeAdapter";
import * as assistantTurnRuntimeDepsAdapter_1 from "./assistantTurnRuntimeDepsAdapter"; import * as assistantTurnRuntimeDepsAdapter_1 from "./assistantTurnRuntimeDepsAdapter";
import * as assistantTurnRuntimeInputBuilder_1 from "./assistantTurnRuntimeInputBuilder"; import * as assistantTurnRuntimeInputBuilder_1 from "./assistantTurnRuntimeInputBuilder";
@ -1427,6 +1428,7 @@ function buildAddressDebugPayload(addressDebug, llmPreDecomposeMeta = null) {
account_scope_drop_reason: addressDebug.account_scope_drop_reason, account_scope_drop_reason: addressDebug.account_scope_drop_reason,
runtime_readiness: addressDebug.runtime_readiness, runtime_readiness: addressDebug.runtime_readiness,
limited_reason_category: addressDebug.limited_reason_category, limited_reason_category: addressDebug.limited_reason_category,
organization_candidates: addressDebug.organization_candidates ?? undefined,
response_type: addressDebug.response_type, response_type: addressDebug.response_type,
requested_result_mode: addressDebug.requested_result_mode ?? undefined, requested_result_mode: addressDebug.requested_result_mode ?? undefined,
result_mode: addressDebug.result_mode ?? undefined, result_mode: addressDebug.result_mode ?? undefined,
@ -2747,9 +2749,18 @@ function resolveDebtRoleSwapFollowupIntent(userMessage, previousIntent) {
} }
return null; return null;
} }
function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMessage = null, llmPreDecomposeMeta = null) { function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMessage = null, llmPreDecomposeMeta = null, addressNavigationState = null) {
const previousAddressItem = findLastAddressAssistantItem(items); const previousAddressItem = findLastAddressAssistantItem(items);
const previousAddressDebug = previousAddressItem?.debug ?? null; const previousAddressDebug = previousAddressItem?.debug ?? null;
const lastOrganizationClarificationDebug = findLastOrganizationClarificationAddressDebug(items);
const organizationClarificationCandidates = Array.isArray(lastOrganizationClarificationDebug?.organization_candidates)
? mergeKnownOrganizations(lastOrganizationClarificationDebug.organization_candidates)
: [];
const organizationClarificationSelection = resolveOrganizationSelectionFromMessage(userMessage, organizationClarificationCandidates) ??
(toNonEmptyString(alternateMessage)
? resolveOrganizationSelectionFromMessage(String(alternateMessage ?? ""), organizationClarificationCandidates)
: null);
const hasOrganizationClarificationContinuation = Boolean(lastOrganizationClarificationDebug && organizationClarificationSelection);
const followupOffer = previousAddressDebug ? buildAddressFollowupOffer(previousAddressDebug) : null; const followupOffer = previousAddressDebug ? buildAddressFollowupOffer(previousAddressDebug) : null;
const hasImplicitContinuationSignal = Boolean(previousAddressDebug) && const hasImplicitContinuationSignal = Boolean(previousAddressDebug) &&
Boolean(followupOffer?.enabled) && Boolean(followupOffer?.enabled) &&
@ -2780,10 +2791,15 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
!hasPrimaryFollowupSignal && !hasPrimaryFollowupSignal &&
!hasAlternateFollowupSignal && !hasAlternateFollowupSignal &&
!hasImplicitContinuationSignal && !hasImplicitContinuationSignal &&
!hasOrganizationClarificationContinuation &&
!hasIndexReferenceSignal) { !hasIndexReferenceSignal) {
return null; return null;
} }
if (!hasPrimaryFollowupSignal && !hasAlternateFollowupSignal && !hasImplicitContinuationSignal && !hasIndexReferenceSignal) { if (!hasPrimaryFollowupSignal &&
!hasAlternateFollowupSignal &&
!hasImplicitContinuationSignal &&
!hasOrganizationClarificationContinuation &&
!hasIndexReferenceSignal) {
return null; return null;
} }
if (!previousAddressDebug) { if (!previousAddressDebug) {
@ -2811,7 +2827,45 @@ 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 navigationSessionContext = addressNavigationState && typeof addressNavigationState === "object"
? (addressNavigationState.session_context && typeof addressNavigationState.session_context === "object"
? addressNavigationState.session_context
: null)
: null;
const navigationDateScope = navigationSessionContext && typeof navigationSessionContext.date_scope === "object"
? navigationSessionContext.date_scope
: null;
const navigationOrganization = normalizeOrganizationScopeValue(navigationSessionContext?.organization_scope);
const navigationFocusObject = navigationSessionContext && typeof navigationSessionContext.active_focus_object === "object"
? navigationSessionContext.active_focus_object
: null;
const navigationFocusObjectType = toNonEmptyString(navigationFocusObject?.object_type);
const navigationFocusObjectLabel = toNonEmptyString(navigationFocusObject?.label);
const hasSelectedObjectInventorySignalPrimary = /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(String(userMessage ?? ""));
const hasSelectedObjectInventorySignalAlternate = toNonEmptyString(alternateMessage)
? /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(String(alternateMessage ?? ""))
: false;
let inventoryRootFrame = findRecentInventoryRootFrame(items);
if (inventoryRootFrame && navigationOrganization && !toNonEmptyString(inventoryRootFrame.filters?.organization)) {
inventoryRootFrame = {
...inventoryRootFrame,
filters: {
...(inventoryRootFrame.filters ?? {}),
organization: navigationOrganization
}
};
}
if (inventoryRootFrame && navigationDateScope) {
inventoryRootFrame = {
...inventoryRootFrame,
filters: {
...(inventoryRootFrame.filters ?? {}),
as_of_date: toNonEmptyString(inventoryRootFrame.filters?.as_of_date) ?? toNonEmptyString(navigationDateScope.as_of_date) ?? undefined,
period_from: toNonEmptyString(inventoryRootFrame.filters?.period_from) ?? toNonEmptyString(navigationDateScope.period_from) ?? undefined,
period_to: toNonEmptyString(inventoryRootFrame.filters?.period_to) ?? toNonEmptyString(navigationDateScope.period_to) ?? undefined
}
};
}
const currentFrameKind = inventoryRootFrame const currentFrameKind = inventoryRootFrame
? isInventoryDrilldownFrameIntent(sourceIntent) ? isInventoryDrilldownFrameIntent(sourceIntent)
? "inventory_drilldown" ? "inventory_drilldown"
@ -2842,6 +2896,21 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
previousFilters.organization = historicalOrganization; previousFilters.organization = historicalOrganization;
} }
} }
if (!toNonEmptyString(previousFilters.organization) && navigationOrganization) {
previousFilters.organization = navigationOrganization;
}
if (!toNonEmptyString(previousFilters.organization) && organizationClarificationSelection) {
previousFilters.organization = organizationClarificationSelection;
}
if (!toNonEmptyString(previousFilters.as_of_date) && toNonEmptyString(navigationDateScope?.as_of_date)) {
previousFilters.as_of_date = toNonEmptyString(navigationDateScope?.as_of_date);
}
if (!toNonEmptyString(previousFilters.period_from) && toNonEmptyString(navigationDateScope?.period_from)) {
previousFilters.period_from = toNonEmptyString(navigationDateScope?.period_from);
}
if (!toNonEmptyString(previousFilters.period_to) && toNonEmptyString(navigationDateScope?.period_to)) {
previousFilters.period_to = toNonEmptyString(navigationDateScope?.period_to);
}
const displayedEntityType = inferDisplayedEntityTypeFromIntent(sourceIntent); const displayedEntityType = inferDisplayedEntityTypeFromIntent(sourceIntent);
const displayedEntities = extractDisplayedAddressEntityCandidates(toNonEmptyString(previousAddressItem?.text) ?? "", displayedEntityType); const displayedEntities = extractDisplayedAddressEntityCandidates(toNonEmptyString(previousAddressItem?.text) ?? "", displayedEntityType);
const resolvedEntityFromFollowup = resolveDisplayedAddressEntityMention(userMessage, displayedEntities) ?? const resolvedEntityFromFollowup = resolveDisplayedAddressEntityMention(userMessage, displayedEntities) ??
@ -2869,6 +2938,36 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
followupSelectionMode = "carry_referenced_entity"; followupSelectionMode = "carry_referenced_entity";
} }
} }
if (!toNonEmptyString(previousFilters.item) &&
navigationFocusObjectType === "item" &&
navigationFocusObjectLabel &&
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
sourceIntentHint === "inventory_purchase_provenance_for_item" ||
sourceIntentHint === "inventory_purchase_documents_for_item" ||
sourceIntentHint === "inventory_sale_trace_for_item" ||
sourceIntentHint === "inventory_purchase_to_sale_chain" ||
sourceIntentHint === "inventory_aging_by_purchase_date" ||
hasSelectedObjectInventorySignalPrimary ||
hasSelectedObjectInventorySignalAlternate)) {
previousFilters.item = navigationFocusObjectLabel;
if (!previousAnchor) {
previousAnchorType = "item";
previousAnchor = navigationFocusObjectLabel;
}
}
if (organizationClarificationSelection && !previousAnchor) {
previousAnchorType = "organization";
previousAnchor = organizationClarificationSelection;
}
if (inventoryRootFrame && organizationClarificationSelection && !toNonEmptyString(inventoryRootFrame.filters?.organization)) {
inventoryRootFrame = {
...inventoryRootFrame,
filters: {
...(inventoryRootFrame.filters ?? {}),
organization: organizationClarificationSelection
}
};
}
if (!previousIntent && !previousAnchor && Object.keys(previousFilters).length === 0) { if (!previousIntent && !previousAnchor && Object.keys(previousFilters).length === 0) {
return null; return null;
} }
@ -3994,6 +4093,28 @@ export function resolveAssistantOrchestrationDecision(input) {
const llmPreDecomposeMeta = input?.llmPreDecomposeMeta ?? null; const llmPreDecomposeMeta = input?.llmPreDecomposeMeta ?? null;
const useMock = Boolean(input?.useMock); const useMock = Boolean(input?.useMock);
const sessionItems = Array.isArray(input?.sessionItems) ? input.sessionItems : null; const sessionItems = Array.isArray(input?.sessionItems) ? input.sessionItems : null;
const sessionOrganizationScope = input?.sessionOrganizationScope && typeof input.sessionOrganizationScope === "object"
? input.sessionOrganizationScope
: null;
const lastGroundedAddressDebug = findLastGroundedAddressAnswerDebug(sessionItems);
const lastOrganizationClarificationDebug = findLastOrganizationClarificationAddressDebug(sessionItems);
const organizationClarificationCandidates = Array.isArray(lastOrganizationClarificationDebug?.organization_candidates)
? mergeKnownOrganizations([
...lastOrganizationClarificationDebug.organization_candidates,
...((Array.isArray(sessionOrganizationScope?.knownOrganizations)
? sessionOrganizationScope.knownOrganizations
: []))
])
: [];
const organizationClarificationSelectionFromScope = normalizeOrganizationScopeValue(sessionOrganizationScope?.selectedOrganization);
const organizationClarificationSelection = resolveOrganizationSelectionFromMessage(rawUserMessage, organizationClarificationCandidates) ??
resolveOrganizationSelectionFromMessage(repairedRawUserMessage, organizationClarificationCandidates) ??
resolveOrganizationSelectionFromMessage(effectiveAddressUserMessage, organizationClarificationCandidates) ??
resolveOrganizationSelectionFromMessage(repairedEffectiveAddressUserMessage, organizationClarificationCandidates) ??
(organizationClarificationSelectionFromScope &&
organizationClarificationCandidates.some((candidate) => normalizeOrganizationScopeValue(candidate) === organizationClarificationSelectionFromScope)
? organizationClarificationSelectionFromScope
: null);
const dataScopeMetaQuery = hasAssistantDataScopeMetaQuestionSignal(rawUserMessage) || const dataScopeMetaQuery = hasAssistantDataScopeMetaQuestionSignal(rawUserMessage) ||
hasAssistantDataScopeMetaQuestionSignal(repairedRawUserMessage) || hasAssistantDataScopeMetaQuestionSignal(repairedRawUserMessage) ||
hasAssistantDataScopeMetaQuestionSignal(effectiveAddressUserMessage) || hasAssistantDataScopeMetaQuestionSignal(effectiveAddressUserMessage) ||
@ -4081,6 +4202,12 @@ export function resolveAssistantOrchestrationDecision(input) {
hasShortInventoryObjectFollowupSignal(repairedRawUserMessage) || hasShortInventoryObjectFollowupSignal(repairedRawUserMessage) ||
hasShortInventoryObjectFollowupSignal(effectiveAddressUserMessage) || hasShortInventoryObjectFollowupSignal(effectiveAddressUserMessage) ||
hasShortInventoryObjectFollowupSignal(repairedEffectiveAddressUserMessage))); hasShortInventoryObjectFollowupSignal(repairedEffectiveAddressUserMessage)));
const organizationClarificationContinuationDetected = Boolean(followupContext &&
lastOrganizationClarificationDebug &&
organizationClarificationSelection &&
!dataScopeMetaQuery &&
!capabilityMetaQuery &&
!dataRetrievalSignal);
const effectiveAddressFollowupSignal = explicitAddressFollowupSignal && !dangerOrCoercionSignal; const effectiveAddressFollowupSignal = explicitAddressFollowupSignal && !dangerOrCoercionSignal;
const deterministicNonDomainGuard = Boolean(!dataScopeMetaQuery && const deterministicNonDomainGuard = Boolean(!dataScopeMetaQuery &&
!capabilityMetaQuery && !capabilityMetaQuery &&
@ -4091,7 +4218,16 @@ export function resolveAssistantOrchestrationDecision(input) {
const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate && const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate &&
deterministicNonDomainGuard && deterministicNonDomainGuard &&
(llmFirstUnsupportedCandidate || llmContractMode === null) && (llmFirstUnsupportedCandidate || llmContractMode === null) &&
!protectedInventoryShortFollowup); !protectedInventoryShortFollowup &&
!organizationClarificationContinuationDetected);
const contextualHistoricalCapabilityFollowupDetected = Boolean(capabilityMetaQuery &&
!dataScopeMetaQuery &&
!dataRetrievalSignal &&
(hasHistoricalCapabilityFollowupSignal(rawUserMessage) ||
hasHistoricalCapabilityFollowupSignal(repairedRawUserMessage) ||
hasHistoricalCapabilityFollowupSignal(effectiveAddressUserMessage) ||
hasHistoricalCapabilityFollowupSignal(repairedEffectiveAddressUserMessage)) &&
isGroundedInventoryContextDebug(lastGroundedAddressDebug));
const hardMetaMode = dataScopeMetaQuery const hardMetaMode = dataScopeMetaQuery
? "data_scope" ? "data_scope"
: capabilityMetaQuery && !dataRetrievalSignal : capabilityMetaQuery && !dataRetrievalSignal
@ -4126,6 +4262,34 @@ export function resolveAssistantOrchestrationDecision(input) {
}; };
} }
if (hardMetaMode === "capability") { if (hardMetaMode === "capability") {
if (contextualHistoricalCapabilityFollowupDetected) {
return {
runAddressLane: false,
toolGateDecision: "skip_address_lane",
toolGateReason: "inventory_history_capability_followup_detected",
livingMode: "chat",
livingReason: "inventory_history_capability_followup_detected",
orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1",
hard_meta_mode: "capability",
address_mode: resolvedModeDetection.mode,
address_mode_confidence: resolvedModeDetection.confidence,
address_intent: resolvedIntentResolution.intent,
address_intent_confidence: resolvedIntentResolution.confidence,
strong_data_signal_detected: strongDataSignal,
data_retrieval_signal_detected: dataRetrievalSignal,
followup_context_detected: Boolean(followupContext || lastGroundedAddressDebug),
unsupported_address_intent_fallback_to_deep: false,
final_decision: {
run_address_lane: false,
tool_gate_decision: "skip_address_lane",
tool_gate_reason: "inventory_history_capability_followup_detected",
living_mode: "chat",
living_reason: "inventory_history_capability_followup_detected"
}
}
};
}
return { return {
runAddressLane: false, runAddressLane: false,
toolGateDecision: "skip_address_lane", toolGateDecision: "skip_address_lane",
@ -4181,6 +4345,10 @@ export function resolveAssistantOrchestrationDecision(input) {
} }
}; };
} }
const metaAnswerFollowupSignal = hasMetaAnswerFollowupSignal(rawUserMessage) ||
hasMetaAnswerFollowupSignal(repairedRawUserMessage) ||
hasMetaAnswerFollowupSignal(effectiveAddressUserMessage) ||
hasMetaAnswerFollowupSignal(repairedEffectiveAddressUserMessage);
const baseToolGate = resolveAddressToolGateDecision(effectiveAddressUserMessage, followupContext, llmPreDecomposeMeta, rawUserMessage); const baseToolGate = resolveAddressToolGateDecision(effectiveAddressUserMessage, followupContext, llmPreDecomposeMeta, rawUserMessage);
const preserveAddressLaneSignal = Boolean((llmPreDecomposeMeta?.llmCanonicalCandidateDetected && const preserveAddressLaneSignal = Boolean((llmPreDecomposeMeta?.llmCanonicalCandidateDetected &&
llmPreDecomposeMeta?.applied && llmPreDecomposeMeta?.applied &&
@ -4275,6 +4443,19 @@ export function resolveAssistantOrchestrationDecision(input) {
repairedEffectiveAddressUserMessage, repairedEffectiveAddressUserMessage,
sessionItems sessionItems
})); }));
const hasPriorAddressAnswerContext = Boolean(lastGroundedAddressDebug || toNonEmptyString(followupContext?.previous_intent));
const metaFollowupOverGroundedAnswer = Boolean(followupContext &&
hasPriorAddressAnswerContext &&
metaAnswerFollowupSignal &&
!dataScopeMetaQuery &&
!capabilityMetaQuery &&
!aggregateBusinessAnalyticsSignal &&
!dataRetrievalSignal &&
!strongDataSignal &&
resolvedModeDetection.mode !== "address_query" &&
resolvedIntentResolution.intent === "unknown" &&
(!llmContractIntent || llmContractIntent === "unknown") &&
llmContractMode !== "address_query");
let runAddressLane = Boolean(baseToolGate?.runAddressLane); let runAddressLane = Boolean(baseToolGate?.runAddressLane);
let toolGateDecision = String(baseToolGate?.decision ?? "skip_address_lane"); let toolGateDecision = String(baseToolGate?.decision ?? "skip_address_lane");
let toolGateReason = String(baseToolGate?.reason ?? "no_address_signal_after_l0"); let toolGateReason = String(baseToolGate?.reason ?? "no_address_signal_after_l0");
@ -4300,6 +4481,11 @@ export function resolveAssistantOrchestrationDecision(input) {
toolGateDecision = "skip_address_lane"; toolGateDecision = "skip_address_lane";
toolGateReason = "deep_session_continuation_fallback_to_deep"; toolGateReason = "deep_session_continuation_fallback_to_deep";
} }
if (metaFollowupOverGroundedAnswer) {
runAddressLane = false;
toolGateDecision = "skip_address_lane";
toolGateReason = "meta_followup_over_grounded_answer";
}
let livingDecision = resolveLivingAssistantModeDecision({ let livingDecision = resolveLivingAssistantModeDecision({
userMessage: rawUserMessage, userMessage: rawUserMessage,
addressLaneTriggered: runAddressLane, addressLaneTriggered: runAddressLane,
@ -4333,6 +4519,12 @@ export function resolveAssistantOrchestrationDecision(input) {
reason: "deep_session_continuation_fallback_to_deep" reason: "deep_session_continuation_fallback_to_deep"
}; };
} }
if (metaFollowupOverGroundedAnswer) {
livingDecision = {
mode: "chat",
reason: "meta_followup_over_grounded_answer"
};
}
return { return {
runAddressLane, runAddressLane,
toolGateDecision, toolGateDecision,
@ -4434,6 +4626,105 @@ function findLastAssistantLivingChatDebug(items) {
} }
return null; return null;
} }
function findLastGroundedAddressAnswerDebug(items) {
if (!Array.isArray(items)) {
return null;
}
for (let index = items.length - 1; index >= 0; index -= 1) {
const item = items[index];
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
continue;
}
const debug = item.debug;
if (debug.execution_lane !== "address_query") {
continue;
}
const groundingStatus = toNonEmptyString(debug.answer_grounding_check?.status);
if (groundingStatus === "grounded") {
return debug;
}
}
return null;
}
function findLastOrganizationClarificationAddressDebug(items) {
if (!Array.isArray(items)) {
return null;
}
for (let index = items.length - 1; index >= 0; index -= 1) {
const item = items[index];
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
continue;
}
const debug = item.debug;
if (debug.execution_lane !== "address_query" && debug.detected_mode !== "address_query") {
continue;
}
const limitedCategory = toNonEmptyString(debug.limited_reason_category);
const candidates = Array.isArray(debug.organization_candidates)
? mergeKnownOrganizations(debug.organization_candidates)
: [];
if (limitedCategory === "missing_anchor" && candidates.length > 0) {
return debug;
}
}
return null;
}
function hasMetaAnswerFollowupSignal(userMessage) {
const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase());
const repairedText = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
const samples = [rawText, repairedText]
.filter((item) => item.length > 0)
.map((item) => item.replace(/ё/g, "е"));
if (samples.length === 0) {
return false;
}
const hasReflectionCue = samples.some((sample) => sample.includes("дума") ||
sample.includes("скаж") ||
sample.includes("мнение") ||
sample.includes("как тебе") ||
sample.includes("норм") ||
sample.includes("стран") ||
sample.includes("логич") ||
sample.includes("смуща") ||
sample.includes("выгляд"));
const hasTopicPointerCue = samples.some((sample) => sample.includes("на эту тему") ||
sample.includes("по этому поводу") ||
sample.includes("об этом") ||
(sample.includes("это") && hasReferentialPointer(sample)));
if (!(hasReflectionCue && hasTopicPointerCue)) {
return false;
}
return !samples.some((sample) => hasAssistantDataScopeMetaQuestionSignal(sample) ||
shouldHandleAsAssistantCapabilityMetaQuery(sample) ||
hasDataRetrievalRequestSignal(sample) ||
hasStrongDataIntentSignal(sample));
}
function hasHistoricalCapabilityFollowupSignal(text) {
const repaired = repairAddressMojibake(String(text ?? ""));
const normalized = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е");
if (!normalized) {
return false;
}
const hasHistoryCue = /(?:историческ|история|архив|прошл(?:ый|ые|ую|ых)?|раньше|ретро|старые\s+данные)/iu.test(normalized);
if (!hasHistoryCue) {
return false;
}
return /(?:мож(?:ешь|ете|но)|уме(?:ешь|ете)|показ|вывед|дай|раскрой)/iu.test(normalized);
}
function isGroundedInventoryContextDebug(debug) {
if (!debug || typeof debug !== "object") {
return false;
}
const detectedIntent = toNonEmptyString(debug.detected_intent);
const capabilityId = toNonEmptyString(debug.capability_id);
const rootFrameContext = debug.address_root_frame_context && typeof debug.address_root_frame_context === "object"
? debug.address_root_frame_context
: null;
const rootIntent = toNonEmptyString(rootFrameContext?.root_intent);
return detectedIntent === "inventory_on_hand_as_of_date" ||
capabilityId === "confirmed_inventory_on_hand_as_of_date" ||
rootIntent === "inventory_on_hand_as_of_date";
}
function hasOrganizationFactFollowupSignal(userMessage, items) { function hasOrganizationFactFollowupSignal(userMessage, items) {
const repaired = repairAddressMojibake(String(userMessage ?? "")); const repaired = repairAddressMojibake(String(userMessage ?? ""));
const normalized = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е"); const normalized = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е");
@ -4722,7 +5013,6 @@ function normalizeOrganizationScopeValue(value) {
return null; return null;
} }
const unwrapped = normalized const unwrapped = normalized
.replace(/^\\+|\\+$/g, "")
.replace(/^"+|"+$/g, "") .replace(/^"+|"+$/g, "")
.replace(/^'+|'+$/g, "") .replace(/^'+|'+$/g, "")
.trim(); .trim();
@ -4862,8 +5152,34 @@ function mergeKnownOrganizations(values) {
} }
return Array.from(dedup.values()).slice(0, 20); return Array.from(dedup.values()).slice(0, 20);
} }
function extractKnownOrganizationsFromHistory(items) { function extractKnownOrganizationsFromNavigationState(addressNavigationState) {
if (!addressNavigationState || typeof addressNavigationState !== "object") {
return [];
}
const collected = []; const collected = [];
const sessionContext = addressNavigationState.session_context && typeof addressNavigationState.session_context === "object"
? addressNavigationState.session_context
: null;
const directOrganization = normalizeOrganizationScopeValue(sessionContext?.organization_scope);
if (directOrganization) {
collected.push(directOrganization);
}
const resultSets = Array.isArray(addressNavigationState.result_sets) ? addressNavigationState.result_sets : [];
for (const resultSet of resultSets) {
const filters = resultSet?.filters && typeof resultSet.filters === "object" ? resultSet.filters : null;
const scopedOrganization = normalizeOrganizationScopeValue(filters?.organization);
if (scopedOrganization) {
collected.push(scopedOrganization);
}
}
return mergeKnownOrganizations(collected);
}
function extractKnownOrganizationsFromHistory(items, addressNavigationState = null) {
const collected = [];
const navigationOrganizations = extractKnownOrganizationsFromNavigationState(addressNavigationState);
if (navigationOrganizations.length > 0) {
collected.push(...navigationOrganizations);
}
for (let index = (Array.isArray(items) ? items.length : 0) - 1; index >= 0; index -= 1) { for (let index = (Array.isArray(items) ? items.length : 0) - 1; index >= 0; index -= 1) {
const item = items[index]; const item = items[index];
if (!item || item.role !== "assistant") { if (!item || item.role !== "assistant") {
@ -4877,8 +5193,17 @@ function extractKnownOrganizationsFromHistory(items) {
const knownFromDebug = Array.isArray(debug.assistant_known_organizations) const knownFromDebug = Array.isArray(debug.assistant_known_organizations)
? debug.assistant_known_organizations ? debug.assistant_known_organizations
: []; : [];
if (directFromProbe.length > 0 || knownFromDebug.length > 0) { const directFromCandidates = Array.isArray(debug.organization_candidates)
collected.push(...directFromProbe, ...knownFromDebug); ? debug.organization_candidates
: [];
const directFromResolved = [
normalizeOrganizationScopeValue(debug.assistant_active_organization),
normalizeOrganizationScopeValue(debug.living_chat_selected_organization),
normalizeOrganizationScopeValue(debug.extracted_filters?.organization),
normalizeOrganizationScopeValue(debug.address_root_frame_context?.organization)
].filter(Boolean);
if (directFromProbe.length > 0 || knownFromDebug.length > 0 || directFromCandidates.length > 0 || directFromResolved.length > 0) {
collected.push(...directFromProbe, ...knownFromDebug, ...directFromCandidates, ...directFromResolved);
} }
} }
const parsedFromText = parseOrganizationsFromDataScopeAssistantText(item.text); const parsedFromText = parseOrganizationsFromDataScopeAssistantText(item.text);
@ -4891,7 +5216,16 @@ function extractKnownOrganizationsFromHistory(items) {
} }
return mergeKnownOrganizations(collected); return mergeKnownOrganizations(collected);
} }
function findLastAssistantActiveOrganization(items) { function findLastAssistantActiveOrganization(items, addressNavigationState = null) {
const sessionContext = addressNavigationState && typeof addressNavigationState === "object"
? (addressNavigationState.session_context && typeof addressNavigationState.session_context === "object"
? addressNavigationState.session_context
: null)
: null;
const navigationOrganization = normalizeOrganizationScopeValue(sessionContext?.organization_scope);
if (navigationOrganization) {
return navigationOrganization;
}
for (let index = (Array.isArray(items) ? items.length : 0) - 1; index >= 0; index -= 1) { for (let index = (Array.isArray(items) ? items.length : 0) - 1; index >= 0; index -= 1) {
const item = items[index]; const item = items[index];
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") { if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
@ -4937,10 +5271,11 @@ function resolveOrganizationSelectionFromMessage(userMessage, knownOrganizations
} }
return best.organization; return best.organization;
} }
function resolveSessionOrganizationScopeContext(userMessage, items) { function resolveSessionOrganizationScopeContext(userMessage, items, addressNavigationState = null) {
return (0, assistantOrganizationScopeRuntimeAdapter_1.resolveSessionOrganizationScopeContextRuntime)({ return (0, assistantOrganizationScopeRuntimeAdapter_1.resolveSessionOrganizationScopeContextRuntime)({
userMessage, userMessage,
items, items,
addressNavigationState,
extractKnownOrganizationsFromHistory, extractKnownOrganizationsFromHistory,
resolveOrganizationSelectionFromMessage, resolveOrganizationSelectionFromMessage,
findLastAssistantActiveOrganization, findLastAssistantActiveOrganization,
@ -4955,8 +5290,8 @@ function mergeFollowupContextWithOrganizationScope(followupContext, organization
toNonEmptyString toNonEmptyString
}); });
} }
export function resolveSessionOrganizationScopeContextForTests(userMessage, items) { export function resolveSessionOrganizationScopeContextForTests(userMessage, items, addressNavigationState = null) {
return resolveSessionOrganizationScopeContext(userMessage, items); return resolveSessionOrganizationScopeContext(userMessage, items, addressNavigationState);
} }
function normalizeGuidValue(value) { function normalizeGuidValue(value) {
const source = normalizeScopeLabel(value); const source = normalizeScopeLabel(value);

View File

@ -19,6 +19,7 @@ export function buildAssistantTurnAttemptAddressRuntimeInput<PayloadType = unkno
sessionId: input.userTurn.sessionId, sessionId: input.userTurn.sessionId,
userMessage: input.userTurn.userMessage, userMessage: input.userTurn.userMessage,
sessionItems: input.userTurn.session.items, sessionItems: input.userTurn.session.items,
sessionAddressNavigationState: input.userTurn.session.address_navigation_state ?? null,
runtimeAnalysisContext: input.userTurn.runtimeAnalysisContext, runtimeAnalysisContext: input.userTurn.runtimeAnalysisContext,
sessionOrganizationScope: input.sessionOrganizationScope sessionOrganizationScope: input.sessionOrganizationScope
}; };

View File

@ -16,6 +16,7 @@ export interface RunAssistantTurnAttemptRuntimeAddressInput<PayloadType = unknow
sessionId: string; sessionId: string;
userMessage: string; userMessage: string;
sessionItems: unknown[]; sessionItems: unknown[];
sessionAddressNavigationState?: unknown;
runtimeAnalysisContext: { as_of_date: string | null }; runtimeAnalysisContext: { as_of_date: string | null };
sessionOrganizationScope: AssistantSessionOrganizationScopeContext; sessionOrganizationScope: AssistantSessionOrganizationScopeContext;
} }
@ -35,7 +36,8 @@ export interface RunAssistantTurnAttemptRuntimeInput<ResponseType = unknown, Pay
runUserTurnBootstrapRuntime: (payload: PayloadType) => RunAssistantUserTurnBootstrapRuntimeOutput; runUserTurnBootstrapRuntime: (payload: PayloadType) => RunAssistantUserTurnBootstrapRuntimeOutput;
resolveSessionOrganizationScopeContext: ( resolveSessionOrganizationScopeContext: (
userMessage: string, userMessage: string,
sessionItems: unknown[] sessionItems: unknown[],
sessionAddressNavigationState?: unknown
) => AssistantSessionOrganizationScopeContext; ) => AssistantSessionOrganizationScopeContext;
runAddressAttemptRuntime: ( runAddressAttemptRuntime: (
input: RunAssistantTurnAttemptRuntimeAddressInput<PayloadType> input: RunAssistantTurnAttemptRuntimeAddressInput<PayloadType>
@ -59,7 +61,8 @@ export async function runAssistantTurnAttemptRuntime<ResponseType = unknown, Pay
const userTurn = input.runUserTurnBootstrapRuntime(input.payload); const userTurn = input.runUserTurnBootstrapRuntime(input.payload);
const sessionOrganizationScope = input.resolveSessionOrganizationScopeContext( const sessionOrganizationScope = input.resolveSessionOrganizationScopeContext(
userTurn.userMessage, userTurn.userMessage,
userTurn.session.items userTurn.session.items,
userTurn.session.address_navigation_state ?? null
); );
const addressRuntime = await input.runAddressAttemptRuntime( const addressRuntime = await input.runAddressAttemptRuntime(
buildAssistantTurnAttemptAddressRuntimeInput({ buildAssistantTurnAttemptAddressRuntimeInput({

View File

@ -136,6 +136,7 @@ export function buildAssistantAddressAttemptRuntimeInput<ResponseType = unknown>
sessionId: runtimeInput.sessionId, sessionId: runtimeInput.sessionId,
userMessage: runtimeInput.userMessage, userMessage: runtimeInput.userMessage,
sessionItems: runtimeInput.sessionItems, sessionItems: runtimeInput.sessionItems,
sessionAddressNavigationState: runtimeInput.sessionAddressNavigationState,
payload: runtimeInput.payload, payload: runtimeInput.payload,
sessionScope: { sessionScope: {
knownOrganizations: runtimeInput.sessionOrganizationScope.knownOrganizations, knownOrganizations: runtimeInput.sessionOrganizationScope.knownOrganizations,

View File

@ -8,11 +8,21 @@ export type AddressResultSetType =
| "document_list" | "document_list"
| "bank_operations_list" | "bank_operations_list"
| "open_items_list" | "open_items_list"
| "inventory_snapshot"
| "inventory_trace"
| "balance_snapshot" | "balance_snapshot"
| "profile_summary" | "profile_summary"
| "unknown"; | "unknown";
export type AddressFocusObjectType = "counterparty" | "contract" | "document_ref" | "account" | "unknown"; export type AddressFocusObjectType =
| "counterparty"
| "contract"
| "document_ref"
| "account"
| "item"
| "organization"
| "warehouse"
| "unknown";
export type AddressNavigationAction = "open" | "drilldown" | "refine" | "back" | "reset"; export type AddressNavigationAction = "open" | "drilldown" | "refine" | "back" | "reset";

View File

@ -260,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;
organization_candidates?: string[];
semantic_frame?: AddressSemanticFrame | null; semantic_frame?: AddressSemanticFrame | null;
response_type: AddressResponseType; response_type: AddressResponseType;
requested_result_mode?: AddressResultMode; requested_result_mode?: AddressResultMode;

View File

@ -0,0 +1,146 @@
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("inventory organization scope grounding", () => {
it("asks for organization clarification when multiple known companies exist", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("покажи остатки по складу", {
knownOrganizations: ["ООО Альтернатива Плюс", "ООО Лайсвуд"]
});
expect(result?.handled).toBe(true);
expect(result?.response_type).toBe("LIMITED_WITH_REASON");
expect(result?.debug.limited_reason_category).toBe("missing_anchor");
expect(result?.debug.organization_candidates).toEqual(["ООО Альтернатива Плюс", "ООО Лайсвуд"]);
expect(String(result?.reply_text ?? "")).toContain("ООО Альтернатива Плюс");
expect(String(result?.reply_text ?? "")).toContain("ООО Лайсвуд");
expect(executeAddressMcpQueryMock).not.toHaveBeenCalled();
});
it("auto-selects the only known organization for inventory root queries", 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("покажи остатки по складу", {
knownOrganizations: ["ООО Альтернатива Плюс"]
});
expect(result?.handled).toBe(true);
expect(result?.response_type).toBe("FACTUAL_LIST");
expect(result?.debug.extracted_filters?.organization).toBe("ООО Альтернатива Плюс");
expect(result?.debug.reasons).toContain("organization_auto_selected_from_single_scope_candidate");
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
});
it("grounds organization from observed rows when the result belongs to a single company", async () => {
executeAddressMcpQueryMock.mockResolvedValueOnce({
fetched_rows: 1,
matched_rows: 1,
raw_rows: [
{
Period: "2020-05-31T23:59:59Z",
Registrator: "Остатки товаров на складах",
AccountDt: "41.01",
AccountKt: "00.00",
Amount: 13490,
Quantity: 1,
SubcontoDt1: "Кресло орион",
Warehouse: "Основной склад",
Organization: "ООО \\Альтернатива Плюс\\"
}
],
rows: [],
error: null
});
const service = new AddressQueryService();
const result = await service.tryHandle("покажи остатки по складу");
expect(result?.handled).toBe(true);
expect(result?.response_type).toBe("FACTUAL_LIST");
expect(result?.debug.extracted_filters?.organization).toBe("ООО \\Альтернатива Плюс\\");
expect(result?.debug.reasons).toContain("organization_grounded_from_observed_rows");
});
it("asks for organization clarification when observed rows contain multiple companies", async () => {
executeAddressMcpQueryMock.mockResolvedValueOnce({
fetched_rows: 2,
matched_rows: 2,
raw_rows: [
{
Period: "2020-05-31T23:59:59Z",
Registrator: "Остатки товаров на складах",
AccountDt: "41.01",
AccountKt: "00.00",
Amount: 6490,
Quantity: 1,
SubcontoDt1: "Пуф арий",
Warehouse: "Основной склад",
Organization: "ООО Альтернатива Плюс"
},
{
Period: "2020-05-31T23: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("покажи остатки по складу");
expect(result?.handled).toBe(true);
expect(result?.response_type).toBe("LIMITED_WITH_REASON");
expect(result?.debug.limited_reason_category).toBe("missing_anchor");
expect(result?.debug.organization_candidates).toEqual(["ООО Альтернатива Плюс", "ООО Лайсвуд"]);
expect(String(result?.reply_text ?? "")).toContain("ООО Альтернатива Плюс");
expect(String(result?.reply_text ?? "")).toContain("ООО Лайсвуд");
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
});
});

View File

@ -292,8 +292,8 @@ describe("inventory selected-object follow-up", () => {
expect(result?.debug.selected_recipe).toBe("address_inventory_purchase_provenance_for_item_v1"); expect(result?.debug.selected_recipe).toBe("address_inventory_purchase_provenance_for_item_v1");
expect(result?.debug.extracted_filters?.item).toBe("Конструкция трансформер рабочей станции 1300*900*2000"); expect(result?.debug.extracted_filters?.item).toBe("Конструкция трансформер рабочей станции 1300*900*2000");
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-06-30"); expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-06-30");
expect(result?.debug.extracted_filters?.period_from).toBe("2020-06-01"); expect(result?.debug.extracted_filters?.period_from).toBeUndefined();
expect(result?.debug.extracted_filters?.period_to).toBe("2020-06-30"); expect(result?.debug.extracted_filters?.period_to).toBeUndefined();
expect(result?.debug.capability_id).toBe("inventory_inventory_purchase_provenance_for_item"); expect(result?.debug.capability_id).toBe("inventory_inventory_purchase_provenance_for_item");
expect(result?.debug.capability_route_mode).toBe("exact"); expect(result?.debug.capability_route_mode).toBe("exact");
expect(String(result?.reply_text ?? "")).toContain("ООО \\Гамма-мебель\\"); expect(String(result?.reply_text ?? "")).toContain("ООО \\Гамма-мебель\\");
@ -491,6 +491,107 @@ describe("inventory selected-object follow-up", () => {
expect(String(result?.reply_text ?? "")).toContain("Документы выбытия"); expect(String(result?.reply_text ?? "")).toContain("Документы выбытия");
}); });
it("routes selected-object wording 'куда мы продали эту позицию' into sale trace instead of replaying stock slice", async () => {
executeAddressMcpQueryMock.mockResolvedValueOnce({
fetched_rows: 1,
matched_rows: 1,
raw_rows: [
{
Period: "2020-06-18T00:00:00Z",
Registrator: "Реализация товаров и услуг 00000000131 от 18.06.2020 0:00:00",
AccountDt: "90.02",
AccountKt: "41.01",
Amount: 6490,
SubcontoKt1: "Пуф арий",
SubcontoKt3: "Основной склад",
SubcontoDt1: "ООО \\Ромашка\\",
SubcontoDt2: "Договор реализации № 14 от 17.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-05-31",
period_from: "2020-05-01",
period_to: "2020-05-31"
},
previous_anchor_type: "unknown",
previous_anchor_value: null
}
});
expect(result?.handled).toBe(true);
expect(result?.response_type).toBe("FACTUAL_LIST");
expect(result?.debug.detected_intent).toBe("inventory_sale_trace_for_item");
expect(result?.debug.selected_recipe).toBe("address_inventory_sale_trace_for_item_v1");
expect(result?.debug.extracted_filters?.item).toBe("Пуф арий");
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-05-31");
expect(result?.debug.reasons).toContain("inventory_selected_object_sale_trace_signal_detected");
expect(String(result?.reply_text ?? "").split("\n")[0]).toContain("ООО \\Ромашка\\");
expect(String(result?.reply_text ?? "")).toContain("Документы выбытия");
});
it("detaches snapshot date from execution query during sale-trace history recovery", async () => {
executeAddressMcpQueryMock.mockResolvedValueOnce({
fetched_rows: 1,
matched_rows: 1,
raw_rows: [
{
Period: "2025-10-12T00:00:00Z",
Registrator: "Реализация товаров и услуг 00000000421 от 12.10.2025 0:00:00",
AccountDt: "90.02",
AccountKt: "41.01",
Amount: 165.83,
SubcontoKt1: "Кромка с клеем 33 дуб ниагара 137 м",
SubcontoKt3: "Основной склад",
SubcontoDt1: "ООО \\Покупатель\\",
SubcontoDt2: "Договор реализации № 55 от 01.10.2025",
Organization: "ООО \\Альтернатива Плюс\\"
}
],
rows: [],
error: null
});
const service = new AddressQueryService();
const result = await service.tryHandle(
'По выбранному объекту "Кромка с клеем 33 дуб ниагара 137 м": куда в итоге продали эту позицию?',
{
followupContext: {
previous_intent: "inventory_on_hand_as_of_date",
previous_filters: {
as_of_date: "2019-03-31",
period_from: "2019-03-01",
period_to: "2019-03-31",
organization: "ООО \\Альтернатива Плюс\\"
},
previous_anchor_type: "counterparty",
previous_anchor_value: "ООО \\Альтернатива Плюс\\"
}
}
);
expect(result?.handled).toBe(true);
expect(result?.response_type).toBe("FACTUAL_LIST");
expect(result?.debug.detected_intent).toBe("inventory_sale_trace_for_item");
expect(result?.debug.reasons).toContain("lifecycle_execution_detached_from_snapshot_date");
expect(result?.debug.reasons).toContain("as_of_date_cleared_for_history_recovery");
expect(result?.debug.limitations).toContain("lifecycle_execution_detached_from_snapshot_date");
expect(result?.debug.limitations).toContain("as_of_date_cleared_for_history_recovery");
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
expect(query).not.toContain("2019-03-31");
expect(query).not.toContain("2019-03-01");
expect(String(result?.reply_text ?? "")).toContain("ООО \\Покупатель\\");
});
it("matches sale-trace item anchors from subconto fields when the item is not materialized explicitly", async () => { it("matches sale-trace item anchors from subconto fields when the item is not materialized explicitly", async () => {
executeAddressMcpQueryMock.mockResolvedValueOnce({ executeAddressMcpQueryMock.mockResolvedValueOnce({
fetched_rows: 1, fetched_rows: 1,
@ -522,16 +623,8 @@ describe("inventory selected-object follow-up", () => {
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 () => { it("detaches snapshot date from execution query for selected-object provenance after dated stock slice", async () => {
executeAddressMcpQueryMock executeAddressMcpQueryMock.mockResolvedValueOnce({
.mockResolvedValueOnce({
fetched_rows: 0,
matched_rows: 0,
raw_rows: [],
rows: [],
error: null
})
.mockResolvedValueOnce({
fetched_rows: 1, fetched_rows: 1,
matched_rows: 1, matched_rows: 1,
raw_rows: [ raw_rows: [
@ -572,13 +665,18 @@ describe("inventory selected-object follow-up", () => {
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item"); expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
expect(result?.debug.extracted_filters?.item).toBe("Кресло орион"); expect(result?.debug.extracted_filters?.item).toBe("Кресло орион");
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-03-31"); 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_from).toBeUndefined();
expect(result?.debug.extracted_filters?.period_to).toBe("2020-03-31"); expect(result?.debug.extracted_filters?.period_to).toBeUndefined();
expect(result?.debug.reasons).toContain("lifecycle_execution_detached_from_snapshot_date");
expect(result?.debug.reasons).toContain("as_of_date_cleared_for_history_recovery"); 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("lifecycle_execution_detached_from_snapshot_date");
expect(result?.debug.limitations).toContain("as_of_date_cleared_for_history_recovery"); 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(String(result?.reply_text ?? "")).toContain("ООО \\Гамма-мебель\\");
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(2); expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
expect(query).not.toContain("2020-03-31");
expect(query).not.toContain("2020-03-01");
expect(query).toContain("ПРЕДСТАВЛЕНИЕ(Движения.Организация) КАК Организация");
expect(query).toContain('ПРЕДСТАВЛЕНИЕ(Движения.Организация) = "ООО \\Альтернатива Плюс\\"');
}); });
}); });

View File

@ -144,4 +144,41 @@ describe("address navigation state", () => {
expect(evolved.session_context.active_focus_object?.label).toBe("Диван трехместный"); expect(evolved.session_context.active_focus_object?.label).toBe("Диван трехместный");
expect(evolved.session_context.active_focus_object?.provenance_result_set_id).toBe("rs-msg-a3"); expect(evolved.session_context.active_focus_object?.provenance_result_set_id).toBe("rs-msg-a3");
}); });
it("derives single organization scope from inventory answer text when filters omit organization", () => {
const base = createEmptyAddressNavigationState("asst-5", "2026-04-12T10:00:00.000Z");
const assistantItem = {
message_id: "msg-a4",
session_id: "asst-5",
role: "assistant",
text: [
"На 31.05.2020 на складе подтверждено 1 позиция.",
"1. Пуф арий | склад: Основной склад | количество: 1,000 | стоимость: 6.490,00 ₽ | организация: ООО Альтернатива Плюс | дата строки: 2020-05-31T23:59:59Z"
].join("\n"),
reply_type: "factual",
created_at: "2026-04-12T10:04:00.000Z",
trace_id: "address-790",
debug: {
detected_mode: "address_query",
detected_intent: "inventory_on_hand_as_of_date",
selected_recipe: "address_inventory_on_hand_as_of_date_v1",
extracted_filters: {
as_of_date: "2020-05-31"
},
anchor_type: "unknown",
anchor_value_resolved: null,
anchor_value_raw: null,
dialog_continuation_contract_v2: {
decision: "new_topic"
}
}
} as any;
const evolved = evolveAddressNavigationStateWithAssistantItem(base, assistantItem, 4);
expect(evolved.result_sets[0]?.type).toBe("inventory_snapshot");
expect(evolved.result_sets[0]?.filters.organization).toBe("ООО Альтернатива Плюс");
expect(evolved.result_sets[0]?.entity_refs[0]?.entity_type).toBe("item");
expect(evolved.result_sets[0]?.entity_refs[0]?.value).toBe("Пуф арий");
expect(evolved.session_context.organization_scope).toBe("ООО Альтернатива Плюс");
expect(evolved.session_context.active_focus_object).toBeNull();
});
}); });

View File

@ -1857,5 +1857,94 @@ describe("assistant address follow-up carryover", () => {
expect(scopedCall).toBeTruthy(); expect(scopedCall).toBeTruthy();
expect(scopedCall?.options?.followupContext?.previous_filters?.organization).toBe("Alternative Plus LLC"); expect(scopedCall?.options?.followupContext?.previous_filters?.organization).toBe("Alternative Plus LLC");
}); });
it("continues the original inventory query after organization clarification with a bare company reply", async () => {
const calls: Array<{ message: string; options?: any }> = [];
const firstMessage = "покажи остатки по складу";
const secondMessage = "Альтернатива";
const addressQueryService = {
tryHandle: vi.fn(async (message: string, options?: any) => {
calls.push({ message, options });
if (message === firstMessage) {
return buildAddressLimitedLaneResult("missing_anchor", {
reply_text: [
"Нужно уточнить организацию, чтобы не смешивать компании в одном ответе.",
"Сейчас в доступном контуре вижу такие организации:",
"- ООО Альтернатива Плюс",
"- ООО Лайсвуд"
].join("\n"),
debug: {
...buildAddressLimitedLaneResult("missing_anchor").debug,
detected_intent: "inventory_on_hand_as_of_date",
extracted_filters: {
as_of_date: "2026-04-15"
},
selected_recipe: null,
organization_candidates: ["ООО Альтернатива Плюс", "ООО Лайсвуд"],
reasons: ["organization_clarification_required", "multiple_known_organizations_detected"]
}
});
}
if (message === secondMessage && options?.followupContext && options?.activeOrganization === "ООО Альтернатива Плюс") {
return buildAddressLaneResult({
reply_text: "На 15.04.2026 по ООО Альтернатива Плюс подтвержден складской остаток.",
debug: {
...buildAddressLaneResult().debug,
detected_intent: "inventory_on_hand_as_of_date",
extracted_filters: {
as_of_date: "2026-04-15",
organization: "ООО Альтернатива Плюс"
},
reasons: ["address_followup_context_applied", "organization_grounded_from_scope_candidates"]
}
});
}
return null;
})
} as any;
const normalizerService = {
normalize: vi.fn(async () => ({
assistant_reply: "normalizer_fallback_should_not_be_used",
reply_type: "partial_coverage",
debug: {}
}))
} 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-address-org-clarification-${Date.now()}`;
const first = await service.handleMessage({
session_id: sessionId,
user_message: firstMessage,
useMock: true
} as any);
expect(first.ok).toBe(true);
expect(first.reply_type).toBe("partial_coverage");
const second = await service.handleMessage({
session_id: sessionId,
user_message: secondMessage,
useMock: true
} as any);
expect(second.ok).toBe(true);
expect(second.reply_type).toBe("factual");
expect(calls).toHaveLength(2);
expect(calls[1].message).toBe(secondMessage);
expect(calls[1].options?.activeOrganization).toBe("ООО Альтернатива Плюс");
expect(calls[1].options?.knownOrganizations).toEqual(["ООО Альтернатива Плюс", "ООО Лайсвуд"]);
expect(calls[1].options?.followupContext?.previous_intent).toBe("inventory_on_hand_as_of_date");
expect(calls[1].options?.followupContext?.previous_filters?.organization).toBe("ООО Альтернатива Плюс");
expect(calls[1].options?.followupContext?.root_filters?.organization).toBe("ООО Альтернатива Плюс");
expect(normalizerService.normalize).not.toHaveBeenCalled();
});
}); });

View File

@ -237,4 +237,71 @@ describe("assistant address orchestration runtime adapter", () => {
}) })
); );
}); });
it("prefers raw selected-object sale-destination wording over generic canonical drift intent", async () => {
const resolveAddressFollowupCarryoverContext = vi.fn(() => ({
followupContext: {
previous_intent: "inventory_on_hand_as_of_date",
previous_filters: {
as_of_date: "2020-05-31",
period_from: "2020-05-01",
period_to: "2020-05-31"
}
}
}));
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

@ -657,6 +657,88 @@ describe("assistant living chat mode", () => {
expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(0); expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(0);
}); });
it("answers historical capability follow-up in current inventory context instead of generic capability contract", async () => {
const normalizer = {
normalize: vi.fn().mockResolvedValue({
ok: false,
trace_id: "norm-inventory-history-capability",
prompt_version: "normalizer_v2_0_2",
schema_version: "v2_0_2",
normalized: null,
validation: { passed: false, errors: ["mock"] },
route_hint_summary: null,
raw_model_output: {},
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
latency_ms: 1,
request_count_for_case: 1
})
} as any;
const sessions = new AssistantSessionStore();
const sessionId = "asst-living-chat-inventory-history-capability";
sessions.ensureSession(sessionId);
sessions.appendItem(sessionId, {
message_id: "msg-seed-inventory-slice",
session_id: sessionId,
role: "assistant",
text: "На 15.04.2026 на складе подтверждено 11 позиций.",
reply_type: "factual",
created_at: new Date().toISOString(),
trace_id: "address-seed-inventory-history-capability",
debug: {
execution_lane: "address_query",
answer_grounding_check: {
status: "grounded"
},
detected_intent: "inventory_on_hand_as_of_date",
capability_id: "confirmed_inventory_on_hand_as_of_date",
assistant_active_organization: "альтернатива",
extracted_filters: {
organization: "альтернатива",
as_of_date: "2026-04-15"
},
address_root_frame_context: {
root_intent: "inventory_on_hand_as_of_date",
current_frame_kind: "inventory_root",
organization: "альтернатива",
as_of_date: "2026-04-15"
}
}
} as any);
const addressQueryService = {
tryHandle: vi.fn().mockResolvedValue({ handled: false })
} as any;
const chatClient = {
chat: vi.fn().mockResolvedValue({
raw: { id: "chat-inventory-history-capability-should-not-run" },
outputText: "unused",
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 }
})
} as any;
const service = new AssistantService(normalizer as any, sessions, undefined as any, undefined as any, addressQueryService, chatClient);
const response = await service.handleMessage({
session_id: sessionId,
user_message: "а исторические данные ты можешь же показать?",
llmProvider: "local",
model: "qwen3",
useMock: false
} as any);
expect(response.ok).toBe(true);
expect(response.reply_type).toBe("factual_with_explanation");
expect(String(response.assistant_reply).toLowerCase()).toContain("историческ");
expect(String(response.assistant_reply).toLowerCase()).toContain("альтернатив");
expect(String(response.assistant_reply).toLowerCase()).toContain("март 2020");
expect(String(response.assistant_reply)).not.toContain("Что умею по группам");
expect(response.debug?.tool_gate_reason).toBe("inventory_history_capability_followup_detected");
expect(response.debug?.living_chat_response_source).toBe("deterministic_inventory_history_capability_contract");
expect(chatClient.chat).toHaveBeenCalledTimes(0);
expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(0);
});
it("handles data-scope meta question as deterministic chat contract", async () => { it("handles data-scope meta question as deterministic chat contract", async () => {
const normalizer = { const normalizer = {
normalize: vi.fn().mockResolvedValue({ normalize: vi.fn().mockResolvedValue({

View File

@ -235,6 +235,56 @@ describe("assistant orchestration contract", () => {
expect(decision.orchestrationContract?.hard_meta_mode).toBe("non_domain"); expect(decision.orchestrationContract?.hard_meta_mode).toBe("non_domain");
}); });
it("routes historical capability follow-up over grounded inventory answer to contextual chat", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "а исторические данные ты можешь же показать?",
effectiveAddressUserMessage: "а исторические данные ты можешь же показать?",
followupContext: null,
llmPreDecomposeMeta: {
applied: false,
reason: "normalized_fragment_rejected_semantic_guard",
predecomposeContract: {
mode: "unsupported",
mode_confidence: "low",
intent: "unknown",
intent_confidence: "low"
},
semanticExtractionContract: {
valid: false,
apply_canonical_recommended: false
}
} as any,
sessionItems: [
{
role: "assistant",
debug: {
execution_lane: "address_query",
answer_grounding_check: {
status: "grounded"
},
detected_intent: "inventory_on_hand_as_of_date",
capability_id: "confirmed_inventory_on_hand_as_of_date",
assistant_active_organization: "альтернатива",
address_root_frame_context: {
root_intent: "inventory_on_hand_as_of_date",
current_frame_kind: "inventory_root",
organization: "альтернатива",
as_of_date: "2026-04-15"
}
}
}
],
useMock: false
} as any);
expect(decision.runAddressLane).toBe(false);
expect(decision.toolGateDecision).toBe("skip_address_lane");
expect(decision.toolGateReason).toBe("inventory_history_capability_followup_detected");
expect(decision.livingMode).toBe("chat");
expect(decision.livingReason).toBe("inventory_history_capability_followup_detected");
expect(decision.orchestrationContract?.followup_context_detected).toBe(true);
});
it("keeps VAT payable forecast query in address lane", () => { it("keeps VAT payable forecast query in address lane", () => {
const decision = resolveAssistantOrchestrationDecision({ const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "какой прогноз оплаты ндс за 12 мая 2020", rawUserMessage: "какой прогноз оплаты ндс за 12 мая 2020",
@ -631,6 +681,55 @@ describe("assistant orchestration contract", () => {
expect(decision.livingReason).toBe("address_lane_triggered"); expect(decision.livingReason).toBe("address_lane_triggered");
}); });
it("routes meta follow-up over grounded inventory answer to chat instead of rerunning address lane", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "\u0447\u0435 \u0434\u0443\u043c\u0430\u0435\u0448\u044c \u043d\u0430 \u044d\u0442\u0443 \u0442\u0435\u043c\u0443",
effectiveAddressUserMessage: "\u0447\u0435 \u0434\u0443\u043c\u0430\u0435\u0448\u044c \u043d\u0430 \u044d\u0442\u0443 \u0442\u0435\u043c\u0443",
followupContext: {
previous_intent: "inventory_on_hand_as_of_date",
previous_filters: {
as_of_date: "2016-06-30",
organization: "alt"
},
previous_anchor_type: "counterparty",
previous_anchor_value: "ALT"
},
llmPreDecomposeMeta: {
applied: false,
reason: "normalized_fragment_rejected_semantic_guard",
llmCanonicalCandidateDetected: true,
predecomposeContract: {
mode: "unsupported",
mode_confidence: "low",
intent: "unknown",
intent_confidence: "low"
},
semanticExtractionContract: {
valid: false,
apply_canonical_recommended: false
}
} as any,
sessionItems: [
{
role: "assistant",
debug: {
execution_lane: "address_query",
answer_grounding_check: {
status: "grounded"
}
}
}
],
useMock: false
} as any);
expect(decision.runAddressLane).toBe(false);
expect(decision.toolGateDecision).toBe("skip_address_lane");
expect(decision.toolGateReason).toBe("meta_followup_over_grounded_answer");
expect(decision.livingMode).toBe("chat");
expect(decision.livingReason).toBe("meta_followup_over_grounded_answer");
});
it("keeps documentary inventory chain verification in address lane for supported exact intent", () => { it("keeps documentary inventory chain verification in address lane for supported exact intent", () => {
const question = const question =
"Есть ли документально подтвержденная цепочка: поставщик Гамма-мебель, ООО -> товар Шкаф картотечный 1000*400*2100 -> покупатель Департамент капитального ремонта города Москвы"; "Есть ли документально подтвержденная цепочка: поставщик Гамма-мебель, ООО -> товар Шкаф картотечный 1000*400*2100 -> покупатель Департамент капитального ремонта города Москвы";

View File

@ -51,6 +51,45 @@ describe("assistant organization scope runtime adapter", () => {
expect(normalizeOrganizationScopeValue).toHaveBeenCalledWith("Org A"); expect(normalizeOrganizationScopeValue).toHaveBeenCalledWith("Org A");
}); });
it("prefers organization scope from address navigation state when present", () => {
const normalizeOrganizationScopeValue = vi.fn((value: unknown) =>
typeof value === "string" && value.trim() ? value.trim() : null
);
const context = resolveSessionOrganizationScopeContextRuntime({
userMessage: "просто продолжай",
items: [] as any[],
addressNavigationState: {
schema_version: "address_navigation_state_v1",
session_id: "asst-nav-org",
updated_at: "2026-04-15T10:00:00.000Z",
session_context: {
active_result_set_id: "rs-1",
active_focus_object: null,
last_confirmed_route: "address_inventory_on_hand_as_of_date_v1",
date_scope: {
as_of_date: "2020-05-31",
period_from: null,
period_to: null
},
organization_scope: "Org B"
},
result_sets: [],
navigation_history: []
} as any,
extractKnownOrganizationsFromHistory: () => ["Org A"],
resolveOrganizationSelectionFromMessage: () => null,
findLastAssistantActiveOrganization: () => "Org A",
normalizeOrganizationScopeValue
});
expect(context).toEqual({
knownOrganizations: ["Org B", "Org A"],
selectedOrganization: null,
activeOrganization: "Org B"
});
});
it("merges organization into followup previous filters when organization is missing", () => { it("merges organization into followup previous filters when organization is missing", () => {
const merged = mergeFollowupContextWithOrganizationScopeRuntime({ const merged = mergeFollowupContextWithOrganizationScopeRuntime({
followupContext: { followupContext: {
@ -69,6 +108,9 @@ describe("assistant organization scope runtime adapter", () => {
previous_filters: { previous_filters: {
period: "2020-07", period: "2020-07",
organization: "Org A" organization: "Org A"
},
root_filters: {
organization: "Org A"
} }
}); });
}); });