ARCH: добавить open-scope totals по организации в MCP discovery
This commit is contained in:
parent
f2bd2dfdb1
commit
94e537210c
|
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase34_open_scope_value_flow_totals",
|
||||
"domain": "address_phase34_open_scope_value_flow_totals",
|
||||
"title": "Phase 34 open-scope value-flow totals replay",
|
||||
"description": "Targeted AGENT replay for Big Block D where organization-scoped incoming/outgoing money totals must be understood as bounded open-scope value-flow questions rather than as missing-counterparty fact asks.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_incoming_total_for_org",
|
||||
"title": "Raw organization-scoped incoming wording produces a bounded incoming total without inventing a counterparty",
|
||||
"question": "Сколько входящих денег за 2020 год по ООО Альтернатива Плюс?",
|
||||
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2020",
|
||||
"(?i)входящ|получ|поступ",
|
||||
"(?i)руб"
|
||||
],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)альтернатива",
|
||||
"(?i)проверенн|найденн"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните контрагента",
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)по какому контрагенту",
|
||||
"(?i)не найдено контрагента"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["value_flow_total", "incoming", "open_scope", "organization_scoped", "bounded_autonomy"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_outgoing_total_for_org",
|
||||
"title": "Raw organization-scoped outgoing wording produces a bounded payout total without inventing a counterparty",
|
||||
"question": "Сколько исходящих денег за 2020 год по ООО Альтернатива Плюс?",
|
||||
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2020",
|
||||
"(?i)исходящ|заплат|списан|платеж",
|
||||
"(?i)руб"
|
||||
],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)альтернатива",
|
||||
"(?i)проверенн|найденн"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните контрагента",
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)по какому контрагенту",
|
||||
"(?i)не найдено контрагента"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["value_flow_total", "outgoing", "open_scope", "organization_scoped", "bounded_autonomy"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -114,4 +114,27 @@ describe("assistant MCP discovery data need graph", () => {
|
|||
]);
|
||||
expect(result.reason_codes).toContain("data_need_graph_comparison_incoming_vs_outgoing");
|
||||
});
|
||||
it("treats organization-scoped incoming totals as an open-scope value need rather than a missing-subject fact ask", () => {
|
||||
const result = buildAssistantMcpDiscoveryDataNeedGraph({
|
||||
semanticDataNeed: "counterparty value-flow evidence",
|
||||
rawUtterance: "сколько входящих денег за 2020 год по ООО Альтернатива Плюс?",
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "turnover",
|
||||
explicit_date_scope: "2020",
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс"
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.business_fact_family).toBe("value_flow");
|
||||
expect(result.comparison_need).toBeNull();
|
||||
expect(result.ranking_need).toBeNull();
|
||||
expect(result.clarification_gaps).toEqual([]);
|
||||
expect(result.decomposition_candidates).toEqual([
|
||||
"collect_scoped_movements",
|
||||
"aggregate_checked_amounts",
|
||||
"probe_coverage"
|
||||
]);
|
||||
expect(result.reason_codes).toContain("data_need_graph_open_scope_total_without_subject");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -440,4 +440,38 @@ describe("assistant MCP discovery planner", () => {
|
|||
expect(result.selected_chain_summary).toContain("resolve the most relevant 1C reference");
|
||||
expect(result.proposed_primitives).toEqual(["search_business_entity", "resolve_entity_reference", "probe_coverage"]);
|
||||
});
|
||||
it("keeps organization-scoped one-sided value-flow totals executable without forcing a counterparty", () => {
|
||||
const result = planAssistantMcpDiscovery({
|
||||
dataNeedGraph: {
|
||||
schema_version: "assistant_data_need_graph_v1",
|
||||
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||
subject_candidates: [],
|
||||
business_fact_family: "value_flow",
|
||||
action_family: "turnover",
|
||||
aggregation_need: null,
|
||||
time_scope_need: "explicit_period",
|
||||
comparison_need: null,
|
||||
ranking_need: null,
|
||||
proof_expectation: "coverage_checked_fact",
|
||||
clarification_gaps: [],
|
||||
decomposition_candidates: ["collect_scoped_movements", "aggregate_checked_amounts", "probe_coverage"],
|
||||
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
|
||||
reason_codes: ["data_need_graph_built", "data_need_graph_open_scope_total_without_subject"]
|
||||
},
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "turnover",
|
||||
explicit_date_scope: "2020",
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс"
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.planner_status).toBe("ready_for_execution");
|
||||
expect(result.semantic_data_need).toBe("organization-scoped value-flow evidence");
|
||||
expect(result.selected_chain_id).toBe("value_flow");
|
||||
expect(result.proposed_primitives).toEqual(["query_movements", "aggregate_by_axis", "probe_coverage"]);
|
||||
expect(result.required_axes).toEqual(["organization", "period", "aggregate_axis", "amount", "coverage_target"]);
|
||||
expect(result.catalog_review.review_status).toBe("catalog_compatible");
|
||||
expect(result.reason_codes).toContain("planner_selected_open_scope_value_flow_total_from_data_need_graph");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -275,4 +275,45 @@ describe("assistant MCP discovery runtime bridge", () => {
|
|||
expect(userFacing).not.toContain("runtime_bridge");
|
||||
expect(userFacing).not.toContain("primitive");
|
||||
});
|
||||
it("produces a bounded one-sided value-flow answer for an organization-scoped total without inventing a counterparty", async () => {
|
||||
const result = await runAssistantMcpDiscoveryRuntimeBridge({
|
||||
dataNeedGraph: {
|
||||
schema_version: "assistant_data_need_graph_v1",
|
||||
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||
subject_candidates: [],
|
||||
business_fact_family: "value_flow",
|
||||
action_family: "turnover",
|
||||
aggregation_need: null,
|
||||
time_scope_need: "explicit_period",
|
||||
comparison_need: null,
|
||||
ranking_need: null,
|
||||
proof_expectation: "coverage_checked_fact",
|
||||
clarification_gaps: [],
|
||||
decomposition_candidates: ["collect_scoped_movements", "aggregate_checked_amounts", "probe_coverage"],
|
||||
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
|
||||
reason_codes: ["data_need_graph_built", "data_need_graph_open_scope_total_without_subject"]
|
||||
},
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "turnover",
|
||||
explicit_date_scope: "2020",
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс"
|
||||
},
|
||||
deps: buildDeps([
|
||||
{ Period: "2020-01-10T00:00:00", Amount: 3200, Counterparty: "Клиент-А" },
|
||||
{ Period: "2020-05-22T00:00:00", Amount: 1800, Counterparty: "Клиент-Б" }
|
||||
])
|
||||
});
|
||||
|
||||
expect(result.bridge_status).toBe("answer_draft_ready");
|
||||
expect(result.business_fact_answer_allowed).toBe(true);
|
||||
expect(result.planner.selected_chain_id).toBe("value_flow");
|
||||
expect(result.pilot.derived_value_flow).toMatchObject({
|
||||
counterparty: null,
|
||||
period_scope: "2020",
|
||||
total_amount: 5000
|
||||
});
|
||||
expect(result.answer_draft.confirmed_lines.join("\n")).toContain("5 000");
|
||||
expect(result.answer_draft.confirmed_lines.join("\n")).not.toContain("контрагенту");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -353,4 +353,59 @@ describe("assistant MCP discovery runtime entry point", () => {
|
|||
expect(result.turn_input.data_need_graph?.subject_candidates).toEqual([]);
|
||||
expect(result.bridge?.planner.selected_chain_id).toBe("value_flow_comparison");
|
||||
});
|
||||
it("runs raw organization-scoped incoming totals as an open value-flow chain without inventing a counterparty", async () => {
|
||||
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
|
||||
userMessage: "сколько входящих денег за 2020 год по ООО Альтернатива Плюс?",
|
||||
predecomposeContract: {
|
||||
entities: { organization: "ООО Альтернатива Плюс" },
|
||||
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
|
||||
},
|
||||
deps: buildDeps([
|
||||
{ Period: "2020-01-15T00:00:00", Amount: 2500, Counterparty: "Клиент-А" },
|
||||
{ Period: "2020-06-20T00:00:00", Amount: 1000, Counterparty: "Клиент-Б" }
|
||||
])
|
||||
});
|
||||
|
||||
expect(result.entry_status).toBe("bridge_executed");
|
||||
expect(result.discovery_attempted).toBe(true);
|
||||
expect(result.turn_input.turn_meaning_ref).toMatchObject({
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "turnover",
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||
explicit_date_scope: "2020"
|
||||
});
|
||||
expect(result.turn_input.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
|
||||
expect(result.turn_input.data_need_graph?.subject_candidates).toEqual([]);
|
||||
expect(result.bridge?.planner.selected_chain_id).toBe("value_flow");
|
||||
expect(result.bridge?.pilot.pilot_scope).toBe("counterparty_value_flow_query_movements_v1");
|
||||
expect(result.bridge?.answer_draft.confirmed_lines.join("\n")).toContain("входящ");
|
||||
});
|
||||
|
||||
it("runs raw organization-scoped outgoing totals as an open payout chain without inventing a counterparty", async () => {
|
||||
const orgName = "ООО Альтернатива Плюс";
|
||||
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
|
||||
userMessage: "сколько исходящих денег за 2020 год по ООО Альтернатива Плюс?",
|
||||
predecomposeContract: {
|
||||
entities: { counterparty: orgName, organization: orgName },
|
||||
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
|
||||
},
|
||||
deps: buildDeps([
|
||||
{ Period: "2020-02-18T00:00:00", Amount: 900, Counterparty: "Поставщик-А" },
|
||||
{ Period: "2020-08-07T00:00:00", Amount: 300, Counterparty: "Поставщик-Б" }
|
||||
])
|
||||
});
|
||||
|
||||
expect(result.entry_status).toBe("bridge_executed");
|
||||
expect(result.discovery_attempted).toBe(true);
|
||||
expect(result.turn_input.turn_meaning_ref).toMatchObject({
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "payout",
|
||||
explicit_organization_scope: orgName,
|
||||
explicit_date_scope: "2020"
|
||||
});
|
||||
expect(result.turn_input.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
|
||||
expect(result.bridge?.planner.selected_chain_id).toBe("value_flow");
|
||||
expect(result.bridge?.pilot.pilot_scope).toBe("counterparty_supplier_payout_query_movements_v1");
|
||||
expect(result.bridge?.answer_draft.confirmed_lines.join("\n")).toContain("исход");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1377,4 +1377,49 @@ describe("assistant MCP discovery turn input adapter", () => {
|
|||
expect(result.data_need_graph?.comparison_need).toBe("incoming_vs_outgoing");
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_counterparty_from_predecompose");
|
||||
});
|
||||
it("keeps organization-scoped incoming totals in an open value-flow lane without inventing a counterparty", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage: "сколько входящих денег за 2020 год по ООО Альтернатива Плюс?",
|
||||
predecomposeContract: {
|
||||
entities: { organization: "ООО Альтернатива Плюс" },
|
||||
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.adapter_status).toBe("ready");
|
||||
expect(result.should_run_discovery).toBe(true);
|
||||
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
|
||||
expect(result.turn_meaning_ref).toMatchObject({
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "turnover",
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "counterparty_value_or_turnover",
|
||||
stale_replay_forbidden: true
|
||||
});
|
||||
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
|
||||
expect(result.data_need_graph?.subject_candidates).toEqual([]);
|
||||
expect(result.data_need_graph?.reason_codes).toContain("data_need_graph_open_scope_total_without_subject");
|
||||
});
|
||||
|
||||
it("does not treat mirrored organization/counterparty predecompose as a real subject for organization-scoped payouts", () => {
|
||||
const orgName = "ООО Альтернатива Плюс";
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage: "сколько исходящих денег за 2020 год по ООО Альтернатива Плюс?",
|
||||
predecomposeContract: {
|
||||
entities: { counterparty: orgName, organization: orgName },
|
||||
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.adapter_status).toBe("ready");
|
||||
expect(result.turn_meaning_ref).toMatchObject({
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "payout",
|
||||
explicit_organization_scope: orgName,
|
||||
explicit_date_scope: "2020"
|
||||
});
|
||||
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
|
||||
expect(result.data_need_graph?.subject_candidates).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue