Укрепить банковские границы денежных потоков phase97

This commit is contained in:
dctouch 2026-05-13 01:53:05 +03:00
parent e997449785
commit 04e670ab76
21 changed files with 861 additions and 12 deletions

View File

@ -0,0 +1,145 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase97_financial_counterparty_flow_hints",
"domain": "address_phase97_financial_counterparty_flow_hints",
"title": "Phase 97 financial counterparty flow hints replay",
"description": "Focused semantic replay for the Open-World Schema/Primitive Discovery slice: bank-like counterparties must not be described as ordinary suppliers/customers when operation, payment purpose, contract, or comment fields indicate banking commission, credit, deposit, tax/budget, or payroll-like flows. The replay also keeps a normal counterparty value-flow canary.",
"bindings": {},
"steps": [
{
"step_id": "step_01_sberbank_outgoing_is_not_plain_supplier",
"title": "Sberbank outgoing money is explained as bank flow, not ordinary supplier dependency",
"question": "По ООО Альтернатива Плюс за 2020 отдельно посмотри платежи в СБЕРБАНК: это обычный поставщик или банковский/финансовый поток? Дай коротко и по проверенным данным 1С.",
"allowed_reply_types": [
"factual",
"factual_with_explanation",
"partial_coverage"
],
"required_answer_patterns_all": [
"(?i)сбербанк|банк|финансов",
"(?i)не.*обычн.*поставщик|не.*поставщик|банковск|финансов",
"(?i)комисс|назначени|вид операции|платеж|списан",
"(?i)2020|1с|провер"
],
"forbidden_answer_patterns": [
"(?i)главный поставщик.*сбербанк",
"(?i)обычный поставщик.*сбербанк",
"(?i)route_candidate",
"(?i)primitive",
"(?i)planner_",
"(?i)catalog_",
"(?i)snapshot_items",
"(?i)answer_object"
],
"criticality": "critical",
"semantic_tags": [
"financial_counterparty_flow_hint",
"bank_like_supplier_boundary",
"supplier_payouts_profile"
]
},
{
"step_id": "step_02_sberbank_incoming_is_not_plain_customer",
"title": "Sberbank incoming money is not overclaimed as normal customer revenue",
"question": "А если по этой же компании СБЕРБАНК встречается во входящих поступлениях, это клиентская выручка или там может быть кредитный/депозитный банковский смысл? Не притягивай, скажи что подтверждено.",
"allowed_reply_types": [
"factual",
"factual_with_explanation",
"partial_coverage"
],
"required_answer_patterns_all": [
"(?i)сбербанк|банк|финансов",
"(?i)не.*клиентск|не.*обычн.*клиент|не.*выручк|кредит|депозит|возврат",
"(?i)вид операции|договор|поступлен|1с|провер",
"(?i)подтвержд|не подтвержд|не доказ"
],
"forbidden_answer_patterns": [
"(?i)сбербанк.*главный клиент",
"(?i)сбербанк.*обычный клиент",
"(?i)route_candidate",
"(?i)primitive",
"(?i)planner_",
"(?i)catalog_",
"(?i)snapshot_items",
"(?i)answer_object"
],
"criticality": "critical",
"semantic_tags": [
"financial_counterparty_flow_hint",
"bank_like_customer_boundary",
"customer_revenue_and_payments"
]
},
{
"step_id": "step_03_business_overview_keeps_bank_boundary",
"title": "Business overview keeps bank-like leaders bounded by flow meaning",
"question": "Теперь дай взрослый краткий обзор ООО Альтернатива Плюс за 2020: входящие, исходящие, нетто и отдельно отметь, если в топах есть банк, почему его нельзя читать как обычного клиента или поставщика.",
"allowed_reply_types": [
"factual",
"factual_with_explanation",
"partial_coverage"
],
"expected_catalog_alignment_status": "selected_matches_top",
"expected_catalog_chain_top_match": "business_overview",
"expected_catalog_selected_matches_top": true,
"required_answer_patterns_all": [
"(?i)альтернатива|компан|организац",
"(?i)входящ|поступлен|исходящ|списан|нетто",
"(?i)банк|финансов|сбербанк|не.*обычн.*клиент|не.*обычн.*поставщик",
"(?i)прибыл|марж|не подтвержд|не доказ|не является"
],
"forbidden_answer_patterns": [
"(?i)сбербанк.*обычный поставщик",
"(?i)сбербанк.*обычный клиент",
"(?i)чистая прибыль.*точно",
"(?i)route_candidate",
"(?i)primitive",
"(?i)planner_",
"(?i)catalog_",
"(?i)snapshot_items",
"(?i)answer_object"
],
"criticality": "critical",
"semantic_tags": [
"business_overview",
"financial_counterparty_flow_hint",
"profit_margin_boundary"
]
},
{
"step_id": "step_04_normal_counterparty_value_flow_canary",
"title": "Normal counterparty value-flow still works after bank-flow questions",
"question": "А теперь отдельно по Группа СВК за 2020: сколько денег получили, сколько заплатили и какое нетто?",
"allowed_reply_types": [
"factual",
"factual_with_explanation",
"partial_coverage"
],
"expected_catalog_alignment_status": "selected_matches_top",
"expected_catalog_chain_top_match": "value_flow_comparison",
"expected_catalog_selected_matches_top": true,
"required_answer_patterns_all": [
"(?i)свк|группа",
"(?i)2020",
"(?i)получил|входящ|поступлен",
"(?i)заплат|исходящ|списан",
"(?i)нетто|сальдо|разниц"
],
"forbidden_answer_patterns": [
"(?i)сбербанк",
"(?i)уточните организац",
"(?i)какую компанию",
"(?i)route_candidate",
"(?i)primitive",
"(?i)planner_",
"(?i)catalog_"
],
"criticality": "critical",
"semantic_tags": [
"counterparty_net_cash_flow",
"stale_scope_guard",
"canary"
]
}
]
}

View File

