diff --git a/docs/orchestration/agent_autonomy_business_quality_20260523.json b/docs/orchestration/agent_autonomy_business_quality_20260523.json index e235816..901bdb2 100644 --- a/docs/orchestration/agent_autonomy_business_quality_20260523.json +++ b/docs/orchestration/agent_autonomy_business_quality_20260523.json @@ -3,7 +3,7 @@ "scenario_id": "agent_autonomy_business_quality_20260523", "domain": "autonomy_business_quality", "title": "AGENT | Autonomy business quality pack", - "description": "Expanded targeted AGENT replay for the current autonomy milestone: value-flow hot handoff must survive realistic business questions, while final answers must read like a useful 1C analyst instead of runtime/debug prose.", + "description": "Expanded targeted AGENT replay for the autonomy milestone: value-flow, business overview, debts, VAT, profit/cashflow distinction, nomenclature margin boundary, and final answer quality must survive realistic business questions.", "bindings": {}, "steps": [ { @@ -101,6 +101,19 @@ "criticality": "critical", "semantic_tags": ["followup_context", "profit_vs_cashflow"] }, + { + "step_id": "step_06b_direct_clean_profit", + "title": "Direct clean profit question starts with accounting result, not with denial", + "question": "Какая чистая прибыль по ООО Альтернатива Плюс за 2020?", + "allowed_reply_types": ["partial_coverage", "factual_with_explanation", "factual"], + "expected_catalog_alignment_status": "selected_matches_top", + "expected_catalog_chain_top_match": "business_overview", + "expected_catalog_selected_matches_top": true, + "required_answer_patterns_all": ["2020", "7\\s*136\\s*815|7,?136,?815|7136815", "убыт|минус", "90|91|99"], + "forbidden_answer_patterns": ["Коротко: нет", "денежное операционное нетто не стоит считать чистой прибылью", "Учтено строк", "Первая найденная дата", "runtime_", "planner_", "query_movements", "primitive"], + "criticality": "critical", + "semantic_tags": ["direct_profit", "profit_vs_cashflow", "business_answer_quality"] + }, { "step_id": "step_07_sberbank_financial_flow", "title": "Sberbank role is financial flow, not ordinary customer/supplier", diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js index b5a9336..6594bab 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js @@ -28,6 +28,9 @@ function uniqueStrings(values) { } return result; } +function trimTrailingSentenceDot(value) { + return value.trim().replace(/[.]+$/u, ""); +} function formatNamedChoiceList(values) { return uniqueStrings(values) .slice(0, 6) @@ -837,10 +840,7 @@ function headlineFor(mode, pilot) { return "По данным 1С найдены строки входящих и исходящих денежных движений; нетто можно называть только как расчет по найденным строкам и проверенному периоду."; } if (pilot.derived_value_flow && mode === "confirmed_with_bounded_inference") { - if (pilot.derived_value_flow.value_flow_direction === "outgoing_supplier_payout") { - return "По данным 1С найдены строки исходящих платежей/списаний; сумму можно называть только в рамках проверенного периода и найденных строк."; - } - return "По данным 1С найдены строки входящих денежных поступлений; сумму можно называть только в рамках проверенного периода и найденных строк."; + return derivedValueFlowHeadlineLine(pilot) ?? "По данным 1С найдены денежные строки; сумму можно называть только в рамках проверенного периода и найденных строк."; } const zeroValueFlowHeadline = valueFlowZeroResultHeadline(pilot); if (mode === "checked_sources_only" && zeroValueFlowHeadline) { @@ -1203,6 +1203,20 @@ function derivedRankedValueFlowConfirmedLine(pilot) { : ""; return `${directionLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${roleCaveat}${trail}${limitCaveat}`; } +function derivedValueFlowHeadlineLine(pilot) { + const flow = pilot.derived_value_flow; + 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 totalLabel = flow.value_flow_direction === "outgoing_supplier_payout" + ? "Исходящие платежи/списания" + : "Входящие денежные поступления"; + return `${totalLabel}${counterparty || organization}${period}: ${trimTrailingSentenceDot(flow.total_amount_human_ru)}.`; +} function derivedValueFlowConfirmedLine(pilot) { const flow = pilot.derived_value_flow; if (!flow) { @@ -1221,7 +1235,7 @@ function derivedValueFlowConfirmedLine(pilot) { const limitCaveat = flow.coverage_limited_by_probe_limit ? " Проверка уперлась в лимит строк, поэтому полный период может быть покрыт не полностью." : ""; - return `${totalLabel}${counterparty || organization}${period}: ${flow.total_amount_human_ru}.${limitCaveat} ${caveat}`; + return `${totalLabel}${counterparty || organization}${period}: ${trimTrailingSentenceDot(flow.total_amount_human_ru)}.${limitCaveat} ${caveat}`; } function derivedValueFlowMonthlyLines(pilot) { const flow = pilot.derived_value_flow; diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts index 94bff67..f626c24 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts @@ -56,6 +56,10 @@ function uniqueStrings(values: string[]): string[] { return result; } +function trimTrailingSentenceDot(value: string): string { + return value.trim().replace(/[.]+$/u, ""); +} + function formatNamedChoiceList(values: string[]): string { return uniqueStrings(values) .slice(0, 6) @@ -990,10 +994,7 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD return "По данным 1С найдены строки входящих и исходящих денежных движений; нетто можно называть только как расчет по найденным строкам и проверенному периоду."; } if (pilot.derived_value_flow && mode === "confirmed_with_bounded_inference") { - if (pilot.derived_value_flow.value_flow_direction === "outgoing_supplier_payout") { - return "По данным 1С найдены строки исходящих платежей/списаний; сумму можно называть только в рамках проверенного периода и найденных строк."; - } - return "По данным 1С найдены строки входящих денежных поступлений; сумму можно называть только в рамках проверенного периода и найденных строк."; + return derivedValueFlowHeadlineLine(pilot) ?? "По данным 1С найдены денежные строки; сумму можно называть только в рамках проверенного периода и найденных строк."; } const zeroValueFlowHeadline = valueFlowZeroResultHeadline(pilot); if (mode === "checked_sources_only" && zeroValueFlowHeadline) { @@ -1383,6 +1384,22 @@ function derivedRankedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotEx return `${directionLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${roleCaveat}${trail}${limitCaveat}`; } +function derivedValueFlowHeadlineLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null { + const flow = pilot.derived_value_flow; + 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 totalLabel = + flow.value_flow_direction === "outgoing_supplier_payout" + ? "Исходящие платежи/списания" + : "Входящие денежные поступления"; + return `${totalLabel}${counterparty || organization}${period}: ${trimTrailingSentenceDot(flow.total_amount_human_ru)}.`; +} + function derivedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null { const flow = pilot.derived_value_flow; if (!flow) { @@ -1403,7 +1420,7 @@ function derivedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutio const limitCaveat = flow.coverage_limited_by_probe_limit ? " Проверка уперлась в лимит строк, поэтому полный период может быть покрыт не полностью." : ""; - return `${totalLabel}${counterparty || organization}${period}: ${flow.total_amount_human_ru}.${limitCaveat} ${caveat}`; + return `${totalLabel}${counterparty || organization}${period}: ${trimTrailingSentenceDot(flow.total_amount_human_ru)}.${limitCaveat} ${caveat}`; } function derivedValueFlowMonthlyLines(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] { diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts index 0f95982..eca2089 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts @@ -1401,11 +1401,10 @@ describe("assistant MCP discovery answer adapter", () => { const confirmedText = draft.confirmed_lines.join("\n"); expect(draft.answer_mode).toBe("confirmed_with_bounded_inference"); - expect(draft.headline).toContain("входящих денежных поступлений"); + expect(draft.headline).toContain("Входящие денежные поступления"); + expect(draft.headline).toContain("3 750,50 руб"); expect(confirmedText).toContain("3 750,50 руб."); - expect(confirmedText).toContain("входящих денежных поступлений"); - expect(confirmedText).toContain("2020-01-15"); - expect(confirmedText).toContain("2020-02-20"); + expect(confirmedText).toContain("входящие денежные поступления"); expect(draft.unknown_lines).toContain("Full turnover outside the checked period is not proven by this MCP discovery pilot"); expect(draft.must_not_claim).toContain("Do not claim full all-time turnover unless the checked period and coverage prove it."); expect(draft.limitation_lines.join("\n")).not.toContain("pilot_"); @@ -1433,8 +1432,9 @@ describe("assistant MCP discovery answer adapter", () => { const confirmedText = draft.confirmed_lines.join("\n"); expect(draft.answer_mode).toBe("confirmed_with_bounded_inference"); - expect(draft.headline).toContain("исходящих платежей"); - expect(confirmedText).toContain("исходящих платежей/списаний"); + expect(draft.headline).toContain("Исходящие платежи"); + expect(draft.headline).toContain("5 000,25 руб"); + expect(confirmedText).toContain("исходящие платежи/списания"); expect(confirmedText).toContain("5 000,25 руб."); expect(draft.inference_lines.join("\n")).toContain("supplier-payout total"); expect(draft.unknown_lines).toContain("Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot");