Укрепить банковские границы денежных потоков phase97
This commit is contained in:
parent
e997449785
commit
04e670ab76
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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 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 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 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/;
|
||||
|
|
@ -608,6 +626,18 @@ function isLikelyCounterpartyToken(rawToken) {
|
|||
}
|
||||
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) {
|
||||
const value = String(rawValue ?? "")
|
||||
.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)) {
|
||||
return true;
|
||||
}
|
||||
if (/^(?:или|это|там|может|можно|обычн\p{L}*|клиентск\p{L}*|банковск\p{L}*(?:\/|\s+и\s+)?финансов\p{L}*)\b/iu.test(value)) {
|
||||
return true;
|
||||
}
|
||||
const tokens = value
|
||||
.split(/[^a-zа-я0-9]+/iu)
|
||||
.map((token) => token.trim())
|
||||
|
|
@ -1495,6 +1528,13 @@ function shouldExpandSampleForValueAnalytics(intent) {
|
|||
intent === "supplier_payouts_profile" ||
|
||||
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) {
|
||||
const rawText = String(userMessage ?? "").trim();
|
||||
const text = normalizeMojibakeString(rawText);
|
||||
|
|
@ -1573,6 +1613,13 @@ function extractAddressFilters(userMessage, 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;
|
||||
if (counterpartyMatch && !filters.counterparty) {
|
||||
filters.counterparty = cleanupAnchorValue(String(counterpartyMatch[1]));
|
||||
|
|
|
|||
|
|
@ -1658,9 +1658,9 @@ function hasBidirectionalValueFlowComparisonSignal(text) {
|
|||
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 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 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);
|
||||
return hasIncomingCue && hasOutgoingCue && hasComparisonCue && (hasValueFlowCue || hasNetAmountCue);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1189,6 +1189,9 @@ function toNormalizedRows(rows) {
|
|||
const organization = firstNonEmptyString(row.Организация, row.Organization, row.organization, row.organization_name, row.ОрганизацияПредставление);
|
||||
const counterparty = firstNonEmptyString(row.Контрагент, row.Counterparty, row.counterparty);
|
||||
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);
|
||||
return {
|
||||
period,
|
||||
|
|
@ -1202,7 +1205,10 @@ function toNormalizedRows(rows) {
|
|||
warehouse,
|
||||
organization,
|
||||
counterparty,
|
||||
contract
|
||||
contract,
|
||||
operation_kind: operationKind,
|
||||
payment_purpose: paymentPurpose,
|
||||
comment
|
||||
};
|
||||
})
|
||||
.filter((item) => Boolean(item.period || item.registrator));
|
||||
|
|
|
|||
|
|
@ -277,6 +277,77 @@ function isDirectBalanceQuestion(userMessage) {
|
|||
}
|
||||
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) {
|
||||
const text = normalizeQuestionText(userMessage);
|
||||
if (!text) {
|
||||
|
|
@ -3820,9 +3891,15 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
|||
};
|
||||
}
|
||||
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 = [
|
||||
`Коротко: найдено банковских операций по контрагенту — ${rows.length}.`,
|
||||
"Показываю подтвержденные банковские операции из текущего среза.",
|
||||
`Коротко: найдено банковских операций${counterparty ? ` по ${counterparty}` : " по контрагенту"} — ${rows.length}.`,
|
||||
roleBoundary ?? "Показываю подтвержденные банковские операции из текущего среза.",
|
||||
bankOperationEvidenceLine(rows),
|
||||
...formatTopRows(rows, rows.length)
|
||||
];
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,23 @@ function toNonEmptyString(value) {
|
|||
const text = String(value).trim();
|
||||
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) {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
|
|
@ -693,6 +710,12 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
|||
: `; крупнейший получатель исходящих денег: ${topSupplier}`
|
||||
: "";
|
||||
const roleBoundaryLead = topCustomer || topSupplier ? "; клиент/поставщик как бизнес-роли этим денежным срезом не подтверждены" : "";
|
||||
const financialBoundaryRequested = requestsFinancialCounterpartyBoundary(turnMeaning, graph);
|
||||
const requestedFinancialBoundaryLine = financialBoundaryRequested
|
||||
? topCustomerLooksFinancial || topSupplierLooksFinancial
|
||||
? "Отдельно по банкам: если денежный топ ведет банк/финансовая организация, это нельзя автоматически читать как обычного клиента или поставщика; нужны назначение платежа, вид операции и договор. Поэтому такой поток не является доказанной клиентской выручкой, обычной поставкой или чистой прибылью без отдельной проверки."
|
||||
: "Отдельно по банкам: банк/финансовую организацию в денежных топах нельзя автоматически читать как обычного клиента или поставщика; нужны назначение платежа, вид операции и договор. Поэтому такой поток не является доказанной клиентской выручкой, обычной поставкой или чистой прибылью без отдельной проверки."
|
||||
: null;
|
||||
const graphReasonCodes = toStringList(graph?.reason_codes);
|
||||
const directMoneyAnswer = graphReasonCodes.includes("data_need_graph_business_overview_direct_money_answer");
|
||||
const crossScopeExecutiveSummary = Boolean(separateSubject && previousCounterpartySummary);
|
||||
|
|
@ -901,7 +924,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
|||
if (!leaderYear || !leaderAmount) {
|
||||
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 netYearAmount = moneyText(netLeader?.net_amount_human_ru);
|
||||
if (netYear && netYearAmount) {
|
||||
|
|
@ -912,6 +935,9 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
|||
if (incomingAmount && outgoingAmount && netAmount) {
|
||||
lines.push(`Сверка по окну: входящие ${incomingAmount}, исходящие ${outgoingAmount}, ${netDirection} ${sentenceAmount(netAmount) ?? netAmount}.`);
|
||||
}
|
||||
if (requestedFinancialBoundaryLine) {
|
||||
lines.push(requestedFinancialBoundaryLine);
|
||||
}
|
||||
const yearRows = businessOverviewYearRowsLine(overview);
|
||||
if (yearRows) {
|
||||
lines.push(yearRows);
|
||||
|
|
@ -925,6 +951,9 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
|||
? `Крупнейший входящий денежный источник в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}. По названию это банк/финансовая организация, поэтому без назначения платежа не называю это клиентской выручкой.${nonFinancialCustomer ? ` Крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}.` : ""}`
|
||||
: `Крупнейший подтвержденный источник входящих денег в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}.`);
|
||||
}
|
||||
if (requestedFinancialBoundaryLine) {
|
||||
lines.push(requestedFinancialBoundaryLine);
|
||||
}
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -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) &&
|
||||
/(?:\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) {
|
||||
if (hasCrossScopeExecutiveSummarySignal(text) ||
|
||||
hasOrganizationLevelEarningsOverviewSignal(text) ||
|
||||
hasOrganizationLevelDebtPositionOverviewSignal(text) ||
|
||||
hasOrganizationLevelDebtDueDateOverviewSignal(text) ||
|
||||
hasOrganizationLevelInventoryReserveLiquidationOverviewSignal(text) ||
|
||||
hasPlainBusinessOverviewSignal(text) ||
|
||||
hasOrganizationLevelSupplierQualityOverviewSignal(text)) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1786,6 +1792,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
normalizedFollowupDateScope);
|
||||
const clarificationLoopSeedApplied = Boolean(followupSeed.loopStatus === "awaiting_clarification" && followupSeed.loopSelectedChainId);
|
||||
const turnMeaning = {
|
||||
raw_message: repairedUserText ?? rawUserText ?? null,
|
||||
effective_message: repairedEffectiveText ?? rawEffectiveText ?? null,
|
||||
asked_domain_family: businessOverviewSignal
|
||||
? "business_overview"
|
||||
: lifecycleSignal
|
||||
|
|
@ -1882,6 +1890,12 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
followupDiscoverySeedApplicable)
|
||||
};
|
||||
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)) {
|
||||
cleanTurnMeaning.asked_domain_family = turnMeaning.asked_domain_family;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,25 @@ const LIMIT_PATTERN = /(?:\btop\b|\blimit\b|первые|топ)[\s\-–—_:№
|
|||
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 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 =
|
||||
/(?:по\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/;
|
||||
|
|
@ -694,6 +713,19 @@ function isLikelyCounterpartyToken(rawToken: string): boolean {
|
|||
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 {
|
||||
const value = String(rawValue ?? "")
|
||||
.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)) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
/^(?:или|это|там|может|можно|обычн\p{L}*|клиентск\p{L}*|банковск\p{L}*(?:\/|\s+и\s+)?финансов\p{L}*)\b/iu.test(
|
||||
value
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const tokens = value
|
||||
.split(/[^a-zа-я0-9]+/iu)
|
||||
.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 {
|
||||
const rawText = String(userMessage ?? "").trim();
|
||||
const text = normalizeMojibakeString(rawText);
|
||||
|
|
@ -1828,6 +1877,14 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
|
|||
}
|
||||
|
||||
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;
|
||||
if (counterpartyMatch && !filters.counterparty) {
|
||||
filters.counterparty = cleanupAnchorValue(String(counterpartyMatch[1]));
|
||||
|
|
|
|||
|
|
@ -2136,7 +2136,7 @@ function hasBidirectionalValueFlowComparisonSignal(text: string): boolean {
|
|||
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(
|
||||
/(?:\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 =
|
||||
|
|
@ -2144,7 +2144,7 @@ function hasBidirectionalValueFlowComparisonSignal(text: string): boolean {
|
|||
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(
|
||||
/(?:\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);
|
||||
|
|
|
|||
|
|
@ -88,6 +88,9 @@ interface NormalizedAddressRow {
|
|||
organization?: string | null;
|
||||
counterparty?: string | null;
|
||||
contract?: string | null;
|
||||
operation_kind?: string | null;
|
||||
payment_purpose?: string | null;
|
||||
comment?: string | null;
|
||||
}
|
||||
|
||||
interface AddressTryHandleOptions {
|
||||
|
|
@ -1465,6 +1468,14 @@ function toNormalizedRows(rows: Array<Record<string, unknown>>): NormalizedAddre
|
|||
);
|
||||
const counterparty = firstNonEmptyString(row.Контрагент, row.Counterparty, row.counterparty);
|
||||
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);
|
||||
|
||||
return {
|
||||
|
|
@ -1479,7 +1490,10 @@ function toNormalizedRows(rows: Array<Record<string, unknown>>): NormalizedAddre
|
|||
warehouse,
|
||||
organization,
|
||||
counterparty,
|
||||
contract
|
||||
contract,
|
||||
operation_kind: operationKind,
|
||||
payment_purpose: paymentPurpose,
|
||||
comment
|
||||
};
|
||||
})
|
||||
.filter((item) => Boolean(item.period || item.registrator));
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ export interface ComposeStageRow {
|
|||
organization?: string | null;
|
||||
counterparty?: string | null;
|
||||
contract?: string | null;
|
||||
operation_kind?: string | null;
|
||||
payment_purpose?: string | null;
|
||||
comment?: string | null;
|
||||
}
|
||||
|
||||
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 {
|
||||
const text = normalizeQuestionText(userMessage);
|
||||
if (!text) {
|
||||
|
|
@ -4876,9 +4963,17 @@ function composeFactualReplyBody(
|
|||
}
|
||||
|
||||
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 = [
|
||||
`Коротко: найдено банковских операций по контрагенту — ${rows.length}.`,
|
||||
"Показываю подтвержденные банковские операции из текущего среза.",
|
||||
`Коротко: найдено банковских операций${counterparty ? ` по ${counterparty}` : " по контрагенту"} — ${rows.length}.`,
|
||||
roleBoundary ?? "Показываю подтвержденные банковские операции из текущего среза.",
|
||||
bankOperationEvidenceLine(rows),
|
||||
...formatTopRows(rows, rows.length)
|
||||
];
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ export type AssistantMcpDiscoveryEvidenceStatus = "confirmed" | "inferred_only"
|
|||
export type AssistantMcpDiscoveryAnswerPermission = "confirmed_answer" | "bounded_inference" | "checked_sources_only";
|
||||
|
||||
export interface AssistantMcpDiscoveryTurnMeaningRef {
|
||||
raw_message?: string | null;
|
||||
effective_message?: string | null;
|
||||
asked_domain_family?: string | null;
|
||||
asked_action_family?: string | null;
|
||||
asked_aggregation_axis?: string | null;
|
||||
|
|
|
|||
|
|
@ -39,6 +39,27 @@ function toNonEmptyString(value: unknown): string | 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[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
|
|
@ -815,6 +836,12 @@ function buildCompactBusinessOverviewReply(
|
|||
: `; крупнейший получатель исходящих денег: ${topSupplier}`
|
||||
: "";
|
||||
const roleBoundaryLead = topCustomer || topSupplier ? "; клиент/поставщик как бизнес-роли этим денежным срезом не подтверждены" : "";
|
||||
const financialBoundaryRequested = requestsFinancialCounterpartyBoundary(turnMeaning, graph);
|
||||
const requestedFinancialBoundaryLine = financialBoundaryRequested
|
||||
? topCustomerLooksFinancial || topSupplierLooksFinancial
|
||||
? "Отдельно по банкам: если денежный топ ведет банк/финансовая организация, это нельзя автоматически читать как обычного клиента или поставщика; нужны назначение платежа, вид операции и договор. Поэтому такой поток не является доказанной клиентской выручкой, обычной поставкой или чистой прибылью без отдельной проверки."
|
||||
: "Отдельно по банкам: банк/финансовую организацию в денежных топах нельзя автоматически читать как обычного клиента или поставщика; нужны назначение платежа, вид операции и договор. Поэтому такой поток не является доказанной клиентской выручкой, обычной поставкой или чистой прибылью без отдельной проверки."
|
||||
: null;
|
||||
const graphReasonCodes = toStringList(graph?.reason_codes);
|
||||
const directMoneyAnswer = graphReasonCodes.includes("data_need_graph_business_overview_direct_money_answer");
|
||||
const crossScopeExecutiveSummary = Boolean(separateSubject && previousCounterpartySummary);
|
||||
|
|
@ -1082,7 +1109,7 @@ function buildCompactBusinessOverviewReply(
|
|||
return null;
|
||||
}
|
||||
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 netYearAmount = moneyText(netLeader?.net_amount_human_ru);
|
||||
|
|
@ -1094,6 +1121,9 @@ function buildCompactBusinessOverviewReply(
|
|||
if (incomingAmount && outgoingAmount && netAmount) {
|
||||
lines.push(`Сверка по окну: входящие ${incomingAmount}, исходящие ${outgoingAmount}, ${netDirection} ${sentenceAmount(netAmount) ?? netAmount}.`);
|
||||
}
|
||||
if (requestedFinancialBoundaryLine) {
|
||||
lines.push(requestedFinancialBoundaryLine);
|
||||
}
|
||||
const yearRows = businessOverviewYearRowsLine(overview);
|
||||
if (yearRows) {
|
||||
lines.push(yearRows);
|
||||
|
|
@ -1110,6 +1140,9 @@ function buildCompactBusinessOverviewReply(
|
|||
: `Крупнейший подтвержденный источник входящих денег в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}.`
|
||||
);
|
||||
}
|
||||
if (requestedFinancialBoundaryLine) {
|
||||
lines.push(requestedFinancialBoundaryLine);
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
if (
|
||||
hasCrossScopeExecutiveSummarySignal(text) ||
|
||||
|
|
@ -991,6 +1003,7 @@ function hasBusinessOverviewSignal(text: string): boolean {
|
|||
hasOrganizationLevelDebtPositionOverviewSignal(text) ||
|
||||
hasOrganizationLevelDebtDueDateOverviewSignal(text) ||
|
||||
hasOrganizationLevelInventoryReserveLiquidationOverviewSignal(text) ||
|
||||
hasPlainBusinessOverviewSignal(text) ||
|
||||
hasOrganizationLevelSupplierQualityOverviewSignal(text)
|
||||
) {
|
||||
return true;
|
||||
|
|
@ -2387,6 +2400,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
);
|
||||
|
||||
const turnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {
|
||||
raw_message: repairedUserText ?? rawUserText ?? null,
|
||||
effective_message: repairedEffectiveText ?? rawEffectiveText ?? null,
|
||||
asked_domain_family:
|
||||
businessOverviewSignal
|
||||
? "business_overview"
|
||||
|
|
@ -2492,6 +2507,12 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
};
|
||||
|
||||
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)) {
|
||||
cleanTurnMeaning.asked_domain_family = turnMeaning.asked_domain_family;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,4 +10,13 @@ describe("address intent resolver bidirectional value-flow arbitration", () => {
|
|||
expect(result.intent).toBe("unknown");
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -561,6 +561,34 @@ describe("address compose stage utf8 headers", () => {
|
|||
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", () => {
|
||||
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");
|
||||
});
|
||||
|
||||
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", () => {
|
||||
const result = resolveAddressIntent("which documents form balance for account 62 as of 2020-07-31");
|
||||
expect(result.intent).toBe("documents_forming_balance");
|
||||
|
|
|
|||
|
|
@ -309,6 +309,13 @@ describe("assistant MCP discovery response candidate", () => {
|
|||
entryPoint({
|
||||
turn_input: {
|
||||
adapter_status: "ready",
|
||||
turn_meaning_ref: {
|
||||
raw_message:
|
||||
"Дай краткий обзор ООО Альтернатива Плюс за 2020 и отметь, если в топах есть банк, почему его нельзя читать как обычного клиента или поставщика.",
|
||||
effective_message:
|
||||
"Дать краткий обзор ООО Альтернатива Плюс за 2020: входящие, исходящие, нетто и банковскую границу.",
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс"
|
||||
},
|
||||
data_need_graph: {
|
||||
business_fact_family: "business_overview",
|
||||
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("по компании ООО Альтернатива Плюс");
|
||||
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("банк/финансовую организацию");
|
||||
expect(candidate.reply_text).toContain("нельзя автоматически читать как обычного клиента или поставщика");
|
||||
expect(candidate.reply_text).toContain("не полный бухгалтерский рейтинг доходности");
|
||||
expect(candidate.reply_text).toContain("не как чистую бухгалтерскую прибыль");
|
||||
expect(candidate.reply_text).toContain("проверка достигла лимита строк");
|
||||
|
|
|
|||
|
|
@ -1641,6 +1641,38 @@ describe("assistant MCP discovery turn input adapter", () => {
|
|||
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", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage:
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
"created_at": "2026-05-12T20:57:28+00:00",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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: сколько денег получили, сколько заплатили и какое нетто?"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue