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

This commit is contained in:
dctouch 2026-04-21 19:37:37 +03:00
parent d323dcd509
commit bda7ca9cc1
29 changed files with 1648 additions and 97 deletions

View File

@ -0,0 +1,78 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase22_broad_business_evaluation_bridge",
"domain": "address_phase22_broad_business_evaluation_bridge",
"title": "Phase 22 broad business evaluation bridge replay",
"description": "Targeted AGENT replay for the broad business evaluation seam where a follow-up like 'Как ты оценишь деятельность компании?' must not replay stale lifecycle routing and should still preserve the chain for the next exact net-flow question.",
"bindings": {},
"steps": [
{
"step_id": "step_01_company_activity_lifecycle",
"title": "Lifecycle answer seeds grounded organization context",
"question": "а по Альтернативе Плюс сколько лет активности в базе 1С?",
"allowed_reply_types": [
"partial_coverage",
"factual",
"factual_with_explanation"
],
"required_answer_patterns_any": [
"(?i)лет",
"(?i)активност",
"(?i)1с",
"(?i)не получил|не подтвержден|проверил доступный контур"
],
"criticality": "critical",
"semantic_tags": [
"company_activity_lifecycle",
"grounded_context_seed"
]
},
{
"step_id": "step_02_broad_business_evaluation",
"title": "Broad business evaluation becomes grounded summary instead of stale lifecycle dump",
"question": "Как ты оценишь деятельность компании?",
"required_answer_patterns_all": [
"(?i)коротко|оценк|частичн",
"(?i)1с|подтвержд",
"(?i)денежн|долг|ндс|контрагент|операц"
],
"forbidden_answer_patterns": [
"(?i)активных заказчиков",
"(?i)последняя активность",
"(?i)^\\s*1\\."
],
"criticality": "critical",
"semantic_tags": [
"broad_business_evaluation",
"grounded_summary"
]
},
{
"step_id": "step_03_net_flow_after_broad_eval",
"title": "Exact net-flow follow-up still answers after the broad bridge",
"question": "какое нетто по деньгам с Группа СВК за 2020 год: сколько получили и сколько заплатили?",
"allowed_reply_types": [
"partial_coverage",
"factual_with_explanation"
],
"required_answer_patterns_all": [
"(?i)свк",
"(?i)получил|входящ|поступ",
"(?i)заплат|исходящ|списан|плат[её]ж",
"(?i)нетто|сальдо|разниц",
"(?i)2020|период",
"(?i)руб"
],
"forbidden_answer_patterns": [
"(?i)активных заказчиков",
"(?i)лет в базе",
"(?i)последняя активность"
],
"criticality": "critical",
"semantic_tags": [
"counterparty_net_cash_flow",
"broad_eval_bridge_preserved"
]
}
]
}

View File

