ARCH: ввести broad business evaluation bridge

This commit is contained in:
2026-04-21 19:37:37 +03:00
parent d323dcd509
commit bda7ca9cc1
29 changed files with 1648 additions and 97 deletions
@@ -284,6 +284,7 @@ export function runAssistantAddressLaneResponseRuntime<ResponseType = AssistantM
const mcpDiscoveryResponsePolicy = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: guardedResponse.assistantReply,
currentReplySource: "address_query_runtime_v1",
currentReplyType: guardedResponse.replyType,
addressRuntimeMeta: debugWithResponseGuard
});
const finalAssistantReply = mcpDiscoveryResponsePolicy.applied
@@ -285,7 +285,7 @@ export async function buildAssistantAddressOrchestrationRuntime(
);
}
const followupContext = carryover?.followupContext ?? null;
const followupContext = toRecordObject(carryover?.followupContext);
const routePolicyRuntime = runAssistantRoutePolicyRuntime({
rawUserMessage: input.userMessage,
effectiveAddressUserMessage: addressInputMessage,
@@ -313,7 +313,8 @@ export async function buildAssistantAddressOrchestrationRuntime(
userMessage: input.userMessage,
effectiveMessage: addressInputMessage,
assistantTurnMeaning: toRecordObject(orchestrationContract?.assistant_turn_meaning),
predecomposeContract
predecomposeContract,
followupContext
})) as Record<string, unknown>;
} catch (error) {
mcpDiscoveryRuntimeEntryPointError = String(error instanceof Error ? error.message : error ?? "unknown_error").slice(0, 280);
@@ -108,6 +108,23 @@ function toRecordObject(value: unknown): Record<string, unknown> | null {
return value as Record<string, unknown>;
}
function candidateValue(value: unknown): string | null {
const direct = fallbackToNonEmptyString(value);
if (direct && direct !== "[object Object]") {
return direct;
}
const record = toRecordObject(value);
if (!record) {
return null;
}
return (
fallbackToNonEmptyString(record.value) ??
fallbackToNonEmptyString(record.name) ??
fallbackToNonEmptyString(record.ref) ??
fallbackToNonEmptyString(record.text)
);
}
function readAssistantMcpDiscoveryEntry(
debug: Record<string, unknown> | null
): Record<string, unknown> | null {
@@ -125,6 +142,13 @@ function readAssistantMcpDiscoveryTurnMeaning(
return toRecordObject(turnInput?.turn_meaning_ref);
}
function readAssistantMcpDiscoveryActionFamily(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string | null {
return toNonEmptyString(readAssistantMcpDiscoveryTurnMeaning(debug)?.asked_action_family);
}
function readAssistantMcpDiscoveryBridge(
debug: Record<string, unknown> | null
): Record<string, unknown> | null {
@@ -140,6 +164,94 @@ export function readAssistantMcpDiscoveryPilotScope(
return toNonEmptyString(pilot?.pilot_scope);
}
function mapAssistantMcpDiscoveryPilotScopeToAddressIntent(
pilotScope: string | null,
actionFamily: string | null
): string | null {
if (pilotScope === "counterparty_lifecycle_query_documents_v1") {
return "counterparty_activity_lifecycle";
}
if (pilotScope === "counterparty_supplier_payout_query_movements_v1") {
return "supplier_payouts_profile";
}
if (pilotScope === "counterparty_value_flow_query_movements_v1") {
return "customer_revenue_and_payments";
}
if (pilotScope === "counterparty_bidirectional_value_flow_query_movements_v1") {
return null;
}
if (actionFamily === "activity_duration") {
return "counterparty_activity_lifecycle";
}
if (actionFamily === "payout") {
return "supplier_payouts_profile";
}
if (actionFamily === "turnover") {
return "customer_revenue_and_payments";
}
return null;
}
function readDiscoveryDateScopeFilters(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): {
asOfDate: string | null;
periodFrom: string | null;
periodTo: string | null;
} {
const explicitDateScope = toNonEmptyString(readAssistantMcpDiscoveryTurnMeaning(debug)?.explicit_date_scope);
if (!explicitDateScope) {
return {
asOfDate: null,
periodFrom: null,
periodTo: null
};
}
const isoDateMatch = explicitDateScope.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (isoDateMatch) {
return {
asOfDate: explicitDateScope,
periodFrom: null,
periodTo: null
};
}
const monthMatch = explicitDateScope.match(/^(\d{4})-(\d{2})$/);
if (monthMatch) {
const year = Number(monthMatch[1]);
const month = Number(monthMatch[2]);
if (Number.isFinite(year) && Number.isFinite(month) && month >= 1 && month <= 12) {
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
return {
asOfDate: null,
periodFrom: `${monthMatch[1]}-${monthMatch[2]}-01`,
periodTo: `${monthMatch[1]}-${monthMatch[2]}-${String(lastDay).padStart(2, "0")}`
};
}
}
const yearMatch = explicitDateScope.match(/^(\d{4})$/);
if (yearMatch) {
return {
asOfDate: null,
periodFrom: `${yearMatch[1]}-01-01`,
periodTo: `${yearMatch[1]}-12-31`
};
}
const rangeMatch = explicitDateScope.match(/^(\d{4}-\d{2}-\d{2})\.\.(\d{4}-\d{2}-\d{2})$/);
if (rangeMatch) {
return {
asOfDate: null,
periodFrom: rangeMatch[1],
periodTo: rangeMatch[2]
};
}
return {
asOfDate: null,
periodFrom: null,
periodTo: null
};
}
function formatDiscoveryDateScopeForReply(value: unknown): string | null {
const text = fallbackToNonEmptyString(value);
if (!text) {
@@ -220,7 +332,7 @@ export function readAddressDebugCounterparty(
? discoveryMeaning?.explicit_entity_candidates
: [];
for (const entity of explicitEntities) {
const text = toNonEmptyString(entity);
const text = candidateValue(entity);
if (text) {
return text;
}
@@ -228,6 +340,20 @@ export function readAddressDebugCounterparty(
return null;
}
export function readAddressDebugIntent(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string | null {
const detectedIntent = toNonEmptyString(debug?.detected_intent);
if (detectedIntent && detectedIntent !== "unknown") {
return detectedIntent;
}
return mapAssistantMcpDiscoveryPilotScopeToAddressIntent(
readAssistantMcpDiscoveryPilotScope(debug, toNonEmptyString),
readAssistantMcpDiscoveryActionFamily(debug, toNonEmptyString)
);
}
export function readAddressDebugOrganization(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
@@ -261,10 +387,20 @@ export function readAddressDebugTemporalScope(
): AssistantAddressDebugTemporalScope {
const extractedFilters = readAddressDebugFilters(debug);
const rootFrameContext = toRecordObject(debug?.address_root_frame_context);
const discoveryDateScope = readDiscoveryDateScopeFilters(debug, toNonEmptyString);
return {
asOfDate: toNonEmptyString(extractedFilters?.as_of_date) ?? toNonEmptyString(rootFrameContext?.as_of_date),
periodFrom: toNonEmptyString(extractedFilters?.period_from) ?? toNonEmptyString(rootFrameContext?.period_from),
periodTo: toNonEmptyString(extractedFilters?.period_to) ?? toNonEmptyString(rootFrameContext?.period_to)
asOfDate:
toNonEmptyString(extractedFilters?.as_of_date) ??
toNonEmptyString(rootFrameContext?.as_of_date) ??
discoveryDateScope.asOfDate,
periodFrom:
toNonEmptyString(extractedFilters?.period_from) ??
toNonEmptyString(rootFrameContext?.period_from) ??
discoveryDateScope.periodFrom,
periodTo:
toNonEmptyString(extractedFilters?.period_to) ??
toNonEmptyString(rootFrameContext?.period_to) ??
discoveryDateScope.periodTo
};
}
@@ -364,6 +500,24 @@ export function resolveAddressDebugCarryoverFilters(
): Record<string, unknown> {
const extractedFilters = readAddressDebugFilters(debug);
const nextFilters = extractedFilters ? { ...extractedFilters } : {};
const discoveryDateScope = readDiscoveryDateScopeFilters(debug, toNonEmptyString);
const counterparty = readAddressDebugCounterparty(debug, toNonEmptyString);
const organization = readAddressDebugOrganization(debug, toNonEmptyString);
if (counterparty && !toNonEmptyString(nextFilters.counterparty)) {
nextFilters.counterparty = counterparty;
}
if (organization && !toNonEmptyString(nextFilters.organization)) {
nextFilters.organization = organization;
}
if (discoveryDateScope.asOfDate && !toNonEmptyString(nextFilters.as_of_date)) {
nextFilters.as_of_date = discoveryDateScope.asOfDate;
}
if (discoveryDateScope.periodFrom && !toNonEmptyString(nextFilters.period_from)) {
nextFilters.period_from = discoveryDateScope.periodFrom;
}
if (discoveryDateScope.periodTo && !toNonEmptyString(nextFilters.period_to)) {
nextFilters.period_to = discoveryDateScope.periodTo;
}
const inventoryRootFrame = buildInventoryRootFrameFromAddressDebug(debug, toNonEmptyString);
const rootFilters =
inventoryRootFrame?.filters && typeof inventoryRootFrame.filters === "object"
@@ -1,5 +1,6 @@
import {
buildAddressMemoryRecapReply as buildAddressMemoryRecapReplyFromPolicy,
buildBroadBusinessEvaluationReply as buildBroadBusinessEvaluationReplyFromPolicy,
buildSelectedObjectAnswerInspectionReply as buildSelectedObjectAnswerInspectionReplyFromPolicy,
buildInventoryHistoryCapabilityFollowupReply as buildInventoryHistoryCapabilityFollowupReplyFromPolicy,
resolveAssistantLivingChatMemoryContext
@@ -191,10 +192,26 @@ export async function runAssistantLivingChatRuntime(
? "deterministic_data_scope_contract_live"
: "deterministic_data_scope_contract";
} else if (unsupportedCurrentTurnMeaningBoundary) {
chatText = buildUnsupportedCurrentTurnMeaningBoundaryReply({
assistantTurnMeaning
});
livingChatSource = "deterministic_unsupported_current_turn_boundary";
const unsupportedFamily =
typeof assistantTurnMeaning?.unsupported_but_understood_family === "string"
? assistantTurnMeaning.unsupported_but_understood_family
: null;
if (unsupportedFamily === "broad_business_evaluation") {
const scopedOrganization = selectedOrganization ?? activeOrganization ?? continuityActiveOrganization ?? null;
chatText = buildBroadBusinessEvaluationReplyFromPolicy({
organization: scopedOrganization,
addressDebug: continuitySnapshot.lastGroundedAddressDebug,
sessionItems: input.sessionItems,
toNonEmptyString: input.toNonEmptyString
});
activeOrganization = scopedOrganization ?? activeOrganization;
livingChatSource = "deterministic_broad_business_evaluation_contract";
} else {
chatText = buildUnsupportedCurrentTurnMeaningBoundaryReply({
assistantTurnMeaning
});
livingChatSource = "deterministic_unsupported_current_turn_boundary";
}
} else if ((selectedOrganization || activeOrganization) && input.hasOrganizationFactLookupSignal(userMessage)) {
const scopedOrganization = selectedOrganization ?? activeOrganization ?? null;
chatText = input.buildAssistantOrganizationFactBoundaryReply(scopedOrganization);
@@ -111,6 +111,13 @@ function isUnsupportedCurrentTurnBoundary(input: ApplyAssistantMcpDiscoveryRespo
);
}
function isDeterministicBroadBusinessEvaluationReply(input: ApplyAssistantMcpDiscoveryResponsePolicyInput): boolean {
return (
input.livingChatSource === "deterministic_broad_business_evaluation_contract" ||
input.currentReplySource === "deterministic_broad_business_evaluation_contract"
);
}
function isDiscoveryReadyChatCandidate(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
@@ -295,6 +302,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
const candidate = buildAssistantMcpDiscoveryResponseCandidate(entryPoint);
const reasonCodes = [...candidate.reason_codes];
const unsupportedBoundary = isUnsupportedCurrentTurnBoundary(input);
const deterministicBroadBusinessEvaluationReply = isDeterministicBroadBusinessEvaluationReply(input);
const discoveryReadyChatCandidate = isDiscoveryReadyChatCandidate(input, entryPoint);
const discoveryReadyDeepCandidate = isDiscoveryReadyDeepCandidate(input, entryPoint);
const discoveryReadyAddressCandidate = isDiscoveryReadyAddressCandidate(input, entryPoint);
@@ -330,6 +338,12 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
if (fullConfirmedFactualAddressReply) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply");
}
if (deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") {
pushReason(
reasonCodes,
"mcp_discovery_response_policy_keep_broad_business_summary_over_clarification_candidate"
);
}
if (!ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status)) {
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_status_not_allowed");
}
@@ -349,6 +363,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
!alignedFactualAddressReply &&
!matchedFactualAddressContinuationTarget &&
!fullConfirmedFactualAddressReply &&
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
candidate.eligible_for_future_hot_runtime &&
Boolean(toNonEmptyString(candidate.reply_text)) &&
@@ -7,12 +7,14 @@ export type AssistantMcpDiscoveryTurnInputStatus = "ready" | "needs_more_context
export type AssistantMcpDiscoveryTurnInputSource =
| "assistant_turn_meaning"
| "predecompose_contract"
| "followup_context"
| "raw_text"
| "none";
export interface BuildAssistantMcpDiscoveryTurnInputAdapterInput {
assistantTurnMeaning?: Record<string, unknown> | null;
predecomposeContract?: Record<string, unknown> | null;
followupContext?: Record<string, unknown> | null;
userMessage?: string | null;
effectiveMessage?: string | null;
}
@@ -132,6 +134,148 @@ function collectDateScope(predecompose: Record<string, unknown> | null): string
return periodFrom ?? periodTo ?? null;
}
function collectDateScopeFromFilters(filters: Record<string, unknown> | null): string | null {
if (!filters) {
return null;
}
const asOfDate = toNonEmptyString(filters.as_of_date);
const periodFrom = toNonEmptyString(filters.period_from);
const periodTo = toNonEmptyString(filters.period_to);
if (asOfDate) {
return asOfDate;
}
const yearFrom = periodFrom?.match(/^(\d{4})-01-01$/);
const yearTo = periodTo?.match(/^(\d{4})-12-31$/);
if (yearFrom && yearTo && yearFrom[1] === yearTo[1]) {
return yearFrom[1];
}
if (periodFrom && periodTo) {
return `${periodFrom}..${periodTo}`;
}
return periodFrom ?? periodTo ?? null;
}
function mapPilotScopeToFollowupMeaning(
pilotScope: string | null
): {
domain: string | null;
action: string | null;
unsupported: string | null;
} {
if (pilotScope === "counterparty_lifecycle_query_documents_v1") {
return {
domain: "counterparty_lifecycle",
action: "activity_duration",
unsupported: "counterparty_lifecycle"
};
}
if (pilotScope === "counterparty_supplier_payout_query_movements_v1") {
return {
domain: "counterparty_value",
action: "payout",
unsupported: "counterparty_payouts_or_outflow"
};
}
if (pilotScope === "counterparty_value_flow_query_movements_v1") {
return {
domain: "counterparty_value",
action: "turnover",
unsupported: "counterparty_value_or_turnover"
};
}
if (pilotScope === "counterparty_bidirectional_value_flow_query_movements_v1") {
return {
domain: "counterparty_value",
action: "net_value_flow",
unsupported: "counterparty_bidirectional_value_flow_or_netting"
};
}
return {
domain: null,
action: null,
unsupported: null
};
}
function mapAddressIntentToFollowupMeaning(
intent: string | null
): {
domain: string | null;
action: string | null;
unsupported: string | null;
} {
if (intent === "counterparty_activity_lifecycle") {
return {
domain: "counterparty_lifecycle",
action: "activity_duration",
unsupported: "counterparty_lifecycle"
};
}
if (intent === "supplier_payouts_profile") {
return {
domain: "counterparty_value",
action: "payout",
unsupported: "counterparty_payouts_or_outflow"
};
}
if (intent === "customer_revenue_and_payments") {
return {
domain: "counterparty_value",
action: "turnover",
unsupported: "counterparty_value_or_turnover"
};
}
return {
domain: null,
action: null,
unsupported: null
};
}
function collectFollowupDiscoverySeed(followupContext: Record<string, unknown> | null): {
pilotScope: string | null;
domain: string | null;
action: string | null;
unsupported: string | null;
counterparty: string | null;
organization: string | null;
dateScope: string | null;
} {
const previousFilters = toRecordObject(followupContext?.previous_filters);
const rootFilters = toRecordObject(followupContext?.root_filters);
const pilotScope = toNonEmptyString(followupContext?.previous_discovery_pilot_scope);
const previousIntent =
toNonEmptyString(followupContext?.target_intent) ?? toNonEmptyString(followupContext?.previous_intent);
const mapped =
mapPilotScopeToFollowupMeaning(pilotScope).domain !== null
? mapPilotScopeToFollowupMeaning(pilotScope)
: mapAddressIntentToFollowupMeaning(previousIntent);
const counterparty =
toNonEmptyString(previousFilters?.counterparty) ??
toNonEmptyString(rootFilters?.counterparty) ??
(toNonEmptyString(followupContext?.previous_anchor_type) === "counterparty"
? toNonEmptyString(followupContext?.previous_anchor_value)
: null);
const organization =
toNonEmptyString(previousFilters?.organization) ??
toNonEmptyString(rootFilters?.organization) ??
(toNonEmptyString(followupContext?.previous_anchor_type) === "organization"
? toNonEmptyString(followupContext?.previous_anchor_value)
: null);
const dateScope =
collectDateScopeFromFilters(previousFilters) ??
collectDateScopeFromFilters(rootFilters);
return {
pilotScope,
domain: mapped.domain,
action: mapped.action,
unsupported: mapped.unsupported,
counterparty,
organization,
dateScope
};
}
function hasLifecycleSignal(text: string): boolean {
return /(?:сколько\s+лет|как\s+давно|давно\s+ли|возраст|перв(?:ая|ый)\s+актив|когда\s+начал|когда\s+появ|lifecycle|activity\s+duration|business\s+age|how\s+long)/iu.test(
text
@@ -162,6 +306,26 @@ function hasMonthlyAggregationSignal(text: string): boolean {
);
}
function hasExplicitDateScopeLiteral(text: string): boolean {
return /(?:\b(?:19|20)\d{2}\b|\b\d{4}-\d{2}-\d{2}\b|\b\d{4}-\d{2}\b)/iu.test(text);
}
function collectDateScopeFromRawText(text: string): string | null {
const isoDate = text.match(/\b(\d{4}-\d{2}-\d{2})\b/u);
if (isoDate?.[1]) {
return isoDate[1];
}
const yearMonth = text.match(/\b(\d{4}-\d{2})\b/u);
if (yearMonth?.[1]) {
return yearMonth[1];
}
const year = text.match(/\b((?:19|20)\d{2})\b/u);
if (year?.[1]) {
return year[1];
}
return null;
}
function semanticNeedFor(input: {
domain: string | null;
action: string | null;
@@ -191,6 +355,7 @@ function shouldRunDiscovery(input: {
valueFlowSignal: boolean;
semanticDataNeed: string | null;
explicitIntentCandidate: string | null;
followupDiscoverySeedApplicable: boolean;
}): boolean {
if (input.lifecycleSignal || input.unsupported) {
return true;
@@ -198,6 +363,9 @@ function shouldRunDiscovery(input: {
if (input.valueFlowSignal && !input.explicitIntentCandidate) {
return true;
}
if (input.followupDiscoverySeedApplicable && !input.explicitIntentCandidate && input.semanticDataNeed) {
return true;
}
if (!input.explicitIntentCandidate && input.semanticDataNeed) {
return true;
}
@@ -209,37 +377,75 @@ export function buildAssistantMcpDiscoveryTurnInput(
): AssistantMcpDiscoveryTurnInputContract {
const assistantTurnMeaning = toRecordObject(input.assistantTurnMeaning);
const predecomposeContract = toRecordObject(input.predecomposeContract);
const followupContext = toRecordObject(input.followupContext);
const predecomposeEntities = collectPredecomposeEntities(predecomposeContract);
const followupSeed = collectFollowupDiscoverySeed(followupContext);
const reasonCodes: string[] = [];
const rawText = compactLower(`${input.userMessage ?? ""} ${input.effectiveMessage ?? ""}`);
const lifecycleSignal = hasLifecycleSignal(rawText);
const bidirectionalValueFlowSignal = !lifecycleSignal && hasBidirectionalValueFlowSignal(rawText);
const valueFlowSignal = !lifecycleSignal && (hasValueFlowSignal(rawText) || bidirectionalValueFlowSignal);
const payoutSignal = valueFlowSignal && !bidirectionalValueFlowSignal && hasPayoutSignal(rawText);
const monthlyAggregationSignal = valueFlowSignal && hasMonthlyAggregationSignal(rawText);
const rawLifecycleSignal = hasLifecycleSignal(rawText);
const rawBidirectionalValueFlowSignal = !rawLifecycleSignal && hasBidirectionalValueFlowSignal(rawText);
const rawValueFlowSignal =
!rawLifecycleSignal && (hasValueFlowSignal(rawText) || rawBidirectionalValueFlowSignal);
const rawPayoutSignal = rawValueFlowSignal && !rawBidirectionalValueFlowSignal && hasPayoutSignal(rawText);
const monthlyAggregationSignal = hasMonthlyAggregationSignal(rawText);
const explicitDateScopeLiteralDetected = hasExplicitDateScopeLiteral(rawText);
const rawDateScope = collectDateScopeFromRawText(rawText);
const rawDomain = toNonEmptyString(assistantTurnMeaning?.asked_domain_family);
const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family);
const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis);
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate);
const assistantTurnMeaningDateScope = toNonEmptyString(assistantTurnMeaning?.explicit_date_scope);
const assistantTurnMeaningOrganizationScope = toNonEmptyString(assistantTurnMeaning?.explicit_organization_scope);
const predecomposeDateScope = collectDateScope(predecomposeContract);
const followupDiscoverySeedApplicable = Boolean(
followupSeed.domain &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
(monthlyAggregationSignal || explicitDateScopeLiteralDetected || predecomposeDateScope)
);
const seededDomain = followupDiscoverySeedApplicable ? followupSeed.domain : null;
const seededAction = followupDiscoverySeedApplicable ? followupSeed.action : null;
const seededUnsupported = followupDiscoverySeedApplicable ? followupSeed.unsupported : null;
const lifecycleSignal =
rawLifecycleSignal || seededDomain === "counterparty_lifecycle";
const bidirectionalValueFlowSignal =
!lifecycleSignal &&
(rawBidirectionalValueFlowSignal || seededAction === "net_value_flow");
const valueFlowSignal =
!lifecycleSignal && (rawValueFlowSignal || seededDomain === "counterparty_value");
const payoutSignal =
valueFlowSignal &&
!bidirectionalValueFlowSignal &&
(rawPayoutSignal || seededAction === "payout");
const semanticDataNeed = semanticNeedFor({
domain: rawDomain,
action: rawAction,
unsupported,
domain: rawDomain ?? seededDomain,
action: rawAction ?? seededAction,
unsupported: unsupported ?? seededUnsupported,
lifecycleSignal,
valueFlowSignal
});
const entityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates);
pushUnique(entityCandidates, predecomposeEntities.counterparty);
if (valueFlowSignal && !predecomposeEntities.counterparty) {
pushUnique(entityCandidates, followupSeed.counterparty);
if (valueFlowSignal && !predecomposeEntities.counterparty && !followupSeed.counterparty) {
pushUnique(entityCandidates, predecomposeEntities.organization);
pushUnique(entityCandidates, followupSeed.organization);
}
const explicitOrganizationScope =
valueFlowSignal && !predecomposeEntities.counterparty ? null : predecomposeEntities.organization;
valueFlowSignal && !predecomposeEntities.counterparty && !followupSeed.counterparty
? null
: predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope ?? followupSeed.organization;
const explicitDateScope = assistantTurnMeaningDateScope ?? predecomposeDateScope ?? rawDateScope ?? followupSeed.dateScope;
const turnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {
asked_domain_family: lifecycleSignal ? "counterparty_lifecycle" : valueFlowSignal ? "counterparty_value" : rawDomain,
asked_domain_family:
lifecycleSignal
? "counterparty_lifecycle"
: valueFlowSignal
? "counterparty_value"
: rawDomain ?? seededDomain,
asked_action_family: lifecycleSignal
? "activity_duration"
: valueFlowSignal
@@ -247,12 +453,12 @@ export function buildAssistantMcpDiscoveryTurnInput(
? "net_value_flow"
: payoutSignal
? "payout"
: "turnover"
: rawAction,
: rawAction ?? seededAction ?? "turnover"
: rawAction ?? seededAction,
asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis,
explicit_entity_candidates: entityCandidates,
explicit_organization_scope: explicitOrganizationScope,
explicit_date_scope: collectDateScope(predecomposeContract),
explicit_date_scope: explicitDateScope,
unsupported_but_understood_family:
unsupported ??
(lifecycleSignal
@@ -262,9 +468,17 @@ export function buildAssistantMcpDiscoveryTurnInput(
? "counterparty_bidirectional_value_flow_or_netting"
: payoutSignal
? "counterparty_payouts_or_outflow"
: "counterparty_value_or_turnover"
: null),
stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden || unsupported || lifecycleSignal || valueFlowSignal)
: seededUnsupported ?? "counterparty_value_or_turnover"
: followupDiscoverySeedApplicable
? seededUnsupported
: null),
stale_replay_forbidden: Boolean(
assistantTurnMeaning?.stale_replay_forbidden ||
unsupported ||
lifecycleSignal ||
valueFlowSignal ||
followupDiscoverySeedApplicable
)
};
const cleanTurnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {};
@@ -294,15 +508,18 @@ export function buildAssistantMcpDiscoveryTurnInput(
}
const runDiscovery = shouldRunDiscovery({
unsupported,
unsupported: unsupported ?? seededUnsupported,
lifecycleSignal,
valueFlowSignal,
semanticDataNeed,
explicitIntentCandidate
explicitIntentCandidate,
followupDiscoverySeedApplicable
});
const hasTurnMeaning = Object.keys(cleanTurnMeaning).length > 0;
const sourceSignal: AssistantMcpDiscoveryTurnInputSource = assistantTurnMeaning
? "assistant_turn_meaning"
: followupDiscoverySeedApplicable
? "followup_context"
: predecomposeContract
? "predecompose_contract"
: lifecycleSignal
@@ -326,12 +543,21 @@ export function buildAssistantMcpDiscoveryTurnInput(
if (monthlyAggregationSignal) {
pushReason(reasonCodes, "mcp_discovery_monthly_aggregation_signal_detected");
}
if (followupDiscoverySeedApplicable) {
pushReason(reasonCodes, "mcp_discovery_seeded_from_followup_context");
}
if (unsupported) {
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
}
if (predecomposeEntities.counterparty) {
pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose");
}
if (followupSeed.counterparty) {
pushReason(reasonCodes, "mcp_discovery_counterparty_from_followup_context");
}
if (followupSeed.dateScope) {
pushReason(reasonCodes, "mcp_discovery_date_scope_from_followup_context");
}
if (entityCandidates.length > 0) {
pushReason(reasonCodes, "mcp_discovery_entity_scope_available");
}
@@ -62,6 +62,14 @@ function toRecordObject(value: unknown): Record<string, unknown> | null {
return value as Record<string, unknown>;
}
function ensureSentence(value: string): string {
const text = String(value ?? "").trim();
if (!text) {
return "";
}
return /[.!?]$/.test(text) ? text : `${text}.`;
}
function periodPartForRecap(scopedDate: string | null): string {
if (!scopedDate) {
return "";
@@ -353,6 +361,38 @@ export function buildAddressMemoryRecapReply(input: {
return "Да, помню предыдущий адресный контур. Могу кратко напомнить, что мы уже подтвердили, или сразу продолжить следующий шаг.";
}
export function buildBroadBusinessEvaluationReply(input: {
organization: string | null;
addressDebug: Record<string, unknown> | null;
sessionItems?: unknown[];
toNonEmptyString: (value: unknown) => string | null;
}): string {
const contextFacts = resolveAddressDebugContextFacts(input.addressDebug, input.toNonEmptyString);
const organization = input.organization ?? contextFacts.organization;
const recapFacts = collectRecentRecapFacts({
sessionItems: input.sessionItems,
item: null,
organization,
toNonEmptyString: input.toNonEmptyString
});
const organizationPart = organization ? ` по компании «${organization}»` : "";
if (recapFacts.length > 0) {
return [
`Коротко: по тому, что мы уже подтвердили в 1С${organizationPart}, компания выглядит операционно живой, но это пока только частичная оценка бизнеса.`,
"Сейчас я опираюсь на такие подтвержденные факты:",
...recapFacts.map((fact) => `- ${ensureSentence(fact)}`),
"Это еще не полная диагностика всего бизнеса и не вывод о прибыли: я честно суммирую только те контуры, которые мы уже проверили в диалоге.",
"Если хочешь, следующим шагом могу сузить оценку до денежного потока, долгов, НДС или ключевых контрагентов."
].join("\n");
}
return [
`Коротко: по нынешнему контексту 1С${organizationPart} я вижу признаки операционной активности, но для содержательной оценки бизнеса нужно еще несколько опорных срезов.`,
"Если хочешь, я быстро доберу основу для такой оценки: денежный поток, дебиторка/кредиторка, НДС или ключевые контрагенты."
].join(" ");
}
export function buildSelectedObjectAnswerInspectionReply(input: {
addressDebug: Record<string, unknown> | null;
toNonEmptyString: (value: unknown) => string | null;
@@ -7,9 +7,11 @@ import {
buildRootScopedCarryoverFilters,
buildInventoryRootFrameFromAddressDebug,
hydrateInventoryRootFrameState,
readAddressDebugIntent,
readAddressDebugFilters,
readAddressDebugItem,
readAddressDebugTemporalScope,
readAssistantMcpDiscoveryPilotScope,
resolveOrganizationClarificationContinuation,
resolveNavigationSessionContextState,
resolveAddressDebugCarryoverFilters,
@@ -171,19 +173,23 @@ export function createAssistantTransitionPolicy(deps) {
return false;
}
const executionLane = deps.toNonEmptyString(debug.execution_lane);
const detectedIntent = deps.toNonEmptyString(debug.detected_intent);
const detectedIntent = readAddressDebugIntent(debug, deps.toNonEmptyString);
const selectedRecipe = deps.toNonEmptyString(debug.selected_recipe);
const answerGroundingCheck =
debug.answer_grounding_check && typeof debug.answer_grounding_check === "object"
? debug.answer_grounding_check
: null;
const groundingStatus = deps.toNonEmptyString(answerGroundingCheck?.status);
const discoveryPilotScope = readAssistantMcpDiscoveryPilotScope(debug, deps.toNonEmptyString);
if (groundingStatus === "grounded") {
return true;
}
if (selectedRecipe) {
return true;
}
if (debug.mcp_discovery_response_applied === true && discoveryPilotScope) {
return true;
}
return executionLane === "address_query" && Boolean(detectedIntent && detectedIntent !== "unknown");
}
@@ -438,7 +444,7 @@ export function createAssistantTransitionPolicy(deps) {
(deps.toNonEmptyString(alternateMessage)
? deps.isImplicitAddressContinuationByLlm(alternateMessage, llmPreDecomposeMeta)
: false));
const sourceIntentHint = deps.toNonEmptyString(carryoverSourceDebug?.detected_intent);
const sourceIntentHint = readAddressDebugIntent(carryoverSourceDebug, deps.toNonEmptyString);
const navigationSessionState = resolveNavigationSessionContextState(
addressNavigationState,
deps.toNonEmptyString,
@@ -599,7 +605,8 @@ export function createAssistantTransitionPolicy(deps) {
if (!carryoverSourceDebug) {
return null;
}
const sourceIntent = deps.toNonEmptyString(carryoverSourceDebug.detected_intent);
const sourceIntent = readAddressDebugIntent(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryPilotScope = readAssistantMcpDiscoveryPilotScope(carryoverSourceDebug, deps.toNonEmptyString);
const llmExplicitIntent = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
const llmSelectedObjectScopeDetected =
llmPreDecomposeMeta?.predecomposeContract?.semantics?.selected_object_scope_detected === true;
@@ -931,6 +938,7 @@ export function createAssistantTransitionPolicy(deps) {
previous_filters: previousFilters,
previous_anchor_type: previousAnchorType ?? undefined,
previous_anchor_value: previousAnchor,
previous_discovery_pilot_scope: sourceDiscoveryPilotScope ?? undefined,
resolved_counterparty_from_display: resolvedCounterpartyFromDisplay || undefined,
root_context_only: rootScopedPivot || undefined,
root_intent: shouldAttachInventoryRootFrame ? inventoryRootFrame?.intent ?? undefined : undefined,
@@ -117,6 +117,23 @@ function detectCounterpartyTurnoverFamily(text) {
};
}
function detectBroadBusinessEvaluation(text) {
const normalized = String(text ?? "");
if (!normalized) {
return null;
}
if (
/(?:как\s+ты\s+оценишь\s+деятельност[ьи]\s+компан|оценк[аи]?\s+деятельност[ьи]\s+компан|что\s+у\s+нас\s+вообще\s+происход|где\s+главн(?:ые|ый)\s+риски|как\s+у\s+нас\s+дела\s+по\s+компан)/iu.test(
normalized
)
) {
return {
family: "broad_business_evaluation"
};
}
return null;
}
function buildEntityCandidates(counterpartyTurnover) {
if (!counterpartyTurnover?.entity) {
return [];
@@ -139,10 +156,17 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
const joinedText = fallbackCompactWhitespace(`${rawText} ${effectiveText}`);
const supportedIntent = detectSupportedIntent(joinedText, deps);
const counterpartyTurnover = detectCounterpartyTurnoverFamily(joinedText);
const broadBusinessEvaluation = detectBroadBusinessEvaluation(joinedText);
const llmIntent = toNonEmptyString(input?.llmPreDecomposeMeta?.predecomposeContract?.intent, deps);
const explicitIntentCandidate =
supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null);
const unsupportedFamily = !explicitIntentCandidate && counterpartyTurnover?.family ? counterpartyTurnover.family : null;
broadBusinessEvaluation?.family
? null
: supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null);
const unsupportedFamily = broadBusinessEvaluation?.family
? broadBusinessEvaluation.family
: !explicitIntentCandidate && counterpartyTurnover?.family
? counterpartyTurnover.family
: null;
const reasonCodes = [];
if (supportedIntent?.reason) {
reasonCodes.push(supportedIntent.reason);
@@ -150,6 +174,9 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
if (counterpartyTurnover?.family) {
reasonCodes.push("counterparty_turnover_current_turn_signal");
}
if (broadBusinessEvaluation?.family) {
reasonCodes.push("broad_business_evaluation_current_turn_signal");
}
if (rawText !== normalizeTurnText(rawMessage, { ...deps, repairAddressMojibake: (value) => String(value ?? "") })) {
reasonCodes.push("mojibake_repair_applied");
}
@@ -168,6 +195,8 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
? "vat"
: explicitIntentCandidate?.startsWith("inventory_")
? "inventory"
: broadBusinessEvaluation?.family
? "business_summary"
: explicitIntentCandidate?.includes("counterparty")
? "counterparty"
: counterpartyTurnover?.family
@@ -178,6 +207,8 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
explicitIntentCandidate === "payables_confirmed_as_of_date" ||
explicitIntentCandidate === "inventory_on_hand_as_of_date"
? "confirmed_snapshot"
: broadBusinessEvaluation?.family
? "broad_evaluation"
: explicitIntentCandidate === "vat_liability_confirmed_for_tax_period"
? "confirmed_tax_period"
: explicitIntentCandidate === "vat_payable_confirmed_as_of_date"
@@ -189,7 +220,9 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
: counterpartyTurnover?.family
? "counterparty_value_or_turnover"
: null;
const staleReplayForbidden = Boolean(unsupportedFamily || (counterpartyTurnover?.entity && !explicitIntentCandidate));
const staleReplayForbidden = Boolean(
unsupportedFamily || broadBusinessEvaluation?.family || (counterpartyTurnover?.entity && !explicitIntentCandidate)
);
return {
schema_version: "assistant_turn_meaning_v1",
raw_message: rawMessage,
@@ -200,7 +233,9 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
asked_action_family: askedActionFamily,
explicit_intent_candidate: explicitIntentCandidate,
explicit_entity_candidates: buildEntityCandidates(counterpartyTurnover),
meaning_confidence: supportedIntent?.confidence ?? (counterpartyTurnover?.family ? "medium" : "low"),
meaning_confidence: broadBusinessEvaluation?.family
? "medium"
: supportedIntent?.confidence ?? (counterpartyTurnover?.family ? "medium" : "low"),
intent_override_strength: explicitIntentCandidate
? "explicit_current_turn_intent"
: staleReplayForbidden