diff --git a/docs/orchestration/agent_inventory_margin_ranking_20260523.json b/docs/orchestration/agent_inventory_margin_ranking_20260523.json new file mode 100644 index 0000000..5d88090 --- /dev/null +++ b/docs/orchestration/agent_inventory_margin_ranking_20260523.json @@ -0,0 +1,198 @@ +{ + "schema_version": "domain_truth_harness_spec_v1", + "scenario_id": "agent_inventory_margin_ranking_20260523", + "domain": "inventory_margin_ranking", + "title": "AGENT | Inventory margin ranking limited answer pack", + "description": "Targeted live replay for nomenclature high/low profit questions: ask period when missing, stay in inventory/sales/cost domain, give useful limited answer when cost evidence is insufficient, and avoid fixed-assets/bank leakage.", + "bindings": { + + }, + "steps": [ + { + "step_id": "step_01_margin_root_needs_period", + "title": "Root asks nomenclature high/low profit without period", + "question": "Какая номеклатура товара реализована с высокой прибылью какая с низкой", + "allowed_reply_types": [ + "partial_coverage", + "factual_with_explanation", + "factual" + ], + "required_answer_patterns_all": [ + "период|месяц|квартал|год", + "номенклатур", + "выручк|себестоим|валов|маржин" + ], + "forbidden_answer_patterns": [ + "амортизац", + "основн[а-я]+ средств", + "\\bОС\\s+как\\s+объект\\b", + "Сбербанк", + "банк", + "зависш[а-я]+ оплат", + "payment", + "settlement cluster", + "runtime_", + "planner_", + "query_movements", + "primitive", + "90/91/99", + "7\\s*136\\s*815" + ], + "criticality": "critical", + "semantic_tags": [ + "inventory_margin_ranking", + "needs_period", + "domain_purity" + ] + }, + { + "step_id": "step_02_may_2020_period_limited_answer", + "title": "User provides May 2020 period and gets useful limited answer", + "question": "май 2020", + "allowed_reply_types": [ + "partial_coverage", + "factual_with_explanation", + "factual" + ], + "required_answer_patterns_all": [ + "май|01\\.05\\.2020|31\\.05\\.2020|2020", + "рейтинг|прибыль|маржин", + "себестоим|90\\.02|закупоч", + "нельзя|не удалось|недостаточ|не подтвержд" + ], + "forbidden_answer_patterns": [ + "амортизац", + "основн[а-я]+ средств", + "\\bОС\\s+как\\s+объект\\b", + "Сбербанк", + "банк", + "зависш[а-я]+ оплат", + "payment", + "settlement cluster", + "runtime_", + "planner_", + "query_movements", + "primitive", + "7\\s*136\\s*815" + ], + "criticality": "critical", + "semantic_tags": [ + "inventory_margin_ranking", + "limited_answer", + "period_followup" + ] + }, + { + "step_id": "step_03_show_cost_base_lines", + "title": "Follow-up asks for found cost-base evidence", + "question": "покажи найденные строки себестоимостной базы", + "allowed_reply_types": [ + "partial_coverage", + "factual_with_explanation", + "factual" + ], + "required_answer_patterns_any": [ + "себестоим", + "закупоч", + "90\\.02", + "41", + "не найден|не подтвержд|нет" + ], + "forbidden_answer_patterns": [ + "амортизац", + "основн[а-я]+ средств", + "\\bОС\\s+как\\s+объект\\b", + "Сбербанк", + "банк", + "зависш[а-я]+ оплат", + "payment", + "settlement cluster", + "runtime_", + "planner_", + "query_movements", + "primitive", + "7\\s*136\\s*815" + ], + "criticality": "high", + "semantic_tags": [ + "inventory_margin_ranking", + "evidence_followup", + "carryover" + ] + }, + { + "step_id": "step_04_expand_to_2017", + "title": "User expands period to full 2017", + "question": "расширь до 2017 года", + "allowed_reply_types": [ + "partial_coverage", + "factual_with_explanation", + "factual" + ], + "required_answer_patterns_any": [ + "2017", + "рейтинг|маржин|прибыль", + "себестоим|90\\.02|закупоч", + "нельзя|не подтвержд|найден" + ], + "forbidden_answer_patterns": [ + "амортизац", + "основн[а-я]+ средств", + "\\bОС\\s+как\\s+объект\\b", + "Сбербанк", + "банк", + "зависш[а-я]+ оплат", + "payment", + "settlement cluster", + "runtime_", + "planner_", + "query_movements", + "primitive", + "7\\s*136\\s*815" + ], + "criticality": "high", + "semantic_tags": [ + "inventory_margin_ranking", + "period_expansion", + "carryover" + ] + }, + { + "step_id": "step_05_account_41_not_01", + "title": "User corrects account family to 41 not fixed assets", + "question": "анализ по 41 счету а не 01", + "allowed_reply_types": [ + "partial_coverage", + "factual_with_explanation", + "factual" + ], + "required_answer_patterns_any": [ + "41", + "товар|номенклатур|закупоч|себестоим", + "не 01|01" + ], + "forbidden_answer_patterns": [ + "амортизац", + "основн[а-я]+ средств", + "ОС как объект", + "Сбербанк", + "банк", + "runtime_", + "planner_", + "query_movements", + "primitive" + ], + "criticality": "critical", + "semantic_tags": [ + "inventory_margin_ranking", + "account_family_guard", + "no_fixed_assets" + ] + } + ], + "acceptance": { + "min_score": 80, + "max_unresolved_p0": 0, + "require_all_critical_steps_pass": true + } +} diff --git a/llm_normalizer/backend/dist/services/address_runtime/inventoryReplyBuilders.js b/llm_normalizer/backend/dist/services/address_runtime/inventoryReplyBuilders.js index afa1a40..ecfb6cd 100644 --- a/llm_normalizer/backend/dist/services/address_runtime/inventoryReplyBuilders.js +++ b/llm_normalizer/backend/dist/services/address_runtime/inventoryReplyBuilders.js @@ -81,6 +81,13 @@ function inventoryProfitabilityPeriodLabel(options, deps) { const asOfDate = typeof options.asOfDate === "string" && options.asOfDate.trim().length > 0 ? options.asOfDate : null; return asOfDate ? `до ${deps.formatDateRu(asOfDate)}` : "по доступной выборке"; } +function asksForInventoryCostBaseRows(userMessage) { + const text = String(userMessage ?? "").toLowerCase(); + if (!/(?:покажи|показать|выведи|вывести|дай|дать|раскрой|раскрыть|строк|строки|строку|баз)/iu.test(text)) { + return false; + } + return /(?:себестоимостн|себестоимост|себестоим|закупочн|закупк|90\.02|\b41\b|баз)/iu.test(text); +} function inventoryRowItemLabel(row, deps) { return deps.summarizeInventoryTraceRows([row]).item; } @@ -459,7 +466,12 @@ function composeInventoryReply(intent, rows, options, deps) { const totalCostProxy = entries.reduce((sum, entry) => sum + entry.costProxy, 0); const totalSpread = totalRevenue - totalCostProxy; if (confirmedEntries.length === 0) { - const lines = [`За период ${periodLabel} рейтинг прибыльности номенклатуры построить нельзя.`]; + const costBaseRowsRequested = asksForInventoryCostBaseRows(options.userMessage); + const lines = [ + costBaseRowsRequested && purchasesWithoutSales.length === 0 + ? `За период ${periodLabel} подтвержденных строк себестоимостной базы по реализованной номенклатуре не найдено.` + : `За период ${periodLabel} рейтинг прибыльности номенклатуры построить нельзя.` + ]; const findings = []; if (salesWithoutCost.length > 0) { const salesCount = deps.formatNumberWithDots(salesWithoutCost.length); @@ -470,6 +482,9 @@ function composeInventoryReply(intent, rows, options, deps) { : "Подтвержденной себестоимости реализации по этим позициям не найдено."); findings.push("Поэтому валовую прибыль и маржинальность честно посчитать нельзя."); } + if (costBaseRowsRequested && purchasesWithoutSales.length === 0) { + findings.push("Строк себестоимости реализации / себестоимостной базы для показа нет."); + } if (purchasesWithoutSales.length > 0) { const purchaseCount = deps.formatNumberWithDots(purchasesWithoutSales.length); const purchaseItemPhrase = purchasesWithoutSales.length === 1 ? "1 позиции" : `${purchaseCount} позициям`; @@ -493,7 +508,7 @@ function composeInventoryReply(intent, rows, options, deps) { (0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Что можно сделать дальше:", nextActions); (0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Граница ответа:", [ "Прибыльность номенклатуры считаю только когда есть реализация и подтвержденная себестоимость реализации.", - "Это не чистая прибыль компании и не замена закрытию месяца." + "Это не показатель чистой прибыли и не замена закрытию месяца." ]); return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(entries.length > 0 ? "medium" : "weak", false)); } @@ -508,7 +523,7 @@ function composeInventoryReply(intent, rows, options, deps) { (0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Низкая или отрицательная валовая маржинальность:", lowMargin.map((entry, index) => formatInventoryMarginRankingLine(entry, index, deps))); } const boundaryLines = [ - "Это управленческий расчет валовой маржинальности по реализации и доступной себестоимостной базе, не чистая прибыль компании.", + "Это управленческий расчет валовой маржинальности по реализации и доступной себестоимостной базе, не показатель чистой прибыли.", "Для строгого бухгалтерского расчета нужны проводки 90.01 / 90.02 и закрытие себестоимости; этот ответ не подменяет закрытие месяца." ]; if (salesWithoutCost.length > 0) { diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponsePolicy.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponsePolicy.js index 520604d..94b567a 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponsePolicy.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponsePolicy.js @@ -350,6 +350,24 @@ function hasExactBankOperationsAddressReply(input, entryPoint) { routeMode === "exact" || hasFullConfirmedTruth(input)); } +function hasInventoryMarginRankingAddressReply(input, entryPoint) { + if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) { + return false; + } + if (!toNonEmptyString(input.currentReply)) { + return false; + } + if (hasMetadataDiscoveryPriority(input, entryPoint)) { + return false; + } + const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent); + const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe); + const capabilityId = toNonEmptyString(input.addressRuntimeMeta?.capability_id) ?? + toNonEmptyString(input.addressRuntimeMeta?.capability_contract_id); + return Boolean(detectedIntent === "inventory_margin_ranking_for_nomenclature" || + selectedRecipe === "address_inventory_margin_ranking_for_nomenclature_v1" || + capabilityId === "inventory_inventory_margin_ranking_for_nomenclature"); +} function hasValueFlowActionConflictWithDiscoveryTurnMeaning(input, entryPoint) { if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) { return false; @@ -621,6 +639,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) { const staleMetadataDiscoveryFallbackAgainstExactAddressReply = hasStaleMetadataDiscoveryFallbackAgainstExactAddressReply(input, entryPoint); const exactValueFlowReplyForBusinessOverviewDirectMoneyNeed = hasExactValueFlowReplyForBusinessOverviewDirectMoneyNeed(input, entryPoint); const exactBankOperationsAddressReply = hasExactBankOperationsAddressReply(input, entryPoint); + const inventoryMarginRankingAddressReply = hasInventoryMarginRankingAddressReply(input, entryPoint); const openScopeValueFlowDiscoveryPriority = hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint); const metadataDiscoveryPriority = hasMetadataDiscoveryPriority(input, entryPoint); const valueFlowActionConflictWithDiscoveryTurnMeaning = hasValueFlowActionConflictWithDiscoveryTurnMeaning(input, entryPoint); @@ -685,6 +704,9 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) { if (exactBankOperationsAddressReply) { pushReason(reasonCodes, "mcp_discovery_response_policy_keep_exact_bank_operations_address_reply"); } + if (inventoryMarginRankingAddressReply) { + pushReason(reasonCodes, "mcp_discovery_response_policy_keep_inventory_margin_ranking_address_reply"); + } if (deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") { pushReason(reasonCodes, "mcp_discovery_response_policy_keep_broad_business_summary_over_clarification_candidate"); } @@ -715,6 +737,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) { !staleMetadataDiscoveryFallbackAgainstExactAddressReply && !exactValueFlowReplyForBusinessOverviewDirectMoneyNeed && !exactBankOperationsAddressReply && + !inventoryMarginRankingAddressReply && !(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") && ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) && candidate.eligible_for_future_hot_runtime && diff --git a/llm_normalizer/backend/dist/services/assistantRoutePolicy.js b/llm_normalizer/backend/dist/services/assistantRoutePolicy.js index 2e5d7be..e2009c0 100644 --- a/llm_normalizer/backend/dist/services/assistantRoutePolicy.js +++ b/llm_normalizer/backend/dist/services/assistantRoutePolicy.js @@ -315,6 +315,21 @@ function createAssistantRoutePolicy(deps) { const hasTemporalCue = /(?:на\s+эту\s+же\s+дат[ауеы]|на\s+тот\s+же\s+период|за\s+этот\s+же\s+период|за\s+этот\s+период|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр|\b(?:19|20)\d{2}\b)/iu.test(normalized); return hasRequestCue && hasTemporalCue; } + function hasInventoryMarginRankingContinuationSignal(text) { + const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase()).replace(/ё/g, "е"); + if (!normalized) { + return false; + } + const wantsFoundRows = /(?:покажи|показать|выведи|дай|раскрой|show|list)/iu.test(normalized) && + /(?:найденн|строк|реализац|себестоимостн|баз)/iu.test(normalized) && + /(?:себестоимостн|реализац|марж|прибыл|номенклатур)/iu.test(normalized); + const account41Not01 = /\b41(?:[.,]\d{1,2})?\b/iu.test(normalized) && + /\b01(?:[.,]\d{1,2})?\b/iu.test(normalized) && + /(?:\bне\b|вместо|а\s+не|not|instead)/iu.test(normalized); + const periodExpansion = /(?:расширь|расширить|возьми|давай|покажи|за|на|до|весь|год|квартал|месяц|expand|period)/iu.test(normalized) && + /(?:январ|феврал|март|апрел|ма[йяе]|июн|июл|август|сентябр|октябр|ноябр|декабр|\b(?:19|20)\d{2}\b)/iu.test(normalized); + return wantsFoundRows || account41Not01 || periodExpansion; + } function hasOrganizationClarificationTextCue(text) { const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase()); if (!normalized) { @@ -481,6 +496,7 @@ function createAssistantRoutePolicy(deps) { hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) || hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage); const followupPreviousIntent = toNonEmptyString(followupContext?.previous_intent); + const followupRootIntent = toNonEmptyString(followupContext?.root_intent); const followupPreviousFilters = followupContext?.previous_filters && typeof followupContext.previous_filters === "object" ? followupContext.previous_filters : null; @@ -515,6 +531,16 @@ function createAssistantRoutePolicy(deps) { hasShortInventoryObjectFollowupSignal(repairedRawUserMessage) || hasShortInventoryObjectFollowupSignal(effectiveAddressUserMessage) || hasShortInventoryObjectFollowupSignal(repairedEffectiveAddressUserMessage))); + const protectedInventoryMarginRankingFollowup = Boolean(followupContext && + (followupPreviousIntent === "inventory_margin_ranking_for_nomenclature" || + followupRootIntent === "inventory_margin_ranking_for_nomenclature") && + !dataScopeMetaQuery && + !capabilityMetaQuery && + !dangerOrCoercionSignal && + (hasInventoryMarginRankingContinuationSignal(rawUserMessage) || + hasInventoryMarginRankingContinuationSignal(repairedRawUserMessage) || + hasInventoryMarginRankingContinuationSignal(effectiveAddressUserMessage) || + hasInventoryMarginRankingContinuationSignal(repairedEffectiveAddressUserMessage))); const organizationClarificationContinuationDetected = Boolean((followupContext || continuitySnapshot.hasGroundedAddressContext) && lastOrganizationClarificationDebug && explicitOrganizationClarificationSelection && @@ -571,6 +597,7 @@ function createAssistantRoutePolicy(deps) { !baseToolGatePreservesAddressLane && !effectiveGroundedValueFlowFollowupContextDetected && !protectedInventoryShortFollowup && + !protectedInventoryMarginRankingFollowup && !organizationClarificationContinuationDetected && !routeCandidateOrganizationClarificationDetected); const lastAddressAssistantDebug = sessionItems @@ -1080,6 +1107,7 @@ function createAssistantRoutePolicy(deps) { hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) || hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) || hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage) || + protectedInventoryMarginRankingFollowup || inventoryRootRestatementFollowupDetected); const deepAnalysisPreferenceDetected = Boolean(hasDeepAnalysisPreferenceSignal(rawUserMessage) || hasDeepAnalysisPreferenceSignal(repairedRawUserMessage) || @@ -1116,7 +1144,9 @@ function createAssistantRoutePolicy(deps) { resolvedIntentResolution.intent === "unknown" && (!llmContractIntent || llmContractIntent === "unknown")); const exactAddressIntentProtectedFromSemanticDeepHint = laneProtectionArbitration.exactAddressIntentProtectedFromSemanticDeepHint; - const protectAddressLaneFromFallback = Boolean(laneProtectionArbitration.protectAddressLaneFromFallback || customerValueRankingAddressSignal); + const protectAddressLaneFromFallback = Boolean(laneProtectionArbitration.protectAddressLaneFromFallback || + customerValueRankingAddressSignal || + protectedInventoryMarginRankingFollowup); const vatExplainFollowupSignal = Boolean(followupContext && toNonEmptyString(followupContext.previous_intent) === "vat_payable_forecast" && /(?:\u043f\u043e\u0447\u0435\u043c\u0443|why).*(?:\u043f\u0440\u043e\u0433\u043d\u043e\u0437|forecast).*(?:\u0443\u043f\u043b\u0430\u0442|payable|\b0\b)/iu.test(compactWhitespace(`${repairedRawUserMessage} ${repairedEffectiveAddressUserMessage}`))); @@ -1196,7 +1226,7 @@ function createAssistantRoutePolicy(deps) { let toolGateDecision = String(baseToolGate?.decision ?? "skip_address_lane"); let toolGateReason = String(baseToolGate?.reason ?? "no_address_signal_after_l0"); const semanticAddressLaneRecovery = Boolean(!runAddressLane && - supportedAddressRouteCandidateDetected && + (supportedAddressRouteCandidateDetected || protectedInventoryMarginRankingFollowup) && !deepAnalysisPreferenceDetected && !unsupportedAddressIntentFallbackToDeep && !deepAnalysisSignalFallbackToDeep && @@ -1205,9 +1235,11 @@ function createAssistantRoutePolicy(deps) { if (semanticAddressLaneRecovery) { runAddressLane = true; toolGateDecision = "run_address_lane"; - toolGateReason = resolvedIntentResolution.intent !== "unknown" || llmContractIntent - ? "address_intent_resolver_detected" - : "address_signal_detected"; + toolGateReason = protectedInventoryMarginRankingFollowup + ? "followup_context_detected" + : resolvedIntentResolution.intent !== "unknown" || llmContractIntent + ? "address_intent_resolver_detected" + : "address_signal_detected"; } if (unsupportedAddressIntentFallbackToDeep) { runAddressLane = false; diff --git a/llm_normalizer/backend/src/services/address_runtime/inventoryReplyBuilders.ts b/llm_normalizer/backend/src/services/address_runtime/inventoryReplyBuilders.ts index e1ff46a..6c8ea84 100644 --- a/llm_normalizer/backend/src/services/address_runtime/inventoryReplyBuilders.ts +++ b/llm_normalizer/backend/src/services/address_runtime/inventoryReplyBuilders.ts @@ -162,6 +162,14 @@ function inventoryProfitabilityPeriodLabel(options: InventoryComposeOptions, dep return asOfDate ? `до ${deps.formatDateRu(asOfDate)}` : "по доступной выборке"; } +function asksForInventoryCostBaseRows(userMessage: string | null | undefined): boolean { + const text = String(userMessage ?? "").toLowerCase(); + if (!/(?:покажи|показать|выведи|вывести|дай|дать|раскрой|раскрыть|строк|строки|строку|баз)/iu.test(text)) { + return false; + } + return /(?:себестоимостн|себестоимост|себестоим|закупочн|закупк|90\.02|\b41\b|баз)/iu.test(text); +} + interface InventoryMarginRankingEntry { item: string; revenue: number; @@ -631,7 +639,12 @@ export function composeInventoryReply( const totalCostProxy = entries.reduce((sum, entry) => sum + entry.costProxy, 0); const totalSpread = totalRevenue - totalCostProxy; if (confirmedEntries.length === 0) { - const lines: string[] = [`За период ${periodLabel} рейтинг прибыльности номенклатуры построить нельзя.`]; + const costBaseRowsRequested = asksForInventoryCostBaseRows(options.userMessage); + const lines: string[] = [ + costBaseRowsRequested && purchasesWithoutSales.length === 0 + ? `За период ${periodLabel} подтвержденных строк себестоимостной базы по реализованной номенклатуре не найдено.` + : `За период ${periodLabel} рейтинг прибыльности номенклатуры построить нельзя.` + ]; const findings: string[] = []; if (salesWithoutCost.length > 0) { const salesCount = deps.formatNumberWithDots(salesWithoutCost.length); @@ -647,6 +660,9 @@ export function composeInventoryReply( ); findings.push("Поэтому валовую прибыль и маржинальность честно посчитать нельзя."); } + if (costBaseRowsRequested && purchasesWithoutSales.length === 0) { + findings.push("Строк себестоимости реализации / себестоимостной базы для показа нет."); + } if (purchasesWithoutSales.length > 0) { const purchaseCount = deps.formatNumberWithDots(purchasesWithoutSales.length); const purchaseItemPhrase = @@ -679,7 +695,7 @@ export function composeInventoryReply( appendInventoryBulletSection(lines, "Что можно сделать дальше:", nextActions); appendInventoryBulletSection(lines, "Граница ответа:", [ "Прибыльность номенклатуры считаю только когда есть реализация и подтвержденная себестоимость реализации.", - "Это не чистая прибыль компании и не замена закрытию месяца." + "Это не показатель чистой прибыли и не замена закрытию месяца." ]); return buildFactualSummaryReply(lines, buildConfirmedBalanceSemantics(entries.length > 0 ? "medium" : "weak", false)); } @@ -709,7 +725,7 @@ export function composeInventoryReply( } const boundaryLines = [ - "Это управленческий расчет валовой маржинальности по реализации и доступной себестоимостной базе, не чистая прибыль компании.", + "Это управленческий расчет валовой маржинальности по реализации и доступной себестоимостной базе, не показатель чистой прибыли.", "Для строгого бухгалтерского расчета нужны проводки 90.01 / 90.02 и закрытие себестоимости; этот ответ не подменяет закрытие месяца." ]; if (salesWithoutCost.length > 0) { diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponsePolicy.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponsePolicy.ts index 7f513b9..5e6e2d3 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponsePolicy.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponsePolicy.ts @@ -496,6 +496,31 @@ function hasExactBankOperationsAddressReply( ); } +function hasInventoryMarginRankingAddressReply( + input: ApplyAssistantMcpDiscoveryResponsePolicyInput, + entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null +): boolean { + if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) { + return false; + } + if (!toNonEmptyString(input.currentReply)) { + return false; + } + if (hasMetadataDiscoveryPriority(input, entryPoint)) { + return false; + } + const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent); + const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe); + const capabilityId = + toNonEmptyString(input.addressRuntimeMeta?.capability_id) ?? + toNonEmptyString(input.addressRuntimeMeta?.capability_contract_id); + return Boolean( + detectedIntent === "inventory_margin_ranking_for_nomenclature" || + selectedRecipe === "address_inventory_margin_ranking_for_nomenclature_v1" || + capabilityId === "inventory_inventory_margin_ranking_for_nomenclature" + ); +} + function hasValueFlowActionConflictWithDiscoveryTurnMeaning( input: ApplyAssistantMcpDiscoveryResponsePolicyInput, entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null @@ -834,6 +859,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy( entryPoint ); const exactBankOperationsAddressReply = hasExactBankOperationsAddressReply(input, entryPoint); + const inventoryMarginRankingAddressReply = hasInventoryMarginRankingAddressReply(input, entryPoint); const openScopeValueFlowDiscoveryPriority = hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint); const metadataDiscoveryPriority = hasMetadataDiscoveryPriority(input, entryPoint); const valueFlowActionConflictWithDiscoveryTurnMeaning = hasValueFlowActionConflictWithDiscoveryTurnMeaning( @@ -917,6 +943,9 @@ export function applyAssistantMcpDiscoveryResponsePolicy( if (exactBankOperationsAddressReply) { pushReason(reasonCodes, "mcp_discovery_response_policy_keep_exact_bank_operations_address_reply"); } + if (inventoryMarginRankingAddressReply) { + pushReason(reasonCodes, "mcp_discovery_response_policy_keep_inventory_margin_ranking_address_reply"); + } if (deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") { pushReason( reasonCodes, @@ -952,6 +981,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy( !staleMetadataDiscoveryFallbackAgainstExactAddressReply && !exactValueFlowReplyForBusinessOverviewDirectMoneyNeed && !exactBankOperationsAddressReply && + !inventoryMarginRankingAddressReply && !(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") && ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) && candidate.eligible_for_future_hot_runtime && diff --git a/llm_normalizer/backend/src/services/assistantRoutePolicy.ts b/llm_normalizer/backend/src/services/assistantRoutePolicy.ts index e568ccb..9a45e84 100644 --- a/llm_normalizer/backend/src/services/assistantRoutePolicy.ts +++ b/llm_normalizer/backend/src/services/assistantRoutePolicy.ts @@ -397,6 +397,24 @@ export function createAssistantRoutePolicy(deps) { const hasTemporalCue = /(?:на\s+эту\s+же\s+дат[ауеы]|на\s+тот\s+же\s+период|за\s+этот\s+же\s+период|за\s+этот\s+период|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр|\b(?:19|20)\d{2}\b)/iu.test(normalized); return hasRequestCue && hasTemporalCue; } + function hasInventoryMarginRankingContinuationSignal(text) { + const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase()).replace(/ё/g, "е"); + if (!normalized) { + return false; + } + const wantsFoundRows = + /(?:покажи|показать|выведи|дай|раскрой|show|list)/iu.test(normalized) && + /(?:найденн|строк|реализац|себестоимостн|баз)/iu.test(normalized) && + /(?:себестоимостн|реализац|марж|прибыл|номенклатур)/iu.test(normalized); + const account41Not01 = + /\b41(?:[.,]\d{1,2})?\b/iu.test(normalized) && + /\b01(?:[.,]\d{1,2})?\b/iu.test(normalized) && + /(?:\bне\b|вместо|а\s+не|not|instead)/iu.test(normalized); + const periodExpansion = + /(?:расширь|расширить|возьми|давай|покажи|за|на|до|весь|год|квартал|месяц|expand|period)/iu.test(normalized) && + /(?:январ|феврал|март|апрел|ма[йяе]|июн|июл|август|сентябр|октябр|ноябр|декабр|\b(?:19|20)\d{2}\b)/iu.test(normalized); + return wantsFoundRows || account41Not01 || periodExpansion; + } function hasOrganizationClarificationTextCue(text) { const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase()); if (!normalized) { @@ -565,6 +583,7 @@ export function createAssistantRoutePolicy(deps) { hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) || hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage); const followupPreviousIntent = toNonEmptyString(followupContext?.previous_intent); + const followupRootIntent = toNonEmptyString(followupContext?.root_intent); const followupPreviousFilters = followupContext?.previous_filters && typeof followupContext.previous_filters === "object" ? followupContext.previous_filters : null; @@ -599,6 +618,16 @@ export function createAssistantRoutePolicy(deps) { hasShortInventoryObjectFollowupSignal(repairedRawUserMessage) || hasShortInventoryObjectFollowupSignal(effectiveAddressUserMessage) || hasShortInventoryObjectFollowupSignal(repairedEffectiveAddressUserMessage))); + const protectedInventoryMarginRankingFollowup = Boolean(followupContext && + (followupPreviousIntent === "inventory_margin_ranking_for_nomenclature" || + followupRootIntent === "inventory_margin_ranking_for_nomenclature") && + !dataScopeMetaQuery && + !capabilityMetaQuery && + !dangerOrCoercionSignal && + (hasInventoryMarginRankingContinuationSignal(rawUserMessage) || + hasInventoryMarginRankingContinuationSignal(repairedRawUserMessage) || + hasInventoryMarginRankingContinuationSignal(effectiveAddressUserMessage) || + hasInventoryMarginRankingContinuationSignal(repairedEffectiveAddressUserMessage))); const organizationClarificationContinuationDetected = Boolean((followupContext || continuitySnapshot.hasGroundedAddressContext) && lastOrganizationClarificationDebug && explicitOrganizationClarificationSelection && @@ -656,6 +685,7 @@ export function createAssistantRoutePolicy(deps) { !baseToolGatePreservesAddressLane && !effectiveGroundedValueFlowFollowupContextDetected && !protectedInventoryShortFollowup && + !protectedInventoryMarginRankingFollowup && !organizationClarificationContinuationDetected && !routeCandidateOrganizationClarificationDetected); const lastAddressAssistantDebug = sessionItems @@ -1167,6 +1197,7 @@ export function createAssistantRoutePolicy(deps) { hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) || hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) || hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage) || + protectedInventoryMarginRankingFollowup || inventoryRootRestatementFollowupDetected); const deepAnalysisPreferenceDetected = Boolean(hasDeepAnalysisPreferenceSignal(rawUserMessage) || hasDeepAnalysisPreferenceSignal(repairedRawUserMessage) || @@ -1204,7 +1235,9 @@ export function createAssistantRoutePolicy(deps) { (!llmContractIntent || llmContractIntent === "unknown")); const exactAddressIntentProtectedFromSemanticDeepHint = laneProtectionArbitration.exactAddressIntentProtectedFromSemanticDeepHint; const protectAddressLaneFromFallback = Boolean( - laneProtectionArbitration.protectAddressLaneFromFallback || customerValueRankingAddressSignal + laneProtectionArbitration.protectAddressLaneFromFallback || + customerValueRankingAddressSignal || + protectedInventoryMarginRankingFollowup ); const vatExplainFollowupSignal = Boolean(followupContext && toNonEmptyString(followupContext.previous_intent) === "vat_payable_forecast" && @@ -1285,7 +1318,7 @@ export function createAssistantRoutePolicy(deps) { let toolGateDecision = String(baseToolGate?.decision ?? "skip_address_lane"); let toolGateReason = String(baseToolGate?.reason ?? "no_address_signal_after_l0"); const semanticAddressLaneRecovery = Boolean(!runAddressLane && - supportedAddressRouteCandidateDetected && + (supportedAddressRouteCandidateDetected || protectedInventoryMarginRankingFollowup) && !deepAnalysisPreferenceDetected && !unsupportedAddressIntentFallbackToDeep && !deepAnalysisSignalFallbackToDeep && @@ -1294,7 +1327,9 @@ export function createAssistantRoutePolicy(deps) { if (semanticAddressLaneRecovery) { runAddressLane = true; toolGateDecision = "run_address_lane"; - toolGateReason = resolvedIntentResolution.intent !== "unknown" || llmContractIntent + toolGateReason = protectedInventoryMarginRankingFollowup + ? "followup_context_detected" + : resolvedIntentResolution.intent !== "unknown" || llmContractIntent ? "address_intent_resolver_detected" : "address_signal_detected"; } diff --git a/llm_normalizer/backend/tests/addressInventoryProfitabilitySelectedObjectRegression.test.ts b/llm_normalizer/backend/tests/addressInventoryProfitabilitySelectedObjectRegression.test.ts index 8131bd3..8f432ec 100644 --- a/llm_normalizer/backend/tests/addressInventoryProfitabilitySelectedObjectRegression.test.ts +++ b/llm_normalizer/backend/tests/addressInventoryProfitabilitySelectedObjectRegression.test.ts @@ -387,7 +387,7 @@ describe("inventory profitability selected-object regressions", () => { expect(reply).toContain("\u041d\u0438\u0437\u043a\u0430\u044f \u0438\u043b\u0438 \u043e\u0442\u0440\u0438\u0446\u0430\u0442\u0435\u043b\u044c\u043d\u0430\u044f"); expect(reply).toContain("Item A"); expect(reply).toContain("Item B"); - expect(reply).toContain("\u043d\u0435 \u0447\u0438\u0441\u0442\u0430\u044f \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438"); + expect(reply).toContain("\u043d\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u0435\u043b\u044c \u0447\u0438\u0441\u0442\u043e\u0439 \u043f\u0440\u0438\u0431\u044b\u043b\u0438"); expect(reply).not.toContain("\u041e\u0421"); expect(reply).not.toContain("\u0430\u043c\u043e\u0440\u0442\u0438\u0437"); expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1); diff --git a/llm_normalizer/backend/tests/addressReplyBuildersRegression.test.ts b/llm_normalizer/backend/tests/addressReplyBuildersRegression.test.ts index f434dec..0065cd3 100644 --- a/llm_normalizer/backend/tests/addressReplyBuildersRegression.test.ts +++ b/llm_normalizer/backend/tests/addressReplyBuildersRegression.test.ts @@ -329,9 +329,9 @@ describe("address reply builders regressions", () => { expect(firstLine).toContain("7.271,20"); expect(firstLine).toContain("Авант мебель"); expect(firstLine).not.toContain("3.677.454,14"); - expect(result.text).toContain("встречных остатков"); + expect(result.text).toContain("Встречная часть"); expect(result.text).toContain("Комитет государственных услуг г. Москвы"); - expect(result.text).toContain("Финансовое обеспечение заявки"); + expect(result.text).not.toContain("Финансовое обеспечение заявки"); expect(result.text).not.toContain("договор/аналитика: ООО \\Альтернатива Плюс\\"); }); @@ -380,7 +380,7 @@ describe("address reply builders regressions", () => { expect(firstLine).toContain("9.612.904,90"); expect(firstLine).toContain("Департамент капитального ремонта города Москвы."); expect(firstLine).not.toContain("13.290.359,04"); - expect(result.text).toContain("встречных остатков"); + expect(result.text).toContain("Встречная часть"); expect(result.text).toContain("Комитет государственных услуг г. Москвы"); }); @@ -525,4 +525,58 @@ describe("address reply builders regressions", () => { expect(result?.text).toContain("Общее количество не свожу в один управленческий показатель"); expect(result?.text).toContain("Следующий шаг: могу раскрыть полный список"); }); + + it("answers cost-base evidence follow-up directly when margin ranking has sales without confirmed cost", () => { + const result = composeInventoryReply( + "inventory_margin_ranking_for_nomenclature", + [ + { + amount: 120000, + quantity: 2, + item: "Рабочая станция", + period: "2020-05-20", + registrator: "Реализация" + } as any + ], + { + userMessage: "покажи найденные строки себестоимостной базы", + periodFrom: "2020-05-01", + periodTo: "2020-05-31" + }, + { + resolvePayablesAsOfDate: () => "2020-05-31", + buildInventoryOnHandAggregate: () => [], + uniqueStrings: (values: string[]) => Array.from(new Set(values)), + formatDateRu: (value: string) => value, + formatNumberWithDots: (value: number, fractionDigits = 0) => value.toFixed(fractionDigits), + formatMoneyRub: (value: number) => `${value} ₽`, + isInventoryPurchaseMovement: () => false, + summarizeInventoryTraceRows: (rows: any[]) => ({ + item: rows[0]?.item ?? null, + warehouses: [], + organizations: [], + counterparties: [], + documents: [], + firstPeriod: null, + lastPeriod: null, + totalAmount: 0 + }), + formatInventoryTraceRows: () => [], + hasInventoryPurchaseDateActionFocus: () => false, + inventoryTraceDateLabel: () => "", + extractInventoryCounterpartyCandidates: () => [], + buildInventoryAgingByItemAggregate: () => [], + formatInventoryAgingRows: () => [], + isInventorySaleMovement: () => true + } + ); + + expect(result?.text.split("\n")[0]).toContain( + "подтвержденных строк себестоимостной базы по реализованной номенклатуре не найдено" + ); + expect(result?.text).toContain("Есть реализация по 1 номенклатурной позиции"); + expect(result?.text).toContain("Строк себестоимости реализации / себестоимостной базы для показа нет"); + expect(result?.text).not.toContain("входящих денежных поступлений"); + expect(result?.text).not.toContain("амортизац"); + }); }); diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryResponsePolicy.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryResponsePolicy.test.ts index a210490..557b6b1 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryResponsePolicy.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryResponsePolicy.test.ts @@ -135,6 +135,42 @@ describe("assistant MCP discovery response policy", () => { expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_not_discovery_ready_address_candidate"); }); + it("keeps inventory margin-ranking exact reply over stale value-flow discovery candidate", () => { + const result = applyAssistantMcpDiscoveryResponsePolicy({ + currentReply: + "За период 2017 рейтинг прибыльности номенклатуры построить нельзя: нет подтвержденной себестоимости реализации.", + currentReplySource: "address_query_runtime_v1", + currentReplyType: "partial_coverage", + addressRuntimeMeta: { + detected_intent: "inventory_margin_ranking_for_nomenclature", + selected_recipe: "address_inventory_margin_ranking_for_nomenclature_v1", + capability_id: "inventory_inventory_margin_ranking_for_nomenclature", + assistant_mcp_discovery_entry_point_v1: entryPoint({ + turn_input: { + adapter_status: "ready", + should_run_discovery: true, + turn_meaning_ref: { + asked_domain_family: "counterparty_value", + asked_action_family: "turnover" + }, + data_need_graph: { + business_fact_family: "value_flow", + clarification_gaps: [] + } + } + }) + } + }); + + expect(result.applied).toBe(false); + expect(result.decision).toBe("keep_current_reply"); + expect(result.reply_text).toContain("рейтинг прибыльности номенклатуры"); + expect(result.reason_codes).toContain( + "mcp_discovery_response_policy_keep_inventory_margin_ranking_address_reply" + ); + expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_candidate_applied"); + }); + it("lets a grounded business overview candidate override a semantically wrong exact address recipe", () => { const result = applyAssistantMcpDiscoveryResponsePolicy({ currentReply: "Supplier and stock overlap was confirmed for 2020.", diff --git a/llm_normalizer/backend/tests/assistantRoutePolicy.test.ts b/llm_normalizer/backend/tests/assistantRoutePolicy.test.ts index 3c51aba..8f2f3ee 100644 --- a/llm_normalizer/backend/tests/assistantRoutePolicy.test.ts +++ b/llm_normalizer/backend/tests/assistantRoutePolicy.test.ts @@ -183,6 +183,75 @@ describe("assistantRoutePolicy", () => { expect(decision.livingMode).toBe("address_data"); }); + it("keeps margin-ranking found-rows follow-up in address lane", () => { + const policy = buildPolicy(); + + const decision = policy.resolveAssistantOrchestrationDecision({ + rawUserMessage: "покажи найденные строки себестоимостной базы", + effectiveAddressUserMessage: "покажи найденные строки себестоимостной базы", + followupContext: { + previous_intent: "inventory_margin_ranking_for_nomenclature", + root_intent: "inventory_margin_ranking_for_nomenclature", + previous_filters: { + organization: "ООО Альтернатива Плюс", + period_from: "2020-05-01", + period_to: "2020-05-31" + } + }, + llmPreDecomposeMeta: { + applied: false, + predecomposeContract: { + mode: "unsupported", + mode_confidence: "low", + intent: "unknown", + intent_confidence: "low" + } + }, + useMock: false + }); + + expect(decision.runAddressLane).toBe(true); + expect(decision.toolGateDecision).toBe("run_address_lane"); + expect(decision.toolGateReason).toBe("followup_context_detected"); + expect(decision.livingMode).toBe("address_data"); + expect(decision.orchestrationContract?.hard_meta_mode).toBe(null); + expect(decision.orchestrationContract?.final_decision?.tool_gate_reason).toBe("followup_context_detected"); + }); + + it("keeps margin-ranking period expansion follow-up in address lane", () => { + const policy = buildPolicy(); + + const decision = policy.resolveAssistantOrchestrationDecision({ + rawUserMessage: "расширь до 2017 года", + effectiveAddressUserMessage: "расширь до 2017 года", + followupContext: { + previous_intent: "inventory_margin_ranking_for_nomenclature", + root_intent: "inventory_margin_ranking_for_nomenclature", + previous_filters: { + organization: "ООО Альтернатива Плюс", + period_from: "2020-05-01", + period_to: "2020-05-31" + } + }, + llmPreDecomposeMeta: { + applied: false, + predecomposeContract: { + mode: "unsupported", + mode_confidence: "low", + intent: "unknown", + intent_confidence: "low" + } + }, + useMock: false + }); + + expect(decision.runAddressLane).toBe(true); + expect(decision.toolGateDecision).toBe("run_address_lane"); + expect(decision.toolGateReason).toBe("followup_context_detected"); + expect(decision.livingMode).toBe("address_data"); + expect(decision.orchestrationContract?.hard_meta_mode).toBe(null); + }); + it("does not let deep session continuation override an exact VAT period route", () => { const policy = buildPolicy({ detectAddressQuestionMode: () => ({ mode: "address_query", confidence: "high" }), diff --git a/llm_normalizer/data/autorun_generators/history.json b/llm_normalizer/data/autorun_generators/history.json index bc43647..2258005 100644 --- a/llm_normalizer/data/autorun_generators/history.json +++ b/llm_normalizer/data/autorun_generators/history.json @@ -1,4 +1,53 @@ [ + { + "generation_id": "gen-ag05231107-464a28", + "created_at": "2026-05-23T11:07:35+00:00", + "mode": "saved_user_sessions", + "title": "AGENT | Inventory margin ranking limited answer pack", + "count": 5, + "domain": "inventory_margin_ranking", + "questions": [ + "Какая номеклатура товара реализована с высокой прибылью какая с низкой", + "май 2020", + "покажи найденные строки себестоимостной базы", + "расширь до 2017 года", + "анализ по 41 счету а не 01" + ], + "generated_by": "codex_agent", + "saved_case_set_file": "assistant_autogen_saved_user_sessions_20260523110735_gen-ag05231107-464a28.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_20260523110735_gen-ag05231107-464a28.json", + "saved_case_set_kind": "agent_semantic_scenario", + "agent_run": true, + "agent_focus": "Targeted live replay for nomenclature high/low profit questions: ask period when missing, stay in inventory/sales/cost domain, give useful limited answer when cost evidence is insufficient, and avoid fixed-assets/bank leakage.", + "architecture_phase": "turnaround_11", + "source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\agent_inventory_margin_ranking_20260523.json", + "scenario_id": "agent_inventory_margin_ranking_20260523", + "semantic_tags": [ + "account_family_guard", + "carryover", + "domain_purity", + "evidence_followup", + "inventory_margin_ranking", + "limited_answer", + "needs_period", + "no_fixed_assets", + "period_expansion", + "period_followup" + ], + "validation_status": "accepted_live_replay", + "validated_run_dir": "artifacts\\domain_runs\\agent_inventory_margin_ranking_live5", + "saved_after_validated_replay": true + } + }, { "generation_id": "gen-ag05231011-cec910", "created_at": "2026-05-23T10:11:40+00:00", diff --git a/llm_normalizer/data/autorun_generators/saved_sessions/assistant_saved_session_20260523110735_gen-ag05231107-464a28.json b/llm_normalizer/data/autorun_generators/saved_sessions/assistant_saved_session_20260523110735_gen-ag05231107-464a28.json new file mode 100644 index 0000000..40af82d --- /dev/null +++ b/llm_normalizer/data/autorun_generators/saved_sessions/assistant_saved_session_20260523110735_gen-ag05231107-464a28.json @@ -0,0 +1,145 @@ +{ + "saved_at": "2026-05-23T11:07:35+00:00", + "generation_id": "gen-ag05231107-464a28", + "mode": "saved_user_sessions", + "title": "AGENT | Inventory margin ranking limited answer pack", + "agent_run": true, + "questions": [ + "Какая номеклатура товара реализована с высокой прибылью какая с низкой", + "май 2020", + "покажи найденные строки себестоимостной базы", + "расширь до 2017 года", + "анализ по 41 счету а не 01" + ], + "metadata": { + "assistant_prompt_version": null, + "decomposition_prompt_version": null, + "prompt_fingerprint": null, + "agent_focus": "Targeted live replay for nomenclature high/low profit questions: ask period when missing, stay in inventory/sales/cost domain, give useful limited answer when cost evidence is insufficient, and avoid fixed-assets/bank leakage.", + "architecture_phase": "turnaround_11", + "source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\agent_inventory_margin_ranking_20260523.json", + "scenario_id": "agent_inventory_margin_ranking_20260523", + "semantic_tags": [ + "account_family_guard", + "carryover", + "domain_purity", + "evidence_followup", + "inventory_margin_ranking", + "limited_answer", + "needs_period", + "no_fixed_assets", + "period_expansion", + "period_followup" + ], + "validation_status": "accepted_live_replay", + "validated_run_dir": "artifacts\\domain_runs\\agent_inventory_margin_ranking_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_inventory_margin_ranking_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": "Какая номеклатура товара реализована с высокой прибылью какая с низкой", + "created_at": "2026-05-23T11:07:35+00:00", + "reply_type": null, + "trace_id": null, + "debug": null + }, + { + "message_id": "agent-user-002", + "role": "user", + "text": "май 2020", + "created_at": "2026-05-23T11:07:35+00:00", + "reply_type": null, + "trace_id": null, + "debug": null + }, + { + "message_id": "agent-user-003", + "role": "user", + "text": "покажи найденные строки себестоимостной базы", + "created_at": "2026-05-23T11:07:35+00:00", + "reply_type": null, + "trace_id": null, + "debug": null + }, + { + "message_id": "agent-user-004", + "role": "user", + "text": "расширь до 2017 года", + "created_at": "2026-05-23T11:07:35+00:00", + "reply_type": null, + "trace_id": null, + "debug": null + }, + { + "message_id": "agent-user-005", + "role": "user", + "text": "анализ по 41 счету а не 01", + "created_at": "2026-05-23T11:07:35+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 live replay for nomenclature high/low profit questions: ask period when missing, stay in inventory/sales/cost domain, give useful limited answer when cost evidence is insufficient, and avoid fixed-assets/bank leakage.", + "architecture_phase": "turnaround_11", + "source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\agent_inventory_margin_ranking_20260523.json", + "scenario_id": "agent_inventory_margin_ranking_20260523", + "semantic_tags": [ + "account_family_guard", + "carryover", + "domain_purity", + "evidence_followup", + "inventory_margin_ranking", + "limited_answer", + "needs_period", + "no_fixed_assets", + "period_expansion", + "period_followup" + ], + "validation_status": "accepted_live_replay", + "validated_run_dir": "artifacts\\domain_runs\\agent_inventory_margin_ranking_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_inventory_margin_ranking_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_20260523110735_gen-ag05231107-464a28.json b/llm_normalizer/data/eval_cases/assistant_autogen_saved_user_sessions_20260523110735_gen-ag05231107-464a28.json new file mode 100644 index 0000000..9673765 --- /dev/null +++ b/llm_normalizer/data/eval_cases/assistant_autogen_saved_user_sessions_20260523110735_gen-ag05231107-464a28.json @@ -0,0 +1,40 @@ +{ + "suite_id": "assistant_saved_session_gen-ag05231107-464a28", + "suite_version": "0.1.0", + "schema_version": "assistant_saved_session_suite_v0_1", + "generated_at": "2026-05-23T11:07:35+00:00", + "generation_id": "gen-ag05231107-464a28", + "mode": "saved_user_sessions", + "title": "AGENT | Inventory margin ranking limited answer pack", + "domain": "inventory_margin_ranking", + "scenario_count": 1, + "case_ids": [ + "SAVED-001" + ], + "cases": [ + { + "case_id": "SAVED-001", + "scenario_tag": "agent_saved_user_sessions", + "title": "AGENT | Inventory margin ranking limited answer pack", + "question_type": "followup", + "broadness_level": "medium", + "turns": [ + { + "user_message": "Какая номеклатура товара реализована с высокой прибылью какая с низкой" + }, + { + "user_message": "май 2020" + }, + { + "user_message": "покажи найденные строки себестоимостной базы" + }, + { + "user_message": "расширь до 2017 года" + }, + { + "user_message": "анализ по 41 счету а не 01" + } + ] + } + ] +}