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

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