Стабилизировать маржинальность номенклатуры 1С

This commit is contained in:
dctouch 2026-05-23 14:39:37 +03:00
parent 473cdc3a9b
commit a15f24f21d
14 changed files with 760 additions and 18 deletions

View File

@ -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
}
}

View File

@ -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) {

View File

@ -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 &&

View File

@ -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;

View File

@ -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) {

View File

@ -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 &&

View File

@ -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";
}

View File

@ -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);

View File

@ -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("амортизац");
});
});

View File

@ -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.",

View File

@ -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" }),

View File

@ -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",

View File

@ -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
}
}
}
}

View File

@ -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"
}
]
}
]
}