@ -225,6 +225,7 @@ function runAssistantAddressLaneResponseRuntime(input) {
const mcpDiscoveryResponsePolicy = (0, assistantMcpDiscoveryResponsePolicy_1.applyAssistantMcpDiscoveryResponsePolicy)({ const mcpDiscoveryResponsePolicy = (0, assistantMcpDiscoveryResponsePolicy_1.applyAssistantMcpDiscoveryResponsePolicy)({
currentReply: guardedResponse.assistantReply, currentReply: guardedResponse.assistantReply,
currentReplySource: "address_query_runtime_v1", currentReplySource: "address_query_runtime_v1",
currentReplyType: guardedResponse.replyType,
addressRuntimeMeta: debugWithResponseGuard addressRuntimeMeta: debugWithResponseGuard
}); });
const finalAssistantReply = mcpDiscoveryResponsePolicy.applied const finalAssistantReply = mcpDiscoveryResponsePolicy.applied

View File

@ -136,7 +136,7 @@ async function buildAssistantAddressOrchestrationRuntime(input) {
}; };
carryover = input.resolveAddressFollowupCarryoverContext(input.userMessage, input.sessionItems, addressInputMessage, addressPreDecompose, input.sessionAddressNavigationState); carryover = input.resolveAddressFollowupCarryoverContext(input.userMessage, input.sessionItems, addressInputMessage, addressPreDecompose, input.sessionAddressNavigationState);
} }
const followupContext = carryover?.followupContext ?? null; const followupContext = toRecordObject(carryover?.followupContext);
const routePolicyRuntime = (0, assistantRoutePolicyRuntimeAdapter_1.runAssistantRoutePolicyRuntime)({ const routePolicyRuntime = (0, assistantRoutePolicyRuntimeAdapter_1.runAssistantRoutePolicyRuntime)({
rawUserMessage: input.userMessage, rawUserMessage: input.userMessage,
effectiveAddressUserMessage: addressInputMessage, effectiveAddressUserMessage: addressInputMessage,
@ -159,7 +159,8 @@ async function buildAssistantAddressOrchestrationRuntime(input) {
userMessage: input.userMessage, userMessage: input.userMessage,
effectiveMessage: addressInputMessage, effectiveMessage: addressInputMessage,
assistantTurnMeaning: toRecordObject(orchestrationContract?.assistant_turn_meaning), assistantTurnMeaning: toRecordObject(orchestrationContract?.assistant_turn_meaning),
predecomposeContract predecomposeContract,
followupContext
})); }));
} }
catch (error) { catch (error) {

View File

@ -5,6 +5,7 @@ exports.formatIsoDateForReply = formatIsoDateForReply;
exports.readAddressDebugFilters = readAddressDebugFilters; exports.readAddressDebugFilters = readAddressDebugFilters;
exports.readAddressDebugItem = readAddressDebugItem; exports.readAddressDebugItem = readAddressDebugItem;
exports.readAddressDebugCounterparty = readAddressDebugCounterparty; exports.readAddressDebugCounterparty = readAddressDebugCounterparty;
exports.readAddressDebugIntent = readAddressDebugIntent;
exports.readAddressDebugOrganization = readAddressDebugOrganization; exports.readAddressDebugOrganization = readAddressDebugOrganization;
exports.readAddressDebugScopedDate = readAddressDebugScopedDate; exports.readAddressDebugScopedDate = readAddressDebugScopedDate;
exports.readAddressDebugTemporalScope = readAddressDebugTemporalScope; exports.readAddressDebugTemporalScope = readAddressDebugTemporalScope;
@ -43,6 +44,20 @@ function toRecordObject(value) {
} }
return value; return value;
} }
function candidateValue(value) {
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) { function readAssistantMcpDiscoveryEntry(debug) {
const entry = toRecordObject(debug?.assistant_mcp_discovery_entry_point_v1); const entry = toRecordObject(debug?.assistant_mcp_discovery_entry_point_v1);
return fallbackToNonEmptyString(entry?.schema_version) === "assistant_mcp_discovery_runtime_entry_point_v1" return fallbackToNonEmptyString(entry?.schema_version) === "assistant_mcp_discovery_runtime_entry_point_v1"
@ -54,6 +69,9 @@ function readAssistantMcpDiscoveryTurnMeaning(debug) {
const turnInput = toRecordObject(entry?.turn_input); const turnInput = toRecordObject(entry?.turn_input);
return toRecordObject(turnInput?.turn_meaning_ref); return toRecordObject(turnInput?.turn_meaning_ref);
} }
function readAssistantMcpDiscoveryActionFamily(debug, toNonEmptyString = fallbackToNonEmptyString) {
return toNonEmptyString(readAssistantMcpDiscoveryTurnMeaning(debug)?.asked_action_family);
}
function readAssistantMcpDiscoveryBridge(debug) { function readAssistantMcpDiscoveryBridge(debug) {
return toRecordObject(readAssistantMcpDiscoveryEntry(debug)?.bridge); return toRecordObject(readAssistantMcpDiscoveryEntry(debug)?.bridge);
} }
@ -62,6 +80,82 @@ function readAssistantMcpDiscoveryPilotScope(debug, toNonEmptyString = fallbackT
const pilot = toRecordObject(bridge?.pilot); const pilot = toRecordObject(bridge?.pilot);
return toNonEmptyString(pilot?.pilot_scope); return toNonEmptyString(pilot?.pilot_scope);
} }
function mapAssistantMcpDiscoveryPilotScopeToAddressIntent(pilotScope, actionFamily) {
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, toNonEmptyString = fallbackToNonEmptyString) {
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) { function formatDiscoveryDateScopeForReply(value) {
const text = fallbackToNonEmptyString(value); const text = fallbackToNonEmptyString(value);
if (!text) { if (!text) {
@ -122,13 +216,20 @@ function readAddressDebugCounterparty(debug, toNonEmptyString = fallbackToNonEmp
? discoveryMeaning?.explicit_entity_candidates ? discoveryMeaning?.explicit_entity_candidates
: []; : [];
for (const entity of explicitEntities) { for (const entity of explicitEntities) {
const text = toNonEmptyString(entity); const text = candidateValue(entity);
if (text) { if (text) {
return text; return text;
} }
} }
return null; return null;
} }
function readAddressDebugIntent(debug, toNonEmptyString = fallbackToNonEmptyString) {
const detectedIntent = toNonEmptyString(debug?.detected_intent);
if (detectedIntent && detectedIntent !== "unknown") {
return detectedIntent;
}
return mapAssistantMcpDiscoveryPilotScopeToAddressIntent(readAssistantMcpDiscoveryPilotScope(debug, toNonEmptyString), readAssistantMcpDiscoveryActionFamily(debug, toNonEmptyString));
}
function readAddressDebugOrganization(debug, toNonEmptyString = fallbackToNonEmptyString) { function readAddressDebugOrganization(debug, toNonEmptyString = fallbackToNonEmptyString) {
const extractedFilters = readAddressDebugFilters(debug); const extractedFilters = readAddressDebugFilters(debug);
const rootFrameContext = toRecordObject(debug?.address_root_frame_context); const rootFrameContext = toRecordObject(debug?.address_root_frame_context);
@ -150,10 +251,17 @@ function readAddressDebugScopedDate(debug) {
function readAddressDebugTemporalScope(debug, toNonEmptyString = fallbackToNonEmptyString) { function readAddressDebugTemporalScope(debug, toNonEmptyString = fallbackToNonEmptyString) {
const extractedFilters = readAddressDebugFilters(debug); const extractedFilters = readAddressDebugFilters(debug);
const rootFrameContext = toRecordObject(debug?.address_root_frame_context); const rootFrameContext = toRecordObject(debug?.address_root_frame_context);
const discoveryDateScope = readDiscoveryDateScopeFilters(debug, toNonEmptyString);
return { return {
asOfDate: toNonEmptyString(extractedFilters?.as_of_date) ?? toNonEmptyString(rootFrameContext?.as_of_date), asOfDate: toNonEmptyString(extractedFilters?.as_of_date) ??
periodFrom: toNonEmptyString(extractedFilters?.period_from) ?? toNonEmptyString(rootFrameContext?.period_from), toNonEmptyString(rootFrameContext?.as_of_date) ??
periodTo: toNonEmptyString(extractedFilters?.period_to) ?? toNonEmptyString(rootFrameContext?.period_to) discoveryDateScope.asOfDate,
periodFrom: toNonEmptyString(extractedFilters?.period_from) ??
toNonEmptyString(rootFrameContext?.period_from) ??
discoveryDateScope.periodFrom,
periodTo: toNonEmptyString(extractedFilters?.period_to) ??
toNonEmptyString(rootFrameContext?.period_to) ??
discoveryDateScope.periodTo
}; };
} }
function resolveAddressDebugAnchorContext(debug, toNonEmptyString = fallbackToNonEmptyString) { function resolveAddressDebugAnchorContext(debug, toNonEmptyString = fallbackToNonEmptyString) {
@ -234,6 +342,24 @@ function resolveAddressDebugContextFacts(debug, toNonEmptyString = fallbackToNon
function resolveAddressDebugCarryoverFilters(debug, toNonEmptyString = fallbackToNonEmptyString) { function resolveAddressDebugCarryoverFilters(debug, toNonEmptyString = fallbackToNonEmptyString) {
const extractedFilters = readAddressDebugFilters(debug); const extractedFilters = readAddressDebugFilters(debug);
const nextFilters = extractedFilters ? { ...extractedFilters } : {}; 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 inventoryRootFrame = buildInventoryRootFrameFromAddressDebug(debug, toNonEmptyString);
const rootFilters = inventoryRootFrame?.filters && typeof inventoryRootFrame.filters === "object" const rootFilters = inventoryRootFrame?.filters && typeof inventoryRootFrame.filters === "object"
? inventoryRootFrame.filters ? inventoryRootFrame.filters

View File

@ -113,11 +113,27 @@ async function runAssistantLivingChatRuntime(input) {
: "deterministic_data_scope_contract"; : "deterministic_data_scope_contract";
} }
else if (unsupportedCurrentTurnMeaningBoundary) { else if (unsupportedCurrentTurnMeaningBoundary) {
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 = (0, assistantMemoryRecapPolicy_1.buildBroadBusinessEvaluationReply)({
organization: scopedOrganization,
addressDebug: continuitySnapshot.lastGroundedAddressDebug,
sessionItems: input.sessionItems,
toNonEmptyString: input.toNonEmptyString
});
activeOrganization = scopedOrganization ?? activeOrganization;
livingChatSource = "deterministic_broad_business_evaluation_contract";
}
else {
chatText = buildUnsupportedCurrentTurnMeaningBoundaryReply({ chatText = buildUnsupportedCurrentTurnMeaningBoundaryReply({
assistantTurnMeaning assistantTurnMeaning
}); });
livingChatSource = "deterministic_unsupported_current_turn_boundary"; livingChatSource = "deterministic_unsupported_current_turn_boundary";
} }
}
else if ((selectedOrganization || activeOrganization) && input.hasOrganizationFactLookupSignal(userMessage)) { else if ((selectedOrganization || activeOrganization) && input.hasOrganizationFactLookupSignal(userMessage)) {
const scopedOrganization = selectedOrganization ?? activeOrganization ?? null; const scopedOrganization = selectedOrganization ?? activeOrganization ?? null;
chatText = input.buildAssistantOrganizationFactBoundaryReply(scopedOrganization); chatText = input.buildAssistantOrganizationFactBoundaryReply(scopedOrganization);

View File

@ -66,6 +66,10 @@ function isUnsupportedCurrentTurnBoundary(input) {
input.livingChatSource === "deterministic_unsupported_current_turn_boundary" || input.livingChatSource === "deterministic_unsupported_current_turn_boundary" ||
input.currentReplySource === "deterministic_unsupported_current_turn_boundary"); input.currentReplySource === "deterministic_unsupported_current_turn_boundary");
} }
function isDeterministicBroadBusinessEvaluationReply(input) {
return (input.livingChatSource === "deterministic_broad_business_evaluation_contract" ||
input.currentReplySource === "deterministic_broad_business_evaluation_contract");
}
function isDiscoveryReadyChatCandidate(input, entryPoint) { function isDiscoveryReadyChatCandidate(input, entryPoint) {
const turnInput = toRecordObject(entryPoint?.turn_input); const turnInput = toRecordObject(entryPoint?.turn_input);
return (entryPoint?.entry_status === "bridge_executed" && return (entryPoint?.entry_status === "bridge_executed" &&
@ -202,6 +206,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
const candidate = (0, assistantMcpDiscoveryResponseCandidate_1.buildAssistantMcpDiscoveryResponseCandidate)(entryPoint); const candidate = (0, assistantMcpDiscoveryResponseCandidate_1.buildAssistantMcpDiscoveryResponseCandidate)(entryPoint);
const reasonCodes = [...candidate.reason_codes]; const reasonCodes = [...candidate.reason_codes];
const unsupportedBoundary = isUnsupportedCurrentTurnBoundary(input); const unsupportedBoundary = isUnsupportedCurrentTurnBoundary(input);
const deterministicBroadBusinessEvaluationReply = isDeterministicBroadBusinessEvaluationReply(input);
const discoveryReadyChatCandidate = isDiscoveryReadyChatCandidate(input, entryPoint); const discoveryReadyChatCandidate = isDiscoveryReadyChatCandidate(input, entryPoint);
const discoveryReadyDeepCandidate = isDiscoveryReadyDeepCandidate(input, entryPoint); const discoveryReadyDeepCandidate = isDiscoveryReadyDeepCandidate(input, entryPoint);
const discoveryReadyAddressCandidate = isDiscoveryReadyAddressCandidate(input, entryPoint); const discoveryReadyAddressCandidate = isDiscoveryReadyAddressCandidate(input, entryPoint);
@ -236,6 +241,9 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
if (fullConfirmedFactualAddressReply) { if (fullConfirmedFactualAddressReply) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply"); 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)) { if (!ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status)) {
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_status_not_allowed"); pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_status_not_allowed");
} }
@ -253,6 +261,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
!alignedFactualAddressReply && !alignedFactualAddressReply &&
!matchedFactualAddressContinuationTarget && !matchedFactualAddressContinuationTarget &&
!fullConfirmedFactualAddressReply && !fullConfirmedFactualAddressReply &&
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) && ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
candidate.eligible_for_future_hot_runtime && candidate.eligible_for_future_hot_runtime &&
Boolean(toNonEmptyString(candidate.reply_text)) && Boolean(toNonEmptyString(candidate.reply_text)) &&

View File

@ -92,6 +92,119 @@ function collectDateScope(predecompose) {
} }
return periodFrom ?? periodTo ?? null; return periodFrom ?? periodTo ?? null;
} }
function collectDateScopeFromFilters(filters) {
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) {
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) {
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) {
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) { function hasLifecycleSignal(text) {
return /(?:сколько\s+лет|как\s+давно|давно\s+ли|возраст|перв(?:ая|ый)\s+актив|когда\s+начал|когда\s+появ|lifecycle|activity\s+duration|business\s+age|how\s+long)/iu.test(text); return /(?:сколько\s+лет|как\s+давно|давно\s+ли|возраст|перв(?:ая|ый)\s+актив|когда\s+начал|когда\s+появ|lifecycle|activity\s+duration|business\s+age|how\s+long)/iu.test(text);
} }
@ -107,6 +220,24 @@ function hasBidirectionalValueFlowSignal(text) {
function hasMonthlyAggregationSignal(text) { function hasMonthlyAggregationSignal(text) {
return /(?:\u043f\u043e\s+\u043c\u0435\u0441\u044f\u0446\u0430\u043c|\u043f\u043e\u043c\u0435\u0441\u044f\u0447\u043d\u043e|\u0435\u0436\u0435\u043c\u0435\u0441\u044f\u0447\u043d\u043e|month\s+by\s+month|by\s+month|monthly)/iu.test(text); return /(?:\u043f\u043e\s+\u043c\u0435\u0441\u044f\u0446\u0430\u043c|\u043f\u043e\u043c\u0435\u0441\u044f\u0447\u043d\u043e|\u0435\u0436\u0435\u043c\u0435\u0441\u044f\u0447\u043d\u043e|month\s+by\s+month|by\s+month|monthly)/iu.test(text);
} }
function hasExplicitDateScopeLiteral(text) {
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) {
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) { function semanticNeedFor(input) {
const combined = compactLower(`${input.domain ?? ""} ${input.action ?? ""} ${input.unsupported ?? ""}`); const combined = compactLower(`${input.domain ?? ""} ${input.action ?? ""} ${input.unsupported ?? ""}`);
if (input.lifecycleSignal || /(?:lifecycle|activity|duration|age)/iu.test(combined)) { if (input.lifecycleSignal || /(?:lifecycle|activity|duration|age)/iu.test(combined)) {
@ -130,6 +261,9 @@ function shouldRunDiscovery(input) {
if (input.valueFlowSignal && !input.explicitIntentCandidate) { if (input.valueFlowSignal && !input.explicitIntentCandidate) {
return true; return true;
} }
if (input.followupDiscoverySeedApplicable && !input.explicitIntentCandidate && input.semanticDataNeed) {
return true;
}
if (!input.explicitIntentCandidate && input.semanticDataNeed) { if (!input.explicitIntentCandidate && input.semanticDataNeed) {
return true; return true;
} }
@ -138,34 +272,64 @@ function shouldRunDiscovery(input) {
function buildAssistantMcpDiscoveryTurnInput(input) { function buildAssistantMcpDiscoveryTurnInput(input) {
const assistantTurnMeaning = toRecordObject(input.assistantTurnMeaning); const assistantTurnMeaning = toRecordObject(input.assistantTurnMeaning);
const predecomposeContract = toRecordObject(input.predecomposeContract); const predecomposeContract = toRecordObject(input.predecomposeContract);
const followupContext = toRecordObject(input.followupContext);
const predecomposeEntities = collectPredecomposeEntities(predecomposeContract); const predecomposeEntities = collectPredecomposeEntities(predecomposeContract);
const followupSeed = collectFollowupDiscoverySeed(followupContext);
const reasonCodes = []; const reasonCodes = [];
const rawText = compactLower(`${input.userMessage ?? ""} ${input.effectiveMessage ?? ""}`); const rawText = compactLower(`${input.userMessage ?? ""} ${input.effectiveMessage ?? ""}`);
const lifecycleSignal = hasLifecycleSignal(rawText); const rawLifecycleSignal = hasLifecycleSignal(rawText);
const bidirectionalValueFlowSignal = !lifecycleSignal && hasBidirectionalValueFlowSignal(rawText); const rawBidirectionalValueFlowSignal = !rawLifecycleSignal && hasBidirectionalValueFlowSignal(rawText);
const valueFlowSignal = !lifecycleSignal && (hasValueFlowSignal(rawText) || bidirectionalValueFlowSignal); const rawValueFlowSignal = !rawLifecycleSignal && (hasValueFlowSignal(rawText) || rawBidirectionalValueFlowSignal);
const payoutSignal = valueFlowSignal && !bidirectionalValueFlowSignal && hasPayoutSignal(rawText); const rawPayoutSignal = rawValueFlowSignal && !rawBidirectionalValueFlowSignal && hasPayoutSignal(rawText);
const monthlyAggregationSignal = valueFlowSignal && hasMonthlyAggregationSignal(rawText); const monthlyAggregationSignal = hasMonthlyAggregationSignal(rawText);
const explicitDateScopeLiteralDetected = hasExplicitDateScopeLiteral(rawText);
const rawDateScope = collectDateScopeFromRawText(rawText);
const rawDomain = toNonEmptyString(assistantTurnMeaning?.asked_domain_family); const rawDomain = toNonEmptyString(assistantTurnMeaning?.asked_domain_family);
const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family); const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family);
const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis); const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis);
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family); const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate); 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({ const semanticDataNeed = semanticNeedFor({
domain: rawDomain, domain: rawDomain ?? seededDomain,
action: rawAction, action: rawAction ?? seededAction,
unsupported, unsupported: unsupported ?? seededUnsupported,
lifecycleSignal, lifecycleSignal,
valueFlowSignal valueFlowSignal
}); });
const entityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates); const entityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates);
pushUnique(entityCandidates, predecomposeEntities.counterparty); pushUnique(entityCandidates, predecomposeEntities.counterparty);
if (valueFlowSignal && !predecomposeEntities.counterparty) { pushUnique(entityCandidates, followupSeed.counterparty);
if (valueFlowSignal && !predecomposeEntities.counterparty && !followupSeed.counterparty) {
pushUnique(entityCandidates, predecomposeEntities.organization); pushUnique(entityCandidates, predecomposeEntities.organization);
pushUnique(entityCandidates, followupSeed.organization);
} }
const explicitOrganizationScope = valueFlowSignal && !predecomposeEntities.counterparty ? null : predecomposeEntities.organization; const explicitOrganizationScope = valueFlowSignal && !predecomposeEntities.counterparty && !followupSeed.counterparty
? null
: predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope ?? followupSeed.organization;
const explicitDateScope = assistantTurnMeaningDateScope ?? predecomposeDateScope ?? rawDateScope ?? followupSeed.dateScope;
const turnMeaning = { const turnMeaning = {
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 asked_action_family: lifecycleSignal
? "activity_duration" ? "activity_duration"
: valueFlowSignal : valueFlowSignal
@ -173,12 +337,12 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
? "net_value_flow" ? "net_value_flow"
: payoutSignal : payoutSignal
? "payout" ? "payout"
: "turnover" : rawAction ?? seededAction ?? "turnover"
: rawAction, : rawAction ?? seededAction,
asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis, asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis,
explicit_entity_candidates: entityCandidates, explicit_entity_candidates: entityCandidates,
explicit_organization_scope: explicitOrganizationScope, explicit_organization_scope: explicitOrganizationScope,
explicit_date_scope: collectDateScope(predecomposeContract), explicit_date_scope: explicitDateScope,
unsupported_but_understood_family: unsupported ?? unsupported_but_understood_family: unsupported ??
(lifecycleSignal (lifecycleSignal
? "counterparty_lifecycle" ? "counterparty_lifecycle"
@ -187,9 +351,15 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
? "counterparty_bidirectional_value_flow_or_netting" ? "counterparty_bidirectional_value_flow_or_netting"
: payoutSignal : payoutSignal
? "counterparty_payouts_or_outflow" ? "counterparty_payouts_or_outflow"
: "counterparty_value_or_turnover" : seededUnsupported ?? "counterparty_value_or_turnover"
: followupDiscoverySeedApplicable
? seededUnsupported
: null), : null),
stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden || unsupported || lifecycleSignal || valueFlowSignal) stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden ||
unsupported ||
lifecycleSignal ||
valueFlowSignal ||
followupDiscoverySeedApplicable)
}; };
const cleanTurnMeaning = {}; const cleanTurnMeaning = {};
if (toNonEmptyString(turnMeaning.asked_domain_family)) { if (toNonEmptyString(turnMeaning.asked_domain_family)) {
@ -217,15 +387,18 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
cleanTurnMeaning.stale_replay_forbidden = true; cleanTurnMeaning.stale_replay_forbidden = true;
} }
const runDiscovery = shouldRunDiscovery({ const runDiscovery = shouldRunDiscovery({
unsupported, unsupported: unsupported ?? seededUnsupported,
lifecycleSignal, lifecycleSignal,
valueFlowSignal, valueFlowSignal,
semanticDataNeed, semanticDataNeed,
explicitIntentCandidate explicitIntentCandidate,
followupDiscoverySeedApplicable
}); });
const hasTurnMeaning = Object.keys(cleanTurnMeaning).length > 0; const hasTurnMeaning = Object.keys(cleanTurnMeaning).length > 0;
const sourceSignal = assistantTurnMeaning const sourceSignal = assistantTurnMeaning
? "assistant_turn_meaning" ? "assistant_turn_meaning"
: followupDiscoverySeedApplicable
? "followup_context"
: predecomposeContract : predecomposeContract
? "predecompose_contract" ? "predecompose_contract"
: lifecycleSignal : lifecycleSignal
@ -248,12 +421,21 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
if (monthlyAggregationSignal) { if (monthlyAggregationSignal) {
pushReason(reasonCodes, "mcp_discovery_monthly_aggregation_signal_detected"); pushReason(reasonCodes, "mcp_discovery_monthly_aggregation_signal_detected");
} }
if (followupDiscoverySeedApplicable) {
pushReason(reasonCodes, "mcp_discovery_seeded_from_followup_context");
}
if (unsupported) { if (unsupported) {
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn"); pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
} }
if (predecomposeEntities.counterparty) { if (predecomposeEntities.counterparty) {
pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose"); 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) { if (entityCandidates.length > 0) {
pushReason(reasonCodes, "mcp_discovery_entity_scope_available"); pushReason(reasonCodes, "mcp_discovery_entity_scope_available");
} }

View File

@ -3,6 +3,7 @@
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.buildInventoryHistoryCapabilityFollowupReply = buildInventoryHistoryCapabilityFollowupReply; exports.buildInventoryHistoryCapabilityFollowupReply = buildInventoryHistoryCapabilityFollowupReply;
exports.buildAddressMemoryRecapReply = buildAddressMemoryRecapReply; exports.buildAddressMemoryRecapReply = buildAddressMemoryRecapReply;
exports.buildBroadBusinessEvaluationReply = buildBroadBusinessEvaluationReply;
exports.buildSelectedObjectAnswerInspectionReply = buildSelectedObjectAnswerInspectionReply; exports.buildSelectedObjectAnswerInspectionReply = buildSelectedObjectAnswerInspectionReply;
exports.resolveAssistantLivingChatMemoryContext = resolveAssistantLivingChatMemoryContext; exports.resolveAssistantLivingChatMemoryContext = resolveAssistantLivingChatMemoryContext;
exports.createAssistantMemoryRecapPolicy = createAssistantMemoryRecapPolicy; exports.createAssistantMemoryRecapPolicy = createAssistantMemoryRecapPolicy;
@ -20,6 +21,13 @@ function toRecordObject(value) {
} }
return value; return value;
} }
function ensureSentence(value) {
const text = String(value ?? "").trim();
if (!text) {
return "";
}
return /[.!?]$/.test(text) ? text : `${text}.`;
}
function periodPartForRecap(scopedDate) { function periodPartForRecap(scopedDate) {
if (!scopedDate) { if (!scopedDate) {
return ""; return "";
@ -261,6 +269,30 @@ function buildAddressMemoryRecapReply(input) {
} }
return "Да, помню предыдущий адресный контур. Могу кратко напомнить, что мы уже подтвердили, или сразу продолжить следующий шаг."; return "Да, помню предыдущий адресный контур. Могу кратко напомнить, что мы уже подтвердили, или сразу продолжить следующий шаг.";
} }
function buildBroadBusinessEvaluationReply(input) {
const contextFacts = (0, assistantContinuityPolicy_1.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(" ");
}
function buildSelectedObjectAnswerInspectionReply(input) { function buildSelectedObjectAnswerInspectionReply(input) {
const contextFacts = (0, assistantContinuityPolicy_1.resolveAddressDebugContextFacts)(input.addressDebug, input.toNonEmptyString); const contextFacts = (0, assistantContinuityPolicy_1.resolveAddressDebugContextFacts)(input.addressDebug, input.toNonEmptyString);
const itemLabel = contextFacts.item ?? "эта позиция"; const itemLabel = contextFacts.item ?? "эта позиция";

View File

@ -121,18 +121,22 @@ function createAssistantTransitionPolicy(deps) {
return false; return false;
} }
const executionLane = deps.toNonEmptyString(debug.execution_lane); const executionLane = deps.toNonEmptyString(debug.execution_lane);
const detectedIntent = deps.toNonEmptyString(debug.detected_intent); const detectedIntent = (0, assistantContinuityPolicy_1.readAddressDebugIntent)(debug, deps.toNonEmptyString);
const selectedRecipe = deps.toNonEmptyString(debug.selected_recipe); const selectedRecipe = deps.toNonEmptyString(debug.selected_recipe);
const answerGroundingCheck = debug.answer_grounding_check && typeof debug.answer_grounding_check === "object" const answerGroundingCheck = debug.answer_grounding_check && typeof debug.answer_grounding_check === "object"
? debug.answer_grounding_check ? debug.answer_grounding_check
: null; : null;
const groundingStatus = deps.toNonEmptyString(answerGroundingCheck?.status); const groundingStatus = deps.toNonEmptyString(answerGroundingCheck?.status);
const discoveryPilotScope = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryPilotScope)(debug, deps.toNonEmptyString);
if (groundingStatus === "grounded") { if (groundingStatus === "grounded") {
return true; return true;
} }
if (selectedRecipe) { if (selectedRecipe) {
return true; return true;
} }
if (debug.mcp_discovery_response_applied === true && discoveryPilotScope) {
return true;
}
return executionLane === "address_query" && Boolean(detectedIntent && detectedIntent !== "unknown"); return executionLane === "address_query" && Boolean(detectedIntent && detectedIntent !== "unknown");
} }
function findRecentUsableAddressAssistantItem(items) { function findRecentUsableAddressAssistantItem(items) {
@ -337,7 +341,7 @@ function createAssistantTransitionPolicy(deps) {
(deps.toNonEmptyString(alternateMessage) (deps.toNonEmptyString(alternateMessage)
? deps.isImplicitAddressContinuationByLlm(alternateMessage, llmPreDecomposeMeta) ? deps.isImplicitAddressContinuationByLlm(alternateMessage, llmPreDecomposeMeta)
: false)); : false));
const sourceIntentHint = deps.toNonEmptyString(carryoverSourceDebug?.detected_intent); const sourceIntentHint = (0, assistantContinuityPolicy_1.readAddressDebugIntent)(carryoverSourceDebug, deps.toNonEmptyString);
const navigationSessionState = (0, assistantContinuityPolicy_1.resolveNavigationSessionContextState)(addressNavigationState, deps.toNonEmptyString, deps.normalizeOrganizationScopeValue); const navigationSessionState = (0, assistantContinuityPolicy_1.resolveNavigationSessionContextState)(addressNavigationState, deps.toNonEmptyString, deps.normalizeOrganizationScopeValue);
const navigationFocusObjectHint = navigationSessionState.focusObject; const navigationFocusObjectHint = navigationSessionState.focusObject;
const hasNavigationInventoryItemFocusHint = Boolean(deps.toNonEmptyString(navigationFocusObjectHint?.label) && const hasNavigationInventoryItemFocusHint = Boolean(deps.toNonEmptyString(navigationFocusObjectHint?.label) &&
@ -457,7 +461,8 @@ function createAssistantTransitionPolicy(deps) {
if (!carryoverSourceDebug) { if (!carryoverSourceDebug) {
return null; return null;
} }
const sourceIntent = deps.toNonEmptyString(carryoverSourceDebug.detected_intent); const sourceIntent = (0, assistantContinuityPolicy_1.readAddressDebugIntent)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryPilotScope = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryPilotScope)(carryoverSourceDebug, deps.toNonEmptyString);
const llmExplicitIntent = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent); const llmExplicitIntent = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
const llmSelectedObjectScopeDetected = llmPreDecomposeMeta?.predecomposeContract?.semantics?.selected_object_scope_detected === true; const llmSelectedObjectScopeDetected = llmPreDecomposeMeta?.predecomposeContract?.semantics?.selected_object_scope_detected === true;
const resolvedPrimaryIntent = deps.resolveAddressIntent(deps.repairAddressMojibake(String(userMessage ?? ""))).intent; const resolvedPrimaryIntent = deps.resolveAddressIntent(deps.repairAddressMojibake(String(userMessage ?? ""))).intent;
@ -687,6 +692,7 @@ function createAssistantTransitionPolicy(deps) {
previous_filters: previousFilters, previous_filters: previousFilters,
previous_anchor_type: previousAnchorType ?? undefined, previous_anchor_type: previousAnchorType ?? undefined,
previous_anchor_value: previousAnchor, previous_anchor_value: previousAnchor,
previous_discovery_pilot_scope: sourceDiscoveryPilotScope ?? undefined,
resolved_counterparty_from_display: resolvedCounterpartyFromDisplay || undefined, resolved_counterparty_from_display: resolvedCounterpartyFromDisplay || undefined,
root_context_only: rootScopedPivot || undefined, root_context_only: rootScopedPivot || undefined,
root_intent: shouldAttachInventoryRootFrame ? inventoryRootFrame?.intent ?? undefined : undefined, root_intent: shouldAttachInventoryRootFrame ? inventoryRootFrame?.intent ?? undefined : undefined,

View File

@ -112,6 +112,18 @@ function detectCounterpartyTurnoverFamily(text) {
entity entity
}; };
} }
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) { function buildEntityCandidates(counterpartyTurnover) {
if (!counterpartyTurnover?.entity) { if (!counterpartyTurnover?.entity) {
return []; return [];
@ -133,9 +145,16 @@ function createAssistantTurnMeaningPolicy(deps = {}) {
const joinedText = fallbackCompactWhitespace(`${rawText} ${effectiveText}`); const joinedText = fallbackCompactWhitespace(`${rawText} ${effectiveText}`);
const supportedIntent = detectSupportedIntent(joinedText, deps); const supportedIntent = detectSupportedIntent(joinedText, deps);
const counterpartyTurnover = detectCounterpartyTurnoverFamily(joinedText); const counterpartyTurnover = detectCounterpartyTurnoverFamily(joinedText);
const broadBusinessEvaluation = detectBroadBusinessEvaluation(joinedText);
const llmIntent = toNonEmptyString(input?.llmPreDecomposeMeta?.predecomposeContract?.intent, deps); const llmIntent = toNonEmptyString(input?.llmPreDecomposeMeta?.predecomposeContract?.intent, deps);
const explicitIntentCandidate = supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null); const explicitIntentCandidate = broadBusinessEvaluation?.family
const unsupportedFamily = !explicitIntentCandidate && counterpartyTurnover?.family ? counterpartyTurnover.family : null; ? null
: supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null);
const unsupportedFamily = broadBusinessEvaluation?.family
? broadBusinessEvaluation.family
: !explicitIntentCandidate && counterpartyTurnover?.family
? counterpartyTurnover.family
: null;
const reasonCodes = []; const reasonCodes = [];
if (supportedIntent?.reason) { if (supportedIntent?.reason) {
reasonCodes.push(supportedIntent.reason); reasonCodes.push(supportedIntent.reason);
@ -143,6 +162,9 @@ function createAssistantTurnMeaningPolicy(deps = {}) {
if (counterpartyTurnover?.family) { if (counterpartyTurnover?.family) {
reasonCodes.push("counterparty_turnover_current_turn_signal"); 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 ?? "") })) { if (rawText !== normalizeTurnText(rawMessage, { ...deps, repairAddressMojibake: (value) => String(value ?? "") })) {
reasonCodes.push("mojibake_repair_applied"); reasonCodes.push("mojibake_repair_applied");
} }
@ -158,6 +180,8 @@ function createAssistantTurnMeaningPolicy(deps = {}) {
? "vat" ? "vat"
: explicitIntentCandidate?.startsWith("inventory_") : explicitIntentCandidate?.startsWith("inventory_")
? "inventory" ? "inventory"
: broadBusinessEvaluation?.family
? "business_summary"
: explicitIntentCandidate?.includes("counterparty") : explicitIntentCandidate?.includes("counterparty")
? "counterparty" ? "counterparty"
: counterpartyTurnover?.family : counterpartyTurnover?.family
@ -167,6 +191,8 @@ function createAssistantTurnMeaningPolicy(deps = {}) {
explicitIntentCandidate === "payables_confirmed_as_of_date" || explicitIntentCandidate === "payables_confirmed_as_of_date" ||
explicitIntentCandidate === "inventory_on_hand_as_of_date" explicitIntentCandidate === "inventory_on_hand_as_of_date"
? "confirmed_snapshot" ? "confirmed_snapshot"
: broadBusinessEvaluation?.family
? "broad_evaluation"
: explicitIntentCandidate === "vat_liability_confirmed_for_tax_period" : explicitIntentCandidate === "vat_liability_confirmed_for_tax_period"
? "confirmed_tax_period" ? "confirmed_tax_period"
: explicitIntentCandidate === "vat_payable_confirmed_as_of_date" : explicitIntentCandidate === "vat_payable_confirmed_as_of_date"
@ -178,7 +204,7 @@ function createAssistantTurnMeaningPolicy(deps = {}) {
: counterpartyTurnover?.family : counterpartyTurnover?.family
? "counterparty_value_or_turnover" ? "counterparty_value_or_turnover"
: null; : null;
const staleReplayForbidden = Boolean(unsupportedFamily || (counterpartyTurnover?.entity && !explicitIntentCandidate)); const staleReplayForbidden = Boolean(unsupportedFamily || broadBusinessEvaluation?.family || (counterpartyTurnover?.entity && !explicitIntentCandidate));
return { return {
schema_version: "assistant_turn_meaning_v1", schema_version: "assistant_turn_meaning_v1",
raw_message: rawMessage, raw_message: rawMessage,
@ -189,7 +215,9 @@ function createAssistantTurnMeaningPolicy(deps = {}) {
asked_action_family: askedActionFamily, asked_action_family: askedActionFamily,
explicit_intent_candidate: explicitIntentCandidate, explicit_intent_candidate: explicitIntentCandidate,
explicit_entity_candidates: buildEntityCandidates(counterpartyTurnover), 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 intent_override_strength: explicitIntentCandidate
? "explicit_current_turn_intent" ? "explicit_current_turn_intent"
: staleReplayForbidden : staleReplayForbidden

View File

@ -284,6 +284,7 @@ export function runAssistantAddressLaneResponseRuntime<ResponseType = AssistantM
const mcpDiscoveryResponsePolicy = applyAssistantMcpDiscoveryResponsePolicy({ const mcpDiscoveryResponsePolicy = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: guardedResponse.assistantReply, currentReply: guardedResponse.assistantReply,
currentReplySource: "address_query_runtime_v1", currentReplySource: "address_query_runtime_v1",
currentReplyType: guardedResponse.replyType,
addressRuntimeMeta: debugWithResponseGuard addressRuntimeMeta: debugWithResponseGuard
}); });
const finalAssistantReply = mcpDiscoveryResponsePolicy.applied const finalAssistantReply = mcpDiscoveryResponsePolicy.applied

View File

@ -285,7 +285,7 @@ export async function buildAssistantAddressOrchestrationRuntime(
); );
} }
const followupContext = carryover?.followupContext ?? null; const followupContext = toRecordObject(carryover?.followupContext);
const routePolicyRuntime = runAssistantRoutePolicyRuntime({ const routePolicyRuntime = runAssistantRoutePolicyRuntime({
rawUserMessage: input.userMessage, rawUserMessage: input.userMessage,
effectiveAddressUserMessage: addressInputMessage, effectiveAddressUserMessage: addressInputMessage,
@ -313,7 +313,8 @@ export async function buildAssistantAddressOrchestrationRuntime(
userMessage: input.userMessage, userMessage: input.userMessage,
effectiveMessage: addressInputMessage, effectiveMessage: addressInputMessage,
assistantTurnMeaning: toRecordObject(orchestrationContract?.assistant_turn_meaning), assistantTurnMeaning: toRecordObject(orchestrationContract?.assistant_turn_meaning),
predecomposeContract predecomposeContract,
followupContext
})) as Record<string, unknown>; })) as Record<string, unknown>;
} catch (error) { } catch (error) {
mcpDiscoveryRuntimeEntryPointError = String(error instanceof Error ? error.message : error ?? "unknown_error").slice(0, 280); mcpDiscoveryRuntimeEntryPointError = String(error instanceof Error ? error.message : error ?? "unknown_error").slice(0, 280);

View File

@ -108,6 +108,23 @@ function toRecordObject(value: unknown): Record<string, unknown> | null {
return value as Record<string, unknown>; 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( function readAssistantMcpDiscoveryEntry(
debug: Record<string, unknown> | null debug: Record<string, unknown> | null
): Record<string, unknown> | null { ): Record<string, unknown> | null {
@ -125,6 +142,13 @@ function readAssistantMcpDiscoveryTurnMeaning(
return toRecordObject(turnInput?.turn_meaning_ref); 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( function readAssistantMcpDiscoveryBridge(
debug: Record<string, unknown> | null debug: Record<string, unknown> | null
): Record<string, unknown> | null { ): Record<string, unknown> | null {
@ -140,6 +164,94 @@ export function readAssistantMcpDiscoveryPilotScope(
return toNonEmptyString(pilot?.pilot_scope); 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 { function formatDiscoveryDateScopeForReply(value: unknown): string | null {
const text = fallbackToNonEmptyString(value); const text = fallbackToNonEmptyString(value);
if (!text) { if (!text) {
@ -220,7 +332,7 @@ export function readAddressDebugCounterparty(
? discoveryMeaning?.explicit_entity_candidates ? discoveryMeaning?.explicit_entity_candidates
: []; : [];
for (const entity of explicitEntities) { for (const entity of explicitEntities) {
const text = toNonEmptyString(entity); const text = candidateValue(entity);
if (text) { if (text) {
return text; return text;
} }
@ -228,6 +340,20 @@ export function readAddressDebugCounterparty(
return null; 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( export function readAddressDebugOrganization(
debug: Record<string, unknown> | null, debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
@ -261,10 +387,20 @@ export function readAddressDebugTemporalScope(
): AssistantAddressDebugTemporalScope { ): AssistantAddressDebugTemporalScope {
const extractedFilters = readAddressDebugFilters(debug); const extractedFilters = readAddressDebugFilters(debug);
const rootFrameContext = toRecordObject(debug?.address_root_frame_context); const rootFrameContext = toRecordObject(debug?.address_root_frame_context);
const discoveryDateScope = readDiscoveryDateScopeFilters(debug, toNonEmptyString);
return { return {
asOfDate: toNonEmptyString(extractedFilters?.as_of_date) ?? toNonEmptyString(rootFrameContext?.as_of_date), asOfDate:
periodFrom: toNonEmptyString(extractedFilters?.period_from) ?? toNonEmptyString(rootFrameContext?.period_from), toNonEmptyString(extractedFilters?.as_of_date) ??
periodTo: toNonEmptyString(extractedFilters?.period_to) ?? toNonEmptyString(rootFrameContext?.period_to) 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> { ): Record<string, unknown> {
const extractedFilters = readAddressDebugFilters(debug); const extractedFilters = readAddressDebugFilters(debug);
const nextFilters = extractedFilters ? { ...extractedFilters } : {}; 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 inventoryRootFrame = buildInventoryRootFrameFromAddressDebug(debug, toNonEmptyString);
const rootFilters = const rootFilters =
inventoryRootFrame?.filters && typeof inventoryRootFrame.filters === "object" inventoryRootFrame?.filters && typeof inventoryRootFrame.filters === "object"

View File

@ -1,5 +1,6 @@
import { import {
buildAddressMemoryRecapReply as buildAddressMemoryRecapReplyFromPolicy, buildAddressMemoryRecapReply as buildAddressMemoryRecapReplyFromPolicy,
buildBroadBusinessEvaluationReply as buildBroadBusinessEvaluationReplyFromPolicy,
buildSelectedObjectAnswerInspectionReply as buildSelectedObjectAnswerInspectionReplyFromPolicy, buildSelectedObjectAnswerInspectionReply as buildSelectedObjectAnswerInspectionReplyFromPolicy,
buildInventoryHistoryCapabilityFollowupReply as buildInventoryHistoryCapabilityFollowupReplyFromPolicy, buildInventoryHistoryCapabilityFollowupReply as buildInventoryHistoryCapabilityFollowupReplyFromPolicy,
resolveAssistantLivingChatMemoryContext resolveAssistantLivingChatMemoryContext
@ -191,10 +192,26 @@ export async function runAssistantLivingChatRuntime(
? "deterministic_data_scope_contract_live" ? "deterministic_data_scope_contract_live"
: "deterministic_data_scope_contract"; : "deterministic_data_scope_contract";
} else if (unsupportedCurrentTurnMeaningBoundary) { } else if (unsupportedCurrentTurnMeaningBoundary) {
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({ chatText = buildUnsupportedCurrentTurnMeaningBoundaryReply({
assistantTurnMeaning assistantTurnMeaning
}); });
livingChatSource = "deterministic_unsupported_current_turn_boundary"; livingChatSource = "deterministic_unsupported_current_turn_boundary";
}
} else if ((selectedOrganization || activeOrganization) && input.hasOrganizationFactLookupSignal(userMessage)) { } else if ((selectedOrganization || activeOrganization) && input.hasOrganizationFactLookupSignal(userMessage)) {
const scopedOrganization = selectedOrganization ?? activeOrganization ?? null; const scopedOrganization = selectedOrganization ?? activeOrganization ?? null;
chatText = input.buildAssistantOrganizationFactBoundaryReply(scopedOrganization); chatText = input.buildAssistantOrganizationFactBoundaryReply(scopedOrganization);

View File

@ -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( function isDiscoveryReadyChatCandidate(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput, input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
@ -295,6 +302,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
const candidate = buildAssistantMcpDiscoveryResponseCandidate(entryPoint); const candidate = buildAssistantMcpDiscoveryResponseCandidate(entryPoint);
const reasonCodes = [...candidate.reason_codes]; const reasonCodes = [...candidate.reason_codes];
const unsupportedBoundary = isUnsupportedCurrentTurnBoundary(input); const unsupportedBoundary = isUnsupportedCurrentTurnBoundary(input);
const deterministicBroadBusinessEvaluationReply = isDeterministicBroadBusinessEvaluationReply(input);
const discoveryReadyChatCandidate = isDiscoveryReadyChatCandidate(input, entryPoint); const discoveryReadyChatCandidate = isDiscoveryReadyChatCandidate(input, entryPoint);
const discoveryReadyDeepCandidate = isDiscoveryReadyDeepCandidate(input, entryPoint); const discoveryReadyDeepCandidate = isDiscoveryReadyDeepCandidate(input, entryPoint);
const discoveryReadyAddressCandidate = isDiscoveryReadyAddressCandidate(input, entryPoint); const discoveryReadyAddressCandidate = isDiscoveryReadyAddressCandidate(input, entryPoint);
@ -330,6 +338,12 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
if (fullConfirmedFactualAddressReply) { if (fullConfirmedFactualAddressReply) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply"); 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)) { if (!ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status)) {
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_status_not_allowed"); pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_status_not_allowed");
} }
@ -349,6 +363,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
!alignedFactualAddressReply && !alignedFactualAddressReply &&
!matchedFactualAddressContinuationTarget && !matchedFactualAddressContinuationTarget &&
!fullConfirmedFactualAddressReply && !fullConfirmedFactualAddressReply &&
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) && ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
candidate.eligible_for_future_hot_runtime && candidate.eligible_for_future_hot_runtime &&
Boolean(toNonEmptyString(candidate.reply_text)) && Boolean(toNonEmptyString(candidate.reply_text)) &&

View File

@ -7,12 +7,14 @@ export type AssistantMcpDiscoveryTurnInputStatus = "ready" | "needs_more_context
export type AssistantMcpDiscoveryTurnInputSource = export type AssistantMcpDiscoveryTurnInputSource =
| "assistant_turn_meaning" | "assistant_turn_meaning"
| "predecompose_contract" | "predecompose_contract"
| "followup_context"
| "raw_text" | "raw_text"
| "none"; | "none";
export interface BuildAssistantMcpDiscoveryTurnInputAdapterInput { export interface BuildAssistantMcpDiscoveryTurnInputAdapterInput {
assistantTurnMeaning?: Record<string, unknown> | null; assistantTurnMeaning?: Record<string, unknown> | null;
predecomposeContract?: Record<string, unknown> | null; predecomposeContract?: Record<string, unknown> | null;
followupContext?: Record<string, unknown> | null;
userMessage?: string | null; userMessage?: string | null;
effectiveMessage?: string | null; effectiveMessage?: string | null;
} }
@ -132,6 +134,148 @@ function collectDateScope(predecompose: Record<string, unknown> | null): string
return periodFrom ?? periodTo ?? null; 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 { function hasLifecycleSignal(text: string): boolean {
return /(?:сколько\s+лет|как\s+давно|давно\s+ли|возраст|перв(?:ая|ый)\s+актив|когда\s+начал|когда\s+появ|lifecycle|activity\s+duration|business\s+age|how\s+long)/iu.test( return /(?:сколько\s+лет|как\s+давно|давно\s+ли|возраст|перв(?:ая|ый)\s+актив|когда\s+начал|когда\s+появ|lifecycle|activity\s+duration|business\s+age|how\s+long)/iu.test(
text 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: { function semanticNeedFor(input: {
domain: string | null; domain: string | null;
action: string | null; action: string | null;
@ -191,6 +355,7 @@ function shouldRunDiscovery(input: {
valueFlowSignal: boolean; valueFlowSignal: boolean;
semanticDataNeed: string | null; semanticDataNeed: string | null;
explicitIntentCandidate: string | null; explicitIntentCandidate: string | null;
followupDiscoverySeedApplicable: boolean;
}): boolean { }): boolean {
if (input.lifecycleSignal || input.unsupported) { if (input.lifecycleSignal || input.unsupported) {
return true; return true;
@ -198,6 +363,9 @@ function shouldRunDiscovery(input: {
if (input.valueFlowSignal && !input.explicitIntentCandidate) { if (input.valueFlowSignal && !input.explicitIntentCandidate) {
return true; return true;
} }
if (input.followupDiscoverySeedApplicable && !input.explicitIntentCandidate && input.semanticDataNeed) {
return true;
}
if (!input.explicitIntentCandidate && input.semanticDataNeed) { if (!input.explicitIntentCandidate && input.semanticDataNeed) {
return true; return true;
} }
@ -209,37 +377,75 @@ export function buildAssistantMcpDiscoveryTurnInput(
): AssistantMcpDiscoveryTurnInputContract { ): AssistantMcpDiscoveryTurnInputContract {
const assistantTurnMeaning = toRecordObject(input.assistantTurnMeaning); const assistantTurnMeaning = toRecordObject(input.assistantTurnMeaning);
const predecomposeContract = toRecordObject(input.predecomposeContract); const predecomposeContract = toRecordObject(input.predecomposeContract);
const followupContext = toRecordObject(input.followupContext);
const predecomposeEntities = collectPredecomposeEntities(predecomposeContract); const predecomposeEntities = collectPredecomposeEntities(predecomposeContract);
const followupSeed = collectFollowupDiscoverySeed(followupContext);
const reasonCodes: string[] = []; const reasonCodes: string[] = [];
const rawText = compactLower(`${input.userMessage ?? ""} ${input.effectiveMessage ?? ""}`); const rawText = compactLower(`${input.userMessage ?? ""} ${input.effectiveMessage ?? ""}`);
const lifecycleSignal = hasLifecycleSignal(rawText); const rawLifecycleSignal = hasLifecycleSignal(rawText);
const bidirectionalValueFlowSignal = !lifecycleSignal && hasBidirectionalValueFlowSignal(rawText); const rawBidirectionalValueFlowSignal = !rawLifecycleSignal && hasBidirectionalValueFlowSignal(rawText);
const valueFlowSignal = !lifecycleSignal && (hasValueFlowSignal(rawText) || bidirectionalValueFlowSignal); const rawValueFlowSignal =
const payoutSignal = valueFlowSignal && !bidirectionalValueFlowSignal && hasPayoutSignal(rawText); !rawLifecycleSignal && (hasValueFlowSignal(rawText) || rawBidirectionalValueFlowSignal);
const monthlyAggregationSignal = valueFlowSignal && hasMonthlyAggregationSignal(rawText); 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 rawDomain = toNonEmptyString(assistantTurnMeaning?.asked_domain_family);
const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family); const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family);
const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis); const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis);
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family); const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate); 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({ const semanticDataNeed = semanticNeedFor({
domain: rawDomain, domain: rawDomain ?? seededDomain,
action: rawAction, action: rawAction ?? seededAction,
unsupported, unsupported: unsupported ?? seededUnsupported,
lifecycleSignal, lifecycleSignal,
valueFlowSignal valueFlowSignal
}); });
const entityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates); const entityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates);
pushUnique(entityCandidates, predecomposeEntities.counterparty); pushUnique(entityCandidates, predecomposeEntities.counterparty);
if (valueFlowSignal && !predecomposeEntities.counterparty) { pushUnique(entityCandidates, followupSeed.counterparty);
if (valueFlowSignal && !predecomposeEntities.counterparty && !followupSeed.counterparty) {
pushUnique(entityCandidates, predecomposeEntities.organization); pushUnique(entityCandidates, predecomposeEntities.organization);
pushUnique(entityCandidates, followupSeed.organization);
} }
const explicitOrganizationScope = 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 = { 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 asked_action_family: lifecycleSignal
? "activity_duration" ? "activity_duration"
: valueFlowSignal : valueFlowSignal
@ -247,12 +453,12 @@ export function buildAssistantMcpDiscoveryTurnInput(
? "net_value_flow" ? "net_value_flow"
: payoutSignal : payoutSignal
? "payout" ? "payout"
: "turnover" : rawAction ?? seededAction ?? "turnover"
: rawAction, : rawAction ?? seededAction,
asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis, asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis,
explicit_entity_candidates: entityCandidates, explicit_entity_candidates: entityCandidates,
explicit_organization_scope: explicitOrganizationScope, explicit_organization_scope: explicitOrganizationScope,
explicit_date_scope: collectDateScope(predecomposeContract), explicit_date_scope: explicitDateScope,
unsupported_but_understood_family: unsupported_but_understood_family:
unsupported ?? unsupported ??
(lifecycleSignal (lifecycleSignal
@ -262,9 +468,17 @@ export function buildAssistantMcpDiscoveryTurnInput(
? "counterparty_bidirectional_value_flow_or_netting" ? "counterparty_bidirectional_value_flow_or_netting"
: payoutSignal : payoutSignal
? "counterparty_payouts_or_outflow" ? "counterparty_payouts_or_outflow"
: "counterparty_value_or_turnover" : seededUnsupported ?? "counterparty_value_or_turnover"
: followupDiscoverySeedApplicable
? seededUnsupported
: null), : null),
stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden || unsupported || lifecycleSignal || valueFlowSignal) stale_replay_forbidden: Boolean(
assistantTurnMeaning?.stale_replay_forbidden ||
unsupported ||
lifecycleSignal ||
valueFlowSignal ||
followupDiscoverySeedApplicable
)
}; };
const cleanTurnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {}; const cleanTurnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {};
@ -294,15 +508,18 @@ export function buildAssistantMcpDiscoveryTurnInput(
} }
const runDiscovery = shouldRunDiscovery({ const runDiscovery = shouldRunDiscovery({
unsupported, unsupported: unsupported ?? seededUnsupported,
lifecycleSignal, lifecycleSignal,
valueFlowSignal, valueFlowSignal,
semanticDataNeed, semanticDataNeed,
explicitIntentCandidate explicitIntentCandidate,
followupDiscoverySeedApplicable
}); });
const hasTurnMeaning = Object.keys(cleanTurnMeaning).length > 0; const hasTurnMeaning = Object.keys(cleanTurnMeaning).length > 0;
const sourceSignal: AssistantMcpDiscoveryTurnInputSource = assistantTurnMeaning const sourceSignal: AssistantMcpDiscoveryTurnInputSource = assistantTurnMeaning
? "assistant_turn_meaning" ? "assistant_turn_meaning"
: followupDiscoverySeedApplicable
? "followup_context"
: predecomposeContract : predecomposeContract
? "predecompose_contract" ? "predecompose_contract"
: lifecycleSignal : lifecycleSignal
@ -326,12 +543,21 @@ export function buildAssistantMcpDiscoveryTurnInput(
if (monthlyAggregationSignal) { if (monthlyAggregationSignal) {
pushReason(reasonCodes, "mcp_discovery_monthly_aggregation_signal_detected"); pushReason(reasonCodes, "mcp_discovery_monthly_aggregation_signal_detected");
} }
if (followupDiscoverySeedApplicable) {
pushReason(reasonCodes, "mcp_discovery_seeded_from_followup_context");
}
if (unsupported) { if (unsupported) {
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn"); pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
} }
if (predecomposeEntities.counterparty) { if (predecomposeEntities.counterparty) {
pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose"); 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) { if (entityCandidates.length > 0) {
pushReason(reasonCodes, "mcp_discovery_entity_scope_available"); pushReason(reasonCodes, "mcp_discovery_entity_scope_available");
} }

View File

@ -62,6 +62,14 @@ function toRecordObject(value: unknown): Record<string, unknown> | null {
return value as Record<string, unknown>; 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 { function periodPartForRecap(scopedDate: string | null): string {
if (!scopedDate) { if (!scopedDate) {
return ""; return "";
@ -353,6 +361,38 @@ export function buildAddressMemoryRecapReply(input: {
return "Да, помню предыдущий адресный контур. Могу кратко напомнить, что мы уже подтвердили, или сразу продолжить следующий шаг."; 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: { export function buildSelectedObjectAnswerInspectionReply(input: {
addressDebug: Record<string, unknown> | null; addressDebug: Record<string, unknown> | null;
toNonEmptyString: (value: unknown) => string | null; toNonEmptyString: (value: unknown) => string | null;

View File

@ -7,9 +7,11 @@ import {
buildRootScopedCarryoverFilters, buildRootScopedCarryoverFilters,
buildInventoryRootFrameFromAddressDebug, buildInventoryRootFrameFromAddressDebug,
hydrateInventoryRootFrameState, hydrateInventoryRootFrameState,
readAddressDebugIntent,
readAddressDebugFilters, readAddressDebugFilters,
readAddressDebugItem, readAddressDebugItem,
readAddressDebugTemporalScope, readAddressDebugTemporalScope,
readAssistantMcpDiscoveryPilotScope,
resolveOrganizationClarificationContinuation, resolveOrganizationClarificationContinuation,
resolveNavigationSessionContextState, resolveNavigationSessionContextState,
resolveAddressDebugCarryoverFilters, resolveAddressDebugCarryoverFilters,
@ -171,19 +173,23 @@ export function createAssistantTransitionPolicy(deps) {
return false; return false;
} }
const executionLane = deps.toNonEmptyString(debug.execution_lane); 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 selectedRecipe = deps.toNonEmptyString(debug.selected_recipe);
const answerGroundingCheck = const answerGroundingCheck =
debug.answer_grounding_check && typeof debug.answer_grounding_check === "object" debug.answer_grounding_check && typeof debug.answer_grounding_check === "object"
? debug.answer_grounding_check ? debug.answer_grounding_check
: null; : null;
const groundingStatus = deps.toNonEmptyString(answerGroundingCheck?.status); const groundingStatus = deps.toNonEmptyString(answerGroundingCheck?.status);
const discoveryPilotScope = readAssistantMcpDiscoveryPilotScope(debug, deps.toNonEmptyString);
if (groundingStatus === "grounded") { if (groundingStatus === "grounded") {
return true; return true;
} }
if (selectedRecipe) { if (selectedRecipe) {
return true; return true;
} }
if (debug.mcp_discovery_response_applied === true && discoveryPilotScope) {
return true;
}
return executionLane === "address_query" && Boolean(detectedIntent && detectedIntent !== "unknown"); return executionLane === "address_query" && Boolean(detectedIntent && detectedIntent !== "unknown");
} }
@ -438,7 +444,7 @@ export function createAssistantTransitionPolicy(deps) {
(deps.toNonEmptyString(alternateMessage) (deps.toNonEmptyString(alternateMessage)
? deps.isImplicitAddressContinuationByLlm(alternateMessage, llmPreDecomposeMeta) ? deps.isImplicitAddressContinuationByLlm(alternateMessage, llmPreDecomposeMeta)
: false)); : false));
const sourceIntentHint = deps.toNonEmptyString(carryoverSourceDebug?.detected_intent); const sourceIntentHint = readAddressDebugIntent(carryoverSourceDebug, deps.toNonEmptyString);
const navigationSessionState = resolveNavigationSessionContextState( const navigationSessionState = resolveNavigationSessionContextState(
addressNavigationState, addressNavigationState,
deps.toNonEmptyString, deps.toNonEmptyString,
@ -599,7 +605,8 @@ export function createAssistantTransitionPolicy(deps) {
if (!carryoverSourceDebug) { if (!carryoverSourceDebug) {
return null; 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 llmExplicitIntent = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
const llmSelectedObjectScopeDetected = const llmSelectedObjectScopeDetected =
llmPreDecomposeMeta?.predecomposeContract?.semantics?.selected_object_scope_detected === true; llmPreDecomposeMeta?.predecomposeContract?.semantics?.selected_object_scope_detected === true;
@ -931,6 +938,7 @@ export function createAssistantTransitionPolicy(deps) {
previous_filters: previousFilters, previous_filters: previousFilters,
previous_anchor_type: previousAnchorType ?? undefined, previous_anchor_type: previousAnchorType ?? undefined,
previous_anchor_value: previousAnchor, previous_anchor_value: previousAnchor,
previous_discovery_pilot_scope: sourceDiscoveryPilotScope ?? undefined,
resolved_counterparty_from_display: resolvedCounterpartyFromDisplay || undefined, resolved_counterparty_from_display: resolvedCounterpartyFromDisplay || undefined,
root_context_only: rootScopedPivot || undefined, root_context_only: rootScopedPivot || undefined,
root_intent: shouldAttachInventoryRootFrame ? inventoryRootFrame?.intent ?? undefined : undefined, root_intent: shouldAttachInventoryRootFrame ? inventoryRootFrame?.intent ?? undefined : undefined,

View File

@ -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) { function buildEntityCandidates(counterpartyTurnover) {
if (!counterpartyTurnover?.entity) { if (!counterpartyTurnover?.entity) {
return []; return [];
@ -139,10 +156,17 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
const joinedText = fallbackCompactWhitespace(`${rawText} ${effectiveText}`); const joinedText = fallbackCompactWhitespace(`${rawText} ${effectiveText}`);
const supportedIntent = detectSupportedIntent(joinedText, deps); const supportedIntent = detectSupportedIntent(joinedText, deps);
const counterpartyTurnover = detectCounterpartyTurnoverFamily(joinedText); const counterpartyTurnover = detectCounterpartyTurnoverFamily(joinedText);
const broadBusinessEvaluation = detectBroadBusinessEvaluation(joinedText);
const llmIntent = toNonEmptyString(input?.llmPreDecomposeMeta?.predecomposeContract?.intent, deps); const llmIntent = toNonEmptyString(input?.llmPreDecomposeMeta?.predecomposeContract?.intent, deps);
const explicitIntentCandidate = const explicitIntentCandidate =
supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null); broadBusinessEvaluation?.family
const unsupportedFamily = !explicitIntentCandidate && counterpartyTurnover?.family ? counterpartyTurnover.family : null; ? null
: supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null);
const unsupportedFamily = broadBusinessEvaluation?.family
? broadBusinessEvaluation.family
: !explicitIntentCandidate && counterpartyTurnover?.family
? counterpartyTurnover.family
: null;
const reasonCodes = []; const reasonCodes = [];
if (supportedIntent?.reason) { if (supportedIntent?.reason) {
reasonCodes.push(supportedIntent.reason); reasonCodes.push(supportedIntent.reason);
@ -150,6 +174,9 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
if (counterpartyTurnover?.family) { if (counterpartyTurnover?.family) {
reasonCodes.push("counterparty_turnover_current_turn_signal"); 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 ?? "") })) { if (rawText !== normalizeTurnText(rawMessage, { ...deps, repairAddressMojibake: (value) => String(value ?? "") })) {
reasonCodes.push("mojibake_repair_applied"); reasonCodes.push("mojibake_repair_applied");
} }
@ -168,6 +195,8 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
? "vat" ? "vat"
: explicitIntentCandidate?.startsWith("inventory_") : explicitIntentCandidate?.startsWith("inventory_")
? "inventory" ? "inventory"
: broadBusinessEvaluation?.family
? "business_summary"
: explicitIntentCandidate?.includes("counterparty") : explicitIntentCandidate?.includes("counterparty")
? "counterparty" ? "counterparty"
: counterpartyTurnover?.family : counterpartyTurnover?.family
@ -178,6 +207,8 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
explicitIntentCandidate === "payables_confirmed_as_of_date" || explicitIntentCandidate === "payables_confirmed_as_of_date" ||
explicitIntentCandidate === "inventory_on_hand_as_of_date" explicitIntentCandidate === "inventory_on_hand_as_of_date"
? "confirmed_snapshot" ? "confirmed_snapshot"
: broadBusinessEvaluation?.family
? "broad_evaluation"
: explicitIntentCandidate === "vat_liability_confirmed_for_tax_period" : explicitIntentCandidate === "vat_liability_confirmed_for_tax_period"
? "confirmed_tax_period" ? "confirmed_tax_period"
: explicitIntentCandidate === "vat_payable_confirmed_as_of_date" : explicitIntentCandidate === "vat_payable_confirmed_as_of_date"
@ -189,7 +220,9 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
: counterpartyTurnover?.family : counterpartyTurnover?.family
? "counterparty_value_or_turnover" ? "counterparty_value_or_turnover"
: null; : null;
const staleReplayForbidden = Boolean(unsupportedFamily || (counterpartyTurnover?.entity && !explicitIntentCandidate)); const staleReplayForbidden = Boolean(
unsupportedFamily || broadBusinessEvaluation?.family || (counterpartyTurnover?.entity && !explicitIntentCandidate)
);
return { return {
schema_version: "assistant_turn_meaning_v1", schema_version: "assistant_turn_meaning_v1",
raw_message: rawMessage, raw_message: rawMessage,
@ -200,7 +233,9 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
asked_action_family: askedActionFamily, asked_action_family: askedActionFamily,
explicit_intent_candidate: explicitIntentCandidate, explicit_intent_candidate: explicitIntentCandidate,
explicit_entity_candidates: buildEntityCandidates(counterpartyTurnover), 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 intent_override_strength: explicitIntentCandidate
? "explicit_current_turn_intent" ? "explicit_current_turn_intent"
: staleReplayForbidden : staleReplayForbidden

View File

@ -3141,5 +3141,113 @@ describe("assistant address follow-up carryover", () => {
expect(calls[0].options?.followupContext?.root_filters?.counterparty).toBeUndefined(); expect(calls[0].options?.followupContext?.root_filters?.counterparty).toBeUndefined();
expect(normalizerService.normalize).not.toHaveBeenCalled(); expect(normalizerService.normalize).not.toHaveBeenCalled();
}); });
it.skip("passes grounded MCP discovery payout context into a short year-switch follow-up", async () => {
const followupMessage = "а теперь за 2021?";
const calls: Array<{ message: string; options?: any }> = [];
const addressQueryService = {
tryHandle: vi.fn(async (message: string, options?: any) => {
calls.push({ message, options });
if (message === followupMessage && options?.followupContext) {
return buildAddressLaneResult({
reply_text: "Подтверждены исходящие платежи по Группа СВК за 2021 год.",
debug: {
...buildAddressLaneResult().debug,
detected_intent: "supplier_payouts_profile",
selected_recipe: "address_supplier_payouts_profile_v1",
extracted_filters: {
counterparty: "Группа СВК",
organization: "ООО Альтернатива Плюс",
period_from: "2021-01-01",
period_to: "2021-12-31"
},
reasons: ["address_action_detected", "address_entity_detected", "address_followup_context_applied"]
}
});
}
return null;
})
} as any;
const normalizerService = {
normalize: vi.fn(async () => ({
assistant_reply: "normalizer_fallback_should_not_be_used",
reply_type: "partial_coverage",
debug: {}
}))
} as any;
const sessions = new AssistantSessionStore();
const service = new AssistantService(
normalizerService,
sessions as any,
{} as any,
{ persistSession: vi.fn() } as any,
addressQueryService
);
const sessionId = `asst-discovery-followup-year-switch-${Date.now()}`;
sessions.appendItem(sessionId, {
message_id: "msg-discovery-payout-seed",
session_id: sessionId,
role: "assistant",
text: "Подтверждены исходящие платежи по Группа СВК за 2020 год.",
reply_type: "partial_coverage",
created_at: "2026-04-20T10:00:00.000Z",
trace_id: "living-discovery-seed",
debug: {
execution_lane: "living_chat",
mcp_discovery_response_applied: true,
assistant_active_organization: "ООО Альтернатива Плюс",
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
turn_input: {
turn_meaning_ref: {
asked_action_family: "payout",
explicit_entity_candidates: ["Группа СВК"],
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: true,
pilot: {
pilot_scope: "counterparty_supplier_payout_query_movements_v1"
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
}
}
}
}
} as any);
const response = await service.handleMessage({
session_id: sessionId,
user_message: followupMessage,
useMock: true
} as any);
expect(response.ok).toBe(true);
expect(response.reply_type).toBe("factual");
expect(calls).toHaveLength(1);
expect(calls[0].message).toBe(followupMessage);
expect(calls[0].options?.followupContext?.previous_intent).toBe("supplier_payouts_profile");
expect(calls[0].options?.followupContext?.previous_discovery_pilot_scope).toBe(
"counterparty_supplier_payout_query_movements_v1"
);
expect(calls[0].options?.followupContext?.previous_anchor_type).toBe("counterparty");
expect(calls[0].options?.followupContext?.previous_anchor_value).toBe("Группа СВК");
expect(calls[0].options?.followupContext?.previous_filters).toMatchObject({
counterparty: "Группа СВК",
organization: "ООО Альтернатива Плюс",
period_from: "2020-01-01",
period_to: "2020-12-31"
});
expect(normalizerService.normalize).not.toHaveBeenCalled();
});
}); });

View File

@ -154,6 +154,97 @@ describe("assistant address orchestration runtime adapter", () => {
); );
}); });
it("passes grounded discovery follow-up carryover into MCP discovery entry point for a short year switch", async () => {
const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: false,
discovery_attempted: true
}));
const input = buildInput({
userMessage: "а теперь за 2021?",
runAddressLlmPreDecompose: vi.fn(async () => ({
attempted: true,
applied: false,
effectiveMessage: "а теперь за 2021?",
reason: "raw_kept",
predecomposeContract: {
mode: "unsupported",
intent: "unknown",
period: {
scope: "year",
period_from: "2021-01-01",
period_to: "2021-12-31",
has_explicit_period: true
}
}
})),
resolveAddressFollowupCarryoverContext: vi.fn(() => ({
followupContext: {
previous_intent: "supplier_payouts_profile",
target_intent: "supplier_payouts_profile",
previous_discovery_pilot_scope: "counterparty_supplier_payout_query_movements_v1",
previous_anchor_type: "counterparty",
previous_anchor_value: "Группа СВК",
previous_filters: {
counterparty: "Группа СВК",
organization: "ООО Альтернатива Плюс",
period_from: "2020-01-01",
period_to: "2020-12-31"
}
}
})),
resolveAssistantOrchestrationDecision: vi.fn(() => ({
runAddressLane: true,
livingMode: "address_data",
livingReason: "address_lane_triggered",
toolGateDecision: "run_address_lane",
toolGateReason: "followup_context_detected",
orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1",
assistant_turn_meaning: {
schema_version: "assistant_turn_meaning_v1",
raw_message: "а теперь за 2021?",
effective_message: "а теперь за 2021?",
explicit_entity_candidates: []
}
}
})),
runMcpDiscoveryRuntimeEntryPoint
});
const output = await buildAssistantAddressOrchestrationRuntime(input);
expect(output.orchestrationDecision.runAddressLane).toBe(true);
expect(runMcpDiscoveryRuntimeEntryPoint).toHaveBeenCalledWith(
expect.objectContaining({
userMessage: "а теперь за 2021?",
effectiveMessage: "а теперь за 2021?",
followupContext: expect.objectContaining({
previous_intent: "supplier_payouts_profile",
target_intent: "supplier_payouts_profile",
previous_discovery_pilot_scope: "counterparty_supplier_payout_query_movements_v1",
previous_anchor_type: "counterparty",
previous_anchor_value: "Группа СВК",
previous_filters: expect.objectContaining({
counterparty: "Группа СВК",
organization: "ООО Альтернатива Плюс",
period_from: "2020-01-01",
period_to: "2020-12-31"
})
})
})
);
expect(output.addressRuntimeMeta.mcpDiscoveryRuntimeEntryPoint).toEqual(
expect.objectContaining({
entry_status: "bridge_executed",
discovery_attempted: true,
hot_runtime_wired: false
})
);
});
it("keeps address orchestration alive when MCP discovery entry point fails", async () => { it("keeps address orchestration alive when MCP discovery entry point fails", async () => {
const input = buildInput({ const input = buildInput({
runMcpDiscoveryRuntimeEntryPoint: vi.fn(async () => { runMcpDiscoveryRuntimeEntryPoint: vi.fn(async () => {

View File

@ -7,6 +7,7 @@ import {
applyTemporalCarryoverFilters, applyTemporalCarryoverFilters,
buildRootScopedCarryoverFilters, buildRootScopedCarryoverFilters,
hydrateInventoryRootFrameState, hydrateInventoryRootFrameState,
readAddressDebugIntent,
readAddressDebugTemporalScope, readAddressDebugTemporalScope,
resolveNavigationSessionContextState, resolveNavigationSessionContextState,
resolveAddressDebugCarryoverFilters, resolveAddressDebugCarryoverFilters,
@ -147,6 +148,49 @@ describe("assistantContinuityPolicy organization authority", () => {
}); });
}); });
it("hydrates intent and carryover filters from grounded MCP discovery payout scope", () => {
const debug = {
execution_lane: "living_chat",
mcp_discovery_response_applied: true,
assistant_active_organization: "ООО Альтернатива Плюс",
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
turn_input: {
turn_meaning_ref: {
asked_action_family: "payout",
explicit_entity_candidates: ["Группа СВК"],
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: true,
pilot: {
pilot_scope: "counterparty_supplier_payout_query_movements_v1"
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
}
}
}
};
expect(readAddressDebugIntent(debug)).toBe("supplier_payouts_profile");
expect(readAddressDebugTemporalScope(debug)).toEqual({
asOfDate: null,
periodFrom: "2020-01-01",
periodTo: "2020-12-31"
});
expect(resolveAddressDebugCarryoverFilters(debug)).toEqual({
counterparty: "Группа СВК",
organization: "ООО Альтернатива Плюс",
period_from: "2020-01-01",
period_to: "2020-12-31"
});
});
it("resolves navigation session context through one shared helper", () => { it("resolves navigation session context through one shared helper", () => {
const state = resolveNavigationSessionContextState({ const state = resolveNavigationSessionContextState({
session_context: { session_context: {

View File

@ -133,6 +133,91 @@ describe("assistant living chat runtime adapter", () => {
expect(executeLlmChat).toHaveBeenCalledTimes(1); expect(executeLlmChat).toHaveBeenCalledTimes(1);
}); });
it("builds deterministic broad business evaluation summary from grounded continuity instead of replaying lifecycle noise", async () => {
const executeLlmChat = vi.fn(async () => "raw-llm");
const input = buildRuntimeInput({
userMessage: "Как ты оценишь деятельность компании?",
modeDecision: { mode: "chat", reason: "unsupported_current_turn_meaning_boundary" },
sessionScope: {
knownOrganizations: ["ООО Альтернатива Плюс"],
selectedOrganization: null,
activeOrganization: "ООО Альтернатива Плюс"
},
sessionItems: [
{
role: "assistant",
debug: {
execution_lane: "address_query",
answer_grounding_check: {
status: "grounded"
},
detected_intent: "counterparty_activity_lifecycle",
extracted_filters: {
organization: "ООО Альтернатива Плюс"
}
}
},
{
role: "assistant",
debug: {
execution_lane: "living_chat",
mcp_discovery_response_applied: true,
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
turn_input: {
turn_meaning_ref: {
explicit_entity_candidates: ["Группа СВК"],
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: true,
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
},
pilot: {
pilot_scope: "counterparty_bidirectional_value_flow_query_movements_v1",
derived_bidirectional_value_flow: {
net_amount_human_ru: "3 865 501,50 руб.",
incoming_customer_revenue: {
total_amount_human_ru: "47 628 853,03 руб."
},
outgoing_supplier_payout: {
total_amount_human_ru: "43 763 351,53 руб."
}
}
}
}
}
}
}
],
addressRuntimeMeta: {
toolGateReason: "unsupported_current_turn_meaning_boundary",
orchestrationContract: {
unsupported_current_turn_meaning_boundary: true,
assistant_turn_meaning: {
unsupported_but_understood_family: "broad_business_evaluation"
}
}
},
executeLlmChat
});
const output = await runAssistantLivingChatRuntime(input);
expect(output.handled).toBe(true);
expect(output.chatText.toLowerCase()).toContain("оценка бизнеса");
expect(output.chatText).toContain("ООО Альтернатива Плюс");
expect(output.chatText).toContain("Группа СВК");
expect(output.chatText).toContain("нетто");
expect(output.debug?.living_chat_response_source).toBe("deterministic_broad_business_evaluation_contract");
expect(executeLlmChat).not.toHaveBeenCalled();
});
it("builds deterministic boundary for unsupported current-turn business meaning", async () => { it("builds deterministic boundary for unsupported current-turn business meaning", async () => {
const executeLlmChat = vi.fn(async () => "raw-llm"); const executeLlmChat = vi.fn(async () => "raw-llm");
const input = buildRuntimeInput({ const input = buildRuntimeInput({

View File

@ -323,4 +323,50 @@ describe("assistant MCP discovery response policy", () => {
expect(result.reason_codes).toContain("mcp_discovery_response_policy_candidate_not_eligible"); expect(result.reason_codes).toContain("mcp_discovery_response_policy_candidate_not_eligible");
expect(result.reason_codes).toContain("mcp_discovery_response_policy_kept_current_reply"); expect(result.reason_codes).toContain("mcp_discovery_response_policy_kept_current_reply");
}); });
it("keeps deterministic broad business evaluation summary instead of replacing it with a clarification candidate", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "Коротко: по уже подтвержденным данным в 1С компания выглядит живой операционно.",
currentReplySource: "deterministic_broad_business_evaluation_contract",
livingChatSource: "deterministic_broad_business_evaluation_contract",
modeDecisionReason: "unsupported_current_turn_meaning_boundary",
addressRuntimeMeta: {
assistant_mcp_discovery_entry_point_v1: entryPoint({
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
turn_meaning_ref: {
asked_domain_family: "business_summary",
asked_action_family: "broad_evaluation",
unsupported_but_understood_family: "broad_business_evaluation",
stale_replay_forbidden: true
}
},
bridge: {
bridge_status: "needs_clarification",
user_facing_response_allowed: true,
business_fact_answer_allowed: false,
requires_user_clarification: true,
answer_draft: {
answer_mode: "needs_clarification",
headline: "Нужно уточнить контекст перед поиском в 1С.",
confirmed_lines: [],
inference_lines: [],
unknown_lines: ["MCP discovery pilot needs more scope before execution"],
limitation_lines: ["MCP discovery pilot needs more scope before execution"],
next_step_line: "Уточните контрагента, период или организацию."
}
}
})
}
});
expect(result.applied).toBe(false);
expect(result.decision).toBe("keep_current_reply");
expect(result.reply_source).toBe("deterministic_broad_business_evaluation_contract");
expect(result.reply_text).toContain("компания выглядит живой операционно");
expect(result.reason_codes).toContain(
"mcp_discovery_response_policy_keep_broad_business_summary_over_clarification_candidate"
);
});
}); });

View File

@ -151,6 +151,69 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).toContain("mcp_discovery_monthly_aggregation_signal_detected"); expect(result.reason_codes).toContain("mcp_discovery_monthly_aggregation_signal_detected");
}); });
it("seeds short monthly follow-up from prior bidirectional discovery context", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "а по месяцам?",
followupContext: {
previous_discovery_pilot_scope: "counterparty_bidirectional_value_flow_query_movements_v1",
previous_filters: {
counterparty: "Группа СВК",
organization: "ООО Альтернатива Плюс",
period_from: "2020-01-01",
period_to: "2020-12-31"
},
previous_anchor_type: "counterparty",
previous_anchor_value: "Группа СВК"
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.source_signal).toBe("followup_context");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
asked_aggregation_axis: "month",
explicit_entity_candidates: ["Группа СВК"],
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting",
stale_replay_forbidden: true
});
expect(result.reason_codes).toContain("mcp_discovery_seeded_from_followup_context");
expect(result.reason_codes).toContain("mcp_discovery_counterparty_from_followup_context");
expect(result.reason_codes).toContain("mcp_discovery_date_scope_from_followup_context");
});
it("switches the checked year on a short payout follow-up while keeping prior discovery counterparty", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "а теперь за 2021?",
followupContext: {
previous_discovery_pilot_scope: "counterparty_supplier_payout_query_movements_v1",
previous_filters: {
counterparty: "Группа СВК",
organization: "ООО Альтернатива Плюс",
period_from: "2020-01-01",
period_to: "2020-12-31"
},
previous_anchor_type: "counterparty",
previous_anchor_value: "Группа СВК"
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "payout",
explicit_entity_candidates: ["Группа СВК"],
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2021",
unsupported_but_understood_family: "counterparty_payouts_or_outflow",
stale_replay_forbidden: true
});
});
it("does not activate discovery for supported exact current-turn intent", () => { it("does not activate discovery for supported exact current-turn intent", () => {
const result = buildAssistantMcpDiscoveryTurnInput({ const result = buildAssistantMcpDiscoveryTurnInput({
assistantTurnMeaning: { assistantTurnMeaning: {

View File

@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { import {
buildAddressMemoryRecapReply, buildAddressMemoryRecapReply,
buildBroadBusinessEvaluationReply,
buildSelectedObjectAnswerInspectionReply, buildSelectedObjectAnswerInspectionReply,
createAssistantMemoryRecapPolicy, createAssistantMemoryRecapPolicy,
resolveAssistantLivingChatMemoryContext resolveAssistantLivingChatMemoryContext
@ -385,6 +386,75 @@ describe("assistantMemoryRecapPolicy", () => {
expect(reply).toContain("43 763 351,53 руб."); expect(reply).toContain("43 763 351,53 руб.");
}); });
it("builds deterministic broad business evaluation summary from recent grounded organization facts", () => {
const sessionItems = [
{
role: "assistant",
debug: {
execution_lane: "address_query",
answer_grounding_check: {
status: "grounded"
},
detected_intent: "counterparty_activity_lifecycle",
extracted_filters: {
organization: "ООО Альтернатива Плюс"
}
}
},
{
role: "assistant",
debug: {
execution_lane: "living_chat",
mcp_discovery_response_applied: true,
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
turn_input: {
turn_meaning_ref: {
explicit_entity_candidates: ["Группа СВК"],
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: true,
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
},
pilot: {
pilot_scope: "counterparty_bidirectional_value_flow_query_movements_v1",
derived_bidirectional_value_flow: {
net_amount_human_ru: "3 865 501,50 руб.",
incoming_customer_revenue: {
total_amount_human_ru: "47 628 853,03 руб."
},
outgoing_supplier_payout: {
total_amount_human_ru: "43 763 351,53 руб."
}
}
}
}
}
}
}
];
const reply = buildBroadBusinessEvaluationReply({
organization: "ООО Альтернатива Плюс",
addressDebug: sessionItems[1].debug as any,
sessionItems,
toNonEmptyString: (value: unknown) => {
const text = String(value ?? "").trim();
return text.length > 0 ? text : null;
}
});
expect(reply.toLowerCase()).toContain("оценка бизнеса");
expect(reply).toContain("ООО Альтернатива Плюс");
expect(reply).toContain("47 628 853,03");
});
it("builds grounded answer inspection reply for MCP discovery net answer", () => { it("builds grounded answer inspection reply for MCP discovery net answer", () => {
const context = resolveAssistantLivingChatMemoryContext({ const context = resolveAssistantLivingChatMemoryContext({
modeDecisionReason: "answer_inspection_followup_detected", modeDecisionReason: "answer_inspection_followup_detected",

View File

@ -606,7 +606,7 @@ describe("assistantRoutePolicy", () => {
expect(decision.orchestrationContract?.organization_scope_switch_detected).not.toBe(true); expect(decision.orchestrationContract?.organization_scope_switch_detected).not.toBe(true);
}); });
it("keeps company activity assessment follow-up in address lane when lifecycle intent is resolved from grounded continuity", () => { it("routes broad business evaluation follow-up to chat instead of replaying lifecycle address intent", () => {
const policy = buildPolicy({ const policy = buildPolicy({
resolveAddressIntent: () => ({ intent: "counterparty_activity_lifecycle", confidence: "high" }), resolveAddressIntent: () => ({ intent: "counterparty_activity_lifecycle", confidence: "high" }),
findLastGroundedAddressAnswerDebug: () => ({ findLastGroundedAddressAnswerDebug: () => ({
@ -618,6 +618,15 @@ describe("assistantRoutePolicy", () => {
period_to: "2026-04-18" period_to: "2026-04-18"
} }
}), }),
resolveAssistantTurnMeaning: () => ({
schema_version: "assistant_turn_meaning_v1",
asked_domain_family: "business_summary",
asked_action_family: "broad_evaluation",
explicit_intent_candidate: null,
unsupported_but_understood_family: "broad_business_evaluation",
stale_replay_forbidden: true,
reason_codes: ["broad_business_evaluation_current_turn_signal"]
}),
resolveAddressToolGateDecision: () => ({ resolveAddressToolGateDecision: () => ({
runAddressLane: false, runAddressLane: false,
decision: "skip_address_lane", decision: "skip_address_lane",
@ -666,9 +675,12 @@ describe("assistantRoutePolicy", () => {
useMock: false useMock: false
}); });
expect(decision.runAddressLane).toBe(true); expect(decision.runAddressLane).toBe(false);
expect(decision.toolGateDecision).toBe("run_address_lane"); expect(decision.toolGateDecision).toBe("skip_address_lane");
expect(decision.livingMode).toBe("address_data"); expect(decision.toolGateReason).toBe("unsupported_current_turn_meaning_boundary");
expect(decision.livingMode).toBe("chat");
expect(decision.livingReason).toBe("unsupported_current_turn_meaning_boundary");
expect(decision.orchestrationContract?.unsupported_current_turn_family).toBe("broad_business_evaluation");
}); });
it("recovers an address route from current-turn meaning when L0 resolver is noisy", () => { it("recovers an address route from current-turn meaning when L0 resolver is noisy", () => {

View File

@ -1014,6 +1014,45 @@ describe("assistantTransitionPolicy", () => {
expect(carryover).toBeNull(); expect(carryover).toBeNull();
}); });
it("drops carryover for broad business evaluation so lifecycle context does not stick to the new question", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
text: "Lifecycle answer",
debug: {
execution_lane: "address_query",
answer_grounding_check: { status: "grounded" },
detected_intent: "counterparty_activity_lifecycle",
extracted_filters: {
organization: 'ООО "Альтернатива Плюс"',
period_to: "2020-12-31"
},
anchor_type: "organization",
anchor_value_resolved: 'ООО "Альтернатива Плюс"'
}
}),
hasAddressFollowupContextSignal: () => true,
resolveAssistantTurnMeaning: () => ({
schema_version: "assistant_turn_meaning_v1",
asked_domain_family: "business_summary",
asked_action_family: "broad_evaluation",
explicit_intent_candidate: null,
unsupported_but_understood_family: "broad_business_evaluation",
explicit_entity_candidates: [],
stale_replay_forbidden: true
})
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
"Как ты оценишь деятельность компании?",
[],
null,
null,
null
);
expect(carryover).toBeNull();
});
it("reuses grounded MCP discovery payout context for a short year-switch follow-up", () => { it("reuses grounded MCP discovery payout context for a short year-switch follow-up", () => {
const policy = buildPolicy({ const policy = buildPolicy({
findLastAddressAssistantItem: () => null, findLastAddressAssistantItem: () => null,

View File

@ -93,4 +93,21 @@ describe("assistantTurnMeaningPolicy", () => {
expect(meaning.asked_action_family).toBe("confirmed_tax_period"); expect(meaning.asked_action_family).toBe("confirmed_tax_period");
expect(meaning.stale_replay_forbidden).toBe(false); expect(meaning.stale_replay_forbidden).toBe(false);
}); });
it("marks broad business evaluation as unsupported-but-understood instead of stale lifecycle replay", () => {
const policy = buildPolicy({
resolveAddressIntent: () => ({ intent: "counterparty_activity_lifecycle", confidence: "high" })
});
const meaning = policy.resolveAssistantTurnMeaning({
rawUserMessage: "Как ты оценишь деятельность компании?"
});
expect(meaning.explicit_intent_candidate).toBeNull();
expect(meaning.asked_domain_family).toBe("business_summary");
expect(meaning.asked_action_family).toBe("broad_evaluation");
expect(meaning.unsupported_but_understood_family).toBe("broad_business_evaluation");
expect(meaning.stale_replay_forbidden).toBe(true);
expect(meaning.reason_codes).toContain("broad_business_evaluation_current_turn_signal");
});
}); });