diff --git a/docs/orchestration/agent_cashflow_compact_display_variants_20260523.json b/docs/orchestration/agent_cashflow_compact_display_variants_20260523.json new file mode 100644 index 0000000..f62d818 --- /dev/null +++ b/docs/orchestration/agent_cashflow_compact_display_variants_20260523.json @@ -0,0 +1,141 @@ +{ + "schema_version": "domain_truth_harness_spec_v1", + "scenario_id": "agent_cashflow_compact_display_variants_20260523", + "domain": "autonomy_business_answer_contract", + "title": "AGENT | Cashflow compact display variants", + "description": "Targeted AGENT replay for compact cashflow display modifiers: no counterparties, no breakdown, one-line and only-total follow-ups must preserve the 2020 company cashflow context.", + "bindings": {}, + "steps": [ + { + "step_id": "step_01_explicit_overview_anchor", + "title": "Create 2020 business overview context", + "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" + ], + "forbidden_answer_patterns": ["runtime_", "planner_", "query_movements", "primitive"], + "criticality": "high", + "semantic_tags": ["business_overview", "context_anchor"] + }, + { + "step_id": "step_02_no_counterparties", + "title": "No-counterparties means no breakdown, not exclusion", + "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": [ + "2026-05-23", + "получили\\s+0\\s*руб", + "нетто\\s+0\\s*руб", + "с исключением", + "исключением крупнейших", + "Комитет государственных услуг", + "Группа СВК", + "СБЕРБАНК", + "Что проверить дальше", + "runtime_", + "planner_", + "query_movements", + "primitive" + ], + "criticality": "critical", + "semantic_tags": ["no_counterparty_breakdown", "display_modifier", "temporal_carryover"] + }, + { + "step_id": "step_03_only_total", + "title": "Only total keeps compact cashflow", + "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": [ + "2026-05-23", + "получили\\s+0\\s*руб", + "нетто\\s+0\\s*руб", + "Комитет государственных услуг", + "Группа СВК", + "СБЕРБАНК", + "Что проверить дальше", + "runtime_", + "planner_", + "query_movements", + "primitive" + ], + "criticality": "critical", + "semantic_tags": ["only_total", "display_modifier", "temporal_carryover"] + }, + { + "step_id": "step_04_one_line_no_breakdown", + "title": "One-line no-breakdown stays cashflow", + "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": [ + "2026-05-23", + "получили\\s+0\\s*руб", + "нетто\\s+0\\s*руб", + "Комитет государственных услуг", + "Группа СВК", + "СБЕРБАНК", + "Что проверить дальше", + "runtime_", + "planner_", + "query_movements", + "primitive" + ], + "criticality": "critical", + "semantic_tags": ["one_line", "no_breakdown", "display_modifier"] + }, + { + "step_id": "step_05_no_details", + "title": "No-details wording remains direct totals", + "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": [ + "2026-05-23", + "получили\\s+0\\s*руб", + "нетто\\s+0\\s*руб", + "Комитет государственных услуг", + "Группа СВК", + "СБЕРБАНК", + "Что проверить дальше", + "runtime_", + "planner_", + "query_movements", + "primitive" + ], + "criticality": "critical", + "semantic_tags": ["no_details", "display_modifier", "direct_totals"] + } + ], + "acceptance": { + "min_score": 80, + "max_unresolved_p0": 0, + "require_all_critical_steps_pass": true + } +} diff --git a/llm_normalizer/backend/dist/services/assistantAddressOrchestrationRuntimeAdapter.js b/llm_normalizer/backend/dist/services/assistantAddressOrchestrationRuntimeAdapter.js index dbeaa25..e998cce 100644 --- a/llm_normalizer/backend/dist/services/assistantAddressOrchestrationRuntimeAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantAddressOrchestrationRuntimeAdapter.js @@ -51,6 +51,106 @@ function mergeOrganizationIntoDiscoveryFollowupContext(followupContext, organiza } return base; } +function compactLower(value) { + return String(value ?? "") + .toLowerCase() + .replace(/\s+/g, " ") + .trim(); +} +function hasCompactCashflowFollowupSignal(text) { + const value = compactLower(text); + if (!value) { + return false; + } + return /(?:\u0431\u0435\u0437\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0431\u0438\u0432\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0440\u0435\u0437\p{L}*|\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b\p{L}*|\u043e\u0434\u043d\u043e\u0439\s+\u0441\u0442\u0440\u043e\u043a\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0438\u0442\u043e\u0433|\u043f\u0440\u0438\u0448\p{L}*[\s\S]{0,80}\u0443\u0448\p{L}*[\s\S]{0,80}\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}\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b)/iu.test(value); +} +function dateScopeToFilterWindow(dateScope) { + if (!dateScope) { + return null; + } + if (/^(?:19|20)\d{2}$/.test(dateScope)) { + return { + period_from: `${dateScope}-01-01`, + period_to: `${dateScope}-12-31` + }; + } + if (/^(?:19|20)\d{2}-\d{2}-\d{2}$/.test(dateScope)) { + return { as_of_date: dateScope }; + } + return null; +} +function looksLikeReliableOrganizationScope(value) { + const text = compactLower(value); + if (!text) { + return false; + } + if (/(?:\u0440\u0430\u0437\u0431\u0438\u0432|\u0440\u0430\u0437\u0440\u0435\u0437|\u0432\u0445\u043e\u0434\u044f\u0449|\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u043d\u0435\u0442\u0442\u043e|\u0442\u043e\u043f)/iu.test(text)) { + return false; + } + return /(?:^|[\s"'\u00ab])(?:\u043e\u043e\u043e|\u0438\u043f|\u043f\u0430\u043e|\u0430\u043e|\u0437\u0430\u043e)(?:[\s"'\u00bb]|$)|\u043e\u0431\u0449\u0435\u0441\u0442\u0432\p{L}*\s+\u0441\s+\u043e\u0433\u0440\u0430\u043d\u0438\u0447\p{L}*\s+\u043e\u0442\u0432\p{L}*|\b(?:llc|inc|corp)\b/iu.test(text); +} +function inferBusinessOverviewDiscoveryContextFromSessionItems(sessionItems, toNonEmptyString) { + for (let index = sessionItems.length - 1; index >= 0; index -= 1) { + const item = toRecordObject(sessionItems[index]); + if (toNonEmptyString(item?.role) !== "assistant") { + continue; + } + const debug = toRecordObject(item?.debug); + const entryPoint = toRecordObject(debug?.assistant_mcp_discovery_entry_point_v1); + const turnInput = toRecordObject(entryPoint?.turn_input); + const dataNeedGraph = toRecordObject(turnInput?.data_need_graph); + if (toNonEmptyString(dataNeedGraph?.business_fact_family) !== "business_overview") { + continue; + } + const turnMeaningRef = toRecordObject(turnInput?.turn_meaning_ref); + const filterWindow = dateScopeToFilterWindow(toNonEmptyString(turnMeaningRef?.explicit_date_scope)); + if (!filterWindow) { + continue; + } + const previousFilters = { ...filterWindow }; + const organization = toNonEmptyString(turnMeaningRef?.explicit_organization_scope); + if (looksLikeReliableOrganizationScope(organization)) { + previousFilters.organization = organization; + } + const rankingNeed = toNonEmptyString(dataNeedGraph?.ranking_need); + return { + previous_discovery_pilot_scope: "business_overview_route_template_v1", + previous_filters: previousFilters, + root_filters: { ...previousFilters }, + previous_seeded_ranking_need: rankingNeed, + previous_intent: "business_overview", + root_intent: "business_overview" + }; + } + return null; +} +function mergeBusinessOverviewDateContextForCompactCashflow(input) { + if (!hasCompactCashflowFollowupSignal(input.userMessage)) { + return input.followupContext; + } + const existingFilters = toRecordObject(input.followupContext?.previous_filters); + if (input.toNonEmptyString(existingFilters?.period_from) || + input.toNonEmptyString(existingFilters?.period_to) || + input.toNonEmptyString(existingFilters?.as_of_date)) { + return input.followupContext; + } + const inferred = inferBusinessOverviewDiscoveryContextFromSessionItems(input.sessionItems, input.toNonEmptyString); + if (!inferred) { + return input.followupContext; + } + return { + ...inferred, + ...(input.followupContext ?? {}), + previous_filters: { + ...(toRecordObject(inferred.previous_filters) ?? {}), + ...(toRecordObject(input.followupContext?.previous_filters) ?? {}) + }, + root_filters: { + ...(toRecordObject(inferred.root_filters) ?? {}), + ...(toRecordObject(input.followupContext?.root_filters) ?? {}) + } + }; +} function hasSelectedObjectInventorySignal(text) { return /(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ним|selected\s+object)/iu.test(String(text ?? "")); } @@ -215,9 +315,15 @@ async function buildAssistantAddressOrchestrationRuntime(input) { const orchestrationContract = toRecordObject(orchestrationDecision.orchestrationContract); const predecomposeContract = toRecordObject(addressPreDecompose.predecomposeContract); const explicitPredecomposeOrganization = predecomposeOrganizationName(predecomposeContract, input.toNonEmptyString); - const discoveryFollowupContext = mergeOrganizationIntoDiscoveryFollowupContext(followupContext, explicitPredecomposeOrganization + const discoveryFollowupContextWithOrganization = mergeOrganizationIntoDiscoveryFollowupContext(followupContext, explicitPredecomposeOrganization ? null : sessionOrganizationName(input.sessionOrganizationScope ?? null, input.toNonEmptyString)); + const discoveryFollowupContext = mergeBusinessOverviewDateContextForCompactCashflow({ + userMessage: input.userMessage, + followupContext: discoveryFollowupContextWithOrganization, + sessionItems: input.sessionItems, + toNonEmptyString: input.toNonEmptyString + }); const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(input.userMessage, addressInputMessage, carryover, addressPreDecompose); const runDiscoveryEntryPoint = input.runMcpDiscoveryRuntimeEntryPoint ?? assistantMcpDiscoveryRuntimeEntryPoint_1.runAssistantMcpDiscoveryRuntimeEntryPoint; let mcpDiscoveryRuntimeEntryPoint = null; diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryDataNeedGraph.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryDataNeedGraph.js index b5c3908..4be636e 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryDataNeedGraph.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryDataNeedGraph.js @@ -118,7 +118,7 @@ function hasBusinessOverviewDirectMoneyAnswerHint(input) { 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)) { + 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|\u0431\u0435\u0437\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0431\u0438\u0432\p{L}*|\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b\p{L}*|\u043e\u0434\u043d\u043e\u0439\s+\u0441\u0442\u0440\u043e\u043a\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0438\u0442\u043e\u0433|\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); diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponseCandidate.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponseCandidate.js index a2f0279..c320483 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponseCandidate.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponseCandidate.js @@ -47,7 +47,7 @@ function requestsCompactCashflowAnswer(turnMeaning, graph) { 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); + 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|\u0431\u0435\u0437\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0431\u0438\u0432\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0440\u0435\u0437\p{L}*|\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b\p{L}*|\u043e\u0434\u043d\u043e\u0439\s+\u0441\u0442\u0440\u043e\u043a\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0438\u0442\u043e\u0433|\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([ diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js index 4216c06..f1d3767 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js @@ -274,6 +274,17 @@ function collectEntityCandidates(value) { } return result; } +function collectReasonCodes(value) { + const result = []; + if (!Array.isArray(value)) { + const reason = toNonEmptyString(value); + return reason ? [reason] : result; + } + for (const item of value) { + pushUnique(result, item); + } + return result; +} function collectPredecomposeEntities(predecompose) { const entities = toRecordObject(predecompose?.entities); const organization = toNonEmptyString(entities?.organization); @@ -1276,6 +1287,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) { const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family); const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis); const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family); + const assistantTurnMeaningReasonCodes = collectReasonCodes(assistantTurnMeaning?.reason_codes); + const compactCashflowDisplayFollowupSignal = assistantTurnMeaningReasonCodes.includes("compact_cashflow_display_current_turn_signal"); const broadBusinessEvaluationUnsupported = unsupported === "broad_business_evaluation"; const seededBusinessOverviewSignal = broadBusinessEvaluationUnsupported || rawDomain === "business_summary" || @@ -1909,6 +1922,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) { (valueFlowOrganizationStaysScope && (Boolean(followupSeed.rankingNeed) || bidirectionalValueFlowSignal)))); const suppressNegatedTaxOnlyDateScope = Boolean(businessOverviewSignal && negatedTaxDateScopeOnlySignal); const topicSwitchSuppressesFollowupScope = Boolean(rawTopicSwitchSignal && + !compactCashflowDisplayFollowupSignal && (rawMetadataSignal || businessOverviewSignal || rawEntitySearchOverridesStaleScope || diff --git a/llm_normalizer/backend/dist/services/assistantTurnMeaningPolicy.js b/llm_normalizer/backend/dist/services/assistantTurnMeaningPolicy.js index aaa1c3a..bc397f5 100644 --- a/llm_normalizer/backend/dist/services/assistantTurnMeaningPolicy.js +++ b/llm_normalizer/backend/dist/services/assistantTurnMeaningPolicy.js @@ -139,14 +139,15 @@ function hasCompactOrganizationCashflowDisplaySignal(text) { if (!normalized) { return false; } - const hasCompactCue = /(?:\u043a\u043e\u0440\u043e\u0442\u043a\w*|\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|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0431\u0438\u0432\p{L}*)/iu.test(normalized); + const hasCompactCue = /(?:\u043a\u043e\u0440\u043e\u0442\u043a\w*|\u043e\u0434\u043d\u043e\u0439\s+\u0441\u0442\u0440\u043e\u043a\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0438\u0442\u043e\u0433\p{L}*|\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|\u0431\u0435\u0437\s+\u0441\u043f\u0438\u0441(?:\u043a\p{L}*)?|\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0431\u0438\u0432\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0440\u0435\u0437\p{L}*|\u0431\u0435\u0437\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\p{L}*)/iu.test(normalized); if (!hasCompactCue) { return false; } const hasMoneyCue = /(?:\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\u0435\u0436\p{L}*|\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}*|\u0441\u043f\u0438\u0441\u0430\p{L}*|\u043d\u0435\u0442\u0442\u043e|cash|money|incoming|outgoing|net)/iu.test(normalized); - const hasOrganizationEarningsCue = /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|\u0437\u0430\u0440\u0430\u0431\u043e\u0442\p{L}*|\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}*|\u0434\u0435\u043d\u0435\u0436\p{L}*\s+\u043d\u0435\u0442\u0442\u043e|how\s+much|earned|received|paid)/iu.test(normalized); + const hasOrganizationEarningsCue = /(?:\u0434\u0430\u0439|\u043f\u043e\u043a\u0430\u0436\p{L}*|\u0438\u0442\u043e\u0433\p{L}*|\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|\u0437\u0430\u0440\u0430\u0431\u043e\u0442\p{L}*|\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}*|\u0434\u0435\u043d\u0435\u0436\p{L}*\s+\u043d\u0435\u0442\u0442\u043e|how\s+much|show|give|earned|received|paid)/iu.test(normalized); const hasExplicitExclusionCue = /(?:\u0438\u0441\u043a\u043b\u044e\u0447\p{L}*|\u0443\u0431\u0435\u0440\p{L}*|\u043a\u0440\u043e\u043c\u0435|exclude|excluding|without)[\s\S]{0,80}(?:\u043a\u0440\u0443\u043f\u043d\p{L}*|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\p{L}*|\u043a\u043b\u0438\u0435\u043d\u0442\p{L}*|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\p{L}*|\u043e\u043f\u0435\u0440\u0430\u0446\p{L}*|counterpart|customer|supplier|operation)/iu.test(normalized); - return hasMoneyCue && hasOrganizationEarningsCue && !hasExplicitExclusionCue; + const hasSpecificCounterpartyScope = detectScopedCounterpartyEntity(normalized) !== null; + return hasMoneyCue && hasOrganizationEarningsCue && !hasExplicitExclusionCue && !hasSpecificCounterpartyScope; } function detectScopedCounterpartyEntity(text) { const patterns = [ @@ -366,8 +367,8 @@ function createAssistantTurnMeaningPolicy(deps = {}) { const joinedText = fallbackCompactWhitespace(`${rawText} ${effectiveText}`); const compactOrganizationCashflowDisplay = hasCompactOrganizationCashflowDisplaySignal(rawText); const supportedIntent = compactOrganizationCashflowDisplay ? null : detectSupportedIntent(joinedText, deps); - const counterpartyBidirectionalValueFlow = detectCounterpartyBidirectionalValueFlowFamily(joinedText); - const counterpartyTurnover = detectCounterpartyTurnoverFamily(joinedText); + const counterpartyBidirectionalValueFlow = compactOrganizationCashflowDisplay ? null : detectCounterpartyBidirectionalValueFlowFamily(joinedText); + const counterpartyTurnover = compactOrganizationCashflowDisplay ? null : detectCounterpartyTurnoverFamily(joinedText); const selectedObjectInventoryExact = hasSelectedObjectInventoryExactSignal(joinedText); const broadBusinessEvaluation = compactOrganizationCashflowDisplay ? { family: "broad_business_evaluation" } diff --git a/llm_normalizer/backend/src/services/assistantAddressOrchestrationRuntimeAdapter.ts b/llm_normalizer/backend/src/services/assistantAddressOrchestrationRuntimeAdapter.ts index cc090c3..c000812 100644 --- a/llm_normalizer/backend/src/services/assistantAddressOrchestrationRuntimeAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantAddressOrchestrationRuntimeAdapter.ts @@ -138,6 +138,122 @@ function mergeOrganizationIntoDiscoveryFollowupContext( return base; } +function compactLower(value: unknown): string { + return String(value ?? "") + .toLowerCase() + .replace(/\s+/g, " ") + .trim(); +} + +function hasCompactCashflowFollowupSignal(text: string | null): boolean { + const value = compactLower(text); + if (!value) { + return false; + } + return /(?:\u0431\u0435\u0437\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0431\u0438\u0432\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0440\u0435\u0437\p{L}*|\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b\p{L}*|\u043e\u0434\u043d\u043e\u0439\s+\u0441\u0442\u0440\u043e\u043a\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0438\u0442\u043e\u0433|\u043f\u0440\u0438\u0448\p{L}*[\s\S]{0,80}\u0443\u0448\p{L}*[\s\S]{0,80}\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}\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b)/iu.test(value); +} + +function dateScopeToFilterWindow(dateScope: string | null): Record | null { + if (!dateScope) { + return null; + } + if (/^(?:19|20)\d{2}$/.test(dateScope)) { + return { + period_from: `${dateScope}-01-01`, + period_to: `${dateScope}-12-31` + }; + } + if (/^(?:19|20)\d{2}-\d{2}-\d{2}$/.test(dateScope)) { + return { as_of_date: dateScope }; + } + return null; +} + +function looksLikeReliableOrganizationScope(value: string | null): boolean { + const text = compactLower(value); + if (!text) { + return false; + } + if (/(?:\u0440\u0430\u0437\u0431\u0438\u0432|\u0440\u0430\u0437\u0440\u0435\u0437|\u0432\u0445\u043e\u0434\u044f\u0449|\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u043d\u0435\u0442\u0442\u043e|\u0442\u043e\u043f)/iu.test(text)) { + return false; + } + return /(?:^|[\s"'\u00ab])(?:\u043e\u043e\u043e|\u0438\u043f|\u043f\u0430\u043e|\u0430\u043e|\u0437\u0430\u043e)(?:[\s"'\u00bb]|$)|\u043e\u0431\u0449\u0435\u0441\u0442\u0432\p{L}*\s+\u0441\s+\u043e\u0433\u0440\u0430\u043d\u0438\u0447\p{L}*\s+\u043e\u0442\u0432\p{L}*|\b(?:llc|inc|corp)\b/iu.test(text); +} + +function inferBusinessOverviewDiscoveryContextFromSessionItems( + sessionItems: unknown[], + toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"] +): Record | null { + for (let index = sessionItems.length - 1; index >= 0; index -= 1) { + const item = toRecordObject(sessionItems[index]); + if (toNonEmptyString(item?.role) !== "assistant") { + continue; + } + const debug = toRecordObject(item?.debug); + const entryPoint = toRecordObject(debug?.assistant_mcp_discovery_entry_point_v1); + const turnInput = toRecordObject(entryPoint?.turn_input); + const dataNeedGraph = toRecordObject(turnInput?.data_need_graph); + if (toNonEmptyString(dataNeedGraph?.business_fact_family) !== "business_overview") { + continue; + } + const turnMeaningRef = toRecordObject(turnInput?.turn_meaning_ref); + const filterWindow = dateScopeToFilterWindow(toNonEmptyString(turnMeaningRef?.explicit_date_scope)); + if (!filterWindow) { + continue; + } + const previousFilters: Record = { ...filterWindow }; + const organization = toNonEmptyString(turnMeaningRef?.explicit_organization_scope); + if (looksLikeReliableOrganizationScope(organization)) { + previousFilters.organization = organization; + } + const rankingNeed = toNonEmptyString(dataNeedGraph?.ranking_need); + return { + previous_discovery_pilot_scope: "business_overview_route_template_v1", + previous_filters: previousFilters, + root_filters: { ...previousFilters }, + previous_seeded_ranking_need: rankingNeed, + previous_intent: "business_overview", + root_intent: "business_overview" + }; + } + return null; +} + +function mergeBusinessOverviewDateContextForCompactCashflow(input: { + userMessage: string; + followupContext: Record | null; + sessionItems: unknown[]; + toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]; +}): Record | null { + if (!hasCompactCashflowFollowupSignal(input.userMessage)) { + return input.followupContext; + } + const existingFilters = toRecordObject(input.followupContext?.previous_filters); + if ( + input.toNonEmptyString(existingFilters?.period_from) || + input.toNonEmptyString(existingFilters?.period_to) || + input.toNonEmptyString(existingFilters?.as_of_date) + ) { + return input.followupContext; + } + const inferred = inferBusinessOverviewDiscoveryContextFromSessionItems(input.sessionItems, input.toNonEmptyString); + if (!inferred) { + return input.followupContext; + } + return { + ...inferred, + ...(input.followupContext ?? {}), + previous_filters: { + ...(toRecordObject(inferred.previous_filters) ?? {}), + ...(toRecordObject(input.followupContext?.previous_filters) ?? {}) + }, + root_filters: { + ...(toRecordObject(inferred.root_filters) ?? {}), + ...(toRecordObject(input.followupContext?.root_filters) ?? {}) + } + }; +} + function hasSelectedObjectInventorySignal(text: string | null): boolean { return /(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ним|selected\s+object)/iu.test( String(text ?? "") @@ -397,12 +513,18 @@ export async function buildAssistantAddressOrchestrationRuntime( const orchestrationContract = toRecordObject(orchestrationDecision.orchestrationContract); const predecomposeContract = toRecordObject(addressPreDecompose.predecomposeContract); const explicitPredecomposeOrganization = predecomposeOrganizationName(predecomposeContract, input.toNonEmptyString); - const discoveryFollowupContext = mergeOrganizationIntoDiscoveryFollowupContext( + const discoveryFollowupContextWithOrganization = mergeOrganizationIntoDiscoveryFollowupContext( followupContext, explicitPredecomposeOrganization ? null : sessionOrganizationName(input.sessionOrganizationScope ?? null, input.toNonEmptyString) ); + const discoveryFollowupContext = mergeBusinessOverviewDateContextForCompactCashflow({ + userMessage: input.userMessage, + followupContext: discoveryFollowupContextWithOrganization, + sessionItems: input.sessionItems, + toNonEmptyString: input.toNonEmptyString + }); const dialogContinuationContract = input.buildAddressDialogContinuationContractV2( input.userMessage, addressInputMessage, diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryDataNeedGraph.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryDataNeedGraph.ts index e6a29aa..c832651 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryDataNeedGraph.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryDataNeedGraph.ts @@ -184,7 +184,7 @@ function hasBusinessOverviewDirectMoneyAnswerHint(input: { 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) + /(?:\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|\u0431\u0435\u0437\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0431\u0438\u0432\p{L}*|\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b\p{L}*|\u043e\u0434\u043d\u043e\u0439\s+\u0441\u0442\u0440\u043e\u043a\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0438\u0442\u043e\u0433|\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; } diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts index 38f67ef..4ee9469 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts @@ -80,7 +80,7 @@ function requestsCompactCashflowAnswer( ) { 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( + 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|\u0431\u0435\u0437\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0431\u0438\u0432\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0440\u0435\u0437\p{L}*|\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b\p{L}*|\u043e\u0434\u043d\u043e\u0439\s+\u0441\u0442\u0440\u043e\u043a\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0438\u0442\u043e\u0433|\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 ); } diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts index d09aec2..d055750 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts @@ -353,6 +353,18 @@ function collectEntityCandidates(value: unknown): string[] { return result; } +function collectReasonCodes(value: unknown): string[] { + const result: string[] = []; + if (!Array.isArray(value)) { + const reason = toNonEmptyString(value); + return reason ? [reason] : result; + } + for (const item of value) { + pushUnique(result, item); + } + return result; +} + function collectPredecomposeEntities(predecompose: Record | null): { counterparty: string | null; organization: string | null; @@ -1782,6 +1794,10 @@ export function buildAssistantMcpDiscoveryTurnInput( const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family); const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis); const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family); + const assistantTurnMeaningReasonCodes = collectReasonCodes(assistantTurnMeaning?.reason_codes); + const compactCashflowDisplayFollowupSignal = assistantTurnMeaningReasonCodes.includes( + "compact_cashflow_display_current_turn_signal" + ); const broadBusinessEvaluationUnsupported = unsupported === "broad_business_evaluation"; const seededBusinessOverviewSignal = broadBusinessEvaluationUnsupported || @@ -2553,6 +2569,7 @@ export function buildAssistantMcpDiscoveryTurnInput( const suppressNegatedTaxOnlyDateScope = Boolean(businessOverviewSignal && negatedTaxDateScopeOnlySignal); const topicSwitchSuppressesFollowupScope = Boolean( rawTopicSwitchSignal && + !compactCashflowDisplayFollowupSignal && (rawMetadataSignal || businessOverviewSignal || rawEntitySearchOverridesStaleScope || diff --git a/llm_normalizer/backend/src/services/assistantTurnMeaningPolicy.ts b/llm_normalizer/backend/src/services/assistantTurnMeaningPolicy.ts index c36bfa3..9289669 100644 --- a/llm_normalizer/backend/src/services/assistantTurnMeaningPolicy.ts +++ b/llm_normalizer/backend/src/services/assistantTurnMeaningPolicy.ts @@ -145,7 +145,7 @@ function hasCompactOrganizationCashflowDisplaySignal(text) { return false; } const hasCompactCue = - /(?:\u043a\u043e\u0440\u043e\u0442\u043a\w*|\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|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0431\u0438\u0432\p{L}*)/iu.test( + /(?:\u043a\u043e\u0440\u043e\u0442\u043a\w*|\u043e\u0434\u043d\u043e\u0439\s+\u0441\u0442\u0440\u043e\u043a\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0438\u0442\u043e\u0433\p{L}*|\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|\u0431\u0435\u0437\s+\u0441\u043f\u0438\u0441(?:\u043a\p{L}*)?|\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0431\u0438\u0432\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0440\u0435\u0437\p{L}*|\u0431\u0435\u0437\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\p{L}*)/iu.test( normalized ); if (!hasCompactCue) { @@ -156,14 +156,15 @@ function hasCompactOrganizationCashflowDisplaySignal(text) { normalized ); const hasOrganizationEarningsCue = - /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|\u0437\u0430\u0440\u0430\u0431\u043e\u0442\p{L}*|\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}*|\u0434\u0435\u043d\u0435\u0436\p{L}*\s+\u043d\u0435\u0442\u0442\u043e|how\s+much|earned|received|paid)/iu.test( + /(?:\u0434\u0430\u0439|\u043f\u043e\u043a\u0430\u0436\p{L}*|\u0438\u0442\u043e\u0433\p{L}*|\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|\u0437\u0430\u0440\u0430\u0431\u043e\u0442\p{L}*|\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}*|\u0434\u0435\u043d\u0435\u0436\p{L}*\s+\u043d\u0435\u0442\u0442\u043e|how\s+much|show|give|earned|received|paid)/iu.test( normalized ); const hasExplicitExclusionCue = /(?:\u0438\u0441\u043a\u043b\u044e\u0447\p{L}*|\u0443\u0431\u0435\u0440\p{L}*|\u043a\u0440\u043e\u043c\u0435|exclude|excluding|without)[\s\S]{0,80}(?:\u043a\u0440\u0443\u043f\u043d\p{L}*|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\p{L}*|\u043a\u043b\u0438\u0435\u043d\u0442\p{L}*|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\p{L}*|\u043e\u043f\u0435\u0440\u0430\u0446\p{L}*|counterpart|customer|supplier|operation)/iu.test( normalized ); - return hasMoneyCue && hasOrganizationEarningsCue && !hasExplicitExclusionCue; + const hasSpecificCounterpartyScope = detectScopedCounterpartyEntity(normalized) !== null; + return hasMoneyCue && hasOrganizationEarningsCue && !hasExplicitExclusionCue && !hasSpecificCounterpartyScope; } function detectScopedCounterpartyEntity(text) { @@ -485,8 +486,8 @@ export function createAssistantTurnMeaningPolicy(deps = {}) { const joinedText = fallbackCompactWhitespace(`${rawText} ${effectiveText}`); const compactOrganizationCashflowDisplay = hasCompactOrganizationCashflowDisplaySignal(rawText); const supportedIntent = compactOrganizationCashflowDisplay ? null : detectSupportedIntent(joinedText, deps); - const counterpartyBidirectionalValueFlow = detectCounterpartyBidirectionalValueFlowFamily(joinedText); - const counterpartyTurnover = detectCounterpartyTurnoverFamily(joinedText); + const counterpartyBidirectionalValueFlow = compactOrganizationCashflowDisplay ? null : detectCounterpartyBidirectionalValueFlowFamily(joinedText); + const counterpartyTurnover = compactOrganizationCashflowDisplay ? null : detectCounterpartyTurnoverFamily(joinedText); const selectedObjectInventoryExact = hasSelectedObjectInventoryExactSignal(joinedText); const broadBusinessEvaluation = compactOrganizationCashflowDisplay diff --git a/llm_normalizer/backend/tests/assistantAddressOrchestrationRuntimeAdapter.test.ts b/llm_normalizer/backend/tests/assistantAddressOrchestrationRuntimeAdapter.test.ts index 26b8e5d..a170a5f 100644 --- a/llm_normalizer/backend/tests/assistantAddressOrchestrationRuntimeAdapter.test.ts +++ b/llm_normalizer/backend/tests/assistantAddressOrchestrationRuntimeAdapter.test.ts @@ -224,6 +224,89 @@ describe("assistant address orchestration runtime adapter", () => { ); }); + it("rebuilds business overview period carryover for compact cashflow follow-up when address carryover is absent", async () => { + const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({ + schema_version: "assistant_mcp_discovery_runtime_entry_point_v1", + policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint", + entry_status: "bridge_executed", + hot_runtime_wired: false, + discovery_attempted: true + })); + const input = buildInput({ + userMessage: + "\u0434\u0430\u0439 \u043e\u0434\u043d\u043e\u0439 \u0441\u0442\u0440\u043e\u043a\u043e\u0439 \u0434\u0435\u043d\u044c\u0433\u0438 \u0431\u0435\u0437 \u0440\u0430\u0437\u0431\u0438\u0432\u043a\u0438", + sessionItems: [ + { + role: "assistant", + debug: { + assistant_mcp_discovery_entry_point_v1: { + turn_input: { + data_need_graph: { + business_fact_family: "business_overview", + ranking_need: "top_desc" + }, + turn_meaning_ref: { + explicit_date_scope: "2020", + explicit_organization_scope: + "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441" + } + } + } + } + } + ], + runAddressLlmPreDecompose: vi.fn(async () => ({ + attempted: true, + applied: false, + effectiveMessage: + "\u0434\u0430\u0439 \u043e\u0434\u043d\u043e\u0439 \u0441\u0442\u0440\u043e\u043a\u043e\u0439 \u0434\u0435\u043d\u044c\u0433\u0438 \u0431\u0435\u0437 \u0440\u0430\u0437\u0431\u0438\u0432\u043a\u0438", + reason: "raw_kept", + predecomposeContract: { + mode: "unsupported", + intent: "unknown", + period: {} + } + })), + resolveAddressFollowupCarryoverContext: vi.fn(() => null), + resolveAssistantOrchestrationDecision: vi.fn(() => ({ + runAddressLane: false, + livingMode: "chat", + livingReason: "unsupported_current_turn_meaning_boundary", + toolGateDecision: "skip_address_lane", + toolGateReason: "unsupported_current_turn_meaning_boundary", + orchestrationContract: { + schema_version: "assistant_orchestration_contract_v1", + assistant_turn_meaning: { + schema_version: "assistant_turn_meaning_v1", + asked_domain_family: "business_summary", + asked_action_family: "broad_evaluation", + unsupported_but_understood_family: "broad_business_evaluation" + } + } + })), + runMcpDiscoveryRuntimeEntryPoint + }); + + await buildAssistantAddressOrchestrationRuntime(input); + + expect(runMcpDiscoveryRuntimeEntryPoint).toHaveBeenCalledWith( + expect.objectContaining({ + followupContext: expect.objectContaining({ + previous_discovery_pilot_scope: "business_overview_route_template_v1", + previous_seeded_ranking_need: "top_desc", + previous_filters: expect.objectContaining({ + period_from: "2020-01-01", + period_to: "2020-12-31" + }), + root_filters: expect.objectContaining({ + period_from: "2020-01-01", + period_to: "2020-12-31" + }) + }) + }) + ); + }); + it("passes grounded discovery follow-up carryover into MCP discovery entry point for a short year switch", async () => { const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({ schema_version: "assistant_mcp_discovery_runtime_entry_point_v1", diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryResponseCandidate.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryResponseCandidate.test.ts index 4e9cd76..6b1afa7 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryResponseCandidate.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryResponseCandidate.test.ts @@ -704,6 +704,80 @@ describe("assistant MCP discovery response candidate", () => { expect(candidate.reply_text).not.toContain("wide overview"); }); + it("uses compact cashflow output for one-line no-breakdown wording", () => { + const candidate = buildAssistantMcpDiscoveryResponseCandidate( + entryPoint({ + turn_input: { + adapter_status: "ready", + turn_meaning_ref: { + raw_message: + "\u0434\u0430\u0439 \u043e\u0434\u043d\u043e\u0439 \u0441\u0442\u0440\u043e\u043a\u043e\u0439 \u0434\u0435\u043d\u044c\u0433\u0438 \u0431\u0435\u0437 \u0440\u0430\u0437\u0431\u0438\u0432\u043a\u0438", + asked_action_family: "broad_evaluation", + unsupported_but_understood_family: "broad_business_evaluation", + 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 \u0440\u0443\u0431." + }, + outgoing_supplier_payout: { + total_amount_human_ru: "43 763 351,53 \u0440\u0443\u0431." + }, + net_amount_human_ru: "3 865 501,50 \u0440\u0443\u0431.", + net_direction: "net_incoming", + top_customers: [ + { + axis_value: "\u0421\u0411\u0415\u0420\u0411\u0410\u041d\u041a, \u041f\u0410\u041e", + total_amount_human_ru: "12 792 194,31 \u0440\u0443\u0431." + } + ], + top_suppliers: [ + { + axis_value: "\u0414\u0435\u043f\u0430\u0440\u0442\u0430\u043c\u0435\u043d\u0442", + total_amount_human_ru: "9 612 904,90 \u0440\u0443\u0431." + } + ] + } + }, + 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: "\u0427\u0442\u043e \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u0434\u0430\u043b\u044c\u0448\u0435" + } + } + }) + ); + + expect(candidate.reply_text).toContain("\u0437\u0430 2020"); + 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("\u0421\u0411\u0415\u0420\u0411\u0410\u041d\u041a"); + expect(candidate.reply_text).not.toContain("\u0427\u0442\u043e \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c"); + 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 ca3a905..6b370ae 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts @@ -2380,6 +2380,47 @@ describe("assistant MCP discovery turn input adapter", () => { expect(result.data_need_graph?.reason_codes).toContain("data_need_graph_business_overview_direct_money_answer"); }); + it("preserves previous overview period for compact cashflow display follow-ups", () => { + 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: + "\u0434\u0430\u0439 \u043e\u0434\u043d\u043e\u0439 \u0441\u0442\u0440\u043e\u043a\u043e\u0439 \u0434\u0435\u043d\u044c\u0433\u0438 \u0431\u0435\u0437 \u0440\u0430\u0437\u0431\u0438\u0432\u043a\u0438", + assistantTurnMeaning: { + asked_domain_family: "business_summary", + asked_action_family: "broad_evaluation", + unsupported_but_understood_family: "broad_business_evaluation", + stale_replay_forbidden: true, + reason_codes: ["compact_cashflow_display_current_turn_signal"] + }, + followupContext: { + previous_discovery_pilot_scope: "business_overview_route_template_v1", + previous_filters: { + organization: orgName, + period_from: "2020-01-01", + period_to: "2020-12-31" + }, + 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).toMatchObject({ + asked_domain_family: "business_overview", + asked_action_family: "broad_evaluation", + explicit_organization_scope: orgName, + explicit_date_scope: "2020", + unsupported_but_understood_family: "broad_business_evaluation", + stale_replay_forbidden: true + }); + expect(result.reason_codes).toContain("mcp_discovery_date_scope_from_followup_context"); + expect(result.reason_codes).not.toContain("mcp_discovery_topic_switch_suppressed_followup_scope"); + expect(result.data_need_graph?.reason_codes).toContain("data_need_graph_business_overview_direct_money_answer"); + expect(result.data_need_graph?.reason_codes).not.toContain("data_need_graph_all_time_scope_hint"); + }); + 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/backend/tests/assistantTurnMeaningPolicy.test.ts b/llm_normalizer/backend/tests/assistantTurnMeaningPolicy.test.ts index 42dd2b3..2ea01b6 100644 --- a/llm_normalizer/backend/tests/assistantTurnMeaningPolicy.test.ts +++ b/llm_normalizer/backend/tests/assistantTurnMeaningPolicy.test.ts @@ -216,6 +216,32 @@ describe("assistantTurnMeaningPolicy", () => { expect(meaning.reason_codes).not.toContain("counterparty_turnover_current_turn_signal"); }); + it.each([ + "\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438 \u0434\u0435\u043d\u044c\u0433\u0430\u043c\u0438 \u0431\u0435\u0437 \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u043e\u0432?", + "\u0442\u043e\u043b\u044c\u043a\u043e \u0438\u0442\u043e\u0433: \u043f\u0440\u0438\u0448\u043b\u043e, \u0443\u0448\u043b\u043e, \u043d\u0435\u0442\u0442\u043e", + "\u0434\u0430\u0439 \u043e\u0434\u043d\u043e\u0439 \u0441\u0442\u0440\u043e\u043a\u043e\u0439 \u0434\u0435\u043d\u044c\u0433\u0438 \u0431\u0435\u0437 \u0440\u0430\u0437\u0431\u0438\u0432\u043a\u0438", + "\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438 \u0438 \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438 \u0431\u0435\u0437 \u0434\u0435\u0442\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438" + ])("treats compact cashflow wording '%s' as business-summary display request", (rawUserMessage) => { + const policy = buildPolicy({ + resolveAddressIntent: () => ({ intent: "customer_revenue_and_payments", confidence: "high" }) + }); + + const meaning = policy.resolveAssistantTurnMeaning({ + rawUserMessage, + effectiveAddressUserMessage: + "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u043e\u0431\u043e\u0440\u043e\u0442 \u0438 \u0432\u044b\u0440\u0443\u0447\u043a\u0443 \u043f\u043e \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438 \u0431\u0435\u0437 \u0440\u0430\u0437\u0440\u0435\u0437\u0430 \u043f\u043e \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430\u043c." + }); + + expect(meaning.explicit_intent_candidate).toBeNull(); + expect(meaning.asked_domain_family).toBe("business_summary"); + expect(meaning.asked_action_family).toBe("broad_evaluation"); + expect(meaning.explicit_entity_candidates).toEqual([]); + expect(meaning.unsupported_but_understood_family).toBe("broad_business_evaluation"); + expect(meaning.stale_replay_forbidden).toBe(true); + expect(meaning.reason_codes).toContain("compact_cashflow_display_current_turn_signal"); + expect(meaning.reason_codes).not.toContain("counterparty_turnover_current_turn_signal"); + }); + it("treats organization-level earnings and best-year wording as business overview", () => { const policy = buildPolicy({ resolveAddressIntent: () => ({ intent: "customer_revenue_and_payments", confidence: "high" }) diff --git a/llm_normalizer/data/autorun_generators/history.json b/llm_normalizer/data/autorun_generators/history.json index 7774268..16c7cad 100644 --- a/llm_normalizer/data/autorun_generators/history.json +++ b/llm_normalizer/data/autorun_generators/history.json @@ -1,4 +1,53 @@ [ + { + "generation_id": "gen-ag05231427-70915a", + "created_at": "2026-05-23T14:27:55+00:00", + "mode": "saved_user_sessions", + "title": "AGENT | Cashflow compact display variants", + "count": 5, + "domain": "autonomy_business_answer_contract", + "questions": [ + "Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток.", + "сколько заработали деньгами без контрагентов?", + "только итог: пришло, ушло, нетто", + "дай одной строкой деньги без разбивки", + "сколько получили и заплатили без детализации" + ], + "generated_by": "codex_agent", + "saved_case_set_file": "assistant_autogen_saved_user_sessions_20260523142755_gen-ag05231427-70915a.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_20260523142755_gen-ag05231427-70915a.json", + "saved_case_set_kind": "agent_semantic_scenario", + "agent_run": true, + "agent_focus": "Targeted AGENT replay for compact cashflow display modifiers: no counterparties, no breakdown, one-line and only-total follow-ups must preserve the 2020 company cashflow context.", + "architecture_phase": "turnaround_11", + "source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\agent_cashflow_compact_display_variants_20260523.json", + "scenario_id": "agent_cashflow_compact_display_variants_20260523", + "semantic_tags": [ + "business_overview", + "context_anchor", + "direct_totals", + "display_modifier", + "no_breakdown", + "no_counterparty_breakdown", + "no_details", + "one_line", + "only_total", + "temporal_carryover" + ], + "validation_status": "accepted_live_replay", + "validated_run_dir": "artifacts\\domain_runs\\agent_cashflow_compact_display_variants_live5", + "saved_after_validated_replay": true + } + }, { "generation_id": "gen-ag05231310-3d45fe", "created_at": "2026-05-23T13:10:25+00:00", diff --git a/llm_normalizer/data/autorun_generators/saved_sessions/assistant_saved_session_20260523142755_gen-ag05231427-70915a.json b/llm_normalizer/data/autorun_generators/saved_sessions/assistant_saved_session_20260523142755_gen-ag05231427-70915a.json new file mode 100644 index 0000000..4abcc15 --- /dev/null +++ b/llm_normalizer/data/autorun_generators/saved_sessions/assistant_saved_session_20260523142755_gen-ag05231427-70915a.json @@ -0,0 +1,145 @@ +{ + "saved_at": "2026-05-23T14:27:55+00:00", + "generation_id": "gen-ag05231427-70915a", + "mode": "saved_user_sessions", + "title": "AGENT | Cashflow compact display variants", + "agent_run": true, + "questions": [ + "Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток.", + "сколько заработали деньгами без контрагентов?", + "только итог: пришло, ушло, нетто", + "дай одной строкой деньги без разбивки", + "сколько получили и заплатили без детализации" + ], + "metadata": { + "assistant_prompt_version": null, + "decomposition_prompt_version": null, + "prompt_fingerprint": null, + "agent_focus": "Targeted AGENT replay for compact cashflow display modifiers: no counterparties, no breakdown, one-line and only-total follow-ups must preserve the 2020 company cashflow context.", + "architecture_phase": "turnaround_11", + "source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\agent_cashflow_compact_display_variants_20260523.json", + "scenario_id": "agent_cashflow_compact_display_variants_20260523", + "semantic_tags": [ + "business_overview", + "context_anchor", + "direct_totals", + "display_modifier", + "no_breakdown", + "no_counterparty_breakdown", + "no_details", + "one_line", + "only_total", + "temporal_carryover" + ], + "validation_status": "accepted_live_replay", + "validated_run_dir": "artifacts\\domain_runs\\agent_cashflow_compact_display_variants_live5", + "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_compact_display_variants_live5", + "final_status": "accepted", + "review_overall_status": "pass", + "business_overall_status": "pass", + "steps_total": 5, + "steps_passed": 5, + "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-23T14:27:55+00:00", + "reply_type": null, + "trace_id": null, + "debug": null + }, + { + "message_id": "agent-user-002", + "role": "user", + "text": "сколько заработали деньгами без контрагентов?", + "created_at": "2026-05-23T14:27:55+00:00", + "reply_type": null, + "trace_id": null, + "debug": null + }, + { + "message_id": "agent-user-003", + "role": "user", + "text": "только итог: пришло, ушло, нетто", + "created_at": "2026-05-23T14:27:55+00:00", + "reply_type": null, + "trace_id": null, + "debug": null + }, + { + "message_id": "agent-user-004", + "role": "user", + "text": "дай одной строкой деньги без разбивки", + "created_at": "2026-05-23T14:27:55+00:00", + "reply_type": null, + "trace_id": null, + "debug": null + }, + { + "message_id": "agent-user-005", + "role": "user", + "text": "сколько получили и заплатили без детализации", + "created_at": "2026-05-23T14:27:55+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 compact cashflow display modifiers: no counterparties, no breakdown, one-line and only-total follow-ups must preserve the 2020 company cashflow context.", + "architecture_phase": "turnaround_11", + "source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\agent_cashflow_compact_display_variants_20260523.json", + "scenario_id": "agent_cashflow_compact_display_variants_20260523", + "semantic_tags": [ + "business_overview", + "context_anchor", + "direct_totals", + "display_modifier", + "no_breakdown", + "no_counterparty_breakdown", + "no_details", + "one_line", + "only_total", + "temporal_carryover" + ], + "validation_status": "accepted_live_replay", + "validated_run_dir": "artifacts\\domain_runs\\agent_cashflow_compact_display_variants_live5", + "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_compact_display_variants_live5", + "final_status": "accepted", + "review_overall_status": "pass", + "business_overall_status": "pass", + "steps_total": 5, + "steps_passed": 5, + "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_20260523142755_gen-ag05231427-70915a.json b/llm_normalizer/data/eval_cases/assistant_autogen_saved_user_sessions_20260523142755_gen-ag05231427-70915a.json new file mode 100644 index 0000000..4f6288c --- /dev/null +++ b/llm_normalizer/data/eval_cases/assistant_autogen_saved_user_sessions_20260523142755_gen-ag05231427-70915a.json @@ -0,0 +1,40 @@ +{ + "suite_id": "assistant_saved_session_gen-ag05231427-70915a", + "suite_version": "0.1.0", + "schema_version": "assistant_saved_session_suite_v0_1", + "generated_at": "2026-05-23T14:27:55+00:00", + "generation_id": "gen-ag05231427-70915a", + "mode": "saved_user_sessions", + "title": "AGENT | Cashflow compact display variants", + "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 compact display variants", + "question_type": "followup", + "broadness_level": "medium", + "turns": [ + { + "user_message": "Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток." + }, + { + "user_message": "сколько заработали деньгами без контрагентов?" + }, + { + "user_message": "только итог: пришло, ушло, нетто" + }, + { + "user_message": "дай одной строкой деньги без разбивки" + }, + { + "user_message": "сколько получили и заплатили без детализации" + } + ] + } + ] +}