Стабилизировать денежные ответы и прибыль 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); const scope = toRecordObject(sessionOrganizationScope);
return toNonEmptyString(scope?.selectedOrganization) ?? toNonEmptyString(scope?.activeOrganization); 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) { function predecomposeOrganizationName(predecomposeContract, toNonEmptyString) {
const entities = toRecordObject(predecomposeContract?.entities); const entities = toRecordObject(predecomposeContract?.entities);
return (toNonEmptyString(entities?.organization) ?? return (toNonEmptyString(entities?.organization) ??
@ -223,7 +228,8 @@ async function buildAssistantAddressOrchestrationRuntime(input) {
effectiveMessage: addressInputMessage, effectiveMessage: addressInputMessage,
assistantTurnMeaning: toRecordObject(orchestrationContract?.assistant_turn_meaning), assistantTurnMeaning: toRecordObject(orchestrationContract?.assistant_turn_meaning),
predecomposeContract, predecomposeContract,
followupContext: discoveryFollowupContext followupContext: discoveryFollowupContext,
knownOrganizations: sessionKnownOrganizations(input.sessionOrganizationScope ?? null)
})); }));
} }
catch (error) { catch (error) {

View File

@ -115,6 +115,12 @@ function hasBusinessOverviewDirectMoneyAnswerHint(input) {
return true; return true;
} }
const text = input.rawUtterance; 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); 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) { function timeScopeNeedFor(input) {
@ -225,6 +231,13 @@ function rankingNeedFromRawUtterance(value) {
} }
return null; 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) { function proofExpectationFor(input) {
if (input.clarificationGaps.length > 0) { if (input.clarificationGaps.length > 0) {
return "clarification_required"; return "clarification_required";
@ -383,6 +396,7 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
const action = lower(turnMeaning?.asked_action_family); const action = lower(turnMeaning?.asked_action_family);
const unsupported = lower(turnMeaning?.unsupported_but_understood_family); const unsupported = lower(turnMeaning?.unsupported_but_understood_family);
const rawUtterance = lower(input.rawUtterance); 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 aggregationAxis = lower(turnMeaning?.asked_aggregation_axis);
const seededRankingNeed = toNonEmptyString(turnMeaning?.seeded_ranking_need); const seededRankingNeed = toNonEmptyString(turnMeaning?.seeded_ranking_need);
const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope); const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope);
@ -400,15 +414,18 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
}); });
const aggregationNeed = aggregationNeedFor(aggregationAxis); const aggregationNeed = aggregationNeedFor(aggregationAxis);
const comparisonNeed = comparisonNeedFor(action); const comparisonNeed = comparisonNeedFor(action);
const rankingNeed = rankingNeedFromRawUtterance(rawUtterance) ?? seededRankingNeed;
const allTimeScopeHint = hasAllTimeScopeHint(rawUtterance); const allTimeScopeHint = hasAllTimeScopeHint(rawUtterance);
const subjectScopedBidirectionalAllTime = businessFactFamily === "value_flow" && const subjectScopedBidirectionalAllTime = businessFactFamily === "value_flow" &&
comparisonNeed === "incoming_vs_outgoing" && comparisonNeed === "incoming_vs_outgoing" &&
subjectCandidates.length > 0 && subjectCandidates.length > 0 &&
!explicitDateScope; !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({ const directBusinessOverviewMoneyAnswerHint = hasBusinessOverviewDirectMoneyAnswerHint({
family: businessFactFamily, family: businessFactFamily,
rawUtterance, rawUtterance: rawQuestionSignal,
rankingNeed rankingNeed
}); });
const oneSidedOpenScopeTotalHint = hasOpenScopeOneSidedValueTotalHintUtf8Safe(rawUtterance, action); const oneSidedOpenScopeTotalHint = hasOpenScopeOneSidedValueTotalHintUtf8Safe(rawUtterance, action);

View File