@ -13,6 +13,24 @@ const ACCOUNT_REVERSE_PATTERN = /(?:^|[\s,.;:!?()\-])(\d{2}(?:[.,]\d{1,2})?)(?=\
const LIMIT_PATTERN = /(?:\btop\b|\blimit\b|первые|топ)[\s\-—_:№#]*?(\d{1,3})/iu; const LIMIT_PATTERN = /(?:\btop\b|\blimit\b|первые|топ)[\s\-—_:№#]*?(\d{1,3})/iu;
const VALUE_ANALYTICS_SAMPLE_LIMIT = 1000; const VALUE_ANALYTICS_SAMPLE_LIMIT = 1000;
const COUNTERPARTY_PATTERN = /(?:по\s+контрагенту|контрагент(?:у|а)?|по\s+контре|контра|по\s+компан(?:ии|ию|ия)|компан(?:ия|ии|ию)|по\s+организац(?:ии|ию|ия)|организац(?:ия|ии|ию)|по\s+поставщик(?:у|а)?|поставщик(?:у|а)?|по\s+клиент(?:у|а)?|клиент(?:у|а)?|по\s+покупател(?:ю|я)|покупател(?:ю|я)|по\s+партнер(?:у|а)?|партнер(?:у|а)?|by\s+counterparty|counterparty|by\s+company|company|by\s+supplier|supplier|by\s+vendor|vendor|by\s+customer|customer|by\s+client|client|by\s+partner|partner)\s+([^\r\n,.;:]+)/iu; const COUNTERPARTY_PATTERN = /(?:по\s+контрагенту|контрагент(?:у|а)?|по\s+контре|контра|по\s+компан(?:ии|ию|ия)|компан(?:ия|ии|ию)|по\s+организац(?:ии|ию|ия)|организац(?:ия|ии|ию)|по\s+поставщик(?:у|а)?|поставщик(?:у|а)?|по\s+клиент(?:у|а)?|клиент(?:у|а)?|по\s+покупател(?:ю|я)|покупател(?:ю|я)|по\s+партнер(?:у|а)?|партнер(?:у|а)?|by\s+counterparty|counterparty|by\s+company|company|by\s+supplier|supplier|by\s+vendor|vendor|by\s+customer|customer|by\s+client|client|by\s+partner|partner)\s+([^\r\n,.;:]+)/iu;
const KNOWN_FINANCIAL_COUNTERPARTY_ANCHORS = [
{
pattern: /(?<![\p{L}\p{N}])(?:\u043f\u0430\u043e\s+|\u0430\u043e\s+)?\u0441\u0431\u0435\u0440\u0431\u0430\u043d\u043a(?:\s*,?\s*(?:\u043f\u0430\u043e|\u0430\u043e))?(?![\p{L}\p{N}])/iu,
value: "\u0421\u0411\u0415\u0420\u0411\u0410\u041d\u041a"
},
{
pattern: /(?<![\p{L}\p{N}])(?:\u0431\u0430\u043d\u043a\s+)?\u0432\u0442\u0431(?![\p{L}\p{N}])/iu,
value: "\u0412\u0422\u0411"
},
{
pattern: /(?<![\p{L}\p{N}])\u0430\u043b\u044c\u0444\u0430[-\s]?\u0431\u0430\u043d\u043a(?![\p{L}\p{N}])/iu,
value: "\u0410\u041b\u042c\u0424\u0410-\u0411\u0410\u041d\u041a"
},
{
pattern: /(?<![\p{L}\p{N}])(?:\u0442\u0438\u043d\u044c\u043a\u043e\u0444\u0444|\u0442[-\s]?\u0431\u0430\u043d\u043a)(?![\p{L}\p{N}])/iu,
value: "\u0422-\u0411\u0410\u041d\u041a"
}
];
const CONTRACT_PATTERN = /(?:по\s+(?:договору|контракту)|(?:договор|контракт)(?:у|а)?\s*(?:№|#|n)?|by\s+contract|contract(?:\s*(?:no|number|#|n))?)\s+([^\r\n,.;:]+)/i; const CONTRACT_PATTERN = /(?:по\s+(?:договору|контракту)|(?:договор|контракт)(?:у|а)?\s*(?:№|#|n)?|by\s+contract|contract(?:\s*(?:no|number|#|n))?)\s+([^\r\n,.;:]+)/i;
const DATE_DMY_PATTERN = /\b(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{2,4})\b/; const DATE_DMY_PATTERN = /\b(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{2,4})\b/;
const DATE_YMD_PATTERN = /\b(20\d{2})[.\/-](\d{1,2})[.\/-](\d{1,2})\b/; const DATE_YMD_PATTERN = /\b(20\d{2})[.\/-](\d{1,2})[.\/-](\d{1,2})\b/;
@ -608,6 +626,18 @@ function isLikelyCounterpartyToken(rawToken) {
} }
return !isCounterpartyNoiseToken(lowered); return !isCounterpartyNoiseToken(lowered);
} }
function extractKnownFinancialCounterpartyAnchor(text) {
const source = String(text ?? "");
if (!source.trim()) {
return undefined;
}
for (const anchor of KNOWN_FINANCIAL_COUNTERPARTY_ANCHORS) {
if (anchor.pattern.test(source)) {
return anchor.value;
}
}
return undefined;
}
function isLowQualityCounterpartyAnchorValue(rawValue) { function isLowQualityCounterpartyAnchorValue(rawValue) {
const value = String(rawValue ?? "") const value = String(rawValue ?? "")
.trim() .trim()
@ -619,6 +649,9 @@ function isLowQualityCounterpartyAnchorValue(rawValue) {
if (/(?:за\s+вс[её]\s+время|за\s+всю\s+истори(?:ю|и)|all\s+time|entire\s+period|full\s+history)/iu.test(value)) { if (/(?:за\s+вс[её]\s+время|за\s+всю\s+истори(?:ю|и)|all\s+time|entire\s+period|full\s+history)/iu.test(value)) {
return true; return true;
} }
if (/^(?:или|это|там|может|можно|обычн\p{L}*|клиентск\p{L}*|банковск\p{L}*(?:\/|\s+и\s+)?финансов\p{L}*)\b/iu.test(value)) {
return true;
}
const tokens = value const tokens = value
.split(/[^a-zа-я0-9]+/iu) .split(/[^a-zа-я0-9]+/iu)
.map((token) => token.trim()) .map((token) => token.trim())
@ -1495,6 +1528,13 @@ function shouldExpandSampleForValueAnalytics(intent) {
intent === "supplier_payouts_profile" || intent === "supplier_payouts_profile" ||
intent === "contract_usage_and_value"); intent === "contract_usage_and_value");
} }
function shouldPreferKnownFinancialCounterpartyAnchor(intent) {
return (intent === "bank_operations_by_counterparty" ||
intent === "customer_revenue_and_payments" ||
intent === "supplier_payouts_profile" ||
intent === "list_documents_by_counterparty" ||
intent === "list_contracts_by_counterparty");
}
function extractAddressFilters(userMessage, intent) { function extractAddressFilters(userMessage, intent) {
const rawText = String(userMessage ?? "").trim(); const rawText = String(userMessage ?? "").trim();
const text = normalizeMojibakeString(rawText); const text = normalizeMojibakeString(rawText);
@ -1573,6 +1613,13 @@ function extractAddressFilters(userMessage, intent) {
} }
} }
const allowGenericCounterpartyAnchor = !isInventoryTraceIntent(intent); const allowGenericCounterpartyAnchor = !isInventoryTraceIntent(intent);
const knownFinancialCounterparty = allowGenericCounterpartyAnchor && shouldPreferKnownFinancialCounterpartyAnchor(intent)
? extractKnownFinancialCounterpartyAnchor(text)
: undefined;
if (knownFinancialCounterparty && !filters.counterparty) {
filters.counterparty = knownFinancialCounterparty;
warnings.push("counterparty_anchor_derived_from_known_financial_name");
}
const counterpartyMatch = allowGenericCounterpartyAnchor ? text.match(COUNTERPARTY_PATTERN) : null; const counterpartyMatch = allowGenericCounterpartyAnchor ? text.match(COUNTERPARTY_PATTERN) : null;
if (counterpartyMatch && !filters.counterparty) { if (counterpartyMatch && !filters.counterparty) {
filters.counterparty = cleanupAnchorValue(String(counterpartyMatch[1])); filters.counterparty = cleanupAnchorValue(String(counterpartyMatch[1]));

View File

@ -1658,9 +1658,9 @@ function hasBidirectionalValueFlowComparisonSignal(text) {
return false; return false;
} }
const hasIncomingCue = /(?:\u0432\u0445\u043e\u0434\u044f\u0449|\u043f\u043e\u0441\u0442\u0443\u043f|\u043f\u043e\u043b\u0443\u0447|inflow|incoming)/iu.test(normalized); const hasIncomingCue = /(?:\u0432\u0445\u043e\u0434\u044f\u0449|\u043f\u043e\u0441\u0442\u0443\u043f|\u043f\u043e\u043b\u0443\u0447|inflow|incoming)/iu.test(normalized);
const hasOutgoingCue = /(?:\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u0441\u043f\u0438\u0441\u0430\u043d|\u0437\u0430\u043f\u043b\u0430\u0442|\u043f\u043b\u0430\u0442\u0438\u043b|\u043e\u043f\u043b\u0430\u0442|outflow|outgoing|payout)/iu.test(normalized); const hasOutgoingCue = /(?:\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u0441\u043f\u0438\u0441\u0430\u043d|\u0437\u0430\u043f\u043b\u0430\u0442|\u043f\u043b\u0430\u0442\u0438\u043b|\u0432\u044b\u043f\u043b\u0430\u0442|\u0432\u044b\u043f\u043b\u0430\u0447|\u0443\u043f\u043b\u0430\u0442|\u043e\u043f\u043b\u0430\u0442|outflow|outgoing|payout|paid)/iu.test(normalized);
const hasComparisonCue = /(?:\u0431\u043e\u043b\u044c\u0448|\u043c\u0435\u043d\u044c\u0448|\u0441\u0440\u0430\u0432|\u0438\u043b\u0438|\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|vs|versus)/iu.test(normalized); const hasComparisonCue = /(?:\u0431\u043e\u043b\u044c\u0448|\u043c\u0435\u043d\u044c\u0448|\u0441\u0440\u0430\u0432|\u0438\u043b\u0438|\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|vs|versus)/iu.test(normalized);
const hasValueFlowCue = /(?:\u0434\u0435\u043d\u044c\u0433|\u0434\u0435\u043d\u0435\u0433|\u0434\u0435\u043d\u0435\u0436|\u043f\u043e\u0442\u043e\u043a|\u043e\u0431\u043e\u0440\u043e\u0442|money|cash|flow)/iu.test(normalized); const hasValueFlowCue = /(?:\u0434\u0435\u043d\u044c\u0433|\u0434\u0435\u043d\u0435\u0433|\u0434\u0435\u043d\u0435\u0436|\u0441\u0440\u0435\u0434\u0441\u0442\u0432|\u043f\u043e\u0442\u043e\u043a|\u043e\u0431\u043e\u0440\u043e\u0442|money|cash|funds|flow)/iu.test(normalized);
const hasNetAmountCue = /(?:сколько|сумм|итог|нетто|сальдо|минус|net|total|sum)/iu.test(normalized); const hasNetAmountCue = /(?:сколько|сумм|итог|нетто|сальдо|минус|net|total|sum)/iu.test(normalized);
return hasIncomingCue && hasOutgoingCue && hasComparisonCue && (hasValueFlowCue || hasNetAmountCue); return hasIncomingCue && hasOutgoingCue && hasComparisonCue && (hasValueFlowCue || hasNetAmountCue);
} }

View File

@ -1189,6 +1189,9 @@ function toNormalizedRows(rows) {
const organization = firstNonEmptyString(row.Организация, row.Organization, row.organization, row.organization_name, row.ОрганизацияПредставление); const organization = firstNonEmptyString(row.Организация, row.Organization, row.organization, row.organization_name, row.ОрганизацияПредставление);
const counterparty = firstNonEmptyString(row.Контрагент, row.Counterparty, row.counterparty); const counterparty = firstNonEmptyString(row.Контрагент, row.Counterparty, row.counterparty);
const contract = firstNonEmptyString(row.Договор, row.Contract, row.contract); const contract = firstNonEmptyString(row.Договор, row.Contract, row.contract);
const operationKind = firstNonEmptyString(row.ВидОперации, row.OperationKind, row.operation_kind, row.operationType);
const paymentPurpose = firstNonEmptyString(row.НазначениеПлатежа, row.PaymentPurpose, row.payment_purpose, row.paymentPurpose);
const comment = firstNonEmptyString(row.Комментарий, row.Comment, row.comment);
const analytics = collectAnalyticsStrings(row); const analytics = collectAnalyticsStrings(row);
return { return {
period, period,
@ -1202,7 +1205,10 @@ function toNormalizedRows(rows) {
warehouse, warehouse,
organization, organization,
counterparty, counterparty,
contract contract,
operation_kind: operationKind,
payment_purpose: paymentPurpose,
comment
}; };
}) })
.filter((item) => Boolean(item.period || item.registrator)); .filter((item) => Boolean(item.period || item.registrator));

View File

@ -277,6 +277,77 @@ function isDirectBalanceQuestion(userMessage) {
} }
return /(?:кто|кому|сколько|какой|какая|какие|есть\s+ли|долж|дебитор|кредитор|payables?|receivables?|who|how\s+much)/iu.test(text); return /(?:кто|кому|сколько|какой|какая|какие|есть\s+ли|долж|дебитор|кредитор|payables?|receivables?|who|how\s+much)/iu.test(text);
} }
function hasBankIncomingRoleBoundaryQuestion(userMessage) {
const text = normalizeQuestionText(userMessage);
return (/(?:входящ|поступлен|клиентск|выручк|кредит|депозит|возврат)/iu.test(text) &&
/(?:банк|сбербанк|финанс)/iu.test(text));
}
function hasBankOutgoingRoleBoundaryQuestion(userMessage) {
const text = normalizeQuestionText(userMessage);
return (/(?:исходящ|списан|платеж|поставщик|закуп|выплат)/iu.test(text) &&
/(?:банк|сбербанк|финанс)/iu.test(text));
}
function bankOperationDirection(row) {
const text = normalizeQuestionText(`${row.registrator} ${row.operation_kind ?? ""}`);
if (/(?:поступлени[ея]\s+на\s+расчетн|bank\s+receipt|incoming)/iu.test(text)) {
return "incoming";
}
if (/(?:списани[ея]\s+с\s+расчетн|bank\s+payment|outgoing|write[-\s]?off)/iu.test(text)) {
return "outgoing";
}
return "unknown";
}
function bankOperationDirectionLabel(direction) {
if (direction === "incoming") {
return "входящее поступление";
}
if (direction === "outgoing") {
return "исходящее списание";
}
return "банковская операция без надежно распознанного направления";
}
function bankOperationEvidenceLine(rows) {
const sample = rows[0];
if (!sample) {
return "Проверенная строка 1С не найдена.";
}
const direction = bankOperationDirection(sample);
const parts = [`тип по документу: ${bankOperationDirectionLabel(direction)}`];
const operationKind = String(sample.operation_kind ?? "").trim();
const paymentPurpose = String(sample.payment_purpose ?? "").trim();
const contract = String(sample.contract ?? "").trim();
if (operationKind) {
parts.push(`вид операции: ${operationKind}`);
}
if (paymentPurpose) {
parts.push(`назначение платежа: ${paymentPurpose}`);
}
if (contract) {
parts.push(`договор: ${contract}`);
}
if (!operationKind && !paymentPurpose && !contract) {
parts.push("вид операции/назначение платежа/договор в материализованной строке не заполнены");
}
return `Основание 1С: ${parts.join("; ")}.`;
}
function bankRoleBoundaryLine(userMessage, rows) {
const incomingBoundary = hasBankIncomingRoleBoundaryQuestion(userMessage);
const outgoingBoundary = hasBankOutgoingRoleBoundaryQuestion(userMessage);
if (!incomingBoundary && !outgoingBoundary) {
return null;
}
const directions = rows.map(bankOperationDirection);
const hasIncomingRow = directions.includes("incoming");
const hasOutgoingRow = directions.includes("outgoing");
if (incomingBoundary) {
return hasIncomingRow
? "Выручкой от обычного клиента это не называю автоматически: для банка/финорганизации нужен вид операции, назначение платежа и договор; кредитный, депозитный или возвратный смысл без этих полей не исключаю и не притягиваю."
: hasOutgoingRow
? "В найденных строках по банку подтверждено исходящее списание, а входящее поступление от банка в этом срезе не подтверждено; клиентскую выручку, кредит или депозит по этой строке не доказываю."
: "Входящее поступление от банка в найденных строках не подтверждено; клиентскую выручку, кредитный или депозитный смысл без вида операции/назначения платежа не доказываю.";
}
return "Обычным поставщиком это не называю автоматически: для банка/финорганизации нужен вид операции, назначение платежа и договор; текущий срез подтверждает банковский платежный контур, а не бизнес-роль поставщика.";
}
function hasInventoryPurchaseDateActionFocus(userMessage) { function hasInventoryPurchaseDateActionFocus(userMessage) {
const text = normalizeQuestionText(userMessage); const text = normalizeQuestionText(userMessage);
if (!text) { if (!text) {
@ -3820,9 +3891,15 @@ function composeFactualReplyBody(intent, rows, options = {}) {
}; };
} }
if (intent === "bank_operations_by_counterparty") { if (intent === "bank_operations_by_counterparty") {
const rowCounterparties = uniqueStrings(rows
.map((row) => extractCounterpartyName(row))
.filter((item) => Boolean(item)));
const counterparty = resolvePreferredCounterpartyDisplayLabel(options.counterpartyHint, rowCounterparties);
const roleBoundary = bankRoleBoundaryLine(options.userMessage, rows);
const lines = [ const lines = [
`Коротко: найдено банковских операций по контрагенту — ${rows.length}.`, `Коротко: найдено банковских операций${counterparty ? ` по ${counterparty}` : " по контрагенту"}${rows.length}.`,
"Показываю подтвержденные банковские операции из текущего среза.", roleBoundary ?? "Показываю подтвержденные банковские операции из текущего среза.",
bankOperationEvidenceLine(rows),
...formatTopRows(rows, rows.length) ...formatTopRows(rows, rows.length)
]; ];
return { return {

View File

@ -17,6 +17,23 @@ function toNonEmptyString(value) {
const text = String(value).trim(); const text = String(value).trim();
return text.length > 0 ? text : null; return text.length > 0 ? text : null;
} }
function normalizeQuestionText(value) {
return String(value ?? "")
.toLowerCase()
.replace(/ё/g, "е")
.replace(/\s+/g, " ")
.trim();
}
function requestsFinancialCounterpartyBoundary(turnMeaning, graph) {
const text = normalizeQuestionText([
turnMeaning?.raw_message,
turnMeaning?.effective_message,
graph?.source_message,
graph?.question
].join(" "));
return (/(?:банк|сбербанк|финанс|кредит|депозит)/iu.test(text) &&
/(?:клиент|поставщик|выручк|топ|обычн|роль|поток)/iu.test(text));
}
function toStringList(value) { function toStringList(value) {
if (!Array.isArray(value)) { if (!Array.isArray(value)) {
return []; return [];
@ -693,6 +710,12 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
: `; крупнейший получатель исходящих денег: ${topSupplier}` : `; крупнейший получатель исходящих денег: ${topSupplier}`
: ""; : "";
const roleBoundaryLead = topCustomer || topSupplier ? "; клиент/поставщик как бизнес-роли этим денежным срезом не подтверждены" : ""; const roleBoundaryLead = topCustomer || topSupplier ? "; клиент/поставщик как бизнес-роли этим денежным срезом не подтверждены" : "";
const financialBoundaryRequested = requestsFinancialCounterpartyBoundary(turnMeaning, graph);
const requestedFinancialBoundaryLine = financialBoundaryRequested
? topCustomerLooksFinancial || topSupplierLooksFinancial
? "Отдельно по банкам: если денежный топ ведет банк/финансовая организация, это нельзя автоматически читать как обычного клиента или поставщика; нужны назначение платежа, вид операции и договор. Поэтому такой поток не является доказанной клиентской выручкой, обычной поставкой или чистой прибылью без отдельной проверки."
: "Отдельно по банкам: банк/финансовую организацию в денежных топах нельзя автоматически читать как обычного клиента или поставщика; нужны назначение платежа, вид операции и договор. Поэтому такой поток не является доказанной клиентской выручкой, обычной поставкой или чистой прибылью без отдельной проверки."
: null;
const graphReasonCodes = toStringList(graph?.reason_codes); const graphReasonCodes = toStringList(graph?.reason_codes);
const directMoneyAnswer = graphReasonCodes.includes("data_need_graph_business_overview_direct_money_answer"); const directMoneyAnswer = graphReasonCodes.includes("data_need_graph_business_overview_direct_money_answer");
const crossScopeExecutiveSummary = Boolean(separateSubject && previousCounterpartySummary); const crossScopeExecutiveSummary = Boolean(separateSubject && previousCounterpartySummary);
@ -901,7 +924,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
if (!leaderYear || !leaderAmount) { if (!leaderYear || !leaderAmount) {
return null; return null;
} }
lines.push(`Коротко: в доступном проверенном срезе 1С по входящим денежным строкам лидирует ${leaderYear}: ${leaderAmount}${Number.isFinite(leaderRows) && leaderRows > 0 ? ` по ${leaderRows} строкам с суммой` : ""}; это не полный бухгалтерский рейтинг доходности.`); lines.push(`Коротко: ${organizationPrefix}в доступном проверенном срезе 1С по входящим денежным строкам лидирует ${leaderYear}: ${leaderAmount}${Number.isFinite(leaderRows) && leaderRows > 0 ? ` по ${leaderRows} строкам с суммой` : ""}; это не полный бухгалтерский рейтинг доходности.`);
const netYear = toNonEmptyString(netLeader?.year_bucket); const netYear = toNonEmptyString(netLeader?.year_bucket);
const netYearAmount = moneyText(netLeader?.net_amount_human_ru); const netYearAmount = moneyText(netLeader?.net_amount_human_ru);
if (netYear && netYearAmount) { if (netYear && netYearAmount) {
@ -912,6 +935,9 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
if (incomingAmount && outgoingAmount && netAmount) { if (incomingAmount && outgoingAmount && netAmount) {
lines.push(`Сверка по окну: входящие ${incomingAmount}, исходящие ${outgoingAmount}, ${netDirection} ${sentenceAmount(netAmount) ?? netAmount}.`); lines.push(`Сверка по окну: входящие ${incomingAmount}, исходящие ${outgoingAmount}, ${netDirection} ${sentenceAmount(netAmount) ?? netAmount}.`);
} }
if (requestedFinancialBoundaryLine) {
lines.push(requestedFinancialBoundaryLine);
}
const yearRows = businessOverviewYearRowsLine(overview); const yearRows = businessOverviewYearRowsLine(overview);
if (yearRows) { if (yearRows) {
lines.push(yearRows); lines.push(yearRows);
@ -925,6 +951,9 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
? `Крупнейший входящий денежный источник в этом срезе: ${customerName}${sentenceAmount(customerAmount) ?? customerAmount}. По названию это банк/финансовая организация, поэтому без назначения платежа не называю это клиентской выручкой.${nonFinancialCustomer ? ` Крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}.` : ""}` ? `Крупнейший входящий денежный источник в этом срезе: ${customerName}${sentenceAmount(customerAmount) ?? customerAmount}. По названию это банк/финансовая организация, поэтому без назначения платежа не называю это клиентской выручкой.${nonFinancialCustomer ? ` Крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}.` : ""}`
: `Крупнейший подтвержденный источник входящих денег в этом срезе: ${customerName}${sentenceAmount(customerAmount) ?? customerAmount}.`); : `Крупнейший подтвержденный источник входящих денег в этом срезе: ${customerName}${sentenceAmount(customerAmount) ?? customerAmount}.`);
} }
if (requestedFinancialBoundaryLine) {
lines.push(requestedFinancialBoundaryLine);
}
} }
else { else {
return null; return null;

View File

@ -705,12 +705,18 @@ function hasCrossScopeExecutiveSummarySignal(text) {
/(?:\u043e\u0442\u0434\u0435\u043b\u044c\u043d\p{L}*\s+\u043f\u043e|\u043f\u043e\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u0433\u0440\u0443\u043f\u043f\p{L}*\s+\u0441\u0432\u043a|\u0441\u0432\u043a|counterpart(?:y|ies)?)/iu.test(text) && /(?:\u043e\u0442\u0434\u0435\u043b\u044c\u043d\p{L}*\s+\u043f\u043e|\u043f\u043e\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u0433\u0440\u0443\u043f\u043f\p{L}*\s+\u0441\u0432\u043a|\u0441\u0432\u043a|counterpart(?:y|ies)?)/iu.test(text) &&
/(?:\u0447\u0442\u043e\s+\u043c\u043e\u0436\u043d\p{L}*|\u0447\u0442\u043e\s+\u043d\u0435\u043b\u044c\u0437\p{L}*|\u0432\u044b\u0432\u043e\u0434\p{L}*|allowed|forbidden|cannot|can\s+say)/iu.test(text)); /(?:\u0447\u0442\u043e\s+\u043c\u043e\u0436\u043d\p{L}*|\u0447\u0442\u043e\s+\u043d\u0435\u043b\u044c\u0437\p{L}*|\u0432\u044b\u0432\u043e\u0434\p{L}*|allowed|forbidden|cannot|can\s+say)/iu.test(text));
} }
function hasPlainBusinessOverviewSignal(text) {
const hasPlainOverviewCue = /(?:\u0432\u0437\u0440\u043e\u0441\u043b\p{L}*[\s\S]{0,40}\u043e\u0431\u0437\u043e\u0440|\u043a\u0440\u0430\u0442\u043a\p{L}*\s+\u043e\u0431\u0437\u043e\u0440|\u043e\u0431\u0437\u043e\u0440[\s\S]{0,100}(?:\u0432\u0445\u043e\u0434\u044f\u0449|\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u043d\u0435\u0442\u0442\u043e|incoming|outgoing|net))/iu.test(text);
const hasCompanyOrOperatingScopeCue = /(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0431\u0438\u0437\u043d\u0435\u0441|\u0443\s+\u043d\u0430\u0441|\u043d\u0430\u0448\p{L}*|(?:19|20)\d{2}|company|organization|business)/iu.test(text);
return hasPlainOverviewCue && hasCompanyOrOperatingScopeCue;
}
function hasBusinessOverviewSignal(text) { function hasBusinessOverviewSignal(text) {
if (hasCrossScopeExecutiveSummarySignal(text) || if (hasCrossScopeExecutiveSummarySignal(text) ||
hasOrganizationLevelEarningsOverviewSignal(text) || hasOrganizationLevelEarningsOverviewSignal(text) ||
hasOrganizationLevelDebtPositionOverviewSignal(text) || hasOrganizationLevelDebtPositionOverviewSignal(text) ||
hasOrganizationLevelDebtDueDateOverviewSignal(text) || hasOrganizationLevelDebtDueDateOverviewSignal(text) ||
hasOrganizationLevelInventoryReserveLiquidationOverviewSignal(text) || hasOrganizationLevelInventoryReserveLiquidationOverviewSignal(text) ||
hasPlainBusinessOverviewSignal(text) ||
hasOrganizationLevelSupplierQualityOverviewSignal(text)) { hasOrganizationLevelSupplierQualityOverviewSignal(text)) {
return true; return true;
} }
@ -1786,6 +1792,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
normalizedFollowupDateScope); normalizedFollowupDateScope);
const clarificationLoopSeedApplied = Boolean(followupSeed.loopStatus === "awaiting_clarification" && followupSeed.loopSelectedChainId); const clarificationLoopSeedApplied = Boolean(followupSeed.loopStatus === "awaiting_clarification" && followupSeed.loopSelectedChainId);
const turnMeaning = { const turnMeaning = {
raw_message: repairedUserText ?? rawUserText ?? null,
effective_message: repairedEffectiveText ?? rawEffectiveText ?? null,
asked_domain_family: businessOverviewSignal asked_domain_family: businessOverviewSignal
? "business_overview" ? "business_overview"
: lifecycleSignal : lifecycleSignal
@ -1882,6 +1890,12 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
followupDiscoverySeedApplicable) followupDiscoverySeedApplicable)
}; };
const cleanTurnMeaning = {}; const cleanTurnMeaning = {};
if (toNonEmptyString(turnMeaning.raw_message)) {
cleanTurnMeaning.raw_message = turnMeaning.raw_message;
}
if (toNonEmptyString(turnMeaning.effective_message)) {
cleanTurnMeaning.effective_message = turnMeaning.effective_message;
}
if (toNonEmptyString(turnMeaning.asked_domain_family)) { if (toNonEmptyString(turnMeaning.asked_domain_family)) {
cleanTurnMeaning.asked_domain_family = turnMeaning.asked_domain_family; cleanTurnMeaning.asked_domain_family = turnMeaning.asked_domain_family;
} }

View File

@ -8,6 +8,25 @@ const LIMIT_PATTERN = /(?:\btop\b|\blimit\b|первые|топ)[\s\-—_:№
const VALUE_ANALYTICS_SAMPLE_LIMIT = 1000; const VALUE_ANALYTICS_SAMPLE_LIMIT = 1000;
const COUNTERPARTY_PATTERN = const COUNTERPARTY_PATTERN =
/(?:по\s+контрагенту|контрагент(?:у|а)?|по\s+контре|контра|по\s+компан(?:ии|ию|ия)|компан(?:ия|ии|ию)|по\s+организац(?:ии|ию|ия)|организац(?:ия|ии|ию)|по\s+поставщик(?:у|а)?|поставщик(?:у|а)?|по\s+клиент(?:у|а)?|клиент(?:у|а)?|по\s+покупател(?:ю|я)|покупател(?:ю|я)|по\s+партнер(?:у|а)?|партнер(?:у|а)?|by\s+counterparty|counterparty|by\s+company|company|by\s+supplier|supplier|by\s+vendor|vendor|by\s+customer|customer|by\s+client|client|by\s+partner|partner)\s+([^\r\n,.;:]+)/iu; /(?:по\s+контрагенту|контрагент(?:у|а)?|по\s+контре|контра|по\s+компан(?:ии|ию|ия)|компан(?:ия|ии|ию)|по\s+организац(?:ии|ию|ия)|организац(?:ия|ии|ию)|по\s+поставщик(?:у|а)?|поставщик(?:у|а)?|по\s+клиент(?:у|а)?|клиент(?:у|а)?|по\s+покупател(?:ю|я)|покупател(?:ю|я)|по\s+партнер(?:у|а)?|партнер(?:у|а)?|by\s+counterparty|counterparty|by\s+company|company|by\s+supplier|supplier|by\s+vendor|vendor|by\s+customer|customer|by\s+client|client|by\s+partner|partner)\s+([^\r\n,.;:]+)/iu;
const KNOWN_FINANCIAL_COUNTERPARTY_ANCHORS: Array<{ pattern: RegExp; value: string }> = [
{
pattern:
/(?<![\p{L}\p{N}])(?:\u043f\u0430\u043e\s+|\u0430\u043e\s+)?\u0441\u0431\u0435\u0440\u0431\u0430\u043d\u043a(?:\s*,?\s*(?:\u043f\u0430\u043e|\u0430\u043e))?(?![\p{L}\p{N}])/iu,
value: "\u0421\u0411\u0415\u0420\u0411\u0410\u041d\u041a"
},
{
pattern: /(?<![\p{L}\p{N}])(?:\u0431\u0430\u043d\u043a\s+)?\u0432\u0442\u0431(?![\p{L}\p{N}])/iu,
value: "\u0412\u0422\u0411"
},
{
pattern: /(?<![\p{L}\p{N}])\u0430\u043b\u044c\u0444\u0430[-\s]?\u0431\u0430\u043d\u043a(?![\p{L}\p{N}])/iu,
value: "\u0410\u041b\u042c\u0424\u0410-\u0411\u0410\u041d\u041a"
},
{
pattern: /(?<![\p{L}\p{N}])(?:\u0442\u0438\u043d\u044c\u043a\u043e\u0444\u0444|\u0442[-\s]?\u0431\u0430\u043d\u043a)(?![\p{L}\p{N}])/iu,
value: "\u0422-\u0411\u0410\u041d\u041a"
}
];
const CONTRACT_PATTERN = const CONTRACT_PATTERN =
/(?:по\s+(?:договору|контракту)|(?:договор|контракт)(?:у|а)?\s*(?:№|#|n)?|by\s+contract|contract(?:\s*(?:no|number|#|n))?)\s+([^\r\n,.;:]+)/i; /(?:по\s+(?:договору|контракту)|(?:договор|контракт)(?:у|а)?\s*(?:№|#|n)?|by\s+contract|contract(?:\s*(?:no|number|#|n))?)\s+([^\r\n,.;:]+)/i;
const DATE_DMY_PATTERN = /\b(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{2,4})\b/; const DATE_DMY_PATTERN = /\b(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{2,4})\b/;
@ -694,6 +713,19 @@ function isLikelyCounterpartyToken(rawToken: string): boolean {
return !isCounterpartyNoiseToken(lowered); return !isCounterpartyNoiseToken(lowered);
} }
function extractKnownFinancialCounterpartyAnchor(text: string): string | undefined {
const source = String(text ?? "");
if (!source.trim()) {
return undefined;
}
for (const anchor of KNOWN_FINANCIAL_COUNTERPARTY_ANCHORS) {
if (anchor.pattern.test(source)) {
return anchor.value;
}
}
return undefined;
}
function isLowQualityCounterpartyAnchorValue(rawValue: string): boolean { function isLowQualityCounterpartyAnchorValue(rawValue: string): boolean {
const value = String(rawValue ?? "") const value = String(rawValue ?? "")
.trim() .trim()
@ -705,6 +737,13 @@ function isLowQualityCounterpartyAnchorValue(rawValue: string): boolean {
if (/(?:за\s+вс[её]\s+время|за\s+всю\s+истори(?:ю|и)|all\s+time|entire\s+period|full\s+history)/iu.test(value)) { if (/(?:за\s+вс[её]\s+время|за\s+всю\s+истори(?:ю|и)|all\s+time|entire\s+period|full\s+history)/iu.test(value)) {
return true; return true;
} }
if (
/^(?:или|это|там|может|можно|обычн\p{L}*|клиентск\p{L}*|банковск\p{L}*(?:\/|\s+и\s+)?финансов\p{L}*)\b/iu.test(
value
)
) {
return true;
}
const tokens = value const tokens = value
.split(/[^a-zа-я0-9]+/iu) .split(/[^a-zа-я0-9]+/iu)
.map((token) => token.trim()) .map((token) => token.trim())
@ -1741,6 +1780,16 @@ function shouldExpandSampleForValueAnalytics(intent: AddressIntent): boolean {
); );
} }
function shouldPreferKnownFinancialCounterpartyAnchor(intent: AddressIntent): boolean {
return (
intent === "bank_operations_by_counterparty" ||
intent === "customer_revenue_and_payments" ||
intent === "supplier_payouts_profile" ||
intent === "list_documents_by_counterparty" ||
intent === "list_contracts_by_counterparty"
);
}
export function extractAddressFilters(userMessage: string, intent: AddressIntent): AddressFilterExtraction { export function extractAddressFilters(userMessage: string, intent: AddressIntent): AddressFilterExtraction {
const rawText = String(userMessage ?? "").trim(); const rawText = String(userMessage ?? "").trim();
const text = normalizeMojibakeString(rawText); const text = normalizeMojibakeString(rawText);
@ -1828,6 +1877,14 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
} }
const allowGenericCounterpartyAnchor = !isInventoryTraceIntent(intent); const allowGenericCounterpartyAnchor = !isInventoryTraceIntent(intent);
const knownFinancialCounterparty =
allowGenericCounterpartyAnchor && shouldPreferKnownFinancialCounterpartyAnchor(intent)
? extractKnownFinancialCounterpartyAnchor(text)
: undefined;
if (knownFinancialCounterparty && !filters.counterparty) {
filters.counterparty = knownFinancialCounterparty;
warnings.push("counterparty_anchor_derived_from_known_financial_name");
}
const counterpartyMatch = allowGenericCounterpartyAnchor ? text.match(COUNTERPARTY_PATTERN) : null; const counterpartyMatch = allowGenericCounterpartyAnchor ? text.match(COUNTERPARTY_PATTERN) : null;
if (counterpartyMatch && !filters.counterparty) { if (counterpartyMatch && !filters.counterparty) {
filters.counterparty = cleanupAnchorValue(String(counterpartyMatch[1])); filters.counterparty = cleanupAnchorValue(String(counterpartyMatch[1]));

View File

@ -2136,7 +2136,7 @@ function hasBidirectionalValueFlowComparisonSignal(text: string): boolean {
normalized normalized
); );
const hasOutgoingCue = const hasOutgoingCue =
/(?:\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u0441\u043f\u0438\u0441\u0430\u043d|\u0437\u0430\u043f\u043b\u0430\u0442|\u043f\u043b\u0430\u0442\u0438\u043b|\u043e\u043f\u043b\u0430\u0442|outflow|outgoing|payout)/iu.test( /(?:\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u0441\u043f\u0438\u0441\u0430\u043d|\u0437\u0430\u043f\u043b\u0430\u0442|\u043f\u043b\u0430\u0442\u0438\u043b|\u0432\u044b\u043f\u043b\u0430\u0442|\u0432\u044b\u043f\u043b\u0430\u0447|\u0443\u043f\u043b\u0430\u0442|\u043e\u043f\u043b\u0430\u0442|outflow|outgoing|payout|paid)/iu.test(
normalized normalized
); );
const hasComparisonCue = const hasComparisonCue =
@ -2144,7 +2144,7 @@ function hasBidirectionalValueFlowComparisonSignal(text: string): boolean {
normalized normalized
); );
const hasValueFlowCue = const hasValueFlowCue =
/(?:\u0434\u0435\u043d\u044c\u0433|\u0434\u0435\u043d\u0435\u0433|\u0434\u0435\u043d\u0435\u0436|\u043f\u043e\u0442\u043e\u043a|\u043e\u0431\u043e\u0440\u043e\u0442|money|cash|flow)/iu.test( /(?:\u0434\u0435\u043d\u044c\u0433|\u0434\u0435\u043d\u0435\u0433|\u0434\u0435\u043d\u0435\u0436|\u0441\u0440\u0435\u0434\u0441\u0442\u0432|\u043f\u043e\u0442\u043e\u043a|\u043e\u0431\u043e\u0440\u043e\u0442|money|cash|funds|flow)/iu.test(
normalized normalized
); );
const hasNetAmountCue = /(?:сколько|сумм|итог|нетто|сальдо|минус|net|total|sum)/iu.test(normalized); const hasNetAmountCue = /(?:сколько|сумм|итог|нетто|сальдо|минус|net|total|sum)/iu.test(normalized);

View File

@ -88,6 +88,9 @@ interface NormalizedAddressRow {
organization?: string | null; organization?: string | null;
counterparty?: string | null; counterparty?: string | null;
contract?: string | null; contract?: string | null;
operation_kind?: string | null;
payment_purpose?: string | null;
comment?: string | null;
} }
interface AddressTryHandleOptions { interface AddressTryHandleOptions {
@ -1465,6 +1468,14 @@ function toNormalizedRows(rows: Array<Record<string, unknown>>): NormalizedAddre
); );
const counterparty = firstNonEmptyString(row.Контрагент, row.Counterparty, row.counterparty); const counterparty = firstNonEmptyString(row.Контрагент, row.Counterparty, row.counterparty);
const contract = firstNonEmptyString(row.Договор, row.Contract, row.contract); const contract = firstNonEmptyString(row.Договор, row.Contract, row.contract);
const operationKind = firstNonEmptyString(row.ВидОперации, row.OperationKind, row.operation_kind, row.operationType);
const paymentPurpose = firstNonEmptyString(
row.НазначениеПлатежа,
row.PaymentPurpose,
row.payment_purpose,
row.paymentPurpose
);
const comment = firstNonEmptyString(row.Комментарий, row.Comment, row.comment);
const analytics = collectAnalyticsStrings(row); const analytics = collectAnalyticsStrings(row);
return { return {
@ -1479,7 +1490,10 @@ function toNormalizedRows(rows: Array<Record<string, unknown>>): NormalizedAddre
warehouse, warehouse,
organization, organization,
counterparty, counterparty,
contract contract,
operation_kind: operationKind,
payment_purpose: paymentPurpose,
comment
}; };
}) })
.filter((item) => Boolean(item.period || item.registrator)); .filter((item) => Boolean(item.period || item.registrator));

