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
@@ -132,15 +132,24 @@ function comparisonNeedFor(action: string): string | null {
return null;
}
function supportsOrganizationScopedOpenTotal(action: string): boolean {
return action === "turnover" || action === "payout";
}
function allowsOpenScopeWithoutSubject(input: {
family: string | null;
action: string;
organizationScope: string | null;
comparisonNeed: string | null;
rankingNeed: string | null;
}): boolean {
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: string): string | null {
@@ -215,6 +224,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");
@@ -275,6 +290,7 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
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): item is string => Boolean(item));
@@ -291,6 +307,8 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
subjectCandidates.length === 0 &&
allowsOpenScopeWithoutSubject({
family: businessFactFamily,
action,
organizationScope: explicitOrganizationScope,
comparisonNeed,
rankingNeed
});
@@ -332,6 +350,9 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
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");
}
@@ -153,6 +153,7 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
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: string[] = [];
const requestedAggregationAxis = aggregationAxis(meaning);
@@ -201,7 +202,27 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
reason:
dataNeedGraph.ranking_need === "bottom_asc"
? "planner_selected_bottom_ranked_value_flow_from_data_need_graph"
: "planner_selected_top_ranked_value_flow_from_data_need_graph"
: "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");
@@ -422,6 +422,20 @@ function hasValueRankingSignal(text: string): boolean {
);
}
function hasOrganizationScopeSignal(text: string): boolean {
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: string): boolean {
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: string): boolean {
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
@@ -789,12 +803,18 @@ export function buildAssistantMcpDiscoveryTurnInput(
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)) &&
(rawBidirectionalValueFlowSignal || hasValueRankingSignal(rawText) || rawOpenScopeValueFlowOrganizationSignal) &&
(sameScopedName(predecomposeEntities.counterparty, assistantTurnMeaningOrganizationScope) ||
predecomposeOrganizationMirrorsCounterparty)
);
@@ -1090,7 +1110,12 @@ export function buildAssistantMcpDiscoveryTurnInput(
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);