@ -34,6 +34,30 @@ function requestsFinancialCounterpartyBoundary(turnMeaning, graph) {
return (/(?:банк|сбербанк|финанс|кредит|депозит)/iu.test(text) && return (/(?:банк|сбербанк|финанс|кредит|депозит)/iu.test(text) &&
/(?:клиент|поставщик|выручк|топ|обычн|роль|поток)/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) { function toStringList(value) {
if (!Array.isArray(value)) { if (!Array.isArray(value)) {
return []; return [];
@ -595,6 +619,18 @@ function compactComparable(value) {
.replace(/\s+/g, " ") .replace(/\s+/g, " ")
.trim(); .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) { function businessOverviewSeparateSubjectLabel(graph, turnMeaning, organizationScope) {
const candidates = uniqueStrings([ const candidates = uniqueStrings([
...toStringList(turnMeaning?.business_overview_separate_entity_candidates), ...toStringList(turnMeaning?.business_overview_separate_entity_candidates),
@ -686,7 +722,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
const netDirection = overview.net_direction === "net_outgoing" ? "операционное нетто в минус" : "расчетное операционное нетто"; const netDirection = overview.net_direction === "net_outgoing" ? "операционное нетто в минус" : "расчетное операционное нетто";
const period = businessOverviewPeriodText(overview); const period = businessOverviewPeriodText(overview);
const limitLine = businessOverviewCoverageLimitLine(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 separateSubject = businessOverviewSeparateSubjectLabel(graph, turnMeaning, organizationScope);
const previousCounterpartySummary = buildPreviousCounterpartyValueFlowSummary(toRecordObject(turnMeaning?.previous_counterparty_value_flow_bundle), separateSubject, toRecordObject(turnMeaning?.previous_counterparty_document_bundle)); const previousCounterpartySummary = buildPreviousCounterpartyValueFlowSummary(toRecordObject(turnMeaning?.previous_counterparty_value_flow_bundle), separateSubject, toRecordObject(turnMeaning?.previous_counterparty_document_bundle));
const organizationPrefix = organizationScope ? `по компании ${organizationScope} ` : ""; 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 debtDueDateBoundary = actionFamily === "debt_due_date_boundary" || unsupportedFamily === "debt_due_date_boundary";
const vendorRiskBoundary = actionFamily === "vendor_risk_procurement_boundary" || unsupportedFamily === "vendor_risk_procurement_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 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) { if (profitMarginBoundary) {
const accountingFinancialResult = toRecordObject(overview.accounting_financial_result); const accountingFinancialResult = toRecordObject(overview.accounting_financial_result);
if (accountingFinancialResult) { 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.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION = void 0;
exports.buildAssistantMcpDiscoveryTurnInput = buildAssistantMcpDiscoveryTurnInput; exports.buildAssistantMcpDiscoveryTurnInput = buildAssistantMcpDiscoveryTurnInput;
const assistantMcpDiscoveryDataNeedGraph_1 = require("./assistantMcpDiscoveryDataNeedGraph"); const assistantMcpDiscoveryDataNeedGraph_1 = require("./assistantMcpDiscoveryDataNeedGraph");
const assistantOrganizationMatcher_1 = require("./assistantOrganizationMatcher");
const addressTextRepair_1 = require("./addressTextRepair"); const addressTextRepair_1 = require("./addressTextRepair");
exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION = "assistant_mcp_discovery_turn_input_v1"; exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION = "assistant_mcp_discovery_turn_input_v1";
function toRecordObject(value) { 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 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) && 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); /(?:\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 || return (hasEvidenceContinuationCue ||
hasAnalystContinuationCue || hasAnalystContinuationCue ||
hasTaxContinuationCue || hasTaxContinuationCue ||
hasFinalSummaryCue || hasFinalSummaryCue ||
hasMoneyBreakdownCue); hasMoneyBreakdownCue ||
hasCashflowPolarityCue);
} }
function hasExplicitVatQuestionSignal(text) { function hasExplicitVatQuestionSignal(text) {
if (!text) { if (!text) {
@ -785,6 +788,9 @@ function hasBusinessOverviewFollowupSeed(followupSeed) {
followupSeed.loopSelectedChainId === "business_overview"); followupSeed.loopSelectedChainId === "business_overview");
} }
function hasValueFlowSignal(text) { 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); 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) { 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); return /(?:\bмы\s+(?:за)?плат|(?:за)?платил|оплатил|перечисл|списан|расход|поставщик|исходящ|supplier|payout|outflow|paid\s+to|payment\s+to)/iu.test(text);
} }
function hasBidirectionalValueFlowSignal(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); 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) { 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, "") .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()); .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) { 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); 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; return null;
} }
function hasExplicitDateScopeLiteral(text) { 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) { function stripNegatedTaxDateScopeClauses(text) {
const dateScopeLiteral = String.raw `\b(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{2}|(?:19|20)\d{2})\b`; 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]) { if (year?.[1]) {
return 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; return null;
} }
function currentIsoDate() { function currentIsoDate() {
@ -1137,6 +1196,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const reasonCodes = []; const reasonCodes = [];
const rawUserText = toNonEmptyString(input.userMessage); const rawUserText = toNonEmptyString(input.userMessage);
const rawEffectiveText = toNonEmptyString(input.effectiveMessage); const rawEffectiveText = toNonEmptyString(input.effectiveMessage);
const knownOrganizations = Array.isArray(input.knownOrganizations) ? input.knownOrganizations : [];
const repairedUserText = rawUserText ? (0, addressTextRepair_1.repairAddressMojibakeText)(rawUserText) : null; const repairedUserText = rawUserText ? (0, addressTextRepair_1.repairAddressMojibakeText)(rawUserText) : null;
const repairedEffectiveText = rawEffectiveText ? (0, addressTextRepair_1.repairAddressMojibakeText)(rawEffectiveText) : null; const repairedEffectiveText = rawEffectiveText ? (0, addressTextRepair_1.repairAddressMojibakeText)(rawEffectiveText) : null;
const rawUserSignalSourceText = repairedUserText ?? rawUserText ?? ""; const rawUserSignalSourceText = repairedUserText ?? rawUserText ?? "";
@ -1246,9 +1306,28 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const assistantTurnMeaningOrganizationScope = isReferentialOrganizationPlaceholder(rawAssistantTurnMeaningOrganizationScope) const assistantTurnMeaningOrganizationScope = isReferentialOrganizationPlaceholder(rawAssistantTurnMeaningOrganizationScope)
? null ? null
: rawAssistantTurnMeaningOrganizationScope; : rawAssistantTurnMeaningOrganizationScope;
const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText); const organizationSelectionFromKnown = (0, assistantOrganizationMatcher_1.resolveOrganizationSelectionFromMessage)(rawUserText ?? rawEffectiveText ?? rawSignalSourceText, knownOrganizations) ??
const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText ?? rawEffectiveText ?? rawSignalSourceText); (0, assistantOrganizationMatcher_1.resolveOrganizationSelectionFromMessage)(rawSignalSourceText, knownOrganizations) ??
const currentTurnFreshOrganizationScope = predecomposeEntities.organization ?? rawOrganizationScope; 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 currentTurnOrganizationScope = currentTurnFreshOrganizationScope ?? assistantTurnMeaningOrganizationScope;
const predecomposeOrganizationMirrorsCounterparty = sameScopedName(predecomposeEntities.counterparty, predecomposeEntities.organization); const predecomposeOrganizationMirrorsCounterparty = sameScopedName(predecomposeEntities.counterparty, predecomposeEntities.organization);
const organizationMirrorsPredecomposeCounterpartyForPivot = Boolean(sameScopedName(predecomposeEntities.counterparty, assistantTurnMeaningOrganizationScope) || 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 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 && const businessOverviewRawWithoutDateScope = Boolean(businessOverviewSignal &&
!rawAllTimeScopeSignal && !rawAllTimeScopeSignal &&
!explicitDateScopeLiteralDetected && !explicitDateScopeLiteralDetected &&
!rawDateScope && !rawDateScope &&
!rawTwoDigitPredecomposeYearHint &&
!relativeCurrentDateHintDetected); !relativeCurrentDateHintDetected);
const predecomposeDateScopeCountsAsCurrentTurnPeriod = Boolean(predecomposeDateScope && const predecomposeDateScopeCountsAsCurrentTurnPeriod = Boolean(predecomposeDateScope &&
!isImplicitCurrentDateScope(predecomposeDateScope) && !isImplicitCurrentDateScope(predecomposeDateScope) &&
@ -2236,6 +2319,15 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
if (businessOverviewRawYearOverridesPredecomposeAsOf) { if (businessOverviewRawYearOverridesPredecomposeAsOf) {
pushReason(reasonCodes, "mcp_discovery_business_overview_raw_year_overrode_predecompose_as_of_scope"); 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) && if (!(valueFlowOrganizationStaysScope && normalizedPredecomposeCounterparty === explicitOrganizationScope) &&
normalizedPredecomposeCounterparty) { normalizedPredecomposeCounterparty) {
pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose"); pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose");

View File

@ -489,8 +489,24 @@ function createAssistantTransitionPolicy(deps) {
llmPreDecomposeMeta llmPreDecomposeMeta
}) })
: null; : 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 && if (assistantTurnMeaning?.stale_replay_forbidden === true &&
!hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage)) { !hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage) &&
!compactCashflowFollowupSignal) {
return null; return null;
} }
const latestAddressItem = deps.findLastAddressAssistantItem(items); const latestAddressItem = deps.findLastAddressAssistantItem(items);
@ -555,7 +571,8 @@ function createAssistantTransitionPolicy(deps) {
const hasValueFlowCarryoverSourceHint = sourceIntentHint === "customer_revenue_and_payments" || const hasValueFlowCarryoverSourceHint = sourceIntentHint === "customer_revenue_and_payments" ||
sourceDiscoveryPilotScopeHint === "counterparty_value_flow_query_movements_v1" || sourceDiscoveryPilotScopeHint === "counterparty_value_flow_query_movements_v1" ||
sourceDiscoveryPilotScopeHint === "counterparty_supplier_payout_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 hasBusinessOverviewCarryoverSourceHint = sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
const navigationSessionState = (0, assistantContinuityPolicy_1.resolveNavigationSessionContextState)(addressNavigationState, deps.toNonEmptyString, deps.normalizeOrganizationScopeValue); const navigationSessionState = (0, assistantContinuityPolicy_1.resolveNavigationSessionContextState)(addressNavigationState, deps.toNonEmptyString, deps.normalizeOrganizationScopeValue);
const navigationFocusObjectHint = navigationSessionState.focusObject; const navigationFocusObjectHint = navigationSessionState.focusObject;
@ -579,9 +596,11 @@ function createAssistantTransitionPolicy(deps) {
? deps.resolveDebtRoleSwapFollowupIntent(String(alternateMessage ?? ""), sourceIntentHint) ? deps.resolveDebtRoleSwapFollowupIntent(String(alternateMessage ?? ""), sourceIntentHint)
: null; : null;
const debtRoleSwapIntent = debtRoleSwapPrimary ?? debtRoleSwapAlternate ?? null; const debtRoleSwapIntent = debtRoleSwapPrimary ?? debtRoleSwapAlternate ?? null;
const shortValueFlowRetargetPrimary = hasValueFlowCarryoverSourceHint && hasShortValueFlowRetargetCue(userMessage); const shortValueFlowRetargetPrimary = hasValueFlowCarryoverSourceHint &&
(hasShortValueFlowRetargetCue(userMessage) || hasCompactCashflowFollowupCue(userMessage));
const shortValueFlowRetargetAlternate = hasValueFlowCarryoverSourceHint && deps.toNonEmptyString(alternateMessage) const shortValueFlowRetargetAlternate = hasValueFlowCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
? hasShortValueFlowRetargetCue(String(alternateMessage ?? "")) ? hasShortValueFlowRetargetCue(String(alternateMessage ?? "")) ||
hasCompactCashflowFollowupCue(String(alternateMessage ?? ""))
: false; : false;
const businessOverviewBoundaryFollowupPrimary = hasBusinessOverviewCarryoverSourceHint && hasBusinessOverviewBoundaryFollowupCue(userMessage); const businessOverviewBoundaryFollowupPrimary = hasBusinessOverviewCarryoverSourceHint && hasBusinessOverviewBoundaryFollowupCue(userMessage);
const businessOverviewBoundaryFollowupAlternate = hasBusinessOverviewCarryoverSourceHint && deps.toNonEmptyString(alternateMessage) const businessOverviewBoundaryFollowupAlternate = hasBusinessOverviewCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)

View File

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

View File

@ -176,6 +176,18 @@ function hasBusinessOverviewDirectMoneyAnswerHint(input: {
return true; return true;
} }
const text = input.rawUtterance; 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( 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 text
); );
@ -330,6 +342,16 @@ function rankingNeedFromRawUtterance(value: string): string | null {
return 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: { function proofExpectationFor(input: {
family: string | null; family: string | null;
clarificationGaps: string[]; clarificationGaps: string[];
@ -503,6 +525,7 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
const action = lower(turnMeaning?.asked_action_family); const action = lower(turnMeaning?.asked_action_family);
const unsupported = lower(turnMeaning?.unsupported_but_understood_family); const unsupported = lower(turnMeaning?.unsupported_but_understood_family);
const rawUtterance = lower(input.rawUtterance); 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 aggregationAxis = lower(turnMeaning?.asked_aggregation_axis);
const seededRankingNeed = toNonEmptyString(turnMeaning?.seeded_ranking_need); const seededRankingNeed = toNonEmptyString(turnMeaning?.seeded_ranking_need);
const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope); const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope);
@ -520,16 +543,22 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
}); });
const aggregationNeed = aggregationNeedFor(aggregationAxis); const aggregationNeed = aggregationNeedFor(aggregationAxis);
const comparisonNeed = comparisonNeedFor(action); const comparisonNeed = comparisonNeedFor(action);
const rankingNeed = rankingNeedFromRawUtterance(rawUtterance) ?? seededRankingNeed;
const allTimeScopeHint = hasAllTimeScopeHint(rawUtterance); const allTimeScopeHint = hasAllTimeScopeHint(rawUtterance);
const subjectScopedBidirectionalAllTime = const subjectScopedBidirectionalAllTime =
businessFactFamily === "value_flow" && businessFactFamily === "value_flow" &&
comparisonNeed === "incoming_vs_outgoing" && comparisonNeed === "incoming_vs_outgoing" &&
subjectCandidates.length > 0 && subjectCandidates.length > 0 &&
!explicitDateScope; !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({ const directBusinessOverviewMoneyAnswerHint = hasBusinessOverviewDirectMoneyAnswerHint({
family: businessFactFamily, family: businessFactFamily,
rawUtterance, rawUtterance: rawQuestionSignal,
rankingNeed rankingNeed
}); });
const oneSidedOpenScopeTotalHint = hasOpenScopeOneSidedValueTotalHintUtf8Safe(rawUtterance, action); 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[] { function toStringList(value: unknown): string[] {
if (!Array.isArray(value)) { if (!Array.isArray(value)) {
return []; return [];
@ -689,6 +729,21 @@ function compactComparable(value: string | null): string {
.trim(); .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( function businessOverviewSeparateSubjectLabel(
graph: Record<string, unknown> | null, graph: Record<string, unknown> | null,
turnMeaning: Record<string, unknown> | null, turnMeaning: Record<string, unknown> | null,
@ -802,7 +857,7 @@ function buildCompactBusinessOverviewReply(
const netDirection = overview.net_direction === "net_outgoing" ? "операционное нетто в минус" : "расчетное операционное нетто"; const netDirection = overview.net_direction === "net_outgoing" ? "операционное нетто в минус" : "расчетное операционное нетто";
const period = businessOverviewPeriodText(overview); const period = businessOverviewPeriodText(overview);
const limitLine = businessOverviewCoverageLimitLine(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 separateSubject = businessOverviewSeparateSubjectLabel(graph, turnMeaning, organizationScope);
const previousCounterpartySummary = buildPreviousCounterpartyValueFlowSummary( const previousCounterpartySummary = buildPreviousCounterpartyValueFlowSummary(
toRecordObject(turnMeaning?.previous_counterparty_value_flow_bundle), toRecordObject(turnMeaning?.previous_counterparty_value_flow_bundle),
@ -857,6 +912,28 @@ function buildCompactBusinessOverviewReply(
actionFamily === "vendor_risk_procurement_boundary" || unsupportedFamily === "vendor_risk_procurement_boundary"; actionFamily === "vendor_risk_procurement_boundary" || unsupportedFamily === "vendor_risk_procurement_boundary";
const inventoryReserveBoundary = const inventoryReserveBoundary =
actionFamily === "inventory_reserve_boundary" || unsupportedFamily === "inventory_reserve_liquidation_boundary"; 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) { if (profitMarginBoundary) {
const accountingFinancialResult = toRecordObject(overview.accounting_financial_result); const accountingFinancialResult = toRecordObject(overview.accounting_financial_result);

View File

@ -3,6 +3,7 @@ import {
buildAssistantMcpDiscoveryDataNeedGraph, buildAssistantMcpDiscoveryDataNeedGraph,
type AssistantMcpDiscoveryDataNeedGraphContract type AssistantMcpDiscoveryDataNeedGraphContract
} from "./assistantMcpDiscoveryDataNeedGraph"; } from "./assistantMcpDiscoveryDataNeedGraph";
import { resolveOrganizationSelectionFromMessage } from "./assistantOrganizationMatcher";
import { normalizeRussianComparableText, repairAddressMojibakeText } from "./addressTextRepair"; import { normalizeRussianComparableText, repairAddressMojibakeText } from "./addressTextRepair";
import type { import type {
AssistantMcpDiscoveryMetadataRecommendedPrimitive, AssistantMcpDiscoveryMetadataRecommendedPrimitive,
@ -27,6 +28,7 @@ export interface BuildAssistantMcpDiscoveryTurnInputAdapterInput {
followupContext?: Record<string, unknown> | null; followupContext?: Record<string, unknown> | null;
userMessage?: string | null; userMessage?: string | null;
effectiveMessage?: string | null; effectiveMessage?: string | null;
knownOrganizations?: unknown[] | null;
} }
export interface AssistantMcpDiscoveryTurnInputContract { 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( /(?:\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 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 ( return (
hasEvidenceContinuationCue || hasEvidenceContinuationCue ||
hasAnalystContinuationCue || hasAnalystContinuationCue ||
hasTaxContinuationCue || hasTaxContinuationCue ||
hasFinalSummaryCue || hasFinalSummaryCue ||
hasMoneyBreakdownCue hasMoneyBreakdownCue ||
hasCashflowPolarityCue
); );
} }
@ -1114,6 +1121,13 @@ function hasBusinessOverviewFollowupSeed(followupSeed: ReturnType<typeof collect
} }
function hasValueFlowSignal(text: string): boolean { 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( 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 text
); );
@ -1132,6 +1146,13 @@ function hasPayoutSignal(text: string): boolean {
} }
function hasBidirectionalValueFlowSignal(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( 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 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 { 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( 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 text
@ -1451,7 +1512,10 @@ function metadataScopeHintFromRawText(text: string): string | null {
} }
function hasExplicitDateScopeLiteral(text: string): boolean { 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 { function stripNegatedTaxDateScopeClauses(text: string): string {
@ -1483,6 +1547,20 @@ function collectDateScopeFromRawText(text: string): string | null {
if (year?.[1]) { if (year?.[1]) {
return 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; return null;
} }
@ -1598,6 +1676,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
const reasonCodes: string[] = []; const reasonCodes: string[] = [];
const rawUserText = toNonEmptyString(input.userMessage); const rawUserText = toNonEmptyString(input.userMessage);
const rawEffectiveText = toNonEmptyString(input.effectiveMessage); const rawEffectiveText = toNonEmptyString(input.effectiveMessage);
const knownOrganizations = Array.isArray(input.knownOrganizations) ? input.knownOrganizations : [];
const repairedUserText = rawUserText ? repairAddressMojibakeText(rawUserText) : null; const repairedUserText = rawUserText ? repairAddressMojibakeText(rawUserText) : null;
const repairedEffectiveText = rawEffectiveText ? repairAddressMojibakeText(rawEffectiveText) : null; const repairedEffectiveText = rawEffectiveText ? repairAddressMojibakeText(rawEffectiveText) : null;
const rawUserSignalSourceText = repairedUserText ?? rawUserText ?? ""; const rawUserSignalSourceText = repairedUserText ?? rawUserText ?? "";
@ -1742,9 +1821,36 @@ export function buildAssistantMcpDiscoveryTurnInput(
) )
? null ? null
: rawAssistantTurnMeaningOrganizationScope; : rawAssistantTurnMeaningOrganizationScope;
const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText); const organizationSelectionFromKnown =
const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText ?? rawEffectiveText ?? rawSignalSourceText); resolveOrganizationSelectionFromMessage(rawUserText ?? rawEffectiveText ?? rawSignalSourceText, knownOrganizations) ??
const currentTurnFreshOrganizationScope = predecomposeEntities.organization ?? rawOrganizationScope; 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 = const currentTurnOrganizationScope =
currentTurnFreshOrganizationScope ?? assistantTurnMeaningOrganizationScope; currentTurnFreshOrganizationScope ?? assistantTurnMeaningOrganizationScope;
const predecomposeOrganizationMirrorsCounterparty = sameScopedName( const predecomposeOrganizationMirrorsCounterparty = sameScopedName(
@ -2411,11 +2517,17 @@ export function buildAssistantMcpDiscoveryTurnInput(
const clarificationLoopStillNeedsPeriod = Boolean( const clarificationLoopStillNeedsPeriod = Boolean(
followupSeed.loopStatus === "awaiting_clarification" && followupSeed.loopPendingAxes.includes("period") 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( const businessOverviewRawWithoutDateScope = Boolean(
businessOverviewSignal && businessOverviewSignal &&
!rawAllTimeScopeSignal && !rawAllTimeScopeSignal &&
!explicitDateScopeLiteralDetected && !explicitDateScopeLiteralDetected &&
!rawDateScope && !rawDateScope &&
!rawTwoDigitPredecomposeYearHint &&
!relativeCurrentDateHintDetected !relativeCurrentDateHintDetected
); );
const predecomposeDateScopeCountsAsCurrentTurnPeriod = Boolean( const predecomposeDateScopeCountsAsCurrentTurnPeriod = Boolean(
@ -2879,6 +2991,15 @@ export function buildAssistantMcpDiscoveryTurnInput(
if (businessOverviewRawYearOverridesPredecomposeAsOf) { if (businessOverviewRawYearOverridesPredecomposeAsOf) {
pushReason(reasonCodes, "mcp_discovery_business_overview_raw_year_overrode_predecompose_as_of_scope"); 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 ( if (
!(valueFlowOrganizationStaysScope && normalizedPredecomposeCounterparty === explicitOrganizationScope) && !(valueFlowOrganizationStaysScope && normalizedPredecomposeCounterparty === explicitOrganizationScope) &&
normalizedPredecomposeCounterparty normalizedPredecomposeCounterparty

View File

@ -665,9 +665,26 @@ export function createAssistantTransitionPolicy(deps) {
llmPreDecomposeMeta llmPreDecomposeMeta
}) })
: null; : 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 ( if (
assistantTurnMeaning?.stale_replay_forbidden === true && assistantTurnMeaning?.stale_replay_forbidden === true &&
!hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage) !hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage) &&
!compactCashflowFollowupSignal
) { ) {
return null; return null;
} }
@ -763,7 +780,8 @@ export function createAssistantTransitionPolicy(deps) {
sourceIntentHint === "customer_revenue_and_payments" || sourceIntentHint === "customer_revenue_and_payments" ||
sourceDiscoveryPilotScopeHint === "counterparty_value_flow_query_movements_v1" || sourceDiscoveryPilotScopeHint === "counterparty_value_flow_query_movements_v1" ||
sourceDiscoveryPilotScopeHint === "counterparty_supplier_payout_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 = const hasBusinessOverviewCarryoverSourceHint =
sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1"; sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
const navigationSessionState = resolveNavigationSessionContextState( const navigationSessionState = resolveNavigationSessionContextState(
@ -807,10 +825,12 @@ export function createAssistantTransitionPolicy(deps) {
: null; : null;
const debtRoleSwapIntent = debtRoleSwapPrimary ?? debtRoleSwapAlternate ?? null; const debtRoleSwapIntent = debtRoleSwapPrimary ?? debtRoleSwapAlternate ?? null;
const shortValueFlowRetargetPrimary = const shortValueFlowRetargetPrimary =
hasValueFlowCarryoverSourceHint && hasShortValueFlowRetargetCue(userMessage); hasValueFlowCarryoverSourceHint &&
(hasShortValueFlowRetargetCue(userMessage) || hasCompactCashflowFollowupCue(userMessage));
const shortValueFlowRetargetAlternate = const shortValueFlowRetargetAlternate =
hasValueFlowCarryoverSourceHint && deps.toNonEmptyString(alternateMessage) hasValueFlowCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
? hasShortValueFlowRetargetCue(String(alternateMessage ?? "")) ? hasShortValueFlowRetargetCue(String(alternateMessage ?? "")) ||
hasCompactCashflowFollowupCue(String(alternateMessage ?? ""))
: false; : false;
const businessOverviewBoundaryFollowupPrimary = const businessOverviewBoundaryFollowupPrimary =
hasBusinessOverviewCarryoverSourceHint && hasBusinessOverviewBoundaryFollowupCue(userMessage); hasBusinessOverviewCarryoverSourceHint && hasBusinessOverviewBoundaryFollowupCue(userMessage);

View File

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

View File

@ -578,6 +578,76 @@ describe("assistant MCP discovery response candidate", () => {
expect(candidate.reply_text).not.toContain("Складской срез"); 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", () => { it("labels organization-scoped bidirectional value-flow continuations as company scope", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate( const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({ 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"); 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", () => { it("keeps all-time business overview from reusing a negated VAT period as active scope", () => {
const result = buildAssistantMcpDiscoveryTurnInput({ const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: 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"); 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", () => { 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 orgName = "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const result = buildAssistantMcpDiscoveryTurnInput({ 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", "generation_id": "gen-ag05230604-098bda",
"created_at": "2026-05-23T06:04:40+00:00", "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?"
}
]
}
]
}