View File

@ -31,6 +31,9 @@ export interface ComposeStageRow {
organization?: string | null; organization?: string | null;
counterparty?: string | null; counterparty?: string | null;
contract?: string | null; contract?: string | null;
operation_kind?: string | null;
payment_purpose?: string | null;
comment?: string | null;
} }
export interface VatDirectSourceProbeItem { export interface VatDirectSourceProbeItem {
@ -416,6 +419,90 @@ function isDirectBalanceQuestion(userMessage: string | null | undefined): boolea
); );
} }
function hasBankIncomingRoleBoundaryQuestion(userMessage: string | null | undefined): boolean {
const text = normalizeQuestionText(userMessage);
return (
/(?:входящ|поступлен|клиентск|выручк|кредит|депозит|возврат)/iu.test(text) &&
/(?:банк|сбербанк|финанс)/iu.test(text)
);
}
function hasBankOutgoingRoleBoundaryQuestion(userMessage: string | null | undefined): boolean {
const text = normalizeQuestionText(userMessage);
return (
/(?:исходящ|списан|платеж|поставщик|закуп|выплат)/iu.test(text) &&
/(?:банк|сбербанк|финанс)/iu.test(text)
);
}
function bankOperationDirection(row: ComposeStageRow): "incoming" | "outgoing" | "unknown" {
const text = normalizeQuestionText(`${row.registrator} ${row.operation_kind ?? ""}`);
if (/(?:поступлени[ея]\s+на\s+расчетн|bank\s+receipt|incoming)/iu.test(text)) {
return "incoming";
}
if (/(?:списани[ея]\s+с\s+расчетн|bank\s+payment|outgoing|write[-\s]?off)/iu.test(text)) {
return "outgoing";
}
return "unknown";
}
function bankOperationDirectionLabel(direction: "incoming" | "outgoing" | "unknown"): string {
if (direction === "incoming") {
return "входящее поступление";
}
if (direction === "outgoing") {
return "исходящее списание";
}
return "банковская операция без надежно распознанного направления";
}
function bankOperationEvidenceLine(rows: ComposeStageRow[]): string {
const sample = rows[0];
if (!sample) {
return "Проверенная строка 1С не найдена.";
}
const direction = bankOperationDirection(sample);
const parts = [`тип по документу: ${bankOperationDirectionLabel(direction)}`];
const operationKind = String(sample.operation_kind ?? "").trim();
const paymentPurpose = String(sample.payment_purpose ?? "").trim();
const contract = String(sample.contract ?? "").trim();
if (operationKind) {
parts.push(`вид операции: ${operationKind}`);
}
if (paymentPurpose) {
parts.push(`назначение платежа: ${paymentPurpose}`);
}
if (contract) {
parts.push(`договор: ${contract}`);
}
if (!operationKind && !paymentPurpose && !contract) {
parts.push("вид операции/назначение платежа/договор в материализованной строке не заполнены");
}
return `Основание 1С: ${parts.join("; ")}.`;
}
function bankRoleBoundaryLine(userMessage: string | null | undefined, rows: ComposeStageRow[]): string | null {
const incomingBoundary = hasBankIncomingRoleBoundaryQuestion(userMessage);
const outgoingBoundary = hasBankOutgoingRoleBoundaryQuestion(userMessage);
if (!incomingBoundary && !outgoingBoundary) {
return null;
}
const directions = rows.map(bankOperationDirection);
const hasIncomingRow = directions.includes("incoming");
const hasOutgoingRow = directions.includes("outgoing");
if (incomingBoundary) {
return hasIncomingRow
? "Выручкой от обычного клиента это не называю автоматически: для банка/финорганизации нужен вид операции, назначение платежа и договор; кредитный, депозитный или возвратный смысл без этих полей не исключаю и не притягиваю."
: hasOutgoingRow
? "В найденных строках по банку подтверждено исходящее списание, а входящее поступление от банка в этом срезе не подтверждено; клиентскую выручку, кредит или депозит по этой строке не доказываю."
: "Входящее поступление от банка в найденных строках не подтверждено; клиентскую выручку, кредитный или депозитный смысл без вида операции/назначения платежа не доказываю.";
}
return "Обычным поставщиком это не называю автоматически: для банка/финорганизации нужен вид операции, назначение платежа и договор; текущий срез подтверждает банковский платежный контур, а не бизнес-роль поставщика.";
}
function hasInventoryPurchaseDateActionFocus(userMessage: string | null | undefined): boolean { function hasInventoryPurchaseDateActionFocus(userMessage: string | null | undefined): boolean {
const text = normalizeQuestionText(userMessage); const text = normalizeQuestionText(userMessage);
if (!text) { if (!text) {
@ -4876,9 +4963,17 @@ function composeFactualReplyBody(
} }
if (intent === "bank_operations_by_counterparty") { if (intent === "bank_operations_by_counterparty") {
const rowCounterparties = uniqueStrings(
rows
.map((row) => extractCounterpartyName(row))
.filter((item): item is string => Boolean(item))
);
const counterparty = resolvePreferredCounterpartyDisplayLabel(options.counterpartyHint, rowCounterparties);
const roleBoundary = bankRoleBoundaryLine(options.userMessage, rows);
const lines = [ const lines = [
`Коротко: найдено банковских операций по контрагенту — ${rows.length}.`, `Коротко: найдено банковских операций${counterparty ? ` по ${counterparty}` : " по контрагенту"}${rows.length}.`,
"Показываю подтвержденные банковские операции из текущего среза.", roleBoundary ?? "Показываю подтвержденные банковские операции из текущего среза.",
bankOperationEvidenceLine(rows),
...formatTopRows(rows, rows.length) ...formatTopRows(rows, rows.length)
]; ];
return { return {

View File

@ -21,6 +21,8 @@ export type AssistantMcpDiscoveryEvidenceStatus = "confirmed" | "inferred_only"
export type AssistantMcpDiscoveryAnswerPermission = "confirmed_answer" | "bounded_inference" | "checked_sources_only"; export type AssistantMcpDiscoveryAnswerPermission = "confirmed_answer" | "bounded_inference" | "checked_sources_only";
export interface AssistantMcpDiscoveryTurnMeaningRef { export interface AssistantMcpDiscoveryTurnMeaningRef {
raw_message?: string | null;
effective_message?: string | null;
asked_domain_family?: string | null; asked_domain_family?: string | null;
asked_action_family?: string | null; asked_action_family?: string | null;
asked_aggregation_axis?: string | null; asked_aggregation_axis?: string | null;

View File

@ -39,6 +39,27 @@ function toNonEmptyString(value: unknown): string | null {
return text.length > 0 ? text : null; return text.length > 0 ? text : null;
} }
function normalizeQuestionText(value: unknown): string {
return String(value ?? "")
.toLowerCase()
.replace(/ё/g, "е")
.replace(/\s+/g, " ")
.trim();
}
function requestsFinancialCounterpartyBoundary(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 (
/(?:банк|сбербанк|финанс|кредит|депозит)/iu.test(text) &&
/(?:клиент|поставщик|выручк|топ|обычн|роль|поток)/iu.test(text)
);
}
function toStringList(value: unknown): string[] { function toStringList(value: unknown): string[] {
if (!Array.isArray(value)) { if (!Array.isArray(value)) {
return []; return [];
@ -815,6 +836,12 @@ function buildCompactBusinessOverviewReply(
: `; крупнейший получатель исходящих денег: ${topSupplier}` : `; крупнейший получатель исходящих денег: ${topSupplier}`
: ""; : "";
const roleBoundaryLead = topCustomer || topSupplier ? "; клиент/поставщик как бизнес-роли этим денежным срезом не подтверждены" : ""; const roleBoundaryLead = topCustomer || topSupplier ? "; клиент/поставщик как бизнес-роли этим денежным срезом не подтверждены" : "";
const financialBoundaryRequested = requestsFinancialCounterpartyBoundary(turnMeaning, graph);
const requestedFinancialBoundaryLine = financialBoundaryRequested
? topCustomerLooksFinancial || topSupplierLooksFinancial
? "Отдельно по банкам: если денежный топ ведет банк/финансовая организация, это нельзя автоматически читать как обычного клиента или поставщика; нужны назначение платежа, вид операции и договор. Поэтому такой поток не является доказанной клиентской выручкой, обычной поставкой или чистой прибылью без отдельной проверки."
: "Отдельно по банкам: банк/финансовую организацию в денежных топах нельзя автоматически читать как обычного клиента или поставщика; нужны назначение платежа, вид операции и договор. Поэтому такой поток не является доказанной клиентской выручкой, обычной поставкой или чистой прибылью без отдельной проверки."
: null;
const graphReasonCodes = toStringList(graph?.reason_codes); const graphReasonCodes = toStringList(graph?.reason_codes);
const directMoneyAnswer = graphReasonCodes.includes("data_need_graph_business_overview_direct_money_answer"); const directMoneyAnswer = graphReasonCodes.includes("data_need_graph_business_overview_direct_money_answer");
const crossScopeExecutiveSummary = Boolean(separateSubject && previousCounterpartySummary); const crossScopeExecutiveSummary = Boolean(separateSubject && previousCounterpartySummary);
@ -1082,7 +1109,7 @@ function buildCompactBusinessOverviewReply(
return null; return null;
} }
lines.push( lines.push(
`Коротко: в доступном проверенном срезе 1С по входящим денежным строкам лидирует ${leaderYear}: ${leaderAmount}${Number.isFinite(leaderRows) && leaderRows > 0 ? ` по ${leaderRows} строкам с суммой` : ""}; это не полный бухгалтерский рейтинг доходности.` `Коротко: ${organizationPrefix}в доступном проверенном срезе 1С по входящим денежным строкам лидирует ${leaderYear}: ${leaderAmount}${Number.isFinite(leaderRows) && leaderRows > 0 ? ` по ${leaderRows} строкам с суммой` : ""}; это не полный бухгалтерский рейтинг доходности.`
); );
const netYear = toNonEmptyString(netLeader?.year_bucket); const netYear = toNonEmptyString(netLeader?.year_bucket);
const netYearAmount = moneyText(netLeader?.net_amount_human_ru); const netYearAmount = moneyText(netLeader?.net_amount_human_ru);
@ -1094,6 +1121,9 @@ function buildCompactBusinessOverviewReply(
if (incomingAmount && outgoingAmount && netAmount) { if (incomingAmount && outgoingAmount && netAmount) {
lines.push(`Сверка по окну: входящие ${incomingAmount}, исходящие ${outgoingAmount}, ${netDirection} ${sentenceAmount(netAmount) ?? netAmount}.`); lines.push(`Сверка по окну: входящие ${incomingAmount}, исходящие ${outgoingAmount}, ${netDirection} ${sentenceAmount(netAmount) ?? netAmount}.`);
} }
if (requestedFinancialBoundaryLine) {
lines.push(requestedFinancialBoundaryLine);
}
const yearRows = businessOverviewYearRowsLine(overview); const yearRows = businessOverviewYearRowsLine(overview);
if (yearRows) { if (yearRows) {
lines.push(yearRows); lines.push(yearRows);
@ -1110,6 +1140,9 @@ function buildCompactBusinessOverviewReply(
: `Крупнейший подтвержденный источник входящих денег в этом срезе: ${customerName}${sentenceAmount(customerAmount) ?? customerAmount}.` : `Крупнейший подтвержденный источник входящих денег в этом срезе: ${customerName}${sentenceAmount(customerAmount) ?? customerAmount}.`
); );
} }
if (requestedFinancialBoundaryLine) {
lines.push(requestedFinancialBoundaryLine);
}
} else { } else {
return null; return null;
} }

View File

@ -984,6 +984,18 @@ function hasCrossScopeExecutiveSummarySignal(text: string): boolean {
); );
} }
function hasPlainBusinessOverviewSignal(text: string): boolean {
const hasPlainOverviewCue =
/(?:\u0432\u0437\u0440\u043e\u0441\u043b\p{L}*[\s\S]{0,40}\u043e\u0431\u0437\u043e\u0440|\u043a\u0440\u0430\u0442\u043a\p{L}*\s+\u043e\u0431\u0437\u043e\u0440|\u043e\u0431\u0437\u043e\u0440[\s\S]{0,100}(?:\u0432\u0445\u043e\u0434\u044f\u0449|\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u043d\u0435\u0442\u0442\u043e|incoming|outgoing|net))/iu.test(
text
);
const hasCompanyOrOperatingScopeCue =
/(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0431\u0438\u0437\u043d\u0435\u0441|\u0443\s+\u043d\u0430\u0441|\u043d\u0430\u0448\p{L}*|(?:19|20)\d{2}|company|organization|business)/iu.test(
text
);
return hasPlainOverviewCue && hasCompanyOrOperatingScopeCue;
}
function hasBusinessOverviewSignal(text: string): boolean { function hasBusinessOverviewSignal(text: string): boolean {
if ( if (
hasCrossScopeExecutiveSummarySignal(text) || hasCrossScopeExecutiveSummarySignal(text) ||
@ -991,6 +1003,7 @@ function hasBusinessOverviewSignal(text: string): boolean {
hasOrganizationLevelDebtPositionOverviewSignal(text) || hasOrganizationLevelDebtPositionOverviewSignal(text) ||
hasOrganizationLevelDebtDueDateOverviewSignal(text) || hasOrganizationLevelDebtDueDateOverviewSignal(text) ||
hasOrganizationLevelInventoryReserveLiquidationOverviewSignal(text) || hasOrganizationLevelInventoryReserveLiquidationOverviewSignal(text) ||
hasPlainBusinessOverviewSignal(text) ||
hasOrganizationLevelSupplierQualityOverviewSignal(text) hasOrganizationLevelSupplierQualityOverviewSignal(text)
) { ) {
return true; return true;
@ -2387,6 +2400,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
); );
const turnMeaning: AssistantMcpDiscoveryTurnMeaningRef = { const turnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {
raw_message: repairedUserText ?? rawUserText ?? null,
effective_message: repairedEffectiveText ?? rawEffectiveText ?? null,
asked_domain_family: asked_domain_family:
businessOverviewSignal businessOverviewSignal
? "business_overview" ? "business_overview"
@ -2492,6 +2507,12 @@ export function buildAssistantMcpDiscoveryTurnInput(
}; };
const cleanTurnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {}; const cleanTurnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {};
if (toNonEmptyString(turnMeaning.raw_message)) {
cleanTurnMeaning.raw_message = turnMeaning.raw_message;
}
if (toNonEmptyString(turnMeaning.effective_message)) {
cleanTurnMeaning.effective_message = turnMeaning.effective_message;
}
if (toNonEmptyString(turnMeaning.asked_domain_family)) { if (toNonEmptyString(turnMeaning.asked_domain_family)) {
cleanTurnMeaning.asked_domain_family = turnMeaning.asked_domain_family; cleanTurnMeaning.asked_domain_family = turnMeaning.asked_domain_family;
} }

View File

@ -10,4 +10,13 @@ describe("address intent resolver bidirectional value-flow arbitration", () => {
expect(result.intent).toBe("unknown"); expect(result.intent).toBe("unknown");
expect(result.reasons).toContain("unicode_bidirectional_value_flow_deferred_to_discovery"); expect(result.reasons).toContain("unicode_bidirectional_value_flow_deferred_to_discovery");
}); });
it("keeps normalized received-paid-net funds wording out of inventory routes", () => {
const result = resolveAddressIntent(
"\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0441\u0443\u043c\u043c\u0443 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0445 \u0441\u0440\u0435\u0434\u0441\u0442\u0432, \u0441\u0443\u043c\u043c\u044b \u0432\u044b\u043f\u043b\u0430\u0447\u0435\u043d\u043d\u044b\u0445 \u0441\u0440\u0435\u0434\u0441\u0442\u0432 \u0438 \u0447\u0438\u0441\u0442\u044b\u0439 \u043e\u0441\u0442\u0430\u0442\u043e\u043a (\u043d\u0435\u0442\u0442\u043e) \u0434\u043b\u044f \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430 '\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a' \u0437\u0430 2020 \u0433\u043e\u0434."
);
expect(result.intent).toBe("unknown");
expect(result.reasons).toContain("unicode_bidirectional_value_flow_deferred_to_discovery");
});
}); });

View File

@ -561,6 +561,34 @@ describe("address compose stage utf8 headers", () => {
expect(reply.text).not.toContain("live address lane"); expect(reply.text).not.toContain("live address lane");
}); });
it("keeps bank counterparty classification bounded for incoming revenue questions", () => {
const reply = composeFactualReply(
"bank_operations_by_counterparty",
[
{
period: "2020-12-16T16:20:51Z",
registrator: "Списание с расчетного счета 00000000293 от 16.12.2020 16:20:51",
account_dt: "0",
account_kt: "0",
amount: 60,
analytics: ["СБЕРБАНК, ПАО", "0"],
counterparty: "СБЕРБАНК, ПАО"
}
],
{
counterpartyHint: "СБЕРБАНК",
userMessage:
"А если СБЕРБАНК встречается во входящих поступлениях, это клиентская выручка или кредитный/депозитный банковский смысл?"
}
);
expect(reply.text).toContain("по СБЕРБАНК");
expect(reply.text).toContain("входящее поступление от банка в этом срезе не подтверждено");
expect(reply.text).toContain("клиентскую выручку");
expect(reply.text).toContain("Основание 1С");
expect(reply.text).toContain("вид операции/назначение платежа/договор");
});
it("renders readable russian header for contracts-by-counterparty list", () => { it("renders readable russian header for contracts-by-counterparty list", () => {
const reply = composeFactualReply("list_contracts_by_counterparty", [ const reply = composeFactualReply("list_contracts_by_counterparty", [
{ {
@ -2264,6 +2292,16 @@ describe("address intent resolver expansion (M2.3a)", () => {
expect(result.intent).toBe("bank_operations_by_counterparty"); expect(result.intent).toBe("bank_operations_by_counterparty");
}); });
it("prefers explicit bank name over supplier-vs-financial comparison text", () => {
const result = extractAddressFilters(
"\u041f\u043e \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441 \u0437\u0430 2020 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e \u043f\u043e\u0441\u043c\u043e\u0442\u0440\u0438 \u043f\u043b\u0430\u0442\u0435\u0436\u0438 \u0432 \u0421\u0411\u0415\u0420\u0411\u0410\u041d\u041a: \u044d\u0442\u043e \u043e\u0431\u044b\u0447\u043d\u044b\u0439 \u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a \u0438\u043b\u0438 \u0431\u0430\u043d\u043a\u043e\u0432\u0441\u043a\u0438\u0439/\u0444\u0438\u043d\u0430\u043d\u0441\u043e\u0432\u044b\u0439 \u043f\u043e\u0442\u043e\u043a?",
"bank_operations_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("\u0421\u0411\u0415\u0420\u0411\u0410\u041d\u041a");
expect(result.warnings).toContain("counterparty_anchor_derived_from_known_financial_name");
});
it("resolves documents forming balance intent", () => { it("resolves documents forming balance intent", () => {
const result = resolveAddressIntent("which documents form balance for account 62 as of 2020-07-31"); const result = resolveAddressIntent("which documents form balance for account 62 as of 2020-07-31");
expect(result.intent).toBe("documents_forming_balance"); expect(result.intent).toBe("documents_forming_balance");

View File

@ -309,6 +309,13 @@ describe("assistant MCP discovery response candidate", () => {
entryPoint({ entryPoint({
turn_input: { turn_input: {
adapter_status: "ready", adapter_status: "ready",
turn_meaning_ref: {
raw_message:
"Дай краткий обзор ООО Альтернатива Плюс за 2020 и отметь, если в топах есть банк, почему его нельзя читать как обычного клиента или поставщика.",
effective_message:
"Дать краткий обзор ООО Альтернатива Плюс за 2020: входящие, исходящие, нетто и банковскую границу.",
explicit_organization_scope: "ООО Альтернатива Плюс"
},
data_need_graph: { data_need_graph: {
business_fact_family: "business_overview", business_fact_family: "business_overview",
ranking_need: "top_desc", ranking_need: "top_desc",
@ -375,9 +382,12 @@ describe("assistant MCP discovery response candidate", () => {
); );
expect(candidate.reply_text).toContain("в доступном проверенном срезе 1С"); expect(candidate.reply_text).toContain("в доступном проверенном срезе 1С");
expect(candidate.reply_text).toContain("по компании ООО Альтернатива Плюс");
expect(candidate.reply_text).toContain("лидирует 2015"); expect(candidate.reply_text).toContain("лидирует 2015");
expect(candidate.reply_text).toContain("2015"); expect(candidate.reply_text).toContain("2015");
expect(candidate.reply_text).toContain("136 723 459,73 руб."); expect(candidate.reply_text).toContain("136 723 459,73 руб.");
expect(candidate.reply_text).toContain("банк/финансовую организацию");
expect(candidate.reply_text).toContain("нельзя автоматически читать как обычного клиента или поставщика");
expect(candidate.reply_text).toContain("не полный бухгалтерский рейтинг доходности"); expect(candidate.reply_text).toContain("не полный бухгалтерский рейтинг доходности");
expect(candidate.reply_text).toContain("не как чистую бухгалтерскую прибыль"); expect(candidate.reply_text).toContain("не как чистую бухгалтерскую прибыль");
expect(candidate.reply_text).toContain("проверка достигла лимита строк"); expect(candidate.reply_text).toContain("проверка достигла лимита строк");

View File

@ -1641,6 +1641,38 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_value_flow_signal_detected"); expect(result.reason_codes).not.toContain("mcp_discovery_value_flow_signal_detected");
}); });
it("routes plain short overview with money axes into business overview over value-flow", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
"\u0422\u0435\u043f\u0435\u0440\u044c \u0434\u0430\u0439 \u0432\u0437\u0440\u043e\u0441\u043b\u044b\u0439 \u043a\u0440\u0430\u0442\u043a\u0438\u0439 \u043e\u0431\u0437\u043e\u0440 \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441 \u0437\u0430 2020: \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0435, \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0435, \u043d\u0435\u0442\u0442\u043e \u0438 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e \u043e\u0442\u043c\u0435\u0442\u044c, \u0435\u0441\u043b\u0438 \u0432 \u0442\u043e\u043f\u0430\u0445 \u0435\u0441\u0442\u044c \u0431\u0430\u043d\u043a.",
assistantTurnMeaning: {
asked_domain_family: "counterparty",
asked_action_family: "counterparty_value_or_turnover",
explicit_intent_candidate: "customer_revenue_and_payments",
explicit_entity_candidates: [{ value: "\u0438\u043b\u0438 \u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\u0430" }],
stale_replay_forbidden: false
},
predecomposeContract: {
entities: {
organization: "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"
},
period: {
period_from: "2020-01-01",
period_to: "2020-12-31"
}
}
});
expect(result.adapter_status).toBe("ready");
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "business_overview",
explicit_date_scope: "2020"
});
expect(result.reason_codes).toContain("mcp_discovery_broad_business_evaluation_route_candidate");
expect(result.reason_codes).not.toContain("mcp_discovery_value_flow_signal_detected");
});
it("keeps explicit year out of the organization scope for raw business overview wording", () => { it("keeps explicit year out of the organization scope for raw business overview wording", () => {
const result = buildAssistantMcpDiscoveryTurnInput({ const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: userMessage:

View File

@ -1,4 +1,52 @@
[ [
{
"generation_id": "gen-ag05122250-4451a8",
"created_at": "2026-05-12T22:50:23+00:00",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 97 financial counterparty flow hints replay",
"count": 4,
"domain": "address_phase97_financial_counterparty_flow_hints",
"questions": [
"По ООО Альтернатива Плюс за 2020 отдельно посмотри платежи в СБЕРБАНК: это обычный поставщик или банковский/финансовый поток? Дай коротко и по проверенным данным 1С.",
"А если по этой же компании СБЕРБАНК встречается во входящих поступлениях, это клиентская выручка или там может быть кредитный/депозитный банковский смысл? Не притягивай, скажи что подтверждено.",
"Теперь дай взрослый краткий обзор ООО Альтернатива Плюс за 2020: входящие, исходящие, нетто и отдельно отметь, если в топах есть банк, почему его нельзя читать как обычного клиента или поставщика.",
"А теперь отдельно по Группа СВК за 2020: сколько денег получили, сколько заплатили и какое нетто?"
],
"generated_by": "codex_agent",
"saved_case_set_file": "assistant_autogen_saved_user_sessions_20260512225023_gen-ag05122250-4451a8.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_20260512225023_gen-ag05122250-4451a8.json",
"saved_case_set_kind": "agent_semantic_scenario",
"agent_run": true,
"agent_focus": "Focused semantic replay for the Open-World Schema/Primitive Discovery slice: bank-like counterparties must not be described as ordinary suppliers/customers when operation, payment purpose, contract, or comment fields indicate banking commission, credit, deposit, tax/budget, or payroll-like flows. The replay also keeps a normal counterparty value-flow canary.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase97_financial_counterparty_flow_hints.json",
"scenario_id": "address_truth_harness_phase97_financial_counterparty_flow_hints",
"semantic_tags": [
"bank_like_customer_boundary",
"bank_like_supplier_boundary",
"business_overview",
"canary",
"counterparty_net_cash_flow",
"customer_revenue_and_payments",
"financial_counterparty_flow_hint",
"profit_margin_boundary",
"stale_scope_guard",
"supplier_payouts_profile"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase97_financial_counterparty_flow_hints_live4",
"saved_after_validated_replay": true
}
},
{ {
"generation_id": "gen-ag05122057-c9786e", "generation_id": "gen-ag05122057-c9786e",
"created_at": "2026-05-12T20:57:28+00:00", "created_at": "2026-05-12T20:57:28+00:00",

View File

@ -0,0 +1,135 @@
{
"saved_at": "2026-05-12T22:50:23+00:00",
"generation_id": "gen-ag05122250-4451a8",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 97 financial counterparty flow hints replay",
"agent_run": true,
"questions": [
"По ООО Альтернатива Плюс за 2020 отдельно посмотри платежи в СБЕРБАНК: это обычный поставщик или банковский/финансовый поток? Дай коротко и по проверенным данным 1С.",
"А если по этой же компании СБЕРБАНК встречается во входящих поступлениях, это клиентская выручка или там может быть кредитный/депозитный банковский смысл? Не притягивай, скажи что подтверждено.",
"Теперь дай взрослый краткий обзор ООО Альтернатива Плюс за 2020: входящие, исходящие, нетто и отдельно отметь, если в топах есть банк, почему его нельзя читать как обычного клиента или поставщика.",
"А теперь отдельно по Группа СВК за 2020: сколько денег получили, сколько заплатили и какое нетто?"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Focused semantic replay for the Open-World Schema/Primitive Discovery slice: bank-like counterparties must not be described as ordinary suppliers/customers when operation, payment purpose, contract, or comment fields indicate banking commission, credit, deposit, tax/budget, or payroll-like flows. The replay also keeps a normal counterparty value-flow canary.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase97_financial_counterparty_flow_hints.json",
"scenario_id": "address_truth_harness_phase97_financial_counterparty_flow_hints",
"semantic_tags": [
"bank_like_customer_boundary",
"bank_like_supplier_boundary",
"business_overview",
"canary",
"counterparty_net_cash_flow",
"customer_revenue_and_payments",
"financial_counterparty_flow_hint",
"profit_margin_boundary",
"stale_scope_guard",
"supplier_payouts_profile"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase97_financial_counterparty_flow_hints_live4",
"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\\phase97_financial_counterparty_flow_hints_live4",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 4,
"steps_passed": 4,
"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 отдельно посмотри платежи в СБЕРБАНК: это обычный поставщик или банковский/финансовый поток? Дай коротко и по проверенным данным 1С.",
"created_at": "2026-05-12T22:50:23+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "А если по этой же компании СБЕРБАНК встречается во входящих поступлениях, это клиентская выручка или там может быть кредитный/депозитный банковский смысл? Не притягивай, скажи что подтверждено.",
"created_at": "2026-05-12T22:50:23+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "Теперь дай взрослый краткий обзор ООО Альтернатива Плюс за 2020: входящие, исходящие, нетто и отдельно отметь, если в топах есть банк, почему его нельзя читать как обычного клиента или поставщика.",
"created_at": "2026-05-12T22:50:23+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "А теперь отдельно по Группа СВК за 2020: сколько денег получили, сколько заплатили и какое нетто?",
"created_at": "2026-05-12T22:50:23+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": "Focused semantic replay for the Open-World Schema/Primitive Discovery slice: bank-like counterparties must not be described as ordinary suppliers/customers when operation, payment purpose, contract, or comment fields indicate banking commission, credit, deposit, tax/budget, or payroll-like flows. The replay also keeps a normal counterparty value-flow canary.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase97_financial_counterparty_flow_hints.json",
"scenario_id": "address_truth_harness_phase97_financial_counterparty_flow_hints",
"semantic_tags": [
"bank_like_customer_boundary",
"bank_like_supplier_boundary",
"business_overview",
"canary",
"counterparty_net_cash_flow",
"customer_revenue_and_payments",
"financial_counterparty_flow_hint",
"profit_margin_boundary",
"stale_scope_guard",
"supplier_payouts_profile"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase97_financial_counterparty_flow_hints_live4",
"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\\phase97_financial_counterparty_flow_hints_live4",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 4,
"steps_passed": 4,
"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,37 @@
{
"suite_id": "assistant_saved_session_gen-ag05122250-4451a8",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-05-12T22:50:23+00:00",
"generation_id": "gen-ag05122250-4451a8",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 97 financial counterparty flow hints replay",
"domain": "address_phase97_financial_counterparty_flow_hints",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Phase 97 financial counterparty flow hints replay",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "По ООО Альтернатива Плюс за 2020 отдельно посмотри платежи в СБЕРБАНК: это обычный поставщик или банковский/финансовый поток? Дай коротко и по проверенным данным 1С."
},
{
"user_message": "А если по этой же компании СБЕРБАНК встречается во входящих поступлениях, это клиентская выручка или там может быть кредитный/депозитный банковский смысл? Не притягивай, скажи что подтверждено."
},
{
"user_message": "Теперь дай взрослый краткий обзор ООО Альтернатива Плюс за 2020: входящие, исходящие, нетто и отдельно отметь, если в топах есть банк, почему его нельзя читать как обычного клиента или поставщика."
},
{
"user_message": "А теперь отдельно по Группа СВК за 2020: сколько денег получили, сколько заплатили и какое нетто?"
}
]
}
]
}