diff --git a/docs/ARCH/11 - architecture_turnaround/21 - current_status_canon_2026-05-01.md b/docs/ARCH/11 - architecture_turnaround/21 - current_status_canon_2026-05-01.md index 04983ed..692db6a 100644 --- a/docs/ARCH/11 - architecture_turnaround/21 - current_status_canon_2026-05-01.md +++ b/docs/ARCH/11 - architecture_turnaround/21 - current_status_canon_2026-05-01.md @@ -88,7 +88,8 @@ Fresh validation cut: - Current live canary: `phase96_inventory_reserve_liquidation_quality_rerun` accepted `2/2`. - Current accepted autorun: `AGENT | Phase 96 inventory reserve/liquidation quality-events` (`gen-ag05122057-c9786e`). - Implementation breadth: `~99% (Open-World Bounded Autonomy Breadth through Slice 25)`. -- Next active slice: start the broader open-world schema/primitive discovery module and use phase91-phase96 as regression canaries. +- Active broader autonomy slice: `Open-World Schema/Primitive Discovery`, starting with `Financial Counterparty Flow Hints`: bank-document money-flow recipes now expose operation/purpose/comment fields and ranked value-flow buckets carry `financial_flow_hint` so bank-like leaders are not treated as ordinary suppliers/customers by name alone. +- Next active slice: add live semantic replay around bank-like counterparty wording, then continue broader schema/primitive discovery while using phase91-phase96 as regression canaries. - Active module progress: `~99% (Agentic Semantic Development Loop, accepted dogfood loop + autorun hygiene; manual GUI confirmation still required)`. ## Reporting Rule @@ -101,6 +102,7 @@ Use these labels when reporting progress: - `Прогресс модуля: 99% (Agentic Semantic Development Loop, accepted dogfood loop + autorun hygiene; manual GUI confirmation still required)` when discussing the current development-loop operating layer. - `Прогресс модуля: 100% (Open-World Route Candidate Promotion, declared phase90 slice accepted)` when discussing the route-candidate handoff slice itself. - `Прогресс модуля: 100% (Route-Candidate-Driven Enablement Loop, final reviewed proof-family route accepted; use as regression gate)` when discussing the current candidate-driven enablement loop. +- `Прогресс модуля: 12% (Open-World Schema/Primitive Discovery, active slice: financial counterparty flow hints)` when discussing the current broader schema/primitive discovery module. - `Open-World Business Overview implementation breadth: ~99%, Semantic Control Gate critical subset accepted, fat GUI pack still pending` when discussing only the already wired Slice 25 breadth. - `Прогресс модуля: X% (Open-World Bounded Autonomy Breadth, active slice: )` for later breadth work after the Semantic Control Gate is accepted. @@ -158,18 +160,19 @@ For current planning, read: 1. `README.md` 2. this document 3. `31 - inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md` -4. `30 - vendor_procurement_quality_reviewed_route_2026-05-12.md` -5. `29 - debt_due_date_aging_reviewed_route_2026-05-10.md` -6. `28 - accounting_profit_margin_reviewed_route_2026-05-10.md` -7. `27 - proof_family_enablement_candidates_2026-05-10.md` -8. `26 - route_candidate_driven_enablement_loop_2026-05-10.md` -9. `25 - open_world_route_candidate_promotion_2026-05-10.md` -10. `24 - agentic_semantic_development_loop_and_autorun_hygiene_2026-05-10.md` -11. `23 - current_execution_spine_and_semantic_control_gate_2026-05-05.md` -12. `22 - open_world_bounded_autonomy_breadth_2026-05-01.md` -13. `20 - planner_autonomy_consolidation_2026-05-01.md` -14. `19 - inventory_stock_open_world_breadth_proof_2026-05-01.md` -15. `17 - post_f_semantic_integrity_hardening_2026-04-23.md` -16. `16 - data_need_graph_and_open_world_mcp_plan_2026-04-22.md` +4. `32 - financial_counterparty_flow_hints_2026-05-13.md` +5. `30 - vendor_procurement_quality_reviewed_route_2026-05-12.md` +6. `29 - debt_due_date_aging_reviewed_route_2026-05-10.md` +7. `28 - accounting_profit_margin_reviewed_route_2026-05-10.md` +8. `27 - proof_family_enablement_candidates_2026-05-10.md` +9. `26 - route_candidate_driven_enablement_loop_2026-05-10.md` +10. `25 - open_world_route_candidate_promotion_2026-05-10.md` +11. `24 - agentic_semantic_development_loop_and_autorun_hygiene_2026-05-10.md` +12. `23 - current_execution_spine_and_semantic_control_gate_2026-05-05.md` +13. `22 - open_world_bounded_autonomy_breadth_2026-05-01.md` +14. `20 - planner_autonomy_consolidation_2026-05-01.md` +15. `19 - inventory_stock_open_world_breadth_proof_2026-05-01.md` +16. `17 - post_f_semantic_integrity_hardening_2026-04-23.md` +17. `16 - data_need_graph_and_open_world_mcp_plan_2026-04-22.md` Documents `01` through `15` remain valuable, but mostly as the historical architecture trail. diff --git a/docs/ARCH/11 - architecture_turnaround/32 - financial_counterparty_flow_hints_2026-05-13.md b/docs/ARCH/11 - architecture_turnaround/32 - financial_counterparty_flow_hints_2026-05-13.md new file mode 100644 index 0000000..a0327b5 --- /dev/null +++ b/docs/ARCH/11 - architecture_turnaround/32 - financial_counterparty_flow_hints_2026-05-13.md @@ -0,0 +1,53 @@ +# 32 - Financial Counterparty Flow Hints (2026-05-13) + +This note opens the next broader autonomy slice after the closed `Route-Candidate-Driven Enablement Loop`. + +The problem came from real business review: bank-like counterparties such as `СБЕРБАНК, ПАО` can dominate incoming or outgoing money-flow rankings. If the assistant treats that row as an ordinary customer or supplier, the business answer becomes misleading even when the amount itself is correct. + +## Scope + +The first cut adds reviewed document-field hints to bank-document money-flow routes: + +- `bank_operations_by_counterparty` now keeps outgoing/incoming bank-document columns aligned across `ОБЪЕДИНИТЬ ВСЕ` while exposing operation type, payment purpose where available, and comment; +- `customer_revenue_and_payments` now includes incoming bank document `ВидОперации` and `Комментарий`; +- `supplier_payouts_profile` now includes outgoing bank document `ВидОперации`, `НазначениеПлатежа`, and `Комментарий`; +- ranked value-flow buckets now carry `financial_flow_hint`; +- current hints are `loan_or_credit` (including deposits/credit-like bank instruments), `bank_fee_or_service`, `tax_or_budget`, `payroll_or_social`, `supplier_payment`, and `unclear`; +- business-overview evidence can now say that a bank-like leader is a bank fee/service, credit/loan, tax/budget, payroll/social, or ordinary supplier-payment signal instead of using name-only caution. + +## Validation + +Local checks: + +- `npm.cmd test -- assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts assistantMcpDiscoveryResponseCandidate.test.ts`: `97/97` passed, `1` skipped; +- `npm.cmd run build`: passed. + +Live MCP checks: + +- `bank_operations_by_counterparty` for `СБЕРБАНК` returned `fetched=593`, `matched=10`, `error=null`, proving the aligned bank-doc union executes with the enriched column set; +- `supplier_payouts_profile` for `СБЕРБАНК` returned `fetched=5`, `matched=5`, `error=null`; +- the live rows include `ВидОперации=Прочее списание` and `НазначениеПлатежа=Комиссия банка`; +- `customer_revenue_and_payments` for `СБЕРБАНК` returned `fetched=5`, `matched=5`, `error=null`; +- the live rows include incoming `ВидОперации`, including `Прочее поступление` and `Возврат от поставщика`. + +## Status + +Current module wording: + +`Open-World Schema/Primitive Discovery, active slice: financial counterparty flow hints` + +Progress: `12%`. + +This is not a full financial-purpose classifier yet. It is the first reviewed primitive that lets downstream business answers avoid the worst bank-as-supplier/customer overclaim. + +## Next Work + +1. Add a small live semantic replay for bank-like counterparty wording in business overview. +2. Extend hints where real 1C rows expose more payment-purpose families. +3. Use this as the first canary for broader schema/primitive discovery after phase96. + +See also: + +- [21 - current_status_canon_2026-05-01.md](./21%20-%20current_status_canon_2026-05-01.md) +- [26 - route_candidate_driven_enablement_loop_2026-05-10.md](./26%20-%20route_candidate_driven_enablement_loop_2026-05-10.md) +- [31 - inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md](./31%20-%20inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md) diff --git a/docs/ARCH/11 - architecture_turnaround/README.md b/docs/ARCH/11 - architecture_turnaround/README.md index 61a74e0..bc97f1e 100644 --- a/docs/ARCH/11 - architecture_turnaround/README.md +++ b/docs/ARCH/11 - architecture_turnaround/README.md @@ -49,6 +49,7 @@ This package answers the next question: 29. [29 - debt_due_date_aging_reviewed_route_2026-05-10.md](./29%20-%20debt_due_date_aging_reviewed_route_2026-05-10.md) 30. [30 - vendor_procurement_quality_reviewed_route_2026-05-12.md](./30%20-%20vendor_procurement_quality_reviewed_route_2026-05-12.md) 31. [31 - inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md](./31%20-%20inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md) +32. [32 - financial_counterparty_flow_hints_2026-05-13.md](./32%20-%20financial_counterparty_flow_hints_2026-05-13.md) ## Current Status Snapshot (2026-05-12) @@ -117,6 +118,7 @@ Status canon for planning: - The second reviewed proof-family route slice is [29 - debt_due_date_aging_reviewed_route_2026-05-10.md](./29%20-%20debt_due_date_aging_reviewed_route_2026-05-10.md). - The third reviewed proof-family route slice is [30 - vendor_procurement_quality_reviewed_route_2026-05-12.md](./30%20-%20vendor_procurement_quality_reviewed_route_2026-05-12.md). - The fourth/final reviewed proof-family route slice is [31 - inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md](./31%20-%20inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md). +- The current broader schema/primitive discovery slice is [32 - financial_counterparty_flow_hints_2026-05-13.md](./32%20-%20financial_counterparty_flow_hints_2026-05-13.md). It now documents a turnaround that is already operational in code, already materially past the acute regression breakpoint, and already moved through bounded MCP autonomy, Post-F hardening, inventory breadth proof, and the declared Planner Autonomy slice: @@ -187,6 +189,7 @@ Current honest status: - Planner Autonomy Consolidation progress: `100%` for the declared module, with catalog-fabric, value-flow arbitration, lifecycle bounded inference, broad-evaluation bridge, inventory catalog templates, inventory runtime-boundary honesty, exact inventory recipe bridging, unambiguous metadata-surface lane inference, catalog chain-template scoring, structured chain-match contract exposure, runtime/debug propagation, subject-aware bidirectional comparison arbitration, structured catalog-alignment verdicts, representative alignment regression guard, catalog-alignment reason-code telemetry, explicit `alignment_status` propagation, truth-harness/acceptance-matrix surfacing, soft divergence warning, `catalog_alignment_ok` acceptance invariant, step-level expected catalog-alignment assertions, phase66 and phase32 spec alignment expectations, AGENT source-catalog surfacing, generated phase83 mixed planner-brain replay spec, checked-source user-facing error sanitation, surface-grounded catalog promotion, and guarded live phase83 acceptance validated. Broader unfamiliar 1C asks are now next-module breadth work rather than an open blocker inside this declared slice - Open-World Route Candidate Promotion progress: `100%` for the declared phase90 slice, with structured `route_candidate` runtime contract, artifact propagation, live semantic replay accepted at `5/5`, and accepted AGENT autorun persistence; broader autonomous route enablement remains the next active slice - Route-Candidate-Driven Enablement Loop progress: `100%`, with deterministic repair-target grouping, Lead Codex handoff surfacing, local tooling tests, live phase91 canary acceptance, phase92 proof-family candidates accepted/saved as a user-runnable AGENT autorun, `accounting_profit_margin` promoted into reviewed 90/91/99 execution by phase93 live replay, `debt_due_date_aging_quality` promoted into reviewed payment-term/open-balance execution by phase94 live replay, `vendor_risk_procurement_quality` promoted into reviewed procurement-concentration evidence by phase95 live replay, and `inventory_reserve_liquidation_quality` promoted into reviewed inventory quality-event evidence by phase96 live replay; the declared route-candidate-driven enablement loop is now closed and should be used as a regression gate for the next broader autonomy slice +- Open-World Schema/Primitive Discovery progress: `12%`, active slice `financial counterparty flow hints`; bank-document money-flow recipes now expose operation/purpose/comment fields, ranked value-flow buckets carry `financial_flow_hint`, and Sberbank-like leaders can be treated as bank fee/service, credit/loan, tax/budget, payroll/social, supplier-payment, or unclear evidence instead of name-only supplier/customer overclaim. - graph snapshot after latest rebuild: see `graphify-out/GRAPH_REPORT.md` - current regression-gate breakpoint: - the validated hot paths are no longer structurally broken; @@ -357,6 +360,7 @@ Read in this order: 30. `29 - debt_due_date_aging_reviewed_route_2026-05-10.md` 31. `30 - vendor_procurement_quality_reviewed_route_2026-05-12.md` 32. `31 - inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md` +33. `32 - financial_counterparty_flow_hints_2026-05-13.md` ## Planning Rules diff --git a/llm_normalizer/backend/dist/services/addressRecipeCatalog.js b/llm_normalizer/backend/dist/services/addressRecipeCatalog.js index cfc5d77..fd4d839 100644 --- a/llm_normalizer/backend/dist/services/addressRecipeCatalog.js +++ b/llm_normalizer/backend/dist/services/addressRecipeCatalog.js @@ -346,7 +346,10 @@ const BANK_DOCS_QUERY_TEMPLATE = ` "" КАК СчетКт, БанкСписание.СуммаДокумента КАК Сумма, ПРЕДСТАВЛЕНИЕ(БанкСписание.Контрагент) КАК Контрагент, - ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор + ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор, + ПРЕДСТАВЛЕНИЕ(БанкСписание.ВидОперации) КАК ВидОперации, + БанкСписание.НазначениеПлатежа КАК НазначениеПлатежа, + БанкСписание.Комментарий КАК Комментарий ИЗ Документ.СписаниеСРасчетногоСчета КАК БанкСписание __WHERE_OUT__ @@ -358,7 +361,10 @@ __WHERE_OUT__ "" КАК СчетКт, БанкПоступление.СуммаДокумента КАК Сумма, ПРЕДСТАВЛЕНИЕ(БанкПоступление.Контрагент) КАК Контрагент, - ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор + ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор, + ПРЕДСТАВЛЕНИЕ(БанкПоступление.ВидОперации) КАК ВидОперации, + "" КАК НазначениеПлатежа, + БанкПоступление.Комментарий КАК Комментарий ИЗ Документ.ПоступлениеНаРасчетныйСчет КАК БанкПоступление __WHERE_IN__ @@ -624,7 +630,9 @@ const CUSTOMER_REVENUE_PROFILE_QUERY_TEMPLATE = ` "" КАК СчетКт, БанкПоступление.СуммаДокумента КАК Сумма, ПРЕДСТАВЛЕНИЕ(БанкПоступление.Контрагент) КАК Контрагент, - ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор + ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор, + ПРЕДСТАВЛЕНИЕ(БанкПоступление.ВидОперации) КАК ВидОперации, + БанкПоступление.Комментарий КАК Комментарий ИЗ Документ.ПоступлениеНаРасчетныйСчет КАК БанкПоступление __WHERE_IN__ @@ -639,7 +647,10 @@ const SUPPLIER_PAYOUT_PROFILE_QUERY_TEMPLATE = ` "" КАК СчетКт, БанкСписание.СуммаДокумента КАК Сумма, ПРЕДСТАВЛЕНИЕ(БанкСписание.Контрагент) КАК Контрагент, - ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор + ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор, + ПРЕДСТАВЛЕНИЕ(БанкСписание.ВидОперации) КАК ВидОперации, + БанкСписание.НазначениеПлатежа КАК НазначениеПлатежа, + БанкСписание.Комментарий КАК Комментарий ИЗ Документ.СписаниеСРасчетногоСчета КАК БанкСписание __WHERE_OUT__ diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js index 92d8fa5..e955ec8 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js @@ -595,6 +595,26 @@ function businessOverviewDebtDueDateAgingText(overview) { } return `Due-date aging на ${aging.as_of_date} проверен: строк с установленным сроком оплаты ${aging.rows_with_payment_terms}, подтвержденной просрочки не найдено; не просрочено по расчету ${aging.not_yet_due_amount_human_ru}.`; } +function financialFlowHintTextRuFromBucket(bucket) { + const rows = bucket?.financial_flow_hint_rows ?? 0; + const rowsText = rows > 0 ? ` (${rows} строк)` : ""; + if (bucket?.financial_flow_hint === "loan_or_credit") { + return ` По полям банковского документа доминирует кредитный/заемный признак${rowsText}; это не обычный поставщик и не клиентская выручка без отдельной проверки назначения.`; + } + if (bucket?.financial_flow_hint === "bank_fee_or_service") { + return ` По полям банковского документа доминирует признак банковской комиссии/услуг банка${rowsText}; это не обычный поставщик товаров/услуг без отдельной проверки договора.`; + } + if (bucket?.financial_flow_hint === "tax_or_budget") { + return ` По полям банковского документа доминирует налоговый/бюджетный признак${rowsText}.`; + } + if (bucket?.financial_flow_hint === "payroll_or_social") { + return ` По полям банковского документа доминирует зарплатный/социальный признак${rowsText}.`; + } + if (bucket?.financial_flow_hint === "supplier_payment") { + return ` По полям банковского документа доминирует признак оплаты поставщику${rowsText}; если получатель по названию является банком, это все равно требует осторожной трактовки.`; + } + return ""; +} function businessOverviewVendorProcurementQualityText(overview) { const quality = overview.vendor_procurement_quality; if (!quality) { @@ -617,7 +637,7 @@ function businessOverviewVendorProcurementQualityText(overview) { ? ` Договорный профиль: используется ${quality.used_contracts} договоров.` : ` Договорный профиль: используется ${quality.used_contracts}/${quality.total_contracts} договоров${quality.used_contract_share_pct === null ? "" : ` (${quality.used_contract_share_pct}%)`}.`; if (quality.evidence_status === "financial_institution_leads_outgoing_cash") { - return `Проверенный procurement-concentration route за ${period}: крупнейший получатель исходящих денег ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}. По названию это банк/финансовая организация, поэтому зависимость от обычного поставщика этим не подтверждается.${nonFinancialText}${contractText} Надежность поставщиков, качество поставок, назначение каждого платежа и полная структура расходов этим маршрутом не доказаны.`; + return `Проверенный procurement-concentration route за ${period}: крупнейший получатель исходящих денег ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}. По названию это банк/финансовая организация, поэтому зависимость от обычного поставщика этим не подтверждается.${financialFlowHintTextRuFromBucket(top)}${nonFinancialText}${contractText} Надежность поставщиков, качество поставок, назначение каждого платежа и полная структура расходов этим маршрутом не доказаны.`; } if (quality.evidence_status === "reviewed_procurement_concentration") { return `Проверенный procurement-concentration route за ${period}: крупнейший поставщик/получатель исходящих платежей ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}.${contractText} Это проверенный сигнал концентрации закупок/исходящих платежей, но не аудит надежности поставщика, качества поставок и полной структуры расходов.`; diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js index 160f85f..5860977 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js @@ -1611,6 +1611,80 @@ function rowCounterpartyValue(row) { } return rowAnalyticsTextValues(row).find(isLikelyCounterpartyToken) ?? null; } +const FINANCIAL_FLOW_HINTS = [ + "loan_or_credit", + "bank_fee_or_service", + "tax_or_budget", + "payroll_or_social", + "supplier_payment", + "unclear" +]; +function normalizeFinancialFlowText(value) { + return String(value ?? "") + .toLowerCase() + .replace(/ё/g, "е") + .replace(/\s+/g, " ") + .trim(); +} +function rowFinancialFlowCorpus(row) { + return [ + rowTextValue(row, ["ВидОперации", "OperationType", "operation_type"]), + rowTextValue(row, ["НазначениеПлатежа", "PaymentPurpose", "payment_purpose"]), + rowTextValue(row, ["Комментарий", "Comment", "comment"]), + rowContractValue(row), + rowDocumentValue(row) + ] + .map(normalizeFinancialFlowText) + .filter(Boolean) + .join(" "); +} +function classifyRowFinancialFlowHint(row) { + const corpus = rowFinancialFlowCorpus(row); + if (!corpus) { + return "unclear"; + } + if (/(?:кредит|кредитн|заем|займ|овердрафт|процент|ссуд|депозит|loan|credit|overdraft|deposit)/iu.test(corpus)) { + return "loan_or_credit"; + } + if (/(?:комисс|эквайр|расчетн[оа -]*кассов|ведение\s+счет|банк[а-я\s-]*услуг|банковск[а-я\s-]*услуг|рко|bank\s*fee|acquiring|bank\s*service)/iu.test(corpus)) { + return "bank_fee_or_service"; + } + if (/(?:налог|взнос|фсс|фомс|пфр|бюджет|казнач|пен[ия]|штраф|tax|budget|treasury)/iu.test(corpus)) { + return "tax_or_budget"; + } + if (/(?:зарплат|заработн|перечислениезаработнойплаты|salary|payroll|wage)/iu.test(corpus)) { + return "payroll_or_social"; + } + if (/(?:оплатапоставщику|оплата\s+поставщ|поставщ|supplier|vendor)/iu.test(corpus)) { + return "supplier_payment"; + } + return "unclear"; +} +function createFinancialFlowHintCounts() { + return { + loan_or_credit: 0, + bank_fee_or_service: 0, + tax_or_budget: 0, + payroll_or_social: 0, + supplier_payment: 0, + unclear: 0 + }; +} +function dominantFinancialFlowHint(counts) { + let bestHint = null; + let bestRows = 0; + for (const hint of FINANCIAL_FLOW_HINTS) { + const rows = counts[hint] ?? 0; + if (rows > bestRows) { + bestHint = hint; + bestRows = rows; + } + } + if (!bestHint || bestRows <= 0 || bestHint === "unclear") { + return { hint: null, rows: 0 }; + } + return { hint: bestHint, rows: bestRows }; +} function normalizeDateParts(yearText, monthText, dayText) { const year = Number(yearText); const month = Number(monthText); @@ -2119,22 +2193,32 @@ function deriveRankedValueFlow(result, input) { continue; } rowsWithAmount += 1; - const current = buckets.get(axisValue) ?? { rows_with_amount: 0, total_amount: 0 }; + const current = buckets.get(axisValue) ?? { + rows_with_amount: 0, + total_amount: 0, + financial_flow_hint_counts: createFinancialFlowHintCounts() + }; current.rows_with_amount += 1; current.total_amount += amount; + current.financial_flow_hint_counts[classifyRowFinancialFlowHint(row)] += 1; buckets.set(axisValue, current); } if (rowsWithAmount <= 0 || buckets.size <= 0) { return null; } const rankedValues = Array.from(buckets.entries()) - .map(([axisValue, bucket]) => ({ - axis_value: axisValue, - rows_with_amount: bucket.rows_with_amount, - total_amount: bucket.total_amount, - total_amount_human_ru: formatAmountHumanRu(bucket.total_amount), - counterparty_role_hint: (0, counterpartyRoleHeuristics_1.counterpartyRoleHintForName)(axisValue) - })) + .map(([axisValue, bucket]) => { + const dominantHint = dominantFinancialFlowHint(bucket.financial_flow_hint_counts); + return { + axis_value: axisValue, + rows_with_amount: bucket.rows_with_amount, + total_amount: bucket.total_amount, + total_amount_human_ru: formatAmountHumanRu(bucket.total_amount), + counterparty_role_hint: (0, counterpartyRoleHeuristics_1.counterpartyRoleHintForName)(axisValue), + financial_flow_hint: dominantHint.hint ?? undefined, + financial_flow_hint_rows: dominantHint.hint ? dominantHint.rows : undefined + }; + }) .sort((left, right) => { const amountDelta = right.total_amount - left.total_amount; if (input.rankingNeed === "bottom_asc") { @@ -3430,6 +3514,26 @@ function summarizeBusinessOverviewRows(input) { } return parts.length > 0 ? parts.join("; ") : null; } +function financialFlowHintTextRu(bucket) { + const rows = bucket?.financial_flow_hint_rows ?? 0; + const rowsText = rows > 0 ? ` (${rows} строк)` : ""; + if (bucket?.financial_flow_hint === "loan_or_credit") { + return ` По полям банковского документа доминирует кредитный/заемный признак${rowsText}.`; + } + if (bucket?.financial_flow_hint === "bank_fee_or_service") { + return ` По полям банковского документа доминирует признак банковской комиссии/услуг банка${rowsText}.`; + } + if (bucket?.financial_flow_hint === "tax_or_budget") { + return ` По полям банковского документа доминирует налоговый/бюджетный признак${rowsText}.`; + } + if (bucket?.financial_flow_hint === "payroll_or_social") { + return ` По полям банковского документа доминирует зарплатный/социальный признак${rowsText}.`; + } + if (bucket?.financial_flow_hint === "supplier_payment") { + return ` По полям банковского документа доминирует признак оплаты поставщику${rowsText}; все равно нужна осторожность, если контрагент по названию является банком.`; + } + return ""; +} function buildBusinessOverviewConfirmedFacts(derived) { if (!derived) { return []; @@ -3447,6 +3551,10 @@ function buildBusinessOverviewConfirmedFacts(derived) { const leader = derived.top_customers[0]; if ((0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(leader.axis_value)) { facts.push(`Крупнейший входящий денежный источник в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}. По названию это банк/финансовая организация, поэтому без назначения платежа это не доказанный клиент или бизнес-выручка.`); + const financialHintText = financialFlowHintTextRu(leader); + if (financialHintText) { + facts.push(financialHintText.trim()); + } const nonFinancialLeader = derived.top_customers.slice(1).find((item) => !(0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(item.axis_value)); if (nonFinancialLeader) { facts.push(`Крупнейший небанковский входящий контрагент в проверенном срезе: ${nonFinancialLeader.axis_value} — ${nonFinancialLeader.total_amount_human_ru}.`); @@ -3460,6 +3568,10 @@ function buildBusinessOverviewConfirmedFacts(derived) { const leader = derived.top_suppliers[0]; if ((0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(leader.axis_value)) { facts.push(`Крупнейший получатель исходящих денег в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}. По названию это банк/финансовая организация, поэтому без назначения платежа/договора это не доказанный обычный поставщик.`); + const financialHintText = financialFlowHintTextRu(leader); + if (financialHintText) { + facts.push(financialHintText.trim()); + } const nonFinancialLeader = derived.top_suppliers.slice(1).find((item) => !(0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(item.axis_value)); if (nonFinancialLeader) { facts.push(`Крупнейший небанковский получатель исходящих денег в проверенном срезе: ${nonFinancialLeader.axis_value} — ${nonFinancialLeader.total_amount_human_ru}.`); diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponseCandidate.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponseCandidate.js index 0e319a1..61e086a 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponseCandidate.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponseCandidate.js @@ -415,6 +415,28 @@ function overviewAxisLooksFinancial(row) { return (row.counterparty_role_hint === "bank_or_financial_institution" || (0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(row.axis_value)); } +function financialFlowHintTextRuFromRecord(row) { + const hint = toNonEmptyString(row?.financial_flow_hint); + const rows = typeof row?.financial_flow_hint_rows === "number" && Number.isFinite(row.financial_flow_hint_rows) + ? ` (${row.financial_flow_hint_rows} строк)` + : ""; + if (hint === "loan_or_credit") { + return `По полям банковского документа доминирует кредитный/заемный признак${rows}; это не обычная поставка и не клиентская выручка без отдельной проверки назначения.`; + } + if (hint === "bank_fee_or_service") { + return `По полям банковского документа доминирует признак банковской комиссии/услуг банка${rows}; это не обычный поставщик товаров/услуг без отдельной проверки договора.`; + } + if (hint === "tax_or_budget") { + return `По полям банковского документа доминирует налоговый/бюджетный признак${rows}; это не поставщик и не клиентская выручка.`; + } + if (hint === "payroll_or_social") { + return `По полям банковского документа доминирует зарплатный/социальный признак${rows}; это не поставщик и не клиентская выручка.`; + } + if (hint === "supplier_payment") { + return `По полям банковского документа доминирует признак оплаты поставщику${rows}; если получатель по названию является банком, это все равно требует осторожной трактовки.`; + } + return null; +} function businessOverviewTaxLine(overview) { const tax = toRecordObject(overview.tax_position); if (!tax) { @@ -795,6 +817,10 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) { const totalText = totalOutgoing ? `; всего исходящих платежей в проверенном срезе ${totalOutgoing}` : ""; if (status === "financial_institution_leads_outgoing_cash") { lines.push(`Коротко: проверенный procurement-concentration route за ${periodScope} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.`); + const financialHintText = financialFlowHintTextRuFromRecord(topOutgoingRecord); + if (financialHintText) { + lines.push(financialHintText); + } if (nonFinancialName) { lines.push(`Крупнейший небанковский получатель исходящих денег: ${nonFinancialName}${nonFinancialShare ? `, около ${nonFinancialShare}` : ""}${nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}. Это уже сигнал закупочной/исходящей концентрации, но не аудит надежности поставщика.`); } diff --git a/llm_normalizer/backend/src/services/addressRecipeCatalog.ts b/llm_normalizer/backend/src/services/addressRecipeCatalog.ts index 050430f..30a1aed 100644 --- a/llm_normalizer/backend/src/services/addressRecipeCatalog.ts +++ b/llm_normalizer/backend/src/services/addressRecipeCatalog.ts @@ -360,7 +360,10 @@ const BANK_DOCS_QUERY_TEMPLATE = ` "" КАК СчетКт, БанкСписание.СуммаДокумента КАК Сумма, ПРЕДСТАВЛЕНИЕ(БанкСписание.Контрагент) КАК Контрагент, - ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор + ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор, + ПРЕДСТАВЛЕНИЕ(БанкСписание.ВидОперации) КАК ВидОперации, + БанкСписание.НазначениеПлатежа КАК НазначениеПлатежа, + БанкСписание.Комментарий КАК Комментарий ИЗ Документ.СписаниеСРасчетногоСчета КАК БанкСписание __WHERE_OUT__ @@ -372,7 +375,10 @@ __WHERE_OUT__ "" КАК СчетКт, БанкПоступление.СуммаДокумента КАК Сумма, ПРЕДСТАВЛЕНИЕ(БанкПоступление.Контрагент) КАК Контрагент, - ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор + ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор, + ПРЕДСТАВЛЕНИЕ(БанкПоступление.ВидОперации) КАК ВидОперации, + "" КАК НазначениеПлатежа, + БанкПоступление.Комментарий КАК Комментарий ИЗ Документ.ПоступлениеНаРасчетныйСчет КАК БанкПоступление __WHERE_IN__ @@ -644,7 +650,9 @@ const CUSTOMER_REVENUE_PROFILE_QUERY_TEMPLATE = ` "" КАК СчетКт, БанкПоступление.СуммаДокумента КАК Сумма, ПРЕДСТАВЛЕНИЕ(БанкПоступление.Контрагент) КАК Контрагент, - ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор + ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор, + ПРЕДСТАВЛЕНИЕ(БанкПоступление.ВидОперации) КАК ВидОперации, + БанкПоступление.Комментарий КАК Комментарий ИЗ Документ.ПоступлениеНаРасчетныйСчет КАК БанкПоступление __WHERE_IN__ @@ -660,7 +668,10 @@ const SUPPLIER_PAYOUT_PROFILE_QUERY_TEMPLATE = ` "" КАК СчетКт, БанкСписание.СуммаДокумента КАК Сумма, ПРЕДСТАВЛЕНИЕ(БанкСписание.Контрагент) КАК Контрагент, - ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор + ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор, + ПРЕДСТАВЛЕНИЕ(БанкСписание.ВидОперации) КАК ВидОперации, + БанкСписание.НазначениеПлатежа КАК НазначениеПлатежа, + БанкСписание.Комментарий КАК Комментарий ИЗ Документ.СписаниеСРасчетногоСчета КАК БанкСписание __WHERE_OUT__ diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts index aa58759..0e66e83 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts @@ -736,6 +736,27 @@ function businessOverviewDebtDueDateAgingText(overview: BusinessOverview): strin return `Due-date aging на ${aging.as_of_date} проверен: строк с установленным сроком оплаты ${aging.rows_with_payment_terms}, подтвержденной просрочки не найдено; не просрочено по расчету ${aging.not_yet_due_amount_human_ru}.`; } +function financialFlowHintTextRuFromBucket(bucket: BusinessOverviewRankedBucket | null | undefined): string { + const rows = bucket?.financial_flow_hint_rows ?? 0; + const rowsText = rows > 0 ? ` (${rows} строк)` : ""; + if (bucket?.financial_flow_hint === "loan_or_credit") { + return ` По полям банковского документа доминирует кредитный/заемный признак${rowsText}; это не обычный поставщик и не клиентская выручка без отдельной проверки назначения.`; + } + if (bucket?.financial_flow_hint === "bank_fee_or_service") { + return ` По полям банковского документа доминирует признак банковской комиссии/услуг банка${rowsText}; это не обычный поставщик товаров/услуг без отдельной проверки договора.`; + } + if (bucket?.financial_flow_hint === "tax_or_budget") { + return ` По полям банковского документа доминирует налоговый/бюджетный признак${rowsText}.`; + } + if (bucket?.financial_flow_hint === "payroll_or_social") { + return ` По полям банковского документа доминирует зарплатный/социальный признак${rowsText}.`; + } + if (bucket?.financial_flow_hint === "supplier_payment") { + return ` По полям банковского документа доминирует признак оплаты поставщику${rowsText}; если получатель по названию является банком, это все равно требует осторожной трактовки.`; + } + return ""; +} + function businessOverviewVendorProcurementQualityText(overview: BusinessOverview): string | null { const quality = overview.vendor_procurement_quality; if (!quality) { @@ -759,7 +780,7 @@ function businessOverviewVendorProcurementQualityText(overview: BusinessOverview ? ` Договорный профиль: используется ${quality.used_contracts} договоров.` : ` Договорный профиль: используется ${quality.used_contracts}/${quality.total_contracts} договоров${quality.used_contract_share_pct === null ? "" : ` (${quality.used_contract_share_pct}%)`}.`; if (quality.evidence_status === "financial_institution_leads_outgoing_cash") { - return `Проверенный procurement-concentration route за ${period}: крупнейший получатель исходящих денег ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}. По названию это банк/финансовая организация, поэтому зависимость от обычного поставщика этим не подтверждается.${nonFinancialText}${contractText} Надежность поставщиков, качество поставок, назначение каждого платежа и полная структура расходов этим маршрутом не доказаны.`; + return `Проверенный procurement-concentration route за ${period}: крупнейший получатель исходящих денег ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}. По названию это банк/финансовая организация, поэтому зависимость от обычного поставщика этим не подтверждается.${financialFlowHintTextRuFromBucket(top)}${nonFinancialText}${contractText} Надежность поставщиков, качество поставок, назначение каждого платежа и полная структура расходов этим маршрутом не доказаны.`; } if (quality.evidence_status === "reviewed_procurement_concentration") { return `Проверенный procurement-concentration route за ${period}: крупнейший поставщик/получатель исходящих платежей ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}.${contractText} Это проверенный сигнал концентрации закупок/исходящих платежей, но не аудит надежности поставщика, качества поставок и полной структуры расходов.`; diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts index fbb6e06..453f4d6 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts @@ -65,6 +65,14 @@ export interface AssistantMcpDiscoveryValueFlowMonthBucket { total_amount_human_ru: string; } +export type AssistantMcpDiscoveryFinancialFlowHint = + | "loan_or_credit" + | "bank_fee_or_service" + | "tax_or_budget" + | "payroll_or_social" + | "supplier_payment" + | "unclear"; + export interface AssistantMcpDiscoveryDerivedValueFlow { value_flow_direction: "incoming_customer_revenue" | "outgoing_supplier_payout"; counterparty: string | null; @@ -89,6 +97,8 @@ export interface AssistantMcpDiscoveryRankedValueFlowBucket { total_amount: number; total_amount_human_ru: string; counterparty_role_hint?: CounterpartyRoleHint; + financial_flow_hint?: AssistantMcpDiscoveryFinancialFlowHint; + financial_flow_hint_rows?: number; } export interface AssistantMcpDiscoveryDerivedRankedValueFlow { @@ -2526,6 +2536,88 @@ function rowCounterpartyValue(row: Record): string | null { return rowAnalyticsTextValues(row).find(isLikelyCounterpartyToken) ?? null; } +const FINANCIAL_FLOW_HINTS: AssistantMcpDiscoveryFinancialFlowHint[] = [ + "loan_or_credit", + "bank_fee_or_service", + "tax_or_budget", + "payroll_or_social", + "supplier_payment", + "unclear" +]; + +function normalizeFinancialFlowText(value: unknown): string { + return String(value ?? "") + .toLowerCase() + .replace(/ё/g, "е") + .replace(/\s+/g, " ") + .trim(); +} + +function rowFinancialFlowCorpus(row: Record): string { + return [ + rowTextValue(row, ["ВидОперации", "OperationType", "operation_type"]), + rowTextValue(row, ["НазначениеПлатежа", "PaymentPurpose", "payment_purpose"]), + rowTextValue(row, ["Комментарий", "Comment", "comment"]), + rowContractValue(row), + rowDocumentValue(row) + ] + .map(normalizeFinancialFlowText) + .filter(Boolean) + .join(" "); +} + +function classifyRowFinancialFlowHint(row: Record): AssistantMcpDiscoveryFinancialFlowHint { + const corpus = rowFinancialFlowCorpus(row); + if (!corpus) { + return "unclear"; + } + if (/(?:кредит|кредитн|заем|займ|овердрафт|процент|ссуд|депозит|loan|credit|overdraft|deposit)/iu.test(corpus)) { + return "loan_or_credit"; + } + if (/(?:комисс|эквайр|расчетн[оа -]*кассов|ведение\s+счет|банк[а-я\s-]*услуг|банковск[а-я\s-]*услуг|рко|bank\s*fee|acquiring|bank\s*service)/iu.test(corpus)) { + return "bank_fee_or_service"; + } + if (/(?:налог|взнос|фсс|фомс|пфр|бюджет|казнач|пен[ия]|штраф|tax|budget|treasury)/iu.test(corpus)) { + return "tax_or_budget"; + } + if (/(?:зарплат|заработн|перечислениезаработнойплаты|salary|payroll|wage)/iu.test(corpus)) { + return "payroll_or_social"; + } + if (/(?:оплатапоставщику|оплата\s+поставщ|поставщ|supplier|vendor)/iu.test(corpus)) { + return "supplier_payment"; + } + return "unclear"; +} + +function createFinancialFlowHintCounts(): Record { + return { + loan_or_credit: 0, + bank_fee_or_service: 0, + tax_or_budget: 0, + payroll_or_social: 0, + supplier_payment: 0, + unclear: 0 + }; +} + +function dominantFinancialFlowHint( + counts: Record +): { hint: AssistantMcpDiscoveryFinancialFlowHint | null; rows: number } { + let bestHint: AssistantMcpDiscoveryFinancialFlowHint | null = null; + let bestRows = 0; + for (const hint of FINANCIAL_FLOW_HINTS) { + const rows = counts[hint] ?? 0; + if (rows > bestRows) { + bestHint = hint; + bestRows = rows; + } + } + if (!bestHint || bestRows <= 0 || bestHint === "unclear") { + return { hint: null, rows: 0 }; + } + return { hint: bestHint, rows: bestRows }; +} + function normalizeDateParts(yearText: string, monthText: string, dayText: string): string | null { const year = Number(yearText); const month = Number(monthText); @@ -3104,7 +3196,14 @@ function deriveRankedValueFlow( return null; } - const buckets = new Map(); + const buckets = new Map< + string, + { + rows_with_amount: number; + total_amount: number; + financial_flow_hint_counts: Record; + } + >(); let rowsWithAmount = 0; for (const row of result.rows) { const axisValue = rowCounterpartyValue(row); @@ -3113,9 +3212,14 @@ function deriveRankedValueFlow( continue; } rowsWithAmount += 1; - const current = buckets.get(axisValue) ?? { rows_with_amount: 0, total_amount: 0 }; + const current = buckets.get(axisValue) ?? { + rows_with_amount: 0, + total_amount: 0, + financial_flow_hint_counts: createFinancialFlowHintCounts() + }; current.rows_with_amount += 1; current.total_amount += amount; + current.financial_flow_hint_counts[classifyRowFinancialFlowHint(row)] += 1; buckets.set(axisValue, current); } @@ -3124,13 +3228,18 @@ function deriveRankedValueFlow( } const rankedValues = Array.from(buckets.entries()) - .map(([axisValue, bucket]) => ({ - axis_value: axisValue, - rows_with_amount: bucket.rows_with_amount, - total_amount: bucket.total_amount, - total_amount_human_ru: formatAmountHumanRu(bucket.total_amount), - counterparty_role_hint: counterpartyRoleHintForName(axisValue) - })) + .map(([axisValue, bucket]) => { + const dominantHint = dominantFinancialFlowHint(bucket.financial_flow_hint_counts); + return { + axis_value: axisValue, + rows_with_amount: bucket.rows_with_amount, + total_amount: bucket.total_amount, + total_amount_human_ru: formatAmountHumanRu(bucket.total_amount), + counterparty_role_hint: counterpartyRoleHintForName(axisValue), + financial_flow_hint: dominantHint.hint ?? undefined, + financial_flow_hint_rows: dominantHint.hint ? dominantHint.rows : undefined + }; + }) .sort((left, right) => { const amountDelta = right.total_amount - left.total_amount; if (input.rankingNeed === "bottom_asc") { @@ -4683,6 +4792,27 @@ function summarizeBusinessOverviewRows(input: { return parts.length > 0 ? parts.join("; ") : null; } +function financialFlowHintTextRu(bucket: AssistantMcpDiscoveryRankedValueFlowBucket | null | undefined): string { + const rows = bucket?.financial_flow_hint_rows ?? 0; + const rowsText = rows > 0 ? ` (${rows} строк)` : ""; + if (bucket?.financial_flow_hint === "loan_or_credit") { + return ` По полям банковского документа доминирует кредитный/заемный признак${rowsText}.`; + } + if (bucket?.financial_flow_hint === "bank_fee_or_service") { + return ` По полям банковского документа доминирует признак банковской комиссии/услуг банка${rowsText}.`; + } + if (bucket?.financial_flow_hint === "tax_or_budget") { + return ` По полям банковского документа доминирует налоговый/бюджетный признак${rowsText}.`; + } + if (bucket?.financial_flow_hint === "payroll_or_social") { + return ` По полям банковского документа доминирует зарплатный/социальный признак${rowsText}.`; + } + if (bucket?.financial_flow_hint === "supplier_payment") { + return ` По полям банковского документа доминирует признак оплаты поставщику${rowsText}; все равно нужна осторожность, если контрагент по названию является банком.`; + } + return ""; +} + function buildBusinessOverviewConfirmedFacts(derived: AssistantMcpDiscoveryDerivedBusinessOverview | null): string[] { if (!derived) { return []; @@ -4706,6 +4836,10 @@ function buildBusinessOverviewConfirmedFacts(derived: AssistantMcpDiscoveryDeriv facts.push( `Крупнейший входящий денежный источник в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}. По названию это банк/финансовая организация, поэтому без назначения платежа это не доказанный клиент или бизнес-выручка.` ); + const financialHintText = financialFlowHintTextRu(leader); + if (financialHintText) { + facts.push(financialHintText.trim()); + } const nonFinancialLeader = derived.top_customers.slice(1).find((item) => !isLikelyFinancialInstitutionCounterparty(item.axis_value)); if (nonFinancialLeader) { facts.push( @@ -4724,6 +4858,10 @@ function buildBusinessOverviewConfirmedFacts(derived: AssistantMcpDiscoveryDeriv facts.push( `Крупнейший получатель исходящих денег в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}. По названию это банк/финансовая организация, поэтому без назначения платежа/договора это не доказанный обычный поставщик.` ); + const financialHintText = financialFlowHintTextRu(leader); + if (financialHintText) { + facts.push(financialHintText.trim()); + } const nonFinancialLeader = derived.top_suppliers.slice(1).find((item) => !isLikelyFinancialInstitutionCounterparty(item.axis_value)); if (nonFinancialLeader) { facts.push( diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts index 8f07ede..810b463 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts @@ -486,6 +486,29 @@ function overviewAxisLooksFinancial(row: Record | null): boolea ); } +function financialFlowHintTextRuFromRecord(row: Record | null): string | null { + const hint = toNonEmptyString(row?.financial_flow_hint); + const rows = typeof row?.financial_flow_hint_rows === "number" && Number.isFinite(row.financial_flow_hint_rows) + ? ` (${row.financial_flow_hint_rows} строк)` + : ""; + if (hint === "loan_or_credit") { + return `По полям банковского документа доминирует кредитный/заемный признак${rows}; это не обычная поставка и не клиентская выручка без отдельной проверки назначения.`; + } + if (hint === "bank_fee_or_service") { + return `По полям банковского документа доминирует признак банковской комиссии/услуг банка${rows}; это не обычный поставщик товаров/услуг без отдельной проверки договора.`; + } + if (hint === "tax_or_budget") { + return `По полям банковского документа доминирует налоговый/бюджетный признак${rows}; это не поставщик и не клиентская выручка.`; + } + if (hint === "payroll_or_social") { + return `По полям банковского документа доминирует зарплатный/социальный признак${rows}; это не поставщик и не клиентская выручка.`; + } + if (hint === "supplier_payment") { + return `По полям банковского документа доминирует признак оплаты поставщику${rows}; если получатель по названию является банком, это все равно требует осторожной трактовки.`; + } + return null; +} + function businessOverviewTaxLine(overview: Record): string | null { const tax = toRecordObject(overview.tax_position); if (!tax) { @@ -948,6 +971,10 @@ function buildCompactBusinessOverviewReply( lines.push( `Коротко: проверенный procurement-concentration route за ${periodScope} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.` ); + const financialHintText = financialFlowHintTextRuFromRecord(topOutgoingRecord); + if (financialHintText) { + lines.push(financialHintText); + } if (nonFinancialName) { lines.push( `Крупнейший небанковский получатель исходящих денег: ${nonFinancialName}${nonFinancialShare ? `, около ${nonFinancialShare}` : ""}${nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}. Это уже сигнал закупочной/исходящей концентрации, но не аудит надежности поставщика.` diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts index f020216..69169f1 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts @@ -345,13 +345,13 @@ describe("assistant MCP discovery pilot executor", () => { const deps = buildSequentialDeps([ { rows: [ - { Period: "2020-01-15T00:00:00", Amount: 1200000, Counterparty: "СБЕРБАНК, ПАО" }, + { Period: "2020-01-15T00:00:00", Amount: 1200000, Counterparty: "СБЕРБАНК, ПАО", ВидОперации: "Возврат от поставщика", Договор: "Депозитный договор" }, { Period: "2020-02-15T00:00:00", Amount: 800000, Counterparty: "Группа СВК" } ] }, { rows: [ - { Period: "2020-01-20T00:00:00", Amount: 650000, Counterparty: "СБЕРБАНК, ПАО" }, + { Period: "2020-01-20T00:00:00", Amount: 650000, Counterparty: "СБЕРБАНК, ПАО", ВидОперации: "ПрочееСписание", НазначениеПлатежа: "Комиссия банка за ведение счета" }, { Period: "2020-03-20T00:00:00", Amount: 50000, Counterparty: "ООО Поставщик" } ] }, @@ -365,7 +365,8 @@ describe("assistant MCP discovery pilot executor", () => { expect(result.derived_business_overview?.top_customers[0]).toMatchObject({ axis_value: "СБЕРБАНК, ПАО", - counterparty_role_hint: "bank_or_financial_institution" + counterparty_role_hint: "bank_or_financial_institution", + financial_flow_hint: "loan_or_credit" }); expect(result.derived_business_overview?.top_customers[1]).toMatchObject({ axis_value: "Группа СВК", @@ -373,13 +374,16 @@ describe("assistant MCP discovery pilot executor", () => { }); expect(result.derived_business_overview?.top_suppliers[0]).toMatchObject({ axis_value: "СБЕРБАНК, ПАО", - counterparty_role_hint: "bank_or_financial_institution" + counterparty_role_hint: "bank_or_financial_institution", + financial_flow_hint: "bank_fee_or_service" }); const confirmedFacts = result.evidence.confirmed_facts.join("\n"); const inferredFacts = result.evidence.inferred_facts.join("\n"); expect(confirmedFacts).toContain("Крупнейший входящий денежный источник"); expect(confirmedFacts).toContain("Крупнейший небанковский входящий контрагент"); expect(confirmedFacts).toContain("Крупнейший получатель исходящих денег"); + expect(confirmedFacts).toContain("кредитный/заемный признак"); + expect(confirmedFacts).toContain("банковской комиссии/услуг банка"); expect(confirmedFacts).not.toContain("Самый крупный подтвержденный клиент в проверенном срезе: СБЕРБАНК"); expect(confirmedFacts).not.toContain("Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: СБЕРБАНК"); expect(inferredFacts).toContain("outgoing cash concentration proxy");