Стабилизировать денежные ответы и прибыль 1С-ассистента

This commit is contained in:
dctouch 2026-05-23 13:14:22 +03:00
parent b18613961c
commit 473cdc3a9b
17 changed files with 1184 additions and 27 deletions

View File

@ -0,0 +1,266 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "agent_cashflow_profit_directness_20260523",
"domain": "autonomy_business_answer_contract",
"title": "AGENT | Cashflow vs profit directness pack",
"description": "Targeted AGENT replay for cashflow-vs-profit directness: colloquial earnings questions must produce a compact money answer, while broad overview remains available only when explicitly requested.",
"bindings": {
},
"steps": [
{
"step_id": "step_01_direct_money_earned_explicit_shape",
"title": "Direct money earned question asks for compact cashflow shape",
"question": "Сколько денег Альтернатива заработала за 2020 год? Ответь коротко: получили, заплатили, денежное нетто, это прибыль или нет.",
"allowed_reply_types": [
"partial_coverage",
"factual_with_explanation",
"factual"
],
"required_answer_patterns_all": [
"47[\\s.]*628[\\s.]*853",
"43[\\s.]*763[\\s.]*351",
"3[\\s.]*865[\\s.]*501"
],
"forbidden_answer_patterns": [
"Учтено строк",
"Первая найденная дата",
"runtime_",
"planner_",
"query_movements",
"primitive",
"7\\s*136\\s*815|7136815",
"Комитет государственных услуг"
],
"criticality": "critical",
"semantic_tags": [
"cashflow_vs_profit",
"direct_answer",
"colloquial_earnings"
]
},
{
"step_id": "step_02_colloquial_money_earned",
"title": "Colloquial earned wording stays cashflow, not broad overview",
"question": "скока денег альтернатива заработала за 20 год?",
"allowed_reply_types": [
"partial_coverage",
"factual_with_explanation",
"factual"
],
"required_answer_patterns_all": [
"47[\\s.]*628[\\s.]*853",
"43[\\s.]*763[\\s.]*351",
"3[\\s.]*865[\\s.]*501"
],
"forbidden_answer_patterns": [
"Учтено строк",
"Первая найденная дата",
"runtime_",
"planner_",
"query_movements",
"primitive",
"7\\s*136\\s*815|7136815",
"Комитет государственных услуг",
"Что проверить дальше"
],
"criticality": "critical",
"semantic_tags": [
"cashflow_vs_profit",
"slang_wording",
"no_overview_substitution"
]
},
{
"step_id": "step_03_profit_followup_after_cashflow",
"title": "Clean profit follow-up keeps distinction",
"question": "а это чистая прибыль?",
"allowed_reply_types": [
"partial_coverage",
"factual_with_explanation",
"factual"
],
"required_answer_patterns_all": [
"7\\s*136\\s*815|7136815",
"90|91|99"
],
"forbidden_answer_patterns": [
"Учтено строк",
"Первая найденная дата",
"runtime_",
"planner_",
"query_movements",
"primitive"
],
"criticality": "critical",
"semantic_tags": [
"profit_vs_cashflow",
"followup_context"
]
},
{
"step_id": "step_04_money_plus_or_minus_followup",
"title": "Money plus/minus follow-up returns cashflow net",
"question": "а по деньгам тогда плюс или минус?",
"allowed_reply_types": [
"partial_coverage",
"factual_with_explanation",
"factual"
],
"required_answer_patterns_all": [
"\\+",
"47[\\s.]*628[\\s.]*853",
"43[\\s.]*763[\\s.]*351",
"3[\\s.]*865[\\s.]*501"
],
"forbidden_answer_patterns": [
"Учтено строк",
"Первая найденная дата",
"runtime_",
"planner_",
"query_movements",
"primitive",
"7\\s*136\\s*815|7136815"
],
"criticality": "critical",
"semantic_tags": [
"cashflow_polarity",
"followup_context",
"no_profit_substitution"
]
},
{
"step_id": "step_05_explicit_business_overview_allowed",
"title": "Explicit adult overview may include business structure and next steps",
"question": "Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток.",
"allowed_reply_types": [
"partial_coverage",
"factual_with_explanation",
"factual"
],
"required_answer_patterns_all": [
"47[\\s.]*628[\\s.]*853",
"43[\\s.]*763[\\s.]*351",
"3[\\s.]*865[\\s.]*501",
"12[\\s.]*792[\\s.]*194",
"12[\\s.]*093[\\s.]*465",
"9[\\s.]*612[\\s.]*904"
],
"forbidden_answer_patterns": [
"Учтено строк",
"Первая найденная дата",
"runtime_",
"planner_",
"query_movements",
"primitive"
],
"criticality": "high",
"semantic_tags": [
"business_overview",
"bank_flow_boundary",
"next_steps"
]
},
{
"step_id": "step_06_return_to_short_money_after_overview",
"title": "Short money follow-up after overview keeps 2020 context",
"question": "а если коротко, сколько заработали деньгами без топов?",
"allowed_reply_types": [
"partial_coverage",
"factual_with_explanation",
"factual"
],
"required_answer_patterns_all": [
"2020",
"47[\\s.]*628[\\s.]*853",
"43[\\s.]*763[\\s.]*351",
"3[\\s.]*865[\\s.]*501"
],
"forbidden_answer_patterns": [
"Учтено строк",
"Первая найденная дата",
"runtime_",
"planner_",
"query_movements",
"primitive",
"285[\\s.]*819[\\s.]*547",
"147[\\s.]*855[\\s.]*827",
"Комитет государственных услуг",
"Что проверить дальше"
],
"criticality": "critical",
"semantic_tags": [
"compact_after_overview",
"temporal_carryover",
"no_all_time_leak"
]
},
{
"step_id": "step_07_plain_money_in_out_net",
"title": "Plain money request suppresses broad overview",
"question": "не обзор, просто деньги: пришло, ушло, нетто за 2020",
"allowed_reply_types": [
"partial_coverage",
"factual_with_explanation",
"factual"
],
"required_answer_patterns_all": [
"47[\\s.]*628[\\s.]*853",
"43[\\s.]*763[\\s.]*351",
"3[\\s.]*865[\\s.]*501"
],
"forbidden_answer_patterns": [
"Учтено строк",
"Первая найденная дата",
"runtime_",
"planner_",
"query_movements",
"primitive",
"Комитет государственных услуг",
"12[\\s.]*093[\\s.]*465",
"12[\\s.]*792[\\s.]*194",
"Что проверить дальше"
],
"criticality": "critical",
"semantic_tags": [
"direct_money_only",
"ranking_suppression"
]
},
{
"step_id": "step_08_direct_clean_profit_2020",
"title": "Explicit clean profit goes to accounting result, not cashflow",
"question": "какая чистая прибыль по Альтернативе за 2020?",
"allowed_reply_types": [
"partial_coverage",
"factual_with_explanation",
"factual"
],
"required_answer_patterns_all": [
"7\\s*136\\s*815|7136815",
"90|91|99"
],
"forbidden_answer_patterns": [
"Учтено строк",
"Первая найденная дата",
"runtime_",
"planner_",
"query_movements",
"primitive",
"получили\\s+47[\\s.]*628[\\s.]*853",
"денежное\\s+нетто\\s+3[\\s.]*865[\\s.]*501"
],
"criticality": "critical",
"semantic_tags": [
"clean_profit",
"profit_route",
"no_cashflow_substitution"
]
}
],
"acceptance": {
"min_score": 80,
"max_unresolved_p0": 0,
"require_all_critical_steps_pass": true
}
}

View File

@ -14,6 +14,11 @@ function sessionOrganizationName(sessionOrganizationScope, toNonEmptyString) {
const scope = toRecordObject(sessionOrganizationScope);
return toNonEmptyString(scope?.selectedOrganization) ?? toNonEmptyString(scope?.activeOrganization);
}
function sessionKnownOrganizations(sessionOrganizationScope) {
const scope = toRecordObject(sessionOrganizationScope);
const knownOrganizations = scope?.knownOrganizations;
return Array.isArray(knownOrganizations) ? knownOrganizations : [];
}
function predecomposeOrganizationName(predecomposeContract, toNonEmptyString) {
const entities = toRecordObject(predecomposeContract?.entities);
return (toNonEmptyString(entities?.organization) ??
@ -223,7 +228,8 @@ async function buildAssistantAddressOrchestrationRuntime(input) {
effectiveMessage: addressInputMessage,
assistantTurnMeaning: toRecordObject(orchestrationContract?.assistant_turn_meaning),
predecomposeContract,
followupContext: discoveryFollowupContext
followupContext: discoveryFollowupContext,
knownOrganizations: sessionKnownOrganizations(input.sessionOrganizationScope ?? null)
}));
}
catch (error) {

View File

@ -115,6 +115,12 @@ function hasBusinessOverviewDirectMoneyAnswerHint(input) {
return true;
}
const text = input.rawUtterance;
if (/(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)[\s\S]{0,80}(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\p{L}*|\u0443\u0448\p{L}*|\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*)|(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e)[\s\S]{0,80}(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)/iu.test(text)) {
return true;
}
if (/(?:\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}+|\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?\b|\u0434\u0435\u043d\p{L}{0,20}\s+\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\u043b\p{L}*[\s\S]{0,80}\u0443\u0448\u043b\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|\u0432\u0445\u043e\u0434\u044f\u0449\p{L}*[\s\S]{0,80}\u0438\u0441\u0445\u043e\u0434\u044f\u0449\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e)/iu.test(text)) {
return true;
}
return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|how\s+much)[\s\S]{0,120}(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u0432\u044b\u0440\u0443\u0447|\u0434\u0435\u043d\p{L}*|\u043f\u043e\u043b\u0443\u0447|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*)|(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u0432\u044b\u0440\u0443\u0447)[\s\S]{0,120}(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|\u0432\u0441\u0435\u0433\u043e|\u0432\u043e\u043e\u0431\u0449\u0435|(?:19|20)\d{2}|all\s+time)|(?:\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0430\u044f|\u043a\u0430\u043a\u0438\u0435|which|what)[\s\S]{0,80}(?:\u0441\u0430\u043c\p{L}*|top|best|most)[\s\S]{0,80}(?:\u0434\u043e\u0445\u043e\u0434\u043d|\u0432\u044b\u0440\u0443\u0447|\u043e\u0431\u043e\u0440\u043e\u0442|revenue|turnover)[\s\S]{0,40}(?:\u0433\u043e\u0434|year)/iu.test(text);
}
function timeScopeNeedFor(input) {
@ -225,6 +231,13 @@ function rankingNeedFromRawUtterance(value) {
}
return null;
}
function suppressRankingNeedFromRawUtterance(value) {
const text = lower(value);
if (!text) {
return false;
}
return /(?:\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?\b|\u043d\u0435\s+\u0442\u043e\u043f\b|\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440\b|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}+|\u0438\u0441\u043a\u043b\u044e\u0447\w*\s+\u0442\u043e\u043f|\u0431\u0435\u0437\s+\u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0430\b|без\s+топ(?:ов|а)?\b|не\s+топ\b|не\s+обзор\b|просто\s+ден\p{L}+|исключ\S*\s+топ|без\s+рейтинга\b)/iu.test(text);
}
function proofExpectationFor(input) {
if (input.clarificationGaps.length > 0) {
return "clarification_required";
@ -383,6 +396,7 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
const action = lower(turnMeaning?.asked_action_family);
const unsupported = lower(turnMeaning?.unsupported_but_understood_family);
const rawUtterance = lower(input.rawUtterance);
const rawQuestionSignal = lower([input.rawUtterance, turnMeaning?.raw_message, turnMeaning?.effective_message].join(" "));
const aggregationAxis = lower(turnMeaning?.asked_aggregation_axis);
const seededRankingNeed = toNonEmptyString(turnMeaning?.seeded_ranking_need);
const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope);
@ -400,15 +414,18 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
});
const aggregationNeed = aggregationNeedFor(aggregationAxis);
const comparisonNeed = comparisonNeedFor(action);
const rankingNeed = rankingNeedFromRawUtterance(rawUtterance) ?? seededRankingNeed;
const allTimeScopeHint = hasAllTimeScopeHint(rawUtterance);
const subjectScopedBidirectionalAllTime = businessFactFamily === "value_flow" &&
comparisonNeed === "incoming_vs_outgoing" &&
subjectCandidates.length > 0 &&
!explicitDateScope;
const suppressRankingNeed = suppressRankingNeedFromRawUtterance(rawQuestionSignal) ||
/(?:\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?|\u043d\u0435\s+\u0442\u043e\u043f|\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}+|\u0438\u0441\u043a\u043b\u044e\u0447\w*\s+\u0442\u043e\u043f|\u0431\u0435\u0437\s+\u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0430)/iu.test(rawQuestionSignal);
const rawRankingNeed = rankingNeedFromRawUtterance(rawQuestionSignal);
const rankingNeed = suppressRankingNeed ? null : rawRankingNeed ?? seededRankingNeed;
const directBusinessOverviewMoneyAnswerHint = hasBusinessOverviewDirectMoneyAnswerHint({
family: businessFactFamily,
rawUtterance,
rawUtterance: rawQuestionSignal,
rankingNeed
});
const oneSidedOpenScopeTotalHint = hasOpenScopeOneSidedValueTotalHintUtf8Safe(rawUtterance, action);

View File

@ -34,6 +34,30 @@ function requestsFinancialCounterpartyBoundary(turnMeaning, graph) {
return (/(?:банк|сбербанк|финанс|кредит|депозит)/iu.test(text) &&
/(?:клиент|поставщик|выручк|топ|обычн|роль|поток)/iu.test(text));
}
function requestsCompactCashflowAnswer(turnMeaning, graph) {
const text = normalizeQuestionText([
turnMeaning?.raw_message,
turnMeaning?.effective_message,
graph?.source_message,
graph?.question
].join(" "));
if (!text) {
return false;
}
if (/(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)[\s\S]{0,80}(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\p{L}*|\u0443\u0448\p{L}*|\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*)|(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e)[\s\S]{0,80}(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)/iu.test(text)) {
return true;
}
return /(?:\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}+|\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?\b|\u0434\u0435\u043d\p{L}{0,20}\s+\u043d\u0435\u0442\u0442\u043e|\u043f\u043e\u043b\u0443\u0447\p{L}*[\s\S]{0,80}\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\u043b\p{L}*[\s\S]{0,80}\u0443\u0448\u043b\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*)[\s\S]{0,120}(?:\u0434\u0435\u043d\p{L}*|\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u043f\u043e\u043b\u0443\u0447|\u043f\u0440\u0438\u0448\u043b))/iu.test(text);
}
function requestsCashflowPolarityAnswer(turnMeaning, graph) {
const text = normalizeQuestionText([
turnMeaning?.raw_message,
turnMeaning?.effective_message,
graph?.source_message,
graph?.question
].join(" "));
return /(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)[\s\S]{0,80}(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441)|(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441)[\s\S]{0,80}(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)/iu.test(text);
}
function toStringList(value) {
if (!Array.isArray(value)) {
return [];
@ -595,6 +619,18 @@ function compactComparable(value) {
.replace(/\s+/g, " ")
.trim();
}
function businessOverviewOrganizationScopeLabel(value) {
const text = toNonEmptyString(value);
if (!text) {
return null;
}
const comparable = compactComparable(text);
if (/^(?:с|без)\s+разбивк/.test(comparable) ||
/\b(?:входящ|исходящ|нетто|топ|контрагент|платеж|поступлен)\b/.test(comparable)) {
return null;
}
return text;
}
function businessOverviewSeparateSubjectLabel(graph, turnMeaning, organizationScope) {
const candidates = uniqueStrings([
...toStringList(turnMeaning?.business_overview_separate_entity_candidates),
@ -686,7 +722,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
const netDirection = overview.net_direction === "net_outgoing" ? "операционное нетто в минус" : "расчетное операционное нетто";
const period = businessOverviewPeriodText(overview);
const limitLine = businessOverviewCoverageLimitLine(overview);
const organizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope);
const organizationScope = businessOverviewOrganizationScopeLabel(turnMeaning?.explicit_organization_scope);
const separateSubject = businessOverviewSeparateSubjectLabel(graph, turnMeaning, organizationScope);
const previousCounterpartySummary = buildPreviousCounterpartyValueFlowSummary(toRecordObject(turnMeaning?.previous_counterparty_value_flow_bundle), separateSubject, toRecordObject(turnMeaning?.previous_counterparty_document_bundle));
const organizationPrefix = organizationScope ? `по компании ${organizationScope} ` : "";
@ -730,6 +766,22 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
const debtDueDateBoundary = actionFamily === "debt_due_date_boundary" || unsupportedFamily === "debt_due_date_boundary";
const vendorRiskBoundary = actionFamily === "vendor_risk_procurement_boundary" || unsupportedFamily === "vendor_risk_procurement_boundary";
const inventoryReserveBoundary = actionFamily === "inventory_reserve_boundary" || unsupportedFamily === "inventory_reserve_liquidation_boundary";
const compactCashflowRequested = directMoneyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph);
const cashflowPolarityRequested = compactCashflowRequested && requestsCashflowPolarityAnswer(turnMeaning, graph);
if (compactCashflowRequested && !rankingNeed && (incomingAmount || outgoingAmount || netAmount)) {
const netDisplay = sentenceAmount(netAmount) ?? netAmount ?? "0 \u0440\u0443\u0431.";
const signedNetDisplay = cashflowPolarityRequested && netDisplay && !String(netDisplay).trim().startsWith("-")
? `+${netDisplay}`
: netDisplay;
const polarityLead = cashflowPolarityRequested
? `\u041a\u043e\u0440\u043e\u0442\u043a\u043e: \u043f\u043e \u0434\u0435\u043d\u044c\u0433\u0430\u043c \u043f\u043b\u044e\u0441. `
: "\u041a\u043e\u0440\u043e\u0442\u043a\u043e: ";
lines.push(`${polarityLead}${organizationPrefix}${period} \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438 ${incomingAmount ?? "0 \u0440\u0443\u0431."}; \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438/\u0441\u043f\u0438\u0441\u0430\u043b\u0438 ${outgoingAmount ?? "0 \u0440\u0443\u0431."}; \u0434\u0435\u043d\u0435\u0436\u043d\u043e\u0435 \u043d\u0435\u0442\u0442\u043e ${signedNetDisplay}.`);
lines.push(cashflowPolarityRequested
? "\u042d\u0442\u043e \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0439 \u043f\u043e\u0442\u043e\u043a \u043f\u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u043c \u0441\u0442\u0440\u043e\u043a\u0430\u043c 1\u0421, \u043d\u0435 \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0441\u043a\u0438\u0439 \u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442."
: "\u042d\u0442\u043e \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0439 \u043f\u043e\u0442\u043e\u043a \u043f\u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u043c \u0441\u0442\u0440\u043e\u043a\u0430\u043c 1\u0421, \u043d\u0435 \u0447\u0438\u0441\u0442\u0430\u044f \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u0438 \u043d\u0435 \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0441\u043a\u0438\u0439 \u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442.");
return joinBusinessReplyLines(lines);
}
if (profitMarginBoundary) {
const accountingFinancialResult = toRecordObject(overview.accounting_financial_result);
if (accountingFinancialResult) {

View File

@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION = void 0;
exports.buildAssistantMcpDiscoveryTurnInput = buildAssistantMcpDiscoveryTurnInput;
const assistantMcpDiscoveryDataNeedGraph_1 = require("./assistantMcpDiscoveryDataNeedGraph");
const assistantOrganizationMatcher_1 = require("./assistantOrganizationMatcher");
const addressTextRepair_1 = require("./addressTextRepair");
exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION = "assistant_mcp_discovery_turn_input_v1";
function toRecordObject(value) {
@ -734,11 +735,13 @@ function hasBusinessOverviewContinuationSignal(text) {
const hasFinalSummaryCue = /(?:\u0447\u0442\u043e\s+\u043c\u044b\s+\u0437\u043d\u0430\u0435\u043c|\u0447\u0442\u043e\s+\u043f\u043e\u043d\u044f\u0442\u043d\u043e|\u0447\u0442\u043e\s+\u043f\u0440\u043e\u0432\u0435\u0440\w*\s+\u0434\u0430\u043b\u044c\u0448\u0435|\u0441\u043b\u0435\u0434\u0443\u044e\u0449\w*\s+\u0448\u0430\u0433|\u0438\u0442\u043e\u0433\w*\s+\u0432\u044b\u0432\u043e\u0434|\u043a\u0430\u043a\u043e\u0439\s+\u0432\u044b\u0432\u043e\u0434|\u0447\u0442\u043e\s+\u0441\s+\u044d\u0442\u0438\u043c\s+\u0434\u0435\u043b\u0430\u0442\u044c|what\s+do\s+we\s+know|what\s+is\s+missing|next\s+step|final\s+summary)/iu.test(normalized);
const hasMoneyBreakdownCue = /(?:\u0440\u0430\u0441\u043a\u0440\u043e\p{L}*\s+\u0434\u0435\u043d\p{L}*|\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+\u0432\u0441\u0435\u0433\u043e\s+\u043f\u043e\u043b\u0443\u0447|\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:\u0432\u0441\u0435\u0433\u043e\s+)?\u0437\u0430\u043f\u043b\u0430\u0442|\u0447\u0438\u0441\u0442\p{L}*\s+\u0434\u0435\u043d\u0435\u0436\u043d\p{L}*\s+\u043f\u043e\u0442\u043e\u043a|\u0433\u043b\u0430\u0432\u043d\p{L}*\s+(?:\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a)|top\s+(?:customer|supplier)|cash\s+breakdown)/iu.test(normalized) &&
/(?:\u043f\u043e\u043b\u0443\u0447|\u0437\u0430\u043f\u043b\u0430\u0442|\u043d\u0435\u0442\u0442\u043e|\u0434\u0435\u043d\p{L}*|\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|received|paid|net|cash|customer|supplier)/iu.test(normalized);
const hasCashflowPolarityCue = /(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)[\s\S]{0,80}(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\p{L}*|\u0443\u0448\p{L}*|\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*)|(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e)[\s\S]{0,80}(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)/iu.test(normalized);
return (hasEvidenceContinuationCue ||
hasAnalystContinuationCue ||
hasTaxContinuationCue ||
hasFinalSummaryCue ||
hasMoneyBreakdownCue);
hasMoneyBreakdownCue ||
hasCashflowPolarityCue);
}
function hasExplicitVatQuestionSignal(text) {
if (!text) {
@ -785,6 +788,9 @@ function hasBusinessOverviewFollowupSeed(followupSeed) {
followupSeed.loopSelectedChainId === "business_overview");
}
function hasValueFlowSignal(text) {
if (/(?:\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\u043a|\u043e\u043f\u043b\u0430\u0442|\u043f\u043b\u0430\u0442[\u0435\u0451]\u0436|\u0437\u0430\u043f\u043b\u0430\u0442|\u043f\u0435\u0440\u0435\u0447\u0438\u0441\u043b|\u0441\u043f\u0438\u0441\u0430\u043d|\u0440\u0430\u0441\u0445\u043e\u0434|\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u0432\u0445\u043e\u0434\u044f\u0449|\u043f\u043e\u043b\u0443\u0447(?:\u0438\u043b|\u0435\u043d\u043e|\u0435\u043d)|\u043f\u043e\u0441\u0442\u0443\u043f\u0438\u043b|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d|\u0434\u0435\u043d\p{L}*|\u0437\u0430\u0440\u0430\u0431\u043e\u0442|supplier|value[-\s]?flow|turnover|revenue|payment|payout|outflow|cash\s+flow|\bearn(?:ed|ing|ings)?\b)/iu.test(text)) {
return true;
}
return /(?:оборот|выручк|оплат|плат[её]ж|заплат|перечисл|списан|расход|исходящ|входящ|получ(?:ил|ено|ен)|поступил|поступлен|денежн[а-яёa-z0-9_-]*\s+поток|(?<!\p{L})заработ(?:ал|али|ало|аем|ает|ать|ано|ок)(?!\p{L})|supplier|value[-\s]?flow|turnover|revenue|payment|payout|outflow|cash\s+flow|\bearn(?:ed|ing|ings)?\b)/iu.test(text);
}
function hasValueFlowAggregateQuestionSignal(text) {
@ -794,6 +800,9 @@ function hasPayoutSignal(text) {
return /(?:\bмы\s+(?:за)?плат|(?:за)?платил|оплатил|перечисл|списан|расход|поставщик|исходящ|supplier|payout|outflow|paid\s+to|payment\s+to)/iu.test(text);
}
function hasBidirectionalValueFlowSignal(text) {
if (/(?:\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|\u0431\u0430\u043b\u0430\u043d\u0441\s+(?:\u043f\u043b\u0430\u0442|\u0434\u0435\u043d\u0435\u0433|\u0434\u0435\u043d\u0435\u0436)|\u0432\u0437\u0430\u0438\u043c\u043e\u0440\u0430\u0441\u0447[\u0435\u0451]\u0442|\u043f\u043e\u043b\u0443\u0447\p{L}*.*(?:\u0437\u0430)?\u043f\u043b\u0430\u0442\p{L}*|(?:\u0437\u0430)?\u043f\u043b\u0430\u0442\p{L}*.*\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0432\u0445\u043e\u0434\u044f\u0449.*\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u0438\u0441\u0445\u043e\u0434\u044f\u0449.*\u0432\u0445\u043e\u0434\u044f\u0449|\u043f\u0440\u0438\u0448\p{L}*.*\u0443\u0448\p{L}*|\u0443\u0448\p{L}*.*\u043f\u0440\u0438\u0448\p{L}*|\u043f\u043b\u044e\u0441.*\u043c\u0438\u043d\u0443\u0441|\u043c\u0438\u043d\u0443\u0441.*\u043f\u043b\u044e\u0441|net\s+(?:flow|cash|payment)|cash\s+net|incoming\s+and\s+outgoing|received\s+and\s+paid|paid\s+and\s+received)/iu.test(text)) {
return true;
}
return /(?:нетто|сальдо|баланс\s+(?:плат|денег|денеж)|взаиморасч[её]т|получил[иа]?.*(?:за)?платил|(?:за)?платил[иа]?.*получил|входящ.*исходящ|исходящ.*входящ|дебет.*кредит|кредит.*дебет|net\s+(?:flow|cash|payment)|cash\s+net|incoming\s+and\s+outgoing|received\s+and\s+paid|paid\s+and\s+received)/iu.test(text);
}
function hasValueRankingSignal(text) {
@ -820,6 +829,39 @@ function extractOrganizationScopeFromRawText(value) {
.replace(/\s+(?:\u043f\u043e\s+\u0434\u0430\u043d\u043d\u044b\u043c\s+1\u0441|\u0432\s+\u0446\u0435\u043b\u043e\u043c|\u0437\u0430\s+(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{2}|(?:19|20)\d{2})(?:\s+\u0433(?:\u043e\u0434|\.)?)?|\u043d\u0430\s+(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{2}|(?:19|20)\d{2})|\u0437\u0430\s+\u0432\u0441[\u0435\u0451]\s+(?:\u0434\u043e\u0441\u0442\u0443\u043f\p{L}+\s+)?\u0432\u0440\u0435\u043c\u044f|\u0437\u0430\s+\u0432\u0435\u0441\u044c\s+(?:\u0434\u043e\u0441\u0442\u0443\u043f\p{L}+\s+)?\u043f\u0435\u0440\u0438\u043e\u0434|\u0437\u0430\s+\u0432\u0441\u044e\s+\u0438\u0441\u0442\u043e\u0440\u0438(?:\u044e|\u0438)|\u0434\u0430\u0439\s+\u0431\u0438\u0437\u043d\u0435\u0441-?\u043e\u0431\u0437\u043e\u0440|\u0431\u0438\u0437\u043d\u0435\u0441-?\u043e\u0431\u0437\u043e\u0440|\u043d\u043e\s+\u043d\u0435).*$/iu, "")
.trim());
}
function extractOrganizationScopeFromSemanticText(value) {
const text = toNonEmptyString(value);
if (!text) {
return null;
}
const match = text.match(/(?:^|[\s,;:])(?:\u043a\u043e\u043c\u043f\u0430\u043d(?:\u0438\u0438|\u0438\u044f)|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446(?:\u0438\u0438|\u0438\u044f))\s+(.+?)(?=$|[\n,.;:!?]|\s+\u0437\u0430\s+(?:\d{4}|\d{2}\s*\u0433|\d{2}\s+\u0433\u043e\u0434)|\s+\u043d\u0430\s+(?:\d{4}|\d{2}\s*\u0433|\d{2}\s+\u0433\u043e\u0434))/iu);
return toNonEmptyString(match?.[1]);
}
function normalizeLooseOrganizationAlias(value) {
const text = toNonEmptyString(value);
if (!text) {
return null;
}
const normalized = text
.replace(/^[«"'“”]+|[»"'“”]+$/gu, "")
.replace(/\s+/g, " ")
.trim();
if (!normalized) {
return null;
}
const comparable = (0, addressTextRepair_1.normalizeRussianComparableText)(normalized);
const normalizedTokens = normalized.split(/\s+/u).filter(Boolean);
const hasYearOnlyTimeTail = /\b(?:19|20)\d{2}\b/u.test(normalized) &&
normalizedTokens.length <= 4 &&
!/(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e|llc|inc|corp)/iu.test(comparable);
if (hasYearOnlyTimeTail) {
return null;
}
if (/^(?:\u0438|\u0432|\u0432\u043e|\u0437\u0430|\u043d\u0430|\u043f\u043e|\u043a\u0442\u043e|\u0447\u0442\u043e|\u043a\u0430\u043a(?:\u043e\u0439|\u0430\u044f|\u0438\u0435)?|\u0433\u043b\u0430\u0432\u043d\p{L}*)\b/iu.test(comparable)) {
return null;
}
return normalized;
}
function hasMonthlyAggregationSignal(text) {
return /(?:\u043f\u043e\s+\u043c\u0435\u0441\u044f\u0446\u0430\u043c|\u043f\u043e\u043c\u0435\u0441\u044f\u0447\u043d\u043e|\u0435\u0436\u0435\u043c\u0435\u0441\u044f\u0447\u043d\u043e|month\s+by\s+month|by\s+month|monthly)/iu.test(text);
}
@ -1030,7 +1072,10 @@ function metadataScopeHintFromRawText(text) {
return null;
}
function hasExplicitDateScopeLiteral(text) {
return /(?:\b(?:19|20)\d{2}\b|\b\d{4}-\d{2}-\d{2}\b|\b\d{4}-\d{2}\b)/iu.test(text);
if (/\b\d{2}\s*(?:\u0433(?:\u043e\u0434(?:\u0430|\u0443)?)?\.?)\b/iu.test(text)) {
return true;
}
return /(?:\b(?:19|20)\d{2}\b|\b\d{4}-\d{2}-\d{2}\b|\b\d{4}-\d{2}\b|\b\d{2}\s*(?:г(?:од(?:а|у)?)?\.?))\b/iu.test(text);
}
function stripNegatedTaxDateScopeClauses(text) {
const dateScopeLiteral = String.raw `\b(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{2}|(?:19|20)\d{2})\b`;
@ -1054,6 +1099,20 @@ function collectDateScopeFromRawText(text) {
if (year?.[1]) {
return year[1];
}
const utf8ShortYear = text.match(/\b(\d{2})\s*(?:\u0433(?:\u043e\u0434(?:\u0430|\u0443)?)?\.?)\b/iu);
if (utf8ShortYear?.[1]) {
const numericYear = Number.parseInt(utf8ShortYear[1], 10);
if (Number.isFinite(numericYear)) {
return String(numericYear <= 30 ? 2000 + numericYear : 1900 + numericYear);
}
}
const shortYear = text.match(/\b(\d{2})\s*(?:г(?:од(?:а|у)?)?\.?)\b/iu);
if (shortYear?.[1]) {
const numericYear = Number.parseInt(shortYear[1], 10);
if (Number.isFinite(numericYear)) {
return String(numericYear <= 30 ? 2000 + numericYear : 1900 + numericYear);
}
}
return null;
}
function currentIsoDate() {
@ -1137,6 +1196,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const reasonCodes = [];
const rawUserText = toNonEmptyString(input.userMessage);
const rawEffectiveText = toNonEmptyString(input.effectiveMessage);
const knownOrganizations = Array.isArray(input.knownOrganizations) ? input.knownOrganizations : [];
const repairedUserText = rawUserText ? (0, addressTextRepair_1.repairAddressMojibakeText)(rawUserText) : null;
const repairedEffectiveText = rawEffectiveText ? (0, addressTextRepair_1.repairAddressMojibakeText)(rawEffectiveText) : null;
const rawUserSignalSourceText = repairedUserText ?? rawUserText ?? "";
@ -1246,9 +1306,28 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const assistantTurnMeaningOrganizationScope = isReferentialOrganizationPlaceholder(rawAssistantTurnMeaningOrganizationScope)
? null
: rawAssistantTurnMeaningOrganizationScope;
const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText);
const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText ?? rawEffectiveText ?? rawSignalSourceText);
const currentTurnFreshOrganizationScope = predecomposeEntities.organization ?? rawOrganizationScope;
const organizationSelectionFromKnown = (0, assistantOrganizationMatcher_1.resolveOrganizationSelectionFromMessage)(rawUserText ?? rawEffectiveText ?? rawSignalSourceText, knownOrganizations) ??
(0, assistantOrganizationMatcher_1.resolveOrganizationSelectionFromMessage)(rawSignalSourceText, knownOrganizations) ??
null;
const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText) ??
extractOrganizationScopeFromRawText(rawEffectiveText) ??
extractOrganizationScopeFromRawText(rawSignalSourceText);
const semanticOrganizationScope = normalizeLooseOrganizationAlias(extractOrganizationScopeFromSemanticText(rawEffectiveText) ??
extractOrganizationScopeFromSemanticText(rawUserText) ??
extractOrganizationScopeFromSemanticText(rawSignalSourceText));
const predecomposeCounterpartyAsOrganizationScope = (rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
semanticOrganizationScope &&
!rawScopedEntityCandidate &&
!predecomposeEntities.organization
? normalizeLooseOrganizationAlias(predecomposeEntities.counterparty)
: null;
const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText) ||
Boolean(organizationSelectionFromKnown || rawOrganizationScope || semanticOrganizationScope || predecomposeCounterpartyAsOrganizationScope);
const currentTurnFreshOrganizationScope = organizationSelectionFromKnown ??
predecomposeEntities.organization ??
rawOrganizationScope ??
semanticOrganizationScope ??
predecomposeCounterpartyAsOrganizationScope;
const currentTurnOrganizationScope = currentTurnFreshOrganizationScope ?? assistantTurnMeaningOrganizationScope;
const predecomposeOrganizationMirrorsCounterparty = sameScopedName(predecomposeEntities.counterparty, predecomposeEntities.organization);
const organizationMirrorsPredecomposeCounterpartyForPivot = Boolean(sameScopedName(predecomposeEntities.counterparty, assistantTurnMeaningOrganizationScope) ||
@ -1805,10 +1884,14 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
}
}
const clarificationLoopStillNeedsPeriod = Boolean(followupSeed.loopStatus === "awaiting_clarification" && followupSeed.loopPendingAxes.includes("period"));
const rawTwoDigitPredecomposeYearHint = Boolean(predecomposeDateScope &&
/^\d{4}$/.test(predecomposeDateScope) &&
/\b\d{2}\b/u.test(rawText));
const businessOverviewRawWithoutDateScope = Boolean(businessOverviewSignal &&
!rawAllTimeScopeSignal &&
!explicitDateScopeLiteralDetected &&
!rawDateScope &&
!rawTwoDigitPredecomposeYearHint &&
!relativeCurrentDateHintDetected);
const predecomposeDateScopeCountsAsCurrentTurnPeriod = Boolean(predecomposeDateScope &&
!isImplicitCurrentDateScope(predecomposeDateScope) &&
@ -2236,6 +2319,15 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
if (businessOverviewRawYearOverridesPredecomposeAsOf) {
pushReason(reasonCodes, "mcp_discovery_business_overview_raw_year_overrode_predecompose_as_of_scope");
}
if (organizationSelectionFromKnown) {
pushReason(reasonCodes, "mcp_discovery_organization_scope_from_known_organizations");
}
if (semanticOrganizationScope) {
pushReason(reasonCodes, "mcp_discovery_organization_scope_from_semantic_text");
}
if (predecomposeCounterpartyAsOrganizationScope) {
pushReason(reasonCodes, "mcp_discovery_counterparty_reinterpreted_as_organization_scope");
}
if (!(valueFlowOrganizationStaysScope && normalizedPredecomposeCounterparty === explicitOrganizationScope) &&
normalizedPredecomposeCounterparty) {
pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose");

View File

@ -489,8 +489,24 @@ function createAssistantTransitionPolicy(deps) {
llmPreDecomposeMeta
})
: null;
const hasCompactCashflowFollowupCue = (value) => {
const normalized = normalizeFollowupText(value);
if (!normalized) {
return false;
}
const hasCompactCue = /(?:\u043a\u043e\u0440\u043e\u0442\p{L}*|\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u0431\u0435\u0437\s+\u0442\u043e\u043f\p{L}*)/iu.test(normalized);
const hasCashflowCue = /(?:\u0434\u0435\u043d\p{L}*|\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u043f\u043e\u043b\u0443\u0447|\u0437\u0430\u043f\u043b\u0430\u0442|\u043f\u0440\u0438\u0448\p{L}*|\u0443\u0448\p{L}*|\u043d\u0435\u0442\u0442\u043e)/iu.test(normalized);
return hasCompactCue && hasCashflowCue;
};
const compactCashflowFollowupSignal = hasShortValueFlowRetargetCue(userMessage) ||
hasCompactCashflowFollowupCue(userMessage) ||
(deps.toNonEmptyString(alternateMessage)
? hasShortValueFlowRetargetCue(String(alternateMessage ?? "")) ||
hasCompactCashflowFollowupCue(String(alternateMessage ?? ""))
: false);
if (assistantTurnMeaning?.stale_replay_forbidden === true &&
!hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage)) {
!hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage) &&
!compactCashflowFollowupSignal) {
return null;
}
const latestAddressItem = deps.findLastAddressAssistantItem(items);
@ -555,7 +571,8 @@ function createAssistantTransitionPolicy(deps) {
const hasValueFlowCarryoverSourceHint = sourceIntentHint === "customer_revenue_and_payments" ||
sourceDiscoveryPilotScopeHint === "counterparty_value_flow_query_movements_v1" ||
sourceDiscoveryPilotScopeHint === "counterparty_supplier_payout_query_movements_v1" ||
sourceDiscoveryPilotScopeHint === "counterparty_bidirectional_value_flow_query_movements_v1";
sourceDiscoveryPilotScopeHint === "counterparty_bidirectional_value_flow_query_movements_v1" ||
sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
const hasBusinessOverviewCarryoverSourceHint = sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
const navigationSessionState = (0, assistantContinuityPolicy_1.resolveNavigationSessionContextState)(addressNavigationState, deps.toNonEmptyString, deps.normalizeOrganizationScopeValue);
const navigationFocusObjectHint = navigationSessionState.focusObject;
@ -579,9 +596,11 @@ function createAssistantTransitionPolicy(deps) {
? deps.resolveDebtRoleSwapFollowupIntent(String(alternateMessage ?? ""), sourceIntentHint)
: null;
const debtRoleSwapIntent = debtRoleSwapPrimary ?? debtRoleSwapAlternate ?? null;
const shortValueFlowRetargetPrimary = hasValueFlowCarryoverSourceHint && hasShortValueFlowRetargetCue(userMessage);
const shortValueFlowRetargetPrimary = hasValueFlowCarryoverSourceHint &&
(hasShortValueFlowRetargetCue(userMessage) || hasCompactCashflowFollowupCue(userMessage));
const shortValueFlowRetargetAlternate = hasValueFlowCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
? hasShortValueFlowRetargetCue(String(alternateMessage ?? ""))
? hasShortValueFlowRetargetCue(String(alternateMessage ?? "")) ||
hasCompactCashflowFollowupCue(String(alternateMessage ?? ""))
: false;
const businessOverviewBoundaryFollowupPrimary = hasBusinessOverviewCarryoverSourceHint && hasBusinessOverviewBoundaryFollowupCue(userMessage);
const businessOverviewBoundaryFollowupAlternate = hasBusinessOverviewCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)

View File

@ -84,6 +84,14 @@ function sessionOrganizationName(
return toNonEmptyString(scope?.selectedOrganization) ?? toNonEmptyString(scope?.activeOrganization);
}
function sessionKnownOrganizations(
sessionOrganizationScope: BuildAssistantAddressOrchestrationRuntimeInput["sessionOrganizationScope"]
): unknown[] {
const scope = toRecordObject(sessionOrganizationScope);
const knownOrganizations = scope?.knownOrganizations;
return Array.isArray(knownOrganizations) ? knownOrganizations : [];
}
function predecomposeOrganizationName(
predecomposeContract: Record<string, unknown> | null,
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]
@ -410,7 +418,8 @@ export async function buildAssistantAddressOrchestrationRuntime(
effectiveMessage: addressInputMessage,
assistantTurnMeaning: toRecordObject(orchestrationContract?.assistant_turn_meaning),
predecomposeContract,
followupContext: discoveryFollowupContext
followupContext: discoveryFollowupContext,
knownOrganizations: sessionKnownOrganizations(input.sessionOrganizationScope ?? null)
})) as Record<string, unknown>;
} catch (error) {
mcpDiscoveryRuntimeEntryPointError = String(error instanceof Error ? error.message : error ?? "unknown_error").slice(0, 280);

View File

@ -176,6 +176,18 @@ function hasBusinessOverviewDirectMoneyAnswerHint(input: {
return true;
}
const text = input.rawUtterance;
if (
/(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)[\s\S]{0,80}(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\p{L}*|\u0443\u0448\p{L}*|\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*)|(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e)[\s\S]{0,80}(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)/iu.test(
text
)
) {
return true;
}
if (
/(?:\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}+|\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?\b|\u0434\u0435\u043d\p{L}{0,20}\s+\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\u043b\p{L}*[\s\S]{0,80}\u0443\u0448\u043b\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|\u0432\u0445\u043e\u0434\u044f\u0449\p{L}*[\s\S]{0,80}\u0438\u0441\u0445\u043e\u0434\u044f\u0449\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e)/iu.test(text)
) {
return true;
}
return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|how\s+much)[\s\S]{0,120}(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u0432\u044b\u0440\u0443\u0447|\u0434\u0435\u043d\p{L}*|\u043f\u043e\u043b\u0443\u0447|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*)|(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u0432\u044b\u0440\u0443\u0447)[\s\S]{0,120}(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|\u0432\u0441\u0435\u0433\u043e|\u0432\u043e\u043e\u0431\u0449\u0435|(?:19|20)\d{2}|all\s+time)|(?:\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0430\u044f|\u043a\u0430\u043a\u0438\u0435|which|what)[\s\S]{0,80}(?:\u0441\u0430\u043c\p{L}*|top|best|most)[\s\S]{0,80}(?:\u0434\u043e\u0445\u043e\u0434\u043d|\u0432\u044b\u0440\u0443\u0447|\u043e\u0431\u043e\u0440\u043e\u0442|revenue|turnover)[\s\S]{0,40}(?:\u0433\u043e\u0434|year)/iu.test(
text
);
@ -330,6 +342,16 @@ function rankingNeedFromRawUtterance(value: string): string | null {
return null;
}
function suppressRankingNeedFromRawUtterance(value: string): boolean {
const text = lower(value);
if (!text) {
return false;
}
return /(?:\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?\b|\u043d\u0435\s+\u0442\u043e\u043f\b|\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440\b|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}+|\u0438\u0441\u043a\u043b\u044e\u0447\w*\s+\u0442\u043e\u043f|\u0431\u0435\u0437\s+\u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0430\b|без\s+С‚РСР С(?:Р СР Р|Р В°)?\b|Р РР Вµ\s+С‚РСР С\b|Р РР Вµ\s+Р СР В±Р В·Р СРЎР\b|Р СРЎРР ССЃС‚РС\s+Р ТРµРР\p{L}+|Р СРЎРѓР СР»СРС‡\S*\s+С‚РСР С|без\s+РЎРРµРвС‚РСР РР СР В°\b)/iu.test(
text
);
}
function proofExpectationFor(input: {
family: string | null;
clarificationGaps: string[];
@ -503,6 +525,7 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
const action = lower(turnMeaning?.asked_action_family);
const unsupported = lower(turnMeaning?.unsupported_but_understood_family);
const rawUtterance = lower(input.rawUtterance);
const rawQuestionSignal = lower([input.rawUtterance, turnMeaning?.raw_message, turnMeaning?.effective_message].join(" "));
const aggregationAxis = lower(turnMeaning?.asked_aggregation_axis);
const seededRankingNeed = toNonEmptyString(turnMeaning?.seeded_ranking_need);
const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope);
@ -520,16 +543,22 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
});
const aggregationNeed = aggregationNeedFor(aggregationAxis);
const comparisonNeed = comparisonNeedFor(action);
const rankingNeed = rankingNeedFromRawUtterance(rawUtterance) ?? seededRankingNeed;
const allTimeScopeHint = hasAllTimeScopeHint(rawUtterance);
const subjectScopedBidirectionalAllTime =
businessFactFamily === "value_flow" &&
comparisonNeed === "incoming_vs_outgoing" &&
subjectCandidates.length > 0 &&
!explicitDateScope;
const suppressRankingNeed =
suppressRankingNeedFromRawUtterance(rawQuestionSignal) ||
/(?:\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?|\u043d\u0435\s+\u0442\u043e\u043f|\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}+|\u0438\u0441\u043a\u043b\u044e\u0447\w*\s+\u0442\u043e\u043f|\u0431\u0435\u0437\s+\u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0430)/iu.test(
rawQuestionSignal
);
const rawRankingNeed = rankingNeedFromRawUtterance(rawQuestionSignal);
const rankingNeed = suppressRankingNeed ? null : rawRankingNeed ?? seededRankingNeed;
const directBusinessOverviewMoneyAnswerHint = hasBusinessOverviewDirectMoneyAnswerHint({
family: businessFactFamily,
rawUtterance,
rawUtterance: rawQuestionSignal,
rankingNeed
});
const oneSidedOpenScopeTotalHint = hasOpenScopeOneSidedValueTotalHintUtf8Safe(rawUtterance, action);

View File

@ -60,6 +60,46 @@ function requestsFinancialCounterpartyBoundary(turnMeaning: Record<string, unkno
);
}
function requestsCompactCashflowAnswer(
turnMeaning: Record<string, unknown> | null,
graph: Record<string, unknown> | null
): boolean {
const text = normalizeQuestionText([
turnMeaning?.raw_message,
turnMeaning?.effective_message,
graph?.source_message,
graph?.question
].join(" "));
if (!text) {
return false;
}
if (
/(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)[\s\S]{0,80}(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\p{L}*|\u0443\u0448\p{L}*|\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*)|(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e)[\s\S]{0,80}(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)/iu.test(
text
)
) {
return true;
}
return /(?:\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}+|\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?\b|\u0434\u0435\u043d\p{L}{0,20}\s+\u043d\u0435\u0442\u0442\u043e|\u043f\u043e\u043b\u0443\u0447\p{L}*[\s\S]{0,80}\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\u043b\p{L}*[\s\S]{0,80}\u0443\u0448\u043b\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*)[\s\S]{0,120}(?:\u0434\u0435\u043d\p{L}*|\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u043f\u043e\u043b\u0443\u0447|\u043f\u0440\u0438\u0448\u043b))/iu.test(
text
);
}
function requestsCashflowPolarityAnswer(
turnMeaning: Record<string, unknown> | null,
graph: Record<string, unknown> | null
): boolean {
const text = normalizeQuestionText([
turnMeaning?.raw_message,
turnMeaning?.effective_message,
graph?.source_message,
graph?.question
].join(" "));
return /(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)[\s\S]{0,80}(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441)|(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441)[\s\S]{0,80}(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)/iu.test(
text
);
}
function toStringList(value: unknown): string[] {
if (!Array.isArray(value)) {
return [];
@ -689,6 +729,21 @@ function compactComparable(value: string | null): string {
.trim();
}
function businessOverviewOrganizationScopeLabel(value: unknown): string | null {
const text = toNonEmptyString(value);
if (!text) {
return null;
}
const comparable = compactComparable(text);
if (
/^(?:с|без)\s+разбивк/.test(comparable) ||
/\b(?:входящ|исходящ|нетто|топ|контрагент|платеж|поступлен)\b/.test(comparable)
) {
return null;
}
return text;
}
function businessOverviewSeparateSubjectLabel(
graph: Record<string, unknown> | null,
turnMeaning: Record<string, unknown> | null,
@ -802,7 +857,7 @@ function buildCompactBusinessOverviewReply(
const netDirection = overview.net_direction === "net_outgoing" ? "операционное нетто в минус" : "расчетное операционное нетто";
const period = businessOverviewPeriodText(overview);
const limitLine = businessOverviewCoverageLimitLine(overview);
const organizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope);
const organizationScope = businessOverviewOrganizationScopeLabel(turnMeaning?.explicit_organization_scope);
const separateSubject = businessOverviewSeparateSubjectLabel(graph, turnMeaning, organizationScope);
const previousCounterpartySummary = buildPreviousCounterpartyValueFlowSummary(
toRecordObject(turnMeaning?.previous_counterparty_value_flow_bundle),
@ -857,6 +912,28 @@ function buildCompactBusinessOverviewReply(
actionFamily === "vendor_risk_procurement_boundary" || unsupportedFamily === "vendor_risk_procurement_boundary";
const inventoryReserveBoundary =
actionFamily === "inventory_reserve_boundary" || unsupportedFamily === "inventory_reserve_liquidation_boundary";
const compactCashflowRequested = directMoneyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph);
const cashflowPolarityRequested = compactCashflowRequested && requestsCashflowPolarityAnswer(turnMeaning, graph);
if (compactCashflowRequested && !rankingNeed && (incomingAmount || outgoingAmount || netAmount)) {
const netDisplay = sentenceAmount(netAmount) ?? netAmount ?? "0 \u0440\u0443\u0431.";
const signedNetDisplay =
cashflowPolarityRequested && netDisplay && !String(netDisplay).trim().startsWith("-")
? `+${netDisplay}`
: netDisplay;
const polarityLead = cashflowPolarityRequested
? `\u041a\u043e\u0440\u043e\u0442\u043a\u043e: \u043f\u043e \u0434\u0435\u043d\u044c\u0433\u0430\u043c \u043f\u043b\u044e\u0441. `
: "\u041a\u043e\u0440\u043e\u0442\u043a\u043e: ";
lines.push(
`${polarityLead}${organizationPrefix}${period} \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438 ${incomingAmount ?? "0 \u0440\u0443\u0431."}; \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438/\u0441\u043f\u0438\u0441\u0430\u043b\u0438 ${outgoingAmount ?? "0 \u0440\u0443\u0431."}; \u0434\u0435\u043d\u0435\u0436\u043d\u043e\u0435 \u043d\u0435\u0442\u0442\u043e ${signedNetDisplay}.`
);
lines.push(
cashflowPolarityRequested
? "\u042d\u0442\u043e \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0439 \u043f\u043e\u0442\u043e\u043a \u043f\u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u043c \u0441\u0442\u0440\u043e\u043a\u0430\u043c 1\u0421, \u043d\u0435 \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0441\u043a\u0438\u0439 \u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442."
: "\u042d\u0442\u043e \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0439 \u043f\u043e\u0442\u043e\u043a \u043f\u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u043c \u0441\u0442\u0440\u043e\u043a\u0430\u043c 1\u0421, \u043d\u0435 \u0447\u0438\u0441\u0442\u0430\u044f \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u0438 \u043d\u0435 \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0441\u043a\u0438\u0439 \u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442."
);
return joinBusinessReplyLines(lines);
}
if (profitMarginBoundary) {
const accountingFinancialResult = toRecordObject(overview.accounting_financial_result);

View File

@ -3,6 +3,7 @@ import {
buildAssistantMcpDiscoveryDataNeedGraph,
type AssistantMcpDiscoveryDataNeedGraphContract
} from "./assistantMcpDiscoveryDataNeedGraph";
import { resolveOrganizationSelectionFromMessage } from "./assistantOrganizationMatcher";
import { normalizeRussianComparableText, repairAddressMojibakeText } from "./addressTextRepair";
import type {
AssistantMcpDiscoveryMetadataRecommendedPrimitive,
@ -27,6 +28,7 @@ export interface BuildAssistantMcpDiscoveryTurnInputAdapterInput {
followupContext?: Record<string, unknown> | null;
userMessage?: string | null;
effectiveMessage?: string | null;
knownOrganizations?: unknown[] | null;
}
export interface AssistantMcpDiscoveryTurnInputContract {
@ -1044,12 +1046,17 @@ function hasBusinessOverviewContinuationSignal(text: string): boolean {
/(?:\u043f\u043e\u043b\u0443\u0447|\u0437\u0430\u043f\u043b\u0430\u0442|\u043d\u0435\u0442\u0442\u043e|\u0434\u0435\u043d\p{L}*|\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|received|paid|net|cash|customer|supplier)/iu.test(
normalized
);
const hasCashflowPolarityCue =
/(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)[\s\S]{0,80}(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\p{L}*|\u0443\u0448\p{L}*|\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*)|(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e)[\s\S]{0,80}(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)/iu.test(
normalized
);
return (
hasEvidenceContinuationCue ||
hasAnalystContinuationCue ||
hasTaxContinuationCue ||
hasFinalSummaryCue ||
hasMoneyBreakdownCue
hasMoneyBreakdownCue ||
hasCashflowPolarityCue
);
}
@ -1114,6 +1121,13 @@ function hasBusinessOverviewFollowupSeed(followupSeed: ReturnType<typeof collect
}
function hasValueFlowSignal(text: string): boolean {
if (
/(?:\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\u043a|\u043e\u043f\u043b\u0430\u0442|\u043f\u043b\u0430\u0442[\u0435\u0451]\u0436|\u0437\u0430\u043f\u043b\u0430\u0442|\u043f\u0435\u0440\u0435\u0447\u0438\u0441\u043b|\u0441\u043f\u0438\u0441\u0430\u043d|\u0440\u0430\u0441\u0445\u043e\u0434|\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u0432\u0445\u043e\u0434\u044f\u0449|\u043f\u043e\u043b\u0443\u0447(?:\u0438\u043b|\u0435\u043d\u043e|\u0435\u043d)|\u043f\u043e\u0441\u0442\u0443\u043f\u0438\u043b|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d|\u0434\u0435\u043d\p{L}*|\u0437\u0430\u0440\u0430\u0431\u043e\u0442|supplier|value[-\s]?flow|turnover|revenue|payment|payout|outflow|cash\s+flow|\bearn(?:ed|ing|ings)?\b)/iu.test(
text
)
) {
return true;
}
return /(?:оборот|выручк|оплат|плат[её]ж|заплат|перечисл|списан|расход|исходящ|входящ|получ(?:ил|ено|ен)|поступил|поступлен|денежн[а-яёa-z0-9_-]*\s+поток|(?<!\p{L})заработ(?:ал|али|ало|аем|ает|ать|ано|ок)(?!\p{L})|supplier|value[-\s]?flow|turnover|revenue|payment|payout|outflow|cash\s+flow|\bearn(?:ed|ing|ings)?\b)/iu.test(
text
);
@ -1132,6 +1146,13 @@ function hasPayoutSignal(text: string): boolean {
}
function hasBidirectionalValueFlowSignal(text: string): boolean {
if (
/(?:\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|\u0431\u0430\u043b\u0430\u043d\u0441\s+(?:\u043f\u043b\u0430\u0442|\u0434\u0435\u043d\u0435\u0433|\u0434\u0435\u043d\u0435\u0436)|\u0432\u0437\u0430\u0438\u043c\u043e\u0440\u0430\u0441\u0447[\u0435\u0451]\u0442|\u043f\u043e\u043b\u0443\u0447\p{L}*.*(?:\u0437\u0430)?\u043f\u043b\u0430\u0442\p{L}*|(?:\u0437\u0430)?\u043f\u043b\u0430\u0442\p{L}*.*\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0432\u0445\u043e\u0434\u044f\u0449.*\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u0438\u0441\u0445\u043e\u0434\u044f\u0449.*\u0432\u0445\u043e\u0434\u044f\u0449|\u043f\u0440\u0438\u0448\p{L}*.*\u0443\u0448\p{L}*|\u0443\u0448\p{L}*.*\u043f\u0440\u0438\u0448\p{L}*|\u043f\u043b\u044e\u0441.*\u043c\u0438\u043d\u0443\u0441|\u043c\u0438\u043d\u0443\u0441.*\u043f\u043b\u044e\u0441|net\s+(?:flow|cash|payment)|cash\s+net|incoming\s+and\s+outgoing|received\s+and\s+paid|paid\s+and\s+received)/iu.test(
text
)
) {
return true;
}
return /(?:нетто|сальдо|баланс\s+(?:плат|денег|денеж)|взаиморасч[её]т|получил[иа]?.*(?:за)?платил|(?:за)?платил[иа]?.*получил|входящ.*исходящ|исходящ.*входящ|дебет.*кредит|кредит.*дебет|net\s+(?:flow|cash|payment)|cash\s+net|incoming\s+and\s+outgoing|received\s+and\s+paid|paid\s+and\s+received)/iu.test(
text
);
@ -1178,6 +1199,46 @@ function extractOrganizationScopeFromRawText(value: unknown): string | null {
);
}
function extractOrganizationScopeFromSemanticText(value: unknown): string | null {
const text = toNonEmptyString(value);
if (!text) {
return null;
}
const match = text.match(
/(?:^|[\s,;:])(?:\u043a\u043e\u043c\u043f\u0430\u043d(?:\u0438\u0438|\u0438\u044f)|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446(?:\u0438\u0438|\u0438\u044f))\s+(.+?)(?=$|[\n,.;:!?]|\s+\u0437\u0430\s+(?:\d{4}|\d{2}\s*\u0433|\d{2}\s+\u0433\u043e\u0434)|\s+\u043d\u0430\s+(?:\d{4}|\d{2}\s*\u0433|\d{2}\s+\u0433\u043e\u0434))/iu
);
return toNonEmptyString(match?.[1]);
}
function normalizeLooseOrganizationAlias(value: string | null): string | null {
const text = toNonEmptyString(value);
if (!text) {
return null;
}
const normalized = text
.replace(/^[«"']+|[»"']+$/gu, "")
.replace(/\s+/g, " ")
.trim();
if (!normalized) {
return null;
}
const comparable = normalizeRussianComparableText(normalized);
const normalizedTokens = normalized.split(/\s+/u).filter(Boolean);
const hasYearOnlyTimeTail =
/\b(?:19|20)\d{2}\b/u.test(normalized) &&
normalizedTokens.length <= 4 &&
!/(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e|llc|inc|corp)/iu.test(
comparable
);
if (hasYearOnlyTimeTail) {
return null;
}
if (/^(?:\u0438|\u0432|\u0432\u043e|\u0437\u0430|\u043d\u0430|\u043f\u043e|\u043a\u0442\u043e|\u0447\u0442\u043e|\u043a\u0430\u043a(?:\u043e\u0439|\u0430\u044f|\u0438\u0435)?|\u0433\u043b\u0430\u0432\u043d\p{L}*)\b/iu.test(comparable)) {
return null;
}
return normalized;
}
function hasMonthlyAggregationSignal(text: string): boolean {
return /(?:\u043f\u043e\s+\u043c\u0435\u0441\u044f\u0446\u0430\u043c|\u043f\u043e\u043c\u0435\u0441\u044f\u0447\u043d\u043e|\u0435\u0436\u0435\u043c\u0435\u0441\u044f\u0447\u043d\u043e|month\s+by\s+month|by\s+month|monthly)/iu.test(
text
@ -1451,7 +1512,10 @@ function metadataScopeHintFromRawText(text: string): string | null {
}
function hasExplicitDateScopeLiteral(text: string): boolean {
return /(?:\b(?:19|20)\d{2}\b|\b\d{4}-\d{2}-\d{2}\b|\b\d{4}-\d{2}\b)/iu.test(text);
if (/\b\d{2}\s*(?:\u0433(?:\u043e\u0434(?:\u0430|\u0443)?)?\.?)\b/iu.test(text)) {
return true;
}
return /(?:\b(?:19|20)\d{2}\b|\b\d{4}-\d{2}-\d{2}\b|\b\d{4}-\d{2}\b|\b\d{2}\s*(?:г(?:од(?:а|у)?)?\.?))\b/iu.test(text);
}
function stripNegatedTaxDateScopeClauses(text: string): string {
@ -1483,6 +1547,20 @@ function collectDateScopeFromRawText(text: string): string | null {
if (year?.[1]) {
return year[1];
}
const utf8ShortYear = text.match(/\b(\d{2})\s*(?:\u0433(?:\u043e\u0434(?:\u0430|\u0443)?)?\.?)\b/iu);
if (utf8ShortYear?.[1]) {
const numericYear = Number.parseInt(utf8ShortYear[1], 10);
if (Number.isFinite(numericYear)) {
return String(numericYear <= 30 ? 2000 + numericYear : 1900 + numericYear);
}
}
const shortYear = text.match(/\b(\d{2})\s*(?:г(?:од(?:а|у)?)?\.?)\b/iu);
if (shortYear?.[1]) {
const numericYear = Number.parseInt(shortYear[1], 10);
if (Number.isFinite(numericYear)) {
return String(numericYear <= 30 ? 2000 + numericYear : 1900 + numericYear);
}
}
return null;
}
@ -1598,6 +1676,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
const reasonCodes: string[] = [];
const rawUserText = toNonEmptyString(input.userMessage);
const rawEffectiveText = toNonEmptyString(input.effectiveMessage);
const knownOrganizations = Array.isArray(input.knownOrganizations) ? input.knownOrganizations : [];
const repairedUserText = rawUserText ? repairAddressMojibakeText(rawUserText) : null;
const repairedEffectiveText = rawEffectiveText ? repairAddressMojibakeText(rawEffectiveText) : null;
const rawUserSignalSourceText = repairedUserText ?? rawUserText ?? "";
@ -1742,9 +1821,36 @@ export function buildAssistantMcpDiscoveryTurnInput(
)
? null
: rawAssistantTurnMeaningOrganizationScope;
const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText);
const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText ?? rawEffectiveText ?? rawSignalSourceText);
const currentTurnFreshOrganizationScope = predecomposeEntities.organization ?? rawOrganizationScope;
const organizationSelectionFromKnown =
resolveOrganizationSelectionFromMessage(rawUserText ?? rawEffectiveText ?? rawSignalSourceText, knownOrganizations) ??
resolveOrganizationSelectionFromMessage(rawSignalSourceText, knownOrganizations) ??
null;
const rawOrganizationScope =
extractOrganizationScopeFromRawText(rawUserText) ??
extractOrganizationScopeFromRawText(rawEffectiveText) ??
extractOrganizationScopeFromRawText(rawSignalSourceText);
const semanticOrganizationScope =
normalizeLooseOrganizationAlias(
extractOrganizationScopeFromSemanticText(rawEffectiveText) ??
extractOrganizationScopeFromSemanticText(rawUserText) ??
extractOrganizationScopeFromSemanticText(rawSignalSourceText)
);
const predecomposeCounterpartyAsOrganizationScope =
(rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
semanticOrganizationScope &&
!rawScopedEntityCandidate &&
!predecomposeEntities.organization
? normalizeLooseOrganizationAlias(predecomposeEntities.counterparty)
: null;
const rawOrganizationMentionSignal =
hasOrganizationScopeSignalUtf8(rawText) ||
Boolean(organizationSelectionFromKnown || rawOrganizationScope || semanticOrganizationScope || predecomposeCounterpartyAsOrganizationScope);
const currentTurnFreshOrganizationScope =
organizationSelectionFromKnown ??
predecomposeEntities.organization ??
rawOrganizationScope ??
semanticOrganizationScope ??
predecomposeCounterpartyAsOrganizationScope;
const currentTurnOrganizationScope =
currentTurnFreshOrganizationScope ?? assistantTurnMeaningOrganizationScope;
const predecomposeOrganizationMirrorsCounterparty = sameScopedName(
@ -2411,11 +2517,17 @@ export function buildAssistantMcpDiscoveryTurnInput(
const clarificationLoopStillNeedsPeriod = Boolean(
followupSeed.loopStatus === "awaiting_clarification" && followupSeed.loopPendingAxes.includes("period")
);
const rawTwoDigitPredecomposeYearHint = Boolean(
predecomposeDateScope &&
/^\d{4}$/.test(predecomposeDateScope) &&
/\b\d{2}\b/u.test(rawText)
);
const businessOverviewRawWithoutDateScope = Boolean(
businessOverviewSignal &&
!rawAllTimeScopeSignal &&
!explicitDateScopeLiteralDetected &&
!rawDateScope &&
!rawTwoDigitPredecomposeYearHint &&
!relativeCurrentDateHintDetected
);
const predecomposeDateScopeCountsAsCurrentTurnPeriod = Boolean(
@ -2879,6 +2991,15 @@ export function buildAssistantMcpDiscoveryTurnInput(
if (businessOverviewRawYearOverridesPredecomposeAsOf) {
pushReason(reasonCodes, "mcp_discovery_business_overview_raw_year_overrode_predecompose_as_of_scope");
}
if (organizationSelectionFromKnown) {
pushReason(reasonCodes, "mcp_discovery_organization_scope_from_known_organizations");
}
if (semanticOrganizationScope) {
pushReason(reasonCodes, "mcp_discovery_organization_scope_from_semantic_text");
}
if (predecomposeCounterpartyAsOrganizationScope) {
pushReason(reasonCodes, "mcp_discovery_counterparty_reinterpreted_as_organization_scope");
}
if (
!(valueFlowOrganizationStaysScope && normalizedPredecomposeCounterparty === explicitOrganizationScope) &&
normalizedPredecomposeCounterparty

View File

@ -665,9 +665,26 @@ export function createAssistantTransitionPolicy(deps) {
llmPreDecomposeMeta
})
: null;
const hasCompactCashflowFollowupCue = (value) => {
const normalized = normalizeFollowupText(value);
if (!normalized) {
return false;
}
const hasCompactCue = /(?:\u043a\u043e\u0440\u043e\u0442\p{L}*|\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u0431\u0435\u0437\s+\u0442\u043e\u043f\p{L}*)/iu.test(normalized);
const hasCashflowCue = /(?:\u0434\u0435\u043d\p{L}*|\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u043f\u043e\u043b\u0443\u0447|\u0437\u0430\u043f\u043b\u0430\u0442|\u043f\u0440\u0438\u0448\p{L}*|\u0443\u0448\p{L}*|\u043d\u0435\u0442\u0442\u043e)/iu.test(normalized);
return hasCompactCue && hasCashflowCue;
};
const compactCashflowFollowupSignal =
hasShortValueFlowRetargetCue(userMessage) ||
hasCompactCashflowFollowupCue(userMessage) ||
(deps.toNonEmptyString(alternateMessage)
? hasShortValueFlowRetargetCue(String(alternateMessage ?? "")) ||
hasCompactCashflowFollowupCue(String(alternateMessage ?? ""))
: false);
if (
assistantTurnMeaning?.stale_replay_forbidden === true &&
!hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage)
!hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage) &&
!compactCashflowFollowupSignal
) {
return null;
}
@ -763,7 +780,8 @@ export function createAssistantTransitionPolicy(deps) {
sourceIntentHint === "customer_revenue_and_payments" ||
sourceDiscoveryPilotScopeHint === "counterparty_value_flow_query_movements_v1" ||
sourceDiscoveryPilotScopeHint === "counterparty_supplier_payout_query_movements_v1" ||
sourceDiscoveryPilotScopeHint === "counterparty_bidirectional_value_flow_query_movements_v1";
sourceDiscoveryPilotScopeHint === "counterparty_bidirectional_value_flow_query_movements_v1" ||
sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
const hasBusinessOverviewCarryoverSourceHint =
sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
const navigationSessionState = resolveNavigationSessionContextState(
@ -807,10 +825,12 @@ export function createAssistantTransitionPolicy(deps) {
: null;
const debtRoleSwapIntent = debtRoleSwapPrimary ?? debtRoleSwapAlternate ?? null;
const shortValueFlowRetargetPrimary =
hasValueFlowCarryoverSourceHint && hasShortValueFlowRetargetCue(userMessage);
hasValueFlowCarryoverSourceHint &&
(hasShortValueFlowRetargetCue(userMessage) || hasCompactCashflowFollowupCue(userMessage));
const shortValueFlowRetargetAlternate =
hasValueFlowCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
? hasShortValueFlowRetargetCue(String(alternateMessage ?? ""))
? hasShortValueFlowRetargetCue(String(alternateMessage ?? "")) ||
hasCompactCashflowFollowupCue(String(alternateMessage ?? ""))
: false;
const businessOverviewBoundaryFollowupPrimary =
hasBusinessOverviewCarryoverSourceHint && hasBusinessOverviewBoundaryFollowupCue(userMessage);

View File

@ -218,7 +218,8 @@ describe("assistant address orchestration runtime adapter", () => {
root_filters: expect.objectContaining({
organization: "Org A"
})
})
}),
knownOrganizations: ["Org A"]
})
);
});

View File

@ -578,6 +578,76 @@ describe("assistant MCP discovery response candidate", () => {
expect(candidate.reply_text).not.toContain("Складской срез");
});
it("lets compact cashflow wording override profit-boundary and overview prose", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
turn_input: {
adapter_status: "ready",
turn_meaning_ref: {
raw_message:
"\u0421\u043a\u043e\u043b\u044c\u043a\u043e \u0434\u0435\u043d\u0435\u0433 \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0430 \u0437\u0430 2020 \u0433\u043e\u0434? \u041e\u0442\u0432\u0435\u0442\u044c \u043a\u043e\u0440\u043e\u0442\u043a\u043e: \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438, \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438, \u0434\u0435\u043d\u0435\u0436\u043d\u043e\u0435 \u043d\u0435\u0442\u0442\u043e, \u044d\u0442\u043e \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u0438\u043b\u0438 \u043d\u0435\u0442.",
asked_action_family: "profit_margin_boundary",
unsupported_but_understood_family: "profit_margin_boundary",
explicit_organization_scope:
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
explicit_date_scope: "2020"
},
data_need_graph: {
business_fact_family: "business_overview",
ranking_need: null,
reason_codes: [
"data_need_graph_family_business_overview",
"data_need_graph_business_overview_direct_money_answer"
]
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
pilot: {
pilot_scope: "business_overview_route_template_v1",
derived_business_overview: {
period_scope: "2020",
incoming_customer_revenue: {
total_amount_human_ru: "47 628 853,03 руб."
},
outgoing_supplier_payout: {
total_amount_human_ru: "43 763 351,53 руб."
},
net_amount_human_ru: "3 865 501,50 руб.",
net_direction: "net_incoming",
top_customers: [{ axis_value: "СБЕРБАНК", total_amount_human_ru: "12 792 194,31 руб." }],
top_suppliers: [],
accounting_financial_result: {
final_result_direction: "loss",
final_result_amount_human_ru: "7 136 815,85 руб.",
period_scope: "2020"
}
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "wide overview should not leak",
confirmed_lines: [],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: null
}
}
})
);
expect(candidate.reply_text).toContain("47 628 853,03");
expect(candidate.reply_text).toContain("43 763 351,53");
expect(candidate.reply_text).toContain("3 865 501,50");
expect(candidate.reply_text).not.toContain("СБЕРБАНК");
expect(candidate.reply_text).not.toContain("7 136 815,85");
expect(candidate.reply_text).not.toContain("wide overview");
});
it("labels organization-scoped bidirectional value-flow continuations as company scope", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({

View File

@ -1789,6 +1789,56 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_counterparty_from_predecompose");
});
it("grounds organization aliases from known organizations for clean-session earnings questions", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "скока денег альтернатива заработала за 20 год?",
effectiveMessage: "скока денег альтернатива заработала за 20 год?",
knownOrganizations: ["ООО Альтернатива Плюс", "ООО Лайсвуд", "РАЙМ"],
predecomposeContract: {
period: {
period_from: "2020-01-01",
period_to: "2020-12-31",
has_explicit_period: true
}
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.turn_meaning_ref?.explicit_organization_scope).toBe("ООО Альтернатива Плюс");
expect(result.turn_meaning_ref?.explicit_date_scope).toBe("2020");
expect(result.reason_codes).toContain("mcp_discovery_organization_scope_from_known_organizations");
expect(result.data_need_graph?.clarification_gaps ?? []).not.toContain("organization");
});
it("treats a business-overview company alias misread as counterparty as organization scope", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "Сколько денег Альтернатива заработала за 2020 год?",
effectiveMessage: "Определить финансовый результат компании Альтернатива за 2020 год",
assistantTurnMeaning: {
asked_domain_family: "business_summary",
asked_action_family: "broad_evaluation",
unsupported_but_understood_family: "broad_business_evaluation"
},
predecomposeContract: {
entities: { counterparty: "Альтернатива", organization: null },
period: {
period_from: "2020-01-01",
period_to: "2020-12-31",
has_explicit_period: true
}
}
});
expect(result.adapter_status).toBe("ready");
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.turn_meaning_ref?.explicit_organization_scope).toBe("Альтернатива");
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.data_need_graph?.clarification_gaps ?? []).not.toContain("organization");
expect(result.reason_codes).toContain("mcp_discovery_counterparty_reinterpreted_as_organization_scope");
});
it("keeps all-time business overview from reusing a negated VAT period as active scope", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
@ -2308,6 +2358,28 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.data_need_graph?.reason_codes).toContain("data_need_graph_business_overview_direct_money_answer");
});
it("does not inherit ranking when a cashflow follow-up explicitly says no overview or tops", () => {
const orgName = "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
"\u043d\u0435 \u043e\u0431\u0437\u043e\u0440, \u043f\u0440\u043e\u0441\u0442\u043e \u0434\u0435\u043d\u044c\u0433\u0438: \u043f\u0440\u0438\u0448\u043b\u043e, \u0443\u0448\u043b\u043e, \u043d\u0435\u0442\u0442\u043e \u0437\u0430 2020 \u0431\u0435\u0437 \u0442\u043e\u043f\u043e\u0432",
followupContext: {
previous_discovery_pilot_scope: "business_overview_route_template_v1",
previous_filters: {
organization: orgName
},
previous_seeded_ranking_need: "top_desc"
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.data_need_graph?.ranking_need).toBeNull();
expect(result.turn_meaning_ref?.explicit_date_scope).toBe("2020");
expect(result.data_need_graph?.reason_codes).toContain("data_need_graph_business_overview_direct_money_answer");
});
it("routes organization-level profit and margin wording to business overview instead of exact value recipes", () => {
const orgName = "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const result = buildAssistantMcpDiscoveryTurnInput({

View File

@ -1,4 +1,66 @@
[
{
"generation_id": "gen-ag05231011-cec910",
"created_at": "2026-05-23T10:11:40+00:00",
"mode": "saved_user_sessions",
"title": "AGENT | Cashflow vs profit directness pack",
"count": 8,
"domain": "autonomy_business_answer_contract",
"questions": [
"Сколько денег Альтернатива заработала за 2020 год? Ответь коротко: получили, заплатили, денежное нетто, это прибыль или нет.",
"скока денег альтернатива заработала за 20 год?",
"а это чистая прибыль?",
"а по деньгам тогда плюс или минус?",
"Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток.",
"а если коротко, сколько заработали деньгами без топов?",
"не обзор, просто деньги: пришло, ушло, нетто за 2020",
"какая чистая прибыль по Альтернативе за 2020?"
],
"generated_by": "codex_agent",
"saved_case_set_file": "assistant_autogen_saved_user_sessions_20260523101140_gen-ag05231011-cec910.json",
"context": {
"llm_provider": null,
"model": null,
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"autogen_personality_id": null,
"autogen_personality_prompt": null,
"source_session_id": null,
"saved_session_file": "assistant_saved_session_20260523101140_gen-ag05231011-cec910.json",
"saved_case_set_kind": "agent_semantic_scenario",
"agent_run": true,
"agent_focus": "Targeted AGENT replay for cashflow-vs-profit directness: colloquial earnings questions must produce a compact money answer, while broad overview remains available only when explicitly requested.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\agent_cashflow_profit_directness_20260523.json",
"scenario_id": "agent_cashflow_profit_directness_20260523",
"semantic_tags": [
"bank_flow_boundary",
"business_overview",
"cashflow_polarity",
"cashflow_vs_profit",
"clean_profit",
"colloquial_earnings",
"compact_after_overview",
"direct_answer",
"direct_money_only",
"followup_context",
"next_steps",
"no_all_time_leak",
"no_cashflow_substitution",
"no_overview_substitution",
"no_profit_substitution",
"profit_route",
"profit_vs_cashflow",
"ranking_suppression",
"slang_wording",
"temporal_carryover"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\agent_cashflow_profit_directness_live11",
"saved_after_validated_replay": true
}
},
{
"generation_id": "gen-ag05230604-098bda",
"created_at": "2026-05-23T06:04:40+00:00",

View File

@ -0,0 +1,195 @@
{
"saved_at": "2026-05-23T10:11:40+00:00",
"generation_id": "gen-ag05231011-cec910",
"mode": "saved_user_sessions",
"title": "AGENT | Cashflow vs profit directness pack",
"agent_run": true,
"questions": [
"Сколько денег Альтернатива заработала за 2020 год? Ответь коротко: получили, заплатили, денежное нетто, это прибыль или нет.",
"скока денег альтернатива заработала за 20 год?",
"а это чистая прибыль?",
"а по деньгам тогда плюс или минус?",
"Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток.",
"а если коротко, сколько заработали деньгами без топов?",
"не обзор, просто деньги: пришло, ушло, нетто за 2020",
"какая чистая прибыль по Альтернативе за 2020?"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Targeted AGENT replay for cashflow-vs-profit directness: colloquial earnings questions must produce a compact money answer, while broad overview remains available only when explicitly requested.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\agent_cashflow_profit_directness_20260523.json",
"scenario_id": "agent_cashflow_profit_directness_20260523",
"semantic_tags": [
"bank_flow_boundary",
"business_overview",
"cashflow_polarity",
"cashflow_vs_profit",
"clean_profit",
"colloquial_earnings",
"compact_after_overview",
"direct_answer",
"direct_money_only",
"followup_context",
"next_steps",
"no_all_time_leak",
"no_cashflow_substitution",
"no_overview_substitution",
"no_profit_substitution",
"profit_route",
"profit_vs_cashflow",
"ranking_suppression",
"slang_wording",
"temporal_carryover"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\agent_cashflow_profit_directness_live11",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\agent_cashflow_profit_directness_live11",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 8,
"steps_passed": 8,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "Сколько денег Альтернатива заработала за 2020 год? Ответь коротко: получили, заплатили, денежное нетто, это прибыль или нет.",
"created_at": "2026-05-23T10:11:40+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "скока денег альтернатива заработала за 20 год?",
"created_at": "2026-05-23T10:11:40+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "а это чистая прибыль?",
"created_at": "2026-05-23T10:11:40+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "а по деньгам тогда плюс или минус?",
"created_at": "2026-05-23T10:11:40+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-005",
"role": "user",
"text": "Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток.",
"created_at": "2026-05-23T10:11:40+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-006",
"role": "user",
"text": "а если коротко, сколько заработали деньгами без топов?",
"created_at": "2026-05-23T10:11:40+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-007",
"role": "user",
"text": "не обзор, просто деньги: пришло, ушло, нетто за 2020",
"created_at": "2026-05-23T10:11:40+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-008",
"role": "user",
"text": "какая чистая прибыль по Альтернативе за 2020?",
"created_at": "2026-05-23T10:11:40+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
}
],
"agent_run": true,
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Targeted AGENT replay for cashflow-vs-profit directness: colloquial earnings questions must produce a compact money answer, while broad overview remains available only when explicitly requested.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\agent_cashflow_profit_directness_20260523.json",
"scenario_id": "agent_cashflow_profit_directness_20260523",
"semantic_tags": [
"bank_flow_boundary",
"business_overview",
"cashflow_polarity",
"cashflow_vs_profit",
"clean_profit",
"colloquial_earnings",
"compact_after_overview",
"direct_answer",
"direct_money_only",
"followup_context",
"next_steps",
"no_all_time_leak",
"no_cashflow_substitution",
"no_overview_substitution",
"no_profit_substitution",
"profit_route",
"profit_vs_cashflow",
"ranking_suppression",
"slang_wording",
"temporal_carryover"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\agent_cashflow_profit_directness_live11",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\agent_cashflow_profit_directness_live11",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 8,
"steps_passed": 8,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
}
}
}

View File

@ -0,0 +1,49 @@
{
"suite_id": "assistant_saved_session_gen-ag05231011-cec910",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-05-23T10:11:40+00:00",
"generation_id": "gen-ag05231011-cec910",
"mode": "saved_user_sessions",
"title": "AGENT | Cashflow vs profit directness pack",
"domain": "autonomy_business_answer_contract",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Cashflow vs profit directness pack",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "Сколько денег Альтернатива заработала за 2020 год? Ответь коротко: получили, заплатили, денежное нетто, это прибыль или нет."
},
{
"user_message": "скока денег альтернатива заработала за 20 год?"
},
{
"user_message": "а это чистая прибыль?"
},
{
"user_message": "а по деньгам тогда плюс или минус?"
},
{
"user_message": "Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток."
},
{
"user_message": "а если коротко, сколько заработали деньгами без топов?"
},
{
"user_message": "не обзор, просто деньги: пришло, ушло, нетто за 2020"
},
{
"user_message": "какая чистая прибыль по Альтернативе за 2020?"
}
]
}
]
}