ARCH: ввести broad business evaluation bridge
This commit is contained in:
parent
d323dcd509
commit
bda7ca9cc1
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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)) &&
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 ?? "эта позиция";
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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)) &&
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 () => {
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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", () => {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue