АРЧ - Ассистент: отделить reference-срез от execution-окна в ответах по lifecycle follow-up
This commit is contained in:
parent
70cc5a99f1
commit
7a6d8eb070
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>)
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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("'"))
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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('ПРЕДСТАВЛЕНИЕ(Движения.Организация) = "ООО \\Альтернатива Плюс\\"');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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 -> покупатель Департамент капитального ремонта города Москвы";
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue