From 473cdc3a9ba975d40ac1bf052bd5e064c1ca57fd Mon Sep 17 00:00:00 2001 From: dctouch Date: Sat, 23 May 2026 13:14:22 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A1=D1=82=D0=B0=D0=B1=D0=B8=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20=D0=B4=D0=B5?= =?UTF-8?q?=D0=BD=D0=B5=D0=B6=D0=BD=D1=8B=D0=B5=20=D0=BE=D1=82=D0=B2=D0=B5?= =?UTF-8?q?=D1=82=D1=8B=20=D0=B8=20=D0=BF=D1=80=D0=B8=D0=B1=D1=8B=D0=BB?= =?UTF-8?q?=D1=8C=201=D0=A1-=D0=B0=D1=81=D1=81=D0=B8=D1=81=D1=82=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...t_cashflow_profit_directness_20260523.json | 266 ++++++++++++++++++ ...stantAddressOrchestrationRuntimeAdapter.js | 8 +- .../assistantMcpDiscoveryDataNeedGraph.js | 21 +- .../assistantMcpDiscoveryResponseCandidate.js | 54 +++- .../assistantMcpDiscoveryTurnInputAdapter.js | 102 ++++++- .../services/assistantTransitionPolicy.js | 27 +- ...stantAddressOrchestrationRuntimeAdapter.ts | 11 +- .../assistantMcpDiscoveryDataNeedGraph.ts | 33 ++- .../assistantMcpDiscoveryResponseCandidate.ts | 79 +++++- .../assistantMcpDiscoveryTurnInputAdapter.ts | 131 ++++++++- .../src/services/assistantTransitionPolicy.ts | 28 +- ...AddressOrchestrationRuntimeAdapter.test.ts | 3 +- ...stantMcpDiscoveryResponseCandidate.test.ts | 70 +++++ ...istantMcpDiscoveryTurnInputAdapter.test.ts | 72 +++++ .../data/autorun_generators/history.json | 62 ++++ ..._20260523101140_gen-ag05231011-cec910.json | 195 +++++++++++++ ..._20260523101140_gen-ag05231011-cec910.json | 49 ++++ 17 files changed, 1184 insertions(+), 27 deletions(-) create mode 100644 docs/orchestration/agent_cashflow_profit_directness_20260523.json create mode 100644 llm_normalizer/data/autorun_generators/saved_sessions/assistant_saved_session_20260523101140_gen-ag05231011-cec910.json create mode 100644 llm_normalizer/data/eval_cases/assistant_autogen_saved_user_sessions_20260523101140_gen-ag05231011-cec910.json diff --git a/docs/orchestration/agent_cashflow_profit_directness_20260523.json b/docs/orchestration/agent_cashflow_profit_directness_20260523.json new file mode 100644 index 0000000..141db56 --- /dev/null +++ b/docs/orchestration/agent_cashflow_profit_directness_20260523.json @@ -0,0 +1,266 @@ +{ + "schema_version": "domain_truth_harness_spec_v1", + "scenario_id": "agent_cashflow_profit_directness_20260523", + "domain": "autonomy_business_answer_contract", + "title": "AGENT | Cashflow vs profit directness pack", + "description": "Targeted AGENT replay for cashflow-vs-profit directness: colloquial earnings questions must produce a compact money answer, while broad overview remains available only when explicitly requested.", + "bindings": { + + }, + "steps": [ + { + "step_id": "step_01_direct_money_earned_explicit_shape", + "title": "Direct money earned question asks for compact cashflow shape", + "question": "Сколько денег Альтернатива заработала за 2020 год? Ответь коротко: получили, заплатили, денежное нетто, это прибыль или нет.", + "allowed_reply_types": [ + "partial_coverage", + "factual_with_explanation", + "factual" + ], + "required_answer_patterns_all": [ + "47[\\s.]*628[\\s.]*853", + "43[\\s.]*763[\\s.]*351", + "3[\\s.]*865[\\s.]*501" + ], + "forbidden_answer_patterns": [ + "Учтено строк", + "Первая найденная дата", + "runtime_", + "planner_", + "query_movements", + "primitive", + "7\\s*136\\s*815|7136815", + "Комитет государственных услуг" + ], + "criticality": "critical", + "semantic_tags": [ + "cashflow_vs_profit", + "direct_answer", + "colloquial_earnings" + ] + }, + { + "step_id": "step_02_colloquial_money_earned", + "title": "Colloquial earned wording stays cashflow, not broad overview", + "question": "скока денег альтернатива заработала за 20 год?", + "allowed_reply_types": [ + "partial_coverage", + "factual_with_explanation", + "factual" + ], + "required_answer_patterns_all": [ + "47[\\s.]*628[\\s.]*853", + "43[\\s.]*763[\\s.]*351", + "3[\\s.]*865[\\s.]*501" + ], + "forbidden_answer_patterns": [ + "Учтено строк", + "Первая найденная дата", + "runtime_", + "planner_", + "query_movements", + "primitive", + "7\\s*136\\s*815|7136815", + "Комитет государственных услуг", + "Что проверить дальше" + ], + "criticality": "critical", + "semantic_tags": [ + "cashflow_vs_profit", + "slang_wording", + "no_overview_substitution" + ] + }, + { + "step_id": "step_03_profit_followup_after_cashflow", + "title": "Clean profit follow-up keeps distinction", + "question": "а это чистая прибыль?", + "allowed_reply_types": [ + "partial_coverage", + "factual_with_explanation", + "factual" + ], + "required_answer_patterns_all": [ + "7\\s*136\\s*815|7136815", + "90|91|99" + ], + "forbidden_answer_patterns": [ + "Учтено строк", + "Первая найденная дата", + "runtime_", + "planner_", + "query_movements", + "primitive" + ], + "criticality": "critical", + "semantic_tags": [ + "profit_vs_cashflow", + "followup_context" + ] + }, + { + "step_id": "step_04_money_plus_or_minus_followup", + "title": "Money plus/minus follow-up returns cashflow net", + "question": "а по деньгам тогда плюс или минус?", + "allowed_reply_types": [ + "partial_coverage", + "factual_with_explanation", + "factual" + ], + "required_answer_patterns_all": [ + "\\+", + "47[\\s.]*628[\\s.]*853", + "43[\\s.]*763[\\s.]*351", + "3[\\s.]*865[\\s.]*501" + ], + "forbidden_answer_patterns": [ + "Учтено строк", + "Первая найденная дата", + "runtime_", + "planner_", + "query_movements", + "primitive", + "7\\s*136\\s*815|7136815" + ], + "criticality": "critical", + "semantic_tags": [ + "cashflow_polarity", + "followup_context", + "no_profit_substitution" + ] + }, + { + "step_id": "step_05_explicit_business_overview_allowed", + "title": "Explicit adult overview may include business structure and next steps", + "question": "Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток.", + "allowed_reply_types": [ + "partial_coverage", + "factual_with_explanation", + "factual" + ], + "required_answer_patterns_all": [ + "47[\\s.]*628[\\s.]*853", + "43[\\s.]*763[\\s.]*351", + "3[\\s.]*865[\\s.]*501", + "12[\\s.]*792[\\s.]*194", + "12[\\s.]*093[\\s.]*465", + "9[\\s.]*612[\\s.]*904" + ], + "forbidden_answer_patterns": [ + "Учтено строк", + "Первая найденная дата", + "runtime_", + "planner_", + "query_movements", + "primitive" + ], + "criticality": "high", + "semantic_tags": [ + "business_overview", + "bank_flow_boundary", + "next_steps" + ] + }, + { + "step_id": "step_06_return_to_short_money_after_overview", + "title": "Short money follow-up after overview keeps 2020 context", + "question": "а если коротко, сколько заработали деньгами без топов?", + "allowed_reply_types": [ + "partial_coverage", + "factual_with_explanation", + "factual" + ], + "required_answer_patterns_all": [ + "2020", + "47[\\s.]*628[\\s.]*853", + "43[\\s.]*763[\\s.]*351", + "3[\\s.]*865[\\s.]*501" + ], + "forbidden_answer_patterns": [ + "Учтено строк", + "Первая найденная дата", + "runtime_", + "planner_", + "query_movements", + "primitive", + "285[\\s.]*819[\\s.]*547", + "147[\\s.]*855[\\s.]*827", + "Комитет государственных услуг", + "Что проверить дальше" + ], + "criticality": "critical", + "semantic_tags": [ + "compact_after_overview", + "temporal_carryover", + "no_all_time_leak" + ] + }, + { + "step_id": "step_07_plain_money_in_out_net", + "title": "Plain money request suppresses broad overview", + "question": "не обзор, просто деньги: пришло, ушло, нетто за 2020", + "allowed_reply_types": [ + "partial_coverage", + "factual_with_explanation", + "factual" + ], + "required_answer_patterns_all": [ + "47[\\s.]*628[\\s.]*853", + "43[\\s.]*763[\\s.]*351", + "3[\\s.]*865[\\s.]*501" + ], + "forbidden_answer_patterns": [ + "Учтено строк", + "Первая найденная дата", + "runtime_", + "planner_", + "query_movements", + "primitive", + "Комитет государственных услуг", + "12[\\s.]*093[\\s.]*465", + "12[\\s.]*792[\\s.]*194", + "Что проверить дальше" + ], + "criticality": "critical", + "semantic_tags": [ + "direct_money_only", + "ranking_suppression" + ] + }, + { + "step_id": "step_08_direct_clean_profit_2020", + "title": "Explicit clean profit goes to accounting result, not cashflow", + "question": "какая чистая прибыль по Альтернативе за 2020?", + "allowed_reply_types": [ + "partial_coverage", + "factual_with_explanation", + "factual" + ], + "required_answer_patterns_all": [ + "7\\s*136\\s*815|7136815", + "90|91|99" + ], + "forbidden_answer_patterns": [ + "Учтено строк", + "Первая найденная дата", + "runtime_", + "planner_", + "query_movements", + "primitive", + "получили\\s+47[\\s.]*628[\\s.]*853", + "денежное\\s+нетто\\s+3[\\s.]*865[\\s.]*501" + ], + "criticality": "critical", + "semantic_tags": [ + "clean_profit", + "profit_route", + "no_cashflow_substitution" + ] + } + ], + "acceptance": { + "min_score": 80, + "max_unresolved_p0": 0, + "require_all_critical_steps_pass": true + } +} \ No newline at end of file diff --git a/llm_normalizer/backend/dist/services/assistantAddressOrchestrationRuntimeAdapter.js b/llm_normalizer/backend/dist/services/assistantAddressOrchestrationRuntimeAdapter.js index fad6768..dbeaa25 100644 --- a/llm_normalizer/backend/dist/services/assistantAddressOrchestrationRuntimeAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantAddressOrchestrationRuntimeAdapter.js @@ -14,6 +14,11 @@ function sessionOrganizationName(sessionOrganizationScope, toNonEmptyString) { const scope = toRecordObject(sessionOrganizationScope); return toNonEmptyString(scope?.selectedOrganization) ?? toNonEmptyString(scope?.activeOrganization); } +function sessionKnownOrganizations(sessionOrganizationScope) { + const scope = toRecordObject(sessionOrganizationScope); + const knownOrganizations = scope?.knownOrganizations; + return Array.isArray(knownOrganizations) ? knownOrganizations : []; +} function predecomposeOrganizationName(predecomposeContract, toNonEmptyString) { const entities = toRecordObject(predecomposeContract?.entities); return (toNonEmptyString(entities?.organization) ?? @@ -223,7 +228,8 @@ async function buildAssistantAddressOrchestrationRuntime(input) { effectiveMessage: addressInputMessage, assistantTurnMeaning: toRecordObject(orchestrationContract?.assistant_turn_meaning), predecomposeContract, - followupContext: discoveryFollowupContext + followupContext: discoveryFollowupContext, + knownOrganizations: sessionKnownOrganizations(input.sessionOrganizationScope ?? null) })); } catch (error) { diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryDataNeedGraph.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryDataNeedGraph.js index 7d8517d..b5c3908 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryDataNeedGraph.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryDataNeedGraph.js @@ -115,6 +115,12 @@ function hasBusinessOverviewDirectMoneyAnswerHint(input) { return true; } const text = input.rawUtterance; + if (/(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)[\s\S]{0,80}(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\p{L}*|\u0443\u0448\p{L}*|\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*)|(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e)[\s\S]{0,80}(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)/iu.test(text)) { + return true; + } + if (/(?:\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}+|\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?\b|\u0434\u0435\u043d\p{L}{0,20}\s+\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\u043b\p{L}*[\s\S]{0,80}\u0443\u0448\u043b\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|\u0432\u0445\u043e\u0434\u044f\u0449\p{L}*[\s\S]{0,80}\u0438\u0441\u0445\u043e\u0434\u044f\u0449\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e)/iu.test(text)) { + return true; + } return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|how\s+much)[\s\S]{0,120}(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u0432\u044b\u0440\u0443\u0447|\u0434\u0435\u043d\p{L}*|\u043f\u043e\u043b\u0443\u0447|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*)|(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u0432\u044b\u0440\u0443\u0447)[\s\S]{0,120}(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|\u0432\u0441\u0435\u0433\u043e|\u0432\u043e\u043e\u0431\u0449\u0435|(?:19|20)\d{2}|all\s+time)|(?:\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0430\u044f|\u043a\u0430\u043a\u0438\u0435|which|what)[\s\S]{0,80}(?:\u0441\u0430\u043c\p{L}*|top|best|most)[\s\S]{0,80}(?:\u0434\u043e\u0445\u043e\u0434\u043d|\u0432\u044b\u0440\u0443\u0447|\u043e\u0431\u043e\u0440\u043e\u0442|revenue|turnover)[\s\S]{0,40}(?:\u0433\u043e\u0434|year)/iu.test(text); } function timeScopeNeedFor(input) { @@ -225,6 +231,13 @@ function rankingNeedFromRawUtterance(value) { } return null; } +function suppressRankingNeedFromRawUtterance(value) { + const text = lower(value); + if (!text) { + return false; + } + return /(?:\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?\b|\u043d\u0435\s+\u0442\u043e\u043f\b|\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440\b|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}+|\u0438\u0441\u043a\u043b\u044e\u0447\w*\s+\u0442\u043e\u043f|\u0431\u0435\u0437\s+\u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0430\b|без\s+топ(?:РѕРІ|Р В°)?\b|Р Р…Р Вµ\s+топ\b|Р Р…Р Вµ\s+РѕР±Р·РѕСЂ\b|просто\s+ден\p{L}+|исключ\S*\s+топ|без\s+рейтинга\b)/iu.test(text); +} function proofExpectationFor(input) { if (input.clarificationGaps.length > 0) { return "clarification_required"; @@ -383,6 +396,7 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) { const action = lower(turnMeaning?.asked_action_family); const unsupported = lower(turnMeaning?.unsupported_but_understood_family); const rawUtterance = lower(input.rawUtterance); + const rawQuestionSignal = lower([input.rawUtterance, turnMeaning?.raw_message, turnMeaning?.effective_message].join(" ")); const aggregationAxis = lower(turnMeaning?.asked_aggregation_axis); const seededRankingNeed = toNonEmptyString(turnMeaning?.seeded_ranking_need); const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope); @@ -400,15 +414,18 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) { }); const aggregationNeed = aggregationNeedFor(aggregationAxis); const comparisonNeed = comparisonNeedFor(action); - const rankingNeed = rankingNeedFromRawUtterance(rawUtterance) ?? seededRankingNeed; const allTimeScopeHint = hasAllTimeScopeHint(rawUtterance); const subjectScopedBidirectionalAllTime = businessFactFamily === "value_flow" && comparisonNeed === "incoming_vs_outgoing" && subjectCandidates.length > 0 && !explicitDateScope; + const suppressRankingNeed = suppressRankingNeedFromRawUtterance(rawQuestionSignal) || + /(?:\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?|\u043d\u0435\s+\u0442\u043e\u043f|\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}+|\u0438\u0441\u043a\u043b\u044e\u0447\w*\s+\u0442\u043e\u043f|\u0431\u0435\u0437\s+\u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0430)/iu.test(rawQuestionSignal); + const rawRankingNeed = rankingNeedFromRawUtterance(rawQuestionSignal); + const rankingNeed = suppressRankingNeed ? null : rawRankingNeed ?? seededRankingNeed; const directBusinessOverviewMoneyAnswerHint = hasBusinessOverviewDirectMoneyAnswerHint({ family: businessFactFamily, - rawUtterance, + rawUtterance: rawQuestionSignal, rankingNeed }); const oneSidedOpenScopeTotalHint = hasOpenScopeOneSidedValueTotalHintUtf8Safe(rawUtterance, action); diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponseCandidate.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponseCandidate.js index de1fe3c..6569e81 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponseCandidate.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponseCandidate.js @@ -34,6 +34,30 @@ function requestsFinancialCounterpartyBoundary(turnMeaning, graph) { return (/(?:банк|сбербанк|финанс|кредит|депозит)/iu.test(text) && /(?:клиент|поставщик|выручк|топ|обычн|роль|поток)/iu.test(text)); } +function requestsCompactCashflowAnswer(turnMeaning, graph) { + const text = normalizeQuestionText([ + turnMeaning?.raw_message, + turnMeaning?.effective_message, + graph?.source_message, + graph?.question + ].join(" ")); + if (!text) { + return false; + } + if (/(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)[\s\S]{0,80}(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\p{L}*|\u0443\u0448\p{L}*|\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*)|(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e)[\s\S]{0,80}(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)/iu.test(text)) { + return true; + } + return /(?:\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}+|\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?\b|\u0434\u0435\u043d\p{L}{0,20}\s+\u043d\u0435\u0442\u0442\u043e|\u043f\u043e\u043b\u0443\u0447\p{L}*[\s\S]{0,80}\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\u043b\p{L}*[\s\S]{0,80}\u0443\u0448\u043b\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*)[\s\S]{0,120}(?:\u0434\u0435\u043d\p{L}*|\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u043f\u043e\u043b\u0443\u0447|\u043f\u0440\u0438\u0448\u043b))/iu.test(text); +} +function requestsCashflowPolarityAnswer(turnMeaning, graph) { + const text = normalizeQuestionText([ + turnMeaning?.raw_message, + turnMeaning?.effective_message, + graph?.source_message, + graph?.question + ].join(" ")); + return /(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)[\s\S]{0,80}(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441)|(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441)[\s\S]{0,80}(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)/iu.test(text); +} function toStringList(value) { if (!Array.isArray(value)) { return []; @@ -595,6 +619,18 @@ function compactComparable(value) { .replace(/\s+/g, " ") .trim(); } +function businessOverviewOrganizationScopeLabel(value) { + const text = toNonEmptyString(value); + if (!text) { + return null; + } + const comparable = compactComparable(text); + if (/^(?:с|без)\s+разбивк/.test(comparable) || + /\b(?:входящ|исходящ|нетто|топ|контрагент|платеж|поступлен)\b/.test(comparable)) { + return null; + } + return text; +} function businessOverviewSeparateSubjectLabel(graph, turnMeaning, organizationScope) { const candidates = uniqueStrings([ ...toStringList(turnMeaning?.business_overview_separate_entity_candidates), @@ -686,7 +722,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) { const netDirection = overview.net_direction === "net_outgoing" ? "операционное нетто в минус" : "расчетное операционное нетто"; const period = businessOverviewPeriodText(overview); const limitLine = businessOverviewCoverageLimitLine(overview); - const organizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope); + const organizationScope = businessOverviewOrganizationScopeLabel(turnMeaning?.explicit_organization_scope); const separateSubject = businessOverviewSeparateSubjectLabel(graph, turnMeaning, organizationScope); const previousCounterpartySummary = buildPreviousCounterpartyValueFlowSummary(toRecordObject(turnMeaning?.previous_counterparty_value_flow_bundle), separateSubject, toRecordObject(turnMeaning?.previous_counterparty_document_bundle)); const organizationPrefix = organizationScope ? `по компании ${organizationScope} ` : ""; @@ -730,6 +766,22 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) { const debtDueDateBoundary = actionFamily === "debt_due_date_boundary" || unsupportedFamily === "debt_due_date_boundary"; const vendorRiskBoundary = actionFamily === "vendor_risk_procurement_boundary" || unsupportedFamily === "vendor_risk_procurement_boundary"; const inventoryReserveBoundary = actionFamily === "inventory_reserve_boundary" || unsupportedFamily === "inventory_reserve_liquidation_boundary"; + const compactCashflowRequested = directMoneyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph); + const cashflowPolarityRequested = compactCashflowRequested && requestsCashflowPolarityAnswer(turnMeaning, graph); + if (compactCashflowRequested && !rankingNeed && (incomingAmount || outgoingAmount || netAmount)) { + const netDisplay = sentenceAmount(netAmount) ?? netAmount ?? "0 \u0440\u0443\u0431."; + const signedNetDisplay = cashflowPolarityRequested && netDisplay && !String(netDisplay).trim().startsWith("-") + ? `+${netDisplay}` + : netDisplay; + const polarityLead = cashflowPolarityRequested + ? `\u041a\u043e\u0440\u043e\u0442\u043a\u043e: \u043f\u043e \u0434\u0435\u043d\u044c\u0433\u0430\u043c \u043f\u043b\u044e\u0441. ` + : "\u041a\u043e\u0440\u043e\u0442\u043a\u043e: "; + lines.push(`${polarityLead}${organizationPrefix}${period} \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438 ${incomingAmount ?? "0 \u0440\u0443\u0431."}; \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438/\u0441\u043f\u0438\u0441\u0430\u043b\u0438 ${outgoingAmount ?? "0 \u0440\u0443\u0431."}; \u0434\u0435\u043d\u0435\u0436\u043d\u043e\u0435 \u043d\u0435\u0442\u0442\u043e ${signedNetDisplay}.`); + lines.push(cashflowPolarityRequested + ? "\u042d\u0442\u043e \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0439 \u043f\u043e\u0442\u043e\u043a \u043f\u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u043c \u0441\u0442\u0440\u043e\u043a\u0430\u043c 1\u0421, \u043d\u0435 \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0441\u043a\u0438\u0439 \u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442." + : "\u042d\u0442\u043e \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0439 \u043f\u043e\u0442\u043e\u043a \u043f\u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u043c \u0441\u0442\u0440\u043e\u043a\u0430\u043c 1\u0421, \u043d\u0435 \u0447\u0438\u0441\u0442\u0430\u044f \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u0438 \u043d\u0435 \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0441\u043a\u0438\u0439 \u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442."); + return joinBusinessReplyLines(lines); + } if (profitMarginBoundary) { const accountingFinancialResult = toRecordObject(overview.accounting_financial_result); if (accountingFinancialResult) { diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js index cdab88a..4216c06 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js @@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION = void 0; exports.buildAssistantMcpDiscoveryTurnInput = buildAssistantMcpDiscoveryTurnInput; const assistantMcpDiscoveryDataNeedGraph_1 = require("./assistantMcpDiscoveryDataNeedGraph"); +const assistantOrganizationMatcher_1 = require("./assistantOrganizationMatcher"); const addressTextRepair_1 = require("./addressTextRepair"); exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION = "assistant_mcp_discovery_turn_input_v1"; function toRecordObject(value) { @@ -734,11 +735,13 @@ function hasBusinessOverviewContinuationSignal(text) { const hasFinalSummaryCue = /(?:\u0447\u0442\u043e\s+\u043c\u044b\s+\u0437\u043d\u0430\u0435\u043c|\u0447\u0442\u043e\s+\u043f\u043e\u043d\u044f\u0442\u043d\u043e|\u0447\u0442\u043e\s+\u043f\u0440\u043e\u0432\u0435\u0440\w*\s+\u0434\u0430\u043b\u044c\u0448\u0435|\u0441\u043b\u0435\u0434\u0443\u044e\u0449\w*\s+\u0448\u0430\u0433|\u0438\u0442\u043e\u0433\w*\s+\u0432\u044b\u0432\u043e\u0434|\u043a\u0430\u043a\u043e\u0439\s+\u0432\u044b\u0432\u043e\u0434|\u0447\u0442\u043e\s+\u0441\s+\u044d\u0442\u0438\u043c\s+\u0434\u0435\u043b\u0430\u0442\u044c|what\s+do\s+we\s+know|what\s+is\s+missing|next\s+step|final\s+summary)/iu.test(normalized); const hasMoneyBreakdownCue = /(?:\u0440\u0430\u0441\u043a\u0440\u043e\p{L}*\s+\u0434\u0435\u043d\p{L}*|\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+\u0432\u0441\u0435\u0433\u043e\s+\u043f\u043e\u043b\u0443\u0447|\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:\u0432\u0441\u0435\u0433\u043e\s+)?\u0437\u0430\u043f\u043b\u0430\u0442|\u0447\u0438\u0441\u0442\p{L}*\s+\u0434\u0435\u043d\u0435\u0436\u043d\p{L}*\s+\u043f\u043e\u0442\u043e\u043a|\u0433\u043b\u0430\u0432\u043d\p{L}*\s+(?:\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a)|top\s+(?:customer|supplier)|cash\s+breakdown)/iu.test(normalized) && /(?:\u043f\u043e\u043b\u0443\u0447|\u0437\u0430\u043f\u043b\u0430\u0442|\u043d\u0435\u0442\u0442\u043e|\u0434\u0435\u043d\p{L}*|\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|received|paid|net|cash|customer|supplier)/iu.test(normalized); + const hasCashflowPolarityCue = /(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)[\s\S]{0,80}(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\p{L}*|\u0443\u0448\p{L}*|\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*)|(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e)[\s\S]{0,80}(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)/iu.test(normalized); return (hasEvidenceContinuationCue || hasAnalystContinuationCue || hasTaxContinuationCue || hasFinalSummaryCue || - hasMoneyBreakdownCue); + hasMoneyBreakdownCue || + hasCashflowPolarityCue); } function hasExplicitVatQuestionSignal(text) { if (!text) { @@ -785,6 +788,9 @@ function hasBusinessOverviewFollowupSeed(followupSeed) { followupSeed.loopSelectedChainId === "business_overview"); } function hasValueFlowSignal(text) { + if (/(?:\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\u043a|\u043e\u043f\u043b\u0430\u0442|\u043f\u043b\u0430\u0442[\u0435\u0451]\u0436|\u0437\u0430\u043f\u043b\u0430\u0442|\u043f\u0435\u0440\u0435\u0447\u0438\u0441\u043b|\u0441\u043f\u0438\u0441\u0430\u043d|\u0440\u0430\u0441\u0445\u043e\u0434|\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u0432\u0445\u043e\u0434\u044f\u0449|\u043f\u043e\u043b\u0443\u0447(?:\u0438\u043b|\u0435\u043d\u043e|\u0435\u043d)|\u043f\u043e\u0441\u0442\u0443\u043f\u0438\u043b|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d|\u0434\u0435\u043d\p{L}*|\u0437\u0430\u0440\u0430\u0431\u043e\u0442|supplier|value[-\s]?flow|turnover|revenue|payment|payout|outflow|cash\s+flow|\bearn(?:ed|ing|ings)?\b)/iu.test(text)) { + return true; + } return /(?:оборот|выручк|оплат|плат[её]ж|заплат|перечисл|списан|расход|исходящ|входящ|получ(?:ил|ено|ен)|поступил|поступлен|денежн[а-яёa-z0-9_-]*\s+поток|(? { + const normalized = normalizeFollowupText(value); + if (!normalized) { + return false; + } + const hasCompactCue = /(?:\u043a\u043e\u0440\u043e\u0442\p{L}*|\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u0431\u0435\u0437\s+\u0442\u043e\u043f\p{L}*)/iu.test(normalized); + const hasCashflowCue = /(?:\u0434\u0435\u043d\p{L}*|\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u043f\u043e\u043b\u0443\u0447|\u0437\u0430\u043f\u043b\u0430\u0442|\u043f\u0440\u0438\u0448\p{L}*|\u0443\u0448\p{L}*|\u043d\u0435\u0442\u0442\u043e)/iu.test(normalized); + return hasCompactCue && hasCashflowCue; + }; + const compactCashflowFollowupSignal = hasShortValueFlowRetargetCue(userMessage) || + hasCompactCashflowFollowupCue(userMessage) || + (deps.toNonEmptyString(alternateMessage) + ? hasShortValueFlowRetargetCue(String(alternateMessage ?? "")) || + hasCompactCashflowFollowupCue(String(alternateMessage ?? "")) + : false); if (assistantTurnMeaning?.stale_replay_forbidden === true && - !hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage)) { + !hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage) && + !compactCashflowFollowupSignal) { return null; } const latestAddressItem = deps.findLastAddressAssistantItem(items); @@ -555,7 +571,8 @@ function createAssistantTransitionPolicy(deps) { const hasValueFlowCarryoverSourceHint = sourceIntentHint === "customer_revenue_and_payments" || sourceDiscoveryPilotScopeHint === "counterparty_value_flow_query_movements_v1" || sourceDiscoveryPilotScopeHint === "counterparty_supplier_payout_query_movements_v1" || - sourceDiscoveryPilotScopeHint === "counterparty_bidirectional_value_flow_query_movements_v1"; + sourceDiscoveryPilotScopeHint === "counterparty_bidirectional_value_flow_query_movements_v1" || + sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1"; const hasBusinessOverviewCarryoverSourceHint = sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1"; const navigationSessionState = (0, assistantContinuityPolicy_1.resolveNavigationSessionContextState)(addressNavigationState, deps.toNonEmptyString, deps.normalizeOrganizationScopeValue); const navigationFocusObjectHint = navigationSessionState.focusObject; @@ -579,9 +596,11 @@ function createAssistantTransitionPolicy(deps) { ? deps.resolveDebtRoleSwapFollowupIntent(String(alternateMessage ?? ""), sourceIntentHint) : null; const debtRoleSwapIntent = debtRoleSwapPrimary ?? debtRoleSwapAlternate ?? null; - const shortValueFlowRetargetPrimary = hasValueFlowCarryoverSourceHint && hasShortValueFlowRetargetCue(userMessage); + const shortValueFlowRetargetPrimary = hasValueFlowCarryoverSourceHint && + (hasShortValueFlowRetargetCue(userMessage) || hasCompactCashflowFollowupCue(userMessage)); const shortValueFlowRetargetAlternate = hasValueFlowCarryoverSourceHint && deps.toNonEmptyString(alternateMessage) - ? hasShortValueFlowRetargetCue(String(alternateMessage ?? "")) + ? hasShortValueFlowRetargetCue(String(alternateMessage ?? "")) || + hasCompactCashflowFollowupCue(String(alternateMessage ?? "")) : false; const businessOverviewBoundaryFollowupPrimary = hasBusinessOverviewCarryoverSourceHint && hasBusinessOverviewBoundaryFollowupCue(userMessage); const businessOverviewBoundaryFollowupAlternate = hasBusinessOverviewCarryoverSourceHint && deps.toNonEmptyString(alternateMessage) diff --git a/llm_normalizer/backend/src/services/assistantAddressOrchestrationRuntimeAdapter.ts b/llm_normalizer/backend/src/services/assistantAddressOrchestrationRuntimeAdapter.ts index 40e3cca..cc090c3 100644 --- a/llm_normalizer/backend/src/services/assistantAddressOrchestrationRuntimeAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantAddressOrchestrationRuntimeAdapter.ts @@ -84,6 +84,14 @@ function sessionOrganizationName( return toNonEmptyString(scope?.selectedOrganization) ?? toNonEmptyString(scope?.activeOrganization); } +function sessionKnownOrganizations( + sessionOrganizationScope: BuildAssistantAddressOrchestrationRuntimeInput["sessionOrganizationScope"] +): unknown[] { + const scope = toRecordObject(sessionOrganizationScope); + const knownOrganizations = scope?.knownOrganizations; + return Array.isArray(knownOrganizations) ? knownOrganizations : []; +} + function predecomposeOrganizationName( predecomposeContract: Record | null, toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"] @@ -410,7 +418,8 @@ export async function buildAssistantAddressOrchestrationRuntime( effectiveMessage: addressInputMessage, assistantTurnMeaning: toRecordObject(orchestrationContract?.assistant_turn_meaning), predecomposeContract, - followupContext: discoveryFollowupContext + followupContext: discoveryFollowupContext, + knownOrganizations: sessionKnownOrganizations(input.sessionOrganizationScope ?? null) })) as Record; } catch (error) { mcpDiscoveryRuntimeEntryPointError = String(error instanceof Error ? error.message : error ?? "unknown_error").slice(0, 280); diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryDataNeedGraph.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryDataNeedGraph.ts index 7b776f6..e6a29aa 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryDataNeedGraph.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryDataNeedGraph.ts @@ -176,6 +176,18 @@ function hasBusinessOverviewDirectMoneyAnswerHint(input: { return true; } const text = input.rawUtterance; + if ( + /(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)[\s\S]{0,80}(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\p{L}*|\u0443\u0448\p{L}*|\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*)|(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e)[\s\S]{0,80}(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)/iu.test( + text + ) + ) { + return true; + } + if ( + /(?:\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}+|\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?\b|\u0434\u0435\u043d\p{L}{0,20}\s+\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\u043b\p{L}*[\s\S]{0,80}\u0443\u0448\u043b\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|\u0432\u0445\u043e\u0434\u044f\u0449\p{L}*[\s\S]{0,80}\u0438\u0441\u0445\u043e\u0434\u044f\u0449\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e)/iu.test(text) + ) { + return true; + } return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|how\s+much)[\s\S]{0,120}(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u0432\u044b\u0440\u0443\u0447|\u0434\u0435\u043d\p{L}*|\u043f\u043e\u043b\u0443\u0447|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*)|(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u0432\u044b\u0440\u0443\u0447)[\s\S]{0,120}(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|\u0432\u0441\u0435\u0433\u043e|\u0432\u043e\u043e\u0431\u0449\u0435|(?:19|20)\d{2}|all\s+time)|(?:\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0430\u044f|\u043a\u0430\u043a\u0438\u0435|which|what)[\s\S]{0,80}(?:\u0441\u0430\u043c\p{L}*|top|best|most)[\s\S]{0,80}(?:\u0434\u043e\u0445\u043e\u0434\u043d|\u0432\u044b\u0440\u0443\u0447|\u043e\u0431\u043e\u0440\u043e\u0442|revenue|turnover)[\s\S]{0,40}(?:\u0433\u043e\u0434|year)/iu.test( text ); @@ -330,6 +342,16 @@ function rankingNeedFromRawUtterance(value: string): string | null { return null; } +function suppressRankingNeedFromRawUtterance(value: string): boolean { + const text = lower(value); + if (!text) { + return false; + } + return /(?:\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?\b|\u043d\u0435\s+\u0442\u043e\u043f\b|\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440\b|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}+|\u0438\u0441\u043a\u043b\u044e\u0447\w*\s+\u0442\u043e\u043f|\u0431\u0435\u0437\s+\u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0430\b|без\s+топ(?:РѕРІ|Р В°)?\b|Р Р…Р Вµ\s+топ\b|Р Р…Р Вµ\s+РѕР±Р·РѕСЂ\b|просто\s+ден\p{L}+|исключ\S*\s+топ|без\s+рейтинга\b)/iu.test( + text + ); +} + function proofExpectationFor(input: { family: string | null; clarificationGaps: string[]; @@ -503,6 +525,7 @@ export function buildAssistantMcpDiscoveryDataNeedGraph( const action = lower(turnMeaning?.asked_action_family); const unsupported = lower(turnMeaning?.unsupported_but_understood_family); const rawUtterance = lower(input.rawUtterance); + const rawQuestionSignal = lower([input.rawUtterance, turnMeaning?.raw_message, turnMeaning?.effective_message].join(" ")); const aggregationAxis = lower(turnMeaning?.asked_aggregation_axis); const seededRankingNeed = toNonEmptyString(turnMeaning?.seeded_ranking_need); const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope); @@ -520,16 +543,22 @@ export function buildAssistantMcpDiscoveryDataNeedGraph( }); const aggregationNeed = aggregationNeedFor(aggregationAxis); const comparisonNeed = comparisonNeedFor(action); - const rankingNeed = rankingNeedFromRawUtterance(rawUtterance) ?? seededRankingNeed; const allTimeScopeHint = hasAllTimeScopeHint(rawUtterance); const subjectScopedBidirectionalAllTime = businessFactFamily === "value_flow" && comparisonNeed === "incoming_vs_outgoing" && subjectCandidates.length > 0 && !explicitDateScope; + const suppressRankingNeed = + suppressRankingNeedFromRawUtterance(rawQuestionSignal) || + /(?:\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?|\u043d\u0435\s+\u0442\u043e\u043f|\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}+|\u0438\u0441\u043a\u043b\u044e\u0447\w*\s+\u0442\u043e\u043f|\u0431\u0435\u0437\s+\u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0430)/iu.test( + rawQuestionSignal + ); + const rawRankingNeed = rankingNeedFromRawUtterance(rawQuestionSignal); + const rankingNeed = suppressRankingNeed ? null : rawRankingNeed ?? seededRankingNeed; const directBusinessOverviewMoneyAnswerHint = hasBusinessOverviewDirectMoneyAnswerHint({ family: businessFactFamily, - rawUtterance, + rawUtterance: rawQuestionSignal, rankingNeed }); const oneSidedOpenScopeTotalHint = hasOpenScopeOneSidedValueTotalHintUtf8Safe(rawUtterance, action); diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts index f8d67da..510f754 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts @@ -60,6 +60,46 @@ function requestsFinancialCounterpartyBoundary(turnMeaning: Record | null, + graph: Record | null +): boolean { + const text = normalizeQuestionText([ + turnMeaning?.raw_message, + turnMeaning?.effective_message, + graph?.source_message, + graph?.question + ].join(" ")); + if (!text) { + return false; + } + if ( + /(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)[\s\S]{0,80}(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\p{L}*|\u0443\u0448\p{L}*|\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*)|(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e)[\s\S]{0,80}(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)/iu.test( + text + ) + ) { + return true; + } + return /(?:\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}+|\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?\b|\u0434\u0435\u043d\p{L}{0,20}\s+\u043d\u0435\u0442\u0442\u043e|\u043f\u043e\u043b\u0443\u0447\p{L}*[\s\S]{0,80}\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\u043b\p{L}*[\s\S]{0,80}\u0443\u0448\u043b\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*)[\s\S]{0,120}(?:\u0434\u0435\u043d\p{L}*|\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u043f\u043e\u043b\u0443\u0447|\u043f\u0440\u0438\u0448\u043b))/iu.test( + text + ); +} + +function requestsCashflowPolarityAnswer( + turnMeaning: Record | null, + graph: Record | null +): boolean { + const text = normalizeQuestionText([ + turnMeaning?.raw_message, + turnMeaning?.effective_message, + graph?.source_message, + graph?.question + ].join(" ")); + return /(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)[\s\S]{0,80}(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441)|(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441)[\s\S]{0,80}(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)/iu.test( + text + ); +} + function toStringList(value: unknown): string[] { if (!Array.isArray(value)) { return []; @@ -689,6 +729,21 @@ function compactComparable(value: string | null): string { .trim(); } +function businessOverviewOrganizationScopeLabel(value: unknown): string | null { + const text = toNonEmptyString(value); + if (!text) { + return null; + } + const comparable = compactComparable(text); + if ( + /^(?:с|без)\s+разбивк/.test(comparable) || + /\b(?:входящ|исходящ|нетто|топ|контрагент|платеж|поступлен)\b/.test(comparable) + ) { + return null; + } + return text; +} + function businessOverviewSeparateSubjectLabel( graph: Record | null, turnMeaning: Record | null, @@ -802,7 +857,7 @@ function buildCompactBusinessOverviewReply( const netDirection = overview.net_direction === "net_outgoing" ? "операционное нетто в минус" : "расчетное операционное нетто"; const period = businessOverviewPeriodText(overview); const limitLine = businessOverviewCoverageLimitLine(overview); - const organizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope); + const organizationScope = businessOverviewOrganizationScopeLabel(turnMeaning?.explicit_organization_scope); const separateSubject = businessOverviewSeparateSubjectLabel(graph, turnMeaning, organizationScope); const previousCounterpartySummary = buildPreviousCounterpartyValueFlowSummary( toRecordObject(turnMeaning?.previous_counterparty_value_flow_bundle), @@ -857,6 +912,28 @@ function buildCompactBusinessOverviewReply( actionFamily === "vendor_risk_procurement_boundary" || unsupportedFamily === "vendor_risk_procurement_boundary"; const inventoryReserveBoundary = actionFamily === "inventory_reserve_boundary" || unsupportedFamily === "inventory_reserve_liquidation_boundary"; + const compactCashflowRequested = directMoneyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph); + const cashflowPolarityRequested = compactCashflowRequested && requestsCashflowPolarityAnswer(turnMeaning, graph); + + if (compactCashflowRequested && !rankingNeed && (incomingAmount || outgoingAmount || netAmount)) { + const netDisplay = sentenceAmount(netAmount) ?? netAmount ?? "0 \u0440\u0443\u0431."; + const signedNetDisplay = + cashflowPolarityRequested && netDisplay && !String(netDisplay).trim().startsWith("-") + ? `+${netDisplay}` + : netDisplay; + const polarityLead = cashflowPolarityRequested + ? `\u041a\u043e\u0440\u043e\u0442\u043a\u043e: \u043f\u043e \u0434\u0435\u043d\u044c\u0433\u0430\u043c \u043f\u043b\u044e\u0441. ` + : "\u041a\u043e\u0440\u043e\u0442\u043a\u043e: "; + lines.push( + `${polarityLead}${organizationPrefix}${period} \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438 ${incomingAmount ?? "0 \u0440\u0443\u0431."}; \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438/\u0441\u043f\u0438\u0441\u0430\u043b\u0438 ${outgoingAmount ?? "0 \u0440\u0443\u0431."}; \u0434\u0435\u043d\u0435\u0436\u043d\u043e\u0435 \u043d\u0435\u0442\u0442\u043e ${signedNetDisplay}.` + ); + lines.push( + cashflowPolarityRequested + ? "\u042d\u0442\u043e \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0439 \u043f\u043e\u0442\u043e\u043a \u043f\u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u043c \u0441\u0442\u0440\u043e\u043a\u0430\u043c 1\u0421, \u043d\u0435 \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0441\u043a\u0438\u0439 \u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442." + : "\u042d\u0442\u043e \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0439 \u043f\u043e\u0442\u043e\u043a \u043f\u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u043c \u0441\u0442\u0440\u043e\u043a\u0430\u043c 1\u0421, \u043d\u0435 \u0447\u0438\u0441\u0442\u0430\u044f \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u0438 \u043d\u0435 \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0441\u043a\u0438\u0439 \u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442." + ); + return joinBusinessReplyLines(lines); + } if (profitMarginBoundary) { const accountingFinancialResult = toRecordObject(overview.accounting_financial_result); diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts index 39fc051..d09aec2 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts @@ -3,6 +3,7 @@ import { buildAssistantMcpDiscoveryDataNeedGraph, type AssistantMcpDiscoveryDataNeedGraphContract } from "./assistantMcpDiscoveryDataNeedGraph"; +import { resolveOrganizationSelectionFromMessage } from "./assistantOrganizationMatcher"; import { normalizeRussianComparableText, repairAddressMojibakeText } from "./addressTextRepair"; import type { AssistantMcpDiscoveryMetadataRecommendedPrimitive, @@ -27,6 +28,7 @@ export interface BuildAssistantMcpDiscoveryTurnInputAdapterInput { followupContext?: Record | null; userMessage?: string | null; effectiveMessage?: string | null; + knownOrganizations?: unknown[] | null; } export interface AssistantMcpDiscoveryTurnInputContract { @@ -1044,12 +1046,17 @@ function hasBusinessOverviewContinuationSignal(text: string): boolean { /(?:\u043f\u043e\u043b\u0443\u0447|\u0437\u0430\u043f\u043b\u0430\u0442|\u043d\u0435\u0442\u0442\u043e|\u0434\u0435\u043d\p{L}*|\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|received|paid|net|cash|customer|supplier)/iu.test( normalized ); + const hasCashflowPolarityCue = + /(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)[\s\S]{0,80}(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\p{L}*|\u0443\u0448\p{L}*|\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*)|(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e)[\s\S]{0,80}(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)/iu.test( + normalized + ); return ( hasEvidenceContinuationCue || hasAnalystContinuationCue || hasTaxContinuationCue || hasFinalSummaryCue || - hasMoneyBreakdownCue + hasMoneyBreakdownCue || + hasCashflowPolarityCue ); } @@ -1114,6 +1121,13 @@ function hasBusinessOverviewFollowupSeed(followupSeed: ReturnType { + const normalized = normalizeFollowupText(value); + if (!normalized) { + return false; + } + const hasCompactCue = /(?:\u043a\u043e\u0440\u043e\u0442\p{L}*|\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u0431\u0435\u0437\s+\u0442\u043e\u043f\p{L}*)/iu.test(normalized); + const hasCashflowCue = /(?:\u0434\u0435\u043d\p{L}*|\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u043f\u043e\u043b\u0443\u0447|\u0437\u0430\u043f\u043b\u0430\u0442|\u043f\u0440\u0438\u0448\p{L}*|\u0443\u0448\p{L}*|\u043d\u0435\u0442\u0442\u043e)/iu.test(normalized); + return hasCompactCue && hasCashflowCue; + }; + const compactCashflowFollowupSignal = + hasShortValueFlowRetargetCue(userMessage) || + hasCompactCashflowFollowupCue(userMessage) || + (deps.toNonEmptyString(alternateMessage) + ? hasShortValueFlowRetargetCue(String(alternateMessage ?? "")) || + hasCompactCashflowFollowupCue(String(alternateMessage ?? "")) + : false); if ( assistantTurnMeaning?.stale_replay_forbidden === true && - !hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage) + !hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage) && + !compactCashflowFollowupSignal ) { return null; } @@ -763,7 +780,8 @@ export function createAssistantTransitionPolicy(deps) { sourceIntentHint === "customer_revenue_and_payments" || sourceDiscoveryPilotScopeHint === "counterparty_value_flow_query_movements_v1" || sourceDiscoveryPilotScopeHint === "counterparty_supplier_payout_query_movements_v1" || - sourceDiscoveryPilotScopeHint === "counterparty_bidirectional_value_flow_query_movements_v1"; + sourceDiscoveryPilotScopeHint === "counterparty_bidirectional_value_flow_query_movements_v1" || + sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1"; const hasBusinessOverviewCarryoverSourceHint = sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1"; const navigationSessionState = resolveNavigationSessionContextState( @@ -807,10 +825,12 @@ export function createAssistantTransitionPolicy(deps) { : null; const debtRoleSwapIntent = debtRoleSwapPrimary ?? debtRoleSwapAlternate ?? null; const shortValueFlowRetargetPrimary = - hasValueFlowCarryoverSourceHint && hasShortValueFlowRetargetCue(userMessage); + hasValueFlowCarryoverSourceHint && + (hasShortValueFlowRetargetCue(userMessage) || hasCompactCashflowFollowupCue(userMessage)); const shortValueFlowRetargetAlternate = hasValueFlowCarryoverSourceHint && deps.toNonEmptyString(alternateMessage) - ? hasShortValueFlowRetargetCue(String(alternateMessage ?? "")) + ? hasShortValueFlowRetargetCue(String(alternateMessage ?? "")) || + hasCompactCashflowFollowupCue(String(alternateMessage ?? "")) : false; const businessOverviewBoundaryFollowupPrimary = hasBusinessOverviewCarryoverSourceHint && hasBusinessOverviewBoundaryFollowupCue(userMessage); diff --git a/llm_normalizer/backend/tests/assistantAddressOrchestrationRuntimeAdapter.test.ts b/llm_normalizer/backend/tests/assistantAddressOrchestrationRuntimeAdapter.test.ts index 973576a..26b8e5d 100644 --- a/llm_normalizer/backend/tests/assistantAddressOrchestrationRuntimeAdapter.test.ts +++ b/llm_normalizer/backend/tests/assistantAddressOrchestrationRuntimeAdapter.test.ts @@ -218,7 +218,8 @@ describe("assistant address orchestration runtime adapter", () => { root_filters: expect.objectContaining({ organization: "Org A" }) - }) + }), + knownOrganizations: ["Org A"] }) ); }); diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryResponseCandidate.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryResponseCandidate.test.ts index 4be5bf8..0de26c3 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryResponseCandidate.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryResponseCandidate.test.ts @@ -578,6 +578,76 @@ describe("assistant MCP discovery response candidate", () => { expect(candidate.reply_text).not.toContain("Складской срез"); }); + it("lets compact cashflow wording override profit-boundary and overview prose", () => { + const candidate = buildAssistantMcpDiscoveryResponseCandidate( + entryPoint({ + turn_input: { + adapter_status: "ready", + turn_meaning_ref: { + raw_message: + "\u0421\u043a\u043e\u043b\u044c\u043a\u043e \u0434\u0435\u043d\u0435\u0433 \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0430 \u0437\u0430 2020 \u0433\u043e\u0434? \u041e\u0442\u0432\u0435\u0442\u044c \u043a\u043e\u0440\u043e\u0442\u043a\u043e: \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438, \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438, \u0434\u0435\u043d\u0435\u0436\u043d\u043e\u0435 \u043d\u0435\u0442\u0442\u043e, \u044d\u0442\u043e \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u0438\u043b\u0438 \u043d\u0435\u0442.", + asked_action_family: "profit_margin_boundary", + unsupported_but_understood_family: "profit_margin_boundary", + 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" + }, + data_need_graph: { + business_fact_family: "business_overview", + ranking_need: null, + reason_codes: [ + "data_need_graph_family_business_overview", + "data_need_graph_business_overview_direct_money_answer" + ] + } + }, + bridge: { + bridge_status: "answer_draft_ready", + user_facing_response_allowed: true, + business_fact_answer_allowed: true, + requires_user_clarification: false, + pilot: { + pilot_scope: "business_overview_route_template_v1", + derived_business_overview: { + period_scope: "2020", + incoming_customer_revenue: { + total_amount_human_ru: "47 628 853,03 руб." + }, + outgoing_supplier_payout: { + total_amount_human_ru: "43 763 351,53 руб." + }, + net_amount_human_ru: "3 865 501,50 руб.", + net_direction: "net_incoming", + top_customers: [{ axis_value: "СБЕРБАНК", total_amount_human_ru: "12 792 194,31 руб." }], + top_suppliers: [], + accounting_financial_result: { + final_result_direction: "loss", + final_result_amount_human_ru: "7 136 815,85 руб.", + period_scope: "2020" + } + } + }, + answer_draft: { + answer_mode: "confirmed_with_bounded_inference", + headline: "wide overview should not leak", + confirmed_lines: [], + inference_lines: [], + unknown_lines: [], + limitation_lines: [], + next_step_line: null + } + } + }) + ); + + expect(candidate.reply_text).toContain("47 628 853,03"); + expect(candidate.reply_text).toContain("43 763 351,53"); + expect(candidate.reply_text).toContain("3 865 501,50"); + expect(candidate.reply_text).not.toContain("СБЕРБАНК"); + expect(candidate.reply_text).not.toContain("7 136 815,85"); + expect(candidate.reply_text).not.toContain("wide overview"); + }); + it("labels organization-scoped bidirectional value-flow continuations as company scope", () => { const candidate = buildAssistantMcpDiscoveryResponseCandidate( entryPoint({ diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts index 7b0f4fc..ca3a905 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts @@ -1789,6 +1789,56 @@ describe("assistant MCP discovery turn input adapter", () => { expect(result.reason_codes).not.toContain("mcp_discovery_counterparty_from_predecompose"); }); + it("grounds organization aliases from known organizations for clean-session earnings questions", () => { + const result = buildAssistantMcpDiscoveryTurnInput({ + userMessage: "скока денег альтернатива заработала за 20 год?", + effectiveMessage: "скока денег альтернатива заработала за 20 год?", + knownOrganizations: ["ООО Альтернатива Плюс", "ООО Лайсвуд", "РАЙМ"], + predecomposeContract: { + period: { + period_from: "2020-01-01", + period_to: "2020-12-31", + has_explicit_period: true + } + } + }); + + expect(result.adapter_status).toBe("ready"); + expect(result.should_run_discovery).toBe(true); + expect(result.data_need_graph?.business_fact_family).toBe("business_overview"); + expect(result.turn_meaning_ref?.explicit_organization_scope).toBe("ООО Альтернатива Плюс"); + expect(result.turn_meaning_ref?.explicit_date_scope).toBe("2020"); + expect(result.reason_codes).toContain("mcp_discovery_organization_scope_from_known_organizations"); + expect(result.data_need_graph?.clarification_gaps ?? []).not.toContain("organization"); + }); + + it("treats a business-overview company alias misread as counterparty as organization scope", () => { + const result = buildAssistantMcpDiscoveryTurnInput({ + userMessage: "Сколько денег Альтернатива заработала за 2020 год?", + effectiveMessage: "Определить финансовый результат компании Альтернатива за 2020 год", + assistantTurnMeaning: { + asked_domain_family: "business_summary", + asked_action_family: "broad_evaluation", + unsupported_but_understood_family: "broad_business_evaluation" + }, + predecomposeContract: { + entities: { counterparty: "Альтернатива", organization: null }, + period: { + period_from: "2020-01-01", + period_to: "2020-12-31", + has_explicit_period: true + } + } + }); + + expect(result.adapter_status).toBe("ready"); + expect(result.data_need_graph?.business_fact_family).toBe("business_overview"); + expect(result.turn_meaning_ref?.explicit_organization_scope).toBe("Альтернатива"); + expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined(); + expect(result.data_need_graph?.clarification_gaps ?? []).not.toContain("organization"); + expect(result.reason_codes).toContain("mcp_discovery_counterparty_reinterpreted_as_organization_scope"); + }); + it("keeps all-time business overview from reusing a negated VAT period as active scope", () => { const result = buildAssistantMcpDiscoveryTurnInput({ userMessage: @@ -2308,6 +2358,28 @@ describe("assistant MCP discovery turn input adapter", () => { expect(result.data_need_graph?.reason_codes).toContain("data_need_graph_business_overview_direct_money_answer"); }); + it("does not inherit ranking when a cashflow follow-up explicitly says no overview or tops", () => { + 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: + "\u043d\u0435 \u043e\u0431\u0437\u043e\u0440, \u043f\u0440\u043e\u0441\u0442\u043e \u0434\u0435\u043d\u044c\u0433\u0438: \u043f\u0440\u0438\u0448\u043b\u043e, \u0443\u0448\u043b\u043e, \u043d\u0435\u0442\u0442\u043e \u0437\u0430 2020 \u0431\u0435\u0437 \u0442\u043e\u043f\u043e\u0432", + followupContext: { + previous_discovery_pilot_scope: "business_overview_route_template_v1", + previous_filters: { + organization: orgName + }, + previous_seeded_ranking_need: "top_desc" + } + }); + + expect(result.adapter_status).toBe("ready"); + expect(result.should_run_discovery).toBe(true); + expect(result.data_need_graph?.business_fact_family).toBe("business_overview"); + expect(result.data_need_graph?.ranking_need).toBeNull(); + expect(result.turn_meaning_ref?.explicit_date_scope).toBe("2020"); + expect(result.data_need_graph?.reason_codes).toContain("data_need_graph_business_overview_direct_money_answer"); + }); + it("routes organization-level profit and margin wording to business overview instead of exact value recipes", () => { const orgName = "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"; const result = buildAssistantMcpDiscoveryTurnInput({ diff --git a/llm_normalizer/data/autorun_generators/history.json b/llm_normalizer/data/autorun_generators/history.json index 01ad428..bc43647 100644 --- a/llm_normalizer/data/autorun_generators/history.json +++ b/llm_normalizer/data/autorun_generators/history.json @@ -1,4 +1,66 @@ [ + { + "generation_id": "gen-ag05231011-cec910", + "created_at": "2026-05-23T10:11:40+00:00", + "mode": "saved_user_sessions", + "title": "AGENT | Cashflow vs profit directness pack", + "count": 8, + "domain": "autonomy_business_answer_contract", + "questions": [ + "Сколько денег Альтернатива заработала за 2020 год? Ответь коротко: получили, заплатили, денежное нетто, это прибыль или нет.", + "скока денег альтернатива заработала за 20 год?", + "а это чистая прибыль?", + "а по деньгам тогда плюс или минус?", + "Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток.", + "а если коротко, сколько заработали деньгами без топов?", + "не обзор, просто деньги: пришло, ушло, нетто за 2020", + "какая чистая прибыль по Альтернативе за 2020?" + ], + "generated_by": "codex_agent", + "saved_case_set_file": "assistant_autogen_saved_user_sessions_20260523101140_gen-ag05231011-cec910.json", + "context": { + "llm_provider": null, + "model": null, + "assistant_prompt_version": null, + "decomposition_prompt_version": null, + "prompt_fingerprint": null, + "autogen_personality_id": null, + "autogen_personality_prompt": null, + "source_session_id": null, + "saved_session_file": "assistant_saved_session_20260523101140_gen-ag05231011-cec910.json", + "saved_case_set_kind": "agent_semantic_scenario", + "agent_run": true, + "agent_focus": "Targeted AGENT replay for cashflow-vs-profit directness: colloquial earnings questions must produce a compact money answer, while broad overview remains available only when explicitly requested.", + "architecture_phase": "turnaround_11", + "source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\agent_cashflow_profit_directness_20260523.json", + "scenario_id": "agent_cashflow_profit_directness_20260523", + "semantic_tags": [ + "bank_flow_boundary", + "business_overview", + "cashflow_polarity", + "cashflow_vs_profit", + "clean_profit", + "colloquial_earnings", + "compact_after_overview", + "direct_answer", + "direct_money_only", + "followup_context", + "next_steps", + "no_all_time_leak", + "no_cashflow_substitution", + "no_overview_substitution", + "no_profit_substitution", + "profit_route", + "profit_vs_cashflow", + "ranking_suppression", + "slang_wording", + "temporal_carryover" + ], + "validation_status": "accepted_live_replay", + "validated_run_dir": "artifacts\\domain_runs\\agent_cashflow_profit_directness_live11", + "saved_after_validated_replay": true + } + }, { "generation_id": "gen-ag05230604-098bda", "created_at": "2026-05-23T06:04:40+00:00", diff --git a/llm_normalizer/data/autorun_generators/saved_sessions/assistant_saved_session_20260523101140_gen-ag05231011-cec910.json b/llm_normalizer/data/autorun_generators/saved_sessions/assistant_saved_session_20260523101140_gen-ag05231011-cec910.json new file mode 100644 index 0000000..2c3d87d --- /dev/null +++ b/llm_normalizer/data/autorun_generators/saved_sessions/assistant_saved_session_20260523101140_gen-ag05231011-cec910.json @@ -0,0 +1,195 @@ +{ + "saved_at": "2026-05-23T10:11:40+00:00", + "generation_id": "gen-ag05231011-cec910", + "mode": "saved_user_sessions", + "title": "AGENT | Cashflow vs profit directness pack", + "agent_run": true, + "questions": [ + "Сколько денег Альтернатива заработала за 2020 год? Ответь коротко: получили, заплатили, денежное нетто, это прибыль или нет.", + "скока денег альтернатива заработала за 20 год?", + "а это чистая прибыль?", + "а по деньгам тогда плюс или минус?", + "Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток.", + "а если коротко, сколько заработали деньгами без топов?", + "не обзор, просто деньги: пришло, ушло, нетто за 2020", + "какая чистая прибыль по Альтернативе за 2020?" + ], + "metadata": { + "assistant_prompt_version": null, + "decomposition_prompt_version": null, + "prompt_fingerprint": null, + "agent_focus": "Targeted AGENT replay for cashflow-vs-profit directness: colloquial earnings questions must produce a compact money answer, while broad overview remains available only when explicitly requested.", + "architecture_phase": "turnaround_11", + "source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\agent_cashflow_profit_directness_20260523.json", + "scenario_id": "agent_cashflow_profit_directness_20260523", + "semantic_tags": [ + "bank_flow_boundary", + "business_overview", + "cashflow_polarity", + "cashflow_vs_profit", + "clean_profit", + "colloquial_earnings", + "compact_after_overview", + "direct_answer", + "direct_money_only", + "followup_context", + "next_steps", + "no_all_time_leak", + "no_cashflow_substitution", + "no_overview_substitution", + "no_profit_substitution", + "profit_route", + "profit_vs_cashflow", + "ranking_suppression", + "slang_wording", + "temporal_carryover" + ], + "validation_status": "accepted_live_replay", + "validated_run_dir": "artifacts\\domain_runs\\agent_cashflow_profit_directness_live11", + "saved_after_validated_replay": true, + "save_gate": { + "schema_version": "agent_semantic_save_gate_v1", + "validation_status": "accepted_live_replay", + "validated_run_dir": "artifacts\\domain_runs\\agent_cashflow_profit_directness_live11", + "final_status": "accepted", + "review_overall_status": "pass", + "business_overall_status": "pass", + "steps_total": 8, + "steps_passed": 8, + "steps_failed": 0, + "steps_with_business_failures": 0, + "steps_with_business_warnings": 0, + "acceptance_gate_passed": true, + "saved_after_validated_replay": true + } + }, + "source_session_id": null, + "session": { + "session_id": null, + "mode": "agent_semantic_run", + "items": [ + { + "message_id": "agent-user-001", + "role": "user", + "text": "Сколько денег Альтернатива заработала за 2020 год? Ответь коротко: получили, заплатили, денежное нетто, это прибыль или нет.", + "created_at": "2026-05-23T10:11:40+00:00", + "reply_type": null, + "trace_id": null, + "debug": null + }, + { + "message_id": "agent-user-002", + "role": "user", + "text": "скока денег альтернатива заработала за 20 год?", + "created_at": "2026-05-23T10:11:40+00:00", + "reply_type": null, + "trace_id": null, + "debug": null + }, + { + "message_id": "agent-user-003", + "role": "user", + "text": "а это чистая прибыль?", + "created_at": "2026-05-23T10:11:40+00:00", + "reply_type": null, + "trace_id": null, + "debug": null + }, + { + "message_id": "agent-user-004", + "role": "user", + "text": "а по деньгам тогда плюс или минус?", + "created_at": "2026-05-23T10:11:40+00:00", + "reply_type": null, + "trace_id": null, + "debug": null + }, + { + "message_id": "agent-user-005", + "role": "user", + "text": "Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток.", + "created_at": "2026-05-23T10:11:40+00:00", + "reply_type": null, + "trace_id": null, + "debug": null + }, + { + "message_id": "agent-user-006", + "role": "user", + "text": "а если коротко, сколько заработали деньгами без топов?", + "created_at": "2026-05-23T10:11:40+00:00", + "reply_type": null, + "trace_id": null, + "debug": null + }, + { + "message_id": "agent-user-007", + "role": "user", + "text": "не обзор, просто деньги: пришло, ушло, нетто за 2020", + "created_at": "2026-05-23T10:11:40+00:00", + "reply_type": null, + "trace_id": null, + "debug": null + }, + { + "message_id": "agent-user-008", + "role": "user", + "text": "какая чистая прибыль по Альтернативе за 2020?", + "created_at": "2026-05-23T10:11:40+00:00", + "reply_type": null, + "trace_id": null, + "debug": null + } + ], + "agent_run": true, + "metadata": { + "assistant_prompt_version": null, + "decomposition_prompt_version": null, + "prompt_fingerprint": null, + "agent_focus": "Targeted AGENT replay for cashflow-vs-profit directness: colloquial earnings questions must produce a compact money answer, while broad overview remains available only when explicitly requested.", + "architecture_phase": "turnaround_11", + "source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\agent_cashflow_profit_directness_20260523.json", + "scenario_id": "agent_cashflow_profit_directness_20260523", + "semantic_tags": [ + "bank_flow_boundary", + "business_overview", + "cashflow_polarity", + "cashflow_vs_profit", + "clean_profit", + "colloquial_earnings", + "compact_after_overview", + "direct_answer", + "direct_money_only", + "followup_context", + "next_steps", + "no_all_time_leak", + "no_cashflow_substitution", + "no_overview_substitution", + "no_profit_substitution", + "profit_route", + "profit_vs_cashflow", + "ranking_suppression", + "slang_wording", + "temporal_carryover" + ], + "validation_status": "accepted_live_replay", + "validated_run_dir": "artifacts\\domain_runs\\agent_cashflow_profit_directness_live11", + "saved_after_validated_replay": true, + "save_gate": { + "schema_version": "agent_semantic_save_gate_v1", + "validation_status": "accepted_live_replay", + "validated_run_dir": "artifacts\\domain_runs\\agent_cashflow_profit_directness_live11", + "final_status": "accepted", + "review_overall_status": "pass", + "business_overall_status": "pass", + "steps_total": 8, + "steps_passed": 8, + "steps_failed": 0, + "steps_with_business_failures": 0, + "steps_with_business_warnings": 0, + "acceptance_gate_passed": true, + "saved_after_validated_replay": true + } + } + } +} diff --git a/llm_normalizer/data/eval_cases/assistant_autogen_saved_user_sessions_20260523101140_gen-ag05231011-cec910.json b/llm_normalizer/data/eval_cases/assistant_autogen_saved_user_sessions_20260523101140_gen-ag05231011-cec910.json new file mode 100644 index 0000000..6402f1d --- /dev/null +++ b/llm_normalizer/data/eval_cases/assistant_autogen_saved_user_sessions_20260523101140_gen-ag05231011-cec910.json @@ -0,0 +1,49 @@ +{ + "suite_id": "assistant_saved_session_gen-ag05231011-cec910", + "suite_version": "0.1.0", + "schema_version": "assistant_saved_session_suite_v0_1", + "generated_at": "2026-05-23T10:11:40+00:00", + "generation_id": "gen-ag05231011-cec910", + "mode": "saved_user_sessions", + "title": "AGENT | Cashflow vs profit directness pack", + "domain": "autonomy_business_answer_contract", + "scenario_count": 1, + "case_ids": [ + "SAVED-001" + ], + "cases": [ + { + "case_id": "SAVED-001", + "scenario_tag": "agent_saved_user_sessions", + "title": "AGENT | Cashflow vs profit directness pack", + "question_type": "followup", + "broadness_level": "medium", + "turns": [ + { + "user_message": "Сколько денег Альтернатива заработала за 2020 год? Ответь коротко: получили, заплатили, денежное нетто, это прибыль или нет." + }, + { + "user_message": "скока денег альтернатива заработала за 20 год?" + }, + { + "user_message": "а это чистая прибыль?" + }, + { + "user_message": "а по деньгам тогда плюс или минус?" + }, + { + "user_message": "Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток." + }, + { + "user_message": "а если коротко, сколько заработали деньгами без топов?" + }, + { + "user_message": "не обзор, просто деньги: пришло, ушло, нетто за 2020" + }, + { + "user_message": "какая чистая прибыль по Альтернативе за 2020?" + } + ] + } + ] +}