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

This commit is contained in:
2026-04-15 14:50:16 +03:00
parent 70cc5a99f1
commit 7a6d8eb070
44 changed files with 2394 additions and 118 deletions
@@ -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 {
const hasItemCue = /(?:товар|номенклатур|sku|item|product|остат(?:ок|ки)|склад)/iu.test(text);
const hasSupplierCue =
@@ -1663,9 +1672,9 @@ function hasInventoryPurchaseDocumentsSignalV2(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 =
/(?:кому\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
);
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)) {
return {
intent: "inventory_sale_trace_for_item",
@@ -29,7 +29,14 @@ const DISPLAY_ENTITY_TYPE_BY_INTENT: Partial<Record<AddressIntent, AddressFocusO
list_documents_by_contract: "document_ref",
bank_operations_by_counterparty: "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>> = {
@@ -48,6 +55,13 @@ const RESULT_SET_TYPE_BY_INTENT: Partial<Record<AddressIntent, AddressResultSetT
bank_operations_by_counterparty: "bank_operations_list",
bank_operations_by_contract: "bank_operations_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",
document_type_and_account_section_profile: "profile_summary",
counterparty_population_and_roles: "profile_summary",
@@ -76,7 +90,15 @@ function toAddressFocusObjectType(value: unknown): AddressFocusObjectType {
if (!normalized) {
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 "unknown";
@@ -149,6 +171,44 @@ function extractEntityRefsFromAssistantReply(
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 {
if (!value) {
return null;
@@ -392,6 +452,14 @@ export function evolveAddressNavigationStateWithAssistantItem(
const resultSetId = `rs-${item.message_id}`;
const routeId = toNonEmptyString(debug.selected_recipe);
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 entityRefs = extractEntityRefsFromAssistantReply(item.text, intent);
const resultSet: AddressResultSet = {
@@ -399,7 +467,7 @@ export function evolveAddressNavigationStateWithAssistantItem(
type: inferResultSetType(intent),
intent,
route_id: routeId,
filters,
filters: filtersWithDerivedScope,
source_refs: sourceRefs,
entity_refs: entityRefs,
created_from_turn: turnIndex,
@@ -418,11 +486,11 @@ export function evolveAddressNavigationStateWithAssistantItem(
created_at: createdAt
};
const normalizedDateScope = {
as_of_date: toNonEmptyString(filters.as_of_date),
period_from: toNonEmptyString(filters.period_from),
period_to: toNonEmptyString(filters.period_to)
as_of_date: toNonEmptyString(filtersWithDerivedScope.as_of_date),
period_from: toNonEmptyString(filtersWithDerivedScope.period_from),
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
@@ -18,11 +18,13 @@ import type {
AddressLimitedReasonCategory,
AddressMatchFailureStage,
AddressMcpCallStatus,
AddressModeDetection,
AddressQueryShapeDetection,
AddressResultMode,
AddressResponseType,
AddressRuntimeReadiness,
AddressSemanticFrame
AddressSemanticFrame,
AddressIntentResolution
} from "../types/addressQuery";
import {
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;
}
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 {
return (
intent === "list_receivables_counterparties" ||
@@ -1976,7 +2007,32 @@ function shouldBoostAutoBroadenedLimit(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"] {
@@ -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 {
public async tryHandle(userMessage: string, options: AddressTryHandleOptions = {}): Promise<AddressExecutionResult | null> {
if (!FEATURE_ASSISTANT_ADDRESS_QUERY_V1) {
@@ -2843,6 +2955,31 @@ export class AddressQueryService {
activeOrganization: options.activeOrganization ?? null,
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 confirmedBalancePayablesIntent =
(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");
}
}
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 capabilityAudit = buildCapabilityAudit(intent.intent);
const shadowRouteAudit = buildShadowRouteAudit({
@@ -3419,6 +3600,42 @@ export class AddressQueryService {
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) {
const recoveredBankRows = applyIntentSpecificFilter("bank_operations_by_contract", filterByAnchors);
@@ -3655,7 +3872,7 @@ export class AddressQueryService {
const broadenedAdjustments: string[] = [];
delete autoBroadenedFilters.period_from;
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;
broadenedAdjustments.push("as_of_date_cleared_for_history_recovery");
}
@@ -38,7 +38,8 @@ const INVENTORY_MOVEMENTS_QUERY_TEMPLATE = `
ПРЕДСТАВЛЕНИЕ(Движения.СубконтоДт3) КАК СубконтоДт3,
ПРЕДСТАВЛЕНИЕ(Движения.СубконтоКт1) КАК СубконтоКт1,
ПРЕДСТАВЛЕНИЕ(Движения.СубконтоКт2) КАК СубконтоКт2,
ПРЕДСТАВЛЕНИЕ(Движения.СубконтоКт3) КАК СубконтоКт3
ПРЕДСТАВЛЕНИЕ(Движения.СубконтоКт3) КАК СубконтоКт3,
ПРЕДСТАВЛЕНИЕ(Движения.Организация) КАК Организация
ИЗ
РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто КАК Движения
__WHERE_CLAUSE__
@@ -967,6 +968,21 @@ function toDateTimeExpr(isoDate: string, endOfDay: boolean): string | null {
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 {
const periodFromExpr =
typeof filters.period_from === "string" && filters.period_from.trim().length > 0
@@ -1138,11 +1154,16 @@ function buildInventoryMovementQuery(
: side === "kt"
? creditPredicate
: `(${debitPredicate} ИЛИ ${creditPredicate})`;
const organizationCondition = buildOrganizationPresentationCondition(filters, "Движения.Организация");
return INVENTORY_MOVEMENTS_QUERY_TEMPLATE
.replace("__LIMIT__", String(resolvedLimit))
.replace(
"__WHERE_CLAUSE__",
buildWhereClause(filters, "Движения.Период", [inventoryCondition])
buildWhereClause(
filters,
"Движения.Период",
[inventoryCondition, organizationCondition].filter((item): item is string => Boolean(item))
)
)
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
}
@@ -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(
followupContext: AddressFollowupContext | null
): AddressFollowupContext | null {
@@ -529,7 +538,7 @@ function hasBareInventoryPurchaseDateFollowupCue(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 ?? "")
);
}
@@ -786,21 +795,19 @@ function mergeFollowupFilters(
}
if (
!sameDateRequested &&
(intent === "inventory_purchase_provenance_for_item" ||
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date") &&
(intent === "inventory_aging_by_purchase_date" || isInventoryLifecycleHistoryIntent(intent)) &&
!hasExplicitPeriodLiteral(userMessage) &&
!hasExplicitCurrentDateHint(userMessage)
) {
const inheritedAsOfDate = previousAsOfDate ?? previousPeriodTo ?? previousPeriodFrom;
const currentAsOfDate = toNonEmptyString(merged.as_of_date);
const todayIso = new Date().toISOString().slice(0, 10);
const currentLooksDefaultedToToday = currentAsOfDate === todayIso;
if (inheritedAsOfDate && (!currentAsOfDate || currentLooksDefaultedToToday) && currentAsOfDate !== inheritedAsOfDate) {
merged.as_of_date = inheritedAsOfDate;
reasons.push("as_of_date_from_followup_context");
if (intent === "inventory_aging_by_purchase_date") {
const inheritedAsOfDate = previousAsOfDate ?? previousPeriodTo ?? previousPeriodFrom;
const currentAsOfDate = toNonEmptyString(merged.as_of_date);
const todayIso = new Date().toISOString().slice(0, 10);
const currentLooksDefaultedToToday = currentAsOfDate === todayIso;
if (inheritedAsOfDate && (!currentAsOfDate || currentLooksDefaultedToToday) && currentAsOfDate !== inheritedAsOfDate) {
merged.as_of_date = inheritedAsOfDate;
reasons.push("as_of_date_from_followup_context");
}
}
}
if (
@@ -890,6 +897,7 @@ function mergeFollowupFilters(
const hasFollowupSignal = hasAddressFollowupContextSignal(userMessage);
const hasExplicitPeriodInMessage = hasExplicitPeriodLiteral(userMessage);
const hasExplicitCurrentDateInMessage = hasExplicitCurrentDateHint(userMessage);
const inventoryLifecycleHistoryIntent = isInventoryLifecycleHistoryIntent(intent);
const asOfPrimaryIntent =
intent === "account_balance_snapshot" ||
intent === "documents_forming_balance" ||
@@ -928,7 +936,13 @@ function mergeFollowupFilters(
}
}
if (!currentHasPeriod && previousHasPeriod && hasFollowupSignal && !hasExplicitPeriodInMessage) {
if (
!currentHasPeriod &&
previousHasPeriod &&
hasFollowupSignal &&
!hasExplicitPeriodInMessage &&
!inventoryLifecycleHistoryIntent
) {
if (previousPeriodFrom) {
merged.period_from = previousPeriodFrom;
}
@@ -42,10 +42,16 @@ interface AddressSessionScope {
export interface RunAssistantAddressAttemptRuntimeInput<ResponseType = unknown>
extends Omit<
RunAssistantAddressRuntimeInput<ResponseType>,
"llmProvider" | "useMock" | "payloadContextPeriodHint" | "runAddressLaneAttempt" | "finalizeAddressLaneResponse" | "tryHandleLivingChat"
| "llmProvider"
| "useMock"
| "payloadContextPeriodHint"
| "runAddressLaneAttempt"
| "finalizeAddressLaneResponse"
| "tryHandleLivingChat"
> {
payload: AddressAttemptPayload;
sessionScope: AddressSessionScope;
sessionAddressNavigationState?: unknown;
mergeFollowupContextWithOrganizationScope: RunAssistantAddressLaneAttemptRuntimeInput["mergeFollowupContextWithOrganizationScope"];
runAddressQueryTryHandle: RunAssistantAddressLaneAttemptRuntimeInput["runAddressQueryTryHandle"];
mergeKnownOrganizations: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["mergeKnownOrganizations"];
@@ -227,6 +233,8 @@ export async function runAssistantAddressAttemptRuntime<ResponseType = unknown>(
sessionId: input.sessionId,
userMessage: input.userMessage,
sessionItems: input.sessionItems,
sessionAddressNavigationState: input.sessionAddressNavigationState,
sessionScope: input.sessionScope,
payload: input.payload,
featureAddressLlmPredecomposeV1: input.featureAddressLlmPredecomposeV1,
runAddressLlmPreDecompose: input.runAddressLlmPreDecompose,
@@ -206,7 +206,13 @@ export function runAssistantAddressLaneResponseRuntime<ResponseType = AssistantM
if (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"
? (debug.extracted_filters as Record<string, unknown>)
@@ -1,6 +1,12 @@
export interface BuildAssistantAddressOrchestrationRuntimeInput {
userMessage: string;
sessionItems: unknown[];
sessionAddressNavigationState?: unknown;
sessionOrganizationScope?: {
knownOrganizations?: unknown;
selectedOrganization?: unknown;
activeOrganization?: unknown;
} | null;
llmProvider: unknown;
useMock: boolean;
featureAddressLlmPredecomposeV1: boolean;
@@ -15,7 +21,8 @@ export interface BuildAssistantAddressOrchestrationRuntimeInput {
userMessage: string,
sessionItems: unknown[],
addressInputMessage: string,
addressPreDecompose: Record<string, unknown>
addressPreDecompose: Record<string, unknown>,
sessionAddressNavigationState?: unknown
) => AssistantAddressCarryoverLike | null;
resolveAssistantOrchestrationDecision: (input: {
rawUserMessage: string;
@@ -23,6 +30,7 @@ export interface BuildAssistantAddressOrchestrationRuntimeInput {
followupContext: unknown;
llmPreDecomposeMeta: Record<string, unknown>;
sessionItems?: unknown[];
sessionOrganizationScope?: unknown;
useMock: boolean;
}) => Record<string, unknown>;
buildAddressDialogContinuationContractV2: (
@@ -57,7 +65,7 @@ function hasSelectedObjectInventorySignal(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 ?? "")
);
}
@@ -154,7 +162,8 @@ export async function buildAssistantAddressOrchestrationRuntime(
input.userMessage,
input.sessionItems,
addressInputMessage,
addressPreDecompose
addressPreDecompose,
input.sessionAddressNavigationState
);
if (
shouldPreferRawFollowupMessage(
@@ -180,7 +189,8 @@ export async function buildAssistantAddressOrchestrationRuntime(
input.userMessage,
input.sessionItems,
addressInputMessage,
addressPreDecompose
addressPreDecompose,
input.sessionAddressNavigationState
);
}
@@ -191,6 +201,7 @@ export async function buildAssistantAddressOrchestrationRuntime(
followupContext,
llmPreDecomposeMeta: addressPreDecompose,
sessionItems: input.sessionItems,
sessionOrganizationScope: input.sessionOrganizationScope ?? null,
useMock: input.useMock
});
const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(
@@ -19,6 +19,12 @@ export interface RunAssistantAddressRuntimeInput<ResponseType = unknown> {
sessionId: string;
userMessage: string;
sessionItems: unknown[];
sessionAddressNavigationState?: unknown;
sessionOrganizationScope?: {
knownOrganizations?: unknown;
selectedOrganization?: unknown;
activeOrganization?: unknown;
} | null;
llmProvider: unknown;
useMock: boolean;
featureAddressLlmPredecomposeV1: boolean;
@@ -112,6 +118,8 @@ export async function runAssistantAddressRuntime<ResponseType = unknown>(
const addressOrchestrationRuntime = await runAddressOrchestrationRuntimeSafe({
userMessage: input.userMessage,
sessionItems: input.sessionItems,
sessionAddressNavigationState: input.sessionAddressNavigationState,
sessionOrganizationScope: input.sessionOrganizationScope ?? null,
llmProvider: input.llmProvider,
useMock: input.useMock,
featureAddressLlmPredecomposeV1: input.featureAddressLlmPredecomposeV1,
@@ -11,6 +11,11 @@ interface AssistantAddressAttemptPayloadLike {
export interface BuildAssistantAddressRuntimeInputInput<ResponseType = unknown>
extends Omit<RunAssistantAddressRuntimeInput<ResponseType>, "llmProvider" | "useMock" | "payloadContextPeriodHint"> {
payload: AssistantAddressAttemptPayloadLike;
sessionScope?: {
knownOrganizations?: unknown;
selectedOrganization?: unknown;
activeOrganization?: unknown;
} | null;
}
export function buildAssistantAddressRuntimeInput<ResponseType = unknown>(
@@ -21,6 +26,8 @@ export function buildAssistantAddressRuntimeInput<ResponseType = unknown>(
sessionId: input.sessionId,
userMessage: input.userMessage,
sessionItems: input.sessionItems,
sessionAddressNavigationState: input.sessionAddressNavigationState,
sessionOrganizationScope: input.sessionScope,
llmProvider: input.payload.llmProvider,
useMock: Boolean(input.payload.useMock),
featureAddressLlmPredecomposeV1: input.featureAddressLlmPredecomposeV1,
@@ -57,6 +57,84 @@ export interface AssistantLivingChatRuntimeOutput {
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(
input: AssistantLivingChatRuntimeInput
): Promise<AssistantLivingChatRuntimeOutput> {
@@ -77,6 +155,11 @@ export async function runAssistantLivingChatRuntime(
let knownOrganizations = input.mergeKnownOrganizations(input.sessionScope.knownOrganizations ?? []);
let selectedOrganization = input.toNonEmptyString(input.sessionScope.selectedOrganization);
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)) {
chatText = input.buildAssistantSafetyRefusalReply();
@@ -119,6 +202,15 @@ export async function runAssistantLivingChatRuntime(
} else if (capabilityMetaQuery && operationalSignal && !input.hasAssistantCapabilityQuestionSignal(userMessage)) {
chatText = input.buildAssistantOperationalBoundaryReply();
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) {
chatText = input.buildAssistantCapabilityContractReply();
livingChatSource = "deterministic_capability_contract";
@@ -61,7 +61,7 @@ export function normalizeOrganizationScopeValue(value: unknown): string | null {
if (!normalized) {
return null;
}
let unwrapped = normalized.replace(/^\\+|\\+$/g, "").trim();
let unwrapped = normalized.trim();
if (
(unwrapped.startsWith('"') && unwrapped.endsWith('"')) ||
(unwrapped.startsWith("'") && unwrapped.endsWith("'"))
@@ -1,3 +1,5 @@
import type { AddressNavigationState } from "../types/addressNavigation";
export interface AssistantSessionOrganizationScopeContext {
knownOrganizations: string[];
selectedOrganization: string | null;
@@ -7,6 +9,7 @@ export interface AssistantSessionOrganizationScopeContext {
export interface ResolveSessionOrganizationScopeContextRuntimeInput<ItemType = unknown> {
userMessage: string;
items: ItemType[];
addressNavigationState?: AddressNavigationState | null;
extractKnownOrganizationsFromHistory: (items: ItemType[]) => string[];
resolveOrganizationSelectionFromMessage: (userMessage: string, knownOrganizations: string[]) => string | null;
findLastAssistantActiveOrganization: (items: ItemType[]) => string | null;
@@ -20,16 +23,62 @@ export interface MergeFollowupContextWithOrganizationScopeRuntimeInput {
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>(
input: ResolveSessionOrganizationScopeContextRuntimeInput<ItemType>
): 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(
input.userMessage,
knownOrganizations
);
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 {
knownOrganizations,
@@ -57,5 +106,16 @@ export function mergeFollowupContextWithOrganizationScopeRuntime(
previousFilters.organization = normalizedOrganization;
}
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;
}
@@ -20,6 +20,7 @@ import * as assistantAddressAttemptRuntimeAdapter_1 from "./assistantAddressAtte
import * as assistantCoverageGrounding_1 from "./assistantCoverageGrounding";
import * as assistantDeepTurnAttemptRuntimeAdapter_1 from "./assistantDeepTurnAttemptRuntimeAdapter";
import * as assistantOrganizationScopeRuntimeAdapter_1 from "./assistantOrganizationScopeRuntimeAdapter";
import * as assistantOrganizationMatcher_1 from "./assistantOrganizationMatcher";
import * as assistantTurnAttemptRuntimeAdapter_1 from "./assistantTurnAttemptRuntimeAdapter";
import * as assistantTurnRuntimeDepsAdapter_1 from "./assistantTurnRuntimeDepsAdapter";
import * as assistantTurnRuntimeInputBuilder_1 from "./assistantTurnRuntimeInputBuilder";
@@ -1427,6 +1428,7 @@ function buildAddressDebugPayload(addressDebug, llmPreDecomposeMeta = null) {
account_scope_drop_reason: addressDebug.account_scope_drop_reason,
runtime_readiness: addressDebug.runtime_readiness,
limited_reason_category: addressDebug.limited_reason_category,
organization_candidates: addressDebug.organization_candidates ?? undefined,
response_type: addressDebug.response_type,
requested_result_mode: addressDebug.requested_result_mode ?? undefined,
result_mode: addressDebug.result_mode ?? undefined,
@@ -2747,9 +2749,18 @@ function resolveDebtRoleSwapFollowupIntent(userMessage, previousIntent) {
}
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 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 hasImplicitContinuationSignal = Boolean(previousAddressDebug) &&
Boolean(followupOffer?.enabled) &&
@@ -2780,10 +2791,15 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
!hasPrimaryFollowupSignal &&
!hasAlternateFollowupSignal &&
!hasImplicitContinuationSignal &&
!hasOrganizationClarificationContinuation &&
!hasIndexReferenceSignal) {
return null;
}
if (!hasPrimaryFollowupSignal && !hasAlternateFollowupSignal && !hasImplicitContinuationSignal && !hasIndexReferenceSignal) {
if (!hasPrimaryFollowupSignal &&
!hasAlternateFollowupSignal &&
!hasImplicitContinuationSignal &&
!hasOrganizationClarificationContinuation &&
!hasIndexReferenceSignal) {
return null;
}
if (!previousAddressDebug) {
@@ -2811,7 +2827,45 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
readAddressFilterString(previousAddressDebug, "counterparty") ??
readAddressFilterString(previousAddressDebug, "account") ??
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
? isInventoryDrilldownFrameIntent(sourceIntent)
? "inventory_drilldown"
@@ -2842,6 +2896,21 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
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 displayedEntities = extractDisplayedAddressEntityCandidates(toNonEmptyString(previousAddressItem?.text) ?? "", displayedEntityType);
const resolvedEntityFromFollowup = resolveDisplayedAddressEntityMention(userMessage, displayedEntities) ??
@@ -2869,6 +2938,36 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
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) {
return null;
}
@@ -3994,6 +4093,28 @@ export function resolveAssistantOrchestrationDecision(input) {
const llmPreDecomposeMeta = input?.llmPreDecomposeMeta ?? null;
const useMock = Boolean(input?.useMock);
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) ||
hasAssistantDataScopeMetaQuestionSignal(repairedRawUserMessage) ||
hasAssistantDataScopeMetaQuestionSignal(effectiveAddressUserMessage) ||
@@ -4081,6 +4202,12 @@ export function resolveAssistantOrchestrationDecision(input) {
hasShortInventoryObjectFollowupSignal(repairedRawUserMessage) ||
hasShortInventoryObjectFollowupSignal(effectiveAddressUserMessage) ||
hasShortInventoryObjectFollowupSignal(repairedEffectiveAddressUserMessage)));
const organizationClarificationContinuationDetected = Boolean(followupContext &&
lastOrganizationClarificationDebug &&
organizationClarificationSelection &&
!dataScopeMetaQuery &&
!capabilityMetaQuery &&
!dataRetrievalSignal);
const effectiveAddressFollowupSignal = explicitAddressFollowupSignal && !dangerOrCoercionSignal;
const deterministicNonDomainGuard = Boolean(!dataScopeMetaQuery &&
!capabilityMetaQuery &&
@@ -4091,7 +4218,16 @@ export function resolveAssistantOrchestrationDecision(input) {
const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate &&
deterministicNonDomainGuard &&
(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
? "data_scope"
: capabilityMetaQuery && !dataRetrievalSignal
@@ -4126,6 +4262,34 @@ export function resolveAssistantOrchestrationDecision(input) {
};
}
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 {
runAddressLane: false,
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 preserveAddressLaneSignal = Boolean((llmPreDecomposeMeta?.llmCanonicalCandidateDetected &&
llmPreDecomposeMeta?.applied &&
@@ -4275,6 +4443,19 @@ export function resolveAssistantOrchestrationDecision(input) {
repairedEffectiveAddressUserMessage,
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 toolGateDecision = String(baseToolGate?.decision ?? "skip_address_lane");
let toolGateReason = String(baseToolGate?.reason ?? "no_address_signal_after_l0");
@@ -4300,6 +4481,11 @@ export function resolveAssistantOrchestrationDecision(input) {
toolGateDecision = "skip_address_lane";
toolGateReason = "deep_session_continuation_fallback_to_deep";
}
if (metaFollowupOverGroundedAnswer) {
runAddressLane = false;
toolGateDecision = "skip_address_lane";
toolGateReason = "meta_followup_over_grounded_answer";
}
let livingDecision = resolveLivingAssistantModeDecision({
userMessage: rawUserMessage,
addressLaneTriggered: runAddressLane,
@@ -4333,6 +4519,12 @@ export function resolveAssistantOrchestrationDecision(input) {
reason: "deep_session_continuation_fallback_to_deep"
};
}
if (metaFollowupOverGroundedAnswer) {
livingDecision = {
mode: "chat",
reason: "meta_followup_over_grounded_answer"
};
}
return {
runAddressLane,
toolGateDecision,
@@ -4434,6 +4626,105 @@ function findLastAssistantLivingChatDebug(items) {
}
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) {
const repaired = repairAddressMojibake(String(userMessage ?? ""));
const normalized = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е");
@@ -4722,7 +5013,6 @@ function normalizeOrganizationScopeValue(value) {
return null;
}
const unwrapped = normalized
.replace(/^\\+|\\+$/g, "")
.replace(/^"+|"+$/g, "")
.replace(/^'+|'+$/g, "")
.trim();
@@ -4862,8 +5152,34 @@ function mergeKnownOrganizations(values) {
}
return Array.from(dedup.values()).slice(0, 20);
}
function extractKnownOrganizationsFromHistory(items) {
function extractKnownOrganizationsFromNavigationState(addressNavigationState) {
if (!addressNavigationState || typeof addressNavigationState !== "object") {
return [];
}
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) {
const item = items[index];
if (!item || item.role !== "assistant") {
@@ -4877,8 +5193,17 @@ function extractKnownOrganizationsFromHistory(items) {
const knownFromDebug = Array.isArray(debug.assistant_known_organizations)
? debug.assistant_known_organizations
: [];
if (directFromProbe.length > 0 || knownFromDebug.length > 0) {
collected.push(...directFromProbe, ...knownFromDebug);
const directFromCandidates = Array.isArray(debug.organization_candidates)
? 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);
@@ -4891,7 +5216,16 @@ function extractKnownOrganizationsFromHistory(items) {
}
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) {
const item = items[index];
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
@@ -4937,10 +5271,11 @@ function resolveOrganizationSelectionFromMessage(userMessage, knownOrganizations
}
return best.organization;
}
function resolveSessionOrganizationScopeContext(userMessage, items) {
function resolveSessionOrganizationScopeContext(userMessage, items, addressNavigationState = null) {
return (0, assistantOrganizationScopeRuntimeAdapter_1.resolveSessionOrganizationScopeContextRuntime)({
userMessage,
items,
addressNavigationState,
extractKnownOrganizationsFromHistory,
resolveOrganizationSelectionFromMessage,
findLastAssistantActiveOrganization,
@@ -4955,8 +5290,8 @@ function mergeFollowupContextWithOrganizationScope(followupContext, organization
toNonEmptyString
});
}
export function resolveSessionOrganizationScopeContextForTests(userMessage, items) {
return resolveSessionOrganizationScopeContext(userMessage, items);
export function resolveSessionOrganizationScopeContextForTests(userMessage, items, addressNavigationState = null) {
return resolveSessionOrganizationScopeContext(userMessage, items, addressNavigationState);
}
function normalizeGuidValue(value) {
const source = normalizeScopeLabel(value);
@@ -19,6 +19,7 @@ export function buildAssistantTurnAttemptAddressRuntimeInput<PayloadType = unkno
sessionId: input.userTurn.sessionId,
userMessage: input.userTurn.userMessage,
sessionItems: input.userTurn.session.items,
sessionAddressNavigationState: input.userTurn.session.address_navigation_state ?? null,
runtimeAnalysisContext: input.userTurn.runtimeAnalysisContext,
sessionOrganizationScope: input.sessionOrganizationScope
};
@@ -16,6 +16,7 @@ export interface RunAssistantTurnAttemptRuntimeAddressInput<PayloadType = unknow
sessionId: string;
userMessage: string;
sessionItems: unknown[];
sessionAddressNavigationState?: unknown;
runtimeAnalysisContext: { as_of_date: string | null };
sessionOrganizationScope: AssistantSessionOrganizationScopeContext;
}
@@ -35,7 +36,8 @@ export interface RunAssistantTurnAttemptRuntimeInput<ResponseType = unknown, Pay
runUserTurnBootstrapRuntime: (payload: PayloadType) => RunAssistantUserTurnBootstrapRuntimeOutput;
resolveSessionOrganizationScopeContext: (
userMessage: string,
sessionItems: unknown[]
sessionItems: unknown[],
sessionAddressNavigationState?: unknown
) => AssistantSessionOrganizationScopeContext;
runAddressAttemptRuntime: (
input: RunAssistantTurnAttemptRuntimeAddressInput<PayloadType>
@@ -59,7 +61,8 @@ export async function runAssistantTurnAttemptRuntime<ResponseType = unknown, Pay
const userTurn = input.runUserTurnBootstrapRuntime(input.payload);
const sessionOrganizationScope = input.resolveSessionOrganizationScopeContext(
userTurn.userMessage,
userTurn.session.items
userTurn.session.items,
userTurn.session.address_navigation_state ?? null
);
const addressRuntime = await input.runAddressAttemptRuntime(
buildAssistantTurnAttemptAddressRuntimeInput({
@@ -136,6 +136,7 @@ export function buildAssistantAddressAttemptRuntimeInput<ResponseType = unknown>
sessionId: runtimeInput.sessionId,
userMessage: runtimeInput.userMessage,
sessionItems: runtimeInput.sessionItems,
sessionAddressNavigationState: runtimeInput.sessionAddressNavigationState,
payload: runtimeInput.payload,
sessionScope: {
knownOrganizations: runtimeInput.sessionOrganizationScope.knownOrganizations,