ARCH: добавить open-scope totals по организации в MCP discovery

This commit is contained in:
2026-04-22 21:00:15 +03:00
parent f2bd2dfdb1
commit 94e537210c
12 changed files with 380 additions and 7 deletions
@@ -85,11 +85,17 @@ function comparisonNeedFor(action) {
}
return null;
}
function supportsOrganizationScopedOpenTotal(action) {
return action === "turnover" || action === "payout";
}
function allowsOpenScopeWithoutSubject(input) {
if (input.family !== "value_flow") {
return false;
}
return Boolean(input.rankingNeed || input.comparisonNeed === "incoming_vs_outgoing");
if (input.rankingNeed || input.comparisonNeed === "incoming_vs_outgoing") {
return true;
}
return Boolean(input.organizationScope && supportsOrganizationScopedOpenTotal(input.action));
}
function rankingNeedFromRawUtterance(value) {
const text = lower(value);
@@ -147,6 +153,12 @@ function decompositionCandidatesFor(input) {
pushUnique(result, "probe_coverage");
return result;
}
if (input.openScopeWithoutSubject) {
pushUnique(result, "collect_scoped_movements");
pushUnique(result, input.aggregationNeed === "by_month" ? "aggregate_by_month" : "aggregate_checked_amounts");
pushUnique(result, "probe_coverage");
return result;
}
pushUnique(result, "resolve_entity_reference");
if (input.action === "net_value_flow") {
pushUnique(result, "collect_incoming_movements");
@@ -204,6 +216,7 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
const rawUtterance = lower(input.rawUtterance);
const aggregationAxis = lower(turnMeaning?.asked_aggregation_axis);
const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope);
const explicitOrganizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope);
const subjectCandidates = (turnMeaning?.explicit_entity_candidates ?? [])
.map((item) => toNonEmptyString(item))
.filter((item) => Boolean(item));
@@ -219,6 +232,8 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
const openScopeWithoutSubject = subjectCandidates.length === 0 &&
allowsOpenScopeWithoutSubject({
family: businessFactFamily,
action,
organizationScope: explicitOrganizationScope,
comparisonNeed,
rankingNeed
});
@@ -261,6 +276,9 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
if (comparisonNeed) {
pushReason(reasonCodes, `data_need_graph_comparison_${comparisonNeed}`);
}
if (openScopeWithoutSubject && !rankingNeed && !comparisonNeed) {
pushReason(reasonCodes, "data_need_graph_open_scope_total_without_subject");
}
if (clarificationGaps.length > 0) {
pushReason(reasonCodes, "data_need_graph_has_clarification_gaps");
}
@@ -86,6 +86,7 @@ function recipeFor(input) {
const graphAction = lower(dataNeedGraph?.action_family);
const graphAggregation = lower(dataNeedGraph?.aggregation_need);
const graphClarificationGaps = (dataNeedGraph?.clarification_gaps ?? []).map((item) => lower(item));
const organizationScope = toNonEmptyString(meaning?.explicit_organization_scope);
const combined = `${domain} ${action} ${unsupported}`.trim();
const axes = [];
const requestedAggregationAxis = aggregationAxis(meaning);
@@ -132,6 +133,24 @@ function recipeFor(input) {
: "planner_selected_top_ranked_value_flow_from_data_need_graph"
};
}
if (!hasSubjectCandidates(dataNeedGraph) && organizationScope) {
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");
if (requestedAggregationAxis === "month" || graphAggregation === "by_month") {
pushUnique(axes, "calendar_month");
}
return {
semanticDataNeed: "organization-scoped value-flow evidence",
chainId: "value_flow",
chainSummary: "Query scoped movements for the checked period and organization without a preselected counterparty, aggregate checked amounts, then probe coverage before answering a bounded total.",
primitives: ["query_movements", "aggregate_by_axis", "probe_coverage"],
axes,
reason: requestedAggregationAxis === "month" || graphAggregation === "by_month"
? "planner_selected_monthly_open_scope_value_flow_total_from_data_need_graph"
: "planner_selected_open_scope_value_flow_total_from_data_need_graph"
};
}
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");
@@ -305,6 +305,14 @@ function hasBidirectionalValueFlowSignal(text) {
function hasValueRankingSignal(text) {
return /(?:кто\s+больше\s+всего.*ден[её]г|больше\s+всего.*ден[её]г|прин[её]с.*ден[её]г|сам(?:ый|ая|ое|ые).*(?:доходн|прибыльн)|most.*money|highest\s+(?:revenue|payment))/iu.test(text);
}
function hasOrganizationScopeSignal(text) {
return /(?:\bооо\b|\bип\b|\bао\b|\bпао\b|\bзао\b|\bllc\b|\binc\b|\bcorp\b|\bcompany\b|\borganization\b|\borganisation\b|организац|компан)/iu.test(text);
}
function hasOrganizationScopeSignalUtf8(text) {
return (/(?<!\p{L})(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e)(?!\p{L})/iu.test(text) ||
/\b(?:llc|inc|corp|company|organization|organisation)\b/iu.test(text) ||
/(?:\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u043a\u043e\u043c\u043f\u0430\u043d)/iu.test(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);
}
@@ -582,8 +590,12 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate);
const assistantTurnMeaningDateScope = toNonEmptyString(assistantTurnMeaning?.explicit_date_scope);
const assistantTurnMeaningOrganizationScope = toNonEmptyString(assistantTurnMeaning?.explicit_organization_scope);
const rawOpenScopeValueFlowOrganizationSignal = Boolean(rawValueFlowSignal &&
!rawBidirectionalValueFlowSignal &&
hasOrganizationScopeSignalUtf8(rawText) &&
(predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope));
const predecomposeOrganizationMirrorsCounterparty = sameScopedName(predecomposeEntities.counterparty, predecomposeEntities.organization);
const organizationMirrorsPredecomposeCounterparty = Boolean((rawBidirectionalValueFlowSignal || hasValueRankingSignal(rawText)) &&
const organizationMirrorsPredecomposeCounterparty = Boolean((rawBidirectionalValueFlowSignal || hasValueRankingSignal(rawText) || rawOpenScopeValueFlowOrganizationSignal) &&
(sameScopedName(predecomposeEntities.counterparty, assistantTurnMeaningOrganizationScope) ||
predecomposeOrganizationMirrorsCounterparty));
const normalizedPredecomposeCounterparty = organizationMirrorsPredecomposeCounterparty
@@ -829,7 +841,10 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
}
const openScopeValueFlowWithoutCounterparty = valueFlowSignal && !normalizedPredecomposeCounterparty && !followupSeed.counterparty;
const valueFlowOrganizationStaysScope = openScopeValueFlowWithoutCounterparty &&
(bidirectionalValueFlowSignal || hasValueRankingSignal(rawText));
Boolean(bidirectionalValueFlowSignal ||
hasValueRankingSignal(rawText) ||
rawOpenScopeValueFlowOrganizationSignal ||
followupSeed.organization);
if (openScopeValueFlowWithoutCounterparty && !valueFlowOrganizationStaysScope) {
pushUnique(entityCandidates, predecomposeEntities.organization);
pushUnique(entityCandidates, followupSeed.organization);