ARCH: закрепить open-scope continuity для org clarification
This commit is contained in:
parent
94e537210c
commit
8a0a4f0922
|
|
@ -0,0 +1,75 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase35_open_scope_org_clarification",
|
||||
"domain": "address_phase35_open_scope_org_clarification",
|
||||
"title": "Phase 35 open-scope organization clarification replay",
|
||||
"description": "Targeted AGENT replay for Big Block D where a generic one-sided money total must ask for organization, then continue the same bounded open-scope value-flow chain after the user clarifies only the organization.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_generic_incoming_total_requires_organization",
|
||||
"title": "Generic incoming total asks for organization instead of inventing a counterparty",
|
||||
"question": "Сколько входящих денег за 2020 год?",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)уточн|нужно",
|
||||
"(?i)организац"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните контрагента",
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)по какому контрагенту",
|
||||
"(?i)не найдено контрагента"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["value_flow_total", "incoming", "open_scope", "organization_clarification", "bounded_autonomy"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_org_clarification_resumes_incoming_total",
|
||||
"title": "Organization-only clarification resumes the same open-scope incoming total chain",
|
||||
"question": "по ООО Альтернатива Плюс",
|
||||
"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", "organization_followup_reuse", "bounded_autonomy"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_outgoing_followup_reuses_org_and_period",
|
||||
"title": "Short outgoing follow-up reuses the same organization and period without reintroducing a counterparty",
|
||||
"question": "а исходящих?",
|
||||
"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)не найдено контрагента",
|
||||
"(?i)уточните организацию"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["value_flow_total", "outgoing", "organization_followup_reuse", "bounded_autonomy"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase36_open_scope_year_switch_after_org_clarification",
|
||||
"domain": "address_phase36_open_scope_year_switch_after_org_clarification",
|
||||
"title": "Phase 36 open-scope year switch after organization clarification",
|
||||
"description": "Targeted AGENT replay for Big Block D where an open-scope value-flow total asks for organization, then preserves the same organization and money-flow contour across a short year-switch follow-up and a one-sided outgoing follow-up.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_generic_incoming_total_requires_organization",
|
||||
"title": "Generic incoming total asks for organization before execution",
|
||||
"question": "Сколько входящих денег за 2020 год?",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)уточн|нужно",
|
||||
"(?i)организац"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните контрагента",
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)по какому контрагенту",
|
||||
"(?i)не найдено контрагента"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["value_flow_total", "incoming", "open_scope", "organization_clarification", "bounded_autonomy"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_org_clarification_resumes_incoming_total",
|
||||
"title": "Organization-only clarification resumes the same incoming total for 2020",
|
||||
"question": "по ООО Альтернатива Плюс",
|
||||
"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", "organization_followup_reuse", "bounded_autonomy"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_year_switch_reuses_org_and_axis",
|
||||
"title": "Short year-switch follow-up keeps the same organization and incoming contour",
|
||||
"question": "а за 2021?",
|
||||
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2021",
|
||||
"(?i)входящ|получ|поступ",
|
||||
"(?i)руб"
|
||||
],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)альтернатива",
|
||||
"(?i)проверенн|найденн"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните контрагента",
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)по какому контрагенту",
|
||||
"(?i)не найдено контрагента",
|
||||
"(?i)уточните организацию"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["value_flow_total", "incoming", "year_switch", "organization_followup_reuse", "bounded_autonomy"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_outgoing_followup_reuses_org_and_new_year",
|
||||
"title": "Outgoing follow-up keeps the same organization and already switched year",
|
||||
"question": "а исходящих?",
|
||||
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2021",
|
||||
"(?i)исходящ|заплат|списан|платеж",
|
||||
"(?i)руб"
|
||||
],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)альтернатива",
|
||||
"(?i)проверенн|найденн"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните контрагента",
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)по какому контрагенту",
|
||||
"(?i)не найдено контрагента",
|
||||
"(?i)уточните организацию"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["value_flow_total", "outgoing", "year_switch", "organization_followup_reuse", "bounded_autonomy"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase37_open_scope_comparison_org_clarification",
|
||||
"domain": "address_phase37_open_scope_comparison_org_clarification",
|
||||
"title": "Phase 37 open-scope comparison organization clarification",
|
||||
"description": "Targeted AGENT replay for Big Block D where an open-scope incoming-vs-outgoing comparison must ask for organization, then resume the same comparison contour after an organization-only clarification and preserve it across a short year switch.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_open_scope_comparison_requires_organization",
|
||||
"title": "Generic incoming-vs-outgoing comparison asks for organization first",
|
||||
"question": "Что больше за 2020 год: входящих денег или исходящих?",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)уточн|нужно",
|
||||
"(?i)организац"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните контрагента",
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)по какому контрагенту",
|
||||
"(?i)не найдено контрагента"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["value_flow_comparison", "open_scope", "organization_clarification", "bounded_autonomy"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_org_clarification_resumes_comparison",
|
||||
"title": "Organization-only clarification resumes the same comparison contour for 2020",
|
||||
"question": "по ООО Альтернатива Плюс",
|
||||
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2020",
|
||||
"(?i)входящ|исходящ|получ|заплат|списан|платеж",
|
||||
"(?i)руб"
|
||||
],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)больше|меньше|превыш|разниц",
|
||||
"(?i)альтернатива",
|
||||
"(?i)проверенн|найденн"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните контрагента",
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)по какому контрагенту",
|
||||
"(?i)не найдено контрагента"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["value_flow_comparison", "organization_followup_reuse", "bounded_autonomy"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_year_switch_reuses_org_and_comparison",
|
||||
"title": "Short year-switch follow-up keeps the same organization and comparison contour",
|
||||
"question": "а за 2021?",
|
||||
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2021",
|
||||
"(?i)входящ|исходящ|получ|заплат|списан|платеж",
|
||||
"(?i)руб"
|
||||
],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)больше|меньше|превыш|разниц",
|
||||
"(?i)альтернатива",
|
||||
"(?i)проверенн|найденн"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните контрагента",
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)по какому контрагенту",
|
||||
"(?i)не найдено контрагента",
|
||||
"(?i)уточните организацию"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["value_flow_comparison", "year_switch", "organization_followup_reuse", "bounded_autonomy"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -127,6 +127,14 @@ function explicitDateScope(pilot) {
|
|||
const normalized = value.trim();
|
||||
return normalized.length > 0 ? normalized : null;
|
||||
}
|
||||
function explicitOrganizationScope(pilot) {
|
||||
const value = pilot.evidence.query_plan.turn_meaning_ref?.explicit_organization_scope;
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
const normalized = value.trim();
|
||||
return normalized.length > 0 ? normalized : null;
|
||||
}
|
||||
function documentOrMovementScopeRu(pilot) {
|
||||
const entity = firstEntityCandidate(pilot);
|
||||
const period = explicitDateScope(pilot);
|
||||
|
|
@ -151,6 +159,12 @@ function isBidirectionalValueFlowComparisonClarification(pilot) {
|
|||
return (pilot.reason_codes.includes("planner_selected_bidirectional_value_flow_comparison_from_data_need_graph") ||
|
||||
pilot.dry_run.reason_codes.includes("planner_selected_bidirectional_value_flow_comparison_from_data_need_graph"));
|
||||
}
|
||||
function isOpenScopeValueFlowClarification(pilot) {
|
||||
return (pilot.reason_codes.includes("planner_selected_open_scope_value_flow_total_from_data_need_graph") ||
|
||||
pilot.reason_codes.includes("planner_selected_monthly_open_scope_value_flow_total_from_data_need_graph") ||
|
||||
pilot.dry_run.reason_codes.includes("planner_selected_open_scope_value_flow_total_from_data_need_graph") ||
|
||||
pilot.dry_run.reason_codes.includes("planner_selected_monthly_open_scope_value_flow_total_from_data_need_graph"));
|
||||
}
|
||||
function isDocumentLaneClarification(pilot) {
|
||||
return (isDocumentPilot(pilot) ||
|
||||
pilot.reason_codes.includes("planner_selected_document_recipe") ||
|
||||
|
|
@ -172,6 +186,16 @@ function dryRunMissingAxis(pilot, axis) {
|
|||
return pilot.dry_run.execution_steps.some((step) => step.missing_axis_options.some((option) => option.includes(axis)));
|
||||
}
|
||||
function clarificationNeedRu(pilot) {
|
||||
const organizationScopedOpenTotal = pilot.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
|
||||
pilot.dry_run.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
|
||||
pilot.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph") ||
|
||||
pilot.dry_run.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph");
|
||||
if (organizationScopedOpenTotal) {
|
||||
return {
|
||||
subject: "\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044e",
|
||||
verb: "\u043d\u0443\u0436\u043d\u043e"
|
||||
};
|
||||
}
|
||||
const hasCounterparty = dryRunHasAxis(pilot, "counterparty");
|
||||
const hasAccount = dryRunHasAxis(pilot, "account");
|
||||
const needsPeriod = dryRunMissingAxis(pilot, "period");
|
||||
|
|
@ -188,9 +212,16 @@ function clarificationNeedRu(pilot) {
|
|||
return { subject: "контекст проверки", verb: "нужно" };
|
||||
}
|
||||
function clarificationNextStepLine(pilot, laneLabel) {
|
||||
const organizationScopedOpenTotal = pilot.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
|
||||
pilot.dry_run.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
|
||||
pilot.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph") ||
|
||||
pilot.dry_run.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph");
|
||||
const needsPeriod = dryRunMissingAxis(pilot, "period");
|
||||
const needsOrganization = dryRunMissingAxis(pilot, "organization");
|
||||
const scopeSuffix = laneScopeSuffix(pilot);
|
||||
if (organizationScopedOpenTotal && !needsPeriod) {
|
||||
return `Уточните организацию, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
|
||||
}
|
||||
if (needsPeriod && needsOrganization) {
|
||||
return `Уточните период и организацию, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
|
||||
}
|
||||
|
|
@ -298,6 +329,10 @@ function headlineFor(mode, pilot) {
|
|||
const need = clarificationNeedRu(pilot);
|
||||
return `Могу посчитать ranking по денежному потоку между контрагентами, но для bounded поиска в 1С ${need.verb} ${need.subject}.`;
|
||||
}
|
||||
if (mode === "needs_clarification" && isOpenScopeValueFlowClarification(pilot)) {
|
||||
const need = clarificationNeedRu(pilot);
|
||||
return `Могу посчитать общий денежный поток в проверяемом окне, но для bounded поиска в 1С ${need.verb} ${need.subject}.`;
|
||||
}
|
||||
if (mode === "needs_clarification") {
|
||||
return "Нужно уточнить контекст перед поиском в 1С.";
|
||||
}
|
||||
|
|
@ -337,6 +372,9 @@ function nextStepFor(mode, pilot) {
|
|||
if (mode === "needs_clarification" && isRankedValueFlowClarification(pilot)) {
|
||||
return clarificationNextStepLine(pilot, "ranking-поиску между контрагентами");
|
||||
}
|
||||
if (mode === "needs_clarification" && isOpenScopeValueFlowClarification(pilot)) {
|
||||
return clarificationNextStepLine(pilot, "денежному потоку");
|
||||
}
|
||||
if (mode === "needs_clarification") {
|
||||
return "Уточните контрагента, период или организацию, и я смогу выполнить проверку по 1С.";
|
||||
}
|
||||
|
|
@ -541,6 +579,8 @@ function derivedValueFlowConfirmedLine(pilot) {
|
|||
if (!flow) {
|
||||
return null;
|
||||
}
|
||||
const organizationScope = explicitOrganizationScope(pilot);
|
||||
const organization = organizationScope ? ` по организации ${organizationScope}` : "";
|
||||
const counterparty = flow.counterparty ? ` по контрагенту ${flow.counterparty}` : "";
|
||||
const period = flow.period_scope ? ` за период ${flow.period_scope}` : " в проверенном окне";
|
||||
const movementLabel = flow.value_flow_direction === "outgoing_supplier_payout"
|
||||
|
|
|
|||
|
|
@ -85,6 +85,30 @@ function comparisonNeedFor(action) {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
function hasOpenScopeOneSidedValueTotalHint(rawUtterance, action) {
|
||||
if (!rawUtterance) {
|
||||
return false;
|
||||
}
|
||||
if (action === "turnover") {
|
||||
return /(?:\bсколько\s+(?:мы\s+)?(?:получили|получено|входящих(?:\s+денег)?|поступлений|денег\s+пришло)\b|(?:сумма|объем)\s+(?:входящих|поступлений)|поступлений\s+за\b)/iu.test(rawUtterance);
|
||||
}
|
||||
if (action === "payout") {
|
||||
return /(?:\bсколько\s+(?:мы\s+)?(?:заплатили|выплатили|потратили|исходящих(?:\s+денег)?|платежей|списаний)\b|(?:сумма|объем)\s+(?:исходящих|платежей|списаний)|(?:платежей|списаний)\s+за\b)/iu.test(rawUtterance);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function hasOpenScopeOneSidedValueTotalHintUtf8Safe(rawUtterance, action) {
|
||||
if (!rawUtterance) {
|
||||
return false;
|
||||
}
|
||||
if (action === "turnover") {
|
||||
return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:\u043c\u044b\s+)?(?:\u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438|\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043e|\u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445(?:\s+\u0434\u0435\u043d\u0435\u0433)?|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439|\u0434\u0435\u043d\u0435\u0433\s+\u043f\u0440\u0438\u0448\u043b\u043e)|(?:\u0441\u0443\u043c\u043c\u0430|\u043e\u0431\u044a\u0435\u043c)\s+(?:\u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439)|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439\s+\u0437\u0430)/u.test(rawUtterance);
|
||||
}
|
||||
if (action === "payout") {
|
||||
return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:\u043c\u044b\s+)?(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438|\u0432\u044b\u043f\u043b\u0430\u0442\u0438\u043b\u0438|\u043f\u043e\u0442\u0440\u0430\u0442\u0438\u043b\u0438|\u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445(?:\s+\u0434\u0435\u043d\u0435\u0433)?|\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439)|(?:\u0441\u0443\u043c\u043c\u0430|\u043e\u0431\u044a\u0435\u043c)\s+(?:\u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445|\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439)|(?:\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439)\s+\u0437\u0430)/u.test(rawUtterance);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function supportsOrganizationScopedOpenTotal(action) {
|
||||
return action === "turnover" || action === "payout";
|
||||
}
|
||||
|
|
@ -95,7 +119,7 @@ function allowsOpenScopeWithoutSubject(input) {
|
|||
if (input.rankingNeed || input.comparisonNeed === "incoming_vs_outgoing") {
|
||||
return true;
|
||||
}
|
||||
return Boolean(input.organizationScope && supportsOrganizationScopedOpenTotal(input.action));
|
||||
return Boolean(supportsOrganizationScopedOpenTotal(input.action) && (input.organizationScope || input.oneSidedOpenScopeTotalHint));
|
||||
}
|
||||
function rankingNeedFromRawUtterance(value) {
|
||||
const text = lower(value);
|
||||
|
|
@ -229,19 +253,30 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
|
|||
const aggregationNeed = aggregationNeedFor(aggregationAxis);
|
||||
const comparisonNeed = comparisonNeedFor(action);
|
||||
const rankingNeed = rankingNeedFromRawUtterance(rawUtterance);
|
||||
const oneSidedOpenScopeTotalHint = hasOpenScopeOneSidedValueTotalHintUtf8Safe(rawUtterance, action);
|
||||
const openScopeWithoutSubject = subjectCandidates.length === 0 &&
|
||||
allowsOpenScopeWithoutSubject({
|
||||
family: businessFactFamily,
|
||||
action,
|
||||
organizationScope: explicitOrganizationScope,
|
||||
comparisonNeed,
|
||||
rankingNeed
|
||||
rankingNeed,
|
||||
oneSidedOpenScopeTotalHint
|
||||
});
|
||||
const clarificationGaps = [];
|
||||
if (unsupported === "metadata_lane_choice_clarification" || action === "resolve_next_lane") {
|
||||
pushUnique(clarificationGaps, "lane_family_choice");
|
||||
}
|
||||
if (subjectCandidates.length === 0 && businessFactFamily !== "schema_surface" && !openScopeWithoutSubject) {
|
||||
if (subjectCandidates.length === 0 &&
|
||||
businessFactFamily === "value_flow" &&
|
||||
openScopeWithoutSubject &&
|
||||
!rankingNeed &&
|
||||
!comparisonNeed &&
|
||||
oneSidedOpenScopeTotalHint &&
|
||||
!explicitOrganizationScope) {
|
||||
pushUnique(clarificationGaps, "organization");
|
||||
}
|
||||
else if (subjectCandidates.length === 0 && businessFactFamily !== "schema_surface" && !openScopeWithoutSubject) {
|
||||
pushUnique(clarificationGaps, "subject");
|
||||
}
|
||||
const timeScopeNeed = timeScopeNeedFor({
|
||||
|
|
@ -279,6 +314,9 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
|
|||
if (openScopeWithoutSubject && !rankingNeed && !comparisonNeed) {
|
||||
pushReason(reasonCodes, "data_need_graph_open_scope_total_without_subject");
|
||||
}
|
||||
if (clarificationGaps.includes("organization")) {
|
||||
pushReason(reasonCodes, "data_need_graph_open_scope_total_needs_organization");
|
||||
}
|
||||
if (clarificationGaps.length > 0) {
|
||||
pushReason(reasonCodes, "data_need_graph_has_clarification_gaps");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,9 @@ function hasEntity(meaning) {
|
|||
function hasSubjectCandidates(graph) {
|
||||
return (graph?.subject_candidates.length ?? 0) > 0;
|
||||
}
|
||||
function hasReasonCode(graph, reasonCode) {
|
||||
return (graph?.reason_codes ?? []).includes(reasonCode);
|
||||
}
|
||||
function aggregationAxis(meaning) {
|
||||
return toNonEmptyString(meaning?.asked_aggregation_axis)?.toLowerCase() ?? null;
|
||||
}
|
||||
|
|
@ -87,6 +90,9 @@ function recipeFor(input) {
|
|||
const graphAggregation = lower(dataNeedGraph?.aggregation_need);
|
||||
const graphClarificationGaps = (dataNeedGraph?.clarification_gaps ?? []).map((item) => lower(item));
|
||||
const organizationScope = toNonEmptyString(meaning?.explicit_organization_scope);
|
||||
const openScopeTotalWithoutSubject = graphFactFamily === "value_flow" &&
|
||||
!hasSubjectCandidates(dataNeedGraph) &&
|
||||
hasReasonCode(dataNeedGraph, "data_need_graph_open_scope_total_without_subject");
|
||||
const combined = `${domain} ${action} ${unsupported}`.trim();
|
||||
const axes = [];
|
||||
const requestedAggregationAxis = aggregationAxis(meaning);
|
||||
|
|
@ -133,7 +139,8 @@ function recipeFor(input) {
|
|||
: "planner_selected_top_ranked_value_flow_from_data_need_graph"
|
||||
};
|
||||
}
|
||||
if (!hasSubjectCandidates(dataNeedGraph) && organizationScope) {
|
||||
if (openScopeTotalWithoutSubject) {
|
||||
pushUnique(axes, "organization");
|
||||
pushUnique(axes, "aggregate_axis");
|
||||
pushUnique(axes, "amount");
|
||||
pushUnique(axes, "coverage_target");
|
||||
|
|
@ -354,7 +361,27 @@ function planAssistantMcpDiscovery(input) {
|
|||
maxProbeCount: budgetOverride.maxProbeCount
|
||||
});
|
||||
const review = (0, assistantMcpCatalogIndex_1.reviewAssistantMcpDiscoveryPlanAgainstCatalog)(plan);
|
||||
const plannerStatus = statusFrom(plan, review);
|
||||
const organizationClarificationRequired = (dataNeedGraph?.clarification_gaps ?? []).includes("organization") &&
|
||||
!toNonEmptyString(input.turnMeaning?.explicit_organization_scope);
|
||||
const adjustedReview = organizationClarificationRequired && recipe.primitives.includes("query_movements")
|
||||
? {
|
||||
...review,
|
||||
review_status: "needs_more_axes",
|
||||
missing_axes_by_primitive: {
|
||||
...review.missing_axes_by_primitive,
|
||||
query_movements: review.missing_axes_by_primitive.query_movements?.length
|
||||
? review.missing_axes_by_primitive.query_movements
|
||||
: [["organization"]]
|
||||
},
|
||||
reason_codes: review.reason_codes.includes("catalog_requires_organization_scope_from_data_need_graph")
|
||||
? review.reason_codes
|
||||
: [...review.reason_codes, "catalog_requires_organization_scope_from_data_need_graph"]
|
||||
}
|
||||
: review;
|
||||
const plannerStatus = organizationClarificationRequired ? "needs_clarification" : statusFrom(plan, adjustedReview);
|
||||
if (organizationClarificationRequired) {
|
||||
pushReason(reasonCodes, "planner_requires_organization_scope_from_data_need_graph");
|
||||
}
|
||||
if (plannerStatus === "ready_for_execution") {
|
||||
pushReason(reasonCodes, "planner_ready_for_guarded_mcp_execution");
|
||||
}
|
||||
|
|
@ -375,7 +402,7 @@ function planAssistantMcpDiscovery(input) {
|
|||
proposed_primitives: recipe.primitives,
|
||||
required_axes: recipe.axes,
|
||||
discovery_plan: plan,
|
||||
catalog_review: review,
|
||||
catalog_review: adjustedReview,
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,6 +138,19 @@ function readDiscoveryTurnMeaning(entryPoint) {
|
|||
const turnInput = toRecordObject(entryPoint?.turn_input);
|
||||
return toRecordObject(turnInput?.turn_meaning_ref);
|
||||
}
|
||||
function readDiscoveryDataNeedGraph(entryPoint) {
|
||||
const turnInput = toRecordObject(entryPoint?.turn_input);
|
||||
return toRecordObject(turnInput?.data_need_graph);
|
||||
}
|
||||
function isOpenScopeValueFlowWithoutSubject(entryPoint) {
|
||||
const graph = readDiscoveryDataNeedGraph(entryPoint);
|
||||
const businessFactFamily = toNonEmptyString(graph?.business_fact_family);
|
||||
const subjectCandidates = Array.isArray(graph?.subject_candidates) ? graph.subject_candidates : [];
|
||||
const reasonCodes = Array.isArray(graph?.reason_codes) ? graph.reason_codes : [];
|
||||
return (businessFactFamily === "value_flow" &&
|
||||
subjectCandidates.length === 0 &&
|
||||
reasonCodes.some((reason) => toNonEmptyString(reason) === "data_need_graph_open_scope_total_without_subject"));
|
||||
}
|
||||
function readTruthAnswerShape(input) {
|
||||
const directShape = toRecordObject(input.addressRuntimeMeta?.answer_shape_contract);
|
||||
if (directShape) {
|
||||
|
|
@ -197,6 +210,9 @@ function hasAlignedFactualAddressReply(input, entryPoint) {
|
|||
if (!hasEffectivelyFactualAddressReply(input)) {
|
||||
return false;
|
||||
}
|
||||
if (hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||
return isDetectedIntentAlignedWithTurnMeaning(detectedIntent, readDiscoveryTurnMeaning(entryPoint));
|
||||
}
|
||||
|
|
@ -218,6 +234,10 @@ function hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint) {
|
|||
if (!detectedIntent || (!askedDomain && !askedAction && !unsupportedFamily)) {
|
||||
return false;
|
||||
}
|
||||
if (detectedIntent === "customer_revenue_and_payments" &&
|
||||
isOpenScopeValueFlowWithoutSubject(entryPoint)) {
|
||||
return true;
|
||||
}
|
||||
return !isDetectedIntentAlignedWithTurnMeaning(detectedIntent, turnMeaning);
|
||||
}
|
||||
function hasMatchedFactualAddressContinuationTarget(input, entryPoint) {
|
||||
|
|
|
|||
|
|
@ -313,6 +313,17 @@ function hasOrganizationScopeSignalUtf8(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 extractOrganizationScopeFromRawText(value) {
|
||||
const text = toNonEmptyString(value);
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
const match = text.match(/(?:^|[\s,;:])(?:\u043f\u043e|for|in|within)?\s*((?:\u041e\u041e\u041e|\u0418\u041f|\u0410\u041e|\u041f\u0410\u041e|\u0417\u0410\u041e|LLC|Inc|LTD|Corp)\s+[^\n,.;:!?]+)/u);
|
||||
if (!match?.[1]) {
|
||||
return null;
|
||||
}
|
||||
return toNonEmptyString(match[1]);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
|
@ -590,12 +601,25 @@ 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 rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText);
|
||||
const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText ?? rawEffectiveText ?? rawSignalSourceText);
|
||||
const explicitOrganizationScopeSignal = Boolean(rawOrganizationMentionSignal &&
|
||||
(rawOrganizationScope ?? predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope));
|
||||
const organizationClarificationFollowupApplicable = Boolean(followupSeed.domain === "counterparty_value" &&
|
||||
!followupSeed.counterparty &&
|
||||
rawOrganizationMentionSignal &&
|
||||
(rawOrganizationScope || followupSeed.organization) &&
|
||||
!rawLifecycleSignal &&
|
||||
!rawValueFlowSignal &&
|
||||
!rawMetadataSignal);
|
||||
const rawOpenScopeValueFlowOrganizationSignal = Boolean(rawValueFlowSignal &&
|
||||
!rawBidirectionalValueFlowSignal &&
|
||||
hasOrganizationScopeSignalUtf8(rawText) &&
|
||||
(predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope));
|
||||
explicitOrganizationScopeSignal);
|
||||
const predecomposeOrganizationMirrorsCounterparty = sameScopedName(predecomposeEntities.counterparty, predecomposeEntities.organization);
|
||||
const organizationMirrorsPredecomposeCounterparty = Boolean((rawBidirectionalValueFlowSignal || hasValueRankingSignal(rawText) || rawOpenScopeValueFlowOrganizationSignal) &&
|
||||
const organizationMirrorsPredecomposeCounterparty = Boolean((rawBidirectionalValueFlowSignal ||
|
||||
hasValueRankingSignal(rawText) ||
|
||||
rawOpenScopeValueFlowOrganizationSignal ||
|
||||
explicitOrganizationScopeSignal) &&
|
||||
(sameScopedName(predecomposeEntities.counterparty, assistantTurnMeaningOrganizationScope) ||
|
||||
predecomposeOrganizationMirrorsCounterparty));
|
||||
const normalizedPredecomposeCounterparty = organizationMirrorsPredecomposeCounterparty
|
||||
|
|
@ -605,7 +629,11 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
const followupDiscoverySeedApplicable = Boolean(followupSeed.domain &&
|
||||
!rawLifecycleSignal &&
|
||||
!rawValueFlowSignal &&
|
||||
(monthlyAggregationSignal || explicitDateScopeLiteralDetected || predecomposeDateScope));
|
||||
(monthlyAggregationSignal ||
|
||||
explicitDateScopeLiteralDetected ||
|
||||
predecomposeDateScope ||
|
||||
explicitOrganizationScopeSignal ||
|
||||
organizationClarificationFollowupApplicable));
|
||||
const metadataFollowupSeedApplicable = Boolean(followupSeed.domain === "metadata" &&
|
||||
!rawLifecycleSignal &&
|
||||
!rawValueFlowSignal &&
|
||||
|
|
@ -844,13 +872,17 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
Boolean(bidirectionalValueFlowSignal ||
|
||||
hasValueRankingSignal(rawText) ||
|
||||
rawOpenScopeValueFlowOrganizationSignal ||
|
||||
explicitOrganizationScopeSignal ||
|
||||
followupSeed.organization);
|
||||
if (openScopeValueFlowWithoutCounterparty && !valueFlowOrganizationStaysScope) {
|
||||
pushUnique(entityCandidates, predecomposeEntities.organization);
|
||||
pushUnique(entityCandidates, followupSeed.organization);
|
||||
}
|
||||
const explicitOrganizationScope = valueFlowOrganizationStaysScope || !openScopeValueFlowWithoutCounterparty
|
||||
? predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope ?? followupSeed.organization
|
||||
? rawOrganizationScope ??
|
||||
predecomposeEntities.organization ??
|
||||
assistantTurnMeaningOrganizationScope ??
|
||||
followupSeed.organization
|
||||
: null;
|
||||
if (valueFlowOrganizationStaysScope && explicitOrganizationScope) {
|
||||
for (let index = entityCandidates.length - 1; index >= 0; index -= 1) {
|
||||
|
|
@ -976,6 +1008,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
metadataGroundedDocumentLaneApplicable ||
|
||||
groundedValueFlowFollowupApplicable,
|
||||
forceDiscoveryOverExplicitIntent: Boolean(entityResolutionClarificationCandidate) ||
|
||||
organizationClarificationFollowupApplicable ||
|
||||
metadataAmbiguityLaneClarificationApplicable ||
|
||||
metadataGroundedMovementLaneApplicable ||
|
||||
metadataGroundedDocumentLaneApplicable ||
|
||||
|
|
@ -1025,6 +1058,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
if (entityResolutionClarificationCandidate) {
|
||||
pushReason(reasonCodes, "mcp_discovery_entity_resolution_clarification_candidate_selected");
|
||||
}
|
||||
if (organizationClarificationFollowupApplicable) {
|
||||
pushReason(reasonCodes, "mcp_discovery_organization_clarification_followup_from_followup_context");
|
||||
}
|
||||
if (payoutSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_payout_signal_detected");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -174,6 +174,15 @@ function explicitDateScope(pilot: AssistantMcpDiscoveryPilotExecutionContract):
|
|||
return normalized.length > 0 ? normalized : null;
|
||||
}
|
||||
|
||||
function explicitOrganizationScope(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||||
const value = pilot.evidence.query_plan.turn_meaning_ref?.explicit_organization_scope;
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
const normalized = value.trim();
|
||||
return normalized.length > 0 ? normalized : null;
|
||||
}
|
||||
|
||||
function documentOrMovementScopeRu(pilot: AssistantMcpDiscoveryPilotExecutionContract): string {
|
||||
const entity = firstEntityCandidate(pilot);
|
||||
const period = explicitDateScope(pilot);
|
||||
|
|
@ -210,6 +219,15 @@ function isBidirectionalValueFlowComparisonClarification(
|
|||
);
|
||||
}
|
||||
|
||||
function isOpenScopeValueFlowClarification(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||||
return (
|
||||
pilot.reason_codes.includes("planner_selected_open_scope_value_flow_total_from_data_need_graph") ||
|
||||
pilot.reason_codes.includes("planner_selected_monthly_open_scope_value_flow_total_from_data_need_graph") ||
|
||||
pilot.dry_run.reason_codes.includes("planner_selected_open_scope_value_flow_total_from_data_need_graph") ||
|
||||
pilot.dry_run.reason_codes.includes("planner_selected_monthly_open_scope_value_flow_total_from_data_need_graph")
|
||||
);
|
||||
}
|
||||
|
||||
function isDocumentLaneClarification(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||||
return (
|
||||
isDocumentPilot(pilot) ||
|
||||
|
|
@ -241,6 +259,17 @@ function dryRunMissingAxis(pilot: AssistantMcpDiscoveryPilotExecutionContract, a
|
|||
function clarificationNeedRu(
|
||||
pilot: AssistantMcpDiscoveryPilotExecutionContract
|
||||
): { subject: string; verb: string } {
|
||||
const organizationScopedOpenTotal =
|
||||
pilot.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
|
||||
pilot.dry_run.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
|
||||
pilot.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph") ||
|
||||
pilot.dry_run.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph");
|
||||
if (organizationScopedOpenTotal) {
|
||||
return {
|
||||
subject: "\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044e",
|
||||
verb: "\u043d\u0443\u0436\u043d\u043e"
|
||||
};
|
||||
}
|
||||
const hasCounterparty = dryRunHasAxis(pilot, "counterparty");
|
||||
const hasAccount = dryRunHasAxis(pilot, "account");
|
||||
const needsPeriod = dryRunMissingAxis(pilot, "period");
|
||||
|
|
@ -261,9 +290,17 @@ function clarificationNextStepLine(
|
|||
pilot: AssistantMcpDiscoveryPilotExecutionContract,
|
||||
laneLabel: string
|
||||
): string {
|
||||
const organizationScopedOpenTotal =
|
||||
pilot.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
|
||||
pilot.dry_run.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
|
||||
pilot.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph") ||
|
||||
pilot.dry_run.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph");
|
||||
const needsPeriod = dryRunMissingAxis(pilot, "period");
|
||||
const needsOrganization = dryRunMissingAxis(pilot, "organization");
|
||||
const scopeSuffix = laneScopeSuffix(pilot);
|
||||
if (organizationScopedOpenTotal && !needsPeriod) {
|
||||
return `Уточните организацию, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
|
||||
}
|
||||
if (needsPeriod && needsOrganization) {
|
||||
return `Уточните период и организацию, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
|
||||
}
|
||||
|
|
@ -378,6 +415,10 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
|||
const need = clarificationNeedRu(pilot);
|
||||
return `Могу посчитать ranking по денежному потоку между контрагентами, но для bounded поиска в 1С ${need.verb} ${need.subject}.`;
|
||||
}
|
||||
if (mode === "needs_clarification" && isOpenScopeValueFlowClarification(pilot)) {
|
||||
const need = clarificationNeedRu(pilot);
|
||||
return `Могу посчитать общий денежный поток в проверяемом окне, но для bounded поиска в 1С ${need.verb} ${need.subject}.`;
|
||||
}
|
||||
if (mode === "needs_clarification") {
|
||||
return "Нужно уточнить контекст перед поиском в 1С.";
|
||||
}
|
||||
|
|
@ -422,6 +463,9 @@ function nextStepFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
|||
if (mode === "needs_clarification" && isRankedValueFlowClarification(pilot)) {
|
||||
return clarificationNextStepLine(pilot, "ranking-поиску между контрагентами");
|
||||
}
|
||||
if (mode === "needs_clarification" && isOpenScopeValueFlowClarification(pilot)) {
|
||||
return clarificationNextStepLine(pilot, "денежному потоку");
|
||||
}
|
||||
if (mode === "needs_clarification") {
|
||||
return "Уточните контрагента, период или организацию, и я смогу выполнить проверку по 1С.";
|
||||
}
|
||||
|
|
@ -646,6 +690,8 @@ function derivedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutio
|
|||
if (!flow) {
|
||||
return null;
|
||||
}
|
||||
const organizationScope = explicitOrganizationScope(pilot);
|
||||
const organization = organizationScope ? ` по организации ${organizationScope}` : "";
|
||||
const counterparty = flow.counterparty ? ` по контрагенту ${flow.counterparty}` : "";
|
||||
const period = flow.period_scope ? ` за период ${flow.period_scope}` : " в проверенном окне";
|
||||
const movementLabel =
|
||||
|
|
|
|||
|
|
@ -132,6 +132,40 @@ function comparisonNeedFor(action: string): string | null {
|
|||
return null;
|
||||
}
|
||||
|
||||
function hasOpenScopeOneSidedValueTotalHint(rawUtterance: string, action: string): boolean {
|
||||
if (!rawUtterance) {
|
||||
return false;
|
||||
}
|
||||
if (action === "turnover") {
|
||||
return /(?:\bсколько\s+(?:мы\s+)?(?:получили|получено|входящих(?:\s+денег)?|поступлений|денег\s+пришло)\b|(?:сумма|объем)\s+(?:входящих|поступлений)|поступлений\s+за\b)/iu.test(
|
||||
rawUtterance
|
||||
);
|
||||
}
|
||||
if (action === "payout") {
|
||||
return /(?:\bсколько\s+(?:мы\s+)?(?:заплатили|выплатили|потратили|исходящих(?:\s+денег)?|платежей|списаний)\b|(?:сумма|объем)\s+(?:исходящих|платежей|списаний)|(?:платежей|списаний)\s+за\b)/iu.test(
|
||||
rawUtterance
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasOpenScopeOneSidedValueTotalHintUtf8Safe(rawUtterance: string, action: string): boolean {
|
||||
if (!rawUtterance) {
|
||||
return false;
|
||||
}
|
||||
if (action === "turnover") {
|
||||
return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:\u043c\u044b\s+)?(?:\u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438|\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043e|\u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445(?:\s+\u0434\u0435\u043d\u0435\u0433)?|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439|\u0434\u0435\u043d\u0435\u0433\s+\u043f\u0440\u0438\u0448\u043b\u043e)|(?:\u0441\u0443\u043c\u043c\u0430|\u043e\u0431\u044a\u0435\u043c)\s+(?:\u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439)|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439\s+\u0437\u0430)/u.test(
|
||||
rawUtterance
|
||||
);
|
||||
}
|
||||
if (action === "payout") {
|
||||
return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:\u043c\u044b\s+)?(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438|\u0432\u044b\u043f\u043b\u0430\u0442\u0438\u043b\u0438|\u043f\u043e\u0442\u0440\u0430\u0442\u0438\u043b\u0438|\u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445(?:\s+\u0434\u0435\u043d\u0435\u0433)?|\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439)|(?:\u0441\u0443\u043c\u043c\u0430|\u043e\u0431\u044a\u0435\u043c)\s+(?:\u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445|\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439)|(?:\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439)\s+\u0437\u0430)/u.test(
|
||||
rawUtterance
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function supportsOrganizationScopedOpenTotal(action: string): boolean {
|
||||
return action === "turnover" || action === "payout";
|
||||
}
|
||||
|
|
@ -142,6 +176,7 @@ function allowsOpenScopeWithoutSubject(input: {
|
|||
organizationScope: string | null;
|
||||
comparisonNeed: string | null;
|
||||
rankingNeed: string | null;
|
||||
oneSidedOpenScopeTotalHint: boolean;
|
||||
}): boolean {
|
||||
if (input.family !== "value_flow") {
|
||||
return false;
|
||||
|
|
@ -149,7 +184,9 @@ function allowsOpenScopeWithoutSubject(input: {
|
|||
if (input.rankingNeed || input.comparisonNeed === "incoming_vs_outgoing") {
|
||||
return true;
|
||||
}
|
||||
return Boolean(input.organizationScope && supportsOrganizationScopedOpenTotal(input.action));
|
||||
return Boolean(
|
||||
supportsOrganizationScopedOpenTotal(input.action) && (input.organizationScope || input.oneSidedOpenScopeTotalHint)
|
||||
);
|
||||
}
|
||||
|
||||
function rankingNeedFromRawUtterance(value: string): string | null {
|
||||
|
|
@ -303,6 +340,7 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
|
|||
const aggregationNeed = aggregationNeedFor(aggregationAxis);
|
||||
const comparisonNeed = comparisonNeedFor(action);
|
||||
const rankingNeed = rankingNeedFromRawUtterance(rawUtterance);
|
||||
const oneSidedOpenScopeTotalHint = hasOpenScopeOneSidedValueTotalHintUtf8Safe(rawUtterance, action);
|
||||
const openScopeWithoutSubject =
|
||||
subjectCandidates.length === 0 &&
|
||||
allowsOpenScopeWithoutSubject({
|
||||
|
|
@ -310,13 +348,24 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
|
|||
action,
|
||||
organizationScope: explicitOrganizationScope,
|
||||
comparisonNeed,
|
||||
rankingNeed
|
||||
rankingNeed,
|
||||
oneSidedOpenScopeTotalHint
|
||||
});
|
||||
const clarificationGaps: string[] = [];
|
||||
if (unsupported === "metadata_lane_choice_clarification" || action === "resolve_next_lane") {
|
||||
pushUnique(clarificationGaps, "lane_family_choice");
|
||||
}
|
||||
if (subjectCandidates.length === 0 && businessFactFamily !== "schema_surface" && !openScopeWithoutSubject) {
|
||||
if (
|
||||
subjectCandidates.length === 0 &&
|
||||
businessFactFamily === "value_flow" &&
|
||||
openScopeWithoutSubject &&
|
||||
!rankingNeed &&
|
||||
!comparisonNeed &&
|
||||
oneSidedOpenScopeTotalHint &&
|
||||
!explicitOrganizationScope
|
||||
) {
|
||||
pushUnique(clarificationGaps, "organization");
|
||||
} else if (subjectCandidates.length === 0 && businessFactFamily !== "schema_surface" && !openScopeWithoutSubject) {
|
||||
pushUnique(clarificationGaps, "subject");
|
||||
}
|
||||
const timeScopeNeed = timeScopeNeedFor({
|
||||
|
|
@ -353,6 +402,9 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
|
|||
if (openScopeWithoutSubject && !rankingNeed && !comparisonNeed) {
|
||||
pushReason(reasonCodes, "data_need_graph_open_scope_total_without_subject");
|
||||
}
|
||||
if (clarificationGaps.includes("organization")) {
|
||||
pushReason(reasonCodes, "data_need_graph_open_scope_total_needs_organization");
|
||||
}
|
||||
if (clarificationGaps.length > 0) {
|
||||
pushReason(reasonCodes, "data_need_graph_has_clarification_gaps");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,6 +102,13 @@ function hasSubjectCandidates(graph: AssistantMcpDiscoveryDataNeedGraphContract
|
|||
return (graph?.subject_candidates.length ?? 0) > 0;
|
||||
}
|
||||
|
||||
function hasReasonCode(
|
||||
graph: AssistantMcpDiscoveryDataNeedGraphContract | null | undefined,
|
||||
reasonCode: string
|
||||
): boolean {
|
||||
return (graph?.reason_codes ?? []).includes(reasonCode);
|
||||
}
|
||||
|
||||
function aggregationAxis(meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefined): string | null {
|
||||
return toNonEmptyString(meaning?.asked_aggregation_axis)?.toLowerCase() ?? null;
|
||||
}
|
||||
|
|
@ -154,6 +161,10 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
|
|||
const graphAggregation = lower(dataNeedGraph?.aggregation_need);
|
||||
const graphClarificationGaps = (dataNeedGraph?.clarification_gaps ?? []).map((item) => lower(item));
|
||||
const organizationScope = toNonEmptyString(meaning?.explicit_organization_scope);
|
||||
const openScopeTotalWithoutSubject =
|
||||
graphFactFamily === "value_flow" &&
|
||||
!hasSubjectCandidates(dataNeedGraph) &&
|
||||
hasReasonCode(dataNeedGraph, "data_need_graph_open_scope_total_without_subject");
|
||||
const combined = `${domain} ${action} ${unsupported}`.trim();
|
||||
const axes: string[] = [];
|
||||
const requestedAggregationAxis = aggregationAxis(meaning);
|
||||
|
|
@ -205,7 +216,8 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
|
|||
: "planner_selected_top_ranked_value_flow_from_data_need_graph"
|
||||
};
|
||||
}
|
||||
if (!hasSubjectCandidates(dataNeedGraph) && organizationScope) {
|
||||
if (openScopeTotalWithoutSubject) {
|
||||
pushUnique(axes, "organization");
|
||||
pushUnique(axes, "aggregate_axis");
|
||||
pushUnique(axes, "amount");
|
||||
pushUnique(axes, "coverage_target");
|
||||
|
|
@ -451,7 +463,30 @@ export function planAssistantMcpDiscovery(
|
|||
maxProbeCount: budgetOverride.maxProbeCount
|
||||
});
|
||||
const review = reviewAssistantMcpDiscoveryPlanAgainstCatalog(plan);
|
||||
const plannerStatus = statusFrom(plan, review);
|
||||
const organizationClarificationRequired =
|
||||
(dataNeedGraph?.clarification_gaps ?? []).includes("organization") &&
|
||||
!toNonEmptyString(input.turnMeaning?.explicit_organization_scope);
|
||||
const adjustedReview =
|
||||
organizationClarificationRequired && recipe.primitives.includes("query_movements")
|
||||
? {
|
||||
...review,
|
||||
review_status: "needs_more_axes" as const,
|
||||
missing_axes_by_primitive: {
|
||||
...review.missing_axes_by_primitive,
|
||||
query_movements: review.missing_axes_by_primitive.query_movements?.length
|
||||
? review.missing_axes_by_primitive.query_movements
|
||||
: [["organization"]]
|
||||
},
|
||||
reason_codes: review.reason_codes.includes("catalog_requires_organization_scope_from_data_need_graph")
|
||||
? review.reason_codes
|
||||
: [...review.reason_codes, "catalog_requires_organization_scope_from_data_need_graph"]
|
||||
}
|
||||
: review;
|
||||
const plannerStatus = organizationClarificationRequired ? "needs_clarification" : statusFrom(plan, adjustedReview);
|
||||
|
||||
if (organizationClarificationRequired) {
|
||||
pushReason(reasonCodes, "planner_requires_organization_scope_from_data_need_graph");
|
||||
}
|
||||
|
||||
if (plannerStatus === "ready_for_execution") {
|
||||
pushReason(reasonCodes, "planner_ready_for_guarded_mcp_execution");
|
||||
|
|
@ -467,12 +502,12 @@ export function planAssistantMcpDiscovery(
|
|||
planner_status: plannerStatus,
|
||||
semantic_data_need: semanticDataNeed,
|
||||
data_need_graph: dataNeedGraph,
|
||||
selected_chain_id: recipe.chainId,
|
||||
selected_chain_summary: recipe.chainSummary,
|
||||
proposed_primitives: recipe.primitives,
|
||||
required_axes: recipe.axes,
|
||||
discovery_plan: plan,
|
||||
catalog_review: review,
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
selected_chain_id: recipe.chainId,
|
||||
selected_chain_summary: recipe.chainSummary,
|
||||
proposed_primitives: recipe.primitives,
|
||||
required_axes: recipe.axes,
|
||||
discovery_plan: plan,
|
||||
catalog_review: adjustedReview,
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -214,6 +214,27 @@ function readDiscoveryTurnMeaning(
|
|||
return toRecordObject(turnInput?.turn_meaning_ref);
|
||||
}
|
||||
|
||||
function readDiscoveryDataNeedGraph(
|
||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||
): Record<string, unknown> | null {
|
||||
const turnInput = toRecordObject(entryPoint?.turn_input);
|
||||
return toRecordObject(turnInput?.data_need_graph);
|
||||
}
|
||||
|
||||
function isOpenScopeValueFlowWithoutSubject(
|
||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||
): boolean {
|
||||
const graph = readDiscoveryDataNeedGraph(entryPoint);
|
||||
const businessFactFamily = toNonEmptyString(graph?.business_fact_family);
|
||||
const subjectCandidates = Array.isArray(graph?.subject_candidates) ? graph.subject_candidates : [];
|
||||
const reasonCodes = Array.isArray(graph?.reason_codes) ? graph.reason_codes : [];
|
||||
return (
|
||||
businessFactFamily === "value_flow" &&
|
||||
subjectCandidates.length === 0 &&
|
||||
reasonCodes.some((reason) => toNonEmptyString(reason) === "data_need_graph_open_scope_total_without_subject")
|
||||
);
|
||||
}
|
||||
|
||||
function readTruthAnswerShape(input: ApplyAssistantMcpDiscoveryResponsePolicyInput): Record<string, unknown> | null {
|
||||
const directShape = toRecordObject(input.addressRuntimeMeta?.answer_shape_contract);
|
||||
if (directShape) {
|
||||
|
|
@ -286,6 +307,9 @@ function hasAlignedFactualAddressReply(
|
|||
if (!hasEffectivelyFactualAddressReply(input)) {
|
||||
return false;
|
||||
}
|
||||
if (hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||
return isDetectedIntentAlignedWithTurnMeaning(detectedIntent, readDiscoveryTurnMeaning(entryPoint));
|
||||
}
|
||||
|
|
@ -311,6 +335,12 @@ function hasSemanticConflictWithDiscoveryTurnMeaning(
|
|||
if (!detectedIntent || (!askedDomain && !askedAction && !unsupportedFamily)) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
detectedIntent === "customer_revenue_and_payments" &&
|
||||
isOpenScopeValueFlowWithoutSubject(entryPoint)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return !isDetectedIntentAlignedWithTurnMeaning(detectedIntent, turnMeaning);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -436,6 +436,20 @@ function hasOrganizationScopeSignalUtf8(text: string): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
function extractOrganizationScopeFromRawText(value: unknown): string | null {
|
||||
const text = toNonEmptyString(value);
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
const match = text.match(
|
||||
/(?:^|[\s,;:])(?:\u043f\u043e|for|in|within)?\s*((?:\u041e\u041e\u041e|\u0418\u041f|\u0410\u041e|\u041f\u0410\u041e|\u0417\u0410\u041e|LLC|Inc|LTD|Corp)\s+[^\n,.;:!?]+)/u
|
||||
);
|
||||
if (!match?.[1]) {
|
||||
return null;
|
||||
}
|
||||
return toNonEmptyString(match[1]);
|
||||
}
|
||||
|
||||
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
|
||||
|
|
@ -803,18 +817,35 @@ 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 rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText);
|
||||
const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText ?? rawEffectiveText ?? rawSignalSourceText);
|
||||
const explicitOrganizationScopeSignal = Boolean(
|
||||
rawOrganizationMentionSignal &&
|
||||
(rawOrganizationScope ?? predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope)
|
||||
);
|
||||
const organizationClarificationFollowupApplicable = Boolean(
|
||||
followupSeed.domain === "counterparty_value" &&
|
||||
!followupSeed.counterparty &&
|
||||
rawOrganizationMentionSignal &&
|
||||
(rawOrganizationScope || followupSeed.organization) &&
|
||||
!rawLifecycleSignal &&
|
||||
!rawValueFlowSignal &&
|
||||
!rawMetadataSignal
|
||||
);
|
||||
const rawOpenScopeValueFlowOrganizationSignal = Boolean(
|
||||
rawValueFlowSignal &&
|
||||
!rawBidirectionalValueFlowSignal &&
|
||||
hasOrganizationScopeSignalUtf8(rawText) &&
|
||||
(predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope)
|
||||
explicitOrganizationScopeSignal
|
||||
);
|
||||
const predecomposeOrganizationMirrorsCounterparty = sameScopedName(
|
||||
predecomposeEntities.counterparty,
|
||||
predecomposeEntities.organization
|
||||
);
|
||||
const organizationMirrorsPredecomposeCounterparty = Boolean(
|
||||
(rawBidirectionalValueFlowSignal || hasValueRankingSignal(rawText) || rawOpenScopeValueFlowOrganizationSignal) &&
|
||||
(rawBidirectionalValueFlowSignal ||
|
||||
hasValueRankingSignal(rawText) ||
|
||||
rawOpenScopeValueFlowOrganizationSignal ||
|
||||
explicitOrganizationScopeSignal) &&
|
||||
(sameScopedName(predecomposeEntities.counterparty, assistantTurnMeaningOrganizationScope) ||
|
||||
predecomposeOrganizationMirrorsCounterparty)
|
||||
);
|
||||
|
|
@ -826,7 +857,11 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
followupSeed.domain &&
|
||||
!rawLifecycleSignal &&
|
||||
!rawValueFlowSignal &&
|
||||
(monthlyAggregationSignal || explicitDateScopeLiteralDetected || predecomposeDateScope)
|
||||
(monthlyAggregationSignal ||
|
||||
explicitDateScopeLiteralDetected ||
|
||||
predecomposeDateScope ||
|
||||
explicitOrganizationScopeSignal ||
|
||||
organizationClarificationFollowupApplicable)
|
||||
);
|
||||
const metadataFollowupSeedApplicable = Boolean(
|
||||
followupSeed.domain === "metadata" &&
|
||||
|
|
@ -1114,6 +1149,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
bidirectionalValueFlowSignal ||
|
||||
hasValueRankingSignal(rawText) ||
|
||||
rawOpenScopeValueFlowOrganizationSignal ||
|
||||
explicitOrganizationScopeSignal ||
|
||||
followupSeed.organization
|
||||
);
|
||||
if (openScopeValueFlowWithoutCounterparty && !valueFlowOrganizationStaysScope) {
|
||||
|
|
@ -1122,7 +1158,10 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
}
|
||||
const explicitOrganizationScope =
|
||||
valueFlowOrganizationStaysScope || !openScopeValueFlowWithoutCounterparty
|
||||
? predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope ?? followupSeed.organization
|
||||
? rawOrganizationScope ??
|
||||
predecomposeEntities.organization ??
|
||||
assistantTurnMeaningOrganizationScope ??
|
||||
followupSeed.organization
|
||||
: null;
|
||||
if (valueFlowOrganizationStaysScope && explicitOrganizationScope) {
|
||||
for (let index = entityCandidates.length - 1; index >= 0; index -= 1) {
|
||||
|
|
@ -1258,6 +1297,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
groundedValueFlowFollowupApplicable,
|
||||
forceDiscoveryOverExplicitIntent:
|
||||
Boolean(entityResolutionClarificationCandidate) ||
|
||||
organizationClarificationFollowupApplicable ||
|
||||
metadataAmbiguityLaneClarificationApplicable ||
|
||||
metadataGroundedMovementLaneApplicable ||
|
||||
metadataGroundedDocumentLaneApplicable ||
|
||||
|
|
@ -1308,6 +1348,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
if (entityResolutionClarificationCandidate) {
|
||||
pushReason(reasonCodes, "mcp_discovery_entity_resolution_clarification_candidate_selected");
|
||||
}
|
||||
if (organizationClarificationFollowupApplicable) {
|
||||
pushReason(reasonCodes, "mcp_discovery_organization_clarification_followup_from_followup_context");
|
||||
}
|
||||
if (payoutSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_payout_signal_detected");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -137,4 +137,27 @@ describe("assistant MCP discovery data need graph", () => {
|
|||
]);
|
||||
expect(result.reason_codes).toContain("data_need_graph_open_scope_total_without_subject");
|
||||
});
|
||||
it("treats a generic incoming total as an understood open-scope ask that still needs organization", () => {
|
||||
const result = buildAssistantMcpDiscoveryDataNeedGraph({
|
||||
semanticDataNeed: "counterparty value-flow evidence",
|
||||
rawUtterance: "сколько входящих денег за 2020 год?",
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "turnover",
|
||||
explicit_date_scope: "2020"
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.business_fact_family).toBe("value_flow");
|
||||
expect(result.subject_candidates).toEqual([]);
|
||||
expect(result.clarification_gaps).toEqual(["organization"]);
|
||||
expect(result.proof_expectation).toBe("clarification_required");
|
||||
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");
|
||||
expect(result.reason_codes).toContain("data_need_graph_open_scope_total_needs_organization");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -474,4 +474,42 @@ describe("assistant MCP discovery planner", () => {
|
|||
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");
|
||||
});
|
||||
it("keeps generic one-sided open totals in organization clarification instead of forcing entity resolution", () => {
|
||||
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: "clarification_required",
|
||||
clarification_gaps: ["organization"],
|
||||
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",
|
||||
"data_need_graph_open_scope_total_needs_organization"
|
||||
]
|
||||
},
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "turnover",
|
||||
explicit_date_scope: "2020"
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.planner_status).toBe("needs_clarification");
|
||||
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(["period", "organization", "aggregate_axis", "amount", "coverage_target"]);
|
||||
expect(result.catalog_review.review_status).toBe("needs_more_axes");
|
||||
expect(result.catalog_review.missing_axes_by_primitive.query_movements).toContainEqual(["organization"]);
|
||||
expect(result.reason_codes).toContain("planner_selected_open_scope_value_flow_total_from_data_need_graph");
|
||||
expect(result.selected_chain_id).not.toBe("entity_resolution");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -161,6 +161,52 @@ describe("assistant MCP discovery response policy", () => {
|
|||
expect(result.reason_codes).toContain("mcp_discovery_response_policy_keep_aligned_factual_address_reply");
|
||||
});
|
||||
|
||||
it("does not treat an open-scope value-flow total as aligned with exact top-counterparty carryover", () => {
|
||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply: "\u0421\u0430\u043c\u044b\u0439 \u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0439 \u043a\u043b\u0438\u0435\u043d\u0442: \u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a.",
|
||||
currentReplySource: "address_query_runtime_v1",
|
||||
currentReplyType: "factual",
|
||||
addressRuntimeMeta: {
|
||||
detected_intent: "customer_revenue_and_payments",
|
||||
dialogContinuationContract: {
|
||||
target_intent: "customer_revenue_and_payments"
|
||||
},
|
||||
truth_gate_contract_status: "full_confirmed",
|
||||
assistant_truth_answer_policy_v1: {
|
||||
truth_gate: {
|
||||
coverage_status: "full",
|
||||
grounding_status: "grounded",
|
||||
source_truth_gate_status: "full_confirmed"
|
||||
}
|
||||
},
|
||||
assistant_mcp_discovery_entry_point_v1: entryPoint({
|
||||
turn_input: {
|
||||
adapter_status: "ready",
|
||||
should_run_discovery: true,
|
||||
data_need_graph: {
|
||||
business_fact_family: "value_flow",
|
||||
subject_candidates: [],
|
||||
reason_codes: ["data_need_graph_open_scope_total_without_subject"]
|
||||
},
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "turnover",
|
||||
explicit_organization_scope: "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
|
||||
explicit_date_scope: "2020"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.applied).toBe(true);
|
||||
expect(result.decision).toBe("apply_candidate");
|
||||
expect(result.reason_codes).toContain("mcp_discovery_response_policy_semantic_conflict_allows_candidate_override");
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_aligned_factual_address_reply");
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_factual_address_continuation_target");
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply");
|
||||
});
|
||||
|
||||
it("keeps factual address follow-up replies when they already match the continuation target intent", () => {
|
||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply: "ИП Калинин Н.М. | сумма: 216600 | операций: 2",
|
||||
|
|
|
|||
|
|
@ -316,4 +316,42 @@ describe("assistant MCP discovery runtime bridge", () => {
|
|||
expect(result.answer_draft.confirmed_lines.join("\n")).toContain("5 000");
|
||||
expect(result.answer_draft.confirmed_lines.join("\n")).not.toContain("контрагенту");
|
||||
});
|
||||
it("keeps generic one-sided open totals in organization clarification without asking for 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: "clarification_required",
|
||||
clarification_gaps: ["organization"],
|
||||
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",
|
||||
"data_need_graph_open_scope_total_needs_organization"
|
||||
]
|
||||
},
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "turnover",
|
||||
explicit_date_scope: "2020"
|
||||
},
|
||||
deps: buildDeps([])
|
||||
});
|
||||
|
||||
expect(result.bridge_status).toBe("needs_clarification");
|
||||
expect(result.requires_user_clarification).toBe(true);
|
||||
expect(result.planner.selected_chain_id).toBe("value_flow");
|
||||
expect(result.answer_draft.next_step_line).toContain("\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044e");
|
||||
expect(result.answer_draft.next_step_line).not.toContain(
|
||||
"\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -408,4 +408,104 @@ describe("assistant MCP discovery runtime entry point", () => {
|
|||
expect(result.bridge?.pilot.pilot_scope).toBe("counterparty_supplier_payout_query_movements_v1");
|
||||
expect(result.bridge?.answer_draft.confirmed_lines.join("\n")).toContain("исход");
|
||||
});
|
||||
it("keeps a generic incoming total in organization clarification instead of asking for counterparty", async () => {
|
||||
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
|
||||
userMessage: "сколько входящих денег за 2020 год?",
|
||||
deps: buildDeps([])
|
||||
});
|
||||
|
||||
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_date_scope: "2020"
|
||||
});
|
||||
expect(result.turn_input.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
|
||||
expect(result.turn_input.data_need_graph?.clarification_gaps).toEqual(["organization"]);
|
||||
expect(result.bridge?.planner.selected_chain_id).toBe("value_flow");
|
||||
expect(result.bridge?.bridge_status).toBe("needs_clarification");
|
||||
expect(result.bridge?.answer_draft.next_step_line).toContain("\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044e");
|
||||
expect(result.bridge?.answer_draft.next_step_line).not.toContain(
|
||||
"\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430"
|
||||
);
|
||||
});
|
||||
|
||||
it("resumes a generic incoming total after organization clarification without reintroducing a counterparty", async () => {
|
||||
const orgName = "ООО Альтернатива Плюс";
|
||||
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
|
||||
userMessage: "по ООО Альтернатива Плюс",
|
||||
predecomposeContract: {
|
||||
entities: { organization: orgName }
|
||||
},
|
||||
followupContext: {
|
||||
previous_discovery_pilot_scope: "counterparty_value_flow_query_movements_v1",
|
||||
previous_filters: {
|
||||
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.source_signal).toBe("followup_context");
|
||||
expect(result.turn_input.turn_meaning_ref).toMatchObject({
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "turnover",
|
||||
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_value_flow_query_movements_v1");
|
||||
expect(result.bridge?.pilot.derived_value_flow).toMatchObject({
|
||||
counterparty: null,
|
||||
period_scope: "2020",
|
||||
total_amount: 3500
|
||||
});
|
||||
});
|
||||
it("overrides a supported exact intent when organization-only follow-up resolves an open-scope total", async () => {
|
||||
const orgName = "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
||||
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
|
||||
userMessage: "\u043f\u043e \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
|
||||
assistantTurnMeaning: {
|
||||
asked_domain_family: "counterparty",
|
||||
asked_action_family: "turnover",
|
||||
explicit_intent_candidate: "customer_revenue_and_payments"
|
||||
},
|
||||
followupContext: {
|
||||
previous_discovery_pilot_scope: "counterparty_value_flow_query_movements_v1",
|
||||
previous_filters: {
|
||||
period_from: "2020-01-01",
|
||||
period_to: "2020-12-31"
|
||||
}
|
||||
},
|
||||
deps: buildDeps([
|
||||
{ Period: "2020-01-15T00:00:00", Amount: 2500, Counterparty: "\u041a\u043b\u0438\u0435\u043d\u0442-\u0410" },
|
||||
{ Period: "2020-06-20T00:00:00", Amount: 1000, Counterparty: "\u041a\u043b\u0438\u0435\u043d\u0442-\u0411" }
|
||||
])
|
||||
});
|
||||
|
||||
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: 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_value_flow_query_movements_v1");
|
||||
expect(result.bridge?.pilot.derived_value_flow).toMatchObject({
|
||||
counterparty: null,
|
||||
period_scope: "2020",
|
||||
total_amount: 3500
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1422,4 +1422,93 @@ describe("assistant MCP discovery turn input adapter", () => {
|
|||
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
|
||||
expect(result.data_need_graph?.subject_candidates).toEqual([]);
|
||||
});
|
||||
it("treats a generic incoming total as an open-scope value ask that needs organization rather than a counterparty", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage: "сколько входящих денег за 2020 год?"
|
||||
});
|
||||
|
||||
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_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?.clarification_gaps).toEqual(["organization"]);
|
||||
expect(result.data_need_graph?.reason_codes).toContain("data_need_graph_open_scope_total_without_subject");
|
||||
expect(result.data_need_graph?.reason_codes).toContain("data_need_graph_open_scope_total_needs_organization");
|
||||
});
|
||||
|
||||
it("resumes an open-scope incoming total from follow-up context when the user clarifies only the organization", () => {
|
||||
const orgName = "ООО Альтернатива Плюс";
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage: "по ООО Альтернатива Плюс",
|
||||
predecomposeContract: {
|
||||
entities: { organization: orgName }
|
||||
},
|
||||
followupContext: {
|
||||
previous_discovery_pilot_scope: "counterparty_value_flow_query_movements_v1",
|
||||
previous_filters: {
|
||||
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.source_signal).toBe("followup_context");
|
||||
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: orgName,
|
||||
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.reason_codes).toContain("mcp_discovery_seeded_from_followup_context");
|
||||
expect(result.data_need_graph?.clarification_gaps).toEqual([]);
|
||||
});
|
||||
it("forces discovery over a supported exact intent when organization-only follow-up resolves an open-scope total", () => {
|
||||
const orgName = "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage: "\u043f\u043e \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
|
||||
assistantTurnMeaning: {
|
||||
asked_domain_family: "counterparty",
|
||||
asked_action_family: "turnover",
|
||||
explicit_intent_candidate: "customer_revenue_and_payments"
|
||||
},
|
||||
followupContext: {
|
||||
previous_discovery_pilot_scope: "counterparty_value_flow_query_movements_v1",
|
||||
previous_filters: {
|
||||
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: orgName,
|
||||
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.reason_codes).toContain(
|
||||
"mcp_discovery_organization_clarification_followup_from_followup_context"
|
||||
);
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_not_applicable_for_supported_exact_turn");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue