NODEDC_1C/llm_normalizer/backend/dist/services/addressNavigationState.js

473 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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
};
}