Усилить семантический контур агента и большой прогон
This commit is contained in:
@@ -2152,6 +2152,35 @@ function hasBidirectionalValueFlowComparisonSignal(text: string): boolean {
|
||||
return hasIncomingCue && hasOutgoingCue && hasComparisonCue && (hasValueFlowCue || hasNetAmountCue);
|
||||
}
|
||||
|
||||
function countBroadBusinessOverviewBridgeAxes(text: string): number {
|
||||
const axisPatterns = [
|
||||
/(?:\u0434\u0435\u043d\p{L}*|\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\p{L}*|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*|\u043f\u043b\u0430\u0442\p{L}*|money|cash|revenue|turnover)/iu,
|
||||
/(?:\u043d\u0434\u0441|vat)/iu,
|
||||
/(?:\u0434\u043e\u043b\p{L}*|\u0434\u0435\u0431\u0438\u0442\u043e\u0440\p{L}*|\u043a\u0440\u0435\u0434\u0438\u0442\u043e\u0440\p{L}*|receivable|payable|debt)/iu,
|
||||
/(?:\u0441\u043a\u043b\u0430\u0434|\u043e\u0441\u0442\u0430\u0442|\u0437\u0430\u043f\u0430\u0441|\u0442\u043e\u0432\u0430\u0440|warehouse|stock|inventory)/iu,
|
||||
/(?:\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b|\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|customer|client|buyer)/iu,
|
||||
/(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0432\u0435\u043d\u0434\u043e\u0440|\u0437\u0430\u043a\u0443\u043f|supplier|vendor|procurement)/iu,
|
||||
/(?:\u0433\u0434\u0435[\s\S]{0,80}(?:\u043d\u0435\u043b\u044c\u0437\u044f|\u043d\u0435\s+\u0434\u0435\u043b\u0430\p{L}*|\u043d\u0435\s+\u0445\u0432\u0430\p{L}*)|\u0447\u0442\u043e[\s\S]{0,80}(?:\u043d\u0435\u043b\u044c\u0437\u044f|\u043d\u0435\s+\u0445\u0432\u0430\p{L}*)|cannot|unknown|missing|limitation)/iu
|
||||
];
|
||||
return axisPatterns.reduce((count, pattern) => count + (pattern.test(text) ? 1 : 0), 0);
|
||||
}
|
||||
|
||||
function hasBroadBusinessOverviewBridgeSignal(text: string): boolean {
|
||||
const normalized = String(text ?? "").trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const hasBroadCue =
|
||||
/(?:\u043f\u043e[-\s]*\u0447\u0435\u043b\u043e\u0432\u0435\u0447\u0435\u0441\u043a\p{L}*|\u0447\u0442\u043e\s+\u043f\u043e\s+\u0431\u0438\u0437\u043d\u0435\u0441\u0443\s+\u0432\u0438\u0434\u043d\p{L}*|\u043f\u043e\u0441\u043c\u043e\u0442\p{L}*[\s\S]{0,100}(?:\u0431\u0438\u0437\u043d\u0435\u0441|\u0434\u0435\u044f\u0442\u0435\u043b\p{L}*)|\u0431\u0438\u0437\u043d\u0435\u0441[\s\S]{0,80}(?:\u0432\u0438\u0434\u043d\p{L}*|\u0432\u044b\u0432\u043e\u0434|\u0441\u0440\u0435\u0437)|human\s+readable\s+business\s+view)/iu.test(
|
||||
normalized
|
||||
);
|
||||
const hasCompanyScope =
|
||||
/(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e|\u043e\u0430\u043e|\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0431\u0438\u0437\u043d\u0435\u0441|\u0432\s+1\s?\u0441|1\s?c|company|organization|business|(?:19|20)\d{2})/iu.test(
|
||||
normalized
|
||||
);
|
||||
return hasBroadCue && hasCompanyScope && countBroadBusinessOverviewBridgeAxes(normalized) >= 3;
|
||||
}
|
||||
|
||||
function hasNomenclatureMarginRankingSignal(text: string): boolean {
|
||||
const normalized = String(text ?? "").trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
@@ -2357,6 +2386,10 @@ function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolutio
|
||||
);
|
||||
}
|
||||
|
||||
if (hasBroadBusinessOverviewBridgeSignal(normalized)) {
|
||||
return unicodeBridgeResolution("unknown", "high", "unicode_business_overview_multi_surface_deferred_to_discovery");
|
||||
}
|
||||
|
||||
if (hasOrganizationLevelEarningsOverviewBridgeSignal(normalized)) {
|
||||
return unicodeBridgeResolution("unknown", "high", "unicode_business_overview_earnings_deferred_to_discovery");
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ import type { AssistantConversationItem } from "../types/assistant";
|
||||
import type { AddressIntent } from "../types/addressQuery";
|
||||
import {
|
||||
readAddressDebugCounterparty,
|
||||
readAddressDebugItem
|
||||
readAddressDebugItem,
|
||||
readAddressDebugOrganization,
|
||||
readAddressDebugTemporalScope
|
||||
} from "./assistantContinuityPolicy";
|
||||
import {
|
||||
ADDRESS_NAVIGATION_STATE_SCHEMA_VERSION,
|
||||
@@ -21,7 +23,11 @@ const MAX_RESULT_SETS = 40;
|
||||
const MAX_NAVIGATION_EVENTS = 120;
|
||||
const MAX_ENTITY_REFS_PER_RESULT_SET = 40;
|
||||
|
||||
type AddressComparisonScope = NonNullable<AddressNavigationState["session_context"]["comparison_scope"]>;
|
||||
type AddressComparisonProofBundles = NonNullable<AddressComparisonScope["proof_bundles"]>;
|
||||
|
||||
const DISPLAY_ENTITY_TYPE_BY_INTENT: Partial<Record<AddressIntent, AddressFocusObjectType>> = {
|
||||
business_overview: "organization",
|
||||
counterparty_activity_lifecycle: "counterparty",
|
||||
customer_revenue_and_payments: "counterparty",
|
||||
supplier_payouts_profile: "counterparty",
|
||||
@@ -45,6 +51,7 @@ const DISPLAY_ENTITY_TYPE_BY_INTENT: Partial<Record<AddressIntent, AddressFocusO
|
||||
};
|
||||
|
||||
const RESULT_SET_TYPE_BY_INTENT: Partial<Record<AddressIntent, AddressResultSetType>> = {
|
||||
business_overview: "profile_summary",
|
||||
counterparty_activity_lifecycle: "counterparty_list",
|
||||
customer_revenue_and_payments: "counterparty_list",
|
||||
supplier_payouts_profile: "counterparty_list",
|
||||
@@ -83,6 +90,18 @@ function toObject(value: unknown): Record<string, unknown> | null {
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function cloneRecord(value: unknown): Record<string, unknown> | null {
|
||||
const record = toObject(value);
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(record)) as Record<string, unknown>;
|
||||
} catch {
|
||||
return { ...record };
|
||||
}
|
||||
}
|
||||
|
||||
function toNonEmptyString(value: unknown): string | null {
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
@@ -91,6 +110,44 @@ function toNonEmptyString(value: unknown): string | null {
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
|
||||
function candidateLabel(value: unknown): string | null {
|
||||
const direct = toNonEmptyString(value);
|
||||
if (direct && direct !== "[object Object]") {
|
||||
return direct;
|
||||
}
|
||||
const record = toObject(value);
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
toNonEmptyString(record.value) ??
|
||||
toNonEmptyString(record.name) ??
|
||||
toNonEmptyString(record.ref) ??
|
||||
toNonEmptyString(record.text)
|
||||
);
|
||||
}
|
||||
|
||||
function readNavigationDiscoveryCounterparty(debug: Record<string, unknown>): string | null {
|
||||
const entry = toObject(debug.assistant_mcp_discovery_entry_point_v1);
|
||||
const turnInput = toObject(entry?.turn_input);
|
||||
const turnMeaning = toObject(turnInput?.turn_meaning_ref);
|
||||
const dataNeedGraph = toObject(turnInput?.data_need_graph);
|
||||
const candidates = [
|
||||
...(Array.isArray(turnMeaning?.business_overview_separate_entity_candidates)
|
||||
? turnMeaning.business_overview_separate_entity_candidates
|
||||
: []),
|
||||
...(Array.isArray(turnMeaning?.explicit_entity_candidates) ? turnMeaning.explicit_entity_candidates : []),
|
||||
...(Array.isArray(dataNeedGraph?.subject_candidates) ? dataNeedGraph.subject_candidates : [])
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
const label = candidateLabel(candidate);
|
||||
if (label) {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function toAddressFocusObjectType(value: unknown): AddressFocusObjectType {
|
||||
const normalized = toNonEmptyString(value);
|
||||
if (!normalized) {
|
||||
@@ -228,6 +285,26 @@ function cloneFocusObject(value: AddressFocusObject | null): AddressFocusObject
|
||||
};
|
||||
}
|
||||
|
||||
function cloneComparisonProofBundles(value: unknown): AddressComparisonProofBundles | null {
|
||||
const record = toObject(value);
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
const counterpartyValueFlowBundle = cloneRecord(
|
||||
record.counterparty_value_flow_bundle ?? record.previous_counterparty_value_flow_bundle
|
||||
);
|
||||
const counterpartyDocumentBundle = cloneRecord(
|
||||
record.counterparty_document_bundle ?? record.previous_counterparty_document_bundle
|
||||
);
|
||||
if (!counterpartyValueFlowBundle && !counterpartyDocumentBundle) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
counterparty_value_flow_bundle: counterpartyValueFlowBundle,
|
||||
counterparty_document_bundle: counterpartyDocumentBundle
|
||||
};
|
||||
}
|
||||
|
||||
function cloneResultSet(input: AddressResultSet): AddressResultSet {
|
||||
return {
|
||||
result_set_id: input.result_set_id,
|
||||
@@ -296,8 +373,71 @@ function buildFocusObject(
|
||||
};
|
||||
}
|
||||
|
||||
function cloneComparisonScope(
|
||||
value: AddressNavigationState["session_context"]["comparison_scope"]
|
||||
): AddressNavigationState["session_context"]["comparison_scope"] {
|
||||
if (!value || typeof value !== "object") {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
organization: cloneFocusObject(value.organization),
|
||||
counterparty: cloneFocusObject(value.counterparty),
|
||||
proof_bundles: cloneComparisonProofBundles(value.proof_bundles)
|
||||
};
|
||||
}
|
||||
|
||||
function sameBusinessLabel(left: unknown, right: unknown): boolean {
|
||||
const normalizedLeft = toNonEmptyString(left)?.toLocaleLowerCase("ru-RU");
|
||||
const normalizedRight = toNonEmptyString(right)?.toLocaleLowerCase("ru-RU");
|
||||
return Boolean(normalizedLeft && normalizedRight && normalizedLeft === normalizedRight);
|
||||
}
|
||||
|
||||
function readBusinessOverviewComparisonProofBundles(debug: Record<string, unknown>): AddressComparisonProofBundles | null {
|
||||
const entryPoint = toObject(debug.assistant_mcp_discovery_entry_point_v1);
|
||||
const turnInput = toObject(entryPoint?.turn_input);
|
||||
const turnMeaning = toObject(turnInput?.turn_meaning_ref);
|
||||
const bridge = toObject(entryPoint?.bridge);
|
||||
const pilot = toObject(bridge?.pilot);
|
||||
const counterpartyValueFlowBundle =
|
||||
cloneRecord(turnMeaning?.previous_counterparty_value_flow_bundle) ??
|
||||
cloneRecord(pilot?.derived_bidirectional_value_flow);
|
||||
const counterpartyDocumentBundle = cloneRecord(turnMeaning?.previous_counterparty_document_bundle);
|
||||
const counterparty =
|
||||
toNonEmptyString(counterpartyValueFlowBundle?.counterparty) ??
|
||||
toNonEmptyString(counterpartyDocumentBundle?.counterparty) ??
|
||||
readNavigationDiscoveryCounterparty(debug);
|
||||
if (!counterparty) {
|
||||
return null;
|
||||
}
|
||||
if (!counterpartyValueFlowBundle && !counterpartyDocumentBundle) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
counterparty_value_flow_bundle: counterpartyValueFlowBundle,
|
||||
counterparty_document_bundle: counterpartyDocumentBundle
|
||||
};
|
||||
}
|
||||
|
||||
function buildFocusObjectFromDebug(debug: Record<string, unknown>, resultSetId: string, createdAt: string): AddressFocusObject | null {
|
||||
const extractedFilters = toObject(debug.extracted_filters) ?? {};
|
||||
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
|
||||
if (selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true) {
|
||||
const counterparty = readAddressDebugCounterparty(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug);
|
||||
if (counterparty) {
|
||||
return buildFocusObject("counterparty", counterparty, resultSetId, createdAt);
|
||||
}
|
||||
const organization = readAddressDebugOrganization(debug, toNonEmptyString);
|
||||
if (organization) {
|
||||
return buildFocusObject("organization", organization, resultSetId, createdAt);
|
||||
}
|
||||
}
|
||||
if (selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true) {
|
||||
const counterparty =
|
||||
readAddressDebugCounterparty(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug);
|
||||
if (counterparty) {
|
||||
return buildFocusObject("counterparty", counterparty, resultSetId, createdAt);
|
||||
}
|
||||
}
|
||||
const objectType = toAddressFocusObjectType(debug.anchor_type);
|
||||
const canonicalType = objectType === "unknown" ? inferDisplayEntityType(toAddressIntent(debug.detected_intent)) : objectType;
|
||||
if (canonicalType === "item") {
|
||||
@@ -330,11 +470,13 @@ function capNavigationEvents(events: AddressNavigationEvent[]): AddressNavigatio
|
||||
}
|
||||
|
||||
function isAddressAssistantItem(item: AssistantConversationItem): boolean {
|
||||
return (
|
||||
item.role === "assistant" &&
|
||||
Boolean(item.debug) &&
|
||||
toNonEmptyString(item.debug?.detected_mode) === "address_query"
|
||||
);
|
||||
if (item.role !== "assistant" || !item.debug) {
|
||||
return false;
|
||||
}
|
||||
if (toNonEmptyString(item.debug.detected_mode) === "address_query") {
|
||||
return true;
|
||||
}
|
||||
return item.debug.mcp_discovery_response_applied === true && Boolean(toNonEmptyString(item.debug.mcp_discovery_selected_chain_id));
|
||||
}
|
||||
|
||||
export function createEmptyAddressNavigationState(
|
||||
@@ -348,6 +490,7 @@ export function createEmptyAddressNavigationState(
|
||||
session_context: {
|
||||
active_result_set_id: null,
|
||||
active_focus_object: null,
|
||||
comparison_scope: null,
|
||||
last_confirmed_route: null,
|
||||
date_scope: {
|
||||
as_of_date: null,
|
||||
@@ -372,6 +515,7 @@ export function cloneAddressNavigationState(value: AddressNavigationState | null
|
||||
session_context: {
|
||||
active_result_set_id: value.session_context.active_result_set_id,
|
||||
active_focus_object: cloneFocusObject(value.session_context.active_focus_object),
|
||||
comparison_scope: cloneComparisonScope(value.session_context.comparison_scope),
|
||||
last_confirmed_route: value.session_context.last_confirmed_route,
|
||||
date_scope: {
|
||||
as_of_date: value.session_context.date_scope.as_of_date,
|
||||
@@ -406,6 +550,9 @@ export function normalizeAddressNavigationState(
|
||||
session_context: {
|
||||
active_result_set_id: toNonEmptyString(context.active_result_set_id),
|
||||
active_focus_object: cloneFocusObject(context.active_focus_object as AddressFocusObject | null),
|
||||
comparison_scope: cloneComparisonScope(
|
||||
context.comparison_scope as AddressNavigationState["session_context"]["comparison_scope"]
|
||||
),
|
||||
last_confirmed_route: toNonEmptyString(context.last_confirmed_route),
|
||||
date_scope: {
|
||||
as_of_date: toNonEmptyString(dateScope.as_of_date),
|
||||
@@ -464,20 +611,42 @@ export function evolveAddressNavigationStateWithAssistantItem(
|
||||
return state;
|
||||
}
|
||||
const debug = item.debug as unknown as Record<string, unknown>;
|
||||
const intent = toAddressIntent(debug.detected_intent);
|
||||
if (intent === "unknown") {
|
||||
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
|
||||
const discoveryIntent =
|
||||
selectedDiscoveryChain === "business_overview"
|
||||
? "business_overview"
|
||||
: selectedDiscoveryChain === "value_flow_comparison"
|
||||
? "customer_revenue_and_payments"
|
||||
: "unknown";
|
||||
const detectedIntent = toNonEmptyString(debug.detected_intent);
|
||||
const intent = toAddressIntent(detectedIntent && detectedIntent !== "unknown" ? detectedIntent : discoveryIntent);
|
||||
const trackableDiscoveryTurn = debug.mcp_discovery_response_applied === true && Boolean(selectedDiscoveryChain);
|
||||
if (intent === "unknown" && !trackableDiscoveryTurn) {
|
||||
return state;
|
||||
}
|
||||
const createdAt = toNonEmptyString(item.created_at) ?? new Date().toISOString();
|
||||
const resultSetId = `rs-${item.message_id}`;
|
||||
const routeId = toNonEmptyString(debug.selected_recipe);
|
||||
const routeId = toNonEmptyString(debug.selected_recipe) ?? selectedDiscoveryChain;
|
||||
const filters = normalizeFilters(debug.extracted_filters);
|
||||
const derivedOrganizationScope = resolveDerivedOrganizationScope(debug, filters, item.text);
|
||||
const derivedOrganizationScope =
|
||||
resolveDerivedOrganizationScope(debug, filters, item.text) ?? readAddressDebugOrganization(debug, toNonEmptyString);
|
||||
const derivedCounterpartyScope =
|
||||
selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true
|
||||
? readAddressDebugCounterparty(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug)
|
||||
: null;
|
||||
const filtersWithDerivedScope =
|
||||
derivedOrganizationScope && !toNonEmptyString(filters.organization)
|
||||
? {
|
||||
...filters,
|
||||
organization: derivedOrganizationScope
|
||||
organization: derivedOrganizationScope,
|
||||
...(derivedCounterpartyScope && !toNonEmptyString(filters.counterparty)
|
||||
? { counterparty: derivedCounterpartyScope }
|
||||
: {})
|
||||
}
|
||||
: derivedCounterpartyScope && !toNonEmptyString(filters.counterparty)
|
||||
? {
|
||||
...filters,
|
||||
counterparty: derivedCounterpartyScope
|
||||
}
|
||||
: filters;
|
||||
const sourceRefs = routeId ? [routeId] : [];
|
||||
@@ -495,6 +664,41 @@ export function evolveAddressNavigationStateWithAssistantItem(
|
||||
};
|
||||
const previousResultSetId = state.session_context.active_result_set_id;
|
||||
const focusObject = buildFocusObjectFromDebug(debug, resultSetId, createdAt);
|
||||
const comparisonCounterparty =
|
||||
selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
|
||||
? readNavigationDiscoveryCounterparty(debug)
|
||||
: null;
|
||||
const comparisonOrganization =
|
||||
selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
|
||||
? derivedOrganizationScope ?? toNonEmptyString(filtersWithDerivedScope.organization)
|
||||
: null;
|
||||
const currentComparisonProofBundles =
|
||||
selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
|
||||
? readBusinessOverviewComparisonProofBundles(debug)
|
||||
: null;
|
||||
const inheritedComparisonScope = state.session_context.comparison_scope;
|
||||
const inheritedComparisonProofBundles =
|
||||
comparisonCounterparty &&
|
||||
sameBusinessLabel(inheritedComparisonScope?.counterparty?.label, comparisonCounterparty)
|
||||
? cloneComparisonProofBundles(inheritedComparisonScope?.proof_bundles)
|
||||
: null;
|
||||
const comparisonProofBundles = currentComparisonProofBundles ?? inheritedComparisonProofBundles;
|
||||
const comparisonOrganizationObject =
|
||||
comparisonOrganization
|
||||
? buildFocusObject("organization", comparisonOrganization, resultSetId, createdAt)
|
||||
: cloneFocusObject(inheritedComparisonScope?.organization ?? null);
|
||||
const comparisonCounterpartyObject =
|
||||
comparisonCounterparty
|
||||
? buildFocusObject("counterparty", comparisonCounterparty, resultSetId, createdAt)
|
||||
: cloneFocusObject(inheritedComparisonScope?.counterparty ?? null);
|
||||
const comparisonScope =
|
||||
comparisonOrganizationObject || comparisonCounterpartyObject || comparisonProofBundles
|
||||
? {
|
||||
organization: comparisonOrganizationObject,
|
||||
counterparty: comparisonCounterpartyObject,
|
||||
proof_bundles: comparisonProofBundles
|
||||
}
|
||||
: null;
|
||||
const action = resolveNavigationAction(debug, Boolean(focusObject));
|
||||
const navigationEvent: AddressNavigationEvent = {
|
||||
event_id: `nav-${nanoid(10)}`,
|
||||
@@ -505,10 +709,11 @@ export function evolveAddressNavigationStateWithAssistantItem(
|
||||
turn_index: turnIndex,
|
||||
created_at: createdAt
|
||||
};
|
||||
const discoveryTemporalScope = readAddressDebugTemporalScope(debug, toNonEmptyString);
|
||||
const normalizedDateScope = {
|
||||
as_of_date: toNonEmptyString(filtersWithDerivedScope.as_of_date),
|
||||
period_from: toNonEmptyString(filtersWithDerivedScope.period_from),
|
||||
period_to: toNonEmptyString(filtersWithDerivedScope.period_to)
|
||||
as_of_date: toNonEmptyString(filtersWithDerivedScope.as_of_date) ?? discoveryTemporalScope.asOfDate,
|
||||
period_from: toNonEmptyString(filtersWithDerivedScope.period_from) ?? discoveryTemporalScope.periodFrom,
|
||||
period_to: toNonEmptyString(filtersWithDerivedScope.period_to) ?? discoveryTemporalScope.periodTo
|
||||
};
|
||||
const organizationScope = toNonEmptyString(filtersWithDerivedScope.organization);
|
||||
const nextResultSets = capResultSets(
|
||||
@@ -522,6 +727,7 @@ export function evolveAddressNavigationStateWithAssistantItem(
|
||||
? {
|
||||
active_result_set_id: resultSetId,
|
||||
active_focus_object: focusObject ?? null,
|
||||
comparison_scope: comparisonScope,
|
||||
last_confirmed_route: routeId ?? null,
|
||||
date_scope: {
|
||||
as_of_date: normalizedDateScope.as_of_date,
|
||||
@@ -533,6 +739,7 @@ export function evolveAddressNavigationStateWithAssistantItem(
|
||||
: {
|
||||
active_result_set_id: resultSetId,
|
||||
active_focus_object: focusObject ?? state.session_context.active_focus_object,
|
||||
comparison_scope: comparisonScope ?? state.session_context.comparison_scope,
|
||||
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,
|
||||
|
||||
@@ -733,6 +733,44 @@ function needsVatCalendarDetails(userMessage: string | null | undefined): boolea
|
||||
return /(?:срок|когда|дата\s+уплат|декларац|дол(?:я|ями)|по\s+частям|платежн(?:ый|ого)\s+график)/iu.test(text);
|
||||
}
|
||||
|
||||
function needsVatPurchaseDateAnchorDisclosure(userMessage: string | null | undefined): boolean {
|
||||
const text = normalizeQuestionText(userMessage);
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
return /(?:дата|дату|дате|момент)\s+(?:покуп|закуп)|(?:покуп|закуп)\S*\s+(?:дат|момент)|purchase\s+date|date\s+of\s+purchase/iu.test(
|
||||
text
|
||||
);
|
||||
}
|
||||
|
||||
function buildVatPurchaseDateAnchorDisclosureLine(
|
||||
options: ComposeFactualReplyOptions,
|
||||
periodWindowLabel: string | null
|
||||
): string | null {
|
||||
if (!periodWindowLabel || !needsVatPurchaseDateAnchorDisclosure(options.userMessage)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
|
||||
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
|
||||
const periodTo = normalizeIsoDateOnly(options.periodTo);
|
||||
const asOfTs = toUtcDayTimestamp(asOfDate);
|
||||
const fromTs = toUtcDayTimestamp(periodFrom);
|
||||
const toTs = toUtcDayTimestamp(periodTo);
|
||||
if (
|
||||
asOfDate &&
|
||||
asOfTs !== null &&
|
||||
fromTs !== null &&
|
||||
toTs !== null &&
|
||||
asOfTs >= fromTs &&
|
||||
asOfTs <= toTs
|
||||
) {
|
||||
return `- Якорь периода: дата покупки ${formatDateRu(asOfDate)} попадает в налоговый период ${periodWindowLabel}; поэтому расчет ниже взят из книг продаж/покупок за это окно.`;
|
||||
}
|
||||
|
||||
return `- Якорь периода: дата покупки из вопроса/контекста использована для выбора налогового периода ${periodWindowLabel}; сам расчет ниже взят из книг продаж/покупок за это окно.`;
|
||||
}
|
||||
|
||||
function detectRankingLimit(userMessage: string | null | undefined, fallback = 20): number {
|
||||
const text = normalizeQuestionText(userMessage);
|
||||
if (!text) {
|
||||
@@ -3896,6 +3934,7 @@ function composeFactualReplyBody(
|
||||
const formatConfirmedMoney = (value: number): string => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
||||
const organizationLabel = normalizeOrganizationScopeValue(options.organizationHint);
|
||||
const organizationScopeLabel = organizationLabel ? ` по организации ${organizationLabel}` : "";
|
||||
const purchaseDateAnchorLine = buildVatPurchaseDateAnchorDisclosureLine(options, periodWindowLabel);
|
||||
|
||||
const lines = [
|
||||
`Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`,
|
||||
@@ -3904,6 +3943,7 @@ function composeFactualReplyBody(
|
||||
"Что вошло в расчет:",
|
||||
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
|
||||
`- Налоговый период расчета: ${periodWindowLabel ?? "не задан (нужен явный период)"}.`,
|
||||
...(purchaseDateAnchorLine ? [purchaseDateAnchorLine] : []),
|
||||
`- НДС по книге продаж: ${formatConfirmedMoney(salesVat)}.`,
|
||||
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
|
||||
`- Нетто НДС (книга продаж - книга покупок): ${formatConfirmedMoney(netVat)}.`
|
||||
|
||||
@@ -76,6 +76,141 @@ function normalizeAddressReplyType(value: unknown): AssistantReplyType {
|
||||
return value === "factual" || value === "partial_coverage" ? value : "partial_coverage";
|
||||
}
|
||||
|
||||
function sameBusinessLabel(left: unknown, right: unknown): boolean {
|
||||
const normalizedLeft = toNullableString(left)?.toLocaleLowerCase("ru-RU").replace(/ё/g, "е");
|
||||
const normalizedRight = toNullableString(right)?.toLocaleLowerCase("ru-RU").replace(/ё/g, "е");
|
||||
return Boolean(
|
||||
normalizedLeft &&
|
||||
normalizedRight &&
|
||||
(normalizedLeft === normalizedRight ||
|
||||
normalizedLeft.includes(normalizedRight) ||
|
||||
normalizedRight.includes(normalizedLeft))
|
||||
);
|
||||
}
|
||||
|
||||
function firstString(values: unknown[]): string | null {
|
||||
for (const value of values) {
|
||||
const text = toNullableString(value);
|
||||
if (text) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function legalOrganizationLabelFromClarification(value: unknown): string | null {
|
||||
const text = toNullableString(value);
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
const compact = text.replace(/\s+/gu, " ").replace(/[.!?]+$/u, "").trim();
|
||||
if (compact.length > 120 || !/^(?:ООО|ПАО|АО|ИП)\s+\S/iu.test(compact)) {
|
||||
return null;
|
||||
}
|
||||
return compact;
|
||||
}
|
||||
|
||||
function cleanComparisonScopeCompanyLine(line: string, organization: string | null): string {
|
||||
let clean = String(line ?? "")
|
||||
.replace(/\bcompany-level\b/giu, "общий по компании")
|
||||
.replace(/\breusable bundle\b/giu, "сохраненный подтвержденный срез");
|
||||
if (organization) {
|
||||
clean = clean.replace(/по компании\s+Альтернатива Плюс/iu, `по компании ${organization}`);
|
||||
}
|
||||
return clean.trim();
|
||||
}
|
||||
|
||||
function buildComparisonScopeProofReply(input: {
|
||||
baseReply: string;
|
||||
debug: Record<string, unknown>;
|
||||
session: unknown;
|
||||
userMessage?: unknown;
|
||||
}): { reply: string; audit: Record<string, unknown> } | null {
|
||||
const entryPoint = toRecordObject(input.debug.assistant_mcp_discovery_entry_point_v1);
|
||||
const turnInput = toRecordObject(entryPoint?.turn_input);
|
||||
const turnMeaning = toRecordObject(turnInput?.turn_meaning_ref);
|
||||
const isBusinessOverview = toNullableString(turnMeaning?.asked_domain_family) === "business_overview";
|
||||
if (!isBusinessOverview) {
|
||||
return null;
|
||||
}
|
||||
const separateCandidates = Array.isArray(turnMeaning?.business_overview_separate_entity_candidates)
|
||||
? turnMeaning.business_overview_separate_entity_candidates
|
||||
: [];
|
||||
const separateSubject = firstString([...separateCandidates, turnMeaning?.metadata_scope_hint]);
|
||||
if (!separateSubject) {
|
||||
return null;
|
||||
}
|
||||
const sessionRecord = toRecordObject(input.session);
|
||||
const addressNavigationState = toRecordObject(sessionRecord?.address_navigation_state);
|
||||
const sessionContext = toRecordObject(addressNavigationState?.session_context);
|
||||
const comparisonScope = toRecordObject(sessionContext?.comparison_scope);
|
||||
const comparisonCounterparty = toRecordObject(comparisonScope?.counterparty);
|
||||
const proofBundles = toRecordObject(comparisonScope?.proof_bundles);
|
||||
const valueBundle = toRecordObject(proofBundles?.counterparty_value_flow_bundle);
|
||||
const documentBundle = toRecordObject(proofBundles?.counterparty_document_bundle);
|
||||
if (!valueBundle || !sameBusinessLabel(separateSubject, valueBundle.counterparty ?? comparisonCounterparty?.label)) {
|
||||
return null;
|
||||
}
|
||||
const incoming = toRecordObject(valueBundle.incoming_customer_revenue);
|
||||
const outgoing = toRecordObject(valueBundle.outgoing_supplier_payout);
|
||||
const incomingAmount = toNullableString(incoming?.total_amount_human_ru);
|
||||
const outgoingAmount = toNullableString(outgoing?.total_amount_human_ru);
|
||||
const netAmount = toNullableString(valueBundle.net_amount_human_ru);
|
||||
if (!incomingAmount && !outgoingAmount && !netAmount) {
|
||||
return null;
|
||||
}
|
||||
const organization = legalOrganizationLabelFromClarification(input.userMessage)
|
||||
?? toNullableString(turnMeaning?.explicit_organization_scope)
|
||||
?? toNullableString(toRecordObject(comparisonScope?.organization)?.label);
|
||||
const documentCount = Number(toRecordObject(documentBundle)?.document_count);
|
||||
const documentText = Number.isFinite(documentCount) && documentCount > 0 ? `, документы: ${documentCount}` : "";
|
||||
const lines = String(input.baseReply ?? "")
|
||||
.split(/\r?\n/u)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
const companyLine = cleanComparisonScopeCompanyLine(
|
||||
lines[0] ?? `Коротко: по компании ${organization ?? "выбранной организации"} подтвержден общий денежный срез.`,
|
||||
organization
|
||||
);
|
||||
const netDirection =
|
||||
valueBundle.net_direction === "net_outgoing" ? "нетто в минус" : "нетто в нашу сторону";
|
||||
const counterparty = toNullableString(valueBundle.counterparty) ?? separateSubject;
|
||||
return {
|
||||
reply: [
|
||||
`${companyLine}; отдельно по ${counterparty}: получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}, ${netDirection} ${netAmount ?? "0 руб."}${documentText}.`,
|
||||
`Отдельно по контрагенту ${counterparty}: это ранее подтвержденный контрагентский срез, а не перенос общих сумм компании на контрагента.`,
|
||||
`Нельзя утверждать: чистую прибыль, полноценный финрезультат, юридические роли клиентов/поставщиков и выводы по ${counterparty} из общих сумм компании без отдельного контрагентского среза.`
|
||||
].join("\n"),
|
||||
audit: {
|
||||
applied: true,
|
||||
source: "address_navigation_state.comparison_scope.proof_bundles",
|
||||
counterparty,
|
||||
organization: organization ?? null,
|
||||
document_count: Number.isFinite(documentCount) && documentCount > 0 ? documentCount : null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function buildAppliedMcpDiscoveryRoutePatch(
|
||||
debug: Record<string, unknown>,
|
||||
applied: boolean
|
||||
): Record<string, unknown> {
|
||||
if (!applied) {
|
||||
return {};
|
||||
}
|
||||
const selectedChain = toNullableString(debug.mcp_discovery_selected_chain_id);
|
||||
if (selectedChain === "business_overview") {
|
||||
return {
|
||||
detected_intent: "business_overview",
|
||||
detected_intent_confidence: "high",
|
||||
selected_recipe: "business_overview",
|
||||
response_type: "LIMITED_WITH_REASON",
|
||||
mcp_discovery_effective_response_route: "business_overview"
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function normalizeAddressLaneDebug(value: unknown): AddressExecutionDebug {
|
||||
return (toRecordObject(value) ?? {}) as unknown as AddressExecutionDebug;
|
||||
}
|
||||
@@ -290,18 +425,27 @@ export function runAssistantAddressLaneResponseRuntime<ResponseType = AssistantM
|
||||
const finalAssistantReply = mcpDiscoveryResponsePolicy.applied
|
||||
? mcpDiscoveryResponsePolicy.reply_text
|
||||
: guardedResponse.assistantReply;
|
||||
const comparisonScopeProofReply = buildComparisonScopeProofReply({
|
||||
baseReply: finalAssistantReply,
|
||||
debug: debugWithResponseGuard,
|
||||
session: input.getSession(input.sessionId),
|
||||
userMessage: input.userMessage
|
||||
});
|
||||
const finalAssistantReplyWithComparisonProof = comparisonScopeProofReply?.reply ?? finalAssistantReply;
|
||||
const finalReplyType = mcpDiscoveryResponsePolicy.applied ? "partial_coverage" : guardedResponse.replyType;
|
||||
const finalDebug = {
|
||||
...debugWithResponseGuard,
|
||||
mcp_discovery_response_policy_v1: mcpDiscoveryResponsePolicy,
|
||||
mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate,
|
||||
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied
|
||||
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied,
|
||||
comparison_scope_response_augmentation_v1: comparisonScopeProofReply?.audit ?? null,
|
||||
...buildAppliedMcpDiscoveryRoutePatch(debugWithResponseGuard, mcpDiscoveryResponsePolicy.applied)
|
||||
};
|
||||
const finalization = finalizeAddressTurnSafe({
|
||||
sessionId: input.sessionId,
|
||||
userMessage: input.userMessage,
|
||||
effectiveAddressUserMessage: input.effectiveAddressUserMessage,
|
||||
assistantReply: finalAssistantReply,
|
||||
assistantReply: finalAssistantReplyWithComparisonProof,
|
||||
replyType: finalReplyType,
|
||||
addressLaneDebug: normalizeAddressLaneDebug(input.addressLane.debug),
|
||||
debug: finalDebug,
|
||||
|
||||
+338
-1
@@ -254,6 +254,332 @@ function mergeBusinessOverviewDateContextForCompactCashflow(input: {
|
||||
};
|
||||
}
|
||||
|
||||
function firstString(
|
||||
values: unknown[],
|
||||
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]
|
||||
): string | null {
|
||||
for (const value of values) {
|
||||
const text = toNonEmptyString(value);
|
||||
if (text) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function comparableEntityName(value: string | null): string | null {
|
||||
const text = compactLower(value);
|
||||
return text ? text.replace(/["'«»„“”]+/g, "") : null;
|
||||
}
|
||||
|
||||
function sameEntityHint(expected: string | null, actual: string | null): boolean {
|
||||
const left = comparableEntityName(expected);
|
||||
const right = comparableEntityName(actual);
|
||||
if (!left || !right) {
|
||||
return true;
|
||||
}
|
||||
return left === right || left.includes(right) || right.includes(left);
|
||||
}
|
||||
|
||||
function isBusinessOverviewDiscoveryFollowup(
|
||||
followupContext: Record<string, unknown> | null,
|
||||
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]
|
||||
): boolean {
|
||||
if (!followupContext) {
|
||||
return false;
|
||||
}
|
||||
return [
|
||||
followupContext.previous_discovery_pilot_scope,
|
||||
followupContext.previous_discovery_loop_selected_chain_id,
|
||||
followupContext.previous_discovery_loop_asked_domain_family,
|
||||
followupContext.previous_intent,
|
||||
followupContext.target_intent
|
||||
]
|
||||
.map((value) => toNonEmptyString(value))
|
||||
.some((value) => value === "business_overview" || value === "business_overview_route_template_v1");
|
||||
}
|
||||
|
||||
function businessOverviewCounterpartyHint(
|
||||
followupContext: Record<string, unknown>,
|
||||
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]
|
||||
): string | null {
|
||||
const previousFilters = toRecordObject(followupContext.previous_filters);
|
||||
const rootFilters = toRecordObject(followupContext.root_filters);
|
||||
return (
|
||||
toNonEmptyString(followupContext.previous_discovery_loop_metadata_scope_hint) ??
|
||||
(toNonEmptyString(followupContext.previous_anchor_type) === "counterparty"
|
||||
? toNonEmptyString(followupContext.previous_anchor_value)
|
||||
: null) ??
|
||||
toNonEmptyString(previousFilters?.counterparty) ??
|
||||
toNonEmptyString(rootFilters?.counterparty)
|
||||
);
|
||||
}
|
||||
|
||||
function turnMeaningCounterpartyName(
|
||||
turnMeaningRef: Record<string, unknown>,
|
||||
valueBundle: Record<string, unknown> | null,
|
||||
documentBundle: Record<string, unknown> | null,
|
||||
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]
|
||||
): string | null {
|
||||
const separateEntities = Array.isArray(turnMeaningRef.business_overview_separate_entity_candidates)
|
||||
? turnMeaningRef.business_overview_separate_entity_candidates
|
||||
: [];
|
||||
return (
|
||||
toNonEmptyString(valueBundle?.counterparty) ??
|
||||
toNonEmptyString(documentBundle?.counterparty) ??
|
||||
toNonEmptyString(turnMeaningRef.metadata_scope_hint) ??
|
||||
firstString(separateEntities, toNonEmptyString)
|
||||
);
|
||||
}
|
||||
|
||||
function parseBusinessOverviewProofBundlesFromText(
|
||||
value: unknown,
|
||||
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]
|
||||
): { valueBundle: Record<string, unknown> | null; documentBundle: Record<string, unknown> | null } | null {
|
||||
const text = toNonEmptyString(value);
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
const valueMatch = text.match(
|
||||
/Отдельно\s+по\s+контрагенту\s+([^:\n]+):\s*подтверждено\s+получили\s+([^,\n]+?руб\.?),\s*заплатили\s+([^,\n]+?руб\.?),\s*расчетное\s+нетто\s+в\s+нашу\s+сторону\s+([^.\n]+?руб\.?)/iu
|
||||
);
|
||||
const directDocumentMatch = text.match(/Контрагент:\s*([^.\n]+)\.\s*Найдено\s+документов:\s*(\d+)/iu);
|
||||
const summaryDocumentMatch = text.match(/документы\s+по\s+цепочке:\s*найдено\s*(\d+)/iu);
|
||||
const counterparty =
|
||||
toNonEmptyString(valueMatch?.[1]) ?? toNonEmptyString(directDocumentMatch?.[1]);
|
||||
if (!counterparty) {
|
||||
return null;
|
||||
}
|
||||
const valueBundle = valueMatch
|
||||
? {
|
||||
counterparty,
|
||||
incoming_customer_revenue: {
|
||||
total_amount_human_ru: toNonEmptyString(valueMatch[2])
|
||||
},
|
||||
outgoing_supplier_payout: {
|
||||
total_amount_human_ru: toNonEmptyString(valueMatch[3])
|
||||
},
|
||||
net_amount_human_ru: toNonEmptyString(valueMatch[4]),
|
||||
net_direction: "net_incoming",
|
||||
inference_basis: "parsed_from_previous_business_overview_summary"
|
||||
}
|
||||
: null;
|
||||
const documentCount = Number(directDocumentMatch?.[2] ?? summaryDocumentMatch?.[1]);
|
||||
const documentBundle = Number.isFinite(documentCount) && documentCount > 0
|
||||
? {
|
||||
counterparty,
|
||||
document_count: documentCount
|
||||
}
|
||||
: null;
|
||||
return valueBundle || documentBundle ? { valueBundle, documentBundle } : null;
|
||||
}
|
||||
|
||||
function parseBusinessOverviewProofBundlesFromTextV2(
|
||||
value: unknown,
|
||||
counterpartyHint: string | null,
|
||||
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]
|
||||
): { valueBundle: Record<string, unknown> | null; documentBundle: Record<string, unknown> | null } | null {
|
||||
const text = toNonEmptyString(value);
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
const comparableHint = comparableEntityName(counterpartyHint);
|
||||
const lines = text
|
||||
.split(/\r?\n/u)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
const candidateLines = comparableHint
|
||||
? lines.filter((line) => comparableEntityName(line)?.includes(comparableHint))
|
||||
: lines;
|
||||
const rubAmountPattern = /[0-9][0-9\s.,]*\s*\u0440\u0443\u0431\.?/giu;
|
||||
const valueLine = candidateLines.find((line) => (line.match(rubAmountPattern) ?? []).length >= 3) ?? null;
|
||||
const valueAmounts = valueLine?.match(rubAmountPattern) ?? [];
|
||||
const directDocumentMatch = candidateLines
|
||||
.map((line) =>
|
||||
line.match(
|
||||
/\u041a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442:\s*([^.\n]+)\.\s*\u041d\u0430\u0439\u0434\u0435\u043d\u043e\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u043e\u0432:\s*(\d+)/iu
|
||||
)
|
||||
)
|
||||
.find(Boolean);
|
||||
const summaryDocumentMatch = candidateLines
|
||||
.map((line) =>
|
||||
line.match(
|
||||
/\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b\s+\u043f\u043e\s+\u0446\u0435\u043f\u043e\u0447\u043a\u0435:\s*\u043d\u0430\u0439\u0434\u0435\u043d\u043e\s*(\d+)/iu
|
||||
)
|
||||
)
|
||||
.find(Boolean);
|
||||
const counterparty = counterpartyHint ?? toNonEmptyString(directDocumentMatch?.[1]);
|
||||
if (!counterparty) {
|
||||
return null;
|
||||
}
|
||||
const valueBundle = valueAmounts.length >= 3
|
||||
? {
|
||||
counterparty,
|
||||
incoming_customer_revenue: {
|
||||
total_amount_human_ru: toNonEmptyString(valueAmounts[0])
|
||||
},
|
||||
outgoing_supplier_payout: {
|
||||
total_amount_human_ru: toNonEmptyString(valueAmounts[1])
|
||||
},
|
||||
net_amount_human_ru: toNonEmptyString(valueAmounts[2]),
|
||||
net_direction: "net_incoming",
|
||||
inference_basis: "parsed_from_previous_business_overview_summary"
|
||||
}
|
||||
: null;
|
||||
const documentCount = Number(directDocumentMatch?.[2] ?? summaryDocumentMatch?.[1]);
|
||||
const documentBundle = Number.isFinite(documentCount) && documentCount > 0
|
||||
? {
|
||||
counterparty,
|
||||
document_count: documentCount
|
||||
}
|
||||
: null;
|
||||
return valueBundle || documentBundle ? { valueBundle, documentBundle } : null;
|
||||
}
|
||||
|
||||
function findRecentBusinessOverviewProofBundles(input: {
|
||||
sessionItems: unknown[];
|
||||
counterpartyHint: string | null;
|
||||
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"];
|
||||
}): { valueBundle: Record<string, unknown> | null; documentBundle: Record<string, unknown> | null } | null {
|
||||
if (!input.counterpartyHint) {
|
||||
return null;
|
||||
}
|
||||
for (let index = input.sessionItems.length - 1; index >= 0; index -= 1) {
|
||||
const item = toRecordObject(input.sessionItems[index]);
|
||||
if (input.toNonEmptyString(item?.role) !== "assistant") {
|
||||
continue;
|
||||
}
|
||||
const debug = toRecordObject(item?.debug);
|
||||
const entryPoint = toRecordObject(debug?.assistant_mcp_discovery_entry_point_v1);
|
||||
const turnInput = toRecordObject(entryPoint?.turn_input);
|
||||
const turnMeaningRef = toRecordObject(turnInput?.turn_meaning_ref);
|
||||
if (turnMeaningRef) {
|
||||
const valueBundle = toRecordObject(turnMeaningRef.previous_counterparty_value_flow_bundle);
|
||||
const documentBundle = toRecordObject(turnMeaningRef.previous_counterparty_document_bundle);
|
||||
if (valueBundle || documentBundle) {
|
||||
const bundleCounterparty = turnMeaningCounterpartyName(
|
||||
turnMeaningRef,
|
||||
valueBundle,
|
||||
documentBundle,
|
||||
input.toNonEmptyString
|
||||
);
|
||||
if (sameEntityHint(input.counterpartyHint, bundleCounterparty)) {
|
||||
return { valueBundle, documentBundle };
|
||||
}
|
||||
}
|
||||
}
|
||||
const parsedBundles =
|
||||
parseBusinessOverviewProofBundlesFromTextV2(item?.text, input.counterpartyHint, input.toNonEmptyString) ??
|
||||
parseBusinessOverviewProofBundlesFromText(item?.text, input.toNonEmptyString);
|
||||
if (!parsedBundles) {
|
||||
continue;
|
||||
}
|
||||
const parsedCounterparty =
|
||||
input.toNonEmptyString(parsedBundles.valueBundle?.counterparty) ??
|
||||
input.toNonEmptyString(parsedBundles.documentBundle?.counterparty);
|
||||
if (!sameEntityHint(input.counterpartyHint, parsedCounterparty)) {
|
||||
continue;
|
||||
}
|
||||
return parsedBundles;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function mergeBusinessOverviewProofBundlesFromNavigationState(input: {
|
||||
followupContext: Record<string, unknown> | null;
|
||||
sessionAddressNavigationState: unknown;
|
||||
predecomposeContract: Record<string, unknown> | null;
|
||||
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"];
|
||||
}): Record<string, unknown> | null {
|
||||
const currentValueBundle = toRecordObject(input.followupContext?.previous_discovery_bidirectional_value_flow);
|
||||
const currentDocumentBundle = toRecordObject(input.followupContext?.previous_discovery_document_summary);
|
||||
if (currentValueBundle && currentDocumentBundle) {
|
||||
return input.followupContext;
|
||||
}
|
||||
const state = toRecordObject(input.sessionAddressNavigationState);
|
||||
const sessionContext = toRecordObject(state?.session_context);
|
||||
const comparisonScope = toRecordObject(sessionContext?.comparison_scope);
|
||||
const proofBundles = toRecordObject(comparisonScope?.proof_bundles);
|
||||
const valueBundle = toRecordObject(proofBundles?.counterparty_value_flow_bundle);
|
||||
const documentBundle = toRecordObject(proofBundles?.counterparty_document_bundle);
|
||||
if (!valueBundle && !documentBundle) {
|
||||
return input.followupContext;
|
||||
}
|
||||
const entities = toRecordObject(input.predecomposeContract?.entities);
|
||||
const hasCurrentOrganizationSelection = Boolean(input.toNonEmptyString(entities?.organization));
|
||||
const businessOverviewFollowup = isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString);
|
||||
if (!businessOverviewFollowup && !hasCurrentOrganizationSelection) {
|
||||
return input.followupContext;
|
||||
}
|
||||
const comparisonCounterparty = toRecordObject(comparisonScope?.counterparty);
|
||||
const counterpartyHint =
|
||||
(input.followupContext ? businessOverviewCounterpartyHint(input.followupContext, input.toNonEmptyString) : null) ??
|
||||
input.toNonEmptyString(comparisonCounterparty?.label);
|
||||
const bundleCounterparty =
|
||||
input.toNonEmptyString(valueBundle?.counterparty) ?? input.toNonEmptyString(documentBundle?.counterparty);
|
||||
if (!counterpartyHint || !sameEntityHint(counterpartyHint, bundleCounterparty)) {
|
||||
return input.followupContext;
|
||||
}
|
||||
return {
|
||||
...(input.followupContext ?? {}),
|
||||
previous_intent: input.toNonEmptyString(input.followupContext?.previous_intent) ?? "business_overview",
|
||||
target_intent: input.toNonEmptyString(input.followupContext?.target_intent) ?? "business_overview",
|
||||
previous_discovery_pilot_scope:
|
||||
input.toNonEmptyString(input.followupContext?.previous_discovery_pilot_scope) ??
|
||||
"business_overview_route_template_v1",
|
||||
previous_discovery_loop_status:
|
||||
input.toNonEmptyString(input.followupContext?.previous_discovery_loop_status) ?? "awaiting_clarification",
|
||||
previous_discovery_loop_selected_chain_id:
|
||||
input.toNonEmptyString(input.followupContext?.previous_discovery_loop_selected_chain_id) ?? "business_overview",
|
||||
previous_discovery_loop_pending_axes: Array.isArray(input.followupContext?.previous_discovery_loop_pending_axes)
|
||||
? input.followupContext?.previous_discovery_loop_pending_axes
|
||||
: ["organization"],
|
||||
previous_discovery_loop_asked_domain_family:
|
||||
input.toNonEmptyString(input.followupContext?.previous_discovery_loop_asked_domain_family) ?? "business_overview",
|
||||
previous_discovery_loop_asked_action_family:
|
||||
input.toNonEmptyString(input.followupContext?.previous_discovery_loop_asked_action_family) ?? "broad_evaluation",
|
||||
previous_discovery_loop_metadata_scope_hint:
|
||||
input.toNonEmptyString(input.followupContext?.previous_discovery_loop_metadata_scope_hint) ?? counterpartyHint,
|
||||
previous_anchor_type: input.toNonEmptyString(input.followupContext?.previous_anchor_type) ?? "counterparty",
|
||||
previous_anchor_value: input.toNonEmptyString(input.followupContext?.previous_anchor_value) ?? counterpartyHint,
|
||||
previous_filters: toRecordObject(input.followupContext?.previous_filters) ?? {},
|
||||
previous_discovery_bidirectional_value_flow: currentValueBundle ?? valueBundle ?? undefined,
|
||||
previous_discovery_document_summary: currentDocumentBundle ?? documentBundle ?? undefined
|
||||
};
|
||||
}
|
||||
|
||||
function mergeBusinessOverviewProofBundlesFromSessionItems(input: {
|
||||
followupContext: Record<string, unknown> | null;
|
||||
sessionItems: unknown[];
|
||||
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"];
|
||||
}): Record<string, unknown> | null {
|
||||
if (!isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString)) {
|
||||
return input.followupContext;
|
||||
}
|
||||
const currentValueBundle = toRecordObject(input.followupContext?.previous_discovery_bidirectional_value_flow);
|
||||
const currentDocumentBundle = toRecordObject(input.followupContext?.previous_discovery_document_summary);
|
||||
if (currentValueBundle && currentDocumentBundle) {
|
||||
return input.followupContext;
|
||||
}
|
||||
const counterpartyHint = input.followupContext
|
||||
? businessOverviewCounterpartyHint(input.followupContext, input.toNonEmptyString)
|
||||
: null;
|
||||
const proofBundles = findRecentBusinessOverviewProofBundles({
|
||||
sessionItems: input.sessionItems,
|
||||
counterpartyHint,
|
||||
toNonEmptyString: input.toNonEmptyString
|
||||
});
|
||||
if (!proofBundles) {
|
||||
return input.followupContext;
|
||||
}
|
||||
return {
|
||||
...(input.followupContext ?? {}),
|
||||
previous_discovery_bidirectional_value_flow:
|
||||
currentValueBundle ?? proofBundles.valueBundle ?? undefined,
|
||||
previous_discovery_document_summary: currentDocumentBundle ?? proofBundles.documentBundle ?? undefined
|
||||
};
|
||||
}
|
||||
|
||||
function hasSelectedObjectInventorySignal(text: string | null): boolean {
|
||||
return /(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ним|selected\s+object)/iu.test(
|
||||
String(text ?? "")
|
||||
@@ -525,6 +851,17 @@ export async function buildAssistantAddressOrchestrationRuntime(
|
||||
sessionItems: input.sessionItems,
|
||||
toNonEmptyString: input.toNonEmptyString
|
||||
});
|
||||
const discoveryFollowupContextWithProofBundles = mergeBusinessOverviewProofBundlesFromSessionItems({
|
||||
followupContext: discoveryFollowupContext,
|
||||
sessionItems: input.sessionItems,
|
||||
toNonEmptyString: input.toNonEmptyString
|
||||
});
|
||||
const discoveryFollowupContextWithStateProofBundles = mergeBusinessOverviewProofBundlesFromNavigationState({
|
||||
followupContext: discoveryFollowupContextWithProofBundles,
|
||||
sessionAddressNavigationState: input.sessionAddressNavigationState,
|
||||
predecomposeContract,
|
||||
toNonEmptyString: input.toNonEmptyString
|
||||
});
|
||||
const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(
|
||||
input.userMessage,
|
||||
addressInputMessage,
|
||||
@@ -540,7 +877,7 @@ export async function buildAssistantAddressOrchestrationRuntime(
|
||||
effectiveMessage: addressInputMessage,
|
||||
assistantTurnMeaning: toRecordObject(orchestrationContract?.assistant_turn_meaning),
|
||||
predecomposeContract,
|
||||
followupContext: discoveryFollowupContext,
|
||||
followupContext: discoveryFollowupContextWithStateProofBundles,
|
||||
knownOrganizations: sessionKnownOrganizations(input.sessionOrganizationScope ?? null)
|
||||
})) as Record<string, unknown>;
|
||||
} catch (error) {
|
||||
|
||||
@@ -414,6 +414,9 @@ function mapAssistantMcpDiscoveryPilotScopeToAddressIntent(
|
||||
pilotScope: string | null,
|
||||
actionFamily: string | null
|
||||
): string | null {
|
||||
if (pilotScope === "business_overview_route_template_v1" || actionFamily === "broad_evaluation") {
|
||||
return "business_overview";
|
||||
}
|
||||
if (pilotScope === "counterparty_lifecycle_query_documents_v1") {
|
||||
return "counterparty_activity_lifecycle";
|
||||
}
|
||||
|
||||
@@ -131,6 +131,20 @@ function buildDeterministicSmalltalkLeadReply(): string {
|
||||
return "\u041f\u0440\u0438\u0432\u0435\u0442! \u0412\u0441\u0451 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e.";
|
||||
}
|
||||
|
||||
function hasFirstTurnSmalltalkGreetingSignal(value: unknown): boolean {
|
||||
const normalized = String(value ?? "")
|
||||
.toLowerCase()
|
||||
.replace(/\u0451/gu, "\u0435")
|
||||
.replace(/\s+/gu, " ")
|
||||
.trim();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return /^(?:привет(?:ик)?|здравствуй(?:те)?|хай|йо|yo|hello|hi|че\s+как|че\s+там)(?:[\s,.!?;:()\-]+(?:как|дела|там|че|что|у\s+тебя|как\s+там|как\s+дела))*[\s,.!?;:()\-]*$/iu.test(
|
||||
normalized
|
||||
);
|
||||
}
|
||||
|
||||
function hasConversationExecutiveSummarySignal(value: unknown): boolean {
|
||||
const normalized = String(value ?? "")
|
||||
.toLowerCase()
|
||||
@@ -215,6 +229,14 @@ export async function runAssistantLivingChatRuntime(
|
||||
let knownOrganizations = [...organizationAuthority.knownOrganizations];
|
||||
let selectedOrganization = organizationAuthority.selectedOrganization;
|
||||
let activeOrganization = organizationAuthority.activeOrganization;
|
||||
const shouldHandleFirstTurnSmalltalkDeterministically =
|
||||
!selectedOrganization &&
|
||||
!activeOrganization &&
|
||||
!continuitySnapshot.hasGroundedAddressContext &&
|
||||
!hasPriorAssistantTurn(input.sessionItems) &&
|
||||
input.modeDecision?.mode === "chat" &&
|
||||
hasFirstTurnSmalltalkGreetingSignal(userMessage) &&
|
||||
input.hasLivingChatSignal(userMessage);
|
||||
const addressRuntimeMeta = (input.addressRuntimeMeta && typeof input.addressRuntimeMeta === "object"
|
||||
? input.addressRuntimeMeta
|
||||
: {}) as Record<string, unknown>;
|
||||
@@ -365,6 +387,27 @@ export async function runAssistantLivingChatRuntime(
|
||||
} else if (capabilityMetaQuery) {
|
||||
chatText = input.buildAssistantCapabilityContractReply(userMessage);
|
||||
livingChatSource = "deterministic_capability_contract";
|
||||
} else if (shouldHandleFirstTurnSmalltalkDeterministically) {
|
||||
const proactiveScopeProbe = await input.resolveDataScopeProbe();
|
||||
const mergedKnownOrganizations = input.mergeKnownOrganizations([
|
||||
...knownOrganizations,
|
||||
...(Array.isArray(proactiveScopeProbe?.organizations) ? (proactiveScopeProbe.organizations as unknown[]) : [])
|
||||
]);
|
||||
knownOrganizations = mergedKnownOrganizations;
|
||||
if (!activeOrganization && mergedKnownOrganizations.length === 1) {
|
||||
activeOrganization = mergedKnownOrganizations[0];
|
||||
}
|
||||
const proactiveOffer = input.buildAssistantProactiveOrganizationOfferReply(proactiveScopeProbe);
|
||||
chatText = [buildDeterministicSmalltalkLeadReply(), proactiveOffer]
|
||||
.filter((part) => String(part ?? "").trim().length > 0)
|
||||
.join(" ");
|
||||
livingChatProactiveScopeOfferApplied = Boolean(proactiveOffer);
|
||||
livingChatSource = proactiveOffer
|
||||
? "deterministic_smalltalk_with_proactive_scope_offer"
|
||||
: "deterministic_smalltalk";
|
||||
if (!dataScopeProbe) {
|
||||
dataScopeProbe = proactiveScopeProbe;
|
||||
}
|
||||
} else {
|
||||
chatText = await input.executeLlmChat();
|
||||
const scriptGuard = input.applyScriptGuard(chatText, userMessage);
|
||||
@@ -385,36 +428,6 @@ export async function runAssistantLivingChatRuntime(
|
||||
livingChatGroundingGuardReason = groundingGuard.reason;
|
||||
livingChatSource = "llm_chat_grounding_guard";
|
||||
}
|
||||
|
||||
const shouldOfferProactiveOrganizationScope =
|
||||
!selectedOrganization &&
|
||||
!activeOrganization &&
|
||||
!continuitySnapshot.hasGroundedAddressContext &&
|
||||
!hasPriorAssistantTurn(input.sessionItems) &&
|
||||
input.modeDecision?.mode === "chat" &&
|
||||
input.hasLivingChatSignal(userMessage);
|
||||
if (shouldOfferProactiveOrganizationScope) {
|
||||
const proactiveScopeProbe = await input.resolveDataScopeProbe();
|
||||
const mergedKnownOrganizations = input.mergeKnownOrganizations([
|
||||
...knownOrganizations,
|
||||
...(Array.isArray(proactiveScopeProbe?.organizations) ? (proactiveScopeProbe.organizations as unknown[]) : [])
|
||||
]);
|
||||
knownOrganizations = mergedKnownOrganizations;
|
||||
if (!activeOrganization && mergedKnownOrganizations.length === 1) {
|
||||
activeOrganization = mergedKnownOrganizations[0];
|
||||
}
|
||||
const proactiveOffer = input.buildAssistantProactiveOrganizationOfferReply(proactiveScopeProbe);
|
||||
if (proactiveOffer) {
|
||||
chatText = [buildDeterministicSmalltalkLeadReply(), proactiveOffer]
|
||||
.filter((part) => String(part ?? "").trim().length > 0)
|
||||
.join(" ");
|
||||
livingChatProactiveScopeOfferApplied = true;
|
||||
livingChatSource = "deterministic_smalltalk_with_proactive_scope_offer";
|
||||
if (!dataScopeProbe) {
|
||||
dataScopeProbe = proactiveScopeProbe;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!chatText) {
|
||||
|
||||
@@ -42,7 +42,11 @@ export interface AssistantMcpDiscoveryExecutionHandoffContract {
|
||||
reason_codes: string[];
|
||||
}
|
||||
|
||||
const HOT_HANDOFF_CHAIN_ALLOWLIST: AssistantMcpDiscoveryChainId[] = ["value_flow"];
|
||||
const HOT_HANDOFF_CHAIN_ALLOWLIST: AssistantMcpDiscoveryChainId[] = [
|
||||
"value_flow",
|
||||
"value_flow_comparison",
|
||||
"business_overview"
|
||||
];
|
||||
|
||||
function uniqueStrings(values: string[]): string[] {
|
||||
const result: string[] = [];
|
||||
|
||||
@@ -47,19 +47,66 @@ function normalizeQuestionText(value: unknown): string {
|
||||
.trim();
|
||||
}
|
||||
|
||||
function requestsFinancialCounterpartyBoundary(turnMeaning: Record<string, unknown> | null, graph: Record<string, unknown> | null): boolean {
|
||||
const text = normalizeQuestionText([
|
||||
function normalizedTurnAndGraphText(
|
||||
turnMeaning: Record<string, unknown> | null,
|
||||
graph: Record<string, unknown> | null
|
||||
): string {
|
||||
return normalizeQuestionText([
|
||||
turnMeaning?.raw_message,
|
||||
turnMeaning?.effective_message,
|
||||
graph?.source_message,
|
||||
graph?.question
|
||||
].join(" "));
|
||||
}
|
||||
|
||||
function requestsFinancialCounterpartyBoundary(turnMeaning: Record<string, unknown> | null, graph: Record<string, unknown> | null): boolean {
|
||||
const text = normalizedTurnAndGraphText(turnMeaning, graph);
|
||||
return (
|
||||
/(?:банк|сбербанк|финанс|кредит|депозит)/iu.test(text) &&
|
||||
/(?:клиент|поставщик|выручк|топ|обычн|роль|поток)/iu.test(text)
|
||||
);
|
||||
}
|
||||
|
||||
function requestsBroadBusinessOverviewSurface(
|
||||
turnMeaning: Record<string, unknown> | null,
|
||||
graph: Record<string, unknown> | null
|
||||
): boolean {
|
||||
const text = normalizedTurnAndGraphText(turnMeaning, graph);
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
if (/(?:не\s+обзор|просто\s+ден\p{L}*|одной\s+строк\p{L}*|только\s+итог|без\s+разбив\p{L}*)/iu.test(text)) {
|
||||
return false;
|
||||
}
|
||||
if (/(?:бизнес[-\s]*обзор|взросл\p{L}{0,10}\s+бизнес|что\s+(?:пока\s+)?нельзя\s+утвержд)/iu.test(text)) {
|
||||
return true;
|
||||
}
|
||||
const markers = [
|
||||
/(?:ндс|налог\p{L}*)/iu,
|
||||
/(?:долг\p{L}*|дебитор|кредитор)/iu,
|
||||
/(?:склад|остатк|товар\p{L}*)/iu,
|
||||
/(?:клиент|заказчик|покупател)/iu,
|
||||
/(?:поставщик|получател)/iu,
|
||||
/(?:оборот\p{L}*)/iu,
|
||||
/(?:ограничен|не\s+подтвержд|нельзя\s+утвержд)/iu
|
||||
];
|
||||
return markers.filter((marker) => marker.test(text)).length >= 3;
|
||||
}
|
||||
|
||||
function requestsCounterpartyLeaderSurface(
|
||||
turnMeaning: Record<string, unknown> | null,
|
||||
graph: Record<string, unknown> | null
|
||||
): boolean {
|
||||
const text = normalizedTurnAndGraphText(turnMeaning, graph);
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
if (/(?:кто|кому)[\s\S]{0,60}(?:больше\s+всего|крупнее\s+всего|основн\p{L}*)[\s\S]{0,60}(?:зан[её]с|прин[её]с|платил|ушло|получил|перев[её]л|заплатил|внес|вн[её]с)/iu.test(text)) {
|
||||
return true;
|
||||
}
|
||||
return /(?:(?:кто|как\p{L}*|покаж\p{L}*|назов\p{L}*|раскро\p{L}*)[\s\S]{0,100}(?:главн\p{L}*|крупнейш\p{L}*|основн\p{L}*|ведущ\p{L}*|топ)[\s\S]{0,80}(?:клиент|заказчик|поставщик|получател)|(?:главн\p{L}*|крупнейш\p{L}*|основн\p{L}*|ведущ\p{L}*)[\s\S]{0,80}(?:клиент|заказчик|поставщик|получател)|(?:топ[-\s]*(?:клиент|заказчик|поставщик|получател))|(?:клиент|поставщик)[\s\S]{0,80}(?:главн|крупнейш|основн|ведущ|топ))/iu.test(text);
|
||||
}
|
||||
|
||||
function requestsCompactCashflowAnswer(
|
||||
turnMeaning: Record<string, unknown> | null,
|
||||
graph: Record<string, unknown> | null
|
||||
@@ -850,6 +897,49 @@ function buildPreviousCounterpartyValueFlowSummary(
|
||||
};
|
||||
}
|
||||
|
||||
function buildBoundarySummaryFromPreviousCounterpartyBundles(
|
||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract
|
||||
): string | null {
|
||||
const turnInput = toRecordObject(entryPoint.turn_input);
|
||||
const turnMeaning = toRecordObject(turnInput?.turn_meaning_ref);
|
||||
const graph = toRecordObject(turnInput?.data_need_graph);
|
||||
const bridge = toRecordObject(entryPoint.bridge);
|
||||
const pilot = toRecordObject(bridge?.pilot);
|
||||
const overview = toRecordObject(pilot?.derived_business_overview);
|
||||
const isBusinessOverview =
|
||||
toNonEmptyString(graph?.business_fact_family) === "business_overview" ||
|
||||
toNonEmptyString(turnMeaning?.asked_domain_family) === "business_overview";
|
||||
if (!isBusinessOverview || overview) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const organizationScope = businessOverviewOrganizationScopeLabel(turnMeaning?.explicit_organization_scope);
|
||||
const separateSubject = businessOverviewSeparateSubjectLabel(graph, turnMeaning, organizationScope);
|
||||
const previousCounterpartySummary = buildPreviousCounterpartyValueFlowSummary(
|
||||
toRecordObject(turnMeaning?.previous_counterparty_value_flow_bundle),
|
||||
separateSubject,
|
||||
toRecordObject(turnMeaning?.previous_counterparty_document_bundle)
|
||||
);
|
||||
if (!separateSubject || !previousCounterpartySummary) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lines = organizationScope
|
||||
? [
|
||||
`Коротко: по компании ${organizationScope} в этом шаге нет нового полного company-level расчета; отдельно по выбранному контрагенту ${separateSubject} есть ранее подтвержденный контрагентский срез.`,
|
||||
previousCounterpartySummary.line,
|
||||
`Можно утверждать: по ${separateSubject} отдельно подтверждены входящие/исходящие денежные строки, расчетное нетто и документы из предыдущего контрагентского среза.`,
|
||||
`Нельзя утверждать: это не подтверждает чистую прибыль, полный оборот или общую бизнес-роль ${separateSubject}; также нельзя смешивать этот контрагентский срез с выводами по компании без отдельного company-level расчета.`
|
||||
]
|
||||
: [
|
||||
`Коротко: уточните, по какой компании/организации сравнить выбранного контрагента ${separateSubject}; company-level вывод без организации не подтверждаю.`,
|
||||
previousCounterpartySummary.line,
|
||||
`Уже можно утверждать: по ${separateSubject} отдельно подтверждены входящие/исходящие денежные строки, расчетное нетто и документы из предыдущего контрагентского среза.`,
|
||||
`Нельзя утверждать: это не чистая прибыль, не полный оборот компании и не доказанная бизнес-роль ${separateSubject}; контрагентский срез нельзя смешивать с company-level выводом без выбранной компании.`
|
||||
];
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
|
||||
function buildCompactBusinessOverviewReply(
|
||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract,
|
||||
draft: Record<string, unknown>
|
||||
@@ -921,6 +1011,13 @@ function buildCompactBusinessOverviewReply(
|
||||
: null;
|
||||
const graphReasonCodes = toStringList(graph?.reason_codes);
|
||||
const directMoneyAnswer = graphReasonCodes.includes("data_need_graph_business_overview_direct_money_answer");
|
||||
const broadOverviewSurfaceRequested = requestsBroadBusinessOverviewSurface(turnMeaning, graph);
|
||||
const counterpartyLeaderSurfaceRequested = requestsCounterpartyLeaderSurface(turnMeaning, graph);
|
||||
const directMoneyOnlyAnswer =
|
||||
directMoneyAnswer && !broadOverviewSurfaceRequested && !counterpartyLeaderSurfaceRequested;
|
||||
const shouldIncludeCounterpartyLeaders =
|
||||
!directMoneyOnlyAnswer || counterpartyLeaderSurfaceRequested || broadOverviewSurfaceRequested;
|
||||
const shouldIncludeOverviewSurface = !directMoneyAnswer || broadOverviewSurfaceRequested;
|
||||
const crossScopeExecutiveSummary = Boolean(separateSubject && previousCounterpartySummary);
|
||||
const lines: string[] = [];
|
||||
const actionFamily = toNonEmptyString(turnMeaning?.asked_action_family);
|
||||
@@ -931,9 +1028,22 @@ function buildCompactBusinessOverviewReply(
|
||||
actionFamily === "vendor_risk_procurement_boundary" || unsupportedFamily === "vendor_risk_procurement_boundary";
|
||||
const inventoryReserveBoundary =
|
||||
actionFamily === "inventory_reserve_boundary" || unsupportedFamily === "inventory_reserve_liquidation_boundary";
|
||||
const compactCashflowRequested = directMoneyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph);
|
||||
const compactCashflowRequested = directMoneyOnlyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph);
|
||||
const cashflowPolarityRequested = compactCashflowRequested && requestsCashflowPolarityAnswer(turnMeaning, graph);
|
||||
const directAccountingProfitRequested = requestsDirectAccountingProfitAnswer(turnMeaning, graph);
|
||||
const rawMessage = toNonEmptyString(turnMeaning?.raw_message) ?? toNonEmptyString(turnMeaning?.effective_message);
|
||||
const rawMessageComparable = compactComparable(rawMessage);
|
||||
const organizationScopeComparable = compactComparable(organizationScope);
|
||||
const plainOrganizationClarificationSelection = Boolean(
|
||||
separateSubject &&
|
||||
organizationScope &&
|
||||
rawMessage &&
|
||||
rawMessageComparable &&
|
||||
organizationScopeComparable &&
|
||||
rawMessageComparable.includes(organizationScopeComparable) &&
|
||||
rawMessage.length <= 90 &&
|
||||
!/(?:сравн|подтвержд|деньг|сколько|что\s+|покаж|дай|вывод|нельзя|клиент|поставщик|\?)/iu.test(rawMessage)
|
||||
);
|
||||
|
||||
if (compactCashflowRequested && !rankingNeed && (incomingAmount || outgoingAmount || netAmount)) {
|
||||
const netDisplay = sentenceAmount(netAmount) ?? netAmount ?? "0 \u0440\u0443\u0431.";
|
||||
@@ -955,6 +1065,23 @@ function buildCompactBusinessOverviewReply(
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
|
||||
if (plainOrganizationClarificationSelection && (incomingAmount || outgoingAmount || netAmount)) {
|
||||
lines.push(
|
||||
`Коротко: по компании ${organizationScope} ${period} подтвержден company-level денежный срез: входящие ${incomingAmount ?? "0 руб."}, исходящие ${outgoingAmount ?? "0 руб."}, операционное нетто ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`
|
||||
);
|
||||
if (previousCounterpartySummary) {
|
||||
lines.push(previousCounterpartySummary.line);
|
||||
} else {
|
||||
lines.push(
|
||||
`Отдельно по выбранному контрагенту ${separateSubject}: суммы компании на него не переношу; в этом шаге держу только границу, что это отдельный контрагентский контур.`
|
||||
);
|
||||
}
|
||||
lines.push(
|
||||
`Нельзя утверждать: чистую прибыль, полноценный финрезультат, юридические роли клиентов/поставщиков и выводы по ${separateSubject} на основе company-level сумм без отдельного контрагентского среза.`
|
||||
);
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
|
||||
if (profitMarginBoundary) {
|
||||
const accountingFinancialResult = toRecordObject(overview.accounting_financial_result);
|
||||
if (accountingFinancialResult) {
|
||||
@@ -1178,7 +1305,13 @@ function buildCompactBusinessOverviewReply(
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
|
||||
if (!separateSubject && !crossScopeExecutiveSummary && (actionFamily === "broad_evaluation" || unsupportedFamily === "broad_business_evaluation")) {
|
||||
if (
|
||||
!separateSubject &&
|
||||
!crossScopeExecutiveSummary &&
|
||||
!counterpartyLeaderSurfaceRequested &&
|
||||
!rankingNeed &&
|
||||
(actionFamily === "broad_evaluation" || unsupportedFamily === "broad_business_evaluation")
|
||||
) {
|
||||
const subject = organizationScope ?? "компания";
|
||||
const periodWithoutPrefix = period.replace(/^за\s+/iu, "");
|
||||
lines.push(
|
||||
@@ -1208,6 +1341,10 @@ function buildCompactBusinessOverviewReply(
|
||||
: `- крупнейший получатель исходящих денег: ${topSupplier};`
|
||||
);
|
||||
}
|
||||
const taxLine = businessOverviewTaxLine(overview);
|
||||
if (taxLine) {
|
||||
lines.push(`- ${localizeLine(taxLine)}`);
|
||||
}
|
||||
const inventoryLine = businessOverviewInventoryLine(overview);
|
||||
if (inventoryLine) {
|
||||
lines.push(`- ${localizeLine(inventoryLine)}`);
|
||||
@@ -1220,7 +1357,7 @@ function buildCompactBusinessOverviewReply(
|
||||
"Ограничение: это оценка по денежным потокам и найденным срезам 1С, не аудиторское заключение и не подтвержденная чистая прибыль."
|
||||
);
|
||||
const missingOverviewFamilies: string[] = [];
|
||||
if (!businessOverviewTaxLine(overview)) {
|
||||
if (!taxLine) {
|
||||
missingOverviewFamilies.push("НДС/налоговая позиция без отдельного точного расчета");
|
||||
}
|
||||
if (!debtLine) {
|
||||
@@ -1264,9 +1401,26 @@ function buildCompactBusinessOverviewReply(
|
||||
!/(?:все\s+доступное|все\s+время|all\s+time)/iu.test(period) &&
|
||||
(incomingAmount || outgoingAmount || netAmount);
|
||||
if (explicitPeriodRankingOverview) {
|
||||
lines.push(
|
||||
`Коротко: ${organizationPrefix}${period} денежная картина подтверждена по найденным строкам 1С.`
|
||||
);
|
||||
if (counterpartyLeaderSurfaceRequested) {
|
||||
const incomingLeaderText =
|
||||
customerName && customerAmount
|
||||
? topCustomerLooksFinancial
|
||||
? `${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}; это банк/финансовый контур, не называю его обычной клиентской выручкой без назначения платежа${nonFinancialCustomer ? `; крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}` : ""}`
|
||||
: `${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}`
|
||||
: "не распознан";
|
||||
const outgoingLeaderText = topSupplier
|
||||
? topSupplierLooksFinancial
|
||||
? `${topSupplier}; это банк/финансовый контур, не называю его обычным поставщиком без назначения платежа/договора${nonFinancialSupplier ? `; крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}` : ""}`
|
||||
: topSupplier
|
||||
: "не распознан";
|
||||
lines.push(
|
||||
`Коротко: ${organizationPrefix}${period} больше всего занес ${incomingLeaderText}; больше всего ушло ${outgoingLeaderText}.`
|
||||
);
|
||||
} else {
|
||||
lines.push(
|
||||
`Коротко: ${organizationPrefix}${period} денежная картина подтверждена по найденным строкам 1С.`
|
||||
);
|
||||
}
|
||||
lines.push(
|
||||
`Деньги: входящие ${incomingAmount ?? "0 руб."}, исходящие ${outgoingAmount ?? "0 руб."}, расчетное операционное нетто ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`
|
||||
);
|
||||
@@ -1332,7 +1486,7 @@ function buildCompactBusinessOverviewReply(
|
||||
`Коротко: ${organizationPrefix}${period} по подтвержденным строкам 1С получили ${incomingAmount ?? "0 руб."}; исходящие платежи/списания ${outgoingAmount ?? "0 руб."}; ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб"}${topCustomerLead}${topSupplierLead}${roleBoundaryLead}${separateSubjectLead}.`
|
||||
);
|
||||
lines.push('Метод: "заработали" здесь считаю как операционный денежный показатель по 1С; это не чистая прибыль и не финрезультат.');
|
||||
if (!directMoneyAnswer && customerName && customerAmount) {
|
||||
if (shouldIncludeCounterpartyLeaders && customerName && customerAmount) {
|
||||
lines.push(
|
||||
topCustomerLooksFinancial
|
||||
? `Крупнейший входящий денежный источник в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}. По названию это банк/финансовая организация, поэтому без назначения платежа не называю это клиентской выручкой.${nonFinancialCustomer ? ` Крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}.` : ""}`
|
||||
@@ -1353,21 +1507,21 @@ function buildCompactBusinessOverviewReply(
|
||||
);
|
||||
}
|
||||
|
||||
if (!directMoneyAnswer && topSupplier) {
|
||||
if (shouldIncludeCounterpartyLeaders && topSupplier) {
|
||||
lines.push(
|
||||
topSupplierLooksFinancial
|
||||
? `Крупнейший получатель исходящих денег: ${topSupplier}. По названию это банк/финансовая организация, поэтому без назначения платежа/договора не считаю это обычным поставщиком.${nonFinancialSupplier ? ` Крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}.` : ""}`
|
||||
: `Крупнейший подтвержденный получатель исходящих денег: ${topSupplier}.`
|
||||
);
|
||||
}
|
||||
if (!directMoneyAnswer && (topCustomer || topSupplier)) {
|
||||
if (shouldIncludeCounterpartyLeaders && (topCustomer || topSupplier)) {
|
||||
lines.push(
|
||||
topCustomerLooksFinancial || topSupplierLooksFinancial
|
||||
? "Важно по ролям: текущий денежный срез подтверждает источники и получателей денег, но банковские контрагенты требуют проверки назначения платежа/счетов и не доказывают роль клиента или поставщика."
|
||||
: "Важно по ролям: текущий денежный срез подтверждает денежные источники и получателей, но не доказывает, что это главный клиент или главный поставщик как бизнес-роль."
|
||||
);
|
||||
}
|
||||
if (!directMoneyAnswer) {
|
||||
if (shouldIncludeOverviewSurface) {
|
||||
lines.push(
|
||||
`Что подтверждено: денежный срез по компании${organizationScope ? ` ${organizationScope}` : ""}${period ? ` ${period}` : ""}${topCustomer ? ", крупнейший источник входящих денег" : ""}${topSupplier ? ", крупнейший получатель исходящих денег" : ""}.`
|
||||
);
|
||||
@@ -1466,6 +1620,11 @@ function buildReplyText(entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContra
|
||||
return null;
|
||||
}
|
||||
|
||||
const previousCounterpartyBoundaryReply = buildBoundarySummaryFromPreviousCounterpartyBundles(entryPoint);
|
||||
if (previousCounterpartyBoundaryReply) {
|
||||
return previousCounterpartyBoundaryReply;
|
||||
}
|
||||
|
||||
const compactBidirectionalValueFlowReply = buildCompactBidirectionalValueFlowReply(entryPoint, draft);
|
||||
if (compactBidirectionalValueFlowReply) {
|
||||
return compactBidirectionalValueFlowReply;
|
||||
|
||||
@@ -510,6 +510,13 @@ function hasExactDocumentListAddressReply(
|
||||
if (source !== "address_query_runtime_v1" && source !== "address_exact" && source !== "address_lane") {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
hasValueFlowActionConflictWithDiscoveryTurnMeaning(input, entryPoint) ||
|
||||
hasEvidenceLaneConflictWithDiscoveryTurnMeaning(input, entryPoint) ||
|
||||
hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
|
||||
const isDocumentIntent =
|
||||
|
||||
@@ -432,7 +432,12 @@ export async function runAssistantMcpDiscoveryRuntimeBridge(
|
||||
const reasonCodes = uniqueStrings([...planner.reason_codes, ...pilot.reason_codes, ...answerDraft.reason_codes]);
|
||||
|
||||
pushReason(reasonCodes, `runtime_bridge_status_${bridgeStatus}`);
|
||||
pushReason(reasonCodes, "runtime_bridge_not_wired_to_hot_assistant_answer");
|
||||
pushReason(
|
||||
reasonCodes,
|
||||
executionHandoff.can_use_guarded_response
|
||||
? "runtime_bridge_wired_to_guarded_hot_assistant_answer"
|
||||
: "runtime_bridge_not_wired_to_hot_assistant_answer"
|
||||
);
|
||||
pushReason(reasonCodes, `runtime_bridge_loop_state_${loopState.loop_status}`);
|
||||
pushReason(reasonCodes, "runtime_bridge_route_candidate_built");
|
||||
pushReason(reasonCodes, `runtime_bridge_route_candidate_${routeCandidate.candidate_status}`);
|
||||
|
||||
@@ -117,6 +117,7 @@ function isReferentialOrganizationPlaceholder(value: string | null): boolean {
|
||||
"этой компании",
|
||||
"этой компанией",
|
||||
"эту компанию",
|
||||
"в целом",
|
||||
"наша организация",
|
||||
"нашей организации",
|
||||
"нашей компанией"
|
||||
@@ -250,7 +251,13 @@ function normalizeFollowupCounterpartyCandidate(value: unknown): string | null {
|
||||
if (!text || isInvalidEntityCandidate(text)) {
|
||||
return null;
|
||||
}
|
||||
return text;
|
||||
const cleaned = text
|
||||
.replace(
|
||||
/^(?:\u043f\u043e\s+)?\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442(?:\u0443|\u0430|\u043e\u043c|\u0435|\u044b|\u0430\u043c|\u0430\u043c\u0438|\u0430\u0445)?\s+/iu,
|
||||
""
|
||||
)
|
||||
.trim();
|
||||
return cleaned && !isInvalidEntityCandidate(cleaned) ? cleaned : text;
|
||||
}
|
||||
|
||||
function pushScopedEntityCandidate(
|
||||
@@ -702,12 +709,13 @@ function collectFollowupDiscoverySeed(followupContext: Record<string, unknown> |
|
||||
const normalizedDiscoveryEntities = discoveryEntities
|
||||
.map((entity) => normalizeFollowupCounterpartyCandidate(entity))
|
||||
.filter((entity): entity is string => Boolean(entity));
|
||||
const normalizedLoopMetadataScopeHint = normalizeFollowupCounterpartyCandidate(loopMetadataScopeHint);
|
||||
const groundedDiscoveryCounterparty =
|
||||
ambiguityBlocksImplicitGrounding || metadataPilotCarriesScopeOnly
|
||||
? null
|
||||
: normalizedDiscoveryEntities[0] ?? normalizeFollowupCounterpartyCandidate(loopMetadataScopeHint);
|
||||
: normalizedDiscoveryEntities[0] ?? normalizedLoopMetadataScopeHint;
|
||||
const metadataScopeHint =
|
||||
loopMetadataScopeHint ??
|
||||
normalizedLoopMetadataScopeHint ??
|
||||
(loopSubjectResolutionOptional ? normalizedDiscoveryEntities[0] ?? null : null);
|
||||
const previousFiltersCounterparty = normalizeFollowupCounterpartyCandidate(previousFilters?.counterparty);
|
||||
const rootFiltersCounterparty = normalizeFollowupCounterpartyCandidate(rootFilters?.counterparty);
|
||||
@@ -724,9 +732,10 @@ function collectFollowupDiscoverySeed(followupContext: Record<string, unknown> |
|
||||
(toNonEmptyString(followupContext?.previous_anchor_type) === "organization"
|
||||
? toNonEmptyString(followupContext?.previous_anchor_value)
|
||||
: null);
|
||||
const dateScope =
|
||||
collectDateScopeFromFilters(previousFilters) ??
|
||||
collectDateScopeFromFilters(rootFilters);
|
||||
const loopProvidedAllTimeScope = loopProvidedAxes.includes("all_time_scope");
|
||||
const dateScope = loopProvidedAllTimeScope
|
||||
? "all_time_scope"
|
||||
: collectDateScopeFromFilters(previousFilters) ?? collectDateScopeFromFilters(rootFilters);
|
||||
return {
|
||||
pilotScope: effectivePilotScope,
|
||||
domain: mapped.domain,
|
||||
@@ -986,7 +995,7 @@ function hasOrganizationLevelSupplierQualityOverviewSignal(text: string): boolea
|
||||
|
||||
function hasCrossScopeExecutiveSummarySignal(text: string): boolean {
|
||||
return (
|
||||
/(?:\u0441\u043e\u0431\u0435\u0440\p{L}*\s+(?:\u043a\u043e\u0440\u043e\u0442\u043a\p{L}*\s+)?\u0438\u0442\u043e\u0433|\u044d\u043a\u0437\u0435\u043a\u044c\u044e\u0442\u0438\u0432\p{L}*\s+\u0441\u0430\u043c\u043c\u0430\u0440\u0438|executive\s+summary|final\s+summary)/iu.test(
|
||||
/(?:\u0441\u043e\u0431\u0435\u0440\p{L}*\s+(?:\u043a\u043e\u0440\u043e\u0442\u043a\p{L}*\s+)?\u0438\u0442\u043e\u0433|(?:\u043a\u043e\u0440\u043e\u0442\u043a\p{L}*\s+)?\u0441\u0440\u0430\u0432\u043d\p{L}*|\u044d\u043a\u0437\u0435\u043a\u044c\u044e\u0442\u0438\u0432\p{L}*\s+\u0441\u0430\u043c\u043c\u0430\u0440\u0438|executive\s+summary|final\s+summary|brief(?:ly)?\s+compare)/iu.test(
|
||||
text
|
||||
) &&
|
||||
/(?:\u0447\u0442\u043e\s+(?:\u043c\u044b\s+)?\u043f\u043e\u0434\u0442\u0432\u0435\u0440\p{L}*|\u043f\u043e\s+\u043a\u043e\u043c\u043f\u0430\u043d\p{L}*|\u043f\u043e\s+\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\p{L}*|confirmed|company|organization)/iu.test(
|
||||
@@ -1013,6 +1022,35 @@ function hasPlainBusinessOverviewSignal(text: string): boolean {
|
||||
return hasPlainOverviewCue && hasCompanyOrOperatingScopeCue;
|
||||
}
|
||||
|
||||
function countBroadBusinessOverviewAxes(text: string): number {
|
||||
const axisPatterns = [
|
||||
/(?:\u0434\u0435\u043d\p{L}*|\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\p{L}*|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*|\u043f\u043b\u0430\u0442\p{L}*|money|cash|revenue|turnover)/iu,
|
||||
/(?:\u043d\u0434\u0441|vat)/iu,
|
||||
/(?:\u0434\u043e\u043b\p{L}*|\u0434\u0435\u0431\u0438\u0442\u043e\u0440\p{L}*|\u043a\u0440\u0435\u0434\u0438\u0442\u043e\u0440\p{L}*|receivable|payable|debt)/iu,
|
||||
/(?:\u0441\u043a\u043b\u0430\u0434|\u043e\u0441\u0442\u0430\u0442|\u0437\u0430\u043f\u0430\u0441|\u0442\u043e\u0432\u0430\u0440|warehouse|stock|inventory)/iu,
|
||||
/(?:\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b|\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|customer|client|buyer)/iu,
|
||||
/(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0432\u0435\u043d\u0434\u043e\u0440|\u0437\u0430\u043a\u0443\u043f|supplier|vendor|procurement)/iu,
|
||||
/(?:\u0433\u0434\u0435[\s\S]{0,80}(?:\u043d\u0435\u043b\u044c\u0437\u044f|\u043d\u0435\s+\u0434\u0435\u043b\u0430\p{L}*|\u043d\u0435\s+\u0445\u0432\u0430\p{L}*)|\u0447\u0442\u043e[\s\S]{0,80}(?:\u043d\u0435\u043b\u044c\u0437\u044f|\u043d\u0435\s+\u0445\u0432\u0430\p{L}*)|cannot|unknown|missing|limitation)/iu
|
||||
];
|
||||
return axisPatterns.reduce((count, pattern) => count + (pattern.test(text) ? 1 : 0), 0);
|
||||
}
|
||||
|
||||
function hasBroadBusinessOverviewSurfaceSignal(text: string): boolean {
|
||||
const normalized = compactLower(text);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const hasBroadCue =
|
||||
/(?:\u043f\u043e[-\s]*\u0447\u0435\u043b\u043e\u0432\u0435\u0447\u0435\u0441\u043a\p{L}*|\u0447\u0442\u043e\s+\u043f\u043e\s+\u0431\u0438\u0437\u043d\u0435\u0441\u0443\s+\u0432\u0438\u0434\u043d\p{L}*|\u043f\u043e\u0441\u043c\u043e\u0442\p{L}*[\s\S]{0,100}(?:\u0431\u0438\u0437\u043d\u0435\u0441|\u0434\u0435\u044f\u0442\u0435\u043b\p{L}*)|\u0431\u0438\u0437\u043d\u0435\u0441[\s\S]{0,80}(?:\u0432\u0438\u0434\u043d\p{L}*|\u0432\u044b\u0432\u043e\u0434|\u0441\u0440\u0435\u0437)|human\s+readable\s+business\s+view)/iu.test(
|
||||
normalized
|
||||
);
|
||||
const hasCompanyScope =
|
||||
/(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e|\u043e\u0430\u043e|\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0431\u0438\u0437\u043d\u0435\u0441|\u0432\s+1\s?\u0441|1\s?c|company|organization|business|(?:19|20)\d{2})/iu.test(
|
||||
normalized
|
||||
);
|
||||
return hasBroadCue && hasCompanyScope && countBroadBusinessOverviewAxes(normalized) >= 3;
|
||||
}
|
||||
|
||||
function hasBusinessOverviewSignal(text: string): boolean {
|
||||
if (
|
||||
hasCrossScopeExecutiveSummarySignal(text) ||
|
||||
@@ -1021,6 +1059,7 @@ function hasBusinessOverviewSignal(text: string): boolean {
|
||||
hasOrganizationLevelDebtDueDateOverviewSignal(text) ||
|
||||
hasOrganizationLevelInventoryReserveLiquidationOverviewSignal(text) ||
|
||||
hasPlainBusinessOverviewSignal(text) ||
|
||||
hasBroadBusinessOverviewSurfaceSignal(text) ||
|
||||
hasOrganizationLevelSupplierQualityOverviewSignal(text)
|
||||
) {
|
||||
return true;
|
||||
@@ -1101,6 +1140,15 @@ function hasBusinessOverviewSeparateCounterpartySignal(text: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function isGenericSelectedCounterpartyReference(value: string | null): boolean {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
return /^(?:(?:\u0432\u044b\u0431\u0440\u0430\u043d\p{L}*|\u044d\u0442\p{L}*|\u0434\u0430\u043d\p{L}*|\u0442\u0435\u043a\u0443\u0449\p{L}*)\s+)?\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*$|^(?:selected|chosen|current|this)\s+counterpart(?:y|ies)?$/iu.test(
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
function businessOverviewSeparateCounterpartyCandidateFromText(text: string): string | null {
|
||||
const source = repairAddressMojibakeText(String(text ?? ""));
|
||||
const patterns = [
|
||||
@@ -1109,7 +1157,7 @@ function businessOverviewSeparateCounterpartyCandidateFromText(text: string): st
|
||||
];
|
||||
for (const pattern of patterns) {
|
||||
const candidate = normalizeFollowupCounterpartyCandidate(source.match(pattern)?.[1]);
|
||||
if (candidate && !isInvalidEntityCandidate(candidate)) {
|
||||
if (candidate && !isInvalidEntityCandidate(candidate) && !isGenericSelectedCounterpartyReference(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
@@ -1245,6 +1293,9 @@ function normalizeLooseOrganizationAlias(value: string | null): string | null {
|
||||
if (hasYearOnlyTimeTail) {
|
||||
return null;
|
||||
}
|
||||
if (new Set(["в целом", "компания в целом", "организация в целом"]).has(comparable)) {
|
||||
return null;
|
||||
}
|
||||
if (/^(?:\u0438|\u0432|\u0432\u043e|\u0437\u0430|\u043d\u0430|\u043f\u043e|\u043a\u0442\u043e|\u0447\u0442\u043e|\u043a\u0430\u043a(?:\u043e\u0439|\u0430\u044f|\u0438\u0435)?|\u0433\u043b\u0430\u0432\u043d\p{L}*)\b/iu.test(comparable)) {
|
||||
return null;
|
||||
}
|
||||
@@ -1910,11 +1961,21 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
const businessOverviewSignal =
|
||||
!businessOverviewCounterpartyValueFlowPivot &&
|
||||
(rawBusinessOverviewSignal || seededBusinessOverviewSignal);
|
||||
const organizationClarificationBusinessOverviewLoop = Boolean(
|
||||
followupSeed.loopStatus === "awaiting_clarification" &&
|
||||
followupSeed.loopSelectedChainId === "business_overview" &&
|
||||
followupSeed.loopPendingAxes.includes("organization") &&
|
||||
currentTurnOrganizationScope &&
|
||||
!rawLifecycleSignal &&
|
||||
!rawMetadataSignal
|
||||
);
|
||||
const businessOverviewSeparateCounterpartySignal = Boolean(
|
||||
businessOverviewSignal && hasBusinessOverviewSeparateCounterpartySignal(rawText)
|
||||
);
|
||||
const businessOverviewSeparateCounterpartyCandidate = businessOverviewSeparateCounterpartySignal
|
||||
? businessOverviewSeparateCounterpartyCandidateFromText(rawText)
|
||||
: organizationClarificationBusinessOverviewLoop
|
||||
? followupSeed.counterparty ?? followupSeed.discoveryEntity ?? followupSeed.metadataScopeHint
|
||||
: null;
|
||||
const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate);
|
||||
const currentTurnDocumentLaneSignal = rawAction === "list_documents";
|
||||
@@ -1944,6 +2005,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
const businessOverviewSuppressesFollowupCounterparty = Boolean(
|
||||
businessOverviewSignal &&
|
||||
!businessOverviewSeparateCounterpartySignal &&
|
||||
!organizationClarificationBusinessOverviewLoop &&
|
||||
(rawBusinessOverviewSignal ||
|
||||
businessOverviewContinuationSignal ||
|
||||
broadBusinessEvaluationUnsupported ||
|
||||
@@ -2452,7 +2514,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
}
|
||||
pushScopedEntityCandidate(entityCandidates, rawEntityCandidate, groundedFollowupEntity);
|
||||
}
|
||||
const businessOverviewSeparateCounterpartyDisplayCandidate = businessOverviewSeparateCounterpartySignal
|
||||
const shouldPreserveBusinessOverviewSeparateCounterparty =
|
||||
businessOverviewSeparateCounterpartySignal || organizationClarificationBusinessOverviewLoop;
|
||||
const businessOverviewSeparateCounterpartyDisplayCandidate = shouldPreserveBusinessOverviewSeparateCounterparty
|
||||
? preferredScopedDisplayName(businessOverviewSeparateCounterpartyCandidate, [
|
||||
groundedFollowupEntity,
|
||||
effectiveFollowupCounterparty,
|
||||
@@ -2461,7 +2525,24 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
rawScopedEntityCandidate,
|
||||
rawEntityCandidate,
|
||||
...entityCandidates
|
||||
])
|
||||
]) ??
|
||||
preferredScopedDisplayName(
|
||||
groundedFollowupEntity ??
|
||||
effectiveFollowupCounterparty ??
|
||||
followupSeed.discoveryEntity ??
|
||||
normalizedPredecomposeCounterparty ??
|
||||
rawScopedEntityCandidate ??
|
||||
rawEntityCandidate,
|
||||
[
|
||||
groundedFollowupEntity,
|
||||
effectiveFollowupCounterparty,
|
||||
followupSeed.discoveryEntity,
|
||||
normalizedPredecomposeCounterparty,
|
||||
rawScopedEntityCandidate,
|
||||
rawEntityCandidate,
|
||||
...entityCandidates
|
||||
]
|
||||
)
|
||||
: null;
|
||||
const businessOverviewSeparateEntityCandidates = businessOverviewSeparateCounterpartyDisplayCandidate
|
||||
? [businessOverviewSeparateCounterpartyDisplayCandidate]
|
||||
@@ -2586,6 +2667,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
const normalizedAssistantTurnMeaningDateScope =
|
||||
rawEntitySearchOverridesStaleScope ||
|
||||
suppressNegatedTaxOnlyDateScope ||
|
||||
(organizationClarificationBusinessOverviewLoop && !currentTurnCarriesExplicitPeriod) ||
|
||||
(suppressImplicitCurrentDateScope && isImplicitCurrentDateScope(assistantTurnMeaningDateScope))
|
||||
? null
|
||||
: assistantTurnMeaningDateScope;
|
||||
@@ -2603,8 +2685,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
normalizedPredecomposeDateScope &&
|
||||
normalizedPredecomposeDateScope.startsWith(`${rawDateScope}-`)
|
||||
);
|
||||
const followupAllTimeScopeApplied = normalizedFollowupDateScope === "all_time_scope";
|
||||
const explicitDateScope =
|
||||
rawAllTimeScopeSignal
|
||||
rawAllTimeScopeSignal || followupAllTimeScopeApplied
|
||||
? null
|
||||
: normalizedAssistantTurnMeaningDateScope ??
|
||||
(businessOverviewRawYearOverridesPredecomposeAsOf ? rawDateScope : normalizedPredecomposeDateScope) ??
|
||||
@@ -2615,7 +2698,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
!normalizedAssistantTurnMeaningDateScope &&
|
||||
!normalizedPredecomposeDateScope &&
|
||||
!rawDateScope &&
|
||||
normalizedFollowupDateScope
|
||||
normalizedFollowupDateScope &&
|
||||
normalizedFollowupDateScope !== "all_time_scope"
|
||||
);
|
||||
const clarificationLoopSeedApplied = Boolean(
|
||||
followupSeed.loopStatus === "awaiting_clarification" && followupSeed.loopSelectedChainId
|
||||
@@ -2668,7 +2752,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
valueFlowSignal && followupSeed.rankingNeed && !rawEntitySearchOverridesStaleScope
|
||||
? followupSeed.rankingNeed
|
||||
: undefined,
|
||||
explicit_entity_candidates: businessOverviewSignal ? [] : entityCandidates,
|
||||
explicit_entity_candidates:
|
||||
businessOverviewSignal || shouldPreserveBusinessOverviewSeparateCounterparty ? [] : entityCandidates,
|
||||
business_overview_separate_entity_candidates: businessOverviewSeparateEntityCandidates,
|
||||
previous_counterparty_value_flow_bundle:
|
||||
businessOverviewSignal && followupSeed.previousBidirectionalValueFlow
|
||||
@@ -2897,6 +2982,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
if (rawAllTimeScopeSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_all_time_scope_signal_detected");
|
||||
}
|
||||
if (followupAllTimeScopeApplied) {
|
||||
pushReason(reasonCodes, "mcp_discovery_all_time_scope_from_followup_context");
|
||||
}
|
||||
if (suppressNegatedTaxOnlyDateScope) {
|
||||
pushReason(reasonCodes, "mcp_discovery_negated_tax_period_scope_suppressed");
|
||||
}
|
||||
@@ -2999,7 +3087,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
if (businessOverviewSuppressesFollowupCounterparty) {
|
||||
pushReason(reasonCodes, "mcp_discovery_business_overview_suppressed_stale_counterparty");
|
||||
}
|
||||
if (businessOverviewSeparateCounterpartySignal) {
|
||||
if (shouldPreserveBusinessOverviewSeparateCounterparty) {
|
||||
pushReason(reasonCodes, "mcp_discovery_business_overview_preserved_explicit_counterparty_summary_scope");
|
||||
}
|
||||
if (businessOverviewSeparateCounterpartyCandidate) {
|
||||
@@ -3023,7 +3111,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
) {
|
||||
pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose");
|
||||
}
|
||||
if (rawScopedEntityCandidate && !normalizedPredecomposeCounterparty) {
|
||||
if (rawScopedEntityCandidate && !normalizedPredecomposeCounterparty && !businessOverviewSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_counterparty_from_raw_scope");
|
||||
}
|
||||
if (
|
||||
|
||||
@@ -136,6 +136,26 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
return /(?:документ|счет|счет-фактур|накладн|акт|реализац|document|invoice|receipt)/iu.test(normalized);
|
||||
}
|
||||
|
||||
function hasSelectedCounterpartyDocumentFollowupSignal(userMessage, alternateMessage = null) {
|
||||
return [userMessage, alternateMessage]
|
||||
.filter((value) => deps.toNonEmptyString(value))
|
||||
.map((value) => normalizeFollowupText(value).replace(/С‘/g, "Рµ"))
|
||||
.some((normalized) => {
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const hasDocumentCue =
|
||||
/(?:\u0434\u043e\u043a\p{L}*|\u0441\u0447\p{L}*|\u043d\u0430\u043a\u043b\u0430\u0434\p{L}*|\u0430\u043a\u0442|document|docs?|invoice|receipt)/iu.test(
|
||||
normalized
|
||||
) || hasReadableDocumentsPivotCue(normalized);
|
||||
const hasSelectedCounterpartyCue =
|
||||
/(?:\u043f\u043e\s+\u043d(?:\u0435\u043c\u0443|\u0435\u0439)|\u043f\u043e\s+\u044d\u0442(?:\u043e\u043c\u0443|\u043e\u0439)|\u0442\u0435\u043a\u0443\u0449\p{L}*\s+\u043e\u0431\u044a\u0435\u043a\p{L}*|\u0432\u044b\u0431\u0440\u0430\u043d\p{L}*\s+(?:\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043e\u0431\u044a\u0435\u043a\u0442)|selected\s+(?:counterparty|object)|current\s+object)/iu.test(
|
||||
normalized
|
||||
);
|
||||
return hasDocumentCue && hasSelectedCounterpartyCue;
|
||||
});
|
||||
}
|
||||
|
||||
function selectSuggestedIntentByPivotCue(suggestedIntents, userMessage, alternateMessage = null) {
|
||||
if (!Array.isArray(suggestedIntents) || suggestedIntents.length === 0) {
|
||||
return null;
|
||||
@@ -425,20 +445,71 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
return flow;
|
||||
}
|
||||
|
||||
function readMcpDiscoveryPreviousCounterpartyValueFlowBundle(debug) {
|
||||
const entryPoint = debug?.assistant_mcp_discovery_entry_point_v1;
|
||||
const bundle = entryPoint?.turn_input?.turn_meaning_ref?.previous_counterparty_value_flow_bundle;
|
||||
if (!bundle || typeof bundle !== "object" || Array.isArray(bundle)) {
|
||||
return null;
|
||||
}
|
||||
return bundle;
|
||||
}
|
||||
|
||||
function readMcpDiscoveryPreviousCounterpartyDocumentBundle(debug) {
|
||||
const entryPoint = debug?.assistant_mcp_discovery_entry_point_v1;
|
||||
const bundle = entryPoint?.turn_input?.turn_meaning_ref?.previous_counterparty_document_bundle;
|
||||
if (!bundle || typeof bundle !== "object" || Array.isArray(bundle)) {
|
||||
return null;
|
||||
}
|
||||
return bundle;
|
||||
}
|
||||
|
||||
function readCounterpartyDocumentSummaryFromItem(item) {
|
||||
const text = deps.toNonEmptyString(item?.text);
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
const firstLine = text.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "";
|
||||
const match = firstLine.match(/Контрагент:\s*([^.\n]+)\.\s*Найдено документов:\s*(\d+)/iu);
|
||||
if (!match?.[1] || !match?.[2]) {
|
||||
const directMatch = firstLine.match(/Контрагент:\s*([^.\n]+)\.\s*Найдено документов:\s*(\d+)/iu);
|
||||
if (directMatch?.[1] && directMatch?.[2]) {
|
||||
return {
|
||||
counterparty: deps.toNonEmptyString(directMatch[1]),
|
||||
document_count: Number(directMatch[2]),
|
||||
direct_answer: firstLine
|
||||
};
|
||||
}
|
||||
const summaryMatch = text.match(/Отдельно\s+по\s+контрагенту\s+([^:\n]+):[\s\S]{0,260}документы\s+по\s+цепочке:\s*найдено\s*(\d+)/iu);
|
||||
if (!summaryMatch?.[1] || !summaryMatch?.[2]) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
counterparty: deps.toNonEmptyString(summaryMatch[1]),
|
||||
document_count: Number(summaryMatch[2]),
|
||||
direct_answer: summaryMatch[0].replace(/\s+/g, " ").trim()
|
||||
};
|
||||
}
|
||||
|
||||
function readCounterpartyValueFlowSummaryFromItem(item) {
|
||||
const text = deps.toNonEmptyString(item?.text);
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
const match = text.match(
|
||||
/Отдельно\s+по\s+контрагенту\s+([^:\n]+):\s*подтверждено\s+получили\s+([^,\n]+?руб\.?),\s*заплатили\s+([^,\n]+?руб\.?),\s*расчетное\s+нетто\s+в\s+нашу\s+сторону\s+([^.\n]+?руб\.?)/iu
|
||||
);
|
||||
if (!match?.[1] || !match?.[2] || !match?.[3] || !match?.[4]) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
counterparty: deps.toNonEmptyString(match[1]),
|
||||
document_count: Number(match[2]),
|
||||
direct_answer: firstLine
|
||||
incoming_customer_revenue: {
|
||||
total_amount_human_ru: deps.toNonEmptyString(match[2])
|
||||
},
|
||||
outgoing_supplier_payout: {
|
||||
total_amount_human_ru: deps.toNonEmptyString(match[3])
|
||||
},
|
||||
net_amount_human_ru: deps.toNonEmptyString(match[4]),
|
||||
net_direction: "net_incoming",
|
||||
inference_basis: "parsed_from_previous_confirmed_counterparty_boundary_summary"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -446,12 +517,51 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
for (let index = Array.isArray(items) ? items.length - 1 : -1; index >= 0; index -= 1) {
|
||||
const item = items[index];
|
||||
const debug = item?.debug;
|
||||
if (!item || item.role !== "assistant" || !debug || typeof debug !== "object") {
|
||||
const isAssistantItem = item?.role === "assistant" || item?.kind === "assistant";
|
||||
if (!item || !isAssistantItem) {
|
||||
continue;
|
||||
}
|
||||
const flow = readMcpDiscoveryBidirectionalValueFlow(debug);
|
||||
if (flow) {
|
||||
return flow;
|
||||
if (debug && typeof debug === "object") {
|
||||
const flow = readMcpDiscoveryBidirectionalValueFlow(debug);
|
||||
if (flow) {
|
||||
return flow;
|
||||
}
|
||||
}
|
||||
const parsedFlow = readCounterpartyValueFlowSummaryFromItem(item);
|
||||
if (parsedFlow) {
|
||||
return parsedFlow;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function sameCounterpartyHint(expected, actual) {
|
||||
const left = normalizeFollowupText(expected);
|
||||
const right = normalizeFollowupText(actual);
|
||||
if (!left || !right) {
|
||||
return false;
|
||||
}
|
||||
return left === right || left.includes(right) || right.includes(left);
|
||||
}
|
||||
|
||||
function findRecentPreviousCounterpartyValueFlowBundle(items, counterpartyHint = null) {
|
||||
const expectedCounterparty = deps.toNonEmptyString(counterpartyHint);
|
||||
if (!expectedCounterparty) {
|
||||
return null;
|
||||
}
|
||||
for (let index = Array.isArray(items) ? items.length - 1 : -1; index >= 0; index -= 1) {
|
||||
const item = items[index];
|
||||
const debug = item?.debug;
|
||||
const isAssistantItem = item?.role === "assistant" || item?.kind === "assistant";
|
||||
if (!item || !isAssistantItem || !debug || typeof debug !== "object") {
|
||||
continue;
|
||||
}
|
||||
const bundle = readMcpDiscoveryPreviousCounterpartyValueFlowBundle(debug);
|
||||
if (!bundle) {
|
||||
continue;
|
||||
}
|
||||
if (sameCounterpartyHint(expectedCounterparty, deps.toNonEmptyString(bundle.counterparty))) {
|
||||
return bundle;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -460,7 +570,8 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
function findRecentCounterpartyDocumentBundle(items) {
|
||||
for (let index = Array.isArray(items) ? items.length - 1 : -1; index >= 0; index -= 1) {
|
||||
const item = items[index];
|
||||
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
|
||||
const isAssistantItem = item?.role === "assistant" || item?.kind === "assistant";
|
||||
if (!item || !isAssistantItem) {
|
||||
continue;
|
||||
}
|
||||
const summary = readCounterpartyDocumentSummaryFromItem(item);
|
||||
@@ -471,6 +582,29 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function findRecentPreviousCounterpartyDocumentBundle(items, counterpartyHint = null) {
|
||||
const expectedCounterparty = deps.toNonEmptyString(counterpartyHint);
|
||||
if (!expectedCounterparty) {
|
||||
return null;
|
||||
}
|
||||
for (let index = Array.isArray(items) ? items.length - 1 : -1; index >= 0; index -= 1) {
|
||||
const item = items[index];
|
||||
const debug = item?.debug;
|
||||
const isAssistantItem = item?.role === "assistant" || item?.kind === "assistant";
|
||||
if (!item || !isAssistantItem || !debug || typeof debug !== "object") {
|
||||
continue;
|
||||
}
|
||||
const bundle = readMcpDiscoveryPreviousCounterpartyDocumentBundle(debug);
|
||||
if (!bundle) {
|
||||
continue;
|
||||
}
|
||||
if (sameCounterpartyHint(expectedCounterparty, deps.toNonEmptyString(bundle.counterparty))) {
|
||||
return bundle;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function hasInventoryPurchaseDateVatBridgeSignal(userMessage, alternateMessage, sourceIntentHint, hasInventoryItemFocusHint) {
|
||||
if (
|
||||
sourceIntentHint !== "inventory_purchase_provenance_for_item" &&
|
||||
@@ -681,10 +815,22 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
? hasShortValueFlowRetargetCue(String(alternateMessage ?? "")) ||
|
||||
hasCompactCashflowFollowupCue(String(alternateMessage ?? ""))
|
||||
: false);
|
||||
const earlyNavigationSessionState = resolveNavigationSessionContextState(
|
||||
addressNavigationState,
|
||||
deps.toNonEmptyString,
|
||||
deps.normalizeOrganizationScopeValue
|
||||
);
|
||||
const earlyNavigationFocusObject = earlyNavigationSessionState.focusObject;
|
||||
const selectedCounterpartyDocumentFollowupSignal = Boolean(
|
||||
deps.toNonEmptyString(earlyNavigationFocusObject?.label) &&
|
||||
deps.toNonEmptyString(earlyNavigationFocusObject?.objectType) === "counterparty" &&
|
||||
hasSelectedCounterpartyDocumentFollowupSignal(userMessage, alternateMessage)
|
||||
);
|
||||
if (
|
||||
assistantTurnMeaning?.stale_replay_forbidden === true &&
|
||||
!hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage) &&
|
||||
!compactCashflowFollowupSignal
|
||||
!compactCashflowFollowupSignal &&
|
||||
!selectedCounterpartyDocumentFollowupSignal
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
@@ -784,11 +930,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
|
||||
const hasBusinessOverviewCarryoverSourceHint =
|
||||
sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
|
||||
const navigationSessionState = resolveNavigationSessionContextState(
|
||||
addressNavigationState,
|
||||
deps.toNonEmptyString,
|
||||
deps.normalizeOrganizationScopeValue
|
||||
);
|
||||
const navigationSessionState = earlyNavigationSessionState;
|
||||
const navigationFocusObjectHint = navigationSessionState.focusObject;
|
||||
const hasNavigationInventoryItemFocusHint = Boolean(
|
||||
deps.toNonEmptyString(navigationFocusObjectHint?.label) &&
|
||||
@@ -912,6 +1054,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
Boolean(debtRoleSwapIntent) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
selectedCounterpartyDocumentFollowupSignal ||
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
inventoryMarginRankingFollowup ||
|
||||
@@ -937,6 +1080,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
Boolean(debtRoleSwapIntent) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
selectedCounterpartyDocumentFollowupSignal ||
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
inventoryMarginRankingFollowup ||
|
||||
@@ -969,6 +1113,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
!hasInventoryRootRestatementAlternate &&
|
||||
!shortValueFlowRetargetPrimary &&
|
||||
!shortValueFlowRetargetAlternate &&
|
||||
!selectedCounterpartyDocumentFollowupSignal &&
|
||||
!hasImplicitContinuationSignal &&
|
||||
!hasSuggestedIntentPivotSignal &&
|
||||
!hasOrganizationClarificationContinuation &&
|
||||
@@ -987,6 +1132,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
!hasInventoryRootRestatementAlternate &&
|
||||
!shortValueFlowRetargetPrimary &&
|
||||
!shortValueFlowRetargetAlternate &&
|
||||
!selectedCounterpartyDocumentFollowupSignal &&
|
||||
!hasImplicitContinuationSignal &&
|
||||
!hasSuggestedIntentPivotSignal &&
|
||||
!hasOrganizationClarificationContinuation &&
|
||||
@@ -1062,9 +1208,22 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
carryoverSourceDebug,
|
||||
deps.toNonEmptyString
|
||||
);
|
||||
const sourceDiscoveryCounterpartyHint =
|
||||
sourceDiscoveryLoopMetadataScopeHint ??
|
||||
(deps.toNonEmptyString(earlyNavigationFocusObject?.objectType) === "counterparty"
|
||||
? deps.toNonEmptyString(earlyNavigationFocusObject?.label)
|
||||
: null);
|
||||
const sourceDiscoveryBidirectionalValueFlow =
|
||||
readMcpDiscoveryBidirectionalValueFlow(carryoverSourceDebug) ?? findRecentDiscoveryValueFlowBundle(items);
|
||||
const sourceDiscoveryDocumentSummary = findRecentCounterpartyDocumentBundle(items);
|
||||
readMcpDiscoveryBidirectionalValueFlow(carryoverSourceDebug) ??
|
||||
readMcpDiscoveryPreviousCounterpartyValueFlowBundle(carryoverSourceDebug) ??
|
||||
findRecentPreviousCounterpartyValueFlowBundle(items, sourceDiscoveryCounterpartyHint) ??
|
||||
readCounterpartyValueFlowSummaryFromItem(previousAddressItem) ??
|
||||
findRecentDiscoveryValueFlowBundle(items);
|
||||
const sourceDiscoveryDocumentSummary =
|
||||
readMcpDiscoveryPreviousCounterpartyDocumentBundle(carryoverSourceDebug) ??
|
||||
findRecentPreviousCounterpartyDocumentBundle(items, sourceDiscoveryCounterpartyHint) ??
|
||||
readCounterpartyDocumentSummaryFromItem(previousAddressItem) ??
|
||||
findRecentCounterpartyDocumentBundle(items);
|
||||
const llmExplicitIntent = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
|
||||
const llmSelectedObjectScopeDetected =
|
||||
llmPreDecomposeMeta?.predecomposeContract?.semantics?.selected_object_scope_detected === true;
|
||||
@@ -1240,6 +1399,17 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
let resolvedCounterpartyFromDisplay = false;
|
||||
let displayedEntityTargetIntent = null;
|
||||
let previousFilters = resolveAddressDebugCarryoverFilters(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const navigationCounterpartyFocus =
|
||||
navigationFocusObjectType === "counterparty" ? navigationFocusObjectLabel : null;
|
||||
const hasNavigationCounterpartyFocusCarryover = Boolean(
|
||||
navigationCounterpartyFocus &&
|
||||
(hasValueFlowCarryoverSourceHint ||
|
||||
sourceIntentHint === "list_contracts_by_counterparty" ||
|
||||
sourceIntentHint === "list_documents_by_counterparty" ||
|
||||
sourceIntentHint === "bank_operations_by_counterparty" ||
|
||||
sourceIntentHint === "open_items_by_counterparty_or_contract" ||
|
||||
sourceDiscoveryLoopSelectedChainIdHint === "value_flow_comparison")
|
||||
);
|
||||
const shouldBackfillHistoricalPartyAnchors =
|
||||
sourceIntentHint === "list_contracts_by_counterparty" ||
|
||||
sourceIntentHint === "list_documents_by_counterparty" ||
|
||||
@@ -1254,6 +1424,15 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
deps.findRecentAddressFilterValue(items, "counterparty"),
|
||||
deps.toNonEmptyString
|
||||
);
|
||||
if (hasNavigationCounterpartyFocusCarryover && navigationCounterpartyFocus) {
|
||||
if (!previousAnchor) {
|
||||
previousAnchorType = "counterparty";
|
||||
previousAnchor = navigationCounterpartyFocus;
|
||||
}
|
||||
if (!deps.toNonEmptyString(previousFilters.counterparty)) {
|
||||
previousFilters.counterparty = navigationCounterpartyFocus;
|
||||
}
|
||||
}
|
||||
const historicalOrganization = deps.findRecentAddressFilterValue(items, "organization");
|
||||
const authorityActiveOrganization =
|
||||
deps.normalizeOrganizationScopeValue(organizationAuthority.activeOrganization) ??
|
||||
|
||||
@@ -65,6 +65,14 @@ export interface AddressNavigationEvent {
|
||||
export interface AddressNavigationSessionContext {
|
||||
active_result_set_id: string | null;
|
||||
active_focus_object: AddressFocusObject | null;
|
||||
comparison_scope: {
|
||||
organization: AddressFocusObject | null;
|
||||
counterparty: AddressFocusObject | null;
|
||||
proof_bundles: {
|
||||
counterparty_value_flow_bundle: Record<string, unknown> | null;
|
||||
counterparty_document_bundle: Record<string, unknown> | null;
|
||||
} | null;
|
||||
} | null;
|
||||
last_confirmed_route: string | null;
|
||||
date_scope: {
|
||||
as_of_date: string | null;
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
} from "./assistantRuntimeContracts";
|
||||
|
||||
export type AddressIntent =
|
||||
| "business_overview"
|
||||
| "period_coverage_profile"
|
||||
| "document_type_and_account_section_profile"
|
||||
| "counterparty_population_and_roles"
|
||||
|
||||
@@ -529,6 +529,11 @@ export interface AssistantDebugPayload {
|
||||
fa_live_route_audit?: FaLiveRouteAuditDebug;
|
||||
eligibility_time_basis?: GroundedAnswerEligibilityGuardDebug["eligibility_time_basis"];
|
||||
grounded_answer_eligibility_guard?: GroundedAnswerEligibilityGuardDebug;
|
||||
mcp_discovery_response_applied?: boolean;
|
||||
mcp_discovery_selected_chain_id?: string | null;
|
||||
mcp_discovery_effective_response_route?: string | null;
|
||||
assistant_mcp_discovery_entry_point_v1?: unknown;
|
||||
mcp_discovery_response_candidate_v1?: unknown;
|
||||
followup_state_usage?: FollowupStateUsageDebug;
|
||||
problem_centric_answer_applied?: boolean;
|
||||
problem_units_used_count?: number;
|
||||
|
||||
Reference in New Issue
Block a user