473 lines
19 KiB
JavaScript
473 lines
19 KiB
JavaScript
"use strict";
|
||
Object.defineProperty(exports, "__esModule", { value: true });
|
||
exports.createEmptyAddressNavigationState = createEmptyAddressNavigationState;
|
||
exports.cloneAddressNavigationState = cloneAddressNavigationState;
|
||
exports.normalizeAddressNavigationState = normalizeAddressNavigationState;
|
||
exports.evolveAddressNavigationStateWithAssistantItem = evolveAddressNavigationStateWithAssistantItem;
|
||
const nanoid_1 = require("nanoid");
|
||
const addressNavigation_1 = require("../types/addressNavigation");
|
||
const MAX_RESULT_SETS = 40;
|
||
const MAX_NAVIGATION_EVENTS = 120;
|
||
const MAX_ENTITY_REFS_PER_RESULT_SET = 40;
|
||
const DISPLAY_ENTITY_TYPE_BY_INTENT = {
|
||
counterparty_activity_lifecycle: "counterparty",
|
||
customer_revenue_and_payments: "counterparty",
|
||
supplier_payouts_profile: "counterparty",
|
||
list_payables_counterparties: "counterparty",
|
||
receivables_confirmed_as_of_date: "counterparty",
|
||
list_receivables_counterparties: "counterparty",
|
||
list_contracts_by_counterparty: "contract",
|
||
list_documents_by_counterparty: "document_ref",
|
||
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",
|
||
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_profitability_for_item: "item",
|
||
inventory_purchase_to_sale_chain: "item",
|
||
inventory_aging_by_purchase_date: "item"
|
||
};
|
||
const RESULT_SET_TYPE_BY_INTENT = {
|
||
counterparty_activity_lifecycle: "counterparty_list",
|
||
customer_revenue_and_payments: "counterparty_list",
|
||
supplier_payouts_profile: "counterparty_list",
|
||
list_payables_counterparties: "counterparty_list",
|
||
payables_confirmed_as_of_date: "balance_snapshot",
|
||
vat_payable_confirmed_as_of_date: "balance_snapshot",
|
||
vat_liability_confirmed_for_tax_period: "balance_snapshot",
|
||
receivables_confirmed_as_of_date: "balance_snapshot",
|
||
list_receivables_counterparties: "counterparty_list",
|
||
list_contracts_by_counterparty: "contract_list",
|
||
list_documents_by_counterparty: "document_list",
|
||
list_documents_by_contract: "document_list",
|
||
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_profitability_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",
|
||
contract_usage_overview: "profile_summary",
|
||
contract_usage_and_value: "profile_summary",
|
||
vat_payable_forecast: "profile_summary"
|
||
};
|
||
function toObject(value) {
|
||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||
return null;
|
||
}
|
||
return value;
|
||
}
|
||
function toNonEmptyString(value) {
|
||
if (typeof value !== "string") {
|
||
return null;
|
||
}
|
||
const trimmed = value.trim();
|
||
return trimmed.length > 0 ? trimmed : null;
|
||
}
|
||
function toAddressFocusObjectType(value) {
|
||
const normalized = toNonEmptyString(value);
|
||
if (!normalized) {
|
||
return "unknown";
|
||
}
|
||
if (normalized === "counterparty" ||
|
||
normalized === "contract" ||
|
||
normalized === "document_ref" ||
|
||
normalized === "account" ||
|
||
normalized === "item" ||
|
||
normalized === "organization" ||
|
||
normalized === "warehouse") {
|
||
return normalized;
|
||
}
|
||
return "unknown";
|
||
}
|
||
function toAddressIntent(value) {
|
||
const normalized = toNonEmptyString(value);
|
||
return (normalized ?? "unknown");
|
||
}
|
||
function inferDisplayEntityType(intent) {
|
||
return DISPLAY_ENTITY_TYPE_BY_INTENT[intent] ?? "unknown";
|
||
}
|
||
function inferResultSetType(intent) {
|
||
return RESULT_SET_TYPE_BY_INTENT[intent] ?? "unknown";
|
||
}
|
||
function parseEntityCandidateFromLine(line) {
|
||
const compact = String(line ?? "").trim();
|
||
if (!compact) {
|
||
return null;
|
||
}
|
||
const numberedMatch = compact.match(/^(\d+)\.\s+(.+)$/);
|
||
if (!numberedMatch) {
|
||
return null;
|
||
}
|
||
const index = Number.parseInt(String(numberedMatch[1] ?? ""), 10);
|
||
if (!Number.isFinite(index) || index <= 0) {
|
||
return null;
|
||
}
|
||
const afterNumber = String(numberedMatch[2] ?? "");
|
||
const pieces = afterNumber.split("|").map((item) => item.trim()).filter(Boolean);
|
||
const valueCandidate = pieces.length > 0 ? pieces[0] : afterNumber;
|
||
const cleaned = valueCandidate.replace(/^["'«»“”„`’‘]+|["'«»“”„`’‘]+$/gu, "").trim();
|
||
if (!cleaned || cleaned.length < 2) {
|
||
return null;
|
||
}
|
||
return { index, value: cleaned };
|
||
}
|
||
function extractEntityRefsFromAssistantReply(replyText, intent, limit = MAX_ENTITY_REFS_PER_RESULT_SET) {
|
||
const entityType = inferDisplayEntityType(intent);
|
||
if (entityType === "unknown") {
|
||
return [];
|
||
}
|
||
const dedup = new Map();
|
||
const lines = String(replyText ?? "").split(/\r?\n/);
|
||
for (const line of lines) {
|
||
const parsed = parseEntityCandidateFromLine(line);
|
||
if (!parsed) {
|
||
continue;
|
||
}
|
||
const key = `${parsed.index}:${entityType}:${parsed.value.toLowerCase()}`;
|
||
if (!dedup.has(key)) {
|
||
dedup.set(key, {
|
||
index: parsed.index,
|
||
entity_type: entityType,
|
||
value: parsed.value
|
||
});
|
||
}
|
||
if (dedup.size >= limit) {
|
||
break;
|
||
}
|
||
}
|
||
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) {
|
||
if (!value) {
|
||
return null;
|
||
}
|
||
return {
|
||
object_type: value.object_type,
|
||
object_id: value.object_id,
|
||
label: value.label,
|
||
provenance_result_set_id: value.provenance_result_set_id,
|
||
selected_at: value.selected_at
|
||
};
|
||
}
|
||
function cloneResultSet(input) {
|
||
return {
|
||
result_set_id: input.result_set_id,
|
||
type: input.type,
|
||
intent: input.intent,
|
||
route_id: input.route_id,
|
||
filters: { ...input.filters },
|
||
source_refs: [...input.source_refs],
|
||
entity_refs: input.entity_refs.map((item) => ({
|
||
index: item.index,
|
||
entity_type: item.entity_type,
|
||
value: item.value
|
||
})),
|
||
created_from_turn: input.created_from_turn,
|
||
created_at: input.created_at
|
||
};
|
||
}
|
||
function cloneNavigationEvent(input) {
|
||
return {
|
||
event_id: input.event_id,
|
||
action: input.action,
|
||
source_result_set_id: input.source_result_set_id,
|
||
target_object_id: input.target_object_id,
|
||
derived_result_set_id: input.derived_result_set_id,
|
||
turn_index: input.turn_index,
|
||
created_at: input.created_at
|
||
};
|
||
}
|
||
function normalizeFilters(value) {
|
||
const record = toObject(value);
|
||
if (!record) {
|
||
return {};
|
||
}
|
||
return { ...record };
|
||
}
|
||
function resolveNavigationAction(debug, hasFocusObject) {
|
||
const continuationContract = toObject(debug.dialog_continuation_contract_v2);
|
||
const decision = toNonEmptyString(continuationContract?.decision);
|
||
if (decision === "new_topic") {
|
||
return "open";
|
||
}
|
||
if (decision === "continue_previous") {
|
||
return hasFocusObject ? "drilldown" : "refine";
|
||
}
|
||
if (decision === "switch_to_suggested") {
|
||
return "refine";
|
||
}
|
||
return hasFocusObject ? "drilldown" : "open";
|
||
}
|
||
function buildFocusObjectFromDebug(debug, resultSetId, createdAt) {
|
||
const extractedFilters = toObject(debug.extracted_filters) ?? {};
|
||
const rawValue = toNonEmptyString(debug.anchor_value_resolved) ??
|
||
toNonEmptyString(debug.anchor_value_raw) ??
|
||
toNonEmptyString(extractedFilters.item);
|
||
if (!rawValue) {
|
||
return null;
|
||
}
|
||
const objectType = toAddressFocusObjectType(debug.anchor_type);
|
||
const canonicalType = objectType === "unknown" ? inferDisplayEntityType(toAddressIntent(debug.detected_intent)) : objectType;
|
||
return {
|
||
object_type: canonicalType,
|
||
object_id: `${canonicalType}:${rawValue}`.toLowerCase(),
|
||
label: rawValue,
|
||
provenance_result_set_id: resultSetId,
|
||
selected_at: createdAt
|
||
};
|
||
}
|
||
function capResultSets(resultSets) {
|
||
if (resultSets.length <= MAX_RESULT_SETS) {
|
||
return resultSets;
|
||
}
|
||
return resultSets.slice(resultSets.length - MAX_RESULT_SETS);
|
||
}
|
||
function capNavigationEvents(events) {
|
||
if (events.length <= MAX_NAVIGATION_EVENTS) {
|
||
return events;
|
||
}
|
||
return events.slice(events.length - MAX_NAVIGATION_EVENTS);
|
||
}
|
||
function isAddressAssistantItem(item) {
|
||
return (item.role === "assistant" &&
|
||
Boolean(item.debug) &&
|
||
toNonEmptyString(item.debug?.detected_mode) === "address_query");
|
||
}
|
||
function createEmptyAddressNavigationState(sessionId, nowIso = new Date().toISOString()) {
|
||
return {
|
||
schema_version: addressNavigation_1.ADDRESS_NAVIGATION_STATE_SCHEMA_VERSION,
|
||
session_id: sessionId,
|
||
updated_at: nowIso,
|
||
session_context: {
|
||
active_result_set_id: null,
|
||
active_focus_object: null,
|
||
last_confirmed_route: null,
|
||
date_scope: {
|
||
as_of_date: null,
|
||
period_from: null,
|
||
period_to: null
|
||
},
|
||
organization_scope: null
|
||
},
|
||
result_sets: [],
|
||
navigation_history: []
|
||
};
|
||
}
|
||
function cloneAddressNavigationState(value) {
|
||
if (!value) {
|
||
return null;
|
||
}
|
||
return {
|
||
schema_version: value.schema_version,
|
||
session_id: value.session_id,
|
||
updated_at: value.updated_at,
|
||
session_context: {
|
||
active_result_set_id: value.session_context.active_result_set_id,
|
||
active_focus_object: cloneFocusObject(value.session_context.active_focus_object),
|
||
last_confirmed_route: value.session_context.last_confirmed_route,
|
||
date_scope: {
|
||
as_of_date: value.session_context.date_scope.as_of_date,
|
||
period_from: value.session_context.date_scope.period_from,
|
||
period_to: value.session_context.date_scope.period_to
|
||
},
|
||
organization_scope: value.session_context.organization_scope
|
||
},
|
||
result_sets: value.result_sets.map(cloneResultSet),
|
||
navigation_history: value.navigation_history.map(cloneNavigationEvent)
|
||
};
|
||
}
|
||
function normalizeAddressNavigationState(value, sessionId) {
|
||
const fallback = createEmptyAddressNavigationState(sessionId);
|
||
if (!value || typeof value !== "object") {
|
||
return fallback;
|
||
}
|
||
const normalizedSessionId = toNonEmptyString(value.session_id) ?? sessionId;
|
||
const normalizedUpdatedAt = toNonEmptyString(value.updated_at) ?? new Date().toISOString();
|
||
const context = toObject(value.session_context) ?? {};
|
||
const dateScope = toObject(context.date_scope) ?? {};
|
||
const resultSets = Array.isArray(value.result_sets) ? value.result_sets : [];
|
||
const navigationHistory = Array.isArray(value.navigation_history) ? value.navigation_history : [];
|
||
return {
|
||
schema_version: addressNavigation_1.ADDRESS_NAVIGATION_STATE_SCHEMA_VERSION,
|
||
session_id: normalizedSessionId,
|
||
updated_at: normalizedUpdatedAt,
|
||
session_context: {
|
||
active_result_set_id: toNonEmptyString(context.active_result_set_id),
|
||
active_focus_object: cloneFocusObject(context.active_focus_object),
|
||
last_confirmed_route: toNonEmptyString(context.last_confirmed_route),
|
||
date_scope: {
|
||
as_of_date: toNonEmptyString(dateScope.as_of_date),
|
||
period_from: toNonEmptyString(dateScope.period_from),
|
||
period_to: toNonEmptyString(dateScope.period_to)
|
||
},
|
||
organization_scope: toNonEmptyString(context.organization_scope)
|
||
},
|
||
result_sets: resultSets
|
||
.map((item) => toObject(item))
|
||
.filter((item) => item !== null)
|
||
.map((item) => ({
|
||
result_set_id: toNonEmptyString(item.result_set_id) ?? `rs-${(0, nanoid_1.nanoid)(10)}`,
|
||
type: inferResultSetType(toAddressIntent(item.intent)),
|
||
intent: toAddressIntent(item.intent),
|
||
route_id: toNonEmptyString(item.route_id),
|
||
filters: normalizeFilters(item.filters),
|
||
source_refs: Array.isArray(item.source_refs)
|
||
? item.source_refs.map((v) => toNonEmptyString(v)).filter((v) => Boolean(v))
|
||
: [],
|
||
entity_refs: Array.isArray(item.entity_refs)
|
||
? item.entity_refs
|
||
.map((entry) => toObject(entry))
|
||
.filter((entry) => entry !== null)
|
||
.map((entry) => ({
|
||
index: Number.isFinite(Number(entry.index)) ? Number(entry.index) : 0,
|
||
entity_type: toAddressFocusObjectType(entry.entity_type),
|
||
value: toNonEmptyString(entry.value) ?? ""
|
||
}))
|
||
.filter((entry) => entry.index > 0 && entry.value.length > 0)
|
||
: [],
|
||
created_from_turn: Number.isFinite(Number(item.created_from_turn)) ? Number(item.created_from_turn) : 0,
|
||
created_at: toNonEmptyString(item.created_at) ?? normalizedUpdatedAt
|
||
})),
|
||
navigation_history: navigationHistory
|
||
.map((item) => toObject(item))
|
||
.filter((item) => item !== null)
|
||
.map((item) => ({
|
||
event_id: toNonEmptyString(item.event_id) ?? `nav-${(0, nanoid_1.nanoid)(10)}`,
|
||
action: toNonEmptyString(item.action) ?? "open",
|
||
source_result_set_id: toNonEmptyString(item.source_result_set_id),
|
||
target_object_id: toNonEmptyString(item.target_object_id),
|
||
derived_result_set_id: toNonEmptyString(item.derived_result_set_id),
|
||
turn_index: Number.isFinite(Number(item.turn_index)) ? Number(item.turn_index) : 0,
|
||
created_at: toNonEmptyString(item.created_at) ?? normalizedUpdatedAt
|
||
}))
|
||
};
|
||
}
|
||
function evolveAddressNavigationStateWithAssistantItem(state, item, turnIndex) {
|
||
if (!isAddressAssistantItem(item) || !item.debug) {
|
||
return state;
|
||
}
|
||
const debug = item.debug;
|
||
const intent = toAddressIntent(debug.detected_intent);
|
||
if (intent === "unknown") {
|
||
return state;
|
||
}
|
||
const createdAt = toNonEmptyString(item.created_at) ?? new Date().toISOString();
|
||
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 = {
|
||
result_set_id: resultSetId,
|
||
type: inferResultSetType(intent),
|
||
intent,
|
||
route_id: routeId,
|
||
filters: filtersWithDerivedScope,
|
||
source_refs: sourceRefs,
|
||
entity_refs: entityRefs,
|
||
created_from_turn: turnIndex,
|
||
created_at: createdAt
|
||
};
|
||
const previousResultSetId = state.session_context.active_result_set_id;
|
||
const focusObject = buildFocusObjectFromDebug(debug, resultSetId, createdAt);
|
||
const action = resolveNavigationAction(debug, Boolean(focusObject));
|
||
const navigationEvent = {
|
||
event_id: `nav-${(0, nanoid_1.nanoid)(10)}`,
|
||
action,
|
||
source_result_set_id: previousResultSetId,
|
||
target_object_id: focusObject?.object_id ?? null,
|
||
derived_result_set_id: resultSetId,
|
||
turn_index: turnIndex,
|
||
created_at: createdAt
|
||
};
|
||
const normalizedDateScope = {
|
||
as_of_date: toNonEmptyString(filtersWithDerivedScope.as_of_date),
|
||
period_from: toNonEmptyString(filtersWithDerivedScope.period_from),
|
||
period_to: toNonEmptyString(filtersWithDerivedScope.period_to)
|
||
};
|
||
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 nextEvents = capNavigationEvents([...state.navigation_history, navigationEvent]);
|
||
const nextSessionContext = action === "open"
|
||
? {
|
||
active_result_set_id: resultSetId,
|
||
active_focus_object: focusObject ?? null,
|
||
last_confirmed_route: routeId ?? null,
|
||
date_scope: {
|
||
as_of_date: normalizedDateScope.as_of_date,
|
||
period_from: normalizedDateScope.period_from,
|
||
period_to: normalizedDateScope.period_to
|
||
},
|
||
organization_scope: organizationScope ?? state.session_context.organization_scope
|
||
}
|
||
: {
|
||
active_result_set_id: resultSetId,
|
||
active_focus_object: focusObject ?? state.session_context.active_focus_object,
|
||
last_confirmed_route: routeId ?? state.session_context.last_confirmed_route,
|
||
date_scope: {
|
||
as_of_date: normalizedDateScope.as_of_date ?? state.session_context.date_scope.as_of_date,
|
||
period_from: normalizedDateScope.period_from ?? state.session_context.date_scope.period_from,
|
||
period_to: normalizedDateScope.period_to ?? state.session_context.date_scope.period_to
|
||
},
|
||
organization_scope: organizationScope ?? state.session_context.organization_scope
|
||
};
|
||
return {
|
||
...state,
|
||
updated_at: createdAt,
|
||
session_context: nextSessionContext,
|
||
result_sets: nextResultSets,
|
||
navigation_history: nextEvents
|
||
};
|
||
}
|