Compare commits
18 Commits
9c86407937
...
f7314b0212
| Author | SHA1 | Date |
|---|---|---|
|
|
f7314b0212 | |
|
|
91a9dfa3a7 | |
|
|
5f7e201190 | |
|
|
a15f24f21d | |
|
|
473cdc3a9b | |
|
|
b18613961c | |
|
|
e38223e33e | |
|
|
50d938b8f1 | |
|
|
e7603a9d29 | |
|
|
5125c747d8 | |
|
|
d64d7f84cc | |
|
|
f34ae5d6df | |
|
|
0bd631c160 | |
|
|
09c6d1aa0e | |
|
|
f5d86d4bc1 | |
|
|
1a94e72381 | |
|
|
151f2a26de | |
|
|
bbc257fd6c |
|
|
@ -30,3 +30,7 @@ graphify-out/
|
||||||
artifacts/domain_runs/*
|
artifacts/domain_runs/*
|
||||||
!artifacts/domain_runs/.gitkeep
|
!artifacts/domain_runs/.gitkeep
|
||||||
artifacts/runtime_logs/*.log
|
artifacts/runtime_logs/*.log
|
||||||
|
artifacts/runtime/
|
||||||
|
|
||||||
|
# generated assistant-stage runtime eval jobs
|
||||||
|
llm_normalizer/data/eval_cases/assistant_saved_session_runtime_job-*.json
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,27 @@ Fresh validation cut:
|
||||||
- `npm.cmd run build` passed;
|
- `npm.cmd run build` passed;
|
||||||
- graphify rebuilt to `6371` nodes, `14048` edges, `141` communities.
|
- graphify rebuilt to `6371` nodes, `14048` edges, `141` communities.
|
||||||
|
|
||||||
|
## 2026-05-18 Overlay - Context Entry And Latest Semantic Integrity Closure
|
||||||
|
|
||||||
|
The current short handoff document is now `41 - assistant_context_entry_2026-05-18.md`.
|
||||||
|
|
||||||
|
The latest saved-session semantic replay closure is:
|
||||||
|
|
||||||
|
- source saved session: `gen-mo1t93wq-jy0453e`;
|
||||||
|
- final replay artifact: `artifacts/domain_runs/saved_session_gen_mo1t93wq_jy0453e_rerun_final_semantic_20260518`;
|
||||||
|
- result: `accepted`, `31/31 passed`, `0 failed`, `execution_status=exact`;
|
||||||
|
- commit: `9c86407 Зафиксировать семантическую целостность VAT, debt mirror и trace-ответов`;
|
||||||
|
- graphify after the code cut: `6490 nodes`, `14412 edges`, `141 communities`.
|
||||||
|
|
||||||
|
This May-18 closure does not reopen Post-F. It reinforces Post-F as a regression gate and records four extra live-session seams:
|
||||||
|
|
||||||
|
- same-period VAT follow-up now preserves the prior requested tax period instead of drifting to current date;
|
||||||
|
- stale MCP discovery counterparty no longer contaminates short debt mirror follow-ups such as `а нам?`;
|
||||||
|
- inventory sale trace answers now distinguish sale trace by nomenclature from exact selected lot/batch proof;
|
||||||
|
- broad best-year answers no longer rank unreliable yearly operating net when one direction is row-limit constrained.
|
||||||
|
|
||||||
|
Use this overlay when starting a new chat or preparing a Tasker handoff card.
|
||||||
|
|
||||||
## Current Module Map
|
## Current Module Map
|
||||||
|
|
||||||
- `Post-F Semantic Integrity Hardening`: `99%`, operationally closed as a hardening slice and now used as a regression gate.
|
- `Post-F Semantic Integrity Hardening`: `99%`, operationally closed as a hardening slice and now used as a regression gate.
|
||||||
|
|
@ -96,6 +117,7 @@ Fresh validation cut:
|
||||||
- Completed broader schema/primitive discovery closure slice: `Mixed Schema/Primitive Closure Replay`: phase105 validates the combined current module surface across inventory root scope, historical inventory carryover, role-tail hygiene, bank role/purpose, supplier payout, bidirectional SVK value-flow, clean debt polarity, VAT tax-period continuity, and cash-flow/profit boundary; phase105 live replay is accepted.
|
- Completed broader schema/primitive discovery closure slice: `Mixed Schema/Primitive Closure Replay`: phase105 validates the combined current module surface across inventory root scope, historical inventory carryover, role-tail hygiene, bank role/purpose, supplier payout, bidirectional SVK value-flow, clean debt polarity, VAT tax-period continuity, and cash-flow/profit boundary; phase105 live replay is accepted.
|
||||||
- Current live canary: `phase105_mixed_schema_primitive_closure_live3` accepted `13/13`.
|
- Current live canary: `phase105_mixed_schema_primitive_closure_live3` accepted `13/13`.
|
||||||
- Current accepted autorun: `AGENT | Phase 105 mixed schema/primitive closure replay` (`gen-ag05131312-2d0445`).
|
- Current accepted autorun: `AGENT | Phase 105 mixed schema/primitive closure replay` (`gen-ag05131312-2d0445`).
|
||||||
|
- Current saved-session semantic replay closure: `saved_session_gen_mo1t93wq_jy0453e_rerun_final_semantic_20260518` accepted `31/31`.
|
||||||
- Implementation breadth: `~99% (Open-World Bounded Autonomy Breadth through Slice 25)`.
|
- Implementation breadth: `~99% (Open-World Bounded Autonomy Breadth through Slice 25)`.
|
||||||
- Active broader autonomy module: `Open-World Schema/Primitive Discovery`, with phases97-105 accepted and saved; the module is now at manual-review readiness rather than another blind coding slice.
|
- Active broader autonomy module: `Open-World Schema/Primitive Discovery`, with phases97-105 accepted and saved; the module is now at manual-review readiness rather than another blind coding slice.
|
||||||
- Next active slice: run/review the phase105 GUI autorun or the user's fat manual pack; if it stays clean, close this module, otherwise convert the next observed failure into a narrow phase106 repair/replay.
|
- Next active slice: run/review the phase105 GUI autorun or the user's fat manual pack; if it stays clean, close this module, otherwise convert the next observed failure into a narrow phase106 repair/replay.
|
||||||
|
|
@ -167,28 +189,29 @@ After any code or documentation sync that changes the map, rebuild graphify and
|
||||||
For current planning, read:
|
For current planning, read:
|
||||||
|
|
||||||
1. `README.md`
|
1. `README.md`
|
||||||
2. this document
|
2. `41 - assistant_context_entry_2026-05-18.md`
|
||||||
3. `31 - inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md`
|
3. this document
|
||||||
4. `33 - limit_honesty_business_language_2026-05-13.md`
|
4. `31 - inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md`
|
||||||
5. `32 - financial_counterparty_flow_hints_2026-05-13.md`
|
5. `33 - limit_honesty_business_language_2026-05-13.md`
|
||||||
6. `30 - vendor_procurement_quality_reviewed_route_2026-05-12.md`
|
6. `32 - financial_counterparty_flow_hints_2026-05-13.md`
|
||||||
7. `29 - debt_due_date_aging_reviewed_route_2026-05-10.md`
|
7. `30 - vendor_procurement_quality_reviewed_route_2026-05-12.md`
|
||||||
8. `28 - accounting_profit_margin_reviewed_route_2026-05-10.md`
|
8. `29 - debt_due_date_aging_reviewed_route_2026-05-10.md`
|
||||||
9. `27 - proof_family_enablement_candidates_2026-05-10.md`
|
9. `28 - accounting_profit_margin_reviewed_route_2026-05-10.md`
|
||||||
10. `26 - route_candidate_driven_enablement_loop_2026-05-10.md`
|
10. `27 - proof_family_enablement_candidates_2026-05-10.md`
|
||||||
11. `25 - open_world_route_candidate_promotion_2026-05-10.md`
|
11. `26 - route_candidate_driven_enablement_loop_2026-05-10.md`
|
||||||
12. `34 - large_query_budget_continuation_2026-05-13.md`
|
12. `25 - open_world_route_candidate_promotion_2026-05-10.md`
|
||||||
13. `35 - large_query_continuation_ux_2026-05-13.md`
|
13. `34 - large_query_budget_continuation_2026-05-13.md`
|
||||||
14. `36 - inventory_root_scope_no_warehouse_clarification_2026-05-13.md`
|
14. `35 - large_query_continuation_ux_2026-05-13.md`
|
||||||
15. `37 - debt_mirror_clean_scope_polarity_2026-05-13.md`
|
15. `36 - inventory_root_scope_no_warehouse_clarification_2026-05-13.md`
|
||||||
16. `24 - agentic_semantic_development_loop_and_autorun_hygiene_2026-05-10.md`
|
16. `37 - debt_mirror_clean_scope_polarity_2026-05-13.md`
|
||||||
17. `23 - current_execution_spine_and_semantic_control_gate_2026-05-05.md`
|
17. `24 - agentic_semantic_development_loop_and_autorun_hygiene_2026-05-10.md`
|
||||||
18. `22 - open_world_bounded_autonomy_breadth_2026-05-01.md`
|
18. `23 - current_execution_spine_and_semantic_control_gate_2026-05-05.md`
|
||||||
19. `20 - planner_autonomy_consolidation_2026-05-01.md`
|
19. `22 - open_world_bounded_autonomy_breadth_2026-05-01.md`
|
||||||
20. `19 - inventory_stock_open_world_breadth_proof_2026-05-01.md`
|
20. `20 - planner_autonomy_consolidation_2026-05-01.md`
|
||||||
21. `40 - mixed_schema_primitive_closure_replay_2026-05-13.md`
|
21. `19 - inventory_stock_open_world_breadth_proof_2026-05-01.md`
|
||||||
22. `39 - generic_role_tail_anchor_hygiene_2026-05-13.md`
|
22. `40 - mixed_schema_primitive_closure_replay_2026-05-13.md`
|
||||||
23. `17 - post_f_semantic_integrity_hardening_2026-04-23.md`
|
23. `39 - generic_role_tail_anchor_hygiene_2026-05-13.md`
|
||||||
24. `16 - data_need_graph_and_open_world_mcp_plan_2026-04-22.md`
|
24. `17 - post_f_semantic_integrity_hardening_2026-04-23.md`
|
||||||
|
25. `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.
|
Documents `01` through `15` remain valuable, but mostly as the historical architecture trail.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,178 @@
|
||||||
|
# 41 - Вход в контекст разработки ассистента 1C (2026-05-18)
|
||||||
|
|
||||||
|
## Назначение
|
||||||
|
|
||||||
|
Этот документ является короткой, но полной точкой входа для нового чата, нового Codex-сеанса или нового инженера.
|
||||||
|
|
||||||
|
Он фиксирует актуальное состояние архитектуры после закрытия последнего semantic replay по saved-session `gen-mo1t93wq-jy0453e` и коммита:
|
||||||
|
|
||||||
|
- `9c86407 Зафиксировать семантическую целостность VAT, debt mirror и trace-ответов`
|
||||||
|
|
||||||
|
Главная цель проекта не изменилась:
|
||||||
|
|
||||||
|
- построить 1C-ассистента, который сам выбирает безопасные маршруты по MCP/1C evidence;
|
||||||
|
- не превращать систему в набор хардкодных доменных скрепок;
|
||||||
|
- отвечать бизнесово полезно, прямо и честно;
|
||||||
|
- не выдавать неподтвержденные inference как confirmed 1C fact.
|
||||||
|
|
||||||
|
## Что читать первым
|
||||||
|
|
||||||
|
Для быстрого входа читать в таком порядке:
|
||||||
|
|
||||||
|
1. `AGENTS.md`
|
||||||
|
2. `graphify-out/GRAPH_REPORT.md`
|
||||||
|
3. `docs/ARCH/11 - architecture_turnaround/README.md`
|
||||||
|
4. `docs/ARCH/11 - architecture_turnaround/21 - current_status_canon_2026-05-01.md`
|
||||||
|
5. `docs/ARCH/11 - architecture_turnaround/24 - agentic_semantic_development_loop_and_autorun_hygiene_2026-05-10.md`
|
||||||
|
6. `docs/ARCH/11 - architecture_turnaround/40 - mixed_schema_primitive_closure_replay_2026-05-13.md`
|
||||||
|
7. этот документ
|
||||||
|
8. актуальные Tasker-карточки с label `1C assistant` и `Архитектура`
|
||||||
|
|
||||||
|
Исторические документы `01`-`20` важны как trail решений, но текущий рабочий статус берется из `21`, `24`, `40`, этого документа и Tasker.
|
||||||
|
|
||||||
|
## Текущий статус модулей
|
||||||
|
|
||||||
|
Post-F Semantic Integrity Hardening:
|
||||||
|
|
||||||
|
- статус: `99%`, operationally closed;
|
||||||
|
- теперь это regression gate, а не активный denominator;
|
||||||
|
- защищает stale scope, wrong focus_object, repeated pivots, post-pivot arbitration, VAT materialization, debt mirror polarity, selected-object continuity и answer-shape truth.
|
||||||
|
|
||||||
|
Planner Autonomy Consolidation:
|
||||||
|
|
||||||
|
- статус: `100%` для declared phase83 slice;
|
||||||
|
- planner-brain, catalog alignment, live-readiness gate и mixed replay приняты;
|
||||||
|
- это не означает arbitrary 1C autonomy, но это закрывает первый мозг маршрутов.
|
||||||
|
|
||||||
|
Open-World Schema/Primitive Discovery:
|
||||||
|
|
||||||
|
- phases97-105 приняты и сохранены как canaries;
|
||||||
|
- phase105 mixed schema/primitive closure replay accepted `13/13`;
|
||||||
|
- модуль находится на manual-review readiness, а не на blind coding stage.
|
||||||
|
|
||||||
|
Agentic Semantic Development Loop:
|
||||||
|
|
||||||
|
- статус: `99%`;
|
||||||
|
- stage loop, business-audit handoff, save-after-acceptance gate и autorun hygiene работают;
|
||||||
|
- human GUI checkpoint остается финальным high-signal подтверждением.
|
||||||
|
|
||||||
|
Последний semantic integrity cut:
|
||||||
|
|
||||||
|
- saved session: `gen-mo1t93wq-jy0453e`;
|
||||||
|
- final replay: `artifacts/domain_runs/saved_session_gen_mo1t93wq_jy0453e_rerun_final_semantic_20260518`;
|
||||||
|
- результат: `accepted`, `31/31 passed`, `0 failed`, `execution_status=exact`;
|
||||||
|
- закрытые seams: VAT same-period carryover, stale MCP discovery counterparty in short debt mirror, sale-trace lot/batch honesty, broad best-year net-ranking honesty.
|
||||||
|
|
||||||
|
## Архитектурная концепция
|
||||||
|
|
||||||
|
Система состоит из нескольких связанных слоев.
|
||||||
|
|
||||||
|
Assistant runtime:
|
||||||
|
|
||||||
|
- внешний фасад живого ассистента, GUI и runtime-сессий;
|
||||||
|
- основной pressure center все еще вокруг `assistantService.ts`, но бизнесовая логика постепенно вынесена в специализированные модули;
|
||||||
|
- runtime обязан сохранять session state, но не должен позволять старой памяти победить explicit current-turn meaning.
|
||||||
|
|
||||||
|
Exact address lane:
|
||||||
|
|
||||||
|
- `AddressQueryService`, `addressRecipeCatalog`, `addressIntentResolver`, `addressFilterExtractor`, `address_runtime/*`;
|
||||||
|
- отвечает за VAT, receivables/payables, inventory, value-flow, bank operations, accounting result, procurement, debt aging, inventory quality events и связанные factual replies;
|
||||||
|
- exact route может быть fast path, но не может обходить truth gate, scope gate и answer-shape gate.
|
||||||
|
|
||||||
|
MCP discovery/planner lane:
|
||||||
|
|
||||||
|
- `assistantMcpDiscoveryPlanner`, `assistantMcpCatalogIndex`, `assistantMcpDiscoveryPilotExecutor`, `assistantMcpDiscoveryRuntimeBridge`;
|
||||||
|
- выбирает reviewed primitive chains через metadata, entity grounding, documents, movements, value-flow, route candidates;
|
||||||
|
- не должен превращать unknown или proxy-only evidence в confirmed fact.
|
||||||
|
|
||||||
|
Data-need graph and route-candidate layer:
|
||||||
|
|
||||||
|
- описывает вопрос пользователя как бизнесовую потребность, а не только как route id;
|
||||||
|
- хранит subject, fact family, action family, period, aggregation, ranking, comparison, proof expectation, missing axes и forbidden-overclaim flags;
|
||||||
|
- `route_candidate` превращает missing proof family в конкретный enablement target, а не в размытое “не умеем”.
|
||||||
|
|
||||||
|
Continuity and transition layer:
|
||||||
|
|
||||||
|
- `assistantTransitionPolicy`, `assistantContinuityPolicy`, `assistantMcpDiscoveryTurnInputAdapter`, navigation state и focus/answer object helpers;
|
||||||
|
- решает, что переносится между turn-ами: organization, counterparty, period, selected object, answer_object, provenance bundle;
|
||||||
|
- explicit current-turn entity/period/action должны побеждать stale organization, stale focus_object и старые discovery candidates.
|
||||||
|
|
||||||
|
Answer shaping and response policy:
|
||||||
|
|
||||||
|
- `assistantMcpDiscoveryAnswerAdapter`, `assistantMcpDiscoveryResponseCandidate`, `assistantMcpDiscoveryResponsePolicy`, factual reply builders;
|
||||||
|
- пользовательский ответ должен начинаться с прямого business answer;
|
||||||
|
- proof, caveats, row limits и method notes идут после ответа;
|
||||||
|
- internal route ids, capability ids, raw debug enums и service mechanics не должны попадать в финальный ответ.
|
||||||
|
|
||||||
|
GUI, autoruns and runtime artifacts:
|
||||||
|
|
||||||
|
- `autoRuns`, `eval`, `assistantService`, `addressTextRepair`;
|
||||||
|
- GUI autoruns являются human checkpoint и replay history;
|
||||||
|
- сохранение вопросов не равно AGENT replay;
|
||||||
|
- saved AGENT pack допустим только после live replay and review;
|
||||||
|
- UTF-8 без BOM и отсутствие mojibake являются acceptance surface, а не косметикой.
|
||||||
|
|
||||||
|
## Главные инварианты
|
||||||
|
|
||||||
|
- Сначала проверяется человеческий смысл вопроса и ответа, потом debug.
|
||||||
|
- Live replay важнее зеленых unit tests.
|
||||||
|
- Explicit текущий субъект сильнее stale scope.
|
||||||
|
- Valid clarification is not a bug.
|
||||||
|
- Debt mirror должен различать `мы должны` и `нам должны`.
|
||||||
|
- Bank-like counterparty не является обычным поставщиком или клиентом без purpose/operation evidence.
|
||||||
|
- VAT period carryover должен сохранять тот же период, если пользователь говорит `за этот период`.
|
||||||
|
- Inventory sale trace может подтвердить sale trace by nomenclature, но не exact selected lot без batch/lot proof.
|
||||||
|
- Broad business overview не должен ранжировать unreliable net, если один из потоков ограничен row cap.
|
||||||
|
- No route/proxy/MCP/debug garbage in final answer.
|
||||||
|
|
||||||
|
## Текущие canary/replay anchors
|
||||||
|
|
||||||
|
Ключевые canaries:
|
||||||
|
|
||||||
|
- `phase83_planner_brain_alignment_live_20260501_readygate_rerun3`
|
||||||
|
- `address_truth_harness_post_f_cross_stage_canary_agent_20260424_live7`
|
||||||
|
- `inventory_stock_open_world_breadth_rerun_semantic_integrity_20260501_fix5`
|
||||||
|
- `phase90`-`phase96` route-candidate/proof-family acceptance chain
|
||||||
|
- `phase97_financial_counterparty_flow_hints_live4`
|
||||||
|
- `phase98_limit_honesty_business_language_live3`
|
||||||
|
- `phase99_large_query_budget_continuation_live2`
|
||||||
|
- `phase100_large_query_continuation_ux_live2`
|
||||||
|
- `phase101_inventory_root_scope_no_warehouse_clarification_live1`
|
||||||
|
- `phase102_debt_mirror_clean_scope_polarity_live3`
|
||||||
|
- `phase103_financial_role_purpose_arbitration_live3`
|
||||||
|
- `phase104_generic_role_tail_anchor_hygiene`
|
||||||
|
- `phase105_mixed_schema_primitive_closure_live3`
|
||||||
|
- `saved_session_gen_mo1t93wq_jy0453e_rerun_final_semantic_20260518`
|
||||||
|
|
||||||
|
Если новый фикс касается соседней зоны, соответствующий replay нужно использовать как semantic regression gate.
|
||||||
|
|
||||||
|
## Как продолжать в новом чате
|
||||||
|
|
||||||
|
Новый Codex-сеанс должен начинать так:
|
||||||
|
|
||||||
|
1. прочитать `AGENTS.md`;
|
||||||
|
2. прочитать `graphify-out/GRAPH_REPORT.md`;
|
||||||
|
3. прочитать этот документ, README, `21`, `24`, `40`;
|
||||||
|
4. проверить `git status --short`;
|
||||||
|
5. не коммитить runtime_job artifacts без явного решения;
|
||||||
|
6. определить текущий active module и его denominator;
|
||||||
|
7. если пользователь принес run id, сначала читать human Q/A, затем artifacts/debug;
|
||||||
|
8. если внесены code changes, запускать targeted tests, build по необходимости, semantic replay, graphify rebuild;
|
||||||
|
9. после accepted replay обновлять docs/Tasker, если меняется архитектурный статус.
|
||||||
|
|
||||||
|
## Текущее направление после этого среза
|
||||||
|
|
||||||
|
Следующий крупный фокус:
|
||||||
|
|
||||||
|
- закрепить agentic semantic replay loop как регулярный gate после крупных фиксов;
|
||||||
|
- минимизировать ручную роль пользователя до финального GUI checkpoint;
|
||||||
|
- продолжать движение к open-world bounded autonomy, где ассистент не ждет хардкодного route-per-domain, а выбирает reviewed MCP primitive path по data-need graph, route_candidate и evidence gates.
|
||||||
|
|
||||||
|
При этом нельзя объявлять проект universal arbitrary-1C agent.
|
||||||
|
|
||||||
|
Честная формулировка текущего состояния:
|
||||||
|
|
||||||
|
- система уже умеет много устойчивых 1C-контуров;
|
||||||
|
- bounded MCP autonomy substrate реально работает;
|
||||||
|
- route-candidate-driven enablement показал, как превращать gaps в reviewed routes;
|
||||||
|
- но широкая arbitrary schema traversal все еще должна расширяться через replay-backed slices, а не через свободную импровизацию.
|
||||||
|
|
@ -0,0 +1,175 @@
|
||||||
|
# 42 - Project Audit Milestone And Next Vector (2026-05-18)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This note records the May-18 audit milestone after a repo-first review of:
|
||||||
|
|
||||||
|
- current runtime code shape;
|
||||||
|
- graphify pressure centers;
|
||||||
|
- current architecture canon in `21`, `24`, `40`, and `41`;
|
||||||
|
- Tasker handoff cards for `1C assistant` / `1С-ассистент`;
|
||||||
|
- latest accepted live replay and saved-session closure artifacts.
|
||||||
|
|
||||||
|
Its purpose is not to reopen old slices.
|
||||||
|
|
||||||
|
Its purpose is to:
|
||||||
|
|
||||||
|
- freeze the current project map in one place;
|
||||||
|
- state what is actually closed, what is still open, and what is merely pressure-tested;
|
||||||
|
- define the next unified development vector after the current sync pass.
|
||||||
|
|
||||||
|
## Audit Result
|
||||||
|
|
||||||
|
The project is materially beyond the old "build first exact routes" stage.
|
||||||
|
|
||||||
|
The real current architecture is a bounded MCP-first assistant built from these active layers:
|
||||||
|
|
||||||
|
- assistant runtime and orchestration facade;
|
||||||
|
- exact address lane;
|
||||||
|
- MCP discovery / planner lane;
|
||||||
|
- data-need graph and route-candidate layer;
|
||||||
|
- continuity / transition / selected-object state layer;
|
||||||
|
- truth gate and answer-shaping layer;
|
||||||
|
- AGENT semantic replay operating loop;
|
||||||
|
- GUI / autorun / runtime artifact layer.
|
||||||
|
|
||||||
|
The most connected code pressure centers remain:
|
||||||
|
|
||||||
|
- `executeAssistantMcpDiscoveryPilot()`
|
||||||
|
- `buildAssistantMcpDiscoveryTurnInput()`
|
||||||
|
- `ChannelRegistry`
|
||||||
|
- `composeFactualReplyBody()`
|
||||||
|
- `extractAddressFilters()`
|
||||||
|
- `repairAddressMojibake()`
|
||||||
|
|
||||||
|
This confirms that the current blast radius is not only in one route or one wording family. The highest-risk seams still sit at:
|
||||||
|
|
||||||
|
- runtime input assembly;
|
||||||
|
- continuity and stale-scope arbitration;
|
||||||
|
- MCP execution and evidence shaping;
|
||||||
|
- final answer truthfulness and text hygiene.
|
||||||
|
|
||||||
|
## Current Module Map
|
||||||
|
|
||||||
|
### Operationally closed or used as regression gates
|
||||||
|
|
||||||
|
- `Post-F Semantic Integrity Hardening`: operationally closed at `99%`
|
||||||
|
- `Planner Autonomy Consolidation`: closed at `100%`
|
||||||
|
- `Route-Candidate-Driven Enablement Loop`: closed at `100%`
|
||||||
|
- reviewed proof-family routes through phases `93-96`
|
||||||
|
- business-overview breadth slices through the currently accepted reviewed families
|
||||||
|
|
||||||
|
### Active but already implementation-heavy
|
||||||
|
|
||||||
|
- `Open-World Schema/Primitive Discovery`: `95%`
|
||||||
|
- phases `97-105` are accepted and saved
|
||||||
|
- current closure replay: `phase105_mixed_schema_primitive_closure_live3`, `13/13`
|
||||||
|
- module status: manual-review readiness, not blind coding stage
|
||||||
|
|
||||||
|
### Active operating layer
|
||||||
|
|
||||||
|
- `Agentic Semantic Development Loop`: `99%`
|
||||||
|
- dogfood loop accepted
|
||||||
|
- autorun/runtime Cyrillic hygiene accepted
|
||||||
|
- manual GUI confirmation still required before treating fat packs as fully human-accepted
|
||||||
|
|
||||||
|
## Latest Accepted Semantic Closure
|
||||||
|
|
||||||
|
Latest saved-session closure:
|
||||||
|
|
||||||
|
- source saved session: `gen-mo1t93wq-jy0453e`
|
||||||
|
- replay artifact: `artifacts/domain_runs/saved_session_gen_mo1t93wq_jy0453e_rerun_final_semantic_20260518`
|
||||||
|
- result: `accepted`
|
||||||
|
- score: `31/31 passed`
|
||||||
|
- execution status: `exact`
|
||||||
|
- commit anchor: `9c86407 Зафиксировать семантическую целостность VAT, debt mirror и trace-ответов`
|
||||||
|
|
||||||
|
This closure additionally hardened:
|
||||||
|
|
||||||
|
- same-period VAT carryover;
|
||||||
|
- stale MCP discovery counterparty contamination in short debt-mirror follow-ups;
|
||||||
|
- sale-trace honesty between nomenclature trace and exact lot/batch proof;
|
||||||
|
- broad best-year net-ranking honesty under row-cap asymmetry.
|
||||||
|
|
||||||
|
## Important Accepted Breadth Boundary
|
||||||
|
|
||||||
|
`phase86` must be treated as a real accepted contour in the current map:
|
||||||
|
|
||||||
|
- spec: `docs/orchestration/address_truth_harness_phase86_business_overview_debt_position.json`
|
||||||
|
- accepted replay: `artifacts/domain_runs/address_truth_harness_phase86_business_overview_debt_position_live_20260504_debt2`
|
||||||
|
|
||||||
|
Meaning:
|
||||||
|
|
||||||
|
- explicit-period `business_overview` may include confirmed receivables/payables snapshot on an explicit as-of date;
|
||||||
|
- all-time follow-up must not reuse the old debt snapshot as current or general debt position;
|
||||||
|
- debt quality, overdue debt, credit risk, profit, and margin remain bounded unless separately proven.
|
||||||
|
|
||||||
|
This boundary matters because it is a clean example of the current project style:
|
||||||
|
|
||||||
|
- expand breadth through reviewed evidence;
|
||||||
|
- widen answer usefulness;
|
||||||
|
- keep stale-scope and overclaim protection intact.
|
||||||
|
|
||||||
|
## Documentation And Tasker Audit Findings
|
||||||
|
|
||||||
|
### What was already good
|
||||||
|
|
||||||
|
- the repo canon in `21`, `24`, `40`, and `41` is coherent and materially up to date;
|
||||||
|
- the high-level Tasker card set `01-17` already covers the architecture spine well;
|
||||||
|
- the dedicated context-entry card `[1С-ассистент] Вход в контекст разработки ассистента 1С` already works as the right top-level handoff card.
|
||||||
|
|
||||||
|
### What was missing or underrepresented
|
||||||
|
|
||||||
|
- latest May-18 saved-session closure was not reflected everywhere;
|
||||||
|
- the current honest `95%` status for `Open-World Schema/Primitive Discovery` needed to be stated more explicitly in Tasker;
|
||||||
|
- the accepted `phase86` debt-position breadth boundary was present in repo docs and artifacts, but was not explicit enough in the card layer;
|
||||||
|
- the practical review order `human Q/A first -> replay artifacts -> debug later` needed to be restated in the main handoff card layer;
|
||||||
|
- the current "active gate" reality around semantic-control / fat GUI review was still more explicit in repo docs than in the Tasker map.
|
||||||
|
|
||||||
|
## Unified Next Development Vector
|
||||||
|
|
||||||
|
The project should not fork into unrelated planning threads.
|
||||||
|
|
||||||
|
The current unified vector is:
|
||||||
|
|
||||||
|
1. Close the current human acceptance gate cleanly.
|
||||||
|
2. Institutionalize the AGENT semantic replay loop as the normal post-fix gate.
|
||||||
|
3. Expand open-world bounded autonomy only through replay-backed breadth slices.
|
||||||
|
4. Reduce pressure on central continuity and intent seams while preserving existing regression gates.
|
||||||
|
|
||||||
|
In practical terms this means:
|
||||||
|
|
||||||
|
- first finish `phase105` GUI/manual review or the equivalent fat user pack review;
|
||||||
|
- if the review is clean, close the current `Open-World Schema/Primitive Discovery` module honestly;
|
||||||
|
- if the review exposes a real semantic defect, convert it into a narrow `phase106 repair/replay`, not a broad architecture rewrite;
|
||||||
|
- continue future breadth through reviewed primitive descriptors, data-need coverage, route-candidate enablement, and replay-backed acceptance;
|
||||||
|
- do not regress into domain-hardcode-first growth.
|
||||||
|
|
||||||
|
## What Should Not Become The Next Vector
|
||||||
|
|
||||||
|
The next vector should not be:
|
||||||
|
|
||||||
|
- "rewrite the runtime again";
|
||||||
|
- "add routes opportunistically without replay discipline";
|
||||||
|
- "treat green tests or route ids as primary acceptance";
|
||||||
|
- "expand arbitrary schema traversal before the current closure gate is honestly reviewed";
|
||||||
|
- "replace the AGENT loop with ad hoc manual debugging."
|
||||||
|
|
||||||
|
## Recommended Immediate Work Order
|
||||||
|
|
||||||
|
1. Keep the main context-entry card and current module cards synchronized with repo canon.
|
||||||
|
2. Add or maintain one explicit Tasker card for the current semantic-control / GUI/manual acceptance gate if that gate is not clearly represented.
|
||||||
|
3. Use `phase105` and the May-18 saved-session closure as the current top replay anchors.
|
||||||
|
4. After the card sync, plan the next slice only from the audited state above, not from older percentage snapshots or stale docs.
|
||||||
|
|
||||||
|
## Honest Project Status Statement
|
||||||
|
|
||||||
|
The project already has a real bounded MCP autonomy substrate and a strong semantic hardening backbone.
|
||||||
|
|
||||||
|
It is not a universal arbitrary-1C agent yet.
|
||||||
|
|
||||||
|
The correct current status is:
|
||||||
|
|
||||||
|
- many important 1C contours are already stable and replay-backed;
|
||||||
|
- the main remaining risk is not absence of architecture, but semantic drift on mixed human-style pressure surfaces;
|
||||||
|
- the next development denominator is controlled breadth expansion after an honest current acceptance closure, not a reset.
|
||||||
|
|
@ -0,0 +1,360 @@
|
||||||
|
# 43 - Business Answer Contract And Semantic Audit Uplift (2026-05-18)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This note freezes the next real project module after the May-18 project audit.
|
||||||
|
|
||||||
|
Its purpose is to turn a high-quality human audit of real runs into:
|
||||||
|
|
||||||
|
- a runtime answer-shaping module;
|
||||||
|
- a stronger semantic replay audit contract;
|
||||||
|
- a concrete execution order for the next development stage.
|
||||||
|
|
||||||
|
The core conclusion is:
|
||||||
|
|
||||||
|
- the assistant already finds many real facts correctly;
|
||||||
|
- the current weak point is not raw retrieval;
|
||||||
|
- the weak point is the conversion from confirmed facts into a business-useful answer and a business-grade replay verdict.
|
||||||
|
|
||||||
|
In short:
|
||||||
|
|
||||||
|
`truth/retrieval is materially ahead of user-facing answer quality and semantic audit quality`
|
||||||
|
|
||||||
|
## Canonical Diagnosis
|
||||||
|
|
||||||
|
The current assistant too often behaves like a careful technical reporter instead of a strong 1C business analyst.
|
||||||
|
|
||||||
|
This means:
|
||||||
|
|
||||||
|
- confirmed numbers are found;
|
||||||
|
- organization/date context is often preserved;
|
||||||
|
- truth and anti-overclaim guards are active;
|
||||||
|
- but the final answer is still overloaded with caveats, low on management interpretation, weak on next action, and not stable enough on semantic disambiguation.
|
||||||
|
|
||||||
|
The current failure pattern is:
|
||||||
|
|
||||||
|
1. the system proves a fact;
|
||||||
|
2. the truth layer leaks directly into the user answer;
|
||||||
|
3. the user receives a cautious technical block instead of a business answer;
|
||||||
|
4. the replay evaluator notices this only partially;
|
||||||
|
5. the run remains technically respectable but business-weak.
|
||||||
|
|
||||||
|
## What The Human Audit Proved
|
||||||
|
|
||||||
|
The human audit established two linked facts.
|
||||||
|
|
||||||
|
### 1. The current audit layer is too weak
|
||||||
|
|
||||||
|
If the AGENT semantic replay audit were as strong as the human review, more runs would be rejected or repaired before being treated as healthy.
|
||||||
|
|
||||||
|
The current replay stack can already see some of this through metrics such as:
|
||||||
|
|
||||||
|
- `accountant_actionability_score`
|
||||||
|
- `mechanism_specificity_score`
|
||||||
|
- `followup_context_retention_score`
|
||||||
|
|
||||||
|
But the practical business reading is still underpowered.
|
||||||
|
|
||||||
|
The system can tell that the answer is weak.
|
||||||
|
It is still not good enough at explaining why it is weak in the same business language a strong human reviewer uses.
|
||||||
|
|
||||||
|
### 2. The runtime answer layer is the main product gap
|
||||||
|
|
||||||
|
The main user-facing problem is not "the system cannot compute".
|
||||||
|
|
||||||
|
The main problem is:
|
||||||
|
|
||||||
|
`the system cannot consistently convert computed truths into a concise, useful, accountant-grade answer`
|
||||||
|
|
||||||
|
This gap shows up as:
|
||||||
|
|
||||||
|
- dirty answer shape;
|
||||||
|
- too much defensive wording in the main body;
|
||||||
|
- weak distinction between cashflow, profit, debt, bank contour, inventory snapshot, and historical inventory;
|
||||||
|
- weak business takeaway;
|
||||||
|
- missing next action;
|
||||||
|
- technical or truth-gate wording leaking into the user-facing answer.
|
||||||
|
|
||||||
|
## Architectural Reading Of The Gap
|
||||||
|
|
||||||
|
This audit should be read as a split between two layers.
|
||||||
|
|
||||||
|
### Layer A: Truth Layer
|
||||||
|
|
||||||
|
This layer answers:
|
||||||
|
|
||||||
|
- what is confirmed;
|
||||||
|
- by which evidence;
|
||||||
|
- within which date window;
|
||||||
|
- with which limitations;
|
||||||
|
- whether overclaim protection should block or soften the answer.
|
||||||
|
|
||||||
|
This layer is already substantial and should remain strict.
|
||||||
|
|
||||||
|
### Layer B: Business Answer Layer
|
||||||
|
|
||||||
|
This layer answers:
|
||||||
|
|
||||||
|
- what the result means for the business user;
|
||||||
|
- which figures matter first;
|
||||||
|
- what ambiguity should be surfaced explicitly;
|
||||||
|
- what should be checked next;
|
||||||
|
- how to keep the answer readable without lying.
|
||||||
|
|
||||||
|
This layer is the main missing product denominator.
|
||||||
|
|
||||||
|
Current problem:
|
||||||
|
|
||||||
|
`Layer A is too visible; Layer B is too weak`
|
||||||
|
|
||||||
|
## Exact Code Seams
|
||||||
|
|
||||||
|
The current implementation seams match this diagnosis closely.
|
||||||
|
|
||||||
|
### Runtime answer shaping seams
|
||||||
|
|
||||||
|
- `llm_normalizer/backend/src/services/answerComposer.ts`
|
||||||
|
- `llm_normalizer/backend/src/services/address_runtime/composeStage.ts`
|
||||||
|
- `llm_normalizer/backend/src/services/assistantTruthAnswerPolicyRuntimeAdapter.ts`
|
||||||
|
- `llm_normalizer/backend/src/services/assistantDebugPayloadAssembler.ts`
|
||||||
|
|
||||||
|
Relevant active contracts already exist:
|
||||||
|
|
||||||
|
- `answer_contract_stage4_v1`
|
||||||
|
- `answer_structure_v11`
|
||||||
|
- `direct_answer`
|
||||||
|
- `mechanism_block`
|
||||||
|
- `uncertainty_block`
|
||||||
|
- `next_step_block`
|
||||||
|
|
||||||
|
This means the next step is not a greenfield rewrite.
|
||||||
|
It is a targeted strengthening of the answer contract and reply renderer.
|
||||||
|
|
||||||
|
### Replay / audit seams
|
||||||
|
|
||||||
|
- `llm_normalizer/backend/src/services/evalService.ts`
|
||||||
|
- `llm_normalizer/backend/src/eval/p0_eval_runner.ts`
|
||||||
|
- `llm_normalizer/backend/src/eval/p0_metric_definitions.ts`
|
||||||
|
- `llm_normalizer/backend/src/eval/p0_acceptance_gate.ts`
|
||||||
|
|
||||||
|
These seams already compute weak/strong answer signals, but they do not yet encode the full business-grade audit logic demonstrated by the human review.
|
||||||
|
|
||||||
|
## Next Module Definition
|
||||||
|
|
||||||
|
The next module should be named:
|
||||||
|
|
||||||
|
`Business Answer Contract + Semantic Audit Uplift`
|
||||||
|
|
||||||
|
It must be treated as one module with two subtracks, not as unrelated work.
|
||||||
|
|
||||||
|
### Track 1. Runtime Business Answer Contract
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
|
||||||
|
- make the assistant answer like a usable 1C analyst without weakening truthfulness.
|
||||||
|
|
||||||
|
Required outcomes:
|
||||||
|
|
||||||
|
1. Introduce stable answer shapes for business question families.
|
||||||
|
2. Move caveats out of the main answer body and compress them into one honest boundary block.
|
||||||
|
3. Force a management-first direct answer before evidence details.
|
||||||
|
4. Always provide a concrete next action when the contour allows it.
|
||||||
|
5. Prevent technical leakage into the user-facing answer.
|
||||||
|
|
||||||
|
### Track 2. Semantic Audit Uplift
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
|
||||||
|
- make AGENT replay review detect the same answer-quality failures that a strong human auditor detects.
|
||||||
|
|
||||||
|
Required outcomes:
|
||||||
|
|
||||||
|
1. Detect when the answer is factually grounded but business-useless.
|
||||||
|
2. Detect when ambiguity was handled weakly or silently.
|
||||||
|
3. Detect when caveats dominate the main answer instead of being bounded cleanly.
|
||||||
|
4. Detect when heterogeneous quantities are surfaced as misleading business totals.
|
||||||
|
5. Detect when the answer lacks a management takeaway or next step.
|
||||||
|
|
||||||
|
## Required Runtime Changes
|
||||||
|
|
||||||
|
### 1. Introduce answer intent shapes
|
||||||
|
|
||||||
|
The runtime should stop relying only on broad reply classes such as:
|
||||||
|
|
||||||
|
- `factual`
|
||||||
|
- `factual_with_explanation`
|
||||||
|
- `partial_coverage`
|
||||||
|
- `clarification_required`
|
||||||
|
|
||||||
|
It should add stable business answer shapes such as:
|
||||||
|
|
||||||
|
- `inventory_snapshot`
|
||||||
|
- `historical_inventory_snapshot`
|
||||||
|
- `cashflow_overview`
|
||||||
|
- `profit_vs_cashflow_disambiguation`
|
||||||
|
- `counterparty_value_flow`
|
||||||
|
- `bank_flow_classification`
|
||||||
|
- `tax_payable`
|
||||||
|
- `accounts_payable`
|
||||||
|
- `accounts_receivable`
|
||||||
|
- `business_overview_summary`
|
||||||
|
|
||||||
|
Each shape should have its own user-facing render contract.
|
||||||
|
|
||||||
|
### 2. Introduce ambiguity-first handling for overloaded business words
|
||||||
|
|
||||||
|
Critical words such as:
|
||||||
|
|
||||||
|
- "заработала"
|
||||||
|
- "прибыль"
|
||||||
|
- "деньги"
|
||||||
|
- "должны"
|
||||||
|
- "остатки"
|
||||||
|
- "оборот"
|
||||||
|
- "выручка"
|
||||||
|
|
||||||
|
must not silently collapse into one interpretation.
|
||||||
|
|
||||||
|
The answer should explicitly split the meaning when needed:
|
||||||
|
|
||||||
|
- cashflow meaning;
|
||||||
|
- accounting profit meaning;
|
||||||
|
- debt snapshot meaning;
|
||||||
|
- turnover meaning.
|
||||||
|
|
||||||
|
### 3. Force management-first answer order
|
||||||
|
|
||||||
|
The default order should become:
|
||||||
|
|
||||||
|
1. business conclusion;
|
||||||
|
2. 3-5 key figures;
|
||||||
|
3. one short boundary/limitation line;
|
||||||
|
4. next useful step.
|
||||||
|
|
||||||
|
This order should be preferred over:
|
||||||
|
|
||||||
|
- raw table-first answers;
|
||||||
|
- long limitation-first answers;
|
||||||
|
- technical explanation-first answers.
|
||||||
|
|
||||||
|
### 4. Suppress misleading aggregate quantities
|
||||||
|
|
||||||
|
For heterogeneous inventory and similar mixed lists, the runtime should avoid presenting meaningless aggregate counts as if they were strong management metrics.
|
||||||
|
|
||||||
|
This is especially important when a count mixes radically different unit types.
|
||||||
|
|
||||||
|
### 5. Add actionability by contract
|
||||||
|
|
||||||
|
Every relevant answer family should end with a next useful option, for example:
|
||||||
|
|
||||||
|
- show document drilldown;
|
||||||
|
- split by months;
|
||||||
|
- separate bank from clients;
|
||||||
|
- compute pure accounting profit;
|
||||||
|
- show overdue aging;
|
||||||
|
- open top counterparties;
|
||||||
|
- explain why this debt remains open.
|
||||||
|
|
||||||
|
This should be contract-driven, not an optional wording flourish.
|
||||||
|
|
||||||
|
## Required Audit Changes
|
||||||
|
|
||||||
|
### 1. Upgrade replay verdict language
|
||||||
|
|
||||||
|
The replay review should explicitly distinguish:
|
||||||
|
|
||||||
|
- factually wrong;
|
||||||
|
- factually right but business-wrong;
|
||||||
|
- factually right but ambiguity-handled-poorly;
|
||||||
|
- factually right but too technical/leaky;
|
||||||
|
- factually right but low-actionability.
|
||||||
|
|
||||||
|
### 2. Add stronger answer-quality probes
|
||||||
|
|
||||||
|
The semantic audit should explicitly score:
|
||||||
|
|
||||||
|
- management conclusion present or absent;
|
||||||
|
- key figure prioritization quality;
|
||||||
|
- limitation compression quality;
|
||||||
|
- ambiguity handling quality;
|
||||||
|
- next-step usefulness;
|
||||||
|
- bank/business contour classification quality;
|
||||||
|
- follow-up interpretation continuity.
|
||||||
|
|
||||||
|
### 3. Treat “technical cleanliness” as a first-class acceptance surface
|
||||||
|
|
||||||
|
If the answer leaks technical mechanics into the business response, that must be treated as a real quality defect, not as harmless debug residue.
|
||||||
|
|
||||||
|
### 4. Strengthen human-style post-run findings
|
||||||
|
|
||||||
|
The replay artifacts should move closer to the human audit style:
|
||||||
|
|
||||||
|
- what worked;
|
||||||
|
- what the user actually wanted;
|
||||||
|
- what was answered instead;
|
||||||
|
- why the answer is still weak;
|
||||||
|
- what exact runtime behavior should be changed.
|
||||||
|
|
||||||
|
## Proposed Execution Order
|
||||||
|
|
||||||
|
The next execution order should be:
|
||||||
|
|
||||||
|
1. freeze this module and Tasker it;
|
||||||
|
2. implement runtime answer-shape uplift first;
|
||||||
|
3. rerun focused semantic packs against the same business questions;
|
||||||
|
4. then tighten replay audit scoring and narrative findings using the new denominator;
|
||||||
|
5. only after that widen breadth again.
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
|
||||||
|
- if audit is improved before answer contracts, we mostly get stronger failure reporting;
|
||||||
|
- if answer contracts are improved first, the next audit pass can judge a better target surface.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
The module should be accepted only when the following become true on focused replay packs.
|
||||||
|
|
||||||
|
### Runtime acceptance
|
||||||
|
|
||||||
|
- direct answers start with business meaning, not caveat noise;
|
||||||
|
- ambiguous business terms are split honestly when needed;
|
||||||
|
- cashflow vs profit confusion is handled explicitly;
|
||||||
|
- bank-like contours are classified without pretending they are ordinary customer revenue;
|
||||||
|
- mixed inventory counts are not surfaced as misleading management totals;
|
||||||
|
- next-step guidance is present and useful.
|
||||||
|
|
||||||
|
### Audit acceptance
|
||||||
|
|
||||||
|
- weak business answers are called out even when retrieval is correct;
|
||||||
|
- replay findings read closer to a senior analyst review than to a metric dump;
|
||||||
|
- `accountant_actionability_score`, `mechanism_specificity_score`, and `followup_context_retention_score` stop being chronically weak on the target pack;
|
||||||
|
- accepted runs no longer pass merely because the truth layer is clean.
|
||||||
|
|
||||||
|
## What This Module Is Not
|
||||||
|
|
||||||
|
This module is not:
|
||||||
|
|
||||||
|
- a new domain-route spree;
|
||||||
|
- a planner rewrite;
|
||||||
|
- a truth-gate rollback;
|
||||||
|
- a generic UX polish task.
|
||||||
|
|
||||||
|
It is the missing business-answer contract between the already-strong retrieval core and the user.
|
||||||
|
|
||||||
|
## Recommended Immediate Tasker Shape
|
||||||
|
|
||||||
|
One active card should represent this module explicitly, with subtasks for:
|
||||||
|
|
||||||
|
1. answer intent shapes;
|
||||||
|
2. ambiguity handler for overloaded business words;
|
||||||
|
3. business-first render order;
|
||||||
|
4. actionability contract;
|
||||||
|
5. semantic audit uplift and replay rubric alignment.
|
||||||
|
|
||||||
|
## Honest Status Line
|
||||||
|
|
||||||
|
The project is now at the point where:
|
||||||
|
|
||||||
|
- retrieval competence is no longer the main differentiator;
|
||||||
|
- answer usefulness and replay audit quality are the next denominator;
|
||||||
|
- if this module lands well, the assistant will stop sounding like a debug log and start sounding like a real 1C analyst.
|
||||||
|
|
@ -58,13 +58,15 @@ This package answers the next question:
|
||||||
38. [38 - financial_role_purpose_arbitration_2026-05-13.md](./38%20-%20financial_role_purpose_arbitration_2026-05-13.md)
|
38. [38 - financial_role_purpose_arbitration_2026-05-13.md](./38%20-%20financial_role_purpose_arbitration_2026-05-13.md)
|
||||||
39. [39 - generic_role_tail_anchor_hygiene_2026-05-13.md](./39%20-%20generic_role_tail_anchor_hygiene_2026-05-13.md)
|
39. [39 - generic_role_tail_anchor_hygiene_2026-05-13.md](./39%20-%20generic_role_tail_anchor_hygiene_2026-05-13.md)
|
||||||
40. [40 - mixed_schema_primitive_closure_replay_2026-05-13.md](./40%20-%20mixed_schema_primitive_closure_replay_2026-05-13.md)
|
40. [40 - mixed_schema_primitive_closure_replay_2026-05-13.md](./40%20-%20mixed_schema_primitive_closure_replay_2026-05-13.md)
|
||||||
|
41. [41 - assistant_context_entry_2026-05-18.md](./41%20-%20assistant_context_entry_2026-05-18.md)
|
||||||
|
|
||||||
## Current Status Snapshot (2026-05-13)
|
## Current Status Snapshot (2026-05-18)
|
||||||
|
|
||||||
This package is no longer planning-only.
|
This package is no longer planning-only.
|
||||||
|
|
||||||
Status canon for planning:
|
Status canon for planning:
|
||||||
|
|
||||||
|
- The fastest current handoff document is now [41 - assistant_context_entry_2026-05-18.md](./41%20-%20assistant_context_entry_2026-05-18.md).
|
||||||
- The current operational overlay is now [24 - agentic_semantic_development_loop_and_autorun_hygiene_2026-05-10.md](./24%20-%20agentic_semantic_development_loop_and_autorun_hygiene_2026-05-10.md).
|
- The current operational overlay is now [24 - agentic_semantic_development_loop_and_autorun_hygiene_2026-05-10.md](./24%20-%20agentic_semantic_development_loop_and_autorun_hygiene_2026-05-10.md).
|
||||||
- The active engineering surface is no longer only individual route hardening; it is the repo-native AGENT/stage-loop operating system that should generate/review/replay/audit/repair/rerun current-stage packs before saving accepted autoruns.
|
- The active engineering surface is no longer only individual route hardening; it is the repo-native AGENT/stage-loop operating system that should generate/review/replay/audit/repair/rerun current-stage packs before saving accepted autoruns.
|
||||||
- The first dogfood stage loop for `agentic_semantic_development_loop` is accepted in artifacts, but manual GUI confirmation remains required before treating a fat AGENT pack as fully human-accepted.
|
- The first dogfood stage loop for `agentic_semantic_development_loop` is accepted in artifacts, but manual GUI confirmation remains required before treating a fat AGENT pack as fully human-accepted.
|
||||||
|
|
@ -153,6 +155,7 @@ Status canon for planning:
|
||||||
- The seventh broader schema/primitive discovery support slice is [38 - financial_role_purpose_arbitration_2026-05-13.md](./38%20-%20financial_role_purpose_arbitration_2026-05-13.md), now accepted live and saved as a user-runnable AGENT autorun.
|
- The seventh broader schema/primitive discovery support slice is [38 - financial_role_purpose_arbitration_2026-05-13.md](./38%20-%20financial_role_purpose_arbitration_2026-05-13.md), now accepted live and saved as a user-runnable AGENT autorun.
|
||||||
- The eighth broader schema/primitive discovery support slice is [39 - generic_role_tail_anchor_hygiene_2026-05-13.md](./39%20-%20generic_role_tail_anchor_hygiene_2026-05-13.md), now accepted live and saved as a user-runnable AGENT autorun.
|
- The eighth broader schema/primitive discovery support slice is [39 - generic_role_tail_anchor_hygiene_2026-05-13.md](./39%20-%20generic_role_tail_anchor_hygiene_2026-05-13.md), now accepted live and saved as a user-runnable AGENT autorun.
|
||||||
- The mixed schema/primitive closure replay is [40 - mixed_schema_primitive_closure_replay_2026-05-13.md](./40%20-%20mixed_schema_primitive_closure_replay_2026-05-13.md), now accepted live and saved as a user-runnable AGENT autorun.
|
- The mixed schema/primitive closure replay is [40 - mixed_schema_primitive_closure_replay_2026-05-13.md](./40%20-%20mixed_schema_primitive_closure_replay_2026-05-13.md), now accepted live and saved as a user-runnable AGENT autorun.
|
||||||
|
- The latest saved-session semantic integrity closure is `saved_session_gen_mo1t93wq_jy0453e_rerun_final_semantic_20260518`, accepted `31/31` after repairing VAT same-period carryover, stale MCP-discovery counterparty carryover in short debt mirror turns, sale-trace lot/batch honesty, and broad best-year net-ranking honesty.
|
||||||
|
|
||||||
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:
|
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:
|
||||||
|
|
||||||
|
|
@ -423,6 +426,7 @@ Read in this order:
|
||||||
39. `38 - financial_role_purpose_arbitration_2026-05-13.md`
|
39. `38 - financial_role_purpose_arbitration_2026-05-13.md`
|
||||||
40. `39 - generic_role_tail_anchor_hygiene_2026-05-13.md`
|
40. `39 - generic_role_tail_anchor_hygiene_2026-05-13.md`
|
||||||
41. `40 - mixed_schema_primitive_closure_replay_2026-05-13.md`
|
41. `40 - mixed_schema_primitive_closure_replay_2026-05-13.md`
|
||||||
|
42. `41 - assistant_context_entry_2026-05-18.md`
|
||||||
|
|
||||||
## Planning Rules
|
## Planning Rules
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,12 @@
|
||||||
"expected_requested_result_modes": ["confirmed_balance"],
|
"expected_requested_result_modes": ["confirmed_balance"],
|
||||||
"expected_result_modes": ["confirmed_balance"]
|
"expected_result_modes": ["confirmed_balance"]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"intent": "inventory_margin_ranking_for_nomenclature",
|
||||||
|
"expected_selected_recipes": ["address_inventory_margin_ranking_for_nomenclature_v1"],
|
||||||
|
"expected_requested_result_modes": ["confirmed_balance"],
|
||||||
|
"expected_result_modes": ["confirmed_balance"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"intent": "inventory_purchase_to_sale_chain",
|
"intent": "inventory_purchase_to_sale_chain",
|
||||||
"expected_selected_recipes": ["address_inventory_purchase_to_sale_chain_v1"],
|
"expected_selected_recipes": ["address_inventory_purchase_to_sale_chain_v1"],
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,189 @@
|
||||||
|
{
|
||||||
|
"schema_version": "domain_truth_harness_spec_v1",
|
||||||
|
"scenario_id": "agent_autonomy_business_quality_20260523",
|
||||||
|
"domain": "autonomy_business_quality",
|
||||||
|
"title": "AGENT | Autonomy business quality pack",
|
||||||
|
"description": "Expanded targeted AGENT replay for the current autonomy milestone: value-flow hot handoff must survive realistic business questions, while final answers must read like a useful 1C analyst instead of runtime/debug prose.",
|
||||||
|
"bindings": {},
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step_id": "step_01_incoming_total_no_counterparties",
|
||||||
|
"title": "Incoming money total by organization without counterparty split",
|
||||||
|
"question": "Сколько входящих денег за 2020 год по ООО Альтернатива Плюс без разреза по контрагентам?",
|
||||||
|
"allowed_reply_types": ["partial_coverage", "factual_with_explanation", "factual"],
|
||||||
|
"expected_mcp_discovery_response_applied": true,
|
||||||
|
"expected_mcp_discovery_selected_chain_id": "value_flow",
|
||||||
|
"expected_mcp_discovery_response_candidate_status": "ready_for_guarded_use",
|
||||||
|
"expected_mcp_discovery_candidate_hot_runtime_wired": true,
|
||||||
|
"expected_mcp_discovery_hot_runtime_wired": true,
|
||||||
|
"expected_mcp_discovery_execution_handoff_status": "ready_for_guarded_response",
|
||||||
|
"expected_mcp_discovery_execution_handoff_can_use_guarded_response": true,
|
||||||
|
"expected_catalog_alignment_status": "selected_matches_top",
|
||||||
|
"expected_catalog_chain_top_match": "value_flow",
|
||||||
|
"expected_catalog_selected_matches_top": true,
|
||||||
|
"required_answer_patterns_all": ["2020", "47[\\s.]*628[\\s.]*853", "входящ|поступ|получ", "руб"],
|
||||||
|
"forbidden_answer_patterns": ["Учтено строк", "Первая найденная дата", "последняя:", "runtime_", "planner_", "query_movements", "primitive", "уточните контрагента", "по какому контрагенту"],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": ["autonomy_core", "value_flow", "incoming_total", "business_answer_quality"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_02_outgoing_total_no_counterparties",
|
||||||
|
"title": "Outgoing money total by organization without counterparty split",
|
||||||
|
"question": "Сколько исходящих денег за 2020 год по ООО Альтернатива Плюс без разреза по контрагентам?",
|
||||||
|
"allowed_reply_types": ["partial_coverage", "factual_with_explanation", "factual"],
|
||||||
|
"expected_mcp_discovery_response_applied": true,
|
||||||
|
"expected_mcp_discovery_selected_chain_id": "value_flow",
|
||||||
|
"expected_mcp_discovery_response_candidate_status": "ready_for_guarded_use",
|
||||||
|
"expected_mcp_discovery_candidate_hot_runtime_wired": true,
|
||||||
|
"expected_mcp_discovery_hot_runtime_wired": true,
|
||||||
|
"expected_mcp_discovery_execution_handoff_status": "ready_for_guarded_response",
|
||||||
|
"expected_mcp_discovery_execution_handoff_can_use_guarded_response": true,
|
||||||
|
"expected_catalog_alignment_status": "selected_matches_top",
|
||||||
|
"expected_catalog_chain_top_match": "value_flow",
|
||||||
|
"expected_catalog_selected_matches_top": true,
|
||||||
|
"required_answer_patterns_all": ["2020", "исходящ|списан|заплат", "руб"],
|
||||||
|
"required_answer_patterns_any": ["43[\\s.]*763[\\s.]*351", "не полностью покрыт|лимит|нужно дозапрос"],
|
||||||
|
"forbidden_answer_patterns": ["Учтено строк", "Первая найденная дата", "последняя:", "runtime_", "planner_", "query_movements", "primitive", "уточните контрагента", "по какому контрагенту"],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": ["autonomy_core", "value_flow", "outgoing_total", "business_answer_quality", "limit_honesty"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_03_colloquial_incoming_without_tops",
|
||||||
|
"title": "Colloquial no-top incoming wording stays value-flow total",
|
||||||
|
"question": "А всего сколько денег пришло в ООО Альтернатива Плюс за 2020, без топов и без контрагентов?",
|
||||||
|
"allowed_reply_types": ["partial_coverage", "factual_with_explanation", "factual"],
|
||||||
|
"expected_mcp_discovery_response_applied": true,
|
||||||
|
"expected_mcp_discovery_selected_chain_id": "value_flow",
|
||||||
|
"expected_mcp_discovery_response_candidate_status": "ready_for_guarded_use",
|
||||||
|
"expected_mcp_discovery_candidate_hot_runtime_wired": true,
|
||||||
|
"expected_mcp_discovery_hot_runtime_wired": true,
|
||||||
|
"expected_mcp_discovery_execution_handoff_status": "ready_for_guarded_response",
|
||||||
|
"expected_mcp_discovery_execution_handoff_can_use_guarded_response": true,
|
||||||
|
"expected_catalog_alignment_status": "selected_matches_top",
|
||||||
|
"expected_catalog_chain_top_match": "value_flow",
|
||||||
|
"expected_catalog_selected_matches_top": true,
|
||||||
|
"required_answer_patterns_all": ["2020", "47[\\s.]*628[\\s.]*853", "пришл|получ|поступ", "руб"],
|
||||||
|
"forbidden_answer_patterns": ["топ входящ", "топ контрагент", "Учтено строк", "Первая найденная дата", "runtime_", "planner_", "query_movements", "primitive"],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": ["autonomy_core", "value_flow", "no_top_guard", "colloquial_total"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_04_cashflow_overview_2020",
|
||||||
|
"title": "Adult 2020 money overview with bank separated",
|
||||||
|
"question": "Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток.",
|
||||||
|
"allowed_reply_types": ["partial_coverage", "factual_with_explanation", "factual"],
|
||||||
|
"expected_catalog_alignment_status": "selected_matches_top",
|
||||||
|
"expected_catalog_chain_top_match": "business_overview",
|
||||||
|
"expected_catalog_selected_matches_top": true,
|
||||||
|
"required_answer_patterns_all": ["47[\\s.]*628[\\s.]*853", "43[\\s.]*763[\\s.]*351", "3[\\s.]*865[\\s.]*501", "Сбербанк|СБЕРБАНК", "финансов|банк", "Группа СВК", "Департамент"],
|
||||||
|
"required_answer_patterns_any": ["Что проверить дальше", "следующ"],
|
||||||
|
"forbidden_answer_patterns": ["Учтено строк", "Первая найденная дата", "surrogate", "VAT-объект", "runtime_", "planner_", "query_movements", "primitive"],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": ["business_overview", "cashflow_overview", "bank_boundary", "next_action"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_05_money_earned_vs_profit",
|
||||||
|
"title": "Colloquial earned money is cashflow, not clean profit",
|
||||||
|
"question": "скока денег альтернатива заработала за 20 год?",
|
||||||
|
"allowed_reply_types": ["partial_coverage", "factual_with_explanation", "factual"],
|
||||||
|
"required_answer_patterns_all": ["47[\\s.]*628[\\s.]*853", "43[\\s.]*763[\\s.]*351", "3[\\s.]*865[\\s.]*501", "не чистая прибыль|не прибыль|денежн"],
|
||||||
|
"forbidden_answer_patterns": ["Учтено строк", "Первая найденная дата", "surrogate", "runtime_", "planner_", "query_movements", "primitive"],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": ["cashflow_vs_profit", "colloquial_money"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_06_profit_followup",
|
||||||
|
"title": "Follow-up clean profit keeps cashflow/profit distinction",
|
||||||
|
"question": "а это чистая прибыль?",
|
||||||
|
"allowed_reply_types": ["partial_coverage", "factual_with_explanation", "factual"],
|
||||||
|
"required_answer_patterns_all": ["нет|не", "чистая прибыль|прибыль", "7\\s*136\\s*815|7,?136,?815|7136815", "90|91|99"],
|
||||||
|
"forbidden_answer_patterns": ["Учтено строк", "Первая найденная дата", "runtime_", "planner_", "query_movements", "primitive"],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": ["followup_context", "profit_vs_cashflow"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_07_sberbank_financial_flow",
|
||||||
|
"title": "Sberbank role is financial flow, not ordinary customer/supplier",
|
||||||
|
"question": "А отдельно по СБЕРБАНКУ: он для нас клиент, поставщик или финансовый поток? Дай коротко по подтвержденным строкам.",
|
||||||
|
"allowed_reply_types": ["partial_coverage", "factual_with_explanation", "factual"],
|
||||||
|
"required_answer_patterns_all": ["Сбербанк|СБЕРБАНК", "финансов|банк", "не.*клиент|не.*поставщик|не обычн", "36[\\s.]*258[\\s.]*835|12[\\s.]*792[\\s.]*194"],
|
||||||
|
"forbidden_answer_patterns": ["0 / 0", "T00:00:00", "Учтено строк", "Первая найденная дата", "Примеры строк", "Показаны первые", "runtime_", "planner_", "query_movements", "primitive"],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": ["bank_classification", "business_answer_quality"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_08_svk_counterparty_net_flow",
|
||||||
|
"title": "Counterparty net-flow does not become company profit",
|
||||||
|
"question": "какое нетто по деньгам с Группа СВК за 2020 год: сколько получили и сколько заплатили?",
|
||||||
|
"allowed_reply_types": ["partial_coverage", "factual_with_explanation", "factual"],
|
||||||
|
"required_answer_patterns_all": ["Группа СВК", "12[\\s.]*093[\\s.]*465", "заплатили|исходящ", "0", "нетто"],
|
||||||
|
"forbidden_answer_patterns": ["чистая прибыль", "90/91/99", "7[\\s.]*136[\\s.]*815", "Учтено строк", "Первая найденная дата", "Примеры строк", "Показаны первые", "runtime_", "planner_", "query_movements", "primitive"],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": ["counterparty_value_flow", "net_flow", "no_profit_substitution"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_09_payables_end_2020",
|
||||||
|
"title": "Payables answer stays compact and business-first",
|
||||||
|
"question": "кому мы должны на конец 2020?",
|
||||||
|
"allowed_reply_types": ["partial_coverage", "factual_with_explanation", "factual"],
|
||||||
|
"required_answer_patterns_all": ["1[\\s.]*713[\\s.]*210", "ИП Тучкова|Тучкова", "должн|кредитор"],
|
||||||
|
"forbidden_answer_patterns": ["T23:59:59", "эвристический", "shortlist", "Строк в выборке", "Учтено строк", "runtime_", "planner_", "query_movements", "primitive"],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": ["payables", "debt_answer_quality"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_10_receivables_end_2020",
|
||||||
|
"title": "Receivables answer stays compact and business-first",
|
||||||
|
"question": "а нам кто должен на конец 2020?",
|
||||||
|
"allowed_reply_types": ["partial_coverage", "factual_with_explanation", "factual"],
|
||||||
|
"required_answer_patterns_all": ["1[\\s.]*219[\\s.]*200", "СервисКонсалт", "должн|дебитор"],
|
||||||
|
"forbidden_answer_patterns": ["T23:59:59", "эвристический", "shortlist", "Строк в выборке", "Категории дебиторской задолженности", "Учтено строк", "runtime_", "planner_", "query_movements", "primitive"],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": ["receivables", "debt_answer_quality"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_11_vat_december_2019",
|
||||||
|
"title": "VAT answer is clean user-facing accounting answer",
|
||||||
|
"question": "сколько НДС надо заплатить в налоговую за декабрь 2019?",
|
||||||
|
"allowed_reply_types": ["partial_coverage", "factual_with_explanation", "factual"],
|
||||||
|
"required_answer_patterns_all": ["161[\\s.]*852", "к уплате", "книга продаж|продаж", "книга покупок|вычет|покупок", "01\\.10\\.2019|4 квартал|IV квартал"],
|
||||||
|
"forbidden_answer_patterns": ["surrogate", "VAT-объект", "прямых источников", "0,00\\s*₽", "Учтено строк", "runtime_", "planner_", "query_movements", "primitive"],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": ["vat", "technical_garbage_guard"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_12_business_evaluation",
|
||||||
|
"title": "Company evaluation is an analyst answer with next checks",
|
||||||
|
"question": "Как ты оценишь деятельность компании?",
|
||||||
|
"allowed_reply_types": ["partial_coverage", "factual_with_explanation", "factual"],
|
||||||
|
"expected_catalog_alignment_status": "selected_matches_top",
|
||||||
|
"expected_catalog_chain_top_match": "business_overview",
|
||||||
|
"expected_catalog_selected_matches_top": true,
|
||||||
|
"required_answer_patterns_all": ["крупн|контракт|проект", "285\\s*8|285\\s*823|285", "Комитет|госуслуг", "Что проверить дальше|проверить дальше|дальше"],
|
||||||
|
"forbidden_answer_patterns": ["Учтено строк", "Первая найденная дата", "проверка достигла лимита строк", "runtime_", "planner_", "query_movements", "primitive"],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": ["business_overview", "business_evaluation", "next_action"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_13_nomenclature_margin_missing_period",
|
||||||
|
"title": "Nomenclature margin ranking asks for period, not wrong accounting domain",
|
||||||
|
"question": "Какая номенклатура товара реализована с высокой прибылью какая с низкой?",
|
||||||
|
"allowed_reply_types": ["partial_coverage", "factual_with_explanation", "factual"],
|
||||||
|
"required_answer_patterns_all": ["период", "выручк|реализац", "себестоим|валов|маржин"],
|
||||||
|
"forbidden_answer_patterns": ["основн.*средств", "амортизац", "зависш", "оплат", "расчетн", "Учтено строк", "runtime_", "planner_", "query_movements", "primitive"],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": ["nomenclature_margin", "clarification", "domain_purity"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_14_nomenclature_margin_may_2020",
|
||||||
|
"title": "Nomenclature margin ranking with period gives useful limited answer",
|
||||||
|
"question": "май 2020",
|
||||||
|
"allowed_reply_types": ["partial_coverage", "factual_with_explanation", "factual"],
|
||||||
|
"required_answer_patterns_all": ["май|01\\.05\\.2020|31\\.05\\.2020", "рейтинг|прибыль|маржин", "себестоим", "нельзя|не удалось|недостаточно"],
|
||||||
|
"required_answer_patterns_any": ["показать найденную продажу", "расширить период", "90\\.01|90\\.02", "следующ"],
|
||||||
|
"forbidden_answer_patterns": ["основн.*средств", "амортизац", "зависш", "runtime_", "planner_", "query_movements", "primitive"],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": ["nomenclature_margin", "limited_answer", "next_action"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
{
|
||||||
|
"schema_version": "domain_truth_harness_spec_v1",
|
||||||
|
"scenario_id": "agent_cashflow_compact_display_variants_20260523",
|
||||||
|
"domain": "autonomy_business_answer_contract",
|
||||||
|
"title": "AGENT | Cashflow compact display variants",
|
||||||
|
"description": "Targeted AGENT replay for compact cashflow display modifiers: no counterparties, no breakdown, one-line and only-total follow-ups must preserve the 2020 company cashflow context.",
|
||||||
|
"bindings": {},
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step_id": "step_01_explicit_overview_anchor",
|
||||||
|
"title": "Create 2020 business overview context",
|
||||||
|
"question": "Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток.",
|
||||||
|
"allowed_reply_types": ["partial_coverage", "factual_with_explanation", "factual"],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"47[\\s.]*628[\\s.]*853",
|
||||||
|
"43[\\s.]*763[\\s.]*351",
|
||||||
|
"3[\\s.]*865[\\s.]*501",
|
||||||
|
"12[\\s.]*792[\\s.]*194",
|
||||||
|
"12[\\s.]*093[\\s.]*465"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": ["runtime_", "planner_", "query_movements", "primitive"],
|
||||||
|
"criticality": "high",
|
||||||
|
"semantic_tags": ["business_overview", "context_anchor"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_02_no_counterparties",
|
||||||
|
"title": "No-counterparties means no breakdown, not exclusion",
|
||||||
|
"question": "сколько заработали деньгами без контрагентов?",
|
||||||
|
"allowed_reply_types": ["partial_coverage", "factual_with_explanation", "factual"],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"2020",
|
||||||
|
"47[\\s.]*628[\\s.]*853",
|
||||||
|
"43[\\s.]*763[\\s.]*351",
|
||||||
|
"3[\\s.]*865[\\s.]*501"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"2026-05-23",
|
||||||
|
"получили\\s+0\\s*руб",
|
||||||
|
"нетто\\s+0\\s*руб",
|
||||||
|
"с исключением",
|
||||||
|
"исключением крупнейших",
|
||||||
|
"Комитет государственных услуг",
|
||||||
|
"Группа СВК",
|
||||||
|
"СБЕРБАНК",
|
||||||
|
"Что проверить дальше",
|
||||||
|
"runtime_",
|
||||||
|
"planner_",
|
||||||
|
"query_movements",
|
||||||
|
"primitive"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": ["no_counterparty_breakdown", "display_modifier", "temporal_carryover"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_03_only_total",
|
||||||
|
"title": "Only total keeps compact cashflow",
|
||||||
|
"question": "только итог: пришло, ушло, нетто",
|
||||||
|
"allowed_reply_types": ["partial_coverage", "factual_with_explanation", "factual"],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"2020",
|
||||||
|
"47[\\s.]*628[\\s.]*853",
|
||||||
|
"43[\\s.]*763[\\s.]*351",
|
||||||
|
"3[\\s.]*865[\\s.]*501"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"2026-05-23",
|
||||||
|
"получили\\s+0\\s*руб",
|
||||||
|
"нетто\\s+0\\s*руб",
|
||||||
|
"Комитет государственных услуг",
|
||||||
|
"Группа СВК",
|
||||||
|
"СБЕРБАНК",
|
||||||
|
"Что проверить дальше",
|
||||||
|
"runtime_",
|
||||||
|
"planner_",
|
||||||
|
"query_movements",
|
||||||
|
"primitive"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": ["only_total", "display_modifier", "temporal_carryover"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_04_one_line_no_breakdown",
|
||||||
|
"title": "One-line no-breakdown stays cashflow",
|
||||||
|
"question": "дай одной строкой деньги без разбивки",
|
||||||
|
"allowed_reply_types": ["partial_coverage", "factual_with_explanation", "factual"],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"2020",
|
||||||
|
"47[\\s.]*628[\\s.]*853",
|
||||||
|
"43[\\s.]*763[\\s.]*351",
|
||||||
|
"3[\\s.]*865[\\s.]*501"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"2026-05-23",
|
||||||
|
"получили\\s+0\\s*руб",
|
||||||
|
"нетто\\s+0\\s*руб",
|
||||||
|
"Комитет государственных услуг",
|
||||||
|
"Группа СВК",
|
||||||
|
"СБЕРБАНК",
|
||||||
|
"Что проверить дальше",
|
||||||
|
"runtime_",
|
||||||
|
"planner_",
|
||||||
|
"query_movements",
|
||||||
|
"primitive"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": ["one_line", "no_breakdown", "display_modifier"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_05_no_details",
|
||||||
|
"title": "No-details wording remains direct totals",
|
||||||
|
"question": "сколько получили и заплатили без детализации",
|
||||||
|
"allowed_reply_types": ["partial_coverage", "factual_with_explanation", "factual"],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"2020",
|
||||||
|
"47[\\s.]*628[\\s.]*853",
|
||||||
|
"43[\\s.]*763[\\s.]*351",
|
||||||
|
"3[\\s.]*865[\\s.]*501"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"2026-05-23",
|
||||||
|
"получили\\s+0\\s*руб",
|
||||||
|
"нетто\\s+0\\s*руб",
|
||||||
|
"Комитет государственных услуг",
|
||||||
|
"Группа СВК",
|
||||||
|
"СБЕРБАНК",
|
||||||
|
"Что проверить дальше",
|
||||||
|
"runtime_",
|
||||||
|
"planner_",
|
||||||
|
"query_movements",
|
||||||
|
"primitive"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": ["no_details", "display_modifier", "direct_totals"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"acceptance": {
|
||||||
|
"min_score": 80,
|
||||||
|
"max_unresolved_p0": 0,
|
||||||
|
"require_all_critical_steps_pass": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
{
|
||||||
|
"schema_version": "domain_truth_harness_spec_v1",
|
||||||
|
"scenario_id": "agent_cashflow_no_tops_20260523",
|
||||||
|
"domain": "autonomy_business_answer_contract",
|
||||||
|
"title": "AGENT | Cashflow no-tops display modifier",
|
||||||
|
"description": "Targeted AGENT replay: after a 2020 business overview, a compact 'без топов' follow-up must preserve the 2020 company cashflow context and suppress rankings instead of excluding top counterparties.",
|
||||||
|
"bindings": {},
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step_id": "step_01_direct_money_2020",
|
||||||
|
"title": "Direct cashflow baseline for 2020",
|
||||||
|
"question": "Сколько денег Альтернатива заработала за 2020 год? Ответь коротко: получили, заплатили, денежное нетто, это прибыль или нет.",
|
||||||
|
"allowed_reply_types": ["partial_coverage", "factual_with_explanation", "factual"],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"47[\\s.]*628[\\s.]*853",
|
||||||
|
"43[\\s.]*763[\\s.]*351",
|
||||||
|
"3[\\s.]*865[\\s.]*501"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"Учтено строк",
|
||||||
|
"Первая найденная дата",
|
||||||
|
"runtime_",
|
||||||
|
"planner_",
|
||||||
|
"query_movements",
|
||||||
|
"primitive",
|
||||||
|
"7\\s*136\\s*815|7136815"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": ["cashflow", "direct_answer", "baseline"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_02_explicit_overview_with_tops",
|
||||||
|
"title": "Explicit overview may include top counterparties",
|
||||||
|
"question": "Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток.",
|
||||||
|
"allowed_reply_types": ["partial_coverage", "factual_with_explanation", "factual"],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"47[\\s.]*628[\\s.]*853",
|
||||||
|
"43[\\s.]*763[\\s.]*351",
|
||||||
|
"3[\\s.]*865[\\s.]*501",
|
||||||
|
"12[\\s.]*792[\\s.]*194",
|
||||||
|
"12[\\s.]*093[\\s.]*465",
|
||||||
|
"9[\\s.]*612[\\s.]*904"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"Учтено строк",
|
||||||
|
"Первая найденная дата",
|
||||||
|
"runtime_",
|
||||||
|
"planner_",
|
||||||
|
"query_movements",
|
||||||
|
"primitive"
|
||||||
|
],
|
||||||
|
"criticality": "high",
|
||||||
|
"semantic_tags": ["business_overview", "tops_allowed", "bank_boundary"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_03_compact_no_tops_followup",
|
||||||
|
"title": "No-tops compact follow-up preserves 2020 cashflow",
|
||||||
|
"question": "а если коротко, сколько заработали деньгами без топов?",
|
||||||
|
"allowed_reply_types": ["partial_coverage", "factual_with_explanation", "factual"],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"2020",
|
||||||
|
"47[\\s.]*628[\\s.]*853",
|
||||||
|
"43[\\s.]*763[\\s.]*351",
|
||||||
|
"3[\\s.]*865[\\s.]*501"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"2026-05-23",
|
||||||
|
"получили\\s+0\\s*руб",
|
||||||
|
"заплатили(?:/списали)?\\s+0\\s*руб",
|
||||||
|
"нетто\\s+0\\s*руб",
|
||||||
|
"с исключением крупнейших",
|
||||||
|
"исключением крупнейших",
|
||||||
|
"Комитет государственных услуг",
|
||||||
|
"Группа СВК",
|
||||||
|
"СБЕРБАНК",
|
||||||
|
"Что проверить дальше",
|
||||||
|
"Учтено строк",
|
||||||
|
"Первая найденная дата",
|
||||||
|
"runtime_",
|
||||||
|
"planner_",
|
||||||
|
"query_movements",
|
||||||
|
"primitive"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": ["compact_after_overview", "no_tops_display_modifier", "temporal_carryover"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_04_plain_money_in_out_net",
|
||||||
|
"title": "Explicit plain money request remains compact",
|
||||||
|
"question": "не обзор, просто деньги: пришло, ушло, нетто за 2020",
|
||||||
|
"allowed_reply_types": ["partial_coverage", "factual_with_explanation", "factual"],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"47[\\s.]*628[\\s.]*853",
|
||||||
|
"43[\\s.]*763[\\s.]*351",
|
||||||
|
"3[\\s.]*865[\\s.]*501"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"Комитет государственных услуг",
|
||||||
|
"Группа СВК",
|
||||||
|
"СБЕРБАНК",
|
||||||
|
"Что проверить дальше",
|
||||||
|
"Учтено строк",
|
||||||
|
"Первая найденная дата",
|
||||||
|
"runtime_",
|
||||||
|
"planner_",
|
||||||
|
"query_movements",
|
||||||
|
"primitive"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": ["direct_money_only", "ranking_suppression"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"acceptance": {
|
||||||
|
"min_score": 80,
|
||||||
|
"max_unresolved_p0": 0,
|
||||||
|
"require_all_critical_steps_pass": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,266 @@
|
||||||
|
{
|
||||||
|
"schema_version": "domain_truth_harness_spec_v1",
|
||||||
|
"scenario_id": "agent_cashflow_profit_directness_20260523",
|
||||||
|
"domain": "autonomy_business_answer_contract",
|
||||||
|
"title": "AGENT | Cashflow vs profit directness pack",
|
||||||
|
"description": "Targeted AGENT replay for cashflow-vs-profit directness: colloquial earnings questions must produce a compact money answer, while broad overview remains available only when explicitly requested.",
|
||||||
|
"bindings": {
|
||||||
|
|
||||||
|
},
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step_id": "step_01_direct_money_earned_explicit_shape",
|
||||||
|
"title": "Direct money earned question asks for compact cashflow shape",
|
||||||
|
"question": "Сколько денег Альтернатива заработала за 2020 год? Ответь коротко: получили, заплатили, денежное нетто, это прибыль или нет.",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"partial_coverage",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"47[\\s.]*628[\\s.]*853",
|
||||||
|
"43[\\s.]*763[\\s.]*351",
|
||||||
|
"3[\\s.]*865[\\s.]*501"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"Учтено строк",
|
||||||
|
"Первая найденная дата",
|
||||||
|
"runtime_",
|
||||||
|
"planner_",
|
||||||
|
"query_movements",
|
||||||
|
"primitive",
|
||||||
|
"7\\s*136\\s*815|7136815",
|
||||||
|
"Комитет государственных услуг"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"cashflow_vs_profit",
|
||||||
|
"direct_answer",
|
||||||
|
"colloquial_earnings"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_02_colloquial_money_earned",
|
||||||
|
"title": "Colloquial earned wording stays cashflow, not broad overview",
|
||||||
|
"question": "скока денег альтернатива заработала за 20 год?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"partial_coverage",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"47[\\s.]*628[\\s.]*853",
|
||||||
|
"43[\\s.]*763[\\s.]*351",
|
||||||
|
"3[\\s.]*865[\\s.]*501"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"Учтено строк",
|
||||||
|
"Первая найденная дата",
|
||||||
|
"runtime_",
|
||||||
|
"planner_",
|
||||||
|
"query_movements",
|
||||||
|
"primitive",
|
||||||
|
"7\\s*136\\s*815|7136815",
|
||||||
|
"Комитет государственных услуг",
|
||||||
|
"Что проверить дальше"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"cashflow_vs_profit",
|
||||||
|
"slang_wording",
|
||||||
|
"no_overview_substitution"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_03_profit_followup_after_cashflow",
|
||||||
|
"title": "Clean profit follow-up keeps distinction",
|
||||||
|
"question": "а это чистая прибыль?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"partial_coverage",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"7\\s*136\\s*815|7136815",
|
||||||
|
"90|91|99"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"Учтено строк",
|
||||||
|
"Первая найденная дата",
|
||||||
|
"runtime_",
|
||||||
|
"planner_",
|
||||||
|
"query_movements",
|
||||||
|
"primitive"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"profit_vs_cashflow",
|
||||||
|
"followup_context"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_04_money_plus_or_minus_followup",
|
||||||
|
"title": "Money plus/minus follow-up returns cashflow net",
|
||||||
|
"question": "а по деньгам тогда плюс или минус?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"partial_coverage",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"\\+",
|
||||||
|
"47[\\s.]*628[\\s.]*853",
|
||||||
|
"43[\\s.]*763[\\s.]*351",
|
||||||
|
"3[\\s.]*865[\\s.]*501"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"Учтено строк",
|
||||||
|
"Первая найденная дата",
|
||||||
|
"runtime_",
|
||||||
|
"planner_",
|
||||||
|
"query_movements",
|
||||||
|
"primitive",
|
||||||
|
"7\\s*136\\s*815|7136815"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"cashflow_polarity",
|
||||||
|
"followup_context",
|
||||||
|
"no_profit_substitution"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_05_explicit_business_overview_allowed",
|
||||||
|
"title": "Explicit adult overview may include business structure and next steps",
|
||||||
|
"question": "Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток.",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"partial_coverage",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"47[\\s.]*628[\\s.]*853",
|
||||||
|
"43[\\s.]*763[\\s.]*351",
|
||||||
|
"3[\\s.]*865[\\s.]*501",
|
||||||
|
"12[\\s.]*792[\\s.]*194",
|
||||||
|
"12[\\s.]*093[\\s.]*465",
|
||||||
|
"9[\\s.]*612[\\s.]*904"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"Учтено строк",
|
||||||
|
"Первая найденная дата",
|
||||||
|
"runtime_",
|
||||||
|
"planner_",
|
||||||
|
"query_movements",
|
||||||
|
"primitive"
|
||||||
|
],
|
||||||
|
"criticality": "high",
|
||||||
|
"semantic_tags": [
|
||||||
|
"business_overview",
|
||||||
|
"bank_flow_boundary",
|
||||||
|
"next_steps"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_06_return_to_short_money_after_overview",
|
||||||
|
"title": "Short money follow-up after overview keeps 2020 context",
|
||||||
|
"question": "а если коротко, сколько заработали деньгами без топов?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"partial_coverage",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"2020",
|
||||||
|
"47[\\s.]*628[\\s.]*853",
|
||||||
|
"43[\\s.]*763[\\s.]*351",
|
||||||
|
"3[\\s.]*865[\\s.]*501"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"Учтено строк",
|
||||||
|
"Первая найденная дата",
|
||||||
|
"runtime_",
|
||||||
|
"planner_",
|
||||||
|
"query_movements",
|
||||||
|
"primitive",
|
||||||
|
"285[\\s.]*819[\\s.]*547",
|
||||||
|
"147[\\s.]*855[\\s.]*827",
|
||||||
|
"Комитет государственных услуг",
|
||||||
|
"Что проверить дальше"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"compact_after_overview",
|
||||||
|
"temporal_carryover",
|
||||||
|
"no_all_time_leak"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_07_plain_money_in_out_net",
|
||||||
|
"title": "Plain money request suppresses broad overview",
|
||||||
|
"question": "не обзор, просто деньги: пришло, ушло, нетто за 2020",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"partial_coverage",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"47[\\s.]*628[\\s.]*853",
|
||||||
|
"43[\\s.]*763[\\s.]*351",
|
||||||
|
"3[\\s.]*865[\\s.]*501"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"Учтено строк",
|
||||||
|
"Первая найденная дата",
|
||||||
|
"runtime_",
|
||||||
|
"planner_",
|
||||||
|
"query_movements",
|
||||||
|
"primitive",
|
||||||
|
"Комитет государственных услуг",
|
||||||
|
"12[\\s.]*093[\\s.]*465",
|
||||||
|
"12[\\s.]*792[\\s.]*194",
|
||||||
|
"Что проверить дальше"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"direct_money_only",
|
||||||
|
"ranking_suppression"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_08_direct_clean_profit_2020",
|
||||||
|
"title": "Explicit clean profit goes to accounting result, not cashflow",
|
||||||
|
"question": "какая чистая прибыль по Альтернативе за 2020?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"partial_coverage",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"7\\s*136\\s*815|7136815",
|
||||||
|
"90|91|99"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"Учтено строк",
|
||||||
|
"Первая найденная дата",
|
||||||
|
"runtime_",
|
||||||
|
"planner_",
|
||||||
|
"query_movements",
|
||||||
|
"primitive",
|
||||||
|
"получили\\s+47[\\s.]*628[\\s.]*853",
|
||||||
|
"денежное\\s+нетто\\s+3[\\s.]*865[\\s.]*501"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"clean_profit",
|
||||||
|
"profit_route",
|
||||||
|
"no_cashflow_substitution"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"acceptance": {
|
||||||
|
"min_score": 80,
|
||||||
|
"max_unresolved_p0": 0,
|
||||||
|
"require_all_critical_steps_pass": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,157 @@
|
||||||
|
{
|
||||||
|
"schema_version": "domain_truth_harness_spec_v1",
|
||||||
|
"scenario_id": "agent_hot_value_flow_handoff_20260522",
|
||||||
|
"domain": "autonomy_hot_value_flow_handoff",
|
||||||
|
"title": "AGENT | Hot value-flow discovery handoff",
|
||||||
|
"description": "Targeted AGENT replay for the current Autonomy Core slice: organization-scoped value-flow questions must be answered through guarded MCP discovery response with hot handoff, not through stale exact fallback or a counterparty-only route.",
|
||||||
|
"bindings": {},
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step_id": "step_01_incoming_total_hot_handoff",
|
||||||
|
"title": "Organization-scoped incoming total uses hot value-flow discovery candidate",
|
||||||
|
"question": "Сколько входящих денег за 2020 год по ООО Альтернатива Плюс без разреза по контрагентам?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"partial_coverage",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"expected_mcp_discovery_response_applied": true,
|
||||||
|
"expected_mcp_discovery_selected_chain_id": "value_flow",
|
||||||
|
"expected_mcp_discovery_response_candidate_status": "ready_for_guarded_use",
|
||||||
|
"expected_mcp_discovery_candidate_hot_runtime_wired": true,
|
||||||
|
"expected_mcp_discovery_hot_runtime_wired": true,
|
||||||
|
"expected_mcp_discovery_execution_handoff_status": "ready_for_guarded_response",
|
||||||
|
"expected_mcp_discovery_execution_handoff_can_use_guarded_response": true,
|
||||||
|
"expected_catalog_alignment_status": "selected_matches_top",
|
||||||
|
"expected_catalog_chain_top_match": "value_flow",
|
||||||
|
"expected_catalog_selected_matches_top": true,
|
||||||
|
"expected_route_candidate_status": "ready_for_reviewed_execution",
|
||||||
|
"expected_route_candidate_executable_now": true,
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"(?i)2020",
|
||||||
|
"(?i)входящ|получ|поступ",
|
||||||
|
"(?i)руб"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_any": [
|
||||||
|
"(?i)Альтернатива",
|
||||||
|
"(?i)проверенн",
|
||||||
|
"(?i)1С"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"(?i)уточните контрагента",
|
||||||
|
"(?i)по какому контрагенту",
|
||||||
|
"(?i)не найден контрагент",
|
||||||
|
"(?i)runtime_",
|
||||||
|
"(?i)planner_",
|
||||||
|
"(?i)query_movements",
|
||||||
|
"(?i)primitive"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"autonomy_core",
|
||||||
|
"value_flow",
|
||||||
|
"hot_handoff",
|
||||||
|
"guarded_response",
|
||||||
|
"incoming_total"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_02_outgoing_total_hot_handoff",
|
||||||
|
"title": "Organization-scoped outgoing total uses hot value-flow discovery candidate",
|
||||||
|
"question": "Сколько исходящих денег за 2020 год по ООО Альтернатива Плюс без разреза по контрагентам?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"partial_coverage",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"expected_mcp_discovery_response_applied": true,
|
||||||
|
"expected_mcp_discovery_selected_chain_id": "value_flow",
|
||||||
|
"expected_mcp_discovery_response_candidate_status": "ready_for_guarded_use",
|
||||||
|
"expected_mcp_discovery_candidate_hot_runtime_wired": true,
|
||||||
|
"expected_mcp_discovery_hot_runtime_wired": true,
|
||||||
|
"expected_mcp_discovery_execution_handoff_status": "ready_for_guarded_response",
|
||||||
|
"expected_mcp_discovery_execution_handoff_can_use_guarded_response": true,
|
||||||
|
"expected_catalog_alignment_status": "selected_matches_top",
|
||||||
|
"expected_catalog_chain_top_match": "value_flow",
|
||||||
|
"expected_catalog_selected_matches_top": true,
|
||||||
|
"expected_route_candidate_status": "ready_for_reviewed_execution",
|
||||||
|
"expected_route_candidate_executable_now": true,
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"(?i)2020",
|
||||||
|
"(?i)исходящ|списан|заплат",
|
||||||
|
"(?i)руб"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_any": [
|
||||||
|
"(?i)Альтернатива",
|
||||||
|
"(?i)проверенн",
|
||||||
|
"(?i)1С"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"(?i)уточните контрагента",
|
||||||
|
"(?i)по какому контрагенту",
|
||||||
|
"(?i)не найден контрагент",
|
||||||
|
"(?i)runtime_",
|
||||||
|
"(?i)planner_",
|
||||||
|
"(?i)query_movements",
|
||||||
|
"(?i)primitive"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"autonomy_core",
|
||||||
|
"value_flow",
|
||||||
|
"hot_handoff",
|
||||||
|
"guarded_response",
|
||||||
|
"outgoing_total"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_03_colloquial_money_total_hot_handoff",
|
||||||
|
"title": "Colloquial company money wording still uses hot value-flow discovery candidate",
|
||||||
|
"question": "А всего сколько денег пришло в ООО Альтернатива Плюс за 2020, без топов и без контрагентов?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"partial_coverage",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"expected_mcp_discovery_response_applied": true,
|
||||||
|
"expected_mcp_discovery_selected_chain_id": "value_flow",
|
||||||
|
"expected_mcp_discovery_response_candidate_status": "ready_for_guarded_use",
|
||||||
|
"expected_mcp_discovery_candidate_hot_runtime_wired": true,
|
||||||
|
"expected_mcp_discovery_hot_runtime_wired": true,
|
||||||
|
"expected_mcp_discovery_execution_handoff_status": "ready_for_guarded_response",
|
||||||
|
"expected_mcp_discovery_execution_handoff_can_use_guarded_response": true,
|
||||||
|
"expected_catalog_alignment_status": "selected_matches_top",
|
||||||
|
"expected_catalog_chain_top_match": "value_flow",
|
||||||
|
"expected_catalog_selected_matches_top": true,
|
||||||
|
"expected_route_candidate_status": "ready_for_reviewed_execution",
|
||||||
|
"expected_route_candidate_executable_now": true,
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"(?i)2020",
|
||||||
|
"(?i)пришл|получ|поступ|входящ",
|
||||||
|
"(?i)руб"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_any": [
|
||||||
|
"(?i)Альтернатива",
|
||||||
|
"(?i)проверенн",
|
||||||
|
"(?i)1С"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"(?i)уточните контрагента",
|
||||||
|
"(?i)по какому контрагенту",
|
||||||
|
"(?i)не найден контрагент",
|
||||||
|
"(?i)runtime_",
|
||||||
|
"(?i)planner_",
|
||||||
|
"(?i)query_movements",
|
||||||
|
"(?i)primitive"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"autonomy_core",
|
||||||
|
"value_flow",
|
||||||
|
"hot_handoff",
|
||||||
|
"guarded_response",
|
||||||
|
"colloquial_total"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,198 @@
|
||||||
|
{
|
||||||
|
"schema_version": "domain_truth_harness_spec_v1",
|
||||||
|
"scenario_id": "agent_inventory_margin_ranking_20260523",
|
||||||
|
"domain": "inventory_margin_ranking",
|
||||||
|
"title": "AGENT | Inventory margin ranking limited answer pack",
|
||||||
|
"description": "Targeted live replay for nomenclature high/low profit questions: ask period when missing, stay in inventory/sales/cost domain, give useful limited answer when cost evidence is insufficient, and avoid fixed-assets/bank leakage.",
|
||||||
|
"bindings": {
|
||||||
|
|
||||||
|
},
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step_id": "step_01_margin_root_needs_period",
|
||||||
|
"title": "Root asks nomenclature high/low profit without period",
|
||||||
|
"question": "Какая номеклатура товара реализована с высокой прибылью какая с низкой",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"partial_coverage",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"период|месяц|квартал|год",
|
||||||
|
"номенклатур",
|
||||||
|
"выручк|себестоим|валов|маржин"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"амортизац",
|
||||||
|
"основн[а-я]+ средств",
|
||||||
|
"\\bОС\\s+как\\s+объект\\b",
|
||||||
|
"Сбербанк",
|
||||||
|
"банк",
|
||||||
|
"зависш[а-я]+ оплат",
|
||||||
|
"payment",
|
||||||
|
"settlement cluster",
|
||||||
|
"runtime_",
|
||||||
|
"planner_",
|
||||||
|
"query_movements",
|
||||||
|
"primitive",
|
||||||
|
"90/91/99",
|
||||||
|
"7\\s*136\\s*815"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"inventory_margin_ranking",
|
||||||
|
"needs_period",
|
||||||
|
"domain_purity"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_02_may_2020_period_limited_answer",
|
||||||
|
"title": "User provides May 2020 period and gets useful limited answer",
|
||||||
|
"question": "май 2020",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"partial_coverage",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"май|01\\.05\\.2020|31\\.05\\.2020|2020",
|
||||||
|
"рейтинг|прибыль|маржин",
|
||||||
|
"себестоим|90\\.02|закупоч",
|
||||||
|
"нельзя|не удалось|недостаточ|не подтвержд"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"амортизац",
|
||||||
|
"основн[а-я]+ средств",
|
||||||
|
"\\bОС\\s+как\\s+объект\\b",
|
||||||
|
"Сбербанк",
|
||||||
|
"банк",
|
||||||
|
"зависш[а-я]+ оплат",
|
||||||
|
"payment",
|
||||||
|
"settlement cluster",
|
||||||
|
"runtime_",
|
||||||
|
"planner_",
|
||||||
|
"query_movements",
|
||||||
|
"primitive",
|
||||||
|
"7\\s*136\\s*815"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"inventory_margin_ranking",
|
||||||
|
"limited_answer",
|
||||||
|
"period_followup"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_03_show_cost_base_lines",
|
||||||
|
"title": "Follow-up asks for found cost-base evidence",
|
||||||
|
"question": "покажи найденные строки себестоимостной базы",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"partial_coverage",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_any": [
|
||||||
|
"себестоим",
|
||||||
|
"закупоч",
|
||||||
|
"90\\.02",
|
||||||
|
"41",
|
||||||
|
"не найден|не подтвержд|нет"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"амортизац",
|
||||||
|
"основн[а-я]+ средств",
|
||||||
|
"\\bОС\\s+как\\s+объект\\b",
|
||||||
|
"Сбербанк",
|
||||||
|
"банк",
|
||||||
|
"зависш[а-я]+ оплат",
|
||||||
|
"payment",
|
||||||
|
"settlement cluster",
|
||||||
|
"runtime_",
|
||||||
|
"planner_",
|
||||||
|
"query_movements",
|
||||||
|
"primitive",
|
||||||
|
"7\\s*136\\s*815"
|
||||||
|
],
|
||||||
|
"criticality": "high",
|
||||||
|
"semantic_tags": [
|
||||||
|
"inventory_margin_ranking",
|
||||||
|
"evidence_followup",
|
||||||
|
"carryover"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_04_expand_to_2017",
|
||||||
|
"title": "User expands period to full 2017",
|
||||||
|
"question": "расширь до 2017 года",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"partial_coverage",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_any": [
|
||||||
|
"2017",
|
||||||
|
"рейтинг|маржин|прибыль",
|
||||||
|
"себестоим|90\\.02|закупоч",
|
||||||
|
"нельзя|не подтвержд|найден"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"амортизац",
|
||||||
|
"основн[а-я]+ средств",
|
||||||
|
"\\bОС\\s+как\\s+объект\\b",
|
||||||
|
"Сбербанк",
|
||||||
|
"банк",
|
||||||
|
"зависш[а-я]+ оплат",
|
||||||
|
"payment",
|
||||||
|
"settlement cluster",
|
||||||
|
"runtime_",
|
||||||
|
"planner_",
|
||||||
|
"query_movements",
|
||||||
|
"primitive",
|
||||||
|
"7\\s*136\\s*815"
|
||||||
|
],
|
||||||
|
"criticality": "high",
|
||||||
|
"semantic_tags": [
|
||||||
|
"inventory_margin_ranking",
|
||||||
|
"period_expansion",
|
||||||
|
"carryover"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_05_account_41_not_01",
|
||||||
|
"title": "User corrects account family to 41 not fixed assets",
|
||||||
|
"question": "анализ по 41 счету а не 01",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"partial_coverage",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_any": [
|
||||||
|
"41",
|
||||||
|
"товар|номенклатур|закупоч|себестоим",
|
||||||
|
"не 01|01"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"амортизац",
|
||||||
|
"основн[а-я]+ средств",
|
||||||
|
"ОС как объект",
|
||||||
|
"Сбербанк",
|
||||||
|
"банк",
|
||||||
|
"runtime_",
|
||||||
|
"planner_",
|
||||||
|
"query_movements",
|
||||||
|
"primitive"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"inventory_margin_ranking",
|
||||||
|
"account_family_guard",
|
||||||
|
"no_fixed_assets"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"acceptance": {
|
||||||
|
"min_score": 80,
|
||||||
|
"max_unresolved_p0": 0,
|
||||||
|
"require_all_critical_steps_pass": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
{
|
||||||
|
"schema_version": "domain_truth_harness_spec_v1",
|
||||||
|
"scenario_id": "agent_profit_direct_answer_20260523",
|
||||||
|
"domain": "cashflow_profit_boundary",
|
||||||
|
"title": "AGENT | Direct net profit answer shape",
|
||||||
|
"description": "Targeted replay for direct clean-profit questions: follow-up boundary may say cashflow is not profit, but a standalone net-profit question must answer with the accounting result first.",
|
||||||
|
"bindings": {},
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step_id": "step_01_cashflow_money_not_profit",
|
||||||
|
"title": "Cashflow question asks money and profit boundary",
|
||||||
|
"question": "Сколько денег Альтернатива заработала за 2020 год? Ответь коротко: получили, заплатили, денежное нетто, это прибыль или нет.",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"partial_coverage",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"47\\s*628\\s*853,03",
|
||||||
|
"43\\s*763\\s*351,53",
|
||||||
|
"3\\s*865\\s*501,50",
|
||||||
|
"не\\s+чистая\\s+прибыль|не\\s+бухгалтерский\\s+финрезультат"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"7\\s*136\\s*815,85.*получили",
|
||||||
|
"runtime_",
|
||||||
|
"planner_",
|
||||||
|
"primitive"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"cashflow",
|
||||||
|
"profit_boundary"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_02_followup_this_is_profit",
|
||||||
|
"title": "Follow-up asks whether cashflow is clean profit",
|
||||||
|
"question": "а это чистая прибыль?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"partial_coverage",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"нет",
|
||||||
|
"90/91/99",
|
||||||
|
"7\\s*136\\s*815,85",
|
||||||
|
"убыт"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"получили\\s+47\\s*628\\s*853,03.*чистая\\s+прибыль",
|
||||||
|
"runtime_",
|
||||||
|
"planner_",
|
||||||
|
"primitive"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"profit_boundary",
|
||||||
|
"followup"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_03_direct_net_profit_question",
|
||||||
|
"title": "Standalone direct net profit question answers with result first",
|
||||||
|
"question": "какая чистая прибыль по Альтернативе за 2020?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"partial_coverage",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"90/91/99",
|
||||||
|
"7\\s*136\\s*815,85",
|
||||||
|
"убыт",
|
||||||
|
"не\\s+денежный\\s+поток|учетный\\s+финрезультат"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"Коротко:\\s*нет,\\s*денежное\\s+операционное\\s+нетто",
|
||||||
|
"runtime_",
|
||||||
|
"planner_",
|
||||||
|
"primitive"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"direct_profit",
|
||||||
|
"answer_shape"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"acceptance": {
|
||||||
|
"min_score": 80,
|
||||||
|
"max_unresolved_p0": 0,
|
||||||
|
"require_all_critical_steps_pass": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
{
|
||||||
|
"schema_version": "domain_scenario_manifest_v1",
|
||||||
|
"scenario_id": "inventory_margin_ranking_agent_loop_20260522",
|
||||||
|
"domain": "inventory_margin_ranking",
|
||||||
|
"title": "Inventory nomenclature margin ranking limited-answer loop",
|
||||||
|
"description": "Shared-session replay for nomenclature profitability ranking: missing period clarification, period-scoped insufficient realization/cost evidence, and follow-up action quality.",
|
||||||
|
"acceptance_canon": {
|
||||||
|
"root_step_id": "step_01_margin_root_needs_period",
|
||||||
|
"primary_user_path": [
|
||||||
|
"step_01_margin_root_needs_period",
|
||||||
|
"step_02_september_2017_period",
|
||||||
|
"step_03_show_cost_base_lines",
|
||||||
|
"step_04_expand_to_2017",
|
||||||
|
"step_05_account_41_not_01"
|
||||||
|
],
|
||||||
|
"required_paraphrase_families": [
|
||||||
|
"colloquial",
|
||||||
|
"canonical"
|
||||||
|
],
|
||||||
|
"required_carryover_invariants": [
|
||||||
|
"intent_scope",
|
||||||
|
"period_scope",
|
||||||
|
"organization_scope",
|
||||||
|
"answer_shape"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"analysis_context": {
|
||||||
|
"as_of_date": "2026-05-22",
|
||||||
|
"source": "manual_export_and_codex_agent_loop"
|
||||||
|
},
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step_id": "step_01_margin_root_needs_period",
|
||||||
|
"title": "Root asks nomenclature high/low profit without period",
|
||||||
|
"question": "Какая номеклатура товара реализована с высокой прибылью какая с низкой",
|
||||||
|
"node_role": "root",
|
||||||
|
"paraphrase_family": "colloquial",
|
||||||
|
"expected_capability": "inventory_inventory_margin_ranking_for_nomenclature",
|
||||||
|
"expected_result_mode": "clarification_required"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_02_september_2017_period",
|
||||||
|
"title": "User provides period",
|
||||||
|
"question": "сентябрь 2017",
|
||||||
|
"node_role": "critical_child",
|
||||||
|
"paraphrase_family": "colloquial",
|
||||||
|
"depends_on": [
|
||||||
|
"step_01_margin_root_needs_period"
|
||||||
|
],
|
||||||
|
"expected_capability": "inventory_inventory_margin_ranking_for_nomenclature",
|
||||||
|
"expected_result_mode": "limited_accounting_answer",
|
||||||
|
"required_carryover_invariants": [
|
||||||
|
"intent_scope",
|
||||||
|
"period_scope",
|
||||||
|
"organization_scope"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_03_show_cost_base_lines",
|
||||||
|
"title": "Follow-up asks for found evidence after no sales",
|
||||||
|
"question": "покажи найденные строки себестоимостной базы",
|
||||||
|
"node_role": "critical_child",
|
||||||
|
"paraphrase_family": "canonical",
|
||||||
|
"depends_on": [
|
||||||
|
"step_02_september_2017_period"
|
||||||
|
],
|
||||||
|
"expected_result_mode": "evidence_or_honest_boundary",
|
||||||
|
"required_carryover_invariants": [
|
||||||
|
"intent_scope",
|
||||||
|
"period_scope",
|
||||||
|
"organization_scope"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_04_expand_to_2017",
|
||||||
|
"title": "User expands period to full year",
|
||||||
|
"question": "расширь до 2017 года",
|
||||||
|
"node_role": "critical_child",
|
||||||
|
"paraphrase_family": "colloquial",
|
||||||
|
"depends_on": [
|
||||||
|
"step_02_september_2017_period"
|
||||||
|
],
|
||||||
|
"expected_capability": "inventory_inventory_margin_ranking_for_nomenclature",
|
||||||
|
"expected_result_mode": "ranking_or_limited_accounting_answer",
|
||||||
|
"required_carryover_invariants": [
|
||||||
|
"intent_scope",
|
||||||
|
"period_scope",
|
||||||
|
"organization_scope"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_05_account_41_not_01",
|
||||||
|
"title": "User corrects account family to 41 not fixed assets",
|
||||||
|
"question": "анализ по 41 счету а не 01",
|
||||||
|
"node_role": "critical_child",
|
||||||
|
"paraphrase_family": "colloquial",
|
||||||
|
"depends_on": [
|
||||||
|
"step_04_expand_to_2017"
|
||||||
|
],
|
||||||
|
"expected_capability": "inventory_inventory_margin_ranking_for_nomenclature",
|
||||||
|
"expected_result_mode": "same_inventory_margin_context_or_clarification",
|
||||||
|
"required_carryover_invariants": [
|
||||||
|
"intent_scope",
|
||||||
|
"period_scope",
|
||||||
|
"organization_scope"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -1008,7 +1008,8 @@ function loadSessionDialog(runId, caseId) {
|
||||||
text: toStringSafe(item.text) ?? "",
|
text: toStringSafe(item.text) ?? "",
|
||||||
created_at: toStringSafe(item.created_at),
|
created_at: toStringSafe(item.created_at),
|
||||||
trace_id: toStringSafe(item.trace_id),
|
trace_id: toStringSafe(item.trace_id),
|
||||||
reply_type: toStringSafe(item.reply_type)
|
reply_type: toStringSafe(item.reply_type),
|
||||||
|
debug: item.debug ?? null
|
||||||
}));
|
}));
|
||||||
const turns = toArray(record.turns)
|
const turns = toArray(record.turns)
|
||||||
.map((item) => toRecord(item))
|
.map((item) => toRecord(item))
|
||||||
|
|
@ -1076,7 +1077,8 @@ function buildFallbackDialog(run, caseId) {
|
||||||
text: userText,
|
text: userText,
|
||||||
created_at: null,
|
created_at: null,
|
||||||
trace_id: null,
|
trace_id: null,
|
||||||
reply_type: null
|
reply_type: null,
|
||||||
|
debug: null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message_id: null,
|
message_id: null,
|
||||||
|
|
@ -1084,7 +1086,8 @@ function buildFallbackDialog(run, caseId) {
|
||||||
text: assistantSummaryParts.join("\n"),
|
text: assistantSummaryParts.join("\n"),
|
||||||
created_at: null,
|
created_at: null,
|
||||||
trace_id: toStringSafe(targetCase.trace_id),
|
trace_id: toStringSafe(targetCase.trace_id),
|
||||||
reply_type: toStringSafe(targetCase.reply_type)
|
reply_type: toStringSafe(targetCase.reply_type),
|
||||||
|
debug: null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
decomposition: [],
|
decomposition: [],
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ const COMPUTE_EXACT_INTENTS = new Set([
|
||||||
"inventory_purchase_documents_for_item",
|
"inventory_purchase_documents_for_item",
|
||||||
"inventory_supplier_stock_overlap_as_of_date",
|
"inventory_supplier_stock_overlap_as_of_date",
|
||||||
"inventory_sale_trace_for_item",
|
"inventory_sale_trace_for_item",
|
||||||
|
"inventory_margin_ranking_for_nomenclature",
|
||||||
"inventory_profitability_for_item",
|
"inventory_profitability_for_item",
|
||||||
"inventory_purchase_to_sale_chain",
|
"inventory_purchase_to_sale_chain",
|
||||||
"inventory_aging_by_purchase_date",
|
"inventory_aging_by_purchase_date",
|
||||||
|
|
@ -68,6 +69,7 @@ function defaultCapabilityId(intent) {
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date") {
|
intent === "inventory_aging_by_purchase_date") {
|
||||||
|
|
@ -151,12 +153,15 @@ function resolveCapabilityEnabled(intent) {
|
||||||
if (intent === "inventory_purchase_provenance_for_item" ||
|
if (intent === "inventory_purchase_provenance_for_item" ||
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain") {
|
intent === "inventory_purchase_to_sale_chain") {
|
||||||
if (intent === "inventory_profitability_for_item") {
|
if (intent === "inventory_profitability_for_item" || intent === "inventory_margin_ranking_for_nomenclature") {
|
||||||
return {
|
return {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
reason: "inventory_profitability_route_enabled"
|
reason: intent === "inventory_margin_ranking_for_nomenclature"
|
||||||
|
? "inventory_margin_ranking_route_enabled"
|
||||||
|
: "inventory_profitability_route_enabled"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (intent === "inventory_purchase_to_sale_chain") {
|
if (intent === "inventory_purchase_to_sale_chain") {
|
||||||
|
|
@ -249,6 +254,7 @@ function resolveShadowRouteIntent(intent, requestedResultMode) {
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date") {
|
intent === "inventory_aging_by_purchase_date") {
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,7 @@ function isConfirmedBalanceIntent(intent) {
|
||||||
intent === "inventory_purchase_provenance_for_item" ||
|
intent === "inventory_purchase_provenance_for_item" ||
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "open_contracts_confirmed_as_of_date" ||
|
intent === "open_contracts_confirmed_as_of_date" ||
|
||||||
|
|
|
||||||
|
|
@ -971,6 +971,7 @@ function isInventoryTraceIntent(intent) {
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date");
|
intent === "inventory_aging_by_purchase_date");
|
||||||
|
|
@ -989,6 +990,7 @@ function usesRecipeDefaultLimit(intent) {
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date");
|
intent === "inventory_aging_by_purchase_date");
|
||||||
|
|
@ -1407,6 +1409,9 @@ function requiredFiltersByIntent(intent) {
|
||||||
intent === "inventory_purchase_to_sale_chain") {
|
intent === "inventory_purchase_to_sale_chain") {
|
||||||
return ["item"];
|
return ["item"];
|
||||||
}
|
}
|
||||||
|
if (intent === "inventory_margin_ranking_for_nomenclature") {
|
||||||
|
return ["period_from", "period_to"];
|
||||||
|
}
|
||||||
if (intent === "payables_confirmed_as_of_date") {
|
if (intent === "payables_confirmed_as_of_date") {
|
||||||
return ["as_of_date"];
|
return ["as_of_date"];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1664,6 +1664,17 @@ function hasBidirectionalValueFlowComparisonSignal(text) {
|
||||||
const hasNetAmountCue = /(?:сколько|сумм|итог|нетто|сальдо|минус|net|total|sum)/iu.test(normalized);
|
const hasNetAmountCue = /(?:сколько|сумм|итог|нетто|сальдо|минус|net|total|sum)/iu.test(normalized);
|
||||||
return hasIncomingCue && hasOutgoingCue && hasComparisonCue && (hasValueFlowCue || hasNetAmountCue);
|
return hasIncomingCue && hasOutgoingCue && hasComparisonCue && (hasValueFlowCue || hasNetAmountCue);
|
||||||
}
|
}
|
||||||
|
function hasNomenclatureMarginRankingSignal(text) {
|
||||||
|
const normalized = String(text ?? "").trim().toLowerCase();
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasNomenclatureCue = /(?:номенклатур|товар|позици|ассортимент|sku|item|product|goods)/iu.test(normalized);
|
||||||
|
const hasRealizationCue = /(?:реализован|реализац|продан|продаж|отгруж|41(?:[.,]0?1)?|90(?:[.,]\d{1,2})?|sales?|sold)/iu.test(normalized);
|
||||||
|
const hasMarginCue = /(?:прибыл|марж|рентаб|наценк|себестоим|выручк|profit|margin|profitability|gross\s+spread|cogs)/iu.test(normalized);
|
||||||
|
const hasRankingCue = /(?:высок|низк|топ|сам(?:ая|ый|ое|ые)|больш|меньш|ранж|рейтинг|high|low|top|rank|best|worst)/iu.test(normalized);
|
||||||
|
return hasNomenclatureCue && hasRealizationCue && hasMarginCue && hasRankingCue;
|
||||||
|
}
|
||||||
function hasVatPeriodInspectionBridgeSignal(text) {
|
function hasVatPeriodInspectionBridgeSignal(text) {
|
||||||
const normalized = String(text ?? "").trim().toLowerCase();
|
const normalized = String(text ?? "").trim().toLowerCase();
|
||||||
if (!/(?:ндс|vat)/iu.test(normalized)) {
|
if (!/(?:ндс|vat)/iu.test(normalized)) {
|
||||||
|
|
@ -1736,6 +1747,9 @@ function resolveUnicodeAddressIntentBridge(text) {
|
||||||
if (hasSelectedObjectProfitabilityCue) {
|
if (hasSelectedObjectProfitabilityCue) {
|
||||||
return unicodeBridgeResolution("inventory_profitability_for_item", "high", "unicode_selected_object_profitability_bridge_signal_detected");
|
return unicodeBridgeResolution("inventory_profitability_for_item", "high", "unicode_selected_object_profitability_bridge_signal_detected");
|
||||||
}
|
}
|
||||||
|
if (hasNomenclatureMarginRankingSignal(normalized)) {
|
||||||
|
return unicodeBridgeResolution("inventory_margin_ranking_for_nomenclature", "high", "unicode_nomenclature_margin_ranking_bridge_signal_detected");
|
||||||
|
}
|
||||||
const hasOpenItemsAccountCue = /(?:хвост|долг|незакрыт|вис)/iu.test(normalized) &&
|
const hasOpenItemsAccountCue = /(?:хвост|долг|незакрыт|вис)/iu.test(normalized) &&
|
||||||
/(?:сч(?:е|ё)т(?:а|у|ом|е|ов)?\s*(?:№|#)?\s*(?:60|62|76)(?:[.,]\d{1,2})?|\b(?:60|62|76)(?:[.,]\d{1,2})?\b\s*сч(?:е|ё)т)/iu.test(normalized);
|
/(?:сч(?:е|ё)т(?:а|у|ом|е|ов)?\s*(?:№|#)?\s*(?:60|62|76)(?:[.,]\d{1,2})?|\b(?:60|62|76)(?:[.,]\d{1,2})?\b\s*сч(?:е|ё)т)/iu.test(normalized);
|
||||||
if (hasOpenItemsAccountCue) {
|
if (hasOpenItemsAccountCue) {
|
||||||
|
|
|
||||||
|
|
@ -1647,6 +1647,7 @@ function isOrganizationScopedInventoryIntent(intent) {
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date");
|
intent === "inventory_aging_by_purchase_date");
|
||||||
|
|
@ -1680,6 +1681,7 @@ function shouldDeferInventoryOrganizationClarification(intent, filters, semantic
|
||||||
return (intent === "inventory_purchase_provenance_for_item" ||
|
return (intent === "inventory_purchase_provenance_for_item" ||
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date");
|
intent === "inventory_aging_by_purchase_date");
|
||||||
|
|
@ -1981,6 +1983,7 @@ function canAutoBroadenPeriodWindow(intent, filters) {
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date");
|
intent === "inventory_aging_by_purchase_date");
|
||||||
|
|
@ -1990,6 +1993,7 @@ function shouldBoostAutoBroadenedLimit(intent) {
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date");
|
intent === "inventory_aging_by_purchase_date");
|
||||||
|
|
@ -2520,6 +2524,9 @@ async function tryComposeLlmLimitedReply(input) {
|
||||||
if (process.env.VITEST === "true" || process.env.NODE_ENV === "test") {
|
if (process.env.VITEST === "true" || process.env.NODE_ENV === "test") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (input.intent === "inventory_margin_ranking_for_nomenclature" && input.category === "missing_anchor") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (!shouldUseLlmLimitedReply(input.category)) {
|
if (!shouldUseLlmLimitedReply(input.category)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -2580,6 +2587,13 @@ function composeLimitedReply(input) {
|
||||||
.map((item) => normalizeMissingAnchorLabel(String(item ?? "").trim()))
|
.map((item) => normalizeMissingAnchorLabel(String(item ?? "").trim()))
|
||||||
.filter((item) => item.length > 0)));
|
.filter((item) => item.length > 0)));
|
||||||
const missingAnchorPhrase = missingAnchorLabels.length > 0 ? missingAnchorLabels.join(", ") : "контрагент, договор, счет или период";
|
const missingAnchorPhrase = missingAnchorLabels.length > 0 ? missingAnchorLabels.join(", ") : "контрагент, договор, счет или период";
|
||||||
|
if (input.intent === "inventory_margin_ranking_for_nomenclature" && input.category === "missing_anchor") {
|
||||||
|
return [
|
||||||
|
"Для рейтинга прибыльности номенклатуры нужен период.",
|
||||||
|
"Могу посчитать по номенклатуре: выручку без НДС, себестоимость реализации, валовую прибыль и маржинальность.",
|
||||||
|
"Уточните период: месяц, квартал, год или весь доступный период."
|
||||||
|
].join("\n\n");
|
||||||
|
}
|
||||||
const heading = input.category === "empty_match"
|
const heading = input.category === "empty_match"
|
||||||
? pickDeterministicVariant(headingSeed, [
|
? pickDeterministicVariant(headingSeed, [
|
||||||
"По текущим условиям в доступном срезе данных совпадений не нашлось.",
|
"По текущим условиям в доступном срезе данных совпадений не нашлось.",
|
||||||
|
|
|
||||||
|
|
@ -966,6 +966,17 @@ const BASE_RECIPES = [
|
||||||
account_scope_mode: "strict",
|
account_scope_mode: "strict",
|
||||||
query_template: "inventory_trading_margin_proxy_profile"
|
query_template: "inventory_trading_margin_proxy_profile"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
recipe_id: "address_inventory_margin_ranking_for_nomenclature_v1",
|
||||||
|
intent: "inventory_margin_ranking_for_nomenclature",
|
||||||
|
purpose: "Rank realized nomenclature by bounded gross margin proxy for an explicit period using 41.01 purchase and sale document rows",
|
||||||
|
required_filters: ["period_from", "period_to"],
|
||||||
|
optional_filters: ["organization", "warehouse", "limit", "sort"],
|
||||||
|
default_limit: 800,
|
||||||
|
account_scope: ["41.01"],
|
||||||
|
account_scope_mode: "strict",
|
||||||
|
query_template: "inventory_margin_ranking_profile"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
recipe_id: "address_inventory_purchase_to_sale_chain_v1",
|
recipe_id: "address_inventory_purchase_to_sale_chain_v1",
|
||||||
intent: "inventory_purchase_to_sale_chain",
|
intent: "inventory_purchase_to_sale_chain",
|
||||||
|
|
@ -1714,6 +1725,7 @@ function maxLimitForIntent(intent) {
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
intent === "inventory_trading_margin_proxy_for_organization" ||
|
intent === "inventory_trading_margin_proxy_for_organization" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date" ||
|
intent === "inventory_aging_by_purchase_date" ||
|
||||||
|
|
@ -1905,33 +1917,17 @@ function buildAddressRecipePlan(recipe, filters) {
|
||||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||||
: recipe.query_template === "inventory_trading_margin_proxy_profile"
|
: recipe.query_template === "inventory_trading_margin_proxy_profile"
|
||||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||||
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
: recipe.query_template === "inventory_margin_ranking_profile"
|
||||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||||
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
||||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||||
: recipe.query_template === "inventory_quality_events_profile"
|
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
||||||
? buildInventoryQualityEventsQuery(filters, resolvedLimit)
|
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||||
: recipe.query_template === "contracts_by_counterparty_profile"
|
: recipe.query_template === "inventory_quality_events_profile"
|
||||||
? CONTRACTS_BY_COUNTERPARTY_QUERY_TEMPLATE.replaceAll("__LIMIT__", String(resolvedLimit))
|
? buildInventoryQualityEventsQuery(filters, resolvedLimit)
|
||||||
: recipe.query_template === "open_contracts_confirmed_as_of_balance_profile"
|
: recipe.query_template === "contracts_by_counterparty_profile"
|
||||||
? (() => {
|
? CONTRACTS_BY_COUNTERPARTY_QUERY_TEMPLATE.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||||
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
: recipe.query_template === "open_contracts_confirmed_as_of_balance_profile"
|
||||||
? toDateTimeExpr(filters.as_of_date, true)
|
|
||||||
: null) ??
|
|
||||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
|
||||||
? toDateTimeExpr(filters.period_to, true)
|
|
||||||
: null) ??
|
|
||||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
|
||||||
? toDateTimeExpr(filters.period_from, true)
|
|
||||||
: null) ??
|
|
||||||
"ТЕКУЩАЯДАТА()";
|
|
||||||
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
|
||||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
|
||||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
|
||||||
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "62", "76"]))
|
|
||||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
|
||||||
})()
|
|
||||||
: recipe.query_template === "payables_confirmed_as_of_balance_profile"
|
|
||||||
? (() => {
|
? (() => {
|
||||||
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||||
? toDateTimeExpr(filters.as_of_date, true)
|
? toDateTimeExpr(filters.as_of_date, true)
|
||||||
|
|
@ -1946,10 +1942,10 @@ function buildAddressRecipePlan(recipe, filters) {
|
||||||
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||||
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
|
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "62", "76"]))
|
||||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||||
})()
|
})()
|
||||||
: recipe.query_template === "receivables_confirmed_as_of_balance_profile"
|
: recipe.query_template === "payables_confirmed_as_of_balance_profile"
|
||||||
? (() => {
|
? (() => {
|
||||||
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||||
? toDateTimeExpr(filters.as_of_date, true)
|
? toDateTimeExpr(filters.as_of_date, true)
|
||||||
|
|
@ -1964,20 +1960,38 @@ function buildAddressRecipePlan(recipe, filters) {
|
||||||
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||||
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["62", "76"]))
|
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
|
||||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||||
})()
|
})()
|
||||||
: MOVEMENTS_QUERY_TEMPLATE
|
: recipe.query_template === "receivables_confirmed_as_of_balance_profile"
|
||||||
.replace("__LIMIT__", String(resolvedLimit))
|
? (() => {
|
||||||
.replace("__WHERE_CLAUSE__", (() => {
|
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||||
const extraConditions = [];
|
? toDateTimeExpr(filters.as_of_date, true)
|
||||||
const accountCondition = buildMovementAccountCondition(filters);
|
: null) ??
|
||||||
if (accountCondition) {
|
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||||
extraConditions.push(accountCondition);
|
? toDateTimeExpr(filters.period_to, true)
|
||||||
}
|
: null) ??
|
||||||
return buildWhereClause(filters, "Движения.Период", extraConditions);
|
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||||
})())
|
? toDateTimeExpr(filters.period_from, true)
|
||||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
: null) ??
|
||||||
|
"ТЕКУЩАЯДАТА()";
|
||||||
|
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||||
|
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||||
|
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||||
|
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["62", "76"]))
|
||||||
|
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||||
|
})()
|
||||||
|
: MOVEMENTS_QUERY_TEMPLATE
|
||||||
|
.replace("__LIMIT__", String(resolvedLimit))
|
||||||
|
.replace("__WHERE_CLAUSE__", (() => {
|
||||||
|
const extraConditions = [];
|
||||||
|
const accountCondition = buildMovementAccountCondition(filters);
|
||||||
|
if (accountCondition) {
|
||||||
|
extraConditions.push(accountCondition);
|
||||||
|
}
|
||||||
|
return buildWhereClause(filters, "Движения.Период", extraConditions);
|
||||||
|
})())
|
||||||
|
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||||
return {
|
return {
|
||||||
recipe,
|
recipe,
|
||||||
query,
|
query,
|
||||||
|
|
|
||||||
|
|
@ -129,15 +129,6 @@ function formatNumberWithDots(value, fractionDigits = 0) {
|
||||||
function formatMoneyRub(value) {
|
function formatMoneyRub(value) {
|
||||||
return `${formatNumberWithDots(value, 2)} ₽`;
|
return `${formatNumberWithDots(value, 2)} ₽`;
|
||||||
}
|
}
|
||||||
function formatVatProbeStatusRu(status) {
|
|
||||||
if (status === "ok") {
|
|
||||||
return "есть движения";
|
|
||||||
}
|
|
||||||
if (status === "empty") {
|
|
||||||
return "движения не найдены";
|
|
||||||
}
|
|
||||||
return "ошибка запроса";
|
|
||||||
}
|
|
||||||
function emphasizeNumericTokens(line) {
|
function emphasizeNumericTokens(line) {
|
||||||
if (!line) {
|
if (!line) {
|
||||||
return line;
|
return line;
|
||||||
|
|
@ -365,6 +356,63 @@ function bankOperationEvidenceLine(rows, preferredDirection = null) {
|
||||||
}
|
}
|
||||||
return `Основание 1С: ${parts.join("; ")}.`;
|
return `Основание 1С: ${parts.join("; ")}.`;
|
||||||
}
|
}
|
||||||
|
function classifyBankOperationSemanticBucket(row) {
|
||||||
|
const text = [
|
||||||
|
row.registrator,
|
||||||
|
row.operation_kind,
|
||||||
|
row.payment_purpose,
|
||||||
|
row.contract,
|
||||||
|
row.comment
|
||||||
|
]
|
||||||
|
.map((item) => String(item ?? "").toLowerCase())
|
||||||
|
.join(" ");
|
||||||
|
if (/(?:комисс|тариф|эквайр|обслуживан)/iu.test(text)) {
|
||||||
|
return "commission";
|
||||||
|
}
|
||||||
|
if (/(?:депозит|кредит|займ|овердрафт|процент|ссуд)/iu.test(text)) {
|
||||||
|
return "deposit_or_credit";
|
||||||
|
}
|
||||||
|
if (/(?:налог|ндс|взнос|бюджет|фнс|пфр|страхов)/iu.test(text)) {
|
||||||
|
return "tax_or_budget";
|
||||||
|
}
|
||||||
|
if (/(?:возврат|перевод|перечислен|переброс|пополн|инкасс|перенос)/iu.test(text)) {
|
||||||
|
return "transfer_or_return";
|
||||||
|
}
|
||||||
|
return "other";
|
||||||
|
}
|
||||||
|
function bankOperationSemanticBucketLabel(bucket) {
|
||||||
|
if (bucket === "commission") {
|
||||||
|
return "комиссии и банковское обслуживание";
|
||||||
|
}
|
||||||
|
if (bucket === "deposit_or_credit") {
|
||||||
|
return "депозиты, кредиты или проценты";
|
||||||
|
}
|
||||||
|
if (bucket === "tax_or_budget") {
|
||||||
|
return "налоги и бюджетные платежи";
|
||||||
|
}
|
||||||
|
if (bucket === "transfer_or_return") {
|
||||||
|
return "переводы, возвраты или перебросы";
|
||||||
|
}
|
||||||
|
return "прочие банковские операции";
|
||||||
|
}
|
||||||
|
function summarizeBankOperationSemantics(rows) {
|
||||||
|
if (rows.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const counts = new Map();
|
||||||
|
for (const row of rows) {
|
||||||
|
const bucket = classifyBankOperationSemanticBucket(row);
|
||||||
|
counts.set(bucket, (counts.get(bucket) ?? 0) + 1);
|
||||||
|
}
|
||||||
|
const ranked = Array.from(counts.entries())
|
||||||
|
.sort((left, right) => right[1] - left[1])
|
||||||
|
.slice(0, 3);
|
||||||
|
if (ranked.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const parts = ranked.map(([bucket, count]) => `${bankOperationSemanticBucketLabel(bucket)} — ${count}`);
|
||||||
|
return `По смыслу это скорее финансовый/банковский контур: ${parts.join("; ")}.`;
|
||||||
|
}
|
||||||
function bankRoleBoundaryLine(userMessage, rows) {
|
function bankRoleBoundaryLine(userMessage, rows) {
|
||||||
const incomingBoundary = hasBankIncomingRoleBoundaryQuestion(userMessage);
|
const incomingBoundary = hasBankIncomingRoleBoundaryQuestion(userMessage);
|
||||||
const outgoingBoundary = hasBankOutgoingRoleBoundaryQuestion(userMessage);
|
const outgoingBoundary = hasBankOutgoingRoleBoundaryQuestion(userMessage);
|
||||||
|
|
@ -2045,35 +2093,31 @@ function formatPayablesEvidenceSuffix(item) {
|
||||||
const suffix = item.documents.length > 1 ? ` (+${item.documents.length - 1})` : "";
|
const suffix = item.documents.length > 1 ? ` (+${item.documents.length - 1})` : "";
|
||||||
parts.push(`документ: ${item.documents[0]}${suffix}`);
|
parts.push(`документ: ${item.documents[0]}${suffix}`);
|
||||||
}
|
}
|
||||||
if (item.sourceRefs.length > 0) {
|
|
||||||
parts.push(`source refs: ${item.sourceRefs.slice(0, 2).join("; ")}`);
|
|
||||||
}
|
|
||||||
return parts.length > 0 ? ` | ${parts.join(" | ")}` : "";
|
return parts.length > 0 ? ` | ${parts.join(" | ")}` : "";
|
||||||
}
|
}
|
||||||
function formatDebtMirrorGroupLine(item) {
|
function formatDebtMirrorGroupLine(item) {
|
||||||
const details = [
|
|
||||||
item.account ? `счет ${item.account}` : null,
|
|
||||||
item.contract ? `договор/аналитика: ${item.contract}` : null,
|
|
||||||
item.organization ? `организация: ${item.organization}` : null
|
|
||||||
].filter((part) => Boolean(part));
|
|
||||||
const netText = Math.abs(item.netAmount) <= 0.005
|
const netText = Math.abs(item.netAmount) <= 0.005
|
||||||
? "чисто: 0 ₽"
|
? "чисто: 0 ₽"
|
||||||
: item.netAmount > 0
|
: item.netAmount > 0
|
||||||
? `чисто к получению: ${formatMoneyRub(item.netAmount)}`
|
? `чисто к получению: ${formatMoneyRub(item.netAmount)}`
|
||||||
: `чисто к оплате: ${formatMoneyRub(Math.abs(item.netAmount))}`;
|
: `чисто к оплате: ${formatMoneyRub(Math.abs(item.netAmount))}`;
|
||||||
return `${item.name}${details.length > 0 ? ` (${details.join(", ")})` : ""}: дебет ${formatMoneyRub(item.debitAmount)} / кредит ${formatMoneyRub(item.creditAmount)}, ${netText}.`;
|
return `${item.name}: встречная часть ${formatMoneyRub(Math.min(item.debitAmount, item.creditAmount))}, ${netText}.`;
|
||||||
}
|
}
|
||||||
function debtMirrorCleanScopeLabel(kind) {
|
function debtMirrorCleanScopeLabel(kind, grammaticalCase) {
|
||||||
return kind === "payables" ? "чистый долг к оплате" : "чистую дебиторку к получению";
|
if (kind === "payables") {
|
||||||
|
return grammaticalCase === "genitive" ? "чистого долга к оплате" : "чистый долг к оплате";
|
||||||
|
}
|
||||||
|
return grammaticalCase === "genitive" ? "чистой дебиторки к получению" : "чистую дебиторку к получению";
|
||||||
}
|
}
|
||||||
function appendDebtMirrorCompactDisclosure(lines, snapshot, kind) {
|
function appendDebtMirrorCompactDisclosure(lines, snapshot, kind) {
|
||||||
if (snapshot.mirroredOffsetAmount <= 0.005) {
|
if (snapshot.mirroredOffsetAmount <= 0.005) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lines.push(`Отдельно сверено встречных остатков: ${formatMoneyRub(snapshot.mirroredOffsetAmount)}; они не включены в ${debtMirrorCleanScopeLabel(kind)}.`);
|
lines.push("", "Для сверки:");
|
||||||
|
lines.push(`- Встречная часть: ${formatMoneyRub(snapshot.mirroredOffsetAmount)}; она не включена в ${debtMirrorCleanScopeLabel(kind, "accusative")}.`);
|
||||||
const leadingMirror = snapshot.mirrorGroups[0] ?? null;
|
const leadingMirror = snapshot.mirrorGroups[0] ?? null;
|
||||||
if (leadingMirror) {
|
if (leadingMirror) {
|
||||||
lines.push(`Крупнейший встречный хвост: ${formatDebtMirrorGroupLine(leadingMirror)}`);
|
lines.push(`- Крупнейший встречный контрагент: ${formatDebtMirrorGroupLine(leadingMirror)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function appendDebtMirrorDisclosure(lines, snapshot, kind) {
|
function appendDebtMirrorDisclosure(lines, snapshot, kind) {
|
||||||
|
|
@ -2082,8 +2126,11 @@ function appendDebtMirrorDisclosure(lines, snapshot, kind) {
|
||||||
}
|
}
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push("Встречные остатки к сверке");
|
lines.push("Встречные остатки к сверке");
|
||||||
lines.push(`- Встречная часть: ${formatMoneyRub(snapshot.mirroredOffsetAmount)}; она исключена из ${debtMirrorCleanScopeLabel(kind)}.`);
|
lines.push(`- Встречная часть: ${formatMoneyRub(snapshot.mirroredOffsetAmount)}; она исключена из ${debtMirrorCleanScopeLabel(kind, "genitive")}.`);
|
||||||
lines.push(...snapshot.mirrorGroups.slice(0, 3).map((item, index) => `${index + 1}. ${formatDebtMirrorGroupLine(item)}`));
|
const leadingMirror = snapshot.mirrorGroups[0] ?? null;
|
||||||
|
if (leadingMirror) {
|
||||||
|
lines.push(`- Крупнейший встречный контрагент: ${formatDebtMirrorGroupLine(leadingMirror)}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function deriveOperationalYearWindow(yearDocs, yearOps) {
|
function deriveOperationalYearWindow(yearDocs, yearOps) {
|
||||||
const docsSeries = [...yearDocs].sort((a, b) => a.year - b.year);
|
const docsSeries = [...yearDocs].sort((a, b) => a.year - b.year);
|
||||||
|
|
@ -2918,7 +2965,6 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
||||||
const turnover19Credit = rowsByMarker.get("VAT_19_CREDIT") ?? 0;
|
const turnover19Credit = rowsByMarker.get("VAT_19_CREDIT") ?? 0;
|
||||||
const netVat = turnover68Credit - turnover68Debit;
|
const netVat = turnover68Credit - turnover68Debit;
|
||||||
const vatToPay = Math.max(0, netVat);
|
const vatToPay = Math.max(0, netVat);
|
||||||
const carryoverOrOverpayment = Math.max(0, -netVat);
|
|
||||||
const totalVatTurnoverAbs = Math.abs(turnover68Credit) + Math.abs(turnover68Debit) + Math.abs(turnover19Debit) + Math.abs(turnover19Credit);
|
const totalVatTurnoverAbs = Math.abs(turnover68Credit) + Math.abs(turnover68Debit) + Math.abs(turnover19Debit) + Math.abs(turnover19Credit);
|
||||||
const vatActivityDetected = totalVatTurnoverAbs > 0.0000001;
|
const vatActivityDetected = totalVatTurnoverAbs > 0.0000001;
|
||||||
const netVatIsEffectivelyZero = Math.abs(netVat) <= 0.005;
|
const netVatIsEffectivelyZero = Math.abs(netVat) <= 0.005;
|
||||||
|
|
@ -2926,50 +2972,17 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
||||||
const shouldShowCalendarDetails = needsVatCalendarDetails(options.userMessage);
|
const shouldShowCalendarDetails = needsVatCalendarDetails(options.userMessage);
|
||||||
const vatCalendar = deriveVatDeadlineCalendar(options.periodFrom, options.periodTo);
|
const vatCalendar = deriveVatDeadlineCalendar(options.periodFrom, options.periodTo);
|
||||||
const formatForecastMoney = (value) => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
const formatForecastMoney = (value) => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
||||||
const vatProbe = options.vatDirectSourceProbe ?? null;
|
|
||||||
const periodWindowLabel = options.periodFrom && options.periodTo ? `${formatDateRu(options.periodFrom)}..${formatDateRu(options.periodTo)}` : null;
|
const periodWindowLabel = options.periodFrom && options.periodTo ? `${formatDateRu(options.periodFrom)}..${formatDateRu(options.periodTo)}` : null;
|
||||||
const lines = [
|
const lines = [
|
||||||
`Коротко: ориентир по НДС к уплате за ${periodWindowLabel ?? "доступный срез"} — ${formatForecastMoney(vatToPay)}.`,
|
`Коротко: ориентир по НДС к уплате за ${periodWindowLabel ?? "доступный срез"} — ${formatForecastMoney(vatToPay)}.`,
|
||||||
`Если смотреть на возможный перенос или переплату, получается ${formatForecastMoney(carryoverOrOverpayment)}.`,
|
"Это предварительная оценка по бухгалтерским оборотам НДС, а не подтвержденная сумма налога по декларации.",
|
||||||
"Это предварительная оценка по оборотам 68.02*/19*, а не подтвержденная сумма налога по декларации.",
|
|
||||||
"",
|
"",
|
||||||
"Что вошло в расчет:",
|
"Что вошло в расчет:",
|
||||||
`- Период оценки: ${periodWindowLabel ?? "не задан (использован доступный срез)"}.`,
|
`- Период оценки: ${periodWindowLabel ?? "не задан (использован доступный срез)"}.`,
|
||||||
`- Оборот по кредиту 68*: ${formatForecastMoney(turnover68Credit)}.`,
|
`- НДС начисленный: ${formatForecastMoney(turnover68Credit)}.`,
|
||||||
`- Оборот по дебету 68*: ${formatForecastMoney(turnover68Debit)}.`,
|
`- Уменьшение/вычеты по НДС: ${formatForecastMoney(turnover68Debit)}.`,
|
||||||
`- Нетто НДС (68 Кт - 68 Дт): ${formatForecastMoney(netVat)}.`,
|
`- Нетто НДС: ${formatForecastMoney(netVat)}.`
|
||||||
`- Справочно по 19*: дебет ${formatForecastMoney(turnover19Debit)}, кредит ${formatForecastMoney(turnover19Credit)}.`
|
|
||||||
];
|
];
|
||||||
if (vatProbe && vatProbe.status === "ok") {
|
|
||||||
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
|
||||||
const statusRank = (status) => status === "ok" ? 0 : status === "empty" ? 1 : 2;
|
|
||||||
const orderedProbeRows = [...vatProbe.probedSources].sort((a, b) => statusRank(a.status) - statusRank(b.status) ||
|
|
||||||
a.fullName.localeCompare(b.fullName, "ru"));
|
|
||||||
const nonErrorProbeRows = orderedProbeRows.filter((item) => item.status !== "error");
|
|
||||||
const visibleProbeRows = (nonErrorProbeRows.length > 0 ? nonErrorProbeRows : orderedProbeRows).slice(0, 6);
|
|
||||||
const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length;
|
|
||||||
lines.push("", "Покрытие VAT-источников в 1С:", `- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`, `- Источников с ошибкой запроса: ${formatNumberWithDots(erroredSources)}.`);
|
|
||||||
if (visibleProbeRows.length > 0) {
|
|
||||||
lines.push(...visibleProbeRows.map((item, index) => {
|
|
||||||
const name = item.synonym ? `${item.fullName} (${item.synonym})` : item.fullName;
|
|
||||||
const extra = item.status === "ok"
|
|
||||||
? item.lastPeriod
|
|
||||||
? ` | последнее движение: ${item.lastPeriod}`
|
|
||||||
: ""
|
|
||||||
: item.status === "error" && item.error
|
|
||||||
? ` | ошибка: ${item.error}`
|
|
||||||
: "";
|
|
||||||
return `${index + 1}. ${name} | ${formatVatProbeStatusRu(item.status)}${extra}`;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
if (vatProbe.errors.length > 0) {
|
|
||||||
lines.push(`- Ограничения probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
|
||||||
}
|
|
||||||
lines.push("- Сумма прогноза выше рассчитана строго по оборотам 68.02*/19*; прямые VAT-источники показаны для проверки покрытия.");
|
|
||||||
}
|
|
||||||
else if (vatProbe && vatProbe.status === "error") {
|
|
||||||
lines.push("", "Покрытие VAT-источников в 1С: дополнительная проверка завершилась ошибкой, поэтому использован только базовый контур 68.02*/19*.");
|
|
||||||
}
|
|
||||||
if (!vatActivityDetected) {
|
if (!vatActivityDetected) {
|
||||||
lines.push(`В выбранном окне не найдено движений по НДС-субсчетам 68.02*/19*; поэтому оперативный прогноз к уплате равен ${formatForecastMoney(0)}.`);
|
lines.push(`В выбранном окне не найдено движений по НДС-субсчетам 68.02*/19*; поэтому оперативный прогноз к уплате равен ${formatForecastMoney(0)}.`);
|
||||||
}
|
}
|
||||||
|
|
@ -3016,16 +3029,13 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
||||||
const purchaseVat = rowsByMarker.get("VAT_BOOK_PURCHASES") ?? 0;
|
const purchaseVat = rowsByMarker.get("VAT_BOOK_PURCHASES") ?? 0;
|
||||||
const netVat = salesVat - purchaseVat;
|
const netVat = salesVat - purchaseVat;
|
||||||
const vatToPay = Math.max(0, netVat);
|
const vatToPay = Math.max(0, netVat);
|
||||||
const carryoverOrOverpayment = Math.max(0, -netVat);
|
|
||||||
const periodWindowLabel = options.periodFrom && options.periodTo ? `${formatDateRu(options.periodFrom)}..${formatDateRu(options.periodTo)}` : null;
|
const periodWindowLabel = options.periodFrom && options.periodTo ? `${formatDateRu(options.periodFrom)}..${formatDateRu(options.periodTo)}` : null;
|
||||||
const formatConfirmedMoney = (value) => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
const formatConfirmedMoney = (value) => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
||||||
const vatProbe = options.vatDirectSourceProbe ?? null;
|
|
||||||
const organizationLabel = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(options.organizationHint);
|
const organizationLabel = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(options.organizationHint);
|
||||||
const organizationScopeLabel = organizationLabel ? ` по организации ${organizationLabel}` : "";
|
const organizationScopeLabel = organizationLabel ? ` по организации ${organizationLabel}` : "";
|
||||||
const lines = [
|
const lines = [
|
||||||
`Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`,
|
`Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`,
|
||||||
`Если смотреть на возможный перенос или переплату, получается ${formatConfirmedMoney(carryoverOrOverpayment)}.`,
|
"Расчет сделан по книгам продаж и покупок.",
|
||||||
"Это подтвержденный расчет по регистрам книг продаж и покупок, без surrogate-формулы 68/19.",
|
|
||||||
"",
|
"",
|
||||||
"Что вошло в расчет:",
|
"Что вошло в расчет:",
|
||||||
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
|
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
|
||||||
|
|
@ -3034,21 +3044,6 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
||||||
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
|
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
|
||||||
`- Нетто НДС (книга продаж - книга покупок): ${formatConfirmedMoney(netVat)}.`
|
`- Нетто НДС (книга продаж - книга покупок): ${formatConfirmedMoney(netVat)}.`
|
||||||
];
|
];
|
||||||
if (vatProbe && vatProbe.status === "ok") {
|
|
||||||
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
|
||||||
const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length;
|
|
||||||
lines.push("", "Покрытие VAT-источников в 1С:", `- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`, `- Источников с ошибкой запроса: ${formatNumberWithDots(erroredSources)}.`);
|
|
||||||
if (vatProbe.errors.length > 0) {
|
|
||||||
lines.push(`- Ограничения probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
|
||||||
}
|
|
||||||
lines.push("- Сумма расчета выше получена по книгам продаж/покупок; дополнительная проверка использована для контроля полноты VAT-источников.");
|
|
||||||
}
|
|
||||||
else if (vatProbe && vatProbe.status === "error") {
|
|
||||||
lines.push("", "Покрытие VAT-источников в 1С: дополнительная проверка недоступна, поэтому использован основной бухгалтерский срез.", "Итоговая сумма НДС выше рассчитана по основному маршруту книг продаж/покупок; probe влияет только на диагностику покрытия.");
|
|
||||||
if (vatProbe.errors.length > 0) {
|
|
||||||
lines.push(`- Детали probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
lines.push("", "За выбранный налоговый период не найдены строки книг продаж/покупок, поэтому подтвержденная сумма к уплате равна 0.");
|
lines.push("", "За выбранный налоговый период не найдены строки книг продаж/покупок, поэтому подтвержденная сумма к уплате равна 0.");
|
||||||
}
|
}
|
||||||
|
|
@ -3104,40 +3099,14 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
||||||
const lines = [
|
const lines = [
|
||||||
`Итого подтвержденный НДС к уплате на ${formatDateRu(asOfDate)}: ${formatMoneyRub(totalVatPayable)}.`,
|
`Итого подтвержденный НДС к уплате на ${formatDateRu(asOfDate)}: ${formatMoneyRub(totalVatPayable)}.`,
|
||||||
"",
|
"",
|
||||||
"Блок 1. Статус результата",
|
"Что учтено:",
|
||||||
"- Результат: подтвержденный срез НДС к уплате по состоянию на дату.",
|
|
||||||
"",
|
|
||||||
"Блок 2. Что учтено",
|
|
||||||
`- Дата среза: ${formatDateRu(asOfDate)}.`,
|
`- Дата среза: ${formatDateRu(asOfDate)}.`,
|
||||||
"- Контур: остатки по счетам НДС к уплате (68*)."
|
"- Контур: остатки по счетам НДС к уплате."
|
||||||
];
|
];
|
||||||
const vatProbe = options.vatDirectSourceProbe ?? null;
|
lines.push("", "Сводка:", `- Строк в выборке: ${formatNumberWithDots(rows.length)}.`, `- Подтвержденных позиций по НДС: ${formatNumberWithDots(accountRows.length)}.`, "", "Подтвержденные позиции:");
|
||||||
if (vatProbe && vatProbe.status === "ok") {
|
|
||||||
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
|
||||||
lines.push("", "Блок 2.1. Проверка VAT-источников в 1С", `- VAT-объектов в метаданных 1С: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Пробных прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`);
|
|
||||||
if (vatProbe.probedSources.length > 0) {
|
|
||||||
lines.push(...vatProbe.probedSources.slice(0, 4).map((item, index) => {
|
|
||||||
const name = item.synonym ? `${item.fullName} (${item.synonym})` : item.fullName;
|
|
||||||
const suffix = item.status === "ok"
|
|
||||||
? `${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.sampleRegistrator ? ` | пример: ${item.sampleRegistrator}` : ""}`
|
|
||||||
: item.status === "error" && item.error
|
|
||||||
? ` | ошибка: ${item.error}`
|
|
||||||
: "";
|
|
||||||
return `${index + 1}. ${name} | ${formatVatProbeStatusRu(item.status)}${suffix}`;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
if (vatProbe.errors.length > 0) {
|
|
||||||
lines.push(`- Ограничения probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (vatProbe && vatProbe.status === "error") {
|
|
||||||
lines.push("", "Блок 2.1. Проверка VAT-источников в 1С", "- Дополнительная проверка VAT-источников завершилась ошибкой, поэтому срез подтвержден по доступному бухгалтерскому источнику (68*).");
|
|
||||||
}
|
|
||||||
lines.push("", "Блок 3. Сводка", `- Строк в выборке: ${formatNumberWithDots(rows.length)}.`, `- Подтвержденных позиций по НДС: ${formatNumberWithDots(accountRows.length)}.`, "", "Блок 4. Подтвержденные позиции");
|
|
||||||
if (accountRows.length > 0) {
|
if (accountRows.length > 0) {
|
||||||
lines.push(...accountRows.slice(0, 12).map((item, index) => {
|
lines.push(...accountRows.slice(0, 12).map((item, index) => {
|
||||||
const refs = Array.from(item.refs).slice(0, 2).join("; ");
|
return `${index + 1}. ${item.account} — остаток НДС к уплате: ${formatMoneyRub(item.total)}, операций: ${formatNumberWithDots(item.operations)}.`;
|
||||||
return `${index + 1}. ${item.account} | остаток НДС к уплате: ${formatMoneyRub(item.total)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${refs ? ` | source refs: ${refs}` : ""}`;
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
@ -3410,8 +3379,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
||||||
: [`Коротко: на ${formatDateRu(payablesAsOfDate)} подтвержденных обязательств к оплате не найдено.`];
|
: [`Коротко: на ${formatDateRu(payablesAsOfDate)} подтвержденных обязательств к оплате не найдено.`];
|
||||||
if (leading) {
|
if (leading) {
|
||||||
compactLines.push(...confirmedBalances.slice(0, 5).map((item, index) => {
|
compactLines.push(...confirmedBalances.slice(0, 5).map((item, index) => {
|
||||||
const lastPeriod = item.lastPeriod ? `, последнее движение: ${item.lastPeriod}` : "";
|
return `${index + 1}. ${item.name} — ${formatMoneyRub(item.outstandingAmount)} (${formatNumberWithDots(item.operations)} опер.).`;
|
||||||
return `${index + 1}. ${item.name} — ${formatMoneyRub(item.outstandingAmount)} (${formatNumberWithDots(item.operations)} опер.${lastPeriod}).`;
|
|
||||||
}));
|
}));
|
||||||
if (confirmedBalances.length > 5) {
|
if (confirmedBalances.length > 5) {
|
||||||
compactLines.push(`Показаны первые 5 из ${formatNumberWithDots(confirmedBalances.length)} подтвержденных позиций.`);
|
compactLines.push(`Показаны первые 5 из ${formatNumberWithDots(confirmedBalances.length)} подтвержденных позиций.`);
|
||||||
|
|
@ -3458,7 +3426,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
||||||
lines.push("Подтвержденные позиции к оплате");
|
lines.push("Подтвержденные позиции к оплате");
|
||||||
if (confirmedBalances.length > 0) {
|
if (confirmedBalances.length > 0) {
|
||||||
lines.push(...confirmedBalances.slice(0, 10).flatMap((item, index) => [
|
lines.push(...confirmedBalances.slice(0, 10).flatMap((item, index) => [
|
||||||
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`,
|
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`,
|
||||||
""
|
""
|
||||||
]));
|
]));
|
||||||
if (lines[lines.length - 1] === "") {
|
if (lines[lines.length - 1] === "") {
|
||||||
|
|
@ -3506,8 +3474,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
||||||
: [`Коротко: на ${formatDateRu(receivablesAsOfDate)} подтвержденной дебиторской задолженности не найдено.`];
|
: [`Коротко: на ${formatDateRu(receivablesAsOfDate)} подтвержденной дебиторской задолженности не найдено.`];
|
||||||
if (leading) {
|
if (leading) {
|
||||||
compactLines.push(...confirmedBalances.slice(0, 5).map((item, index) => {
|
compactLines.push(...confirmedBalances.slice(0, 5).map((item, index) => {
|
||||||
const lastPeriod = item.lastPeriod ? `, последнее движение: ${item.lastPeriod}` : "";
|
return `${index + 1}. ${item.name} — ${formatMoneyRub(item.outstandingAmount)} (${formatNumberWithDots(item.operations)} опер.).`;
|
||||||
return `${index + 1}. ${item.name} — ${formatMoneyRub(item.outstandingAmount)} (${formatNumberWithDots(item.operations)} опер.${lastPeriod}).`;
|
|
||||||
}));
|
}));
|
||||||
if (confirmedBalances.length > 5) {
|
if (confirmedBalances.length > 5) {
|
||||||
compactLines.push(`Показаны первые 5 из ${formatNumberWithDots(confirmedBalances.length)} подтвержденных позиций.`);
|
compactLines.push(`Показаны первые 5 из ${formatNumberWithDots(confirmedBalances.length)} подтвержденных позиций.`);
|
||||||
|
|
@ -3526,44 +3493,27 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const lines = [
|
const lines = [
|
||||||
`Коротко: подтвержденная дебиторская задолженность на ${formatDateRu(receivablesAsOfDate)} — ${formatMoneyRub(totalOutstandingAmount)}.`,
|
`Коротко: на ${formatDateRu(receivablesAsOfDate)} нам должны ${formatMoneyRub(totalOutstandingAmount)}.`
|
||||||
"Это подтвержденный срез дебиторской задолженности, а не эвристический shortlist."
|
|
||||||
];
|
];
|
||||||
lines.push("");
|
if (periodScopeLine || carryoverLine) {
|
||||||
lines.push("Что учтено");
|
lines.push("");
|
||||||
lines.push(`- Дата среза: ${formatDateRu(receivablesAsOfDate)}.`);
|
lines.push("Граница ответа:");
|
||||||
if (periodScopeLine) {
|
if (periodScopeLine) {
|
||||||
lines.push(periodScopeLine);
|
lines.push(periodScopeLine);
|
||||||
}
|
|
||||||
lines.push("- Контур: дебиторская задолженность по счетам 62/76.");
|
|
||||||
if (carryoverLine) {
|
|
||||||
lines.push(carryoverLine);
|
|
||||||
}
|
|
||||||
lines.push("");
|
|
||||||
lines.push("Сводка");
|
|
||||||
lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`);
|
|
||||||
lines.push(`- Контрагентов с подтвержденным остатком к получению: ${formatNumberWithDots(confirmedBalances.length)}.`);
|
|
||||||
appendDebtMirrorDisclosure(lines, balanceSnapshot, "receivables");
|
|
||||||
lines.push("");
|
|
||||||
lines.push("Категории дебиторской задолженности");
|
|
||||||
lines.push(`- ${receivablesCategoryLabel("supplier_or_contractor")}: ${formatNumberWithDots(categoryCounts.supplier_or_contractor)}.`);
|
|
||||||
lines.push(`- ${receivablesCategoryLabel("bank_or_credit")}: ${formatNumberWithDots(categoryCounts.bank_or_credit)}.`);
|
|
||||||
lines.push(`- ${receivablesCategoryLabel("tax_or_state")}: ${formatNumberWithDots(categoryCounts.tax_or_state)}.`);
|
|
||||||
lines.push(`- ${receivablesCategoryLabel("other")}: ${formatNumberWithDots(categoryCounts.other)}.`);
|
|
||||||
lines.push("");
|
|
||||||
lines.push("Подтвержденные позиции к получению");
|
|
||||||
if (confirmedBalances.length > 0) {
|
|
||||||
lines.push(...confirmedBalances.slice(0, 10).flatMap((item, index) => [
|
|
||||||
`${index + 1}. ${item.name} | категория: ${receivablesCategoryLabel(item.category)} | остаток к получению: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`,
|
|
||||||
""
|
|
||||||
]));
|
|
||||||
if (lines[lines.length - 1] === "") {
|
|
||||||
lines.pop();
|
|
||||||
}
|
}
|
||||||
|
if (carryoverLine) {
|
||||||
|
lines.push(carryoverLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines.push("");
|
||||||
|
lines.push("К получению:");
|
||||||
|
if (confirmedBalances.length > 0) {
|
||||||
|
lines.push(...confirmedBalances.slice(0, 10).map((item, index) => `${index + 1}. ${item.name} — ${formatMoneyRub(item.outstandingAmount)} (${formatNumberWithDots(item.operations)} опер.).`));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
lines.push("- Подтвержденной открытой дебиторской задолженности на дату среза не найдено.");
|
lines.push("- Подтвержденной открытой дебиторской задолженности на дату среза не найдено.");
|
||||||
}
|
}
|
||||||
|
appendDebtMirrorDisclosure(lines, balanceSnapshot, "receivables");
|
||||||
return {
|
return {
|
||||||
responseType: confirmedBalances.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
|
responseType: confirmedBalances.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
|
||||||
text: joinLines(lines),
|
text: joinLines(lines),
|
||||||
|
|
@ -3588,7 +3538,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
||||||
const carryoverLine = asOfDate || periodFrom || periodTo
|
const carryoverLine = asOfDate || periodFrom || periodTo
|
||||||
? "- В список могут попадать обязательства, возникшие раньше выбранного периода, если они потенциально оставались открытыми на дату среза."
|
? "- В список могут попадать обязательства, возникшие раньше выбранного периода, если они потенциально оставались открытыми на дату среза."
|
||||||
: null;
|
: null;
|
||||||
const formatHeuristicItem = (item, index) => `${index + 1}. ${item.name} | сумма к проверке: ${formatMoneyRub(item.totalAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}`;
|
const formatHeuristicItem = (item, index) => `${index + 1}. ${item.name} | сумма к проверке: ${formatMoneyRub(item.totalAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}`;
|
||||||
const pushCategorySlice = (lines, title, items, limit) => {
|
const pushCategorySlice = (lines, title, items, limit) => {
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -3685,7 +3635,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
||||||
`- ${liabilityCategoryLabel("other")}: ${formatNumberWithDots(categoryCounts.other)}`,
|
`- ${liabilityCategoryLabel("other")}: ${formatNumberWithDots(categoryCounts.other)}`,
|
||||||
"",
|
"",
|
||||||
"Блок 5. Крупнейшие подтвержденные позиции к оплате (по сумме остатка):",
|
"Блок 5. Крупнейшие подтвержденные позиции к оплате (по сумме остатка):",
|
||||||
...confirmedBalances.slice(0, 10).map((item, index) => `${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток к оплате: ${formatMoneyRub(item.outstandingAmount)} | операций в срезе: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`)
|
...confirmedBalances.slice(0, 10).map((item, index) => `${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток к оплате: ${formatMoneyRub(item.outstandingAmount)} | операций в срезе: ${formatNumberWithDots(item.operations)}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`)
|
||||||
];
|
];
|
||||||
appendDebtMirrorDisclosure(lines, balanceSnapshot, "payables");
|
appendDebtMirrorDisclosure(lines, balanceSnapshot, "payables");
|
||||||
return {
|
return {
|
||||||
|
|
@ -3931,15 +3881,34 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
||||||
.filter((item) => Boolean(item)));
|
.filter((item) => Boolean(item)));
|
||||||
const counterparty = resolvePreferredCounterpartyDisplayLabel(options.counterpartyHint, rowCounterparties);
|
const counterparty = resolvePreferredCounterpartyDisplayLabel(options.counterpartyHint, rowCounterparties);
|
||||||
const roleBoundary = bankRoleBoundaryLine(options.userMessage, rows);
|
const roleBoundary = bankRoleBoundaryLine(options.userMessage, rows);
|
||||||
const visibleRows = rows.slice(0, Math.min(rows.length, 5));
|
const visibleRows = [...rows]
|
||||||
|
.sort((left, right) => Math.abs(right.amount ?? 0) - Math.abs(left.amount ?? 0) ||
|
||||||
|
(String(right.period ?? "").localeCompare(String(left.period ?? ""), "ru")))
|
||||||
|
.slice(0, Math.min(rows.length, 5));
|
||||||
|
const semanticSummary = summarizeBankOperationSemantics(rows);
|
||||||
|
const compactRoleAnswer = /(?:клиент|поставщик|финансов|роль|коротко)/iu.test(String(options.userMessage ?? ""));
|
||||||
|
const compactEvidenceRows = visibleRows.map((row, index) => {
|
||||||
|
const direction = bankOperationDirectionLabel(bankOperationDirection(row));
|
||||||
|
const amount = formatMoneyRub(row.amount ?? 0);
|
||||||
|
const period = row.period ? formatDateRu(row.period) : "дата не указана";
|
||||||
|
const operationKind = String(row.operation_kind ?? "").trim();
|
||||||
|
const paymentPurpose = String(row.payment_purpose ?? "").trim();
|
||||||
|
const detail = operationKind || paymentPurpose
|
||||||
|
? ` | ${[operationKind, paymentPurpose].filter(Boolean).join("; ")}`
|
||||||
|
: "";
|
||||||
|
return `${index + 1}. ${period} | ${direction} | ${amount}${detail}`;
|
||||||
|
});
|
||||||
const lines = [
|
const lines = [
|
||||||
`Коротко: найдено банковских операций${counterparty ? ` по ${counterparty}` : " по контрагенту"} — ${rows.length}.`,
|
`Коротко: найдено банковских операций${counterparty ? ` по ${counterparty}` : " по контрагенту"} — ${rows.length}.`,
|
||||||
summarizeBankOperationDirections(rows),
|
summarizeBankOperationDirections(rows),
|
||||||
roleBoundary ?? "Показываю подтвержденные банковские операции из текущего среза.",
|
roleBoundary ?? "Показываю подтвержденные банковские операции из текущего среза.",
|
||||||
bankOperationEvidenceLine(rows, preferredBankEvidenceDirection(options.userMessage)),
|
...(semanticSummary ? [semanticSummary] : [])
|
||||||
...formatTopRows(visibleRows, visibleRows.length)
|
|
||||||
];
|
];
|
||||||
if (rows.length > visibleRows.length) {
|
if (!compactRoleAnswer) {
|
||||||
|
lines.push(bankOperationEvidenceLine(rows, preferredBankEvidenceDirection(options.userMessage)), "Примеры строк 1С:", ...compactEvidenceRows);
|
||||||
|
}
|
||||||
|
lines.push("Следующий шаг: могу отдельно разложить назначения платежа, договоры или отделить банковский контур от клиентского/поставщицкого.");
|
||||||
|
if (!compactRoleAnswer && rows.length > visibleRows.length) {
|
||||||
lines.push(`Показаны первые ${visibleRows.length} из ${rows.length}; полный список остается в подтвержденном срезе.`);
|
lines.push(`Показаны первые ${visibleRows.length} из ${rows.length}; полный список остается в подтвержденном срезе.`);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,23 @@ function findFocusedCounterpartyValuePoint(profileRows, counterpartyHint, deps)
|
||||||
}
|
}
|
||||||
return profileRows.length === 1 ? profileRows[0] : null;
|
return profileRows.length === 1 ? profileRows[0] : null;
|
||||||
}
|
}
|
||||||
|
function hasProfitAmbiguityCue(normalizedQuestion) {
|
||||||
|
return /(?:заработ|прибыл|прибыль|доход|выручк)/iu.test(normalizedQuestion);
|
||||||
|
}
|
||||||
|
function buildCashflowBoundaryLine(isSupplier) {
|
||||||
|
return isSupplier
|
||||||
|
? "Граница ответа: это подтвержденный денежный поток по поставщику, а не итоговая задолженность."
|
||||||
|
: "Граница ответа: это подтвержденный денежный поток по поступлениям, а не чистая прибыль.";
|
||||||
|
}
|
||||||
|
function buildCashflowNextStepLine(isSupplier, normalizedQuestion) {
|
||||||
|
if (isSupplier) {
|
||||||
|
return "Следующий шаг: могу отдельно показать остаток долга, просрочку или расшифровку по документам.";
|
||||||
|
}
|
||||||
|
if (hasProfitAmbiguityCue(normalizedQuestion)) {
|
||||||
|
return "Следующий шаг: могу отдельно проверить чистую прибыль по закрытию 90/91/99.";
|
||||||
|
}
|
||||||
|
return "Следующий шаг: могу разложить поток по месяцам, документам или контрагентам.";
|
||||||
|
}
|
||||||
function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
||||||
if (intent === "counterparty_population_and_roles") {
|
if (intent === "counterparty_population_and_roles") {
|
||||||
const rowsByMarker = groupRowsByMarker(rows);
|
const rowsByMarker = groupRowsByMarker(rows);
|
||||||
|
|
@ -396,6 +413,8 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
||||||
/(?:клиент|заказчик|покупател|контрагент|customer|client|counterparty|buyer)/iu.test(normalizedQuestion);
|
/(?:клиент|заказчик|покупател|контрагент|customer|client|counterparty|buyer)/iu.test(normalizedQuestion);
|
||||||
const semanticSingleBestCounterparty = focus === "top_by_total" && hasSingleBestCounterpartyCue && !asksExplicitRankingList;
|
const semanticSingleBestCounterparty = focus === "top_by_total" && hasSingleBestCounterpartyCue && !asksExplicitRankingList;
|
||||||
const effectiveLimit = asksSingleBestCounterparty || semanticSingleBestCounterparty ? 1 : limit;
|
const effectiveLimit = asksSingleBestCounterparty || semanticSingleBestCounterparty ? 1 : limit;
|
||||||
|
const cashflowBoundaryLine = buildCashflowBoundaryLine(isSupplier);
|
||||||
|
const cashflowNextStepLine = buildCashflowNextStepLine(isSupplier, normalizedQuestion);
|
||||||
const byCounterparty = new Map();
|
const byCounterparty = new Map();
|
||||||
const byYear = new Map();
|
const byYear = new Map();
|
||||||
const deals = [];
|
const deals = [];
|
||||||
|
|
@ -490,10 +509,12 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
||||||
? `за период ${deps.formatDateRu(options.periodFrom)}..${deps.formatDateRu(options.periodTo)}`
|
? `за период ${deps.formatDateRu(options.periodFrom)}..${deps.formatDateRu(options.periodTo)}`
|
||||||
: "за доступное время";
|
: "за доступное время";
|
||||||
const directAnswerLine = isSupplier
|
const directAnswerLine = isSupplier
|
||||||
? `Оборот по ${focusedCounterparty.name} ${periodLabel}: ${deps.formatMoneyRub(focusedCounterparty.total)} по ${focusedCounterparty.ops} подтвержденным исходящим операциям. Это денежный поток по поставщику, а не итоговая задолженность.`
|
? `Оборот по ${focusedCounterparty.name} ${periodLabel}: ${deps.formatMoneyRub(focusedCounterparty.total)} по ${focusedCounterparty.ops} подтвержденным исходящим операциям.`
|
||||||
: `Оборот по ${focusedCounterparty.name} ${periodLabel}: ${deps.formatMoneyRub(focusedCounterparty.total)} по ${focusedCounterparty.ops} подтвержденным входящим операциям. Это денежный поток от клиента, а не чистая прибыль.`;
|
: `Оборот по ${focusedCounterparty.name} ${periodLabel}: ${deps.formatMoneyRub(focusedCounterparty.total)} по ${focusedCounterparty.ops} подтвержденным входящим операциям.`;
|
||||||
const summaryLines = [
|
const summaryLines = [
|
||||||
directAnswerLine,
|
directAnswerLine,
|
||||||
|
cashflowBoundaryLine,
|
||||||
|
...(cashflowNextStepLine ? [cashflowNextStepLine] : []),
|
||||||
"",
|
"",
|
||||||
"Подтверждение:",
|
"Подтверждение:",
|
||||||
`- Контрагент в выборке: ${focusedCounterparty.name}.`,
|
`- Контрагент в выборке: ${focusedCounterparty.name}.`,
|
||||||
|
|
@ -511,11 +532,10 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
||||||
const periodLine = options.periodFrom && options.periodTo
|
const periodLine = options.periodFrom && options.periodTo
|
||||||
? `За период ${deps.formatDateRu(options.periodFrom)}..${deps.formatDateRu(options.periodTo)} подтверждено ${deps.formatMoneyRub(totalFlow)} ${isSupplier ? "исходящих выплат" : "входящих поступлений"}.`
|
? `За период ${deps.formatDateRu(options.periodFrom)}..${deps.formatDateRu(options.periodTo)} подтверждено ${deps.formatMoneyRub(totalFlow)} ${isSupplier ? "исходящих выплат" : "входящих поступлений"}.`
|
||||||
: `За все доступное время подтверждено ${deps.formatMoneyRub(totalFlow)} ${isSupplier ? "исходящих выплат" : "входящих поступлений"}.`;
|
: `За все доступное время подтверждено ${deps.formatMoneyRub(totalFlow)} ${isSupplier ? "исходящих выплат" : "входящих поступлений"}.`;
|
||||||
const directAnswerLine = isSupplier
|
|
||||||
? periodLine
|
|
||||||
: `${periodLine} Это денежный поток от клиентов, а не чистая прибыль.`;
|
|
||||||
const summaryLines = [
|
const summaryLines = [
|
||||||
directAnswerLine,
|
periodLine,
|
||||||
|
cashflowBoundaryLine,
|
||||||
|
...(cashflowNextStepLine ? [cashflowNextStepLine] : []),
|
||||||
"",
|
"",
|
||||||
"Подтверждение:",
|
"Подтверждение:",
|
||||||
`- Операций в выборке: ${totalOperations}.`,
|
`- Операций в выборке: ${totalOperations}.`,
|
||||||
|
|
@ -538,11 +558,17 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
||||||
const strongestYear = visible[0];
|
const strongestYear = visible[0];
|
||||||
const directAnswerLine = isSupplier
|
const directAnswerLine = isSupplier
|
||||||
? `Самый крупный год по подтвержденным выплатам: ${strongestYear.year} (${deps.formatMoneyRub(strongestYear.total)} по ${strongestYear.ops} операциям).`
|
? `Самый крупный год по подтвержденным выплатам: ${strongestYear.year} (${deps.formatMoneyRub(strongestYear.total)} по ${strongestYear.ops} операциям).`
|
||||||
: `Самый доходный год по подтвержденным поступлениям: ${strongestYear.year} (${deps.formatMoneyRub(strongestYear.total)} по ${strongestYear.ops} операциям). Это денежный поток, а не чистая прибыль.`;
|
: `Самый доходный год по подтвержденным поступлениям: ${strongestYear.year} (${deps.formatMoneyRub(strongestYear.total)} по ${strongestYear.ops} операциям).`;
|
||||||
const heading = isSupplier
|
const heading = isSupplier
|
||||||
? `Топ-${visible.length} лет по сумме выплат:`
|
? `Топ-${visible.length} лет по сумме выплат:`
|
||||||
: `Топ-${visible.length} лет по сумме поступлений:`;
|
: `Топ-${visible.length} лет по сумме поступлений:`;
|
||||||
lines.unshift(heading);
|
lines.unshift(heading);
|
||||||
|
if (!isSupplier) {
|
||||||
|
lines.unshift(cashflowBoundaryLine);
|
||||||
|
if (cashflowNextStepLine) {
|
||||||
|
lines.unshift(cashflowNextStepLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
lines.unshift(directAnswerLine);
|
lines.unshift(directAnswerLine);
|
||||||
lines.push(...visible.map((item, index) => `${index + 1}. ${item.year} | сумма: ${deps.formatMoneyRub(item.total)} | операций: ${item.ops} | контрагентов: ${item.counterparties.size} | максимальная разовая сумма: ${deps.formatMoneyRub(item.maxSingle)}`));
|
lines.push(...visible.map((item, index) => `${index + 1}. ${item.year} | сумма: ${deps.formatMoneyRub(item.total)} | операций: ${item.ops} | контрагентов: ${item.counterparties.size} | максимальная разовая сумма: ${deps.formatMoneyRub(item.maxSingle)}`));
|
||||||
}
|
}
|
||||||
|
|
@ -622,11 +648,17 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
||||||
const directAnswerLine = singleCandidateOnly
|
const directAnswerLine = singleCandidateOnly
|
||||||
? isSupplier
|
? isSupplier
|
||||||
? `В выбранном срезе найден один поставщик: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
? `В выбранном срезе найден один поставщик: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
||||||
: `В выбранном срезе найден один клиент: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг; сумма является денежным потоком, а не чистой прибылью.`
|
: `В выбранном срезе найден один клиент: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
||||||
: isSupplier
|
: isSupplier
|
||||||
? `Крупнейший поставщик по подтвержденным выплатам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
? `Крупнейший поставщик по подтвержденным выплатам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
||||||
: `Самый доходный клиент ${rankingPeriodLabel} по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это денежный поток, а не чистая прибыль.`;
|
: `Самый доходный клиент ${rankingPeriodLabel} по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`;
|
||||||
lines.unshift(directAnswerLine);
|
lines.unshift(directAnswerLine);
|
||||||
|
if (!isSupplier) {
|
||||||
|
lines.splice(1, 0, cashflowBoundaryLine);
|
||||||
|
if (cashflowNextStepLine) {
|
||||||
|
lines.splice(2, 0, cashflowNextStepLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
lines.push(...visible.map((item, index) => {
|
lines.push(...visible.map((item, index) => {
|
||||||
const avgCheck = item.ops > 0 ? item.total / item.ops : 0;
|
const avgCheck = item.ops > 0 ? item.total / item.ops : 0;
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ exports.hasBareInventoryPurchaseDateFollowupCue = hasBareInventoryPurchaseDateFo
|
||||||
exports.hasInventorySaleFollowupCue = hasInventorySaleFollowupCue;
|
exports.hasInventorySaleFollowupCue = hasInventorySaleFollowupCue;
|
||||||
exports.hasInventoryPurchaseToSaleChainFollowupCue = hasInventoryPurchaseToSaleChainFollowupCue;
|
exports.hasInventoryPurchaseToSaleChainFollowupCue = hasInventoryPurchaseToSaleChainFollowupCue;
|
||||||
exports.hasInventoryPurchaseDateVatBridgeCue = hasInventoryPurchaseDateVatBridgeCue;
|
exports.hasInventoryPurchaseDateVatBridgeCue = hasInventoryPurchaseDateVatBridgeCue;
|
||||||
|
exports.hasInventoryMarginRankingFollowupCue = hasInventoryMarginRankingFollowupCue;
|
||||||
exports.hasAddressFollowupContextSignal = hasAddressFollowupContextSignal;
|
exports.hasAddressFollowupContextSignal = hasAddressFollowupContextSignal;
|
||||||
exports.runAddressDecomposeStage = runAddressDecomposeStage;
|
exports.runAddressDecomposeStage = runAddressDecomposeStage;
|
||||||
const addressQueryClassifier_1 = require("../addressQueryClassifier");
|
const addressQueryClassifier_1 = require("../addressQueryClassifier");
|
||||||
|
|
@ -321,6 +322,7 @@ function isInventoryIntent(intent) {
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date");
|
intent === "inventory_aging_by_purchase_date");
|
||||||
|
|
@ -332,6 +334,7 @@ function isInventoryDrilldownFrameIntent(intent) {
|
||||||
return (intent === "inventory_purchase_provenance_for_item" ||
|
return (intent === "inventory_purchase_provenance_for_item" ||
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date");
|
intent === "inventory_aging_by_purchase_date");
|
||||||
|
|
@ -340,6 +343,7 @@ function isInventoryLifecycleHistoryIntent(intent) {
|
||||||
return (intent === "inventory_purchase_provenance_for_item" ||
|
return (intent === "inventory_purchase_provenance_for_item" ||
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain");
|
intent === "inventory_purchase_to_sale_chain");
|
||||||
}
|
}
|
||||||
|
|
@ -625,11 +629,29 @@ function hasInventoryPurchaseDateVatBridgeCue(text) {
|
||||||
return (/(?:ндс|vat)/iu.test(normalized) &&
|
return (/(?:ндс|vat)/iu.test(normalized) &&
|
||||||
/(?:на\s+дат[ауеы]\s+покупк|на\s+дат[ауеы]\s+закупк|по\s+дат[еу]\s+покупк|по\s+дат[еу]\s+закупк|дата\s+покупк|дата\s+закупк|purchase\s+date)/iu.test(normalized));
|
/(?:на\s+дат[ауеы]\s+покупк|на\s+дат[ауеы]\s+закупк|по\s+дат[еу]\s+покупк|по\s+дат[еу]\s+закупк|дата\s+покупк|дата\s+закупк|purchase\s+date)/iu.test(normalized));
|
||||||
}
|
}
|
||||||
|
function hasInventoryMarginRankingFollowupCue(text) {
|
||||||
|
const normalized = textWithRepairedVariant(String(text ?? ""))
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ё/g, "е");
|
||||||
|
if (!normalized.trim()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const wantsFoundRows = /(?:покажи|показать|выведи|дай|раскрой|show|list|покажи|показать|выведи|дай|раскрой)/iu.test(normalized) &&
|
||||||
|
/(?:найденн|строк|реализац|себестоимостн|баз|найденн|строк|реализац|себестоимостн|баз)/iu.test(normalized) &&
|
||||||
|
/(?:себестоимостн|реализац|марж|прибыл|номенклатур|себестоимостн|реализац|марж|прибыл|номенклат)/iu.test(normalized);
|
||||||
|
const account41Not01 = /\b41(?:[.,]\d{1,2})?\b/iu.test(normalized) &&
|
||||||
|
/\b01(?:[.,]\d{1,2})?\b/iu.test(normalized) &&
|
||||||
|
/(?:\bне\b|вместо|а\s+не|not|instead|РЅРµ|вместо|Р°\s+РЅРµ)/iu.test(normalized);
|
||||||
|
return wantsFoundRows || account41Not01;
|
||||||
|
}
|
||||||
function hasAddressFollowupContextSignal(text) {
|
function hasAddressFollowupContextSignal(text) {
|
||||||
const normalized = String(text ?? "").trim();
|
const normalized = String(text ?? "").trim();
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (hasInventoryMarginRankingFollowupCue(normalized)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (/(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(normalized)) {
|
if (/(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(normalized)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -867,6 +889,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date" ||
|
intent === "inventory_aging_by_purchase_date" ||
|
||||||
|
|
@ -919,6 +942,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
if ((intent === "inventory_purchase_provenance_for_item" ||
|
if ((intent === "inventory_purchase_provenance_for_item" ||
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date")) {
|
intent === "inventory_aging_by_purchase_date")) {
|
||||||
|
|
@ -1122,6 +1146,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date" ||
|
intent === "inventory_aging_by_purchase_date" ||
|
||||||
|
|
@ -1134,6 +1159,8 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
const currentContractExplicit = toNonEmptyString(merged.contract);
|
const currentContractExplicit = toNonEmptyString(merged.contract);
|
||||||
const currentItemExplicit = toNonEmptyString(merged.item);
|
const currentItemExplicit = toNonEmptyString(merged.item);
|
||||||
const currentAccountExplicit = toNonEmptyString(merged.account);
|
const currentAccountExplicit = toNonEmptyString(merged.account);
|
||||||
|
const currentAccountRefinesMarginDomain = intent === "inventory_margin_ranking_for_nomenclature" &&
|
||||||
|
hasInventoryMarginRankingFollowupCue(userMessage);
|
||||||
const shouldSuppressGenericPeriodCarryover = (Boolean(currentCounterpartyExplicit) &&
|
const shouldSuppressGenericPeriodCarryover = (Boolean(currentCounterpartyExplicit) &&
|
||||||
!isLowQualityCounterpartyAnchor(currentCounterpartyExplicit) &&
|
!isLowQualityCounterpartyAnchor(currentCounterpartyExplicit) &&
|
||||||
currentCounterpartyExplicit !== previousCounterparty) ||
|
currentCounterpartyExplicit !== previousCounterparty) ||
|
||||||
|
|
@ -1141,7 +1168,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
!isLowQualityContractAnchor(currentContractExplicit) &&
|
!isLowQualityContractAnchor(currentContractExplicit) &&
|
||||||
currentContractExplicit !== previousContract) ||
|
currentContractExplicit !== previousContract) ||
|
||||||
(Boolean(currentItemExplicit) && currentItemExplicit !== previousItem) ||
|
(Boolean(currentItemExplicit) && currentItemExplicit !== previousItem) ||
|
||||||
(Boolean(currentAccountExplicit) && currentAccountExplicit !== previousAccount);
|
(Boolean(currentAccountExplicit) && currentAccountExplicit !== previousAccount && !currentAccountRefinesMarginDomain);
|
||||||
const vatRelativeMonthFollowup = relativeMonthFromFollowupYear &&
|
const vatRelativeMonthFollowup = relativeMonthFromFollowupYear &&
|
||||||
(intent === "vat_payable_confirmed_as_of_date" ||
|
(intent === "vat_payable_confirmed_as_of_date" ||
|
||||||
intent === "vat_payable_forecast" ||
|
intent === "vat_payable_forecast" ||
|
||||||
|
|
@ -1189,6 +1216,19 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
}
|
}
|
||||||
reasons.push("period_from_followup_context");
|
reasons.push("period_from_followup_context");
|
||||||
}
|
}
|
||||||
|
if (intent === "inventory_margin_ranking_for_nomenclature" &&
|
||||||
|
previousHasPeriod &&
|
||||||
|
hasInventoryMarginRankingFollowupCue(userMessage) &&
|
||||||
|
!hasExplicitPeriodInMessage &&
|
||||||
|
!hasExplicitCurrentDateInMessage) {
|
||||||
|
if (previousPeriodFrom && merged.period_from !== previousPeriodFrom) {
|
||||||
|
merged.period_from = previousPeriodFrom;
|
||||||
|
}
|
||||||
|
if (previousPeriodTo && merged.period_to !== previousPeriodTo) {
|
||||||
|
merged.period_to = previousPeriodTo;
|
||||||
|
}
|
||||||
|
reasons.push("period_from_followup_context");
|
||||||
|
}
|
||||||
if (!currentHasPeriod &&
|
if (!currentHasPeriod &&
|
||||||
previousHasPeriod &&
|
previousHasPeriod &&
|
||||||
hasFollowupSignal &&
|
hasFollowupSignal &&
|
||||||
|
|
@ -1251,6 +1291,7 @@ function resolveMissingRequiredFilters(intent, filters) {
|
||||||
account_balance_snapshot: ["account", "as_of_date"],
|
account_balance_snapshot: ["account", "as_of_date"],
|
||||||
documents_forming_balance: ["account", "as_of_date"],
|
documents_forming_balance: ["account", "as_of_date"],
|
||||||
inventory_on_hand_as_of_date: ["as_of_date"],
|
inventory_on_hand_as_of_date: ["as_of_date"],
|
||||||
|
inventory_margin_ranking_for_nomenclature: ["period_from", "period_to"],
|
||||||
inventory_profitability_for_item: ["item"],
|
inventory_profitability_for_item: ["item"],
|
||||||
open_contracts_confirmed_as_of_date: ["as_of_date"],
|
open_contracts_confirmed_as_of_date: ["as_of_date"],
|
||||||
payables_confirmed_as_of_date: ["as_of_date"],
|
payables_confirmed_as_of_date: ["as_of_date"],
|
||||||
|
|
@ -1319,6 +1360,22 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
||||||
const hasExplicitInventoryItemReference = /(?:товар|номенклатур|позици|склад|остат|sku|item|product|товар|номенклатур|позици|склад|остат)/iu.test(normalizedMessage) || hasSelectedObjectInlineSnapshotMetadata(normalizedMessage);
|
const hasExplicitInventoryItemReference = /(?:товар|номенклатур|позици|склад|остат|sku|item|product|товар|номенклатур|позици|склад|остат)/iu.test(normalizedMessage) || hasSelectedObjectInlineSnapshotMetadata(normalizedMessage);
|
||||||
const staleInventoryLineageCanYieldToCounterparty = previousCounterpartyLaneActive && !hasExplicitInventoryItemReference;
|
const staleInventoryLineageCanYieldToCounterparty = previousCounterpartyLaneActive && !hasExplicitInventoryItemReference;
|
||||||
const inventoryPurchaseDateVatBridge = inventorySelectedObjectFollowup && hasInventoryPurchaseDateVatBridgeCue(normalizedMessage);
|
const inventoryPurchaseDateVatBridge = inventorySelectedObjectFollowup && hasInventoryPurchaseDateVatBridgeCue(normalizedMessage);
|
||||||
|
const marginRankingLineageActive = sourceIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
|
fallbackIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
|
followupContext.root_intent === "inventory_margin_ranking_for_nomenclature";
|
||||||
|
if (marginRankingLineageActive &&
|
||||||
|
hasInventoryMarginRankingFollowupCue(normalizedMessage) &&
|
||||||
|
(detectedIntent.intent === "unknown" ||
|
||||||
|
detectedIntent.intent === "account_balance_snapshot" ||
|
||||||
|
detectedIntent.intent === "documents_forming_balance" ||
|
||||||
|
detectedIntent.intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
|
detectedIntent.intent === sourceIntent)) {
|
||||||
|
return {
|
||||||
|
intent: "inventory_margin_ranking_for_nomenclature",
|
||||||
|
confidence: "low",
|
||||||
|
reasons: [...detectedIntent.reasons, "intent_adjusted_to_inventory_margin_ranking_followup_context"]
|
||||||
|
};
|
||||||
|
}
|
||||||
if (inventoryPurchaseDateVatBridge &&
|
if (inventoryPurchaseDateVatBridge &&
|
||||||
(detectedIntent.intent === "unknown" ||
|
(detectedIntent.intent === "unknown" ||
|
||||||
detectedIntent.intent === sourceIntent ||
|
detectedIntent.intent === sourceIntent ||
|
||||||
|
|
|
||||||
|
|
@ -81,21 +81,76 @@ function inventoryProfitabilityPeriodLabel(options, deps) {
|
||||||
const asOfDate = typeof options.asOfDate === "string" && options.asOfDate.trim().length > 0 ? options.asOfDate : null;
|
const asOfDate = typeof options.asOfDate === "string" && options.asOfDate.trim().length > 0 ? options.asOfDate : null;
|
||||||
return asOfDate ? `до ${deps.formatDateRu(asOfDate)}` : "по доступной выборке";
|
return asOfDate ? `до ${deps.formatDateRu(asOfDate)}` : "по доступной выборке";
|
||||||
}
|
}
|
||||||
|
function asksForInventoryCostBaseRows(userMessage) {
|
||||||
|
const text = String(userMessage ?? "").toLowerCase();
|
||||||
|
if (!/(?:покажи|показать|выведи|вывести|дай|дать|раскрой|раскрыть|строк|строки|строку|баз)/iu.test(text)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(?:себестоимостн|себестоимост|себестоим|закупочн|закупк|90\.02|\b41\b|баз)/iu.test(text);
|
||||||
|
}
|
||||||
|
function inventoryRowItemLabel(row, deps) {
|
||||||
|
return deps.summarizeInventoryTraceRows([row]).item;
|
||||||
|
}
|
||||||
|
function buildInventoryMarginRankingEntries(rows, deps) {
|
||||||
|
const byItem = new Map();
|
||||||
|
for (const row of rows) {
|
||||||
|
const item = inventoryRowItemLabel(row, deps);
|
||||||
|
if (!item) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const key = item.trim().toLocaleLowerCase("ru");
|
||||||
|
const current = byItem.get(key) ?? { item, saleRows: [], purchaseRows: [] };
|
||||||
|
if (deps.isInventorySaleMovement(row)) {
|
||||||
|
current.saleRows.push(row);
|
||||||
|
}
|
||||||
|
if (deps.isInventoryPurchaseMovement(row)) {
|
||||||
|
current.purchaseRows.push(row);
|
||||||
|
}
|
||||||
|
byItem.set(key, current);
|
||||||
|
}
|
||||||
|
return Array.from(byItem.values())
|
||||||
|
.map((entry) => {
|
||||||
|
const revenue = sumInventoryRowAmount(entry.saleRows);
|
||||||
|
const costProxy = sumInventoryRowAmount(entry.purchaseRows);
|
||||||
|
const spread = revenue - costProxy;
|
||||||
|
return {
|
||||||
|
item: entry.item,
|
||||||
|
revenue,
|
||||||
|
costProxy,
|
||||||
|
spread,
|
||||||
|
marginPct: revenue > 0 ? (spread / revenue) * 100 : null,
|
||||||
|
saleQuantity: sumInventoryRowQuantity(entry.saleRows),
|
||||||
|
purchaseQuantity: sumInventoryRowQuantity(entry.purchaseRows),
|
||||||
|
saleDocuments: entry.saleRows.length,
|
||||||
|
purchaseDocuments: entry.purchaseRows.length
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((entry) => entry.revenue > 0 || entry.costProxy > 0);
|
||||||
|
}
|
||||||
|
function formatInventoryMarginRankingLine(entry, index, deps) {
|
||||||
|
return `${index + 1}. ${entry.item} — выручка ${deps.formatMoneyRub(entry.revenue)}, себестоимостная база ${deps.formatMoneyRub(entry.costProxy)}, валовая разница ${deps.formatMoneyRub(entry.spread)}, маржа ${formatInventoryPercent(entry.marginPct, deps.formatNumberWithDots)}.`;
|
||||||
|
}
|
||||||
function composeInventoryReply(intent, rows, options, deps) {
|
function composeInventoryReply(intent, rows, options, deps) {
|
||||||
if (intent === "inventory_on_hand_as_of_date") {
|
if (intent === "inventory_on_hand_as_of_date") {
|
||||||
const asOfDate = deps.resolvePayablesAsOfDate(options);
|
const asOfDate = deps.resolvePayablesAsOfDate(options);
|
||||||
const positions = deps.buildInventoryOnHandAggregate(rows, asOfDate);
|
const positions = deps.buildInventoryOnHandAggregate(rows, asOfDate);
|
||||||
const uniqueItems = deps.uniqueStrings(positions.map((item) => item.item));
|
const uniqueItems = deps.uniqueStrings(positions.map((item) => item.item));
|
||||||
const uniqueWarehouses = deps.uniqueStrings(positions.map((item) => String(item.warehouse ?? "").trim()).filter((item) => item.length > 0));
|
const uniqueWarehouses = deps.uniqueStrings(positions.map((item) => String(item.warehouse ?? "").trim()).filter((item) => item.length > 0));
|
||||||
const totalQuantity = positions.reduce((sum, item) => sum + item.quantity, 0);
|
|
||||||
const totalAmount = positions.reduce((sum, item) => sum + item.amount, 0);
|
const totalAmount = positions.reduce((sum, item) => sum + item.amount, 0);
|
||||||
const directAnswerLine = positions.length > 0
|
const directAnswerLine = positions.length > 0
|
||||||
? `На ${deps.formatDateRu(asOfDate)} на складе подтверждено ${deps.formatNumberWithDots(positions.length)} позиций с остатком на ${deps.formatMoneyRub(totalAmount)}.`
|
? `На ${deps.formatDateRu(asOfDate)} на складе подтверждено ${deps.formatNumberWithDots(positions.length)} позиций на ${deps.formatMoneyRub(totalAmount)}.`
|
||||||
: `На ${deps.formatDateRu(asOfDate)} подтвержденных товарных остатков по счету 41.01 не найдено.`;
|
: `На ${deps.formatDateRu(asOfDate)} подтвержденных товарных остатков по счету 41.01 не найдено.`;
|
||||||
const lines = [directAnswerLine];
|
const lines = [directAnswerLine];
|
||||||
if (positions.length > 0) {
|
if (positions.length > 0) {
|
||||||
const visiblePositionsLimit = 6;
|
const visiblePositionsLimit = 6;
|
||||||
const visiblePositions = positions.slice(0, visiblePositionsLimit);
|
const positionsByAmount = [...positions].sort((left, right) => {
|
||||||
|
const amountDelta = right.amount - left.amount;
|
||||||
|
if (amountDelta !== 0) {
|
||||||
|
return amountDelta;
|
||||||
|
}
|
||||||
|
return left.item.localeCompare(right.item, "ru");
|
||||||
|
});
|
||||||
|
const visiblePositions = positionsByAmount.slice(0, visiblePositionsLimit);
|
||||||
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Позиции:", visiblePositions.map((item, index) => (0, inventoryReplyPresentation_1.formatInventorySnapshotPositionLine)(item, index, {
|
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Позиции:", visiblePositions.map((item, index) => (0, inventoryReplyPresentation_1.formatInventorySnapshotPositionLine)(item, index, {
|
||||||
formatDateRu: deps.formatDateRu,
|
formatDateRu: deps.formatDateRu,
|
||||||
formatNumberWithDots: deps.formatNumberWithDots,
|
formatNumberWithDots: deps.formatNumberWithDots,
|
||||||
|
|
@ -115,11 +170,14 @@ function composeInventoryReply(intent, rows, options, deps) {
|
||||||
`Позиции с остатком: ${deps.formatNumberWithDots(positions.length)}.`,
|
`Позиции с остатком: ${deps.formatNumberWithDots(positions.length)}.`,
|
||||||
`Уникальных товаров: ${deps.formatNumberWithDots(uniqueItems.length)}.`,
|
`Уникальных товаров: ${deps.formatNumberWithDots(uniqueItems.length)}.`,
|
||||||
`Уникальных складов: ${deps.formatNumberWithDots(uniqueWarehouses.length)}.`,
|
`Уникальных складов: ${deps.formatNumberWithDots(uniqueWarehouses.length)}.`,
|
||||||
`Суммарное количество: ${deps.formatNumberWithDots(totalQuantity, 3)}.`
|
"Общее количество не свожу в один управленческий показатель, потому что в остатках смешаны разнородные позиции."
|
||||||
]);
|
]);
|
||||||
if (rows.length !== positions.length) {
|
if (rows.length !== positions.length) {
|
||||||
lines.push(`- Проверенных строк движения: ${deps.formatNumberWithDots(rows.length)}.`);
|
lines.push(`- Проверенных строк движения: ${deps.formatNumberWithDots(rows.length)}.`);
|
||||||
}
|
}
|
||||||
|
if (positions.length > 0) {
|
||||||
|
lines.push("- Следующий шаг: могу раскрыть полный список, разложить остатки по складам или сравнить с другой датой.");
|
||||||
|
}
|
||||||
return positions.length > 0
|
return positions.length > 0
|
||||||
? (0, replyContracts_1.buildFactualListReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)("strong"))
|
? (0, replyContracts_1.buildFactualListReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)("strong"))
|
||||||
: (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)("medium"));
|
: (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)("medium"));
|
||||||
|
|
@ -392,6 +450,91 @@ function composeInventoryReply(intent, rows, options, deps) {
|
||||||
? (0, replyContracts_1.buildFactualListReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(summary.counterparties.length > 0 ? "strong" : "medium", true))
|
? (0, replyContracts_1.buildFactualListReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(summary.counterparties.length > 0 ? "strong" : "medium", true))
|
||||||
: (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)("medium", false));
|
: (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)("medium", false));
|
||||||
}
|
}
|
||||||
|
if (intent === "inventory_margin_ranking_for_nomenclature") {
|
||||||
|
const entries = buildInventoryMarginRankingEntries(rows, deps);
|
||||||
|
const confirmedEntries = entries.filter((entry) => entry.revenue > 0 && entry.costProxy > 0);
|
||||||
|
const highMargin = [...confirmedEntries]
|
||||||
|
.sort((left, right) => right.spread - left.spread || (right.marginPct ?? -Infinity) - (left.marginPct ?? -Infinity))
|
||||||
|
.slice(0, 5);
|
||||||
|
const lowMargin = [...confirmedEntries]
|
||||||
|
.sort((left, right) => left.spread - right.spread || (left.marginPct ?? Infinity) - (right.marginPct ?? Infinity))
|
||||||
|
.slice(0, 5);
|
||||||
|
const salesWithoutCost = entries.filter((entry) => entry.revenue > 0 && entry.costProxy <= 0);
|
||||||
|
const purchasesWithoutSales = entries.filter((entry) => entry.costProxy > 0 && entry.revenue <= 0);
|
||||||
|
const periodLabel = inventoryProfitabilityPeriodLabel(options, deps);
|
||||||
|
const totalRevenue = entries.reduce((sum, entry) => sum + entry.revenue, 0);
|
||||||
|
const totalCostProxy = entries.reduce((sum, entry) => sum + entry.costProxy, 0);
|
||||||
|
const totalSpread = totalRevenue - totalCostProxy;
|
||||||
|
if (confirmedEntries.length === 0) {
|
||||||
|
const costBaseRowsRequested = asksForInventoryCostBaseRows(options.userMessage);
|
||||||
|
const lines = [
|
||||||
|
costBaseRowsRequested && purchasesWithoutSales.length === 0
|
||||||
|
? `За период ${periodLabel} подтвержденных строк себестоимостной базы по реализованной номенклатуре не найдено.`
|
||||||
|
: `За период ${periodLabel} рейтинг прибыльности номенклатуры построить нельзя.`
|
||||||
|
];
|
||||||
|
const findings = [];
|
||||||
|
if (salesWithoutCost.length > 0) {
|
||||||
|
const salesCount = deps.formatNumberWithDots(salesWithoutCost.length);
|
||||||
|
const salesItemPhrase = salesWithoutCost.length === 1 ? "1 номенклатурной позиции" : `${salesCount} номенклатурным позициям`;
|
||||||
|
findings.push(`Есть реализация по ${salesItemPhrase}.`);
|
||||||
|
findings.push(salesWithoutCost.length === 1
|
||||||
|
? "Подтвержденной себестоимости реализации по этой позиции не найдено."
|
||||||
|
: "Подтвержденной себестоимости реализации по этим позициям не найдено.");
|
||||||
|
findings.push("Поэтому валовую прибыль и маржинальность честно посчитать нельзя.");
|
||||||
|
}
|
||||||
|
if (costBaseRowsRequested && purchasesWithoutSales.length === 0) {
|
||||||
|
findings.push("Строк себестоимости реализации / себестоимостной базы для показа нет.");
|
||||||
|
}
|
||||||
|
if (purchasesWithoutSales.length > 0) {
|
||||||
|
const purchaseCount = deps.formatNumberWithDots(purchasesWithoutSales.length);
|
||||||
|
const purchaseItemPhrase = purchasesWithoutSales.length === 1 ? "1 позиции" : `${purchaseCount} позициям`;
|
||||||
|
findings.push(purchasesWithoutSales.length === 1
|
||||||
|
? `Есть себестоимостная база по ${purchaseItemPhrase}, но реализации по ней в периоде не найдено.`
|
||||||
|
: `Есть себестоимостная база по ${purchaseItemPhrase}, но реализации по ним в периоде не найдено.`);
|
||||||
|
}
|
||||||
|
if (entries.length === 0) {
|
||||||
|
findings.push("В доступной выборке нет достаточных строк реализации и себестоимости по номенклатуре.");
|
||||||
|
}
|
||||||
|
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Что нашлось:", findings);
|
||||||
|
lines.push(`Вывод: за период ${periodLabel} нет достаточной базы для рейтинга «высокая / низкая прибыль» по номенклатуре.`);
|
||||||
|
const nextActions = [];
|
||||||
|
if (salesWithoutCost.length > 0) {
|
||||||
|
nextActions.push("показать найденные реализации за этот период;");
|
||||||
|
}
|
||||||
|
if (purchasesWithoutSales.length > 0) {
|
||||||
|
nextActions.push("показать найденные строки себестоимостной базы за этот период;");
|
||||||
|
}
|
||||||
|
nextActions.push("расширить период до квартала или года;", "попробовать строгий расчет по проводкам 90.01 / 90.02;", "построить управленческий proxy по закупочным документам, если такой способ допустим для вашей проверки.");
|
||||||
|
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Что можно сделать дальше:", nextActions);
|
||||||
|
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Граница ответа:", [
|
||||||
|
"Прибыльность номенклатуры считаю только когда есть реализация и подтвержденная себестоимость реализации.",
|
||||||
|
"Это не показатель чистой прибыли и не замена закрытию месяца."
|
||||||
|
]);
|
||||||
|
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(entries.length > 0 ? "medium" : "weak", false));
|
||||||
|
}
|
||||||
|
const directAnswerLine = confirmedEntries.length > 0
|
||||||
|
? `За период ${periodLabel} собран рейтинг реализованной номенклатуры по валовой маржинальности: выручка ${deps.formatMoneyRub(totalRevenue)}, себестоимостная база ${deps.formatMoneyRub(totalCostProxy)}, расчетная валовая разница ${deps.formatMoneyRub(totalSpread)}.`
|
||||||
|
: `За период ${periodLabel} не удалось подтвердить рейтинг прибыльности номенклатуры: нужны одновременно строки реализации и закупочного/себестоимостного следа по товарам.`;
|
||||||
|
const lines = [directAnswerLine];
|
||||||
|
if (highMargin.length > 0) {
|
||||||
|
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Высокая валовая маржинальность:", highMargin.map((entry, index) => formatInventoryMarginRankingLine(entry, index, deps)));
|
||||||
|
}
|
||||||
|
if (lowMargin.length > 0) {
|
||||||
|
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Низкая или отрицательная валовая маржинальность:", lowMargin.map((entry, index) => formatInventoryMarginRankingLine(entry, index, deps)));
|
||||||
|
}
|
||||||
|
const boundaryLines = [
|
||||||
|
"Это управленческий расчет валовой маржинальности по реализации и доступной себестоимостной базе, не показатель чистой прибыли.",
|
||||||
|
"Для строгого бухгалтерского расчета нужны проводки 90.01 / 90.02 и закрытие себестоимости; этот ответ не подменяет закрытие месяца."
|
||||||
|
];
|
||||||
|
if (salesWithoutCost.length > 0) {
|
||||||
|
boundaryLines.push(`По ${deps.formatNumberWithDots(salesWithoutCost.length)} позициям есть продажи, но нет подтвержденной себестоимости реализации — их нельзя честно ранжировать по прибыли.`);
|
||||||
|
}
|
||||||
|
if (purchasesWithoutSales.length > 0) {
|
||||||
|
boundaryLines.push(`По ${deps.formatNumberWithDots(purchasesWithoutSales.length)} позициям есть себестоимостная база без реализации в этом периоде.`);
|
||||||
|
}
|
||||||
|
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Граница ответа:", boundaryLines);
|
||||||
|
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(confirmedEntries.length > 0 ? "strong" : entries.length > 0 ? "medium" : "weak", confirmedEntries.length > 0));
|
||||||
|
}
|
||||||
if (intent === "inventory_profitability_for_item") {
|
if (intent === "inventory_profitability_for_item") {
|
||||||
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
|
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
|
||||||
const saleRows = rows.filter((row) => deps.isInventorySaleMovement(row));
|
const saleRows = rows.filter((row) => deps.isInventorySaleMovement(row));
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,11 @@ function sessionOrganizationName(sessionOrganizationScope, toNonEmptyString) {
|
||||||
const scope = toRecordObject(sessionOrganizationScope);
|
const scope = toRecordObject(sessionOrganizationScope);
|
||||||
return toNonEmptyString(scope?.selectedOrganization) ?? toNonEmptyString(scope?.activeOrganization);
|
return toNonEmptyString(scope?.selectedOrganization) ?? toNonEmptyString(scope?.activeOrganization);
|
||||||
}
|
}
|
||||||
|
function sessionKnownOrganizations(sessionOrganizationScope) {
|
||||||
|
const scope = toRecordObject(sessionOrganizationScope);
|
||||||
|
const knownOrganizations = scope?.knownOrganizations;
|
||||||
|
return Array.isArray(knownOrganizations) ? knownOrganizations : [];
|
||||||
|
}
|
||||||
function predecomposeOrganizationName(predecomposeContract, toNonEmptyString) {
|
function predecomposeOrganizationName(predecomposeContract, toNonEmptyString) {
|
||||||
const entities = toRecordObject(predecomposeContract?.entities);
|
const entities = toRecordObject(predecomposeContract?.entities);
|
||||||
return (toNonEmptyString(entities?.organization) ??
|
return (toNonEmptyString(entities?.organization) ??
|
||||||
|
|
@ -46,6 +51,106 @@ function mergeOrganizationIntoDiscoveryFollowupContext(followupContext, organiza
|
||||||
}
|
}
|
||||||
return base;
|
return base;
|
||||||
}
|
}
|
||||||
|
function compactLower(value) {
|
||||||
|
return String(value ?? "")
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
function hasCompactCashflowFollowupSignal(text) {
|
||||||
|
const value = compactLower(text);
|
||||||
|
if (!value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(?:\u0431\u0435\u0437\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0431\u0438\u0432\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0440\u0435\u0437\p{L}*|\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b\p{L}*|\u043e\u0434\u043d\u043e\u0439\s+\u0441\u0442\u0440\u043e\u043a\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0438\u0442\u043e\u0433|\u043f\u0440\u0438\u0448\p{L}*[\s\S]{0,80}\u0443\u0448\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|\u043f\u043e\u043b\u0443\u0447\p{L}*[\s\S]{0,80}\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*[\s\S]{0,80}\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b)/iu.test(value);
|
||||||
|
}
|
||||||
|
function dateScopeToFilterWindow(dateScope) {
|
||||||
|
if (!dateScope) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (/^(?:19|20)\d{2}$/.test(dateScope)) {
|
||||||
|
return {
|
||||||
|
period_from: `${dateScope}-01-01`,
|
||||||
|
period_to: `${dateScope}-12-31`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (/^(?:19|20)\d{2}-\d{2}-\d{2}$/.test(dateScope)) {
|
||||||
|
return { as_of_date: dateScope };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function looksLikeReliableOrganizationScope(value) {
|
||||||
|
const text = compactLower(value);
|
||||||
|
if (!text) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (/(?:\u0440\u0430\u0437\u0431\u0438\u0432|\u0440\u0430\u0437\u0440\u0435\u0437|\u0432\u0445\u043e\u0434\u044f\u0449|\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u043d\u0435\u0442\u0442\u043e|\u0442\u043e\u043f)/iu.test(text)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(?:^|[\s"'\u00ab])(?:\u043e\u043e\u043e|\u0438\u043f|\u043f\u0430\u043e|\u0430\u043e|\u0437\u0430\u043e)(?:[\s"'\u00bb]|$)|\u043e\u0431\u0449\u0435\u0441\u0442\u0432\p{L}*\s+\u0441\s+\u043e\u0433\u0440\u0430\u043d\u0438\u0447\p{L}*\s+\u043e\u0442\u0432\p{L}*|\b(?:llc|inc|corp)\b/iu.test(text);
|
||||||
|
}
|
||||||
|
function inferBusinessOverviewDiscoveryContextFromSessionItems(sessionItems, toNonEmptyString) {
|
||||||
|
for (let index = sessionItems.length - 1; index >= 0; index -= 1) {
|
||||||
|
const item = toRecordObject(sessionItems[index]);
|
||||||
|
if (toNonEmptyString(item?.role) !== "assistant") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const debug = toRecordObject(item?.debug);
|
||||||
|
const entryPoint = toRecordObject(debug?.assistant_mcp_discovery_entry_point_v1);
|
||||||
|
const turnInput = toRecordObject(entryPoint?.turn_input);
|
||||||
|
const dataNeedGraph = toRecordObject(turnInput?.data_need_graph);
|
||||||
|
if (toNonEmptyString(dataNeedGraph?.business_fact_family) !== "business_overview") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const turnMeaningRef = toRecordObject(turnInput?.turn_meaning_ref);
|
||||||
|
const filterWindow = dateScopeToFilterWindow(toNonEmptyString(turnMeaningRef?.explicit_date_scope));
|
||||||
|
if (!filterWindow) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const previousFilters = { ...filterWindow };
|
||||||
|
const organization = toNonEmptyString(turnMeaningRef?.explicit_organization_scope);
|
||||||
|
if (looksLikeReliableOrganizationScope(organization)) {
|
||||||
|
previousFilters.organization = organization;
|
||||||
|
}
|
||||||
|
const rankingNeed = toNonEmptyString(dataNeedGraph?.ranking_need);
|
||||||
|
return {
|
||||||
|
previous_discovery_pilot_scope: "business_overview_route_template_v1",
|
||||||
|
previous_filters: previousFilters,
|
||||||
|
root_filters: { ...previousFilters },
|
||||||
|
previous_seeded_ranking_need: rankingNeed,
|
||||||
|
previous_intent: "business_overview",
|
||||||
|
root_intent: "business_overview"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function mergeBusinessOverviewDateContextForCompactCashflow(input) {
|
||||||
|
if (!hasCompactCashflowFollowupSignal(input.userMessage)) {
|
||||||
|
return input.followupContext;
|
||||||
|
}
|
||||||
|
const existingFilters = toRecordObject(input.followupContext?.previous_filters);
|
||||||
|
if (input.toNonEmptyString(existingFilters?.period_from) ||
|
||||||
|
input.toNonEmptyString(existingFilters?.period_to) ||
|
||||||
|
input.toNonEmptyString(existingFilters?.as_of_date)) {
|
||||||
|
return input.followupContext;
|
||||||
|
}
|
||||||
|
const inferred = inferBusinessOverviewDiscoveryContextFromSessionItems(input.sessionItems, input.toNonEmptyString);
|
||||||
|
if (!inferred) {
|
||||||
|
return input.followupContext;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...inferred,
|
||||||
|
...(input.followupContext ?? {}),
|
||||||
|
previous_filters: {
|
||||||
|
...(toRecordObject(inferred.previous_filters) ?? {}),
|
||||||
|
...(toRecordObject(input.followupContext?.previous_filters) ?? {})
|
||||||
|
},
|
||||||
|
root_filters: {
|
||||||
|
...(toRecordObject(inferred.root_filters) ?? {}),
|
||||||
|
...(toRecordObject(input.followupContext?.root_filters) ?? {})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
function hasSelectedObjectInventorySignal(text) {
|
function hasSelectedObjectInventorySignal(text) {
|
||||||
return /(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ним|selected\s+object)/iu.test(String(text ?? ""));
|
return /(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ним|selected\s+object)/iu.test(String(text ?? ""));
|
||||||
}
|
}
|
||||||
|
|
@ -61,6 +166,7 @@ function isInventorySelectedObjectOrRootIntent(intent) {
|
||||||
intent === "inventory_purchase_provenance_for_item" ||
|
intent === "inventory_purchase_provenance_for_item" ||
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date");
|
intent === "inventory_aging_by_purchase_date");
|
||||||
|
|
@ -74,6 +180,15 @@ function isGenericCanonicalDriftIntent(intent) {
|
||||||
intent === "bank_operations_by_contract" ||
|
intent === "bank_operations_by_contract" ||
|
||||||
intent === "documents_forming_balance");
|
intent === "documents_forming_balance");
|
||||||
}
|
}
|
||||||
|
function hasInventoryMarginRankingAccountCorrectionCue(text) {
|
||||||
|
const value = String(text ?? "").toLowerCase();
|
||||||
|
if (!value.trim()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (/\b41(?:[.,]\d{1,2})?\b/iu.test(value) &&
|
||||||
|
/\b01(?:[.,]\d{1,2})?\b/iu.test(value) &&
|
||||||
|
/(?:\u0430\s+\u043d\u0435|\u043d\u0435|\u0432\u043c\u0435\u0441\u0442\u043e|not|instead)/iu.test(value));
|
||||||
|
}
|
||||||
function hasSameDateFollowupSignal(text) {
|
function hasSameDateFollowupSignal(text) {
|
||||||
return /(?:эту\s+же\s+дат(?:у|е|ой)|ту\s+же\s+дат(?:у|е|ой)|same\s+date)/iu.test(String(text ?? ""));
|
return /(?:эту\s+же\s+дат(?:у|е|ой)|ту\s+же\s+дат(?:у|е|ой)|same\s+date)/iu.test(String(text ?? ""));
|
||||||
}
|
}
|
||||||
|
|
@ -117,6 +232,10 @@ function shouldPreferRawFollowupMessage(userMessage, addressInputMessage, carryo
|
||||||
const hasInventoryItemCarryover = previousAnchorType === "item" && isInventorySelectedObjectOrRootIntent(previousIntent);
|
const hasInventoryItemCarryover = previousAnchorType === "item" && isInventorySelectedObjectOrRootIntent(previousIntent);
|
||||||
const hasInventoryFrameCarryover = isInventorySelectedObjectOrRootIntent(previousIntent) ||
|
const hasInventoryFrameCarryover = isInventorySelectedObjectOrRootIntent(previousIntent) ||
|
||||||
isInventorySelectedObjectOrRootIntent(rootIntent);
|
isInventorySelectedObjectOrRootIntent(rootIntent);
|
||||||
|
const hasInventoryMarginRankingCarryover = previousIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
|
rootIntent === "inventory_margin_ranking_for_nomenclature";
|
||||||
|
const hasInventoryMarginRankingAccountCorrection = hasInventoryMarginRankingCarryover &&
|
||||||
|
[rawMessage, canonicalMessage].some((message) => hasInventoryMarginRankingAccountCorrectionCue(message));
|
||||||
const hasDocumentCarryover = previousIntent === "list_documents_by_counterparty" || previousIntent === "list_documents_by_contract";
|
const hasDocumentCarryover = previousIntent === "list_documents_by_counterparty" || previousIntent === "list_documents_by_contract";
|
||||||
if (mode === "unsupported" && intent === "unknown") {
|
if (mode === "unsupported" && intent === "unknown") {
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -132,6 +251,10 @@ function shouldPreferRawFollowupMessage(userMessage, addressInputMessage, carryo
|
||||||
(intent === "account_balance_snapshot" || intent === "documents_forming_balance" || intent === "unknown")) {
|
(intent === "account_balance_snapshot" || intent === "documents_forming_balance" || intent === "unknown")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (hasInventoryMarginRankingAccountCorrection &&
|
||||||
|
(intent === "account_balance_snapshot" || intent === "documents_forming_balance" || intent === "unknown")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return ((hasSelectedObjectInventorySignal(rawMessage) || hasInventoryItemCarryover) &&
|
return ((hasSelectedObjectInventorySignal(rawMessage) || hasInventoryItemCarryover) &&
|
||||||
(hasSelectedObjectInventoryActionCue(rawMessage) || hasShortInventoryPurchaseFollowupCue(rawMessage)) &&
|
(hasSelectedObjectInventoryActionCue(rawMessage) || hasShortInventoryPurchaseFollowupCue(rawMessage)) &&
|
||||||
(isGenericCanonicalDriftIntent(intent) || intent === "unknown"));
|
(isGenericCanonicalDriftIntent(intent) || intent === "unknown"));
|
||||||
|
|
@ -192,9 +315,15 @@ async function buildAssistantAddressOrchestrationRuntime(input) {
|
||||||
const orchestrationContract = toRecordObject(orchestrationDecision.orchestrationContract);
|
const orchestrationContract = toRecordObject(orchestrationDecision.orchestrationContract);
|
||||||
const predecomposeContract = toRecordObject(addressPreDecompose.predecomposeContract);
|
const predecomposeContract = toRecordObject(addressPreDecompose.predecomposeContract);
|
||||||
const explicitPredecomposeOrganization = predecomposeOrganizationName(predecomposeContract, input.toNonEmptyString);
|
const explicitPredecomposeOrganization = predecomposeOrganizationName(predecomposeContract, input.toNonEmptyString);
|
||||||
const discoveryFollowupContext = mergeOrganizationIntoDiscoveryFollowupContext(followupContext, explicitPredecomposeOrganization
|
const discoveryFollowupContextWithOrganization = mergeOrganizationIntoDiscoveryFollowupContext(followupContext, explicitPredecomposeOrganization
|
||||||
? null
|
? null
|
||||||
: sessionOrganizationName(input.sessionOrganizationScope ?? null, input.toNonEmptyString));
|
: sessionOrganizationName(input.sessionOrganizationScope ?? null, input.toNonEmptyString));
|
||||||
|
const discoveryFollowupContext = mergeBusinessOverviewDateContextForCompactCashflow({
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
followupContext: discoveryFollowupContextWithOrganization,
|
||||||
|
sessionItems: input.sessionItems,
|
||||||
|
toNonEmptyString: input.toNonEmptyString
|
||||||
|
});
|
||||||
const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(input.userMessage, addressInputMessage, carryover, addressPreDecompose);
|
const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(input.userMessage, addressInputMessage, carryover, addressPreDecompose);
|
||||||
const runDiscoveryEntryPoint = input.runMcpDiscoveryRuntimeEntryPoint ?? assistantMcpDiscoveryRuntimeEntryPoint_1.runAssistantMcpDiscoveryRuntimeEntryPoint;
|
const runDiscoveryEntryPoint = input.runMcpDiscoveryRuntimeEntryPoint ?? assistantMcpDiscoveryRuntimeEntryPoint_1.runAssistantMcpDiscoveryRuntimeEntryPoint;
|
||||||
let mcpDiscoveryRuntimeEntryPoint = null;
|
let mcpDiscoveryRuntimeEntryPoint = null;
|
||||||
|
|
@ -205,7 +334,8 @@ async function buildAssistantAddressOrchestrationRuntime(input) {
|
||||||
effectiveMessage: addressInputMessage,
|
effectiveMessage: addressInputMessage,
|
||||||
assistantTurnMeaning: toRecordObject(orchestrationContract?.assistant_turn_meaning),
|
assistantTurnMeaning: toRecordObject(orchestrationContract?.assistant_turn_meaning),
|
||||||
predecomposeContract,
|
predecomposeContract,
|
||||||
followupContext: discoveryFollowupContext
|
followupContext: discoveryFollowupContext,
|
||||||
|
knownOrganizations: sessionKnownOrganizations(input.sessionOrganizationScope ?? null)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,13 @@ function formatMissingAnchors(anchors) {
|
||||||
return anchors.join(", ");
|
return anchors.join(", ");
|
||||||
}
|
}
|
||||||
function buildClarificationReply(binding) {
|
function buildClarificationReply(binding) {
|
||||||
|
if (binding.capability_contract_id === "inventory_inventory_margin_ranking_for_nomenclature") {
|
||||||
|
return [
|
||||||
|
"Для рейтинга прибыльности номенклатуры нужен период.",
|
||||||
|
"Могу посчитать по номенклатуре: выручку без НДС, себестоимость реализации, валовую прибыль и маржинальность.",
|
||||||
|
"Уточните период: месяц, квартал, год или весь доступный период."
|
||||||
|
].join("\n\n");
|
||||||
|
}
|
||||||
return [
|
return [
|
||||||
"Нужно уточнение, чтобы не подставить неподтвержденный объект в расчет.",
|
"Нужно уточнение, чтобы не подставить неподтвержденный объект в расчет.",
|
||||||
`Не хватает: ${formatMissingAnchors(binding.missing_anchors)}.`,
|
`Не хватает: ${formatMissingAnchors(binding.missing_anchors)}.`,
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,13 @@ function anchorSatisfied(requiredAnchor, providedAnchors, debug) {
|
||||||
if (providedAnchors.includes(requiredAnchor)) {
|
if (providedAnchors.includes(requiredAnchor)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (requiredAnchor === "period") {
|
||||||
|
return ((hasValue(filters?.period_from) && hasValue(filters?.period_to)) ||
|
||||||
|
hasValue(filters?.as_of_date) ||
|
||||||
|
providedAnchors.includes("period_from") ||
|
||||||
|
providedAnchors.includes("period_to") ||
|
||||||
|
providedAnchors.includes("as_of_date"));
|
||||||
|
}
|
||||||
if (requiredAnchor === "item") {
|
if (requiredAnchor === "item") {
|
||||||
return (providedAnchors.includes("selected_object") ||
|
return (providedAnchors.includes("selected_object") ||
|
||||||
providedAnchors.includes("anchor_item") ||
|
providedAnchors.includes("anchor_item") ||
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,181 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.ASSISTANT_EVIDENCE_PLANNER_SCHEMA_VERSION = void 0;
|
||||||
|
exports.buildAssistantEvidencePlanner = buildAssistantEvidencePlanner;
|
||||||
|
exports.ASSISTANT_EVIDENCE_PLANNER_SCHEMA_VERSION = "assistant_evidence_planner_v1";
|
||||||
|
function toNonEmptyString(value) {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const text = String(value).trim();
|
||||||
|
return text.length > 0 ? text : null;
|
||||||
|
}
|
||||||
|
function normalizeReasonCode(value) {
|
||||||
|
const normalized = value
|
||||||
|
.trim()
|
||||||
|
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
|
||||||
|
.replace(/^_+|_+$/g, "")
|
||||||
|
.toLowerCase();
|
||||||
|
return normalized.length > 0 ? normalized.slice(0, 120) : null;
|
||||||
|
}
|
||||||
|
function pushReason(target, value) {
|
||||||
|
const normalized = normalizeReasonCode(value);
|
||||||
|
if (normalized && !target.includes(normalized)) {
|
||||||
|
target.push(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function uniqueStrings(values) {
|
||||||
|
const result = [];
|
||||||
|
for (const value of values) {
|
||||||
|
const text = toNonEmptyString(value);
|
||||||
|
if (text && !result.includes(text)) {
|
||||||
|
result.push(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
function providedAxesFromMeaning(meaning) {
|
||||||
|
const result = [];
|
||||||
|
if ((meaning?.explicit_entity_candidates?.length ?? 0) > 0) {
|
||||||
|
result.push("counterparty");
|
||||||
|
result.push("business_entity");
|
||||||
|
}
|
||||||
|
if (toNonEmptyString(meaning?.explicit_organization_scope)) {
|
||||||
|
result.push("organization");
|
||||||
|
}
|
||||||
|
if (toNonEmptyString(meaning?.explicit_date_scope)) {
|
||||||
|
result.push("period");
|
||||||
|
}
|
||||||
|
if (toNonEmptyString(meaning?.asked_aggregation_axis)) {
|
||||||
|
result.push("aggregate_axis");
|
||||||
|
}
|
||||||
|
if (toNonEmptyString(meaning?.metadata_scope_hint)) {
|
||||||
|
result.push("metadata_scope");
|
||||||
|
}
|
||||||
|
return uniqueStrings(result);
|
||||||
|
}
|
||||||
|
function missingAxes(requiredAxes, providedAxes) {
|
||||||
|
return requiredAxes.filter((axis) => !providedAxes.includes(axis));
|
||||||
|
}
|
||||||
|
const USER_ACTIONABLE_AXIS_SET = new Set([
|
||||||
|
"counterparty",
|
||||||
|
"business_entity",
|
||||||
|
"organization",
|
||||||
|
"period",
|
||||||
|
"as_of_date",
|
||||||
|
"item",
|
||||||
|
"supplier",
|
||||||
|
"buyer",
|
||||||
|
"warehouse",
|
||||||
|
"document",
|
||||||
|
"contract",
|
||||||
|
"metadata_scope",
|
||||||
|
"lane_family_choice"
|
||||||
|
]);
|
||||||
|
function userActionableMissingAxes(axes) {
|
||||||
|
return axes.filter((axis) => USER_ACTIONABLE_AXIS_SET.has(axis));
|
||||||
|
}
|
||||||
|
function coverageExpectationFor(graph) {
|
||||||
|
if (graph?.proof_expectation === "bounded_inference") {
|
||||||
|
return "bounded_inference";
|
||||||
|
}
|
||||||
|
if (graph?.proof_expectation === "clarification_required") {
|
||||||
|
return "clarification";
|
||||||
|
}
|
||||||
|
return "confirmed_coverage";
|
||||||
|
}
|
||||||
|
function answerModeFor(input) {
|
||||||
|
if (input.plannerStatus === "needs_clarification") {
|
||||||
|
return "clarification_required";
|
||||||
|
}
|
||||||
|
if (input.plannerStatus === "blocked") {
|
||||||
|
return "checked_sources_only";
|
||||||
|
}
|
||||||
|
if (input.coverageExpectation === "bounded_inference") {
|
||||||
|
return "bounded_business_inference";
|
||||||
|
}
|
||||||
|
if (input.coverageExpectation === "clarification") {
|
||||||
|
return "clarification_required";
|
||||||
|
}
|
||||||
|
return "confirmed_business_answer";
|
||||||
|
}
|
||||||
|
function requiredUserLayersFor(answerMode) {
|
||||||
|
if (answerMode === "clarification_required") {
|
||||||
|
return ["clarifying_question", "why_needed", "available_calculation"];
|
||||||
|
}
|
||||||
|
if (answerMode === "bounded_business_inference") {
|
||||||
|
return ["business_conclusion", "key_figures", "evidence_boundary", "next_step"];
|
||||||
|
}
|
||||||
|
if (answerMode === "checked_sources_only") {
|
||||||
|
return ["checked_sources_boundary", "unknowns", "next_probe"];
|
||||||
|
}
|
||||||
|
return ["direct_business_answer", "key_figures", "evidence_boundary", "next_step"];
|
||||||
|
}
|
||||||
|
function buildAssistantEvidencePlanner(input) {
|
||||||
|
const graph = input.dataNeedGraph ?? null;
|
||||||
|
const plan = input.discoveryPlan;
|
||||||
|
const turnMeaning = plan.turn_meaning_ref;
|
||||||
|
const requiredAxes = uniqueStrings(plan.required_axes);
|
||||||
|
const providedAxes = providedAxesFromMeaning(turnMeaning);
|
||||||
|
const graphClarificationGaps = uniqueStrings(graph?.clarification_gaps ?? []);
|
||||||
|
const additionalAxisGaps = uniqueStrings(input.additionalMissingAxes ?? []).filter((axis) => !providedAxes.includes(axis) && (requiredAxes.includes(axis) || USER_ACTIONABLE_AXIS_SET.has(axis)));
|
||||||
|
const axisGaps = uniqueStrings([...additionalAxisGaps, ...missingAxes(requiredAxes, providedAxes)]);
|
||||||
|
const actionableAxisGaps = userActionableMissingAxes(axisGaps);
|
||||||
|
const clarificationGaps = uniqueStrings([...graphClarificationGaps, ...axisGaps]);
|
||||||
|
const coverageExpectation = coverageExpectationFor(graph);
|
||||||
|
const answerMode = answerModeFor({
|
||||||
|
plannerStatus: input.plannerStatus,
|
||||||
|
coverageExpectation
|
||||||
|
});
|
||||||
|
const reasonCodes = uniqueStrings(plan.reason_codes);
|
||||||
|
pushReason(reasonCodes, "evidence_planner_contract_built");
|
||||||
|
if (graph) {
|
||||||
|
pushReason(reasonCodes, "evidence_planner_consumed_data_need_graph");
|
||||||
|
}
|
||||||
|
if (clarificationGaps.length > 0) {
|
||||||
|
pushReason(reasonCodes, "evidence_planner_has_missing_axes_or_gaps");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
schema_version: exports.ASSISTANT_EVIDENCE_PLANNER_SCHEMA_VERSION,
|
||||||
|
policy_owner: "assistantEvidencePlanner",
|
||||||
|
planner_status: input.plannerStatus,
|
||||||
|
semantic_data_need: toNonEmptyString(input.semanticDataNeed),
|
||||||
|
selected_chain_id: input.selectedChainId,
|
||||||
|
data_need: {
|
||||||
|
business_fact_family: graph?.business_fact_family ?? null,
|
||||||
|
action_family: graph?.action_family ?? null,
|
||||||
|
aggregation_need: graph?.aggregation_need ?? null,
|
||||||
|
comparison_need: graph?.comparison_need ?? null,
|
||||||
|
ranking_need: graph?.ranking_need ?? null,
|
||||||
|
time_scope_need: graph?.time_scope_need ?? null,
|
||||||
|
proof_expectation: graph?.proof_expectation ?? null,
|
||||||
|
subject_candidates: uniqueStrings(graph?.subject_candidates ?? [])
|
||||||
|
},
|
||||||
|
evidence_axes: {
|
||||||
|
required_axes: requiredAxes,
|
||||||
|
provided_axes: providedAxes,
|
||||||
|
missing_axes: axisGaps,
|
||||||
|
user_actionable_missing_axes: actionableAxisGaps,
|
||||||
|
clarification_gaps: clarificationGaps
|
||||||
|
},
|
||||||
|
primitive_plan: {
|
||||||
|
selected_chain_id: input.selectedChainId,
|
||||||
|
allowed_primitives: plan.allowed_primitives,
|
||||||
|
rejected_primitives: plan.rejected_primitives,
|
||||||
|
execution_budget: plan.execution_budget
|
||||||
|
},
|
||||||
|
coverage_gate: {
|
||||||
|
requires_evidence_gate: true,
|
||||||
|
expected_coverage: coverageExpectation,
|
||||||
|
answer_permission_if_satisfied: answerMode,
|
||||||
|
answer_may_use_raw_model_claims: false
|
||||||
|
},
|
||||||
|
answer_contract: {
|
||||||
|
answer_mode: answerMode,
|
||||||
|
required_user_layers: requiredUserLayersFor(answerMode),
|
||||||
|
forbidden_overclaim_flags: uniqueStrings(graph?.forbidden_overclaim_flags ?? []),
|
||||||
|
must_keep_internal_mechanics_hidden: true
|
||||||
|
},
|
||||||
|
reason_codes: reasonCodes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1212,22 +1212,16 @@ function derivedValueFlowConfirmedLine(pilot) {
|
||||||
const organization = organizationScope ? ` по организации ${organizationScope}` : "";
|
const organization = organizationScope ? ` по организации ${organizationScope}` : "";
|
||||||
const counterparty = flow.counterparty ? ` по контрагенту ${flow.counterparty}` : "";
|
const counterparty = flow.counterparty ? ` по контрагенту ${flow.counterparty}` : "";
|
||||||
const period = flow.period_scope ? ` за период ${flow.period_scope}` : " в проверенном окне";
|
const period = flow.period_scope ? ` за период ${flow.period_scope}` : " в проверенном окне";
|
||||||
const movementLabel = flow.value_flow_direction === "outgoing_supplier_payout"
|
|
||||||
? "исходящих платежей/списаний"
|
|
||||||
: "входящих денежных поступлений";
|
|
||||||
const totalLabel = flow.value_flow_direction === "outgoing_supplier_payout"
|
const totalLabel = flow.value_flow_direction === "outgoing_supplier_payout"
|
||||||
? "сумма исходящих платежей/списаний составляет"
|
? "исходящие платежи/списания"
|
||||||
: "сумма входящих денежных поступлений составляет";
|
: "входящие денежные поступления";
|
||||||
const caveat = flow.value_flow_direction === "outgoing_supplier_payout"
|
const caveat = flow.value_flow_direction === "outgoing_supplier_payout"
|
||||||
? "Это расчет по найденным строкам 1С, а не подтверждение полного объема платежей вне проверенного окна."
|
? "Граница: это проверенный денежный срез по найденным списаниям в 1С, не внешний аудит всех платежей вне этого окна."
|
||||||
: "Это расчет по найденным строкам 1С, а не подтверждение полного объема поступлений вне проверенного окна.";
|
: "Граница: это проверенный денежный срез по найденным поступлениям в 1С, не внешний аудит всех поступлений вне этого окна.";
|
||||||
const dates = flow.first_movement_date && flow.latest_movement_date
|
|
||||||
? ` Первая найденная дата движения: ${flow.first_movement_date}; последняя: ${flow.latest_movement_date}.`
|
|
||||||
: "";
|
|
||||||
const limitCaveat = flow.coverage_limited_by_probe_limit
|
const limitCaveat = flow.coverage_limited_by_probe_limit
|
||||||
? " Лимит строк проверки достигнут; полный запрошенный период может быть покрыт не полностью."
|
? " Проверка уперлась в лимит строк, поэтому полный период может быть покрыт не полностью."
|
||||||
: "";
|
: "";
|
||||||
return `По найденным строкам ${movementLabel} в 1С${counterparty}${period} ${totalLabel} ${flow.total_amount_human_ru} Учтено строк с суммой: ${flow.rows_with_amount} из ${flow.rows_matched}.${dates}${limitCaveat} ${caveat}`;
|
return `${totalLabel}${counterparty || organization}${period}: ${flow.total_amount_human_ru}.${limitCaveat} ${caveat}`;
|
||||||
}
|
}
|
||||||
function derivedValueFlowMonthlyLines(pilot) {
|
function derivedValueFlowMonthlyLines(pilot) {
|
||||||
const flow = pilot.derived_value_flow;
|
const flow = pilot.derived_value_flow;
|
||||||
|
|
@ -1831,7 +1825,7 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
|
||||||
: pilot.derived_ranked_value_flow && derivedValueLine
|
: pilot.derived_ranked_value_flow && derivedValueLine
|
||||||
? [derivedValueLine]
|
? [derivedValueLine]
|
||||||
: derivedValueLine
|
: derivedValueLine
|
||||||
? [...pilot.evidence.confirmed_facts, derivedValueLine, ...monthlyConfirmedLines]
|
? [derivedValueLine, ...monthlyConfirmedLines]
|
||||||
: valueFlowZeroResultConfirmedLine(pilot)
|
: valueFlowZeroResultConfirmedLine(pilot)
|
||||||
? [valueFlowZeroResultConfirmedLine(pilot)]
|
? [valueFlowZeroResultConfirmedLine(pilot)]
|
||||||
: derivedEntityResolutionLine
|
: derivedEntityResolutionLine
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,12 @@ function hasBusinessOverviewDirectMoneyAnswerHint(input) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const text = input.rawUtterance;
|
const text = input.rawUtterance;
|
||||||
|
if (/(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)[\s\S]{0,80}(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\p{L}*|\u0443\u0448\p{L}*|\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*)|(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e)[\s\S]{0,80}(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)/iu.test(text)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (/(?:\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}+|\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?\b|\u0431\u0435\u0437\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0431\u0438\u0432\p{L}*|\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b\p{L}*|\u043e\u0434\u043d\u043e\u0439\s+\u0441\u0442\u0440\u043e\u043a\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0438\u0442\u043e\u0433|\u0434\u0435\u043d\p{L}{0,20}\s+\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\u043b\p{L}*[\s\S]{0,80}\u0443\u0448\u043b\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|\u0432\u0445\u043e\u0434\u044f\u0449\p{L}*[\s\S]{0,80}\u0438\u0441\u0445\u043e\u0434\u044f\u0449\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e)/iu.test(text)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|how\s+much)[\s\S]{0,120}(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u0432\u044b\u0440\u0443\u0447|\u0434\u0435\u043d\p{L}*|\u043f\u043e\u043b\u0443\u0447|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*)|(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u0432\u044b\u0440\u0443\u0447)[\s\S]{0,120}(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|\u0432\u0441\u0435\u0433\u043e|\u0432\u043e\u043e\u0431\u0449\u0435|(?:19|20)\d{2}|all\s+time)|(?:\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0430\u044f|\u043a\u0430\u043a\u0438\u0435|which|what)[\s\S]{0,80}(?:\u0441\u0430\u043c\p{L}*|top|best|most)[\s\S]{0,80}(?:\u0434\u043e\u0445\u043e\u0434\u043d|\u0432\u044b\u0440\u0443\u0447|\u043e\u0431\u043e\u0440\u043e\u0442|revenue|turnover)[\s\S]{0,40}(?:\u0433\u043e\u0434|year)/iu.test(text);
|
return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|how\s+much)[\s\S]{0,120}(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u0432\u044b\u0440\u0443\u0447|\u0434\u0435\u043d\p{L}*|\u043f\u043e\u043b\u0443\u0447|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*)|(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u0432\u044b\u0440\u0443\u0447)[\s\S]{0,120}(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|\u0432\u0441\u0435\u0433\u043e|\u0432\u043e\u043e\u0431\u0449\u0435|(?:19|20)\d{2}|all\s+time)|(?:\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0430\u044f|\u043a\u0430\u043a\u0438\u0435|which|what)[\s\S]{0,80}(?:\u0441\u0430\u043c\p{L}*|top|best|most)[\s\S]{0,80}(?:\u0434\u043e\u0445\u043e\u0434\u043d|\u0432\u044b\u0440\u0443\u0447|\u043e\u0431\u043e\u0440\u043e\u0442|revenue|turnover)[\s\S]{0,40}(?:\u0433\u043e\u0434|year)/iu.test(text);
|
||||||
}
|
}
|
||||||
function timeScopeNeedFor(input) {
|
function timeScopeNeedFor(input) {
|
||||||
|
|
@ -193,6 +199,12 @@ function rankingNeedFromRawUtterance(value) {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (/\u0438\u0441\u043a\u043b\u044e\u0447[\p{L}\p{N}_]*\s+\u0442\u043e\u043f/iu.test(text)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (/(?:\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?\b|\u043d\u0435\s+\u0442\u043e\u043f\b|\u0438\u0441\u043a\u043b\u044e\u0447\w*\s+\u0442\u043e\u043f|\u0431\u0435\u0437\s+\u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0430\b|без\s+топ(?:ов|а)?\b|не\s+топ\b|исключ\S*\s+топ|без\s+рейтинга\b)/iu.test(text)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (/(?:\u0442\u043e\u043f[-\s]?\d*|\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435|\u044b\u0435)|\u0431\u043e\u043b\u044c\u0448\u0435\s+\u0432\u0441\u0435\u0433\u043e|\u043d\u0430\u0438\u0431\u043e\u043b[\u0435\u0451]\u0435|\u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d|\u043c\u0430\u043a\u0441\u0438\u043c\u0443\u043c|\u043a\u0440\u0443\u043f\u043d\u0435\u0439\u0448|\u043b\u0443\u0447\u0448\u0438\u0439)/iu.test(text)) {
|
if (/(?:\u0442\u043e\u043f[-\s]?\d*|\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435|\u044b\u0435)|\u0431\u043e\u043b\u044c\u0448\u0435\s+\u0432\u0441\u0435\u0433\u043e|\u043d\u0430\u0438\u0431\u043e\u043b[\u0435\u0451]\u0435|\u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d|\u043c\u0430\u043a\u0441\u0438\u043c\u0443\u043c|\u043a\u0440\u0443\u043f\u043d\u0435\u0439\u0448|\u043b\u0443\u0447\u0448\u0438\u0439)/iu.test(text)) {
|
||||||
return "top_desc";
|
return "top_desc";
|
||||||
}
|
}
|
||||||
|
|
@ -219,6 +231,13 @@ function rankingNeedFromRawUtterance(value) {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
function suppressRankingNeedFromRawUtterance(value) {
|
||||||
|
const text = lower(value);
|
||||||
|
if (!text) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(?:\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?\b|\u043d\u0435\s+\u0442\u043e\u043f\b|\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440\b|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}+|\u0438\u0441\u043a\u043b\u044e\u0447\w*\s+\u0442\u043e\u043f|\u0431\u0435\u0437\s+\u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0430\b|без\s+топ(?:ов|а)?\b|не\s+топ\b|не\s+обзор\b|просто\s+ден\p{L}+|исключ\S*\s+топ|без\s+рейтинга\b)/iu.test(text);
|
||||||
|
}
|
||||||
function proofExpectationFor(input) {
|
function proofExpectationFor(input) {
|
||||||
if (input.clarificationGaps.length > 0) {
|
if (input.clarificationGaps.length > 0) {
|
||||||
return "clarification_required";
|
return "clarification_required";
|
||||||
|
|
@ -377,6 +396,7 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
|
||||||
const action = lower(turnMeaning?.asked_action_family);
|
const action = lower(turnMeaning?.asked_action_family);
|
||||||
const unsupported = lower(turnMeaning?.unsupported_but_understood_family);
|
const unsupported = lower(turnMeaning?.unsupported_but_understood_family);
|
||||||
const rawUtterance = lower(input.rawUtterance);
|
const rawUtterance = lower(input.rawUtterance);
|
||||||
|
const rawQuestionSignal = lower([input.rawUtterance, turnMeaning?.raw_message, turnMeaning?.effective_message].join(" "));
|
||||||
const aggregationAxis = lower(turnMeaning?.asked_aggregation_axis);
|
const aggregationAxis = lower(turnMeaning?.asked_aggregation_axis);
|
||||||
const seededRankingNeed = toNonEmptyString(turnMeaning?.seeded_ranking_need);
|
const seededRankingNeed = toNonEmptyString(turnMeaning?.seeded_ranking_need);
|
||||||
const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope);
|
const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope);
|
||||||
|
|
@ -394,15 +414,18 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
|
||||||
});
|
});
|
||||||
const aggregationNeed = aggregationNeedFor(aggregationAxis);
|
const aggregationNeed = aggregationNeedFor(aggregationAxis);
|
||||||
const comparisonNeed = comparisonNeedFor(action);
|
const comparisonNeed = comparisonNeedFor(action);
|
||||||
const rankingNeed = rankingNeedFromRawUtterance(rawUtterance) ?? seededRankingNeed;
|
|
||||||
const allTimeScopeHint = hasAllTimeScopeHint(rawUtterance);
|
const allTimeScopeHint = hasAllTimeScopeHint(rawUtterance);
|
||||||
const subjectScopedBidirectionalAllTime = businessFactFamily === "value_flow" &&
|
const subjectScopedBidirectionalAllTime = businessFactFamily === "value_flow" &&
|
||||||
comparisonNeed === "incoming_vs_outgoing" &&
|
comparisonNeed === "incoming_vs_outgoing" &&
|
||||||
subjectCandidates.length > 0 &&
|
subjectCandidates.length > 0 &&
|
||||||
!explicitDateScope;
|
!explicitDateScope;
|
||||||
|
const suppressRankingNeed = suppressRankingNeedFromRawUtterance(rawQuestionSignal) ||
|
||||||
|
/(?:\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?|\u043d\u0435\s+\u0442\u043e\u043f|\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}+|\u0438\u0441\u043a\u043b\u044e\u0447\w*\s+\u0442\u043e\u043f|\u0431\u0435\u0437\s+\u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0430)/iu.test(rawQuestionSignal);
|
||||||
|
const rawRankingNeed = rankingNeedFromRawUtterance(rawQuestionSignal);
|
||||||
|
const rankingNeed = suppressRankingNeed ? null : rawRankingNeed ?? seededRankingNeed;
|
||||||
const directBusinessOverviewMoneyAnswerHint = hasBusinessOverviewDirectMoneyAnswerHint({
|
const directBusinessOverviewMoneyAnswerHint = hasBusinessOverviewDirectMoneyAnswerHint({
|
||||||
family: businessFactFamily,
|
family: businessFactFamily,
|
||||||
rawUtterance,
|
rawUtterance: rawQuestionSignal,
|
||||||
rankingNeed
|
rankingNeed
|
||||||
});
|
});
|
||||||
const oneSidedOpenScopeTotalHint = hasOpenScopeOneSidedValueTotalHintUtf8Safe(rawUtterance, action);
|
const oneSidedOpenScopeTotalHint = hasOpenScopeOneSidedValueTotalHintUtf8Safe(rawUtterance, action);
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,15 @@ function isRouteCandidateContract(value) {
|
||||||
return (record?.schema_version === "assistant_mcp_route_candidate_v1" &&
|
return (record?.schema_version === "assistant_mcp_route_candidate_v1" &&
|
||||||
record?.policy_owner === "assistantMcpDiscoveryRuntimeBridge");
|
record?.policy_owner === "assistantMcpDiscoveryRuntimeBridge");
|
||||||
}
|
}
|
||||||
|
function isEvidencePlannerContract(value) {
|
||||||
|
const record = toRecordObject(value);
|
||||||
|
return record?.schema_version === "assistant_evidence_planner_v1" && record?.policy_owner === "assistantEvidencePlanner";
|
||||||
|
}
|
||||||
|
function isExecutionHandoffContract(value) {
|
||||||
|
const record = toRecordObject(value);
|
||||||
|
return (record?.schema_version === "assistant_mcp_discovery_execution_handoff_v1" &&
|
||||||
|
record?.policy_owner === "assistantMcpDiscoveryExecutionHandoff");
|
||||||
|
}
|
||||||
function resolveEntryPoint(input) {
|
function resolveEntryPoint(input) {
|
||||||
if (isMcpDiscoveryEntryPointContract(input.entryPoint)) {
|
if (isMcpDiscoveryEntryPointContract(input.entryPoint)) {
|
||||||
return input.entryPoint;
|
return input.entryPoint;
|
||||||
|
|
@ -51,16 +60,29 @@ function buildAssistantMcpDiscoveryDebugAttachmentFields(input) {
|
||||||
const entryPoint = resolveEntryPoint(input);
|
const entryPoint = resolveEntryPoint(input);
|
||||||
const bridge = toRecordObject(entryPoint?.bridge);
|
const bridge = toRecordObject(entryPoint?.bridge);
|
||||||
const planner = toRecordObject(bridge?.planner);
|
const planner = toRecordObject(bridge?.planner);
|
||||||
|
const evidencePlan = isEvidencePlannerContract(planner?.evidence_plan) ? planner.evidence_plan : null;
|
||||||
|
const evidencePlanAxes = toRecordObject(evidencePlan?.evidence_axes);
|
||||||
|
const evidencePlanCoverageGate = toRecordObject(evidencePlan?.coverage_gate);
|
||||||
|
const evidencePlanAnswerContract = toRecordObject(evidencePlan?.answer_contract);
|
||||||
const chainAlignment = toRecordObject(planner?.catalog_chain_template_alignment);
|
const chainAlignment = toRecordObject(planner?.catalog_chain_template_alignment);
|
||||||
const routeCandidate = isRouteCandidateContract(bridge?.route_candidate) ? bridge.route_candidate : null;
|
const routeCandidate = isRouteCandidateContract(bridge?.route_candidate) ? bridge.route_candidate : null;
|
||||||
|
const executionHandoff = isExecutionHandoffContract(bridge?.execution_handoff) ? bridge.execution_handoff : null;
|
||||||
const answerDraft = toRecordObject(bridge?.answer_draft);
|
const answerDraft = toRecordObject(bridge?.answer_draft);
|
||||||
|
const hotRuntimeWired = entryPoint?.hot_runtime_wired === true ||
|
||||||
|
bridge?.hot_runtime_wired === true ||
|
||||||
|
executionHandoff?.can_use_guarded_response === true;
|
||||||
return {
|
return {
|
||||||
assistant_mcp_discovery_entry_point_v1: entryPoint,
|
assistant_mcp_discovery_entry_point_v1: entryPoint,
|
||||||
mcp_discovery_entry_status: toNonEmptyString(entryPoint?.entry_status),
|
mcp_discovery_entry_status: toNonEmptyString(entryPoint?.entry_status),
|
||||||
mcp_discovery_attempted: Boolean(entryPoint?.discovery_attempted),
|
mcp_discovery_attempted: Boolean(entryPoint?.discovery_attempted),
|
||||||
mcp_discovery_hot_runtime_wired: false,
|
mcp_discovery_hot_runtime_wired: hotRuntimeWired,
|
||||||
mcp_discovery_bridge_status: toNonEmptyString(bridge?.bridge_status),
|
mcp_discovery_bridge_status: toNonEmptyString(bridge?.bridge_status),
|
||||||
mcp_discovery_selected_chain_id: toNonEmptyString(planner?.selected_chain_id),
|
mcp_discovery_selected_chain_id: toNonEmptyString(planner?.selected_chain_id),
|
||||||
|
mcp_discovery_evidence_plan_v1: evidencePlan,
|
||||||
|
mcp_discovery_evidence_plan_status: toNonEmptyString(evidencePlan?.planner_status),
|
||||||
|
mcp_discovery_evidence_plan_answer_mode: toNonEmptyString(evidencePlanAnswerContract?.answer_mode),
|
||||||
|
mcp_discovery_evidence_plan_missing_axes: toStringArray(evidencePlanAxes?.missing_axes),
|
||||||
|
mcp_discovery_evidence_plan_expected_coverage: toNonEmptyString(evidencePlanCoverageGate?.expected_coverage),
|
||||||
mcp_discovery_catalog_chain_template_matches: toStringArray(planner?.catalog_chain_template_matches),
|
mcp_discovery_catalog_chain_template_matches: toStringArray(planner?.catalog_chain_template_matches),
|
||||||
mcp_discovery_catalog_chain_alignment_status: toNonEmptyString(chainAlignment?.alignment_status),
|
mcp_discovery_catalog_chain_alignment_status: toNonEmptyString(chainAlignment?.alignment_status),
|
||||||
mcp_discovery_catalog_chain_top_match: toNonEmptyString(chainAlignment?.top_chain_template_match),
|
mcp_discovery_catalog_chain_top_match: toNonEmptyString(chainAlignment?.top_chain_template_match),
|
||||||
|
|
@ -70,11 +92,18 @@ function buildAssistantMcpDiscoveryDebugAttachmentFields(input) {
|
||||||
mcp_discovery_route_candidate_fact_family: toNonEmptyString(routeCandidate?.business_fact_family),
|
mcp_discovery_route_candidate_fact_family: toNonEmptyString(routeCandidate?.business_fact_family),
|
||||||
mcp_discovery_route_candidate_action_family: toNonEmptyString(routeCandidate?.action_family),
|
mcp_discovery_route_candidate_action_family: toNonEmptyString(routeCandidate?.action_family),
|
||||||
mcp_discovery_route_candidate_proof_expectation: toNonEmptyString(routeCandidate?.proof_expectation),
|
mcp_discovery_route_candidate_proof_expectation: toNonEmptyString(routeCandidate?.proof_expectation),
|
||||||
|
mcp_discovery_route_candidate_evidence_plan_status: toNonEmptyString(routeCandidate?.evidence_plan_status),
|
||||||
|
mcp_discovery_route_candidate_evidence_answer_mode: toNonEmptyString(routeCandidate?.evidence_answer_mode),
|
||||||
|
mcp_discovery_route_candidate_evidence_expected_coverage: toNonEmptyString(routeCandidate?.evidence_expected_coverage),
|
||||||
mcp_discovery_route_candidate_missing_axes: toStringArray(routeCandidate?.missing_axes),
|
mcp_discovery_route_candidate_missing_axes: toStringArray(routeCandidate?.missing_axes),
|
||||||
mcp_discovery_route_candidate_provided_axes: toStringArray(routeCandidate?.provided_axes),
|
mcp_discovery_route_candidate_provided_axes: toStringArray(routeCandidate?.provided_axes),
|
||||||
mcp_discovery_route_candidate_executable_now: routeCandidate?.executable_now === true,
|
mcp_discovery_route_candidate_executable_now: routeCandidate?.executable_now === true,
|
||||||
mcp_discovery_route_candidate_enablement_reason: toNonEmptyString(routeCandidate?.enablement_reason),
|
mcp_discovery_route_candidate_enablement_reason: toNonEmptyString(routeCandidate?.enablement_reason),
|
||||||
mcp_discovery_route_candidate_next_action: toNonEmptyString(routeCandidate?.recommended_next_action),
|
mcp_discovery_route_candidate_next_action: toNonEmptyString(routeCandidate?.recommended_next_action),
|
||||||
|
mcp_discovery_execution_handoff_v1: executionHandoff,
|
||||||
|
mcp_discovery_execution_handoff_status: toNonEmptyString(executionHandoff?.handoff_status),
|
||||||
|
mcp_discovery_execution_handoff_allowed_hot_chain: executionHandoff?.allowed_hot_chain === true,
|
||||||
|
mcp_discovery_execution_handoff_can_use_guarded_response: executionHandoff?.can_use_guarded_response === true,
|
||||||
mcp_discovery_answer_mode: toNonEmptyString(answerDraft?.answer_mode),
|
mcp_discovery_answer_mode: toNonEmptyString(answerDraft?.answer_mode),
|
||||||
mcp_discovery_business_fact_answer_allowed: bridge?.business_fact_answer_allowed === true,
|
mcp_discovery_business_fact_answer_allowed: bridge?.business_fact_answer_allowed === true,
|
||||||
mcp_discovery_user_facing_response_allowed: bridge?.user_facing_response_allowed === true,
|
mcp_discovery_user_facing_response_allowed: bridge?.user_facing_response_allowed === true,
|
||||||
|
|
|
||||||
83
llm_normalizer/backend/dist/services/assistantMcpDiscoveryExecutionHandoff.js
vendored
Normal file
83
llm_normalizer/backend/dist/services/assistantMcpDiscoveryExecutionHandoff.js
vendored
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.ASSISTANT_MCP_DISCOVERY_EXECUTION_HANDOFF_SCHEMA_VERSION = void 0;
|
||||||
|
exports.buildAssistantMcpDiscoveryExecutionHandoff = buildAssistantMcpDiscoveryExecutionHandoff;
|
||||||
|
exports.ASSISTANT_MCP_DISCOVERY_EXECUTION_HANDOFF_SCHEMA_VERSION = "assistant_mcp_discovery_execution_handoff_v1";
|
||||||
|
const HOT_HANDOFF_CHAIN_ALLOWLIST = ["value_flow"];
|
||||||
|
function uniqueStrings(values) {
|
||||||
|
const result = [];
|
||||||
|
for (const value of values) {
|
||||||
|
const text = String(value ?? "").trim();
|
||||||
|
if (text && !result.includes(text)) {
|
||||||
|
result.push(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
function handoffStatusFor(input, allowedHotChain) {
|
||||||
|
if (input.bridgeStatus === "needs_clarification" || input.routeCandidate.candidate_status === "needs_user_scope") {
|
||||||
|
return "awaiting_user_scope";
|
||||||
|
}
|
||||||
|
if (input.bridgeStatus === "checked_sources_only") {
|
||||||
|
return "checked_sources_only";
|
||||||
|
}
|
||||||
|
if (input.bridgeStatus === "blocked" ||
|
||||||
|
input.bridgeStatus === "unsupported" ||
|
||||||
|
input.routeCandidate.candidate_status === "blocked" ||
|
||||||
|
input.routeCandidate.candidate_status === "needs_route_enablement") {
|
||||||
|
return "blocked";
|
||||||
|
}
|
||||||
|
if (!allowedHotChain) {
|
||||||
|
return "not_enabled_for_chain";
|
||||||
|
}
|
||||||
|
return "ready_for_guarded_response";
|
||||||
|
}
|
||||||
|
function buildAssistantMcpDiscoveryExecutionHandoff(input) {
|
||||||
|
const allowedHotChain = HOT_HANDOFF_CHAIN_ALLOWLIST.includes(input.routeCandidate.selected_chain_id);
|
||||||
|
const baseStatus = handoffStatusFor(input, allowedHotChain);
|
||||||
|
const readinessChecksPassed = baseStatus === "ready_for_guarded_response" &&
|
||||||
|
input.bridgeStatus === "answer_draft_ready" &&
|
||||||
|
input.routeCandidate.candidate_status === "ready_for_reviewed_execution" &&
|
||||||
|
input.routeCandidate.executable_now === true &&
|
||||||
|
input.pilot.pilot_status === "executed" &&
|
||||||
|
input.pilot.mcp_execution_performed === true &&
|
||||||
|
input.businessFactAnswerAllowed === true &&
|
||||||
|
input.userFacingResponseAllowed === true &&
|
||||||
|
input.answerDraft.internal_mechanics_allowed === false;
|
||||||
|
const handoffStatus = readinessChecksPassed ? "ready_for_guarded_response" : baseStatus;
|
||||||
|
const reasonCodes = uniqueStrings([
|
||||||
|
`execution_handoff_status_${handoffStatus}`,
|
||||||
|
allowedHotChain ? "execution_handoff_chain_allowlisted" : "execution_handoff_chain_not_allowlisted",
|
||||||
|
input.routeCandidate.executable_now
|
||||||
|
? "execution_handoff_route_candidate_executable"
|
||||||
|
: "execution_handoff_route_candidate_not_executable",
|
||||||
|
input.pilot.mcp_execution_performed
|
||||||
|
? "execution_handoff_mcp_execution_performed"
|
||||||
|
: "execution_handoff_mcp_execution_not_performed",
|
||||||
|
input.businessFactAnswerAllowed
|
||||||
|
? "execution_handoff_business_fact_allowed"
|
||||||
|
: "execution_handoff_business_fact_not_allowed",
|
||||||
|
input.userFacingResponseAllowed
|
||||||
|
? "execution_handoff_user_facing_allowed"
|
||||||
|
: "execution_handoff_user_facing_not_allowed",
|
||||||
|
readinessChecksPassed
|
||||||
|
? "execution_handoff_guarded_response_ready"
|
||||||
|
: "execution_handoff_guarded_response_not_ready"
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
schema_version: exports.ASSISTANT_MCP_DISCOVERY_EXECUTION_HANDOFF_SCHEMA_VERSION,
|
||||||
|
policy_owner: "assistantMcpDiscoveryExecutionHandoff",
|
||||||
|
handoff_status: handoffStatus,
|
||||||
|
selected_chain_id: input.routeCandidate.selected_chain_id,
|
||||||
|
route_candidate_status: input.routeCandidate.candidate_status,
|
||||||
|
evidence_answer_mode: input.routeCandidate.evidence_answer_mode,
|
||||||
|
evidence_expected_coverage: input.routeCandidate.evidence_expected_coverage,
|
||||||
|
pilot_status: input.pilot.pilot_status,
|
||||||
|
answer_mode: input.answerDraft.answer_mode,
|
||||||
|
mcp_execution_performed: input.pilot.mcp_execution_performed,
|
||||||
|
allowed_hot_chain: allowedHotChain,
|
||||||
|
can_use_guarded_response: readinessChecksPassed,
|
||||||
|
must_keep_internal_mechanics_hidden: true,
|
||||||
|
reason_codes: reasonCodes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -77,15 +77,54 @@ function aggregationAxisForPlanner(planner) {
|
||||||
const axis = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.asked_aggregation_axis)?.toLowerCase();
|
const axis = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.asked_aggregation_axis)?.toLowerCase();
|
||||||
return axis === "month" ? "month" : null;
|
return axis === "month" ? "month" : null;
|
||||||
}
|
}
|
||||||
|
function cleanExplicitEntityCandidate(value) {
|
||||||
|
let text = value.replace(/\s+/g, " ").trim();
|
||||||
|
if (!text) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
text = text
|
||||||
|
.replace(/^(?:по\s+)?(?:деньгам|деньги|денежн(?:ый|ые|ого|ому|ым|ом)?\s+поток(?:у|ом|а|и)?|нетто|расчет(?:ам|ы)?|получили|получено|заплатили|уплачено|сколько)\s+(?:с|по)\s+/iu, "")
|
||||||
|
.replace(/^(?:по\s+)?(?:контрагент(?:у|ом)?|поставщик(?:у|ом)?|клиент(?:у|ом)?)\s+/iu, "")
|
||||||
|
.replace(/\s+за\s+\d{4}(?:\s+год)?$/iu, "")
|
||||||
|
.trim();
|
||||||
|
return text || null;
|
||||||
|
}
|
||||||
|
function scoreExplicitEntityCandidate(raw, cleaned, index) {
|
||||||
|
let score = 100 - index;
|
||||||
|
if (raw.trim() === cleaned) {
|
||||||
|
score += 20;
|
||||||
|
}
|
||||||
|
if (/^(?:по\s+)?(?:деньгам|деньги|денежн(?:ый|ые|ого|ому|ым|ом)?\s+поток(?:у|ом|а|и)?|нетто|расчет(?:ам|ы)?|получили|получено|заплатили|уплачено|сколько)\s+(?:с|по)\s+/iu.test(raw)) {
|
||||||
|
score -= 40;
|
||||||
|
}
|
||||||
|
if (/[А-ЯЁA-Z]/u.test(cleaned)) {
|
||||||
|
score += 8;
|
||||||
|
}
|
||||||
|
if (/(?:^|\s)(?:ООО|АО|ПАО|ИП|Группа|Комитет|Департамент)(?:\s|$)/u.test(cleaned)) {
|
||||||
|
score += 6;
|
||||||
|
}
|
||||||
|
score -= Math.max(0, cleaned.length - 60);
|
||||||
|
return score;
|
||||||
|
}
|
||||||
function firstEntityCandidate(planner) {
|
function firstEntityCandidate(planner) {
|
||||||
const candidates = planner.discovery_plan.turn_meaning_ref?.explicit_entity_candidates ?? [];
|
const candidates = planner.discovery_plan.turn_meaning_ref?.explicit_entity_candidates ?? [];
|
||||||
for (const candidate of candidates) {
|
let best = null;
|
||||||
const text = toNonEmptyString(candidate);
|
for (let index = 0; index < candidates.length; index += 1) {
|
||||||
if (text) {
|
const candidate = candidates[index];
|
||||||
return text;
|
const raw = toNonEmptyString(candidate);
|
||||||
|
if (!raw) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const cleaned = cleanExplicitEntityCandidate(raw);
|
||||||
|
if (!cleaned) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const score = scoreExplicitEntityCandidate(raw, cleaned, index);
|
||||||
|
if (!best || score > best.score) {
|
||||||
|
best = { text: cleaned, score };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return best?.text ?? null;
|
||||||
}
|
}
|
||||||
function dateScopeToFilters(dateScope) {
|
function dateScopeToFilters(dateScope) {
|
||||||
if (!dateScope) {
|
if (!dateScope) {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ exports.ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION = void 0;
|
||||||
exports.planAssistantMcpDiscovery = planAssistantMcpDiscovery;
|
exports.planAssistantMcpDiscovery = planAssistantMcpDiscovery;
|
||||||
const assistantMcpDiscoveryPolicy_1 = require("./assistantMcpDiscoveryPolicy");
|
const assistantMcpDiscoveryPolicy_1 = require("./assistantMcpDiscoveryPolicy");
|
||||||
const assistantMcpCatalogIndex_1 = require("./assistantMcpCatalogIndex");
|
const assistantMcpCatalogIndex_1 = require("./assistantMcpCatalogIndex");
|
||||||
|
const assistantEvidencePlanner_1 = require("./assistantEvidencePlanner");
|
||||||
exports.ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION = "assistant_mcp_discovery_planner_v1";
|
exports.ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION = "assistant_mcp_discovery_planner_v1";
|
||||||
const CHUNKED_COVERAGE_PROBE_BUDGET = 30;
|
const CHUNKED_COVERAGE_PROBE_BUDGET = 30;
|
||||||
function toNonEmptyString(value) {
|
function toNonEmptyString(value) {
|
||||||
|
|
@ -41,6 +42,16 @@ function pushAllUnique(target, values) {
|
||||||
pushUnique(target, value);
|
pushUnique(target, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function uniqueStringList(values) {
|
||||||
|
const result = [];
|
||||||
|
for (const value of values) {
|
||||||
|
const text = toNonEmptyString(value);
|
||||||
|
if (text) {
|
||||||
|
pushUnique(result, text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
function pushCatalogChainTemplateAlignmentReasons(target, alignment) {
|
function pushCatalogChainTemplateAlignmentReasons(target, alignment) {
|
||||||
if (alignment.alignment_status === "selected_matches_top") {
|
if (alignment.alignment_status === "selected_matches_top") {
|
||||||
pushReason(target, "planner_catalog_chain_template_alignment_evaluated");
|
pushReason(target, "planner_catalog_chain_template_alignment_evaluated");
|
||||||
|
|
@ -1077,6 +1088,55 @@ function statusFrom(plan, review) {
|
||||||
}
|
}
|
||||||
return "ready_for_execution";
|
return "ready_for_execution";
|
||||||
}
|
}
|
||||||
|
function providedAxesFromPlanTurnMeaning(plan) {
|
||||||
|
const result = [];
|
||||||
|
const meaning = plan.turn_meaning_ref;
|
||||||
|
if ((meaning?.explicit_entity_candidates?.length ?? 0) > 0) {
|
||||||
|
pushUnique(result, "counterparty");
|
||||||
|
pushUnique(result, "business_entity");
|
||||||
|
}
|
||||||
|
if (toNonEmptyString(meaning?.explicit_organization_scope)) {
|
||||||
|
pushUnique(result, "organization");
|
||||||
|
}
|
||||||
|
if (toNonEmptyString(meaning?.explicit_date_scope)) {
|
||||||
|
pushUnique(result, "period");
|
||||||
|
}
|
||||||
|
if (toNonEmptyString(meaning?.asked_aggregation_axis)) {
|
||||||
|
pushUnique(result, "aggregate_axis");
|
||||||
|
}
|
||||||
|
if (toNonEmptyString(meaning?.metadata_scope_hint)) {
|
||||||
|
pushUnique(result, "metadata_scope");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
function preferredClarificationAxesForRecipe(input) {
|
||||||
|
if (input.recipe.chainId === "value_flow_ranking" ||
|
||||||
|
input.recipe.chainId === "value_flow_comparison" ||
|
||||||
|
input.recipe.chainId === "business_overview") {
|
||||||
|
return ["organization"];
|
||||||
|
}
|
||||||
|
if (input.dataNeedGraph?.business_fact_family === "value_flow" && !hasSubjectCandidates(input.dataNeedGraph)) {
|
||||||
|
return ["organization"];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
function missingAxesFromCatalogReview(review, input) {
|
||||||
|
const result = [];
|
||||||
|
for (const options of Object.values(review.missing_axes_by_primitive)) {
|
||||||
|
const candidates = options
|
||||||
|
.map((option) => uniqueStringList(option).filter((axis) => !input.providedAxes.includes(axis)))
|
||||||
|
.filter((option) => option.length > 0);
|
||||||
|
const preferredCandidate = candidates.find((option) => option.some((axis) => input.preferredAxes.includes(axis)));
|
||||||
|
const candidate = preferredCandidate ?? candidates.sort((left, right) => left.length - right.length)[0];
|
||||||
|
if (!candidate) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const axis of candidate) {
|
||||||
|
pushUnique(result, axis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
function planAssistantMcpDiscovery(input) {
|
function planAssistantMcpDiscovery(input) {
|
||||||
const recipe = recipeFor(input);
|
const recipe = recipeFor(input);
|
||||||
const budgetOverride = budgetOverrideFor(input, recipe);
|
const budgetOverride = budgetOverrideFor(input, recipe);
|
||||||
|
|
@ -1139,6 +1199,17 @@ function planAssistantMcpDiscovery(input) {
|
||||||
else {
|
else {
|
||||||
pushReason(reasonCodes, "planner_needs_more_user_or_scope_context");
|
pushReason(reasonCodes, "planner_needs_more_user_or_scope_context");
|
||||||
}
|
}
|
||||||
|
const evidencePlan = (0, assistantEvidencePlanner_1.buildAssistantEvidencePlanner)({
|
||||||
|
selectedChainId: recipe.chainId,
|
||||||
|
plannerStatus,
|
||||||
|
semanticDataNeed,
|
||||||
|
dataNeedGraph,
|
||||||
|
discoveryPlan: plan,
|
||||||
|
additionalMissingAxes: missingAxesFromCatalogReview(adjustedReview, {
|
||||||
|
providedAxes: providedAxesFromPlanTurnMeaning(plan),
|
||||||
|
preferredAxes: preferredClarificationAxesForRecipe({ recipe, dataNeedGraph })
|
||||||
|
})
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION,
|
schema_version: exports.ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION,
|
||||||
policy_owner: "assistantMcpDiscoveryPlanner",
|
policy_owner: "assistantMcpDiscoveryPlanner",
|
||||||
|
|
@ -1152,6 +1223,7 @@ function planAssistantMcpDiscovery(input) {
|
||||||
catalog_chain_template_alignment: catalogChainTemplateAlignment,
|
catalog_chain_template_alignment: catalogChainTemplateAlignment,
|
||||||
proposed_primitives: recipe.primitives,
|
proposed_primitives: recipe.primitives,
|
||||||
required_axes: recipe.axes,
|
required_axes: recipe.axes,
|
||||||
|
evidence_plan: evidencePlan,
|
||||||
discovery_plan: plan,
|
discovery_plan: plan,
|
||||||
catalog_review: adjustedReview,
|
catalog_review: adjustedReview,
|
||||||
reason_codes: reasonCodes
|
reason_codes: reasonCodes
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,45 @@ function requestsFinancialCounterpartyBoundary(turnMeaning, graph) {
|
||||||
return (/(?:банк|сбербанк|финанс|кредит|депозит)/iu.test(text) &&
|
return (/(?:банк|сбербанк|финанс|кредит|депозит)/iu.test(text) &&
|
||||||
/(?:клиент|поставщик|выручк|топ|обычн|роль|поток)/iu.test(text));
|
/(?:клиент|поставщик|выручк|топ|обычн|роль|поток)/iu.test(text));
|
||||||
}
|
}
|
||||||
|
function requestsCompactCashflowAnswer(turnMeaning, graph) {
|
||||||
|
const text = normalizeQuestionText([
|
||||||
|
turnMeaning?.raw_message,
|
||||||
|
turnMeaning?.effective_message,
|
||||||
|
graph?.source_message,
|
||||||
|
graph?.question
|
||||||
|
].join(" "));
|
||||||
|
if (!text) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (/(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)[\s\S]{0,80}(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\p{L}*|\u0443\u0448\p{L}*|\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*)|(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e)[\s\S]{0,80}(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)/iu.test(text)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return /(?:\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}+|\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?\b|\u0431\u0435\u0437\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0431\u0438\u0432\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0440\u0435\u0437\p{L}*|\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b\p{L}*|\u043e\u0434\u043d\u043e\u0439\s+\u0441\u0442\u0440\u043e\u043a\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0438\u0442\u043e\u0433|\u0434\u0435\u043d\p{L}{0,20}\s+\u043d\u0435\u0442\u0442\u043e|\u043f\u043e\u043b\u0443\u0447\p{L}*[\s\S]{0,80}\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\u043b\p{L}*[\s\S]{0,80}\u0443\u0448\u043b\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*)[\s\S]{0,120}(?:\u0434\u0435\u043d\p{L}*|\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u043f\u043e\u043b\u0443\u0447|\u043f\u0440\u0438\u0448\u043b))/iu.test(text);
|
||||||
|
}
|
||||||
|
function requestsCashflowPolarityAnswer(turnMeaning, graph) {
|
||||||
|
const text = normalizeQuestionText([
|
||||||
|
turnMeaning?.raw_message,
|
||||||
|
turnMeaning?.effective_message,
|
||||||
|
graph?.source_message,
|
||||||
|
graph?.question
|
||||||
|
].join(" "));
|
||||||
|
return /(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)[\s\S]{0,80}(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441)|(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441)[\s\S]{0,80}(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)/iu.test(text);
|
||||||
|
}
|
||||||
|
function requestsDirectAccountingProfitAnswer(turnMeaning, graph) {
|
||||||
|
const text = normalizeQuestionText([
|
||||||
|
turnMeaning?.raw_message,
|
||||||
|
turnMeaning?.effective_message,
|
||||||
|
graph?.source_message,
|
||||||
|
graph?.question
|
||||||
|
].join(" "));
|
||||||
|
if (!/(?:чист\p{L}{0,8}\s+прибыл|финрезультат|финансов\p{L}{0,12}\s+результат)/iu.test(text)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (/(?:^|\s)(?:а\s+)?это\s+чист\p{L}{0,8}\s+прибыл|это\s+прибыл\p{L}{0,4}\s+или\s+нет/iu.test(text)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(?:какая|какой|сколько|посчитай|рассчитай|покажи|дай|определить|определи|найди|итог|за\s+\d{4})/iu.test(text);
|
||||||
|
}
|
||||||
function toStringList(value) {
|
function toStringList(value) {
|
||||||
if (!Array.isArray(value)) {
|
if (!Array.isArray(value)) {
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -215,7 +254,7 @@ function localizeLine(value) {
|
||||||
return "Сумма исходящих платежей рассчитана только по подтвержденным строкам списаний в 1С.";
|
return "Сумма исходящих платежей рассчитана только по подтвержденным строкам списаний в 1С.";
|
||||||
}
|
}
|
||||||
if (/^Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows$/i.test(value)) {
|
if (/^Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows$/i.test(value)) {
|
||||||
return "Нетто денежного потока рассчитано только как входящие подтвержденные строки 1С минус исходящие подтвержденные строки 1С.";
|
return "Нетто денежного потока рассчитано как подтвержденные входящие платежи минус подтвержденные исходящие платежи в 1С.";
|
||||||
}
|
}
|
||||||
if (/^Counterparty monthly net value-flow breakdown was grouped by month over confirmed incoming and outgoing 1C rows$/i.test(value)) {
|
if (/^Counterparty monthly net value-flow breakdown was grouped by month over confirmed incoming and outgoing 1C rows$/i.test(value)) {
|
||||||
return "Помесячная нетто-раскладка сгруппирована только по подтвержденным входящим и исходящим строкам 1С.";
|
return "Помесячная нетто-раскладка сгруппирована только по подтвержденным входящим и исходящим строкам 1С.";
|
||||||
|
|
@ -403,9 +442,9 @@ function businessOverviewCoverageLimitLine(overview) {
|
||||||
if (outgoing?.coverage_limited_by_probe_limit === true) {
|
if (outgoing?.coverage_limited_by_probe_limit === true) {
|
||||||
limited.push("исходящие");
|
limited.push("исходящие");
|
||||||
}
|
}
|
||||||
const continuation = "Если нужен полный сквозной ответ, безопасный следующий шаг — выбрать конкретный год или квартал для дозапроса: тогда широкий срез можно собрать частями без выдачи непроверенного итога.";
|
const continuation = "Для полного сквозного итога лучше разбить проверку по годам или кварталам.";
|
||||||
return limited.length > 0
|
return limited.length > 0
|
||||||
? `Важно: по направлению ${limited.join(" и ")} проверка достигла лимита строк; это расширенный проверенный срез найденных строк, но не гарантия полного бухгалтерского оборота без отдельной полной выгрузки. ${continuation}`
|
? `Ограничение: широкий денежный срез по направлению ${limited.join(" и ")} не считаю полным бухгалтерским оборотом без отдельной полной выгрузки. ${continuation}`
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
function joinBusinessReplyLines(lines) {
|
function joinBusinessReplyLines(lines) {
|
||||||
|
|
@ -520,37 +559,15 @@ function businessOverviewInventoryLine(overview) {
|
||||||
}
|
}
|
||||||
const amount = moneyText(inventory.total_amount_human_ru);
|
const amount = moneyText(inventory.total_amount_human_ru);
|
||||||
const rows = Number(inventory.rows_matched);
|
const rows = Number(inventory.rows_matched);
|
||||||
const quantity = Number(inventory.total_quantity);
|
|
||||||
if (!amount && !Number.isFinite(rows)) {
|
if (!amount && !Number.isFinite(rows)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const pieces = [
|
const pieces = [
|
||||||
Number.isFinite(rows) ? `${rows} позиций` : null,
|
Number.isFinite(rows) ? `${rows} позиций` : null,
|
||||||
amount ? `на ${sentenceAmount(amount) ?? amount}` : null,
|
amount ? `на ${sentenceAmount(amount) ?? amount}` : null
|
||||||
Number.isFinite(quantity) && quantity > 0 ? `количество ${quantity}` : null
|
|
||||||
].filter((item) => Boolean(item));
|
].filter((item) => Boolean(item));
|
||||||
return pieces.length > 0 ? `Склад: ${pieces.join(", ")}.` : null;
|
return pieces.length > 0 ? `Склад: ${pieces.join(", ")}.` : null;
|
||||||
}
|
}
|
||||||
function rowCountText(value) {
|
|
||||||
const count = Number(value);
|
|
||||||
return Number.isFinite(count) ? String(count) : null;
|
|
||||||
}
|
|
||||||
function sideRowsText(side) {
|
|
||||||
const rowsWithAmount = rowCountText(side?.rows_with_amount);
|
|
||||||
const rowsMatched = rowCountText(side?.rows_matched);
|
|
||||||
if (rowsWithAmount && rowsMatched) {
|
|
||||||
return `${rowsWithAmount} из ${rowsMatched}`;
|
|
||||||
}
|
|
||||||
return rowsWithAmount ?? rowsMatched;
|
|
||||||
}
|
|
||||||
function sideDateText(side) {
|
|
||||||
const first = toNonEmptyString(side?.first_movement_date);
|
|
||||||
const latest = toNonEmptyString(side?.latest_movement_date);
|
|
||||||
if (first && latest) {
|
|
||||||
return first === latest ? `дата ${first}` : `даты ${first}..${latest}`;
|
|
||||||
}
|
|
||||||
return first ? `первая дата ${first}` : latest ? `последняя дата ${latest}` : null;
|
|
||||||
}
|
|
||||||
function bidirectionalNetLabel(direction) {
|
function bidirectionalNetLabel(direction) {
|
||||||
if (direction === "net_outgoing") {
|
if (direction === "net_outgoing") {
|
||||||
return "нетто в сторону контрагента";
|
return "нетто в сторону контрагента";
|
||||||
|
|
@ -586,28 +603,24 @@ function buildCompactBidirectionalValueFlowReply(entryPoint, draft) {
|
||||||
: "по выбранному контуру";
|
: "по выбранному контуру";
|
||||||
const period = toNonEmptyString(flow.period_scope);
|
const period = toNonEmptyString(flow.period_scope);
|
||||||
const periodText = period ? ` за период ${period}` : " в проверенном окне";
|
const periodText = period ? ` за период ${period}` : " в проверенном окне";
|
||||||
const incomingRows = sideRowsText(incoming);
|
|
||||||
const outgoingRows = sideRowsText(outgoing);
|
|
||||||
const incomingDates = sideDateText(incoming);
|
|
||||||
const outgoingDates = sideDateText(outgoing);
|
|
||||||
const netLabel = bidirectionalNetLabel(flow.net_direction);
|
const netLabel = bidirectionalNetLabel(flow.net_direction);
|
||||||
const lines = [
|
const lines = [
|
||||||
`Коротко: ${subjectLead}${periodText} по найденным строкам 1С получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}; расчетное ${netLabel}: ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`
|
`Коротко: ${subjectLead}${periodText} по найденным строкам 1С получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}; расчетное ${netLabel}: ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`
|
||||||
];
|
];
|
||||||
const basis = [];
|
const basis = [];
|
||||||
if (incomingRows) {
|
if (incomingAmount) {
|
||||||
basis.push(`входящих строк с суммой ${incomingRows}${incomingDates ? ` (${incomingDates})` : ""}`);
|
basis.push("входящие платежи");
|
||||||
}
|
}
|
||||||
if (outgoingRows) {
|
if (outgoingAmount) {
|
||||||
basis.push(`исходящих строк с суммой ${outgoingRows}${outgoingDates ? ` (${outgoingDates})` : ""}`);
|
basis.push("исходящие платежи");
|
||||||
}
|
}
|
||||||
if (basis.length > 0) {
|
if (basis.length > 0) {
|
||||||
lines.push(`Основа: ${basis.join("; ")}.`);
|
lines.push(`Основа: проверенные ${basis.join(" и ")} по этому контрагенту в 1С.`);
|
||||||
}
|
}
|
||||||
if (flow.coverage_limited_by_probe_limit === true) {
|
if (flow.coverage_limited_by_probe_limit === true) {
|
||||||
lines.push("Важно: часть проверки достигла лимита строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода.");
|
lines.push("Важно: часть проверки достигла лимита строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода.");
|
||||||
}
|
}
|
||||||
lines.push("Метод: нетто рассчитано как подтвержденные входящие строки 1С минус подтвержденные исходящие строки; это не полное бухгалтерское сальдо вне проверенного окна.");
|
lines.push("Метод: нетто рассчитано как подтвержденные входящие платежи минус подтвержденные исходящие платежи; это не полное бухгалтерское сальдо вне проверенного окна.");
|
||||||
const fallbackNextStep = toNonEmptyString(draft.next_step_line);
|
const fallbackNextStep = toNonEmptyString(draft.next_step_line);
|
||||||
if (fallbackNextStep) {
|
if (fallbackNextStep) {
|
||||||
lines.push(`Следующий шаг: ${localizeLine(fallbackNextStep)}`);
|
lines.push(`Следующий шаг: ${localizeLine(fallbackNextStep)}`);
|
||||||
|
|
@ -621,6 +634,18 @@ function compactComparable(value) {
|
||||||
.replace(/\s+/g, " ")
|
.replace(/\s+/g, " ")
|
||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
function businessOverviewOrganizationScopeLabel(value) {
|
||||||
|
const text = toNonEmptyString(value);
|
||||||
|
if (!text) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const comparable = compactComparable(text);
|
||||||
|
if (/^(?:с|без)\s+разбивк/.test(comparable) ||
|
||||||
|
/\b(?:входящ|исходящ|нетто|топ|контрагент|платеж|поступлен)\b/.test(comparable)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
function businessOverviewSeparateSubjectLabel(graph, turnMeaning, organizationScope) {
|
function businessOverviewSeparateSubjectLabel(graph, turnMeaning, organizationScope) {
|
||||||
const candidates = uniqueStrings([
|
const candidates = uniqueStrings([
|
||||||
...toStringList(turnMeaning?.business_overview_separate_entity_candidates),
|
...toStringList(turnMeaning?.business_overview_separate_entity_candidates),
|
||||||
|
|
@ -673,21 +698,17 @@ function buildPreviousCounterpartyValueFlowSummary(flow, separateSubject, docume
|
||||||
const lead = `; отдельно по ${counterparty}: получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}, ` +
|
const lead = `; отдельно по ${counterparty}: получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}, ` +
|
||||||
`${netLabel} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}`;
|
`${netLabel} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}`;
|
||||||
const basis = [];
|
const basis = [];
|
||||||
const incomingRows = sideRowsText(incoming);
|
if (incomingAmount) {
|
||||||
const outgoingRows = sideRowsText(outgoing);
|
basis.push("входящие платежи");
|
||||||
const incomingDates = sideDateText(incoming);
|
|
||||||
const outgoingDates = sideDateText(outgoing);
|
|
||||||
if (incomingRows) {
|
|
||||||
basis.push(`входящие строки ${incomingRows}${incomingDates ? ` (${incomingDates})` : ""}`);
|
|
||||||
}
|
}
|
||||||
if (outgoingRows) {
|
if (outgoingAmount) {
|
||||||
basis.push(`исходящие строки ${outgoingRows}${outgoingDates ? ` (${outgoingDates})` : ""}`);
|
basis.push("исходящие платежи");
|
||||||
}
|
}
|
||||||
const documents = previousDocumentSummaryLine(documentBundle, counterparty);
|
const documents = previousDocumentSummaryLine(documentBundle, counterparty);
|
||||||
if (documents) {
|
if (documents) {
|
||||||
basis.push(documents);
|
basis.push(documents);
|
||||||
}
|
}
|
||||||
const basisText = basis.length > 0 ? ` Основа: ${basis.join("; ")}.` : "";
|
const basisText = basis.length > 0 ? ` Основа: проверенные ${basis.join(" и ")}.` : "";
|
||||||
return {
|
return {
|
||||||
lead,
|
lead,
|
||||||
line: `Отдельно по контрагенту ${counterparty}: подтверждено получили ${incomingAmount ?? "0 руб."}, ` +
|
line: `Отдельно по контрагенту ${counterparty}: подтверждено получили ${incomingAmount ?? "0 руб."}, ` +
|
||||||
|
|
@ -716,7 +737,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||||
const netDirection = overview.net_direction === "net_outgoing" ? "операционное нетто в минус" : "расчетное операционное нетто";
|
const netDirection = overview.net_direction === "net_outgoing" ? "операционное нетто в минус" : "расчетное операционное нетто";
|
||||||
const period = businessOverviewPeriodText(overview);
|
const period = businessOverviewPeriodText(overview);
|
||||||
const limitLine = businessOverviewCoverageLimitLine(overview);
|
const limitLine = businessOverviewCoverageLimitLine(overview);
|
||||||
const organizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope);
|
const organizationScope = businessOverviewOrganizationScopeLabel(turnMeaning?.explicit_organization_scope);
|
||||||
const separateSubject = businessOverviewSeparateSubjectLabel(graph, turnMeaning, organizationScope);
|
const separateSubject = businessOverviewSeparateSubjectLabel(graph, turnMeaning, organizationScope);
|
||||||
const previousCounterpartySummary = buildPreviousCounterpartyValueFlowSummary(toRecordObject(turnMeaning?.previous_counterparty_value_flow_bundle), separateSubject, toRecordObject(turnMeaning?.previous_counterparty_document_bundle));
|
const previousCounterpartySummary = buildPreviousCounterpartyValueFlowSummary(toRecordObject(turnMeaning?.previous_counterparty_value_flow_bundle), separateSubject, toRecordObject(turnMeaning?.previous_counterparty_document_bundle));
|
||||||
const organizationPrefix = organizationScope ? `по компании ${organizationScope} ` : "";
|
const organizationPrefix = organizationScope ? `по компании ${organizationScope} ` : "";
|
||||||
|
|
@ -760,6 +781,23 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||||
const debtDueDateBoundary = actionFamily === "debt_due_date_boundary" || unsupportedFamily === "debt_due_date_boundary";
|
const debtDueDateBoundary = actionFamily === "debt_due_date_boundary" || unsupportedFamily === "debt_due_date_boundary";
|
||||||
const vendorRiskBoundary = actionFamily === "vendor_risk_procurement_boundary" || unsupportedFamily === "vendor_risk_procurement_boundary";
|
const vendorRiskBoundary = actionFamily === "vendor_risk_procurement_boundary" || unsupportedFamily === "vendor_risk_procurement_boundary";
|
||||||
const inventoryReserveBoundary = actionFamily === "inventory_reserve_boundary" || unsupportedFamily === "inventory_reserve_liquidation_boundary";
|
const inventoryReserveBoundary = actionFamily === "inventory_reserve_boundary" || unsupportedFamily === "inventory_reserve_liquidation_boundary";
|
||||||
|
const compactCashflowRequested = directMoneyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph);
|
||||||
|
const cashflowPolarityRequested = compactCashflowRequested && requestsCashflowPolarityAnswer(turnMeaning, graph);
|
||||||
|
const directAccountingProfitRequested = requestsDirectAccountingProfitAnswer(turnMeaning, graph);
|
||||||
|
if (compactCashflowRequested && !rankingNeed && (incomingAmount || outgoingAmount || netAmount)) {
|
||||||
|
const netDisplay = sentenceAmount(netAmount) ?? netAmount ?? "0 \u0440\u0443\u0431.";
|
||||||
|
const signedNetDisplay = cashflowPolarityRequested && netDisplay && !String(netDisplay).trim().startsWith("-")
|
||||||
|
? `+${netDisplay}`
|
||||||
|
: netDisplay;
|
||||||
|
const polarityLead = cashflowPolarityRequested
|
||||||
|
? `\u041a\u043e\u0440\u043e\u0442\u043a\u043e: \u043f\u043e \u0434\u0435\u043d\u044c\u0433\u0430\u043c \u043f\u043b\u044e\u0441. `
|
||||||
|
: "\u041a\u043e\u0440\u043e\u0442\u043a\u043e: ";
|
||||||
|
lines.push(`${polarityLead}${organizationPrefix}${period} \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438 ${incomingAmount ?? "0 \u0440\u0443\u0431."}; \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438/\u0441\u043f\u0438\u0441\u0430\u043b\u0438 ${outgoingAmount ?? "0 \u0440\u0443\u0431."}; \u0434\u0435\u043d\u0435\u0436\u043d\u043e\u0435 \u043d\u0435\u0442\u0442\u043e ${signedNetDisplay}.`);
|
||||||
|
lines.push(cashflowPolarityRequested
|
||||||
|
? "\u042d\u0442\u043e \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0439 \u043f\u043e\u0442\u043e\u043a \u043f\u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u043c \u0441\u0442\u0440\u043e\u043a\u0430\u043c 1\u0421, \u043d\u0435 \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0441\u043a\u0438\u0439 \u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442."
|
||||||
|
: "\u042d\u0442\u043e \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0439 \u043f\u043e\u0442\u043e\u043a \u043f\u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u043c \u0441\u0442\u0440\u043e\u043a\u0430\u043c 1\u0421, \u043d\u0435 \u0447\u0438\u0441\u0442\u0430\u044f \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u0438 \u043d\u0435 \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0441\u043a\u0438\u0439 \u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442.");
|
||||||
|
return joinBusinessReplyLines(lines);
|
||||||
|
}
|
||||||
if (profitMarginBoundary) {
|
if (profitMarginBoundary) {
|
||||||
const accountingFinancialResult = toRecordObject(overview.accounting_financial_result);
|
const accountingFinancialResult = toRecordObject(overview.accounting_financial_result);
|
||||||
if (accountingFinancialResult) {
|
if (accountingFinancialResult) {
|
||||||
|
|
@ -780,8 +818,12 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||||
? `минус ${amount}`
|
? `минус ${amount}`
|
||||||
: amount
|
: amount
|
||||||
: "сумма не распознана";
|
: "сумма не распознана";
|
||||||
lines.push(`Коротко: нет, денежное операционное нетто не стоит считать чистой прибылью. Отдельно по закрытию счетов 90/91/99 в 1С за ${periodScope} подтвержден ${directionText}: ${amountText}${marginPct ? `; маржа к подтвержденной выручке ${marginPct}` : "; маржа к подтвержденной выручке не рассчитана"}.`);
|
lines.push(directAccountingProfitRequested
|
||||||
lines.push("Это учетный финрезультат по найденным строкам закрытия периода в 1С, а не внешний аудит и не юридически подтвержденная отчетность.");
|
? `Коротко: за ${periodScope} по закрытию счетов 90/91/99 в 1С подтвержден ${directionText}: ${amountText}${marginPct ? `; маржа к подтвержденной выручке ${marginPct}` : "; маржа к подтвержденной выручке не рассчитана"}.`
|
||||||
|
: `Коротко: нет, денежное операционное нетто не стоит считать чистой прибылью. Отдельно по закрытию счетов 90/91/99 в 1С за ${periodScope} подтвержден ${directionText}: ${amountText}${marginPct ? `; маржа к подтвержденной выручке ${marginPct}` : "; маржа к подтвержденной выручке не рассчитана"}.`);
|
||||||
|
lines.push(directAccountingProfitRequested
|
||||||
|
? "Это учетный финрезультат по найденным строкам закрытия периода в 1С, не денежный поток, не внешний аудит и не юридически подтвержденная отчетность."
|
||||||
|
: "Это учетный финрезультат по найденным строкам закрытия периода в 1С, а не внешний аудит и не юридически подтвержденная отчетность.");
|
||||||
return joinBusinessReplyLines(lines);
|
return joinBusinessReplyLines(lines);
|
||||||
}
|
}
|
||||||
const headline = toNonEmptyString(draft.headline);
|
const headline = toNonEmptyString(draft.headline);
|
||||||
|
|
@ -931,6 +973,58 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||||
lines.push("Проверить нужно отдельно: складской срез на дату, учетную политику резервов, списания и ликвидационную стоимость; косвенные признаки нельзя выдавать за доказанный факт резерва.");
|
lines.push("Проверить нужно отдельно: складской срез на дату, учетную политику резервов, списания и ликвидационную стоимость; косвенные признаки нельзя выдавать за доказанный факт резерва.");
|
||||||
return joinBusinessReplyLines(lines);
|
return joinBusinessReplyLines(lines);
|
||||||
}
|
}
|
||||||
|
if (!separateSubject && !crossScopeExecutiveSummary && (actionFamily === "broad_evaluation" || unsupportedFamily === "broad_business_evaluation")) {
|
||||||
|
const subject = organizationScope ?? "компания";
|
||||||
|
const periodWithoutPrefix = period.replace(/^за\s+/iu, "");
|
||||||
|
lines.push(`Коротко: по доступным данным ${subject} выглядит как бизнес с крупными контрактными денежными потоками и заметной зависимостью от нескольких крупных контрагентов, а не как равномерный поток мелких продаж.`);
|
||||||
|
lines.push("Что видно:");
|
||||||
|
if (incomingAmount) {
|
||||||
|
lines.push(`- входящие деньги за ${periodWithoutPrefix}: ${incomingAmount};`);
|
||||||
|
}
|
||||||
|
if (outgoingAmount) {
|
||||||
|
lines.push(`- исходящие платежи/списания: ${outgoingAmount};`);
|
||||||
|
}
|
||||||
|
if (netAmount) {
|
||||||
|
lines.push(`- ${netDirection}: ${sentenceAmount(netAmount) ?? netAmount};`);
|
||||||
|
}
|
||||||
|
if (customerName && customerAmount) {
|
||||||
|
lines.push(topCustomerLooksFinancial
|
||||||
|
? `- крупнейший входящий источник: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}; это похоже на финансовый контур, не на обычную клиентскую выручку;${nonFinancialCustomer ? ` крупнейший небанковский входящий контрагент: ${nonFinancialCustomer};` : ""}`
|
||||||
|
: `- крупнейший источник денег: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}; это сигнал концентрации на крупном заказчике;`);
|
||||||
|
}
|
||||||
|
if (topSupplier) {
|
||||||
|
lines.push(topSupplierLooksFinancial
|
||||||
|
? `- крупнейший получатель исходящих денег: ${topSupplier}; это похоже на финансовый контур, не на обычного поставщика;`
|
||||||
|
: `- крупнейший получатель исходящих денег: ${topSupplier};`);
|
||||||
|
}
|
||||||
|
const inventoryLine = businessOverviewInventoryLine(overview);
|
||||||
|
if (inventoryLine) {
|
||||||
|
lines.push(`- ${localizeLine(inventoryLine)}`);
|
||||||
|
}
|
||||||
|
const debtLine = businessOverviewDebtLine(overview);
|
||||||
|
if (debtLine) {
|
||||||
|
lines.push(`- ${localizeLine(debtLine)}`);
|
||||||
|
}
|
||||||
|
lines.push("Ограничение: это оценка по денежным потокам и найденным срезам 1С, не аудиторское заключение и не подтвержденная чистая прибыль.");
|
||||||
|
const missingOverviewFamilies = [];
|
||||||
|
if (!businessOverviewTaxLine(overview)) {
|
||||||
|
missingOverviewFamilies.push("НДС/налоговая позиция без отдельного точного расчета");
|
||||||
|
}
|
||||||
|
if (!debtLine) {
|
||||||
|
missingOverviewFamilies.push("долги без даты среза");
|
||||||
|
}
|
||||||
|
if (!inventoryLine) {
|
||||||
|
missingOverviewFamilies.push("склад без даты среза");
|
||||||
|
}
|
||||||
|
if (missingOverviewFamilies.length > 0) {
|
||||||
|
lines.push(`Что не подтверждено в этом срезе: ${missingOverviewFamilies.join(", ")}.`);
|
||||||
|
}
|
||||||
|
if (limitLine) {
|
||||||
|
lines.push(limitLine);
|
||||||
|
}
|
||||||
|
lines.push("Что проверить дальше: чистую прибыль через 90/91/99, маржинальность по проектам, зависимость от топ-3 контрагентов, старые складские остатки и зависшие расчеты.");
|
||||||
|
return joinBusinessReplyLines(lines);
|
||||||
|
}
|
||||||
if (crossScopeExecutiveSummary && separateSubject && previousCounterpartySummary && (incomingAmount || outgoingAmount || netAmount)) {
|
if (crossScopeExecutiveSummary && separateSubject && previousCounterpartySummary && (incomingAmount || outgoingAmount || netAmount)) {
|
||||||
lines.push(`Коротко: по компании ${organizationScope ?? "в выбранном контуре"} ${period} подтвержден денежный срез: получили ${incomingAmount ?? "0 руб."}, исходящие платежи/списания ${outgoingAmount ?? "0 руб."}, ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}${previousCounterpartySummary.lead}; можно утверждать только эти подтвержденные срезы, нельзя называть это чистой прибылью, полным оборотом или доказанной ролью главного клиента/поставщика.`);
|
lines.push(`Коротко: по компании ${organizationScope ?? "в выбранном контуре"} ${period} подтвержден денежный срез: получили ${incomingAmount ?? "0 руб."}, исходящие платежи/списания ${outgoingAmount ?? "0 руб."}, ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}${previousCounterpartySummary.lead}; можно утверждать только эти подтвержденные срезы, нельзя называть это чистой прибылью, полным оборотом или доказанной ролью главного клиента/поставщика.`);
|
||||||
lines.push(previousCounterpartySummary.line);
|
lines.push(previousCounterpartySummary.line);
|
||||||
|
|
@ -942,6 +1036,29 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||||
return joinBusinessReplyLines(lines);
|
return joinBusinessReplyLines(lines);
|
||||||
}
|
}
|
||||||
if (rankingNeed) {
|
if (rankingNeed) {
|
||||||
|
const explicitPeriodRankingOverview = period &&
|
||||||
|
!/(?:все\s+доступное|все\s+время|all\s+time)/iu.test(period) &&
|
||||||
|
(incomingAmount || outgoingAmount || netAmount);
|
||||||
|
if (explicitPeriodRankingOverview) {
|
||||||
|
lines.push(`Коротко: ${organizationPrefix}${period} денежная картина подтверждена по найденным строкам 1С.`);
|
||||||
|
lines.push(`Деньги: входящие ${incomingAmount ?? "0 руб."}, исходящие ${outgoingAmount ?? "0 руб."}, расчетное операционное нетто ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`);
|
||||||
|
if (customerName && customerAmount) {
|
||||||
|
lines.push(topCustomerLooksFinancial
|
||||||
|
? `Топ входящих: 1. ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}. Это финансовый/банковский контур, не считаю его клиентской выручкой без назначения платежа.${nonFinancialCustomer ? ` 2. Крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}.` : ""}`
|
||||||
|
: `Крупнейший входящий контрагент: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}.`);
|
||||||
|
}
|
||||||
|
if (topSupplier) {
|
||||||
|
lines.push(topSupplierLooksFinancial
|
||||||
|
? `Топ исходящих: 1. ${topSupplier}. Это финансовый/банковский контур, не считаю его обычным поставщиком без назначения платежа и договора.${nonFinancialSupplier ? ` 2. Крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}.` : ""}`
|
||||||
|
: `Крупнейший получатель исходящих денег: ${topSupplier}.`);
|
||||||
|
}
|
||||||
|
lines.push(`Вывод: по движению денег период ${netDirection}; это не чистая прибыль и не бухгалтерский финрезультат.`);
|
||||||
|
if (requestedFinancialBoundaryLine) {
|
||||||
|
lines.push(requestedFinancialBoundaryLine);
|
||||||
|
}
|
||||||
|
lines.push("Следующий шаг: могу отдельно посчитать чистую прибыль через закрытие 90/91/99 или разложить этот период по контрагентам.");
|
||||||
|
return joinBusinessReplyLines(lines);
|
||||||
|
}
|
||||||
const incomingLeader = strongestIncomingYear(overview);
|
const incomingLeader = strongestIncomingYear(overview);
|
||||||
const canRankYearlyNet = !limitLine;
|
const canRankYearlyNet = !limitLine;
|
||||||
const netLeader = canRankYearlyNet ? strongestNetYear(overview) : null;
|
const netLeader = canRankYearlyNet ? strongestNetYear(overview) : null;
|
||||||
|
|
@ -1062,6 +1179,14 @@ function statusFrom(entryPoint) {
|
||||||
}
|
}
|
||||||
return "not_applicable";
|
return "not_applicable";
|
||||||
}
|
}
|
||||||
|
function hasGuardedHotHandoff(entryPoint) {
|
||||||
|
const bridge = toRecordObject(entryPoint?.bridge);
|
||||||
|
const handoff = toRecordObject(bridge?.execution_handoff);
|
||||||
|
return (entryPoint?.hot_runtime_wired === true &&
|
||||||
|
bridge?.hot_runtime_wired === true &&
|
||||||
|
handoff?.can_use_guarded_response === true &&
|
||||||
|
handoff?.must_keep_internal_mechanics_hidden === true);
|
||||||
|
}
|
||||||
function replyTypeFor(status) {
|
function replyTypeFor(status) {
|
||||||
if (status === "clarification_candidate") {
|
if (status === "clarification_candidate") {
|
||||||
return "clarification_required";
|
return "clarification_required";
|
||||||
|
|
@ -1102,9 +1227,12 @@ function buildReplyText(entryPoint, status) {
|
||||||
function buildAssistantMcpDiscoveryResponseCandidate(entryPoint) {
|
function buildAssistantMcpDiscoveryResponseCandidate(entryPoint) {
|
||||||
const entry = entryPoint ?? null;
|
const entry = entryPoint ?? null;
|
||||||
const status = statusFrom(entry);
|
const status = statusFrom(entry);
|
||||||
|
const guardedHotHandoff = hasGuardedHotHandoff(entry);
|
||||||
const reasonCodes = uniqueStrings(entry?.reason_codes ?? []);
|
const reasonCodes = uniqueStrings(entry?.reason_codes ?? []);
|
||||||
pushReason(reasonCodes, `mcp_discovery_response_candidate_${status}`);
|
pushReason(reasonCodes, `mcp_discovery_response_candidate_${status}`);
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_candidate_not_hot_wired");
|
pushReason(reasonCodes, guardedHotHandoff
|
||||||
|
? "mcp_discovery_response_candidate_guarded_hot_wired"
|
||||||
|
: "mcp_discovery_response_candidate_not_hot_wired");
|
||||||
const replyText = entry && (status === "ready_for_guarded_use" || status === "checked_sources_only_candidate" || status === "clarification_candidate")
|
const replyText = entry && (status === "ready_for_guarded_use" || status === "checked_sources_only_candidate" || status === "clarification_candidate")
|
||||||
? buildReplyText(entry, status)
|
? buildReplyText(entry, status)
|
||||||
: null;
|
: null;
|
||||||
|
|
@ -1112,10 +1240,11 @@ function buildAssistantMcpDiscoveryResponseCandidate(entryPoint) {
|
||||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION,
|
schema_version: exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION,
|
||||||
policy_owner: "assistantMcpDiscoveryResponseCandidate",
|
policy_owner: "assistantMcpDiscoveryResponseCandidate",
|
||||||
candidate_status: replyText ? status : status === "clarification_candidate" ? status : status,
|
candidate_status: replyText ? status : status === "clarification_candidate" ? status : status,
|
||||||
hot_runtime_wired: false,
|
hot_runtime_wired: guardedHotHandoff,
|
||||||
reply_type: replyTypeFor(status),
|
reply_type: replyTypeFor(status),
|
||||||
reply_text: replyText,
|
reply_text: replyText,
|
||||||
eligible_for_future_hot_runtime: Boolean(replyText) && (status === "ready_for_guarded_use" || status === "checked_sources_only_candidate" || status === "clarification_candidate"),
|
eligible_for_future_hot_runtime: Boolean(replyText) &&
|
||||||
|
(status === "ready_for_guarded_use" || status === "checked_sources_only_candidate" || status === "clarification_candidate"),
|
||||||
must_keep_internal_mechanics_hidden: true,
|
must_keep_internal_mechanics_hidden: true,
|
||||||
reason_codes: reasonCodes
|
reason_codes: reasonCodes
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,7 @@ function isDetectedIntentAlignedWithTurnMeaning(detectedIntent, turnMeaning) {
|
||||||
if (normalizedIntent === "inventory_purchase_provenance_for_item" ||
|
if (normalizedIntent === "inventory_purchase_provenance_for_item" ||
|
||||||
normalizedIntent === "inventory_purchase_documents_for_item" ||
|
normalizedIntent === "inventory_purchase_documents_for_item" ||
|
||||||
normalizedIntent === "inventory_sale_trace_for_item" ||
|
normalizedIntent === "inventory_sale_trace_for_item" ||
|
||||||
|
normalizedIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
normalizedIntent === "inventory_profitability_for_item" ||
|
normalizedIntent === "inventory_profitability_for_item" ||
|
||||||
normalizedIntent === "inventory_purchase_to_sale_chain") {
|
normalizedIntent === "inventory_purchase_to_sale_chain") {
|
||||||
return askedDomain === "inventory";
|
return askedDomain === "inventory";
|
||||||
|
|
@ -183,7 +184,7 @@ function isExplicitMetadataDiscoveryTurn(entryPoint) {
|
||||||
reasonCodes.some((reason) => toNonEmptyString(reason) === "mcp_discovery_metadata_signal_detected"));
|
reasonCodes.some((reason) => toNonEmptyString(reason) === "mcp_discovery_metadata_signal_detected"));
|
||||||
}
|
}
|
||||||
function isInventoryExactAddressIntent(intent) {
|
function isInventoryExactAddressIntent(intent) {
|
||||||
return /^(?:inventory_purchase_provenance_for_item|inventory_purchase_documents_for_item|inventory_sale_trace_for_item|inventory_profitability_for_item|inventory_purchase_to_sale_chain|inventory_aging_by_purchase_date|inventory_on_hand_as_of_date)$/u.test(String(intent ?? ""));
|
return /^(?:inventory_purchase_provenance_for_item|inventory_purchase_documents_for_item|inventory_sale_trace_for_item|inventory_margin_ranking_for_nomenclature|inventory_profitability_for_item|inventory_purchase_to_sale_chain|inventory_aging_by_purchase_date|inventory_on_hand_as_of_date)$/u.test(String(intent ?? ""));
|
||||||
}
|
}
|
||||||
function hasMetadataDiscoveryPriority(input, entryPoint) {
|
function hasMetadataDiscoveryPriority(input, entryPoint) {
|
||||||
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||||
|
|
@ -203,12 +204,16 @@ function hasMetadataDiscoveryPriority(input, entryPoint) {
|
||||||
}
|
}
|
||||||
function isOpenScopeValueFlowWithoutSubject(entryPoint) {
|
function isOpenScopeValueFlowWithoutSubject(entryPoint) {
|
||||||
const graph = readDiscoveryDataNeedGraph(entryPoint);
|
const graph = readDiscoveryDataNeedGraph(entryPoint);
|
||||||
|
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
|
||||||
const businessFactFamily = toNonEmptyString(graph?.business_fact_family);
|
const businessFactFamily = toNonEmptyString(graph?.business_fact_family);
|
||||||
const subjectCandidates = Array.isArray(graph?.subject_candidates) ? graph.subject_candidates : [];
|
const subjectCandidates = Array.isArray(graph?.subject_candidates) ? graph.subject_candidates : [];
|
||||||
const reasonCodes = Array.isArray(graph?.reason_codes) ? graph.reason_codes : [];
|
const reasonCodes = readStringArray(graph?.reason_codes);
|
||||||
|
const clarificationGaps = readStringArray(graph?.clarification_gaps);
|
||||||
|
const explicitOrganizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope);
|
||||||
return (businessFactFamily === "value_flow" &&
|
return (businessFactFamily === "value_flow" &&
|
||||||
subjectCandidates.length === 0 &&
|
subjectCandidates.length === 0 &&
|
||||||
reasonCodes.some((reason) => toNonEmptyString(reason) === "data_need_graph_open_scope_total_without_subject"));
|
(reasonCodes.includes("data_need_graph_open_scope_total_without_subject") ||
|
||||||
|
(Boolean(explicitOrganizationScope) && clarificationGaps.includes("subject"))));
|
||||||
}
|
}
|
||||||
function needsOpenScopeValueFlowOrganizationClarification(entryPoint) {
|
function needsOpenScopeValueFlowOrganizationClarification(entryPoint) {
|
||||||
const graph = readDiscoveryDataNeedGraph(entryPoint);
|
const graph = readDiscoveryDataNeedGraph(entryPoint);
|
||||||
|
|
@ -345,6 +350,24 @@ function hasExactBankOperationsAddressReply(input, entryPoint) {
|
||||||
routeMode === "exact" ||
|
routeMode === "exact" ||
|
||||||
hasFullConfirmedTruth(input));
|
hasFullConfirmedTruth(input));
|
||||||
}
|
}
|
||||||
|
function hasInventoryMarginRankingAddressReply(input, entryPoint) {
|
||||||
|
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!toNonEmptyString(input.currentReply)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (hasMetadataDiscoveryPriority(input, entryPoint)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||||
|
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
|
||||||
|
const capabilityId = toNonEmptyString(input.addressRuntimeMeta?.capability_id) ??
|
||||||
|
toNonEmptyString(input.addressRuntimeMeta?.capability_contract_id);
|
||||||
|
return Boolean(detectedIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
|
selectedRecipe === "address_inventory_margin_ranking_for_nomenclature_v1" ||
|
||||||
|
capabilityId === "inventory_inventory_margin_ranking_for_nomenclature");
|
||||||
|
}
|
||||||
function hasValueFlowActionConflictWithDiscoveryTurnMeaning(input, entryPoint) {
|
function hasValueFlowActionConflictWithDiscoveryTurnMeaning(input, entryPoint) {
|
||||||
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -439,6 +462,9 @@ function hasRuntimeAdjustedExactReply(input, entryPoint) {
|
||||||
if (hasMetadataDiscoveryPriority(input, entryPoint)) {
|
if (hasMetadataDiscoveryPriority(input, entryPoint)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (hasEvidenceLaneConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
|
if (hasEvidenceLaneConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -462,6 +488,9 @@ function hasRuntimeMatchedExactReply(input, entryPoint) {
|
||||||
if (hasMetadataDiscoveryPriority(input, entryPoint)) {
|
if (hasMetadataDiscoveryPriority(input, entryPoint)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (hasEvidenceLaneConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
|
if (hasEvidenceLaneConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -482,6 +511,9 @@ function hasAlignedFactualAddressReply(input, entryPoint) {
|
||||||
if (hasMetadataDiscoveryPriority(input, entryPoint)) {
|
if (hasMetadataDiscoveryPriority(input, entryPoint)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
|
if (hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -537,6 +569,9 @@ function hasMatchedFactualAddressContinuationTarget(input, entryPoint) {
|
||||||
if (hasMetadataDiscoveryPriority(input, entryPoint)) {
|
if (hasMetadataDiscoveryPriority(input, entryPoint)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||||
const dialogContinuationContract = toRecordObject(input.addressRuntimeMeta?.dialogContinuationContract) ??
|
const dialogContinuationContract = toRecordObject(input.addressRuntimeMeta?.dialogContinuationContract) ??
|
||||||
toRecordObject(input.addressRuntimeMeta?.dialog_continuation_contract_v2);
|
toRecordObject(input.addressRuntimeMeta?.dialog_continuation_contract_v2);
|
||||||
|
|
@ -577,6 +612,9 @@ function hasFullConfirmedFactualAddressReply(input, entryPoint) {
|
||||||
if (hasMetadataDiscoveryPriority(input, entryPoint)) {
|
if (hasMetadataDiscoveryPriority(input, entryPoint)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return hasFullConfirmedTruth(input);
|
return hasFullConfirmedTruth(input);
|
||||||
}
|
}
|
||||||
function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
||||||
|
|
@ -601,6 +639,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
||||||
const staleMetadataDiscoveryFallbackAgainstExactAddressReply = hasStaleMetadataDiscoveryFallbackAgainstExactAddressReply(input, entryPoint);
|
const staleMetadataDiscoveryFallbackAgainstExactAddressReply = hasStaleMetadataDiscoveryFallbackAgainstExactAddressReply(input, entryPoint);
|
||||||
const exactValueFlowReplyForBusinessOverviewDirectMoneyNeed = hasExactValueFlowReplyForBusinessOverviewDirectMoneyNeed(input, entryPoint);
|
const exactValueFlowReplyForBusinessOverviewDirectMoneyNeed = hasExactValueFlowReplyForBusinessOverviewDirectMoneyNeed(input, entryPoint);
|
||||||
const exactBankOperationsAddressReply = hasExactBankOperationsAddressReply(input, entryPoint);
|
const exactBankOperationsAddressReply = hasExactBankOperationsAddressReply(input, entryPoint);
|
||||||
|
const inventoryMarginRankingAddressReply = hasInventoryMarginRankingAddressReply(input, entryPoint);
|
||||||
const openScopeValueFlowDiscoveryPriority = hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint);
|
const openScopeValueFlowDiscoveryPriority = hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint);
|
||||||
const metadataDiscoveryPriority = hasMetadataDiscoveryPriority(input, entryPoint);
|
const metadataDiscoveryPriority = hasMetadataDiscoveryPriority(input, entryPoint);
|
||||||
const valueFlowActionConflictWithDiscoveryTurnMeaning = hasValueFlowActionConflictWithDiscoveryTurnMeaning(input, entryPoint);
|
const valueFlowActionConflictWithDiscoveryTurnMeaning = hasValueFlowActionConflictWithDiscoveryTurnMeaning(input, entryPoint);
|
||||||
|
|
@ -665,6 +704,9 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
||||||
if (exactBankOperationsAddressReply) {
|
if (exactBankOperationsAddressReply) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_exact_bank_operations_address_reply");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_exact_bank_operations_address_reply");
|
||||||
}
|
}
|
||||||
|
if (inventoryMarginRankingAddressReply) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_inventory_margin_ranking_address_reply");
|
||||||
|
}
|
||||||
if (deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") {
|
if (deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") {
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_broad_business_summary_over_clarification_candidate");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_broad_business_summary_over_clarification_candidate");
|
||||||
}
|
}
|
||||||
|
|
@ -674,6 +716,9 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
||||||
if (!candidate.eligible_for_future_hot_runtime) {
|
if (!candidate.eligible_for_future_hot_runtime) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_not_eligible");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_not_eligible");
|
||||||
}
|
}
|
||||||
|
if (candidate.hot_runtime_wired) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_guarded_hot_wired");
|
||||||
|
}
|
||||||
if (!toNonEmptyString(candidate.reply_text)) {
|
if (!toNonEmptyString(candidate.reply_text)) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_missing_reply_text");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_missing_reply_text");
|
||||||
}
|
}
|
||||||
|
|
@ -692,6 +737,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
||||||
!staleMetadataDiscoveryFallbackAgainstExactAddressReply &&
|
!staleMetadataDiscoveryFallbackAgainstExactAddressReply &&
|
||||||
!exactValueFlowReplyForBusinessOverviewDirectMoneyNeed &&
|
!exactValueFlowReplyForBusinessOverviewDirectMoneyNeed &&
|
||||||
!exactBankOperationsAddressReply &&
|
!exactBankOperationsAddressReply &&
|
||||||
|
!inventoryMarginRankingAddressReply &&
|
||||||
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
|
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
|
||||||
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
|
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
|
||||||
candidate.eligible_for_future_hot_runtime &&
|
candidate.eligible_for_future_hot_runtime &&
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ exports.ASSISTANT_MCP_ROUTE_CANDIDATE_SCHEMA_VERSION = exports.ASSISTANT_MCP_DIS
|
||||||
exports.runAssistantMcpDiscoveryRuntimeBridge = runAssistantMcpDiscoveryRuntimeBridge;
|
exports.runAssistantMcpDiscoveryRuntimeBridge = runAssistantMcpDiscoveryRuntimeBridge;
|
||||||
const assistantMcpDiscoveryAnswerAdapter_1 = require("./assistantMcpDiscoveryAnswerAdapter");
|
const assistantMcpDiscoveryAnswerAdapter_1 = require("./assistantMcpDiscoveryAnswerAdapter");
|
||||||
const assistantMcpDiscoveryPilotExecutor_1 = require("./assistantMcpDiscoveryPilotExecutor");
|
const assistantMcpDiscoveryPilotExecutor_1 = require("./assistantMcpDiscoveryPilotExecutor");
|
||||||
|
const assistantMcpDiscoveryExecutionHandoff_1 = require("./assistantMcpDiscoveryExecutionHandoff");
|
||||||
const assistantMcpDiscoveryPlanner_1 = require("./assistantMcpDiscoveryPlanner");
|
const assistantMcpDiscoveryPlanner_1 = require("./assistantMcpDiscoveryPlanner");
|
||||||
exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION = "assistant_mcp_discovery_runtime_bridge_v1";
|
exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION = "assistant_mcp_discovery_runtime_bridge_v1";
|
||||||
exports.ASSISTANT_MCP_DISCOVERY_LOOP_STATE_SCHEMA_VERSION = "assistant_mcp_discovery_loop_state_v1";
|
exports.ASSISTANT_MCP_DISCOVERY_LOOP_STATE_SCHEMA_VERSION = "assistant_mcp_discovery_loop_state_v1";
|
||||||
|
|
@ -185,8 +186,13 @@ function routeCandidateNextAction(status) {
|
||||||
}
|
}
|
||||||
function buildRouteCandidate(planner, pilot, bridgeStatus) {
|
function buildRouteCandidate(planner, pilot, bridgeStatus) {
|
||||||
const plannerClarificationGaps = planner.discovery_plan.clarification_gaps ?? [];
|
const plannerClarificationGaps = planner.discovery_plan.clarification_gaps ?? [];
|
||||||
const providedAxes = flattenAxes(pilot, "provided_axes");
|
const evidenceAxes = planner.evidence_plan.evidence_axes;
|
||||||
const missingAxes = plannerClarificationGaps.length > 0 ? plannerClarificationGaps : flattenAxes(pilot, "missing_axis_options");
|
const providedAxes = uniqueStrings([...evidenceAxes.provided_axes, ...flattenAxes(pilot, "provided_axes")]);
|
||||||
|
const missingAxes = plannerClarificationGaps.length > 0
|
||||||
|
? plannerClarificationGaps
|
||||||
|
: evidenceAxes.user_actionable_missing_axes.length > 0
|
||||||
|
? evidenceAxes.user_actionable_missing_axes
|
||||||
|
: flattenAxes(pilot, "missing_axis_options");
|
||||||
const missingProofFamily = routeCandidateMissingProofFamily(planner, pilot);
|
const missingProofFamily = routeCandidateMissingProofFamily(planner, pilot);
|
||||||
const candidateStatus = routeCandidateStatusFor(bridgeStatus, pilot, missingProofFamily);
|
const candidateStatus = routeCandidateStatusFor(bridgeStatus, pilot, missingProofFamily);
|
||||||
return {
|
return {
|
||||||
|
|
@ -197,9 +203,12 @@ function buildRouteCandidate(planner, pilot, bridgeStatus) {
|
||||||
selected_chain_summary: planner.selected_chain_summary,
|
selected_chain_summary: planner.selected_chain_summary,
|
||||||
nearest_catalog_chain_template: planner.catalog_chain_template_alignment.top_chain_template_match,
|
nearest_catalog_chain_template: planner.catalog_chain_template_alignment.top_chain_template_match,
|
||||||
catalog_alignment_status: planner.catalog_chain_template_alignment.alignment_status,
|
catalog_alignment_status: planner.catalog_chain_template_alignment.alignment_status,
|
||||||
business_fact_family: planner.data_need_graph?.business_fact_family ?? null,
|
business_fact_family: planner.evidence_plan.data_need.business_fact_family,
|
||||||
action_family: planner.data_need_graph?.action_family ?? null,
|
action_family: planner.evidence_plan.data_need.action_family,
|
||||||
proof_expectation: planner.data_need_graph?.proof_expectation ?? null,
|
proof_expectation: planner.evidence_plan.data_need.proof_expectation,
|
||||||
|
evidence_plan_status: planner.evidence_plan.planner_status,
|
||||||
|
evidence_answer_mode: planner.evidence_plan.answer_contract.answer_mode,
|
||||||
|
evidence_expected_coverage: planner.evidence_plan.coverage_gate.expected_coverage,
|
||||||
required_axes: [...planner.required_axes],
|
required_axes: [...planner.required_axes],
|
||||||
provided_axes: providedAxes,
|
provided_axes: providedAxes,
|
||||||
missing_axes: missingAxes,
|
missing_axes: missingAxes,
|
||||||
|
|
@ -207,7 +216,7 @@ function buildRouteCandidate(planner, pilot, bridgeStatus) {
|
||||||
enablement_reason: routeCandidateEnablementReason(candidateStatus, pilot, missingAxes, missingProofFamily),
|
enablement_reason: routeCandidateEnablementReason(candidateStatus, pilot, missingAxes, missingProofFamily),
|
||||||
recommended_next_action: routeCandidateNextAction(candidateStatus),
|
recommended_next_action: routeCandidateNextAction(candidateStatus),
|
||||||
forbidden_overclaim_flags: uniqueStrings([
|
forbidden_overclaim_flags: uniqueStrings([
|
||||||
...(planner.data_need_graph?.forbidden_overclaim_flags ?? []),
|
...planner.evidence_plan.answer_contract.forbidden_overclaim_flags,
|
||||||
...(missingProofFamily ? [missingProofFamily.must_not_claim] : [])
|
...(missingProofFamily ? [missingProofFamily.must_not_claim] : [])
|
||||||
])
|
])
|
||||||
};
|
};
|
||||||
|
|
@ -250,24 +259,39 @@ async function runAssistantMcpDiscoveryRuntimeBridge(input) {
|
||||||
const bridgeStatus = bridgeStatusFor(pilot, answerDraft);
|
const bridgeStatus = bridgeStatusFor(pilot, answerDraft);
|
||||||
const loopState = buildLoopState(planner, pilot, bridgeStatus);
|
const loopState = buildLoopState(planner, pilot, bridgeStatus);
|
||||||
const routeCandidate = buildRouteCandidate(planner, pilot, bridgeStatus);
|
const routeCandidate = buildRouteCandidate(planner, pilot, bridgeStatus);
|
||||||
|
const userFacingResponseAllowed = bridgeStatus !== "blocked";
|
||||||
|
const businessFactAllowed = businessFactAnswerAllowed(answerDraft);
|
||||||
|
const executionHandoff = (0, assistantMcpDiscoveryExecutionHandoff_1.buildAssistantMcpDiscoveryExecutionHandoff)({
|
||||||
|
bridgeStatus,
|
||||||
|
routeCandidate,
|
||||||
|
pilot,
|
||||||
|
answerDraft,
|
||||||
|
businessFactAnswerAllowed: businessFactAllowed,
|
||||||
|
userFacingResponseAllowed
|
||||||
|
});
|
||||||
const reasonCodes = uniqueStrings([...planner.reason_codes, ...pilot.reason_codes, ...answerDraft.reason_codes]);
|
const reasonCodes = uniqueStrings([...planner.reason_codes, ...pilot.reason_codes, ...answerDraft.reason_codes]);
|
||||||
pushReason(reasonCodes, `runtime_bridge_status_${bridgeStatus}`);
|
pushReason(reasonCodes, `runtime_bridge_status_${bridgeStatus}`);
|
||||||
pushReason(reasonCodes, "runtime_bridge_not_wired_to_hot_assistant_answer");
|
pushReason(reasonCodes, "runtime_bridge_not_wired_to_hot_assistant_answer");
|
||||||
pushReason(reasonCodes, `runtime_bridge_loop_state_${loopState.loop_status}`);
|
pushReason(reasonCodes, `runtime_bridge_loop_state_${loopState.loop_status}`);
|
||||||
pushReason(reasonCodes, "runtime_bridge_route_candidate_built");
|
pushReason(reasonCodes, "runtime_bridge_route_candidate_built");
|
||||||
pushReason(reasonCodes, `runtime_bridge_route_candidate_${routeCandidate.candidate_status}`);
|
pushReason(reasonCodes, `runtime_bridge_route_candidate_${routeCandidate.candidate_status}`);
|
||||||
|
pushReason(reasonCodes, `runtime_bridge_execution_handoff_${executionHandoff.handoff_status}`);
|
||||||
|
for (const reasonCode of executionHandoff.reason_codes) {
|
||||||
|
pushReason(reasonCodes, reasonCode);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION,
|
schema_version: exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION,
|
||||||
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
|
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
|
||||||
bridge_status: bridgeStatus,
|
bridge_status: bridgeStatus,
|
||||||
hot_runtime_wired: false,
|
hot_runtime_wired: executionHandoff.can_use_guarded_response,
|
||||||
planner,
|
planner,
|
||||||
pilot,
|
pilot,
|
||||||
answer_draft: answerDraft,
|
answer_draft: answerDraft,
|
||||||
loop_state: loopState,
|
loop_state: loopState,
|
||||||
route_candidate: routeCandidate,
|
route_candidate: routeCandidate,
|
||||||
user_facing_response_allowed: bridgeStatus !== "blocked",
|
execution_handoff: executionHandoff,
|
||||||
business_fact_answer_allowed: businessFactAnswerAllowed(answerDraft),
|
user_facing_response_allowed: userFacingResponseAllowed,
|
||||||
|
business_fact_answer_allowed: businessFactAllowed,
|
||||||
requires_user_clarification: bridgeStatus === "needs_clarification",
|
requires_user_clarification: bridgeStatus === "needs_clarification",
|
||||||
reason_codes: reasonCodes
|
reason_codes: reasonCodes
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -69,12 +69,14 @@ async function runAssistantMcpDiscoveryRuntimeEntryPoint(input) {
|
||||||
});
|
});
|
||||||
const reasonCodes = uniqueStrings([...turnInput.reason_codes, ...bridge.reason_codes]);
|
const reasonCodes = uniqueStrings([...turnInput.reason_codes, ...bridge.reason_codes]);
|
||||||
pushReason(reasonCodes, "runtime_entry_point_bridge_executed");
|
pushReason(reasonCodes, "runtime_entry_point_bridge_executed");
|
||||||
pushReason(reasonCodes, "runtime_entry_point_not_wired_to_hot_assistant_answer");
|
pushReason(reasonCodes, bridge.hot_runtime_wired
|
||||||
|
? "runtime_entry_point_wired_to_guarded_hot_assistant_answer"
|
||||||
|
: "runtime_entry_point_not_wired_to_hot_assistant_answer");
|
||||||
return {
|
return {
|
||||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_ENTRY_POINT_SCHEMA_VERSION,
|
schema_version: exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_ENTRY_POINT_SCHEMA_VERSION,
|
||||||
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
|
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
|
||||||
entry_status: "bridge_executed",
|
entry_status: "bridge_executed",
|
||||||
hot_runtime_wired: false,
|
hot_runtime_wired: bridge.hot_runtime_wired,
|
||||||
discovery_attempted: true,
|
discovery_attempted: true,
|
||||||
turn_input: turnInput,
|
turn_input: turnInput,
|
||||||
bridge,
|
bridge,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION = void 0;
|
exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION = void 0;
|
||||||
exports.buildAssistantMcpDiscoveryTurnInput = buildAssistantMcpDiscoveryTurnInput;
|
exports.buildAssistantMcpDiscoveryTurnInput = buildAssistantMcpDiscoveryTurnInput;
|
||||||
const assistantMcpDiscoveryDataNeedGraph_1 = require("./assistantMcpDiscoveryDataNeedGraph");
|
const assistantMcpDiscoveryDataNeedGraph_1 = require("./assistantMcpDiscoveryDataNeedGraph");
|
||||||
|
const assistantOrganizationMatcher_1 = require("./assistantOrganizationMatcher");
|
||||||
const addressTextRepair_1 = require("./addressTextRepair");
|
const addressTextRepair_1 = require("./addressTextRepair");
|
||||||
exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION = "assistant_mcp_discovery_turn_input_v1";
|
exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION = "assistant_mcp_discovery_turn_input_v1";
|
||||||
function toRecordObject(value) {
|
function toRecordObject(value) {
|
||||||
|
|
@ -273,6 +274,17 @@ function collectEntityCandidates(value) {
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
function collectReasonCodes(value) {
|
||||||
|
const result = [];
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
const reason = toNonEmptyString(value);
|
||||||
|
return reason ? [reason] : result;
|
||||||
|
}
|
||||||
|
for (const item of value) {
|
||||||
|
pushUnique(result, item);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
function collectPredecomposeEntities(predecompose) {
|
function collectPredecomposeEntities(predecompose) {
|
||||||
const entities = toRecordObject(predecompose?.entities);
|
const entities = toRecordObject(predecompose?.entities);
|
||||||
const organization = toNonEmptyString(entities?.organization);
|
const organization = toNonEmptyString(entities?.organization);
|
||||||
|
|
@ -734,11 +746,13 @@ function hasBusinessOverviewContinuationSignal(text) {
|
||||||
const hasFinalSummaryCue = /(?:\u0447\u0442\u043e\s+\u043c\u044b\s+\u0437\u043d\u0430\u0435\u043c|\u0447\u0442\u043e\s+\u043f\u043e\u043d\u044f\u0442\u043d\u043e|\u0447\u0442\u043e\s+\u043f\u0440\u043e\u0432\u0435\u0440\w*\s+\u0434\u0430\u043b\u044c\u0448\u0435|\u0441\u043b\u0435\u0434\u0443\u044e\u0449\w*\s+\u0448\u0430\u0433|\u0438\u0442\u043e\u0433\w*\s+\u0432\u044b\u0432\u043e\u0434|\u043a\u0430\u043a\u043e\u0439\s+\u0432\u044b\u0432\u043e\u0434|\u0447\u0442\u043e\s+\u0441\s+\u044d\u0442\u0438\u043c\s+\u0434\u0435\u043b\u0430\u0442\u044c|what\s+do\s+we\s+know|what\s+is\s+missing|next\s+step|final\s+summary)/iu.test(normalized);
|
const hasFinalSummaryCue = /(?:\u0447\u0442\u043e\s+\u043c\u044b\s+\u0437\u043d\u0430\u0435\u043c|\u0447\u0442\u043e\s+\u043f\u043e\u043d\u044f\u0442\u043d\u043e|\u0447\u0442\u043e\s+\u043f\u0440\u043e\u0432\u0435\u0440\w*\s+\u0434\u0430\u043b\u044c\u0448\u0435|\u0441\u043b\u0435\u0434\u0443\u044e\u0449\w*\s+\u0448\u0430\u0433|\u0438\u0442\u043e\u0433\w*\s+\u0432\u044b\u0432\u043e\u0434|\u043a\u0430\u043a\u043e\u0439\s+\u0432\u044b\u0432\u043e\u0434|\u0447\u0442\u043e\s+\u0441\s+\u044d\u0442\u0438\u043c\s+\u0434\u0435\u043b\u0430\u0442\u044c|what\s+do\s+we\s+know|what\s+is\s+missing|next\s+step|final\s+summary)/iu.test(normalized);
|
||||||
const hasMoneyBreakdownCue = /(?:\u0440\u0430\u0441\u043a\u0440\u043e\p{L}*\s+\u0434\u0435\u043d\p{L}*|\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+\u0432\u0441\u0435\u0433\u043e\s+\u043f\u043e\u043b\u0443\u0447|\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:\u0432\u0441\u0435\u0433\u043e\s+)?\u0437\u0430\u043f\u043b\u0430\u0442|\u0447\u0438\u0441\u0442\p{L}*\s+\u0434\u0435\u043d\u0435\u0436\u043d\p{L}*\s+\u043f\u043e\u0442\u043e\u043a|\u0433\u043b\u0430\u0432\u043d\p{L}*\s+(?:\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a)|top\s+(?:customer|supplier)|cash\s+breakdown)/iu.test(normalized) &&
|
const hasMoneyBreakdownCue = /(?:\u0440\u0430\u0441\u043a\u0440\u043e\p{L}*\s+\u0434\u0435\u043d\p{L}*|\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+\u0432\u0441\u0435\u0433\u043e\s+\u043f\u043e\u043b\u0443\u0447|\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:\u0432\u0441\u0435\u0433\u043e\s+)?\u0437\u0430\u043f\u043b\u0430\u0442|\u0447\u0438\u0441\u0442\p{L}*\s+\u0434\u0435\u043d\u0435\u0436\u043d\p{L}*\s+\u043f\u043e\u0442\u043e\u043a|\u0433\u043b\u0430\u0432\u043d\p{L}*\s+(?:\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a)|top\s+(?:customer|supplier)|cash\s+breakdown)/iu.test(normalized) &&
|
||||||
/(?:\u043f\u043e\u043b\u0443\u0447|\u0437\u0430\u043f\u043b\u0430\u0442|\u043d\u0435\u0442\u0442\u043e|\u0434\u0435\u043d\p{L}*|\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|received|paid|net|cash|customer|supplier)/iu.test(normalized);
|
/(?:\u043f\u043e\u043b\u0443\u0447|\u0437\u0430\u043f\u043b\u0430\u0442|\u043d\u0435\u0442\u0442\u043e|\u0434\u0435\u043d\p{L}*|\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|received|paid|net|cash|customer|supplier)/iu.test(normalized);
|
||||||
|
const hasCashflowPolarityCue = /(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)[\s\S]{0,80}(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\p{L}*|\u0443\u0448\p{L}*|\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*)|(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e)[\s\S]{0,80}(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)/iu.test(normalized);
|
||||||
return (hasEvidenceContinuationCue ||
|
return (hasEvidenceContinuationCue ||
|
||||||
hasAnalystContinuationCue ||
|
hasAnalystContinuationCue ||
|
||||||
hasTaxContinuationCue ||
|
hasTaxContinuationCue ||
|
||||||
hasFinalSummaryCue ||
|
hasFinalSummaryCue ||
|
||||||
hasMoneyBreakdownCue);
|
hasMoneyBreakdownCue ||
|
||||||
|
hasCashflowPolarityCue);
|
||||||
}
|
}
|
||||||
function hasExplicitVatQuestionSignal(text) {
|
function hasExplicitVatQuestionSignal(text) {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
|
|
@ -785,6 +799,9 @@ function hasBusinessOverviewFollowupSeed(followupSeed) {
|
||||||
followupSeed.loopSelectedChainId === "business_overview");
|
followupSeed.loopSelectedChainId === "business_overview");
|
||||||
}
|
}
|
||||||
function hasValueFlowSignal(text) {
|
function hasValueFlowSignal(text) {
|
||||||
|
if (/(?:\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\u043a|\u043e\u043f\u043b\u0430\u0442|\u043f\u043b\u0430\u0442[\u0435\u0451]\u0436|\u0437\u0430\u043f\u043b\u0430\u0442|\u043f\u0435\u0440\u0435\u0447\u0438\u0441\u043b|\u0441\u043f\u0438\u0441\u0430\u043d|\u0440\u0430\u0441\u0445\u043e\u0434|\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u0432\u0445\u043e\u0434\u044f\u0449|\u043f\u043e\u043b\u0443\u0447(?:\u0438\u043b|\u0435\u043d\u043e|\u0435\u043d)|\u043f\u043e\u0441\u0442\u0443\u043f\u0438\u043b|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d|\u0434\u0435\u043d\p{L}*|\u0437\u0430\u0440\u0430\u0431\u043e\u0442|supplier|value[-\s]?flow|turnover|revenue|payment|payout|outflow|cash\s+flow|\bearn(?:ed|ing|ings)?\b)/iu.test(text)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return /(?:оборот|выручк|оплат|плат[её]ж|заплат|перечисл|списан|расход|исходящ|входящ|получ(?:ил|ено|ен)|поступил|поступлен|денежн[а-яёa-z0-9_-]*\s+поток|(?<!\p{L})заработ(?:ал|али|ало|аем|ает|ать|ано|ок)(?!\p{L})|supplier|value[-\s]?flow|turnover|revenue|payment|payout|outflow|cash\s+flow|\bearn(?:ed|ing|ings)?\b)/iu.test(text);
|
return /(?:оборот|выручк|оплат|плат[её]ж|заплат|перечисл|списан|расход|исходящ|входящ|получ(?:ил|ено|ен)|поступил|поступлен|денежн[а-яёa-z0-9_-]*\s+поток|(?<!\p{L})заработ(?:ал|али|ало|аем|ает|ать|ано|ок)(?!\p{L})|supplier|value[-\s]?flow|turnover|revenue|payment|payout|outflow|cash\s+flow|\bearn(?:ed|ing|ings)?\b)/iu.test(text);
|
||||||
}
|
}
|
||||||
function hasValueFlowAggregateQuestionSignal(text) {
|
function hasValueFlowAggregateQuestionSignal(text) {
|
||||||
|
|
@ -794,6 +811,9 @@ function hasPayoutSignal(text) {
|
||||||
return /(?:\bмы\s+(?:за)?плат|(?:за)?платил|оплатил|перечисл|списан|расход|поставщик|исходящ|supplier|payout|outflow|paid\s+to|payment\s+to)/iu.test(text);
|
return /(?:\bмы\s+(?:за)?плат|(?:за)?платил|оплатил|перечисл|списан|расход|поставщик|исходящ|supplier|payout|outflow|paid\s+to|payment\s+to)/iu.test(text);
|
||||||
}
|
}
|
||||||
function hasBidirectionalValueFlowSignal(text) {
|
function hasBidirectionalValueFlowSignal(text) {
|
||||||
|
if (/(?:\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|\u0431\u0430\u043b\u0430\u043d\u0441\s+(?:\u043f\u043b\u0430\u0442|\u0434\u0435\u043d\u0435\u0433|\u0434\u0435\u043d\u0435\u0436)|\u0432\u0437\u0430\u0438\u043c\u043e\u0440\u0430\u0441\u0447[\u0435\u0451]\u0442|\u043f\u043e\u043b\u0443\u0447\p{L}*.*(?:\u0437\u0430)?\u043f\u043b\u0430\u0442\p{L}*|(?:\u0437\u0430)?\u043f\u043b\u0430\u0442\p{L}*.*\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0432\u0445\u043e\u0434\u044f\u0449.*\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u0438\u0441\u0445\u043e\u0434\u044f\u0449.*\u0432\u0445\u043e\u0434\u044f\u0449|\u043f\u0440\u0438\u0448\p{L}*.*\u0443\u0448\p{L}*|\u0443\u0448\p{L}*.*\u043f\u0440\u0438\u0448\p{L}*|\u043f\u043b\u044e\u0441.*\u043c\u0438\u043d\u0443\u0441|\u043c\u0438\u043d\u0443\u0441.*\u043f\u043b\u044e\u0441|net\s+(?:flow|cash|payment)|cash\s+net|incoming\s+and\s+outgoing|received\s+and\s+paid|paid\s+and\s+received)/iu.test(text)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return /(?:нетто|сальдо|баланс\s+(?:плат|денег|денеж)|взаиморасч[её]т|получил[иа]?.*(?:за)?платил|(?:за)?платил[иа]?.*получил|входящ.*исходящ|исходящ.*входящ|дебет.*кредит|кредит.*дебет|net\s+(?:flow|cash|payment)|cash\s+net|incoming\s+and\s+outgoing|received\s+and\s+paid|paid\s+and\s+received)/iu.test(text);
|
return /(?:нетто|сальдо|баланс\s+(?:плат|денег|денеж)|взаиморасч[её]т|получил[иа]?.*(?:за)?платил|(?:за)?платил[иа]?.*получил|входящ.*исходящ|исходящ.*входящ|дебет.*кредит|кредит.*дебет|net\s+(?:flow|cash|payment)|cash\s+net|incoming\s+and\s+outgoing|received\s+and\s+paid|paid\s+and\s+received)/iu.test(text);
|
||||||
}
|
}
|
||||||
function hasValueRankingSignal(text) {
|
function hasValueRankingSignal(text) {
|
||||||
|
|
@ -820,6 +840,39 @@ function extractOrganizationScopeFromRawText(value) {
|
||||||
.replace(/\s+(?:\u043f\u043e\s+\u0434\u0430\u043d\u043d\u044b\u043c\s+1\u0441|\u0432\s+\u0446\u0435\u043b\u043e\u043c|\u0437\u0430\s+(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{2}|(?:19|20)\d{2})(?:\s+\u0433(?:\u043e\u0434|\.)?)?|\u043d\u0430\s+(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{2}|(?:19|20)\d{2})|\u0437\u0430\s+\u0432\u0441[\u0435\u0451]\s+(?:\u0434\u043e\u0441\u0442\u0443\u043f\p{L}+\s+)?\u0432\u0440\u0435\u043c\u044f|\u0437\u0430\s+\u0432\u0435\u0441\u044c\s+(?:\u0434\u043e\u0441\u0442\u0443\u043f\p{L}+\s+)?\u043f\u0435\u0440\u0438\u043e\u0434|\u0437\u0430\s+\u0432\u0441\u044e\s+\u0438\u0441\u0442\u043e\u0440\u0438(?:\u044e|\u0438)|\u0434\u0430\u0439\s+\u0431\u0438\u0437\u043d\u0435\u0441-?\u043e\u0431\u0437\u043e\u0440|\u0431\u0438\u0437\u043d\u0435\u0441-?\u043e\u0431\u0437\u043e\u0440|\u043d\u043e\s+\u043d\u0435).*$/iu, "")
|
.replace(/\s+(?:\u043f\u043e\s+\u0434\u0430\u043d\u043d\u044b\u043c\s+1\u0441|\u0432\s+\u0446\u0435\u043b\u043e\u043c|\u0437\u0430\s+(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{2}|(?:19|20)\d{2})(?:\s+\u0433(?:\u043e\u0434|\.)?)?|\u043d\u0430\s+(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{2}|(?:19|20)\d{2})|\u0437\u0430\s+\u0432\u0441[\u0435\u0451]\s+(?:\u0434\u043e\u0441\u0442\u0443\u043f\p{L}+\s+)?\u0432\u0440\u0435\u043c\u044f|\u0437\u0430\s+\u0432\u0435\u0441\u044c\s+(?:\u0434\u043e\u0441\u0442\u0443\u043f\p{L}+\s+)?\u043f\u0435\u0440\u0438\u043e\u0434|\u0437\u0430\s+\u0432\u0441\u044e\s+\u0438\u0441\u0442\u043e\u0440\u0438(?:\u044e|\u0438)|\u0434\u0430\u0439\s+\u0431\u0438\u0437\u043d\u0435\u0441-?\u043e\u0431\u0437\u043e\u0440|\u0431\u0438\u0437\u043d\u0435\u0441-?\u043e\u0431\u0437\u043e\u0440|\u043d\u043e\s+\u043d\u0435).*$/iu, "")
|
||||||
.trim());
|
.trim());
|
||||||
}
|
}
|
||||||
|
function extractOrganizationScopeFromSemanticText(value) {
|
||||||
|
const text = toNonEmptyString(value);
|
||||||
|
if (!text) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const match = text.match(/(?:^|[\s,;:])(?:\u043a\u043e\u043c\u043f\u0430\u043d(?:\u0438\u0438|\u0438\u044f)|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446(?:\u0438\u0438|\u0438\u044f))\s+(.+?)(?=$|[\n,.;:!?]|\s+\u0437\u0430\s+(?:\d{4}|\d{2}\s*\u0433|\d{2}\s+\u0433\u043e\u0434)|\s+\u043d\u0430\s+(?:\d{4}|\d{2}\s*\u0433|\d{2}\s+\u0433\u043e\u0434))/iu);
|
||||||
|
return toNonEmptyString(match?.[1]);
|
||||||
|
}
|
||||||
|
function normalizeLooseOrganizationAlias(value) {
|
||||||
|
const text = toNonEmptyString(value);
|
||||||
|
if (!text) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const normalized = text
|
||||||
|
.replace(/^[«"'“”]+|[»"'“”]+$/gu, "")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
if (!normalized) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const comparable = (0, addressTextRepair_1.normalizeRussianComparableText)(normalized);
|
||||||
|
const normalizedTokens = normalized.split(/\s+/u).filter(Boolean);
|
||||||
|
const hasYearOnlyTimeTail = /\b(?:19|20)\d{2}\b/u.test(normalized) &&
|
||||||
|
normalizedTokens.length <= 4 &&
|
||||||
|
!/(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e|llc|inc|corp)/iu.test(comparable);
|
||||||
|
if (hasYearOnlyTimeTail) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (/^(?:\u0438|\u0432|\u0432\u043e|\u0437\u0430|\u043d\u0430|\u043f\u043e|\u043a\u0442\u043e|\u0447\u0442\u043e|\u043a\u0430\u043a(?:\u043e\u0439|\u0430\u044f|\u0438\u0435)?|\u0433\u043b\u0430\u0432\u043d\p{L}*)\b/iu.test(comparable)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
function hasMonthlyAggregationSignal(text) {
|
function hasMonthlyAggregationSignal(text) {
|
||||||
return /(?:\u043f\u043e\s+\u043c\u0435\u0441\u044f\u0446\u0430\u043c|\u043f\u043e\u043c\u0435\u0441\u044f\u0447\u043d\u043e|\u0435\u0436\u0435\u043c\u0435\u0441\u044f\u0447\u043d\u043e|month\s+by\s+month|by\s+month|monthly)/iu.test(text);
|
return /(?:\u043f\u043e\s+\u043c\u0435\u0441\u044f\u0446\u0430\u043c|\u043f\u043e\u043c\u0435\u0441\u044f\u0447\u043d\u043e|\u0435\u0436\u0435\u043c\u0435\u0441\u044f\u0447\u043d\u043e|month\s+by\s+month|by\s+month|monthly)/iu.test(text);
|
||||||
}
|
}
|
||||||
|
|
@ -1030,7 +1083,10 @@ function metadataScopeHintFromRawText(text) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
function hasExplicitDateScopeLiteral(text) {
|
function hasExplicitDateScopeLiteral(text) {
|
||||||
return /(?:\b(?:19|20)\d{2}\b|\b\d{4}-\d{2}-\d{2}\b|\b\d{4}-\d{2}\b)/iu.test(text);
|
if (/\b\d{2}\s*(?:\u0433(?:\u043e\u0434(?:\u0430|\u0443)?)?\.?)\b/iu.test(text)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return /(?:\b(?:19|20)\d{2}\b|\b\d{4}-\d{2}-\d{2}\b|\b\d{4}-\d{2}\b|\b\d{2}\s*(?:г(?:од(?:а|у)?)?\.?))\b/iu.test(text);
|
||||||
}
|
}
|
||||||
function stripNegatedTaxDateScopeClauses(text) {
|
function stripNegatedTaxDateScopeClauses(text) {
|
||||||
const dateScopeLiteral = String.raw `\b(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{2}|(?:19|20)\d{2})\b`;
|
const dateScopeLiteral = String.raw `\b(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{2}|(?:19|20)\d{2})\b`;
|
||||||
|
|
@ -1054,6 +1110,20 @@ function collectDateScopeFromRawText(text) {
|
||||||
if (year?.[1]) {
|
if (year?.[1]) {
|
||||||
return year[1];
|
return year[1];
|
||||||
}
|
}
|
||||||
|
const utf8ShortYear = text.match(/\b(\d{2})\s*(?:\u0433(?:\u043e\u0434(?:\u0430|\u0443)?)?\.?)\b/iu);
|
||||||
|
if (utf8ShortYear?.[1]) {
|
||||||
|
const numericYear = Number.parseInt(utf8ShortYear[1], 10);
|
||||||
|
if (Number.isFinite(numericYear)) {
|
||||||
|
return String(numericYear <= 30 ? 2000 + numericYear : 1900 + numericYear);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const shortYear = text.match(/\b(\d{2})\s*(?:г(?:од(?:а|у)?)?\.?)\b/iu);
|
||||||
|
if (shortYear?.[1]) {
|
||||||
|
const numericYear = Number.parseInt(shortYear[1], 10);
|
||||||
|
if (Number.isFinite(numericYear)) {
|
||||||
|
return String(numericYear <= 30 ? 2000 + numericYear : 1900 + numericYear);
|
||||||
|
}
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
function currentIsoDate() {
|
function currentIsoDate() {
|
||||||
|
|
@ -1137,15 +1207,41 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
const reasonCodes = [];
|
const reasonCodes = [];
|
||||||
const rawUserText = toNonEmptyString(input.userMessage);
|
const rawUserText = toNonEmptyString(input.userMessage);
|
||||||
const rawEffectiveText = toNonEmptyString(input.effectiveMessage);
|
const rawEffectiveText = toNonEmptyString(input.effectiveMessage);
|
||||||
|
const knownOrganizations = Array.isArray(input.knownOrganizations) ? input.knownOrganizations : [];
|
||||||
const repairedUserText = rawUserText ? (0, addressTextRepair_1.repairAddressMojibakeText)(rawUserText) : null;
|
const repairedUserText = rawUserText ? (0, addressTextRepair_1.repairAddressMojibakeText)(rawUserText) : null;
|
||||||
const repairedEffectiveText = rawEffectiveText ? (0, addressTextRepair_1.repairAddressMojibakeText)(rawEffectiveText) : null;
|
const repairedEffectiveText = rawEffectiveText ? (0, addressTextRepair_1.repairAddressMojibakeText)(rawEffectiveText) : null;
|
||||||
|
const rawUserSignalSourceText = repairedUserText ?? rawUserText ?? "";
|
||||||
const rawSignalSourceText = `${repairedUserText ?? rawUserText ?? ""} ${repairedEffectiveText ?? rawEffectiveText ?? ""}`.trim();
|
const rawSignalSourceText = `${repairedUserText ?? rawUserText ?? ""} ${repairedEffectiveText ?? rawEffectiveText ?? ""}`.trim();
|
||||||
const rawEntitySourceText = repairedUserText ?? rawUserText ?? repairedEffectiveText ?? rawEffectiveText ?? rawSignalSourceText;
|
const rawEntitySourceText = repairedUserText ?? rawUserText ?? repairedEffectiveText ?? rawEffectiveText ?? rawSignalSourceText;
|
||||||
|
const rawUserEntitySourceText = rawUserSignalSourceText || rawEntitySourceText;
|
||||||
|
const rawUserTextOnly = compactLower(rawUserSignalSourceText);
|
||||||
|
const rawAssistantEntityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates);
|
||||||
|
const rawUserPrimaryBusinessOverviewSignal = hasBusinessOverviewSignal(rawUserTextOnly);
|
||||||
|
const rawUserLifecyclePivotTextSignal = !rawUserPrimaryBusinessOverviewSignal && hasLifecycleSignal(rawUserTextOnly);
|
||||||
|
const rawUserBidirectionalValueFlowPivotTextSignal = !rawUserPrimaryBusinessOverviewSignal &&
|
||||||
|
!rawUserLifecyclePivotTextSignal &&
|
||||||
|
hasBidirectionalValueFlowSignal(rawUserTextOnly);
|
||||||
|
const rawUserScopedEntityCandidate = rawUserSignalSourceText
|
||||||
|
? rawScopedEntityCandidateFromText(rawUserEntitySourceText)
|
||||||
|
: null;
|
||||||
|
const rawUserCounterpartyBidirectionalOverride = Boolean(rawUserBidirectionalValueFlowPivotTextSignal &&
|
||||||
|
(rawUserScopedEntityCandidate ||
|
||||||
|
predecomposeEntities.counterparty ||
|
||||||
|
rawAssistantEntityCandidates.find((candidate) => !isInvalidEntityCandidate(candidate))));
|
||||||
const rawText = compactLower(rawSignalSourceText);
|
const rawText = compactLower(rawSignalSourceText);
|
||||||
const rawReferentialDocumentExclusionSignal = hasReferentialDocumentExclusionFollowupSignal(repairedUserText ?? rawUserText ?? "");
|
const rawReferentialDocumentExclusionSignal = hasReferentialDocumentExclusionFollowupSignal(repairedUserText ?? rawUserText ?? "");
|
||||||
const rawPrimaryBusinessOverviewSignal = hasBusinessOverviewSignal(rawText);
|
const rawPrimaryBusinessOverviewSignal = hasBusinessOverviewSignal(rawText) && !rawUserCounterpartyBidirectionalOverride;
|
||||||
const explicitVatQuestionSignal = hasExplicitVatQuestionSignal(rawText);
|
const explicitVatQuestionSignal = hasExplicitVatQuestionSignal(rawText);
|
||||||
const explicitVatMovementEvidenceSignal = hasExplicitVatMovementEvidenceSignal(rawText);
|
const explicitVatMovementEvidenceSignal = hasExplicitVatMovementEvidenceSignal(rawText);
|
||||||
|
const rawLifecyclePivotTextSignal = !rawPrimaryBusinessOverviewSignal && hasLifecycleSignal(rawText);
|
||||||
|
const rawBidirectionalValueFlowPivotTextSignal = !rawPrimaryBusinessOverviewSignal &&
|
||||||
|
!rawLifecyclePivotTextSignal &&
|
||||||
|
hasBidirectionalValueFlowSignal(rawText);
|
||||||
|
const rawValueFlowPivotTextSignal = !rawPrimaryBusinessOverviewSignal &&
|
||||||
|
!rawLifecyclePivotTextSignal &&
|
||||||
|
(hasValueFlowSignal(rawText) ||
|
||||||
|
hasValueRankingSignal(rawText) ||
|
||||||
|
rawBidirectionalValueFlowPivotTextSignal);
|
||||||
const explicitVatSuppressesBusinessOverviewContinuation = Boolean(explicitVatQuestionSignal && !rawPrimaryBusinessOverviewSignal);
|
const explicitVatSuppressesBusinessOverviewContinuation = Boolean(explicitVatQuestionSignal && !rawPrimaryBusinessOverviewSignal);
|
||||||
const businessOverviewContinuationSignal = hasBusinessOverviewFollowupSeed(followupSeed) &&
|
const businessOverviewContinuationSignal = hasBusinessOverviewFollowupSeed(followupSeed) &&
|
||||||
hasBusinessOverviewContinuationSignal(rawText) &&
|
hasBusinessOverviewContinuationSignal(rawText) &&
|
||||||
|
|
@ -1163,7 +1259,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
hasMetadataSignal(rawText);
|
hasMetadataSignal(rawText);
|
||||||
const rawEntityResolutionSignal = !rawLifecycleSignal && !rawValueFlowSignal && !rawMetadataSignal && hasEntityResolutionSignal(rawText);
|
const rawEntityResolutionSignal = !rawLifecycleSignal && !rawValueFlowSignal && !rawMetadataSignal && hasEntityResolutionSignal(rawText);
|
||||||
const rawPayoutSignal = rawValueFlowSignal && !rawBidirectionalValueFlowSignal && hasPayoutSignal(rawText);
|
const rawPayoutSignal = rawValueFlowSignal && !rawBidirectionalValueFlowSignal && hasPayoutSignal(rawText);
|
||||||
const rawValueFlowAggregateQuestionSignal = rawValueFlowSignal && hasValueFlowAggregateQuestionSignal(rawText);
|
const rawValueFlowAggregateQuestionSignal = (rawValueFlowSignal || rawValueFlowPivotTextSignal) && hasValueFlowAggregateQuestionSignal(rawText);
|
||||||
const monthlyAggregationSignal = hasMonthlyAggregationSignal(rawText);
|
const monthlyAggregationSignal = hasMonthlyAggregationSignal(rawText);
|
||||||
const rawAllTimeScopeSignal = hasAllTimeScopeHint(rawText);
|
const rawAllTimeScopeSignal = hasAllTimeScopeHint(rawText);
|
||||||
const dateScopeSignalText = stripNegatedTaxDateScopeClauses(rawText);
|
const dateScopeSignalText = stripNegatedTaxDateScopeClauses(rawText);
|
||||||
|
|
@ -1191,6 +1287,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family);
|
const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family);
|
||||||
const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis);
|
const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis);
|
||||||
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
|
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
|
||||||
|
const assistantTurnMeaningReasonCodes = collectReasonCodes(assistantTurnMeaning?.reason_codes);
|
||||||
|
const compactCashflowDisplayFollowupSignal = assistantTurnMeaningReasonCodes.includes("compact_cashflow_display_current_turn_signal");
|
||||||
const broadBusinessEvaluationUnsupported = unsupported === "broad_business_evaluation";
|
const broadBusinessEvaluationUnsupported = unsupported === "broad_business_evaluation";
|
||||||
const seededBusinessOverviewSignal = broadBusinessEvaluationUnsupported ||
|
const seededBusinessOverviewSignal = broadBusinessEvaluationUnsupported ||
|
||||||
rawDomain === "business_summary" ||
|
rawDomain === "business_summary" ||
|
||||||
|
|
@ -1216,6 +1314,51 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
: profitMarginBusinessOverviewSignal
|
: profitMarginBusinessOverviewSignal
|
||||||
? "profit_margin_boundary"
|
? "profit_margin_boundary"
|
||||||
: "broad_evaluation";
|
: "broad_evaluation";
|
||||||
|
const assistantTurnMeaningDateScope = toNonEmptyString(assistantTurnMeaning?.explicit_date_scope);
|
||||||
|
const rawAssistantTurnMeaningOrganizationScope = toNonEmptyString(assistantTurnMeaning?.explicit_organization_scope);
|
||||||
|
const assistantTurnMeaningOrganizationScope = isReferentialOrganizationPlaceholder(rawAssistantTurnMeaningOrganizationScope)
|
||||||
|
? null
|
||||||
|
: rawAssistantTurnMeaningOrganizationScope;
|
||||||
|
const organizationSelectionFromKnown = (0, assistantOrganizationMatcher_1.resolveOrganizationSelectionFromMessage)(rawUserText ?? rawEffectiveText ?? rawSignalSourceText, knownOrganizations) ??
|
||||||
|
(0, assistantOrganizationMatcher_1.resolveOrganizationSelectionFromMessage)(rawSignalSourceText, knownOrganizations) ??
|
||||||
|
null;
|
||||||
|
const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText) ??
|
||||||
|
extractOrganizationScopeFromRawText(rawEffectiveText) ??
|
||||||
|
extractOrganizationScopeFromRawText(rawSignalSourceText);
|
||||||
|
const semanticOrganizationScope = normalizeLooseOrganizationAlias(extractOrganizationScopeFromSemanticText(rawEffectiveText) ??
|
||||||
|
extractOrganizationScopeFromSemanticText(rawUserText) ??
|
||||||
|
extractOrganizationScopeFromSemanticText(rawSignalSourceText));
|
||||||
|
const predecomposeCounterpartyAsOrganizationScope = (rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
|
||||||
|
semanticOrganizationScope &&
|
||||||
|
!rawScopedEntityCandidate &&
|
||||||
|
!predecomposeEntities.organization
|
||||||
|
? normalizeLooseOrganizationAlias(predecomposeEntities.counterparty)
|
||||||
|
: null;
|
||||||
|
const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText) ||
|
||||||
|
Boolean(organizationSelectionFromKnown || rawOrganizationScope || semanticOrganizationScope || predecomposeCounterpartyAsOrganizationScope);
|
||||||
|
const currentTurnFreshOrganizationScope = organizationSelectionFromKnown ??
|
||||||
|
predecomposeEntities.organization ??
|
||||||
|
rawOrganizationScope ??
|
||||||
|
semanticOrganizationScope ??
|
||||||
|
predecomposeCounterpartyAsOrganizationScope;
|
||||||
|
const currentTurnOrganizationScope = currentTurnFreshOrganizationScope ?? assistantTurnMeaningOrganizationScope;
|
||||||
|
const predecomposeOrganizationMirrorsCounterparty = sameScopedName(predecomposeEntities.counterparty, predecomposeEntities.organization);
|
||||||
|
const organizationMirrorsPredecomposeCounterpartyForPivot = Boolean(sameScopedName(predecomposeEntities.counterparty, assistantTurnMeaningOrganizationScope) ||
|
||||||
|
sameScopedName(predecomposeEntities.counterparty, currentTurnOrganizationScope) ||
|
||||||
|
predecomposeOrganizationMirrorsCounterparty);
|
||||||
|
const normalizedPredecomposeCounterpartyForPivot = organizationMirrorsPredecomposeCounterpartyForPivot
|
||||||
|
? null
|
||||||
|
: normalizeFollowupCounterpartyCandidate(predecomposeEntities.counterparty);
|
||||||
|
const rawExplicitCounterpartyPivotCandidate = rawScopedEntityCandidate ??
|
||||||
|
rawAssistantEntityCandidates.find((candidate) => !isInvalidEntityCandidate(candidate) &&
|
||||||
|
!sameScopedName(candidate, currentTurnOrganizationScope)) ??
|
||||||
|
normalizedPredecomposeCounterpartyForPivot ??
|
||||||
|
null;
|
||||||
|
const businessOverviewCounterpartyValueFlowPivot = Boolean(businessOverviewContinuationSignal &&
|
||||||
|
!rawPrimaryBusinessOverviewSignal &&
|
||||||
|
rawValueFlowPivotTextSignal &&
|
||||||
|
rawExplicitCounterpartyPivotCandidate &&
|
||||||
|
(rawTopicSwitchSignal || rawValueFlowAggregateQuestionSignal));
|
||||||
const businessOverviewUnsupportedFamily = inventoryReserveBusinessOverviewSignal
|
const businessOverviewUnsupportedFamily = inventoryReserveBusinessOverviewSignal
|
||||||
? "inventory_reserve_liquidation_boundary"
|
? "inventory_reserve_liquidation_boundary"
|
||||||
: debtDueDateBusinessOverviewSignal
|
: debtDueDateBusinessOverviewSignal
|
||||||
|
|
@ -1225,8 +1368,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
: profitMarginBusinessOverviewSignal
|
: profitMarginBusinessOverviewSignal
|
||||||
? "profit_margin_boundary"
|
? "profit_margin_boundary"
|
||||||
: "broad_business_evaluation";
|
: "broad_business_evaluation";
|
||||||
const businessOverviewSignal = rawBusinessOverviewSignal ||
|
const businessOverviewSignal = !businessOverviewCounterpartyValueFlowPivot &&
|
||||||
seededBusinessOverviewSignal;
|
(rawBusinessOverviewSignal || seededBusinessOverviewSignal);
|
||||||
const businessOverviewSeparateCounterpartySignal = Boolean(businessOverviewSignal && hasBusinessOverviewSeparateCounterpartySignal(rawText));
|
const businessOverviewSeparateCounterpartySignal = Boolean(businessOverviewSignal && hasBusinessOverviewSeparateCounterpartySignal(rawText));
|
||||||
const businessOverviewSeparateCounterpartyCandidate = businessOverviewSeparateCounterpartySignal
|
const businessOverviewSeparateCounterpartyCandidate = businessOverviewSeparateCounterpartySignal
|
||||||
? businessOverviewSeparateCounterpartyCandidateFromText(rawText)
|
? businessOverviewSeparateCounterpartyCandidateFromText(rawText)
|
||||||
|
|
@ -1244,15 +1387,6 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
hasSimpleMovementLanePivotSignal(rawText) ||
|
hasSimpleMovementLanePivotSignal(rawText) ||
|
||||||
hasMovementEvidenceFollowupSignal(rawText) ||
|
hasMovementEvidenceFollowupSignal(rawText) ||
|
||||||
hasPronounMovementEvidenceFollowupSignal(rawText);
|
hasPronounMovementEvidenceFollowupSignal(rawText);
|
||||||
const assistantTurnMeaningDateScope = toNonEmptyString(assistantTurnMeaning?.explicit_date_scope);
|
|
||||||
const rawAssistantTurnMeaningOrganizationScope = toNonEmptyString(assistantTurnMeaning?.explicit_organization_scope);
|
|
||||||
const assistantTurnMeaningOrganizationScope = isReferentialOrganizationPlaceholder(rawAssistantTurnMeaningOrganizationScope)
|
|
||||||
? null
|
|
||||||
: rawAssistantTurnMeaningOrganizationScope;
|
|
||||||
const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText);
|
|
||||||
const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText ?? rawEffectiveText ?? rawSignalSourceText);
|
|
||||||
const currentTurnFreshOrganizationScope = predecomposeEntities.organization ?? rawOrganizationScope;
|
|
||||||
const currentTurnOrganizationScope = currentTurnFreshOrganizationScope ?? assistantTurnMeaningOrganizationScope;
|
|
||||||
const followupCounterpartyIsMetadataOrganizationScope = Boolean(followupSeed.subjectResolutionOptional &&
|
const followupCounterpartyIsMetadataOrganizationScope = Boolean(followupSeed.subjectResolutionOptional &&
|
||||||
followupSeed.counterparty &&
|
followupSeed.counterparty &&
|
||||||
(followupSeed.metadataScopeHint ||
|
(followupSeed.metadataScopeHint ||
|
||||||
|
|
@ -1288,7 +1422,6 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
const rawOpenScopeValueFlowOrganizationSignal = Boolean(rawValueFlowSignal &&
|
const rawOpenScopeValueFlowOrganizationSignal = Boolean(rawValueFlowSignal &&
|
||||||
!rawBidirectionalValueFlowSignal &&
|
!rawBidirectionalValueFlowSignal &&
|
||||||
explicitOrganizationScopeSignal);
|
explicitOrganizationScopeSignal);
|
||||||
const predecomposeOrganizationMirrorsCounterparty = sameScopedName(predecomposeEntities.counterparty, predecomposeEntities.organization);
|
|
||||||
const organizationMirrorsPredecomposeCounterparty = Boolean((rawBidirectionalValueFlowSignal ||
|
const organizationMirrorsPredecomposeCounterparty = Boolean((rawBidirectionalValueFlowSignal ||
|
||||||
hasValueRankingSignal(rawText) ||
|
hasValueRankingSignal(rawText) ||
|
||||||
rawOpenScopeValueFlowOrganizationSignal ||
|
rawOpenScopeValueFlowOrganizationSignal ||
|
||||||
|
|
@ -1564,14 +1697,25 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
const lifecycleSignal = !businessOverviewSignal && (rawLifecycleSignal || seededDomain === "counterparty_lifecycle");
|
const lifecycleSignal = !businessOverviewSignal && (rawLifecycleSignal || seededDomain === "counterparty_lifecycle");
|
||||||
const bidirectionalValueFlowSignal = !businessOverviewSignal &&
|
const bidirectionalValueFlowSignal = !businessOverviewSignal &&
|
||||||
!lifecycleSignal &&
|
!lifecycleSignal &&
|
||||||
(rawBidirectionalValueFlowSignal || seededAction === "net_value_flow");
|
((businessOverviewCounterpartyValueFlowPivot
|
||||||
|
? rawBidirectionalValueFlowPivotTextSignal
|
||||||
|
: rawBidirectionalValueFlowSignal) ||
|
||||||
|
seededAction === "net_value_flow");
|
||||||
const valueFlowSignal = !businessOverviewSignal &&
|
const valueFlowSignal = !businessOverviewSignal &&
|
||||||
!lifecycleSignal &&
|
!lifecycleSignal &&
|
||||||
!metadataGroundedMovementLaneApplicable &&
|
!metadataGroundedMovementLaneApplicable &&
|
||||||
(rawValueFlowSignal || seededDomain === "counterparty_value");
|
((businessOverviewCounterpartyValueFlowPivot
|
||||||
|
? rawValueFlowPivotTextSignal
|
||||||
|
: rawValueFlowSignal) ||
|
||||||
|
seededDomain === "counterparty_value");
|
||||||
const payoutSignal = valueFlowSignal &&
|
const payoutSignal = valueFlowSignal &&
|
||||||
!bidirectionalValueFlowSignal &&
|
!bidirectionalValueFlowSignal &&
|
||||||
(rawPayoutSignal || seededAction === "payout");
|
((businessOverviewCounterpartyValueFlowPivot
|
||||||
|
? rawValueFlowPivotTextSignal &&
|
||||||
|
!rawBidirectionalValueFlowPivotTextSignal &&
|
||||||
|
hasPayoutSignal(rawText)
|
||||||
|
: rawPayoutSignal) ||
|
||||||
|
seededAction === "payout");
|
||||||
const semanticDataNeed = metadataAmbiguityLaneClarificationApplicable
|
const semanticDataNeed = metadataAmbiguityLaneClarificationApplicable
|
||||||
? "metadata lane clarification"
|
? "metadata lane clarification"
|
||||||
: semanticNeedFor({
|
: semanticNeedFor({
|
||||||
|
|
@ -1579,17 +1723,37 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
? "movements"
|
? "movements"
|
||||||
: businessOverviewSignal
|
: businessOverviewSignal
|
||||||
? "business_overview"
|
? "business_overview"
|
||||||
: rawDomain ?? seededDomain,
|
: lifecycleSignal
|
||||||
|
? "counterparty_lifecycle"
|
||||||
|
: valueFlowSignal
|
||||||
|
? "counterparty_value"
|
||||||
|
: rawDomain ?? seededDomain,
|
||||||
action: explicitVatMovementEvidenceSignal
|
action: explicitVatMovementEvidenceSignal
|
||||||
? "list_movements"
|
? "list_movements"
|
||||||
: businessOverviewSignal
|
: businessOverviewSignal
|
||||||
? businessOverviewActionFamily
|
? businessOverviewActionFamily
|
||||||
: rawAction ?? seededAction,
|
: lifecycleSignal
|
||||||
|
? "activity_duration"
|
||||||
|
: valueFlowSignal
|
||||||
|
? bidirectionalValueFlowSignal
|
||||||
|
? "net_value_flow"
|
||||||
|
: payoutSignal
|
||||||
|
? "payout"
|
||||||
|
: rawAction ?? seededAction ?? "turnover"
|
||||||
|
: rawAction ?? seededAction,
|
||||||
unsupported: explicitVatMovementEvidenceSignal
|
unsupported: explicitVatMovementEvidenceSignal
|
||||||
? "movement_evidence"
|
? "movement_evidence"
|
||||||
: businessOverviewSignal
|
: businessOverviewSignal
|
||||||
? businessOverviewUnsupportedFamily
|
? businessOverviewUnsupportedFamily
|
||||||
: unsupported ?? seededUnsupported,
|
: lifecycleSignal
|
||||||
|
? "counterparty_lifecycle"
|
||||||
|
: valueFlowSignal
|
||||||
|
? bidirectionalValueFlowSignal
|
||||||
|
? "counterparty_bidirectional_value_flow_or_netting"
|
||||||
|
: payoutSignal
|
||||||
|
? "counterparty_payouts_or_outflow"
|
||||||
|
: seededUnsupported ?? "counterparty_value_or_turnover"
|
||||||
|
: unsupported ?? seededUnsupported,
|
||||||
lifecycleSignal,
|
lifecycleSignal,
|
||||||
valueFlowSignal,
|
valueFlowSignal,
|
||||||
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
|
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
|
||||||
|
|
@ -1733,10 +1897,14 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const clarificationLoopStillNeedsPeriod = Boolean(followupSeed.loopStatus === "awaiting_clarification" && followupSeed.loopPendingAxes.includes("period"));
|
const clarificationLoopStillNeedsPeriod = Boolean(followupSeed.loopStatus === "awaiting_clarification" && followupSeed.loopPendingAxes.includes("period"));
|
||||||
|
const rawTwoDigitPredecomposeYearHint = Boolean(predecomposeDateScope &&
|
||||||
|
/^\d{4}$/.test(predecomposeDateScope) &&
|
||||||
|
/\b\d{2}\b/u.test(rawText));
|
||||||
const businessOverviewRawWithoutDateScope = Boolean(businessOverviewSignal &&
|
const businessOverviewRawWithoutDateScope = Boolean(businessOverviewSignal &&
|
||||||
!rawAllTimeScopeSignal &&
|
!rawAllTimeScopeSignal &&
|
||||||
!explicitDateScopeLiteralDetected &&
|
!explicitDateScopeLiteralDetected &&
|
||||||
!rawDateScope &&
|
!rawDateScope &&
|
||||||
|
!rawTwoDigitPredecomposeYearHint &&
|
||||||
!relativeCurrentDateHintDetected);
|
!relativeCurrentDateHintDetected);
|
||||||
const predecomposeDateScopeCountsAsCurrentTurnPeriod = Boolean(predecomposeDateScope &&
|
const predecomposeDateScopeCountsAsCurrentTurnPeriod = Boolean(predecomposeDateScope &&
|
||||||
!isImplicitCurrentDateScope(predecomposeDateScope) &&
|
!isImplicitCurrentDateScope(predecomposeDateScope) &&
|
||||||
|
|
@ -1754,6 +1922,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
(valueFlowOrganizationStaysScope && (Boolean(followupSeed.rankingNeed) || bidirectionalValueFlowSignal))));
|
(valueFlowOrganizationStaysScope && (Boolean(followupSeed.rankingNeed) || bidirectionalValueFlowSignal))));
|
||||||
const suppressNegatedTaxOnlyDateScope = Boolean(businessOverviewSignal && negatedTaxDateScopeOnlySignal);
|
const suppressNegatedTaxOnlyDateScope = Boolean(businessOverviewSignal && negatedTaxDateScopeOnlySignal);
|
||||||
const topicSwitchSuppressesFollowupScope = Boolean(rawTopicSwitchSignal &&
|
const topicSwitchSuppressesFollowupScope = Boolean(rawTopicSwitchSignal &&
|
||||||
|
!compactCashflowDisplayFollowupSignal &&
|
||||||
(rawMetadataSignal ||
|
(rawMetadataSignal ||
|
||||||
businessOverviewSignal ||
|
businessOverviewSignal ||
|
||||||
rawEntitySearchOverridesStaleScope ||
|
rawEntitySearchOverridesStaleScope ||
|
||||||
|
|
@ -1853,16 +2022,16 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
subject_resolution_optional: metadataScopedLaneWithoutSubject || undefined,
|
subject_resolution_optional: metadataScopedLaneWithoutSubject || undefined,
|
||||||
unsupported_but_understood_family: businessOverviewSignal
|
unsupported_but_understood_family: businessOverviewSignal
|
||||||
? businessOverviewUnsupportedFamily
|
? businessOverviewUnsupportedFamily
|
||||||
: unsupported ??
|
: lifecycleSignal
|
||||||
(lifecycleSignal
|
? "counterparty_lifecycle"
|
||||||
? "counterparty_lifecycle"
|
: valueFlowSignal
|
||||||
: valueFlowSignal
|
? bidirectionalValueFlowSignal
|
||||||
? bidirectionalValueFlowSignal
|
? "counterparty_bidirectional_value_flow_or_netting"
|
||||||
? "counterparty_bidirectional_value_flow_or_netting"
|
: payoutSignal
|
||||||
: payoutSignal
|
? "counterparty_payouts_or_outflow"
|
||||||
? "counterparty_payouts_or_outflow"
|
: seededUnsupported ?? "counterparty_value_or_turnover"
|
||||||
: seededUnsupported ?? "counterparty_value_or_turnover"
|
: unsupported ??
|
||||||
: metadataGroundedMovementLaneApplicable
|
(metadataGroundedMovementLaneApplicable
|
||||||
? "movement_evidence"
|
? "movement_evidence"
|
||||||
: metadataGroundedDocumentLaneApplicable
|
: metadataGroundedDocumentLaneApplicable
|
||||||
? "document_evidence"
|
? "document_evidence"
|
||||||
|
|
@ -2137,6 +2306,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
if (businessOverviewSignal) {
|
if (businessOverviewSignal) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_broad_business_evaluation_route_candidate");
|
pushReason(reasonCodes, "mcp_discovery_broad_business_evaluation_route_candidate");
|
||||||
}
|
}
|
||||||
|
if (businessOverviewCounterpartyValueFlowPivot) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_business_overview_followup_pivoted_to_counterparty_value_flow");
|
||||||
|
}
|
||||||
if (businessOverviewContinuationSignal) {
|
if (businessOverviewContinuationSignal) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_from_followup_context");
|
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_from_followup_context");
|
||||||
}
|
}
|
||||||
|
|
@ -2161,6 +2333,15 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
if (businessOverviewRawYearOverridesPredecomposeAsOf) {
|
if (businessOverviewRawYearOverridesPredecomposeAsOf) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_business_overview_raw_year_overrode_predecompose_as_of_scope");
|
pushReason(reasonCodes, "mcp_discovery_business_overview_raw_year_overrode_predecompose_as_of_scope");
|
||||||
}
|
}
|
||||||
|
if (organizationSelectionFromKnown) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_organization_scope_from_known_organizations");
|
||||||
|
}
|
||||||
|
if (semanticOrganizationScope) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_organization_scope_from_semantic_text");
|
||||||
|
}
|
||||||
|
if (predecomposeCounterpartyAsOrganizationScope) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_counterparty_reinterpreted_as_organization_scope");
|
||||||
|
}
|
||||||
if (!(valueFlowOrganizationStaysScope && normalizedPredecomposeCounterparty === explicitOrganizationScope) &&
|
if (!(valueFlowOrganizationStaysScope && normalizedPredecomposeCounterparty === explicitOrganizationScope) &&
|
||||||
normalizedPredecomposeCounterparty) {
|
normalizedPredecomposeCounterparty) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose");
|
pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose");
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([
|
||||||
"inventory_purchase_documents_for_item",
|
"inventory_purchase_documents_for_item",
|
||||||
"inventory_supplier_stock_overlap_as_of_date",
|
"inventory_supplier_stock_overlap_as_of_date",
|
||||||
"inventory_sale_trace_for_item",
|
"inventory_sale_trace_for_item",
|
||||||
|
"inventory_margin_ranking_for_nomenclature",
|
||||||
"inventory_profitability_for_item",
|
"inventory_profitability_for_item",
|
||||||
"inventory_purchase_to_sale_chain",
|
"inventory_purchase_to_sale_chain",
|
||||||
"inventory_aging_by_purchase_date",
|
"inventory_aging_by_purchase_date",
|
||||||
|
|
@ -40,6 +41,7 @@ const ADDRESS_INTENTS_ALLOW_STRICT_DEEP_INVESTIGATION_BYPASS = new Set([
|
||||||
"inventory_purchase_provenance_for_item",
|
"inventory_purchase_provenance_for_item",
|
||||||
"inventory_purchase_documents_for_item",
|
"inventory_purchase_documents_for_item",
|
||||||
"inventory_sale_trace_for_item",
|
"inventory_sale_trace_for_item",
|
||||||
|
"inventory_margin_ranking_for_nomenclature",
|
||||||
"inventory_profitability_for_item",
|
"inventory_profitability_for_item",
|
||||||
"inventory_purchase_to_sale_chain"
|
"inventory_purchase_to_sale_chain"
|
||||||
]);
|
]);
|
||||||
|
|
@ -204,7 +206,7 @@ function createAssistantRoutePolicy(deps) {
|
||||||
: null;
|
: null;
|
||||||
const semanticCanonicalRecommended = semanticExtractionContract?.apply_canonical_recommended !== false;
|
const semanticCanonicalRecommended = semanticExtractionContract?.apply_canonical_recommended !== false;
|
||||||
const llmSupportedDeepAddressIntentSignal = llmContractMode === "deep_analysis" &&
|
const llmSupportedDeepAddressIntentSignal = llmContractMode === "deep_analysis" &&
|
||||||
/^(?:inventory_purchase_provenance_for_item|inventory_purchase_documents_for_item|inventory_sale_trace_for_item|inventory_profitability_for_item|inventory_purchase_to_sale_chain)$/u.test(llmContractIntent ?? "") &&
|
/^(?:inventory_purchase_provenance_for_item|inventory_purchase_documents_for_item|inventory_sale_trace_for_item|inventory_margin_ranking_for_nomenclature|inventory_profitability_for_item|inventory_purchase_to_sale_chain)$/u.test(llmContractIntent ?? "") &&
|
||||||
semanticCanonicalRecommended;
|
semanticCanonicalRecommended;
|
||||||
const llmCanonicalEntitySignal = /(?:заказчик|поставщик|контрагент|компан|customer|supplier|counterparty|company|vendor|client)/iu.test(compactWhitespace(repairedInputMessage.toLowerCase()));
|
const llmCanonicalEntitySignal = /(?:заказчик|поставщик|контрагент|компан|customer|supplier|counterparty|company|vendor|client)/iu.test(compactWhitespace(repairedInputMessage.toLowerCase()));
|
||||||
const llmCanonicalAppliedSignal = Boolean(llmPreDecomposeMeta?.applied) && llmContractMode !== "deep_analysis";
|
const llmCanonicalAppliedSignal = Boolean(llmPreDecomposeMeta?.applied) && llmContractMode !== "deep_analysis";
|
||||||
|
|
@ -313,6 +315,21 @@ function createAssistantRoutePolicy(deps) {
|
||||||
const hasTemporalCue = /(?:на\s+эту\s+же\s+дат[ауеы]|на\s+тот\s+же\s+период|за\s+этот\s+же\s+период|за\s+этот\s+период|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр|\b(?:19|20)\d{2}\b)/iu.test(normalized);
|
const hasTemporalCue = /(?:на\s+эту\s+же\s+дат[ауеы]|на\s+тот\s+же\s+период|за\s+этот\s+же\s+период|за\s+этот\s+период|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр|\b(?:19|20)\d{2}\b)/iu.test(normalized);
|
||||||
return hasRequestCue && hasTemporalCue;
|
return hasRequestCue && hasTemporalCue;
|
||||||
}
|
}
|
||||||
|
function hasInventoryMarginRankingContinuationSignal(text) {
|
||||||
|
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase()).replace(/ё/g, "е");
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const wantsFoundRows = /(?:покажи|показать|выведи|дай|раскрой|show|list)/iu.test(normalized) &&
|
||||||
|
/(?:найденн|строк|реализац|себестоимостн|баз)/iu.test(normalized) &&
|
||||||
|
/(?:себестоимостн|реализац|марж|прибыл|номенклатур)/iu.test(normalized);
|
||||||
|
const account41Not01 = /\b41(?:[.,]\d{1,2})?\b/iu.test(normalized) &&
|
||||||
|
/\b01(?:[.,]\d{1,2})?\b/iu.test(normalized) &&
|
||||||
|
/(?:\bне\b|вместо|а\s+не|not|instead)/iu.test(normalized);
|
||||||
|
const periodExpansion = /(?:расширь|расширить|возьми|давай|покажи|за|на|до|весь|год|квартал|месяц|expand|period)/iu.test(normalized) &&
|
||||||
|
/(?:январ|феврал|март|апрел|ма[йяе]|июн|июл|август|сентябр|октябр|ноябр|декабр|\b(?:19|20)\d{2}\b)/iu.test(normalized);
|
||||||
|
return wantsFoundRows || account41Not01 || periodExpansion;
|
||||||
|
}
|
||||||
function hasOrganizationClarificationTextCue(text) {
|
function hasOrganizationClarificationTextCue(text) {
|
||||||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
|
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
|
|
@ -479,6 +496,7 @@ function createAssistantRoutePolicy(deps) {
|
||||||
hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) ||
|
hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) ||
|
||||||
hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage);
|
hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage);
|
||||||
const followupPreviousIntent = toNonEmptyString(followupContext?.previous_intent);
|
const followupPreviousIntent = toNonEmptyString(followupContext?.previous_intent);
|
||||||
|
const followupRootIntent = toNonEmptyString(followupContext?.root_intent);
|
||||||
const followupPreviousFilters = followupContext?.previous_filters && typeof followupContext.previous_filters === "object"
|
const followupPreviousFilters = followupContext?.previous_filters && typeof followupContext.previous_filters === "object"
|
||||||
? followupContext.previous_filters
|
? followupContext.previous_filters
|
||||||
: null;
|
: null;
|
||||||
|
|
@ -513,6 +531,16 @@ function createAssistantRoutePolicy(deps) {
|
||||||
hasShortInventoryObjectFollowupSignal(repairedRawUserMessage) ||
|
hasShortInventoryObjectFollowupSignal(repairedRawUserMessage) ||
|
||||||
hasShortInventoryObjectFollowupSignal(effectiveAddressUserMessage) ||
|
hasShortInventoryObjectFollowupSignal(effectiveAddressUserMessage) ||
|
||||||
hasShortInventoryObjectFollowupSignal(repairedEffectiveAddressUserMessage)));
|
hasShortInventoryObjectFollowupSignal(repairedEffectiveAddressUserMessage)));
|
||||||
|
const protectedInventoryMarginRankingFollowup = Boolean(followupContext &&
|
||||||
|
(followupPreviousIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
|
followupRootIntent === "inventory_margin_ranking_for_nomenclature") &&
|
||||||
|
!dataScopeMetaQuery &&
|
||||||
|
!capabilityMetaQuery &&
|
||||||
|
!dangerOrCoercionSignal &&
|
||||||
|
(hasInventoryMarginRankingContinuationSignal(rawUserMessage) ||
|
||||||
|
hasInventoryMarginRankingContinuationSignal(repairedRawUserMessage) ||
|
||||||
|
hasInventoryMarginRankingContinuationSignal(effectiveAddressUserMessage) ||
|
||||||
|
hasInventoryMarginRankingContinuationSignal(repairedEffectiveAddressUserMessage)));
|
||||||
const organizationClarificationContinuationDetected = Boolean((followupContext || continuitySnapshot.hasGroundedAddressContext) &&
|
const organizationClarificationContinuationDetected = Boolean((followupContext || continuitySnapshot.hasGroundedAddressContext) &&
|
||||||
lastOrganizationClarificationDebug &&
|
lastOrganizationClarificationDebug &&
|
||||||
explicitOrganizationClarificationSelection &&
|
explicitOrganizationClarificationSelection &&
|
||||||
|
|
@ -569,6 +597,7 @@ function createAssistantRoutePolicy(deps) {
|
||||||
!baseToolGatePreservesAddressLane &&
|
!baseToolGatePreservesAddressLane &&
|
||||||
!effectiveGroundedValueFlowFollowupContextDetected &&
|
!effectiveGroundedValueFlowFollowupContextDetected &&
|
||||||
!protectedInventoryShortFollowup &&
|
!protectedInventoryShortFollowup &&
|
||||||
|
!protectedInventoryMarginRankingFollowup &&
|
||||||
!organizationClarificationContinuationDetected &&
|
!organizationClarificationContinuationDetected &&
|
||||||
!routeCandidateOrganizationClarificationDetected);
|
!routeCandidateOrganizationClarificationDetected);
|
||||||
const lastAddressAssistantDebug = sessionItems
|
const lastAddressAssistantDebug = sessionItems
|
||||||
|
|
@ -1078,6 +1107,7 @@ function createAssistantRoutePolicy(deps) {
|
||||||
hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) ||
|
hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) ||
|
||||||
hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) ||
|
hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) ||
|
||||||
hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage) ||
|
hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage) ||
|
||||||
|
protectedInventoryMarginRankingFollowup ||
|
||||||
inventoryRootRestatementFollowupDetected);
|
inventoryRootRestatementFollowupDetected);
|
||||||
const deepAnalysisPreferenceDetected = Boolean(hasDeepAnalysisPreferenceSignal(rawUserMessage) ||
|
const deepAnalysisPreferenceDetected = Boolean(hasDeepAnalysisPreferenceSignal(rawUserMessage) ||
|
||||||
hasDeepAnalysisPreferenceSignal(repairedRawUserMessage) ||
|
hasDeepAnalysisPreferenceSignal(repairedRawUserMessage) ||
|
||||||
|
|
@ -1114,7 +1144,9 @@ function createAssistantRoutePolicy(deps) {
|
||||||
resolvedIntentResolution.intent === "unknown" &&
|
resolvedIntentResolution.intent === "unknown" &&
|
||||||
(!llmContractIntent || llmContractIntent === "unknown"));
|
(!llmContractIntent || llmContractIntent === "unknown"));
|
||||||
const exactAddressIntentProtectedFromSemanticDeepHint = laneProtectionArbitration.exactAddressIntentProtectedFromSemanticDeepHint;
|
const exactAddressIntentProtectedFromSemanticDeepHint = laneProtectionArbitration.exactAddressIntentProtectedFromSemanticDeepHint;
|
||||||
const protectAddressLaneFromFallback = Boolean(laneProtectionArbitration.protectAddressLaneFromFallback || customerValueRankingAddressSignal);
|
const protectAddressLaneFromFallback = Boolean(laneProtectionArbitration.protectAddressLaneFromFallback ||
|
||||||
|
customerValueRankingAddressSignal ||
|
||||||
|
protectedInventoryMarginRankingFollowup);
|
||||||
const vatExplainFollowupSignal = Boolean(followupContext &&
|
const vatExplainFollowupSignal = Boolean(followupContext &&
|
||||||
toNonEmptyString(followupContext.previous_intent) === "vat_payable_forecast" &&
|
toNonEmptyString(followupContext.previous_intent) === "vat_payable_forecast" &&
|
||||||
/(?:\u043f\u043e\u0447\u0435\u043c\u0443|why).*(?:\u043f\u0440\u043e\u0433\u043d\u043e\u0437|forecast).*(?:\u0443\u043f\u043b\u0430\u0442|payable|\b0\b)/iu.test(compactWhitespace(`${repairedRawUserMessage} ${repairedEffectiveAddressUserMessage}`)));
|
/(?:\u043f\u043e\u0447\u0435\u043c\u0443|why).*(?:\u043f\u0440\u043e\u0433\u043d\u043e\u0437|forecast).*(?:\u0443\u043f\u043b\u0430\u0442|payable|\b0\b)/iu.test(compactWhitespace(`${repairedRawUserMessage} ${repairedEffectiveAddressUserMessage}`)));
|
||||||
|
|
@ -1194,7 +1226,7 @@ function createAssistantRoutePolicy(deps) {
|
||||||
let toolGateDecision = String(baseToolGate?.decision ?? "skip_address_lane");
|
let toolGateDecision = String(baseToolGate?.decision ?? "skip_address_lane");
|
||||||
let toolGateReason = String(baseToolGate?.reason ?? "no_address_signal_after_l0");
|
let toolGateReason = String(baseToolGate?.reason ?? "no_address_signal_after_l0");
|
||||||
const semanticAddressLaneRecovery = Boolean(!runAddressLane &&
|
const semanticAddressLaneRecovery = Boolean(!runAddressLane &&
|
||||||
supportedAddressRouteCandidateDetected &&
|
(supportedAddressRouteCandidateDetected || protectedInventoryMarginRankingFollowup) &&
|
||||||
!deepAnalysisPreferenceDetected &&
|
!deepAnalysisPreferenceDetected &&
|
||||||
!unsupportedAddressIntentFallbackToDeep &&
|
!unsupportedAddressIntentFallbackToDeep &&
|
||||||
!deepAnalysisSignalFallbackToDeep &&
|
!deepAnalysisSignalFallbackToDeep &&
|
||||||
|
|
@ -1203,9 +1235,11 @@ function createAssistantRoutePolicy(deps) {
|
||||||
if (semanticAddressLaneRecovery) {
|
if (semanticAddressLaneRecovery) {
|
||||||
runAddressLane = true;
|
runAddressLane = true;
|
||||||
toolGateDecision = "run_address_lane";
|
toolGateDecision = "run_address_lane";
|
||||||
toolGateReason = resolvedIntentResolution.intent !== "unknown" || llmContractIntent
|
toolGateReason = protectedInventoryMarginRankingFollowup
|
||||||
? "address_intent_resolver_detected"
|
? "followup_context_detected"
|
||||||
: "address_signal_detected";
|
: resolvedIntentResolution.intent !== "unknown" || llmContractIntent
|
||||||
|
? "address_intent_resolver_detected"
|
||||||
|
: "address_signal_detected";
|
||||||
}
|
}
|
||||||
if (unsupportedAddressIntentFallbackToDeep) {
|
if (unsupportedAddressIntentFallbackToDeep) {
|
||||||
runAddressLane = false;
|
runAddressLane = false;
|
||||||
|
|
|
||||||
|
|
@ -310,6 +310,18 @@ exports.INVENTORY_CAPABILITY_CONTRACTS = [
|
||||||
answerObjectShape: "inventory_profitability_bundle",
|
answerObjectShape: "inventory_profitability_bundle",
|
||||||
bundleReusePolicy: "sale_trace_bundle_preferred"
|
bundleReusePolicy: "sale_trace_bundle_preferred"
|
||||||
}),
|
}),
|
||||||
|
inventoryExactCapability({
|
||||||
|
capability_id: "inventory_inventory_margin_ranking_for_nomenclature",
|
||||||
|
intent_ids: ["inventory_margin_ranking_for_nomenclature"],
|
||||||
|
entry_modes: ["root_entry", "root_followup", "clarification_resume"],
|
||||||
|
transitions: ["T1", "T2", "T7"],
|
||||||
|
requiresFocusObject: false,
|
||||||
|
requiredAnchors: ["period"],
|
||||||
|
resultShape: "nomenclature_margin_ranking",
|
||||||
|
answerObjectShape: "inventory_margin_ranking",
|
||||||
|
bundleReusePolicy: "none",
|
||||||
|
scenarioFamilies: ["canonical", "colloquial", "account_41_correction"]
|
||||||
|
}),
|
||||||
inventoryExactCapability({
|
inventoryExactCapability({
|
||||||
capability_id: "inventory_inventory_purchase_to_sale_chain",
|
capability_id: "inventory_inventory_purchase_to_sale_chain",
|
||||||
intent_ids: ["inventory_purchase_to_sale_chain"],
|
intent_ids: ["inventory_purchase_to_sale_chain"],
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,26 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
function normalizeFollowupText(value) {
|
function normalizeFollowupText(value) {
|
||||||
return deps.compactWhitespace(deps.repairAddressMojibake(String(value ?? "")).toLowerCase()).replace(/ё/g, "е");
|
return deps.compactWhitespace(deps.repairAddressMojibake(String(value ?? "")).toLowerCase()).replace(/ё/g, "е");
|
||||||
}
|
}
|
||||||
|
function hasInventoryMarginRankingFollowupSignal(userMessage, alternateMessage = null, sourceIntentHint = null) {
|
||||||
|
if (sourceIntentHint !== "inventory_margin_ranking_for_nomenclature") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return [userMessage, alternateMessage]
|
||||||
|
.filter((value) => deps.toNonEmptyString(value))
|
||||||
|
.map((value) => deps.compactWhitespace(deps.repairAddressMojibake(String(value ?? "")).toLowerCase()).replace(/ё/g, "е"))
|
||||||
|
.some((normalized) => {
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const wantsFoundRows = /(?:покажи|показать|выведи|дай|раскрой|show|list|покажи|показать|выведи|дай|раскрой)/iu.test(normalized) &&
|
||||||
|
/(?:найденн|строк|реализац|себестоимостн|баз|найденн|строк|реализац|себестоимостн|баз)/iu.test(normalized) &&
|
||||||
|
/(?:себестоимостн|реализац|марж|прибыл|номенклатур|себестоимостн|реализац|марж|прибыл|номенклат)/iu.test(normalized);
|
||||||
|
const account41Not01 = /\b41(?:[.,]\d{1,2})?\b/iu.test(normalized) &&
|
||||||
|
/\b01(?:[.,]\d{1,2})?\b/iu.test(normalized) &&
|
||||||
|
/(?:\bне\b|вместо|а\s+не|not|instead|РЅРµ|вместо|Р°\s+РЅРµ)/iu.test(normalized);
|
||||||
|
return wantsFoundRows || account41Not01;
|
||||||
|
});
|
||||||
|
}
|
||||||
function hasSamePeriodReferenceCue(...values) {
|
function hasSamePeriodReferenceCue(...values) {
|
||||||
return values
|
return values
|
||||||
.map((value) => normalizeFollowupText(value))
|
.map((value) => normalizeFollowupText(value))
|
||||||
|
|
@ -469,8 +489,24 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
llmPreDecomposeMeta
|
llmPreDecomposeMeta
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
const hasCompactCashflowFollowupCue = (value) => {
|
||||||
|
const normalized = normalizeFollowupText(value);
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasCompactCue = /(?:\u043a\u043e\u0440\u043e\u0442\p{L}*|\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u0431\u0435\u0437\s+\u0442\u043e\u043f\p{L}*)/iu.test(normalized);
|
||||||
|
const hasCashflowCue = /(?:\u0434\u0435\u043d\p{L}*|\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u043f\u043e\u043b\u0443\u0447|\u0437\u0430\u043f\u043b\u0430\u0442|\u043f\u0440\u0438\u0448\p{L}*|\u0443\u0448\p{L}*|\u043d\u0435\u0442\u0442\u043e)/iu.test(normalized);
|
||||||
|
return hasCompactCue && hasCashflowCue;
|
||||||
|
};
|
||||||
|
const compactCashflowFollowupSignal = hasShortValueFlowRetargetCue(userMessage) ||
|
||||||
|
hasCompactCashflowFollowupCue(userMessage) ||
|
||||||
|
(deps.toNonEmptyString(alternateMessage)
|
||||||
|
? hasShortValueFlowRetargetCue(String(alternateMessage ?? "")) ||
|
||||||
|
hasCompactCashflowFollowupCue(String(alternateMessage ?? ""))
|
||||||
|
: false);
|
||||||
if (assistantTurnMeaning?.stale_replay_forbidden === true &&
|
if (assistantTurnMeaning?.stale_replay_forbidden === true &&
|
||||||
!hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage)) {
|
!hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage) &&
|
||||||
|
!compactCashflowFollowupSignal) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const latestAddressItem = deps.findLastAddressAssistantItem(items);
|
const latestAddressItem = deps.findLastAddressAssistantItem(items);
|
||||||
|
|
@ -535,7 +571,8 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
const hasValueFlowCarryoverSourceHint = sourceIntentHint === "customer_revenue_and_payments" ||
|
const hasValueFlowCarryoverSourceHint = sourceIntentHint === "customer_revenue_and_payments" ||
|
||||||
sourceDiscoveryPilotScopeHint === "counterparty_value_flow_query_movements_v1" ||
|
sourceDiscoveryPilotScopeHint === "counterparty_value_flow_query_movements_v1" ||
|
||||||
sourceDiscoveryPilotScopeHint === "counterparty_supplier_payout_query_movements_v1" ||
|
sourceDiscoveryPilotScopeHint === "counterparty_supplier_payout_query_movements_v1" ||
|
||||||
sourceDiscoveryPilotScopeHint === "counterparty_bidirectional_value_flow_query_movements_v1";
|
sourceDiscoveryPilotScopeHint === "counterparty_bidirectional_value_flow_query_movements_v1" ||
|
||||||
|
sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
|
||||||
const hasBusinessOverviewCarryoverSourceHint = sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
|
const hasBusinessOverviewCarryoverSourceHint = sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
|
||||||
const navigationSessionState = (0, assistantContinuityPolicy_1.resolveNavigationSessionContextState)(addressNavigationState, deps.toNonEmptyString, deps.normalizeOrganizationScopeValue);
|
const navigationSessionState = (0, assistantContinuityPolicy_1.resolveNavigationSessionContextState)(addressNavigationState, deps.toNonEmptyString, deps.normalizeOrganizationScopeValue);
|
||||||
const navigationFocusObjectHint = navigationSessionState.focusObject;
|
const navigationFocusObjectHint = navigationSessionState.focusObject;
|
||||||
|
|
@ -545,6 +582,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
deps.isInventorySelectedObjectIntent(sourceIntentHint)));
|
deps.isInventorySelectedObjectIntent(sourceIntentHint)));
|
||||||
const inventoryPurchaseDateVatBridge = hasInventoryPurchaseDateVatBridgeSignal(userMessage, alternateMessage, sourceIntentHint, hasNavigationInventoryItemFocusHint);
|
const inventoryPurchaseDateVatBridge = hasInventoryPurchaseDateVatBridgeSignal(userMessage, alternateMessage, sourceIntentHint, hasNavigationInventoryItemFocusHint);
|
||||||
|
const inventoryMarginRankingFollowup = hasInventoryMarginRankingFollowupSignal(userMessage, alternateMessage, sourceIntentHint);
|
||||||
let inventoryShortFollowupPrimary = (deps.isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
|
let inventoryShortFollowupPrimary = (deps.isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
|
||||||
deps.hasShortInventoryObjectFollowupSignal(userMessage);
|
deps.hasShortInventoryObjectFollowupSignal(userMessage);
|
||||||
let inventoryShortFollowupAlternate = (deps.isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
|
let inventoryShortFollowupAlternate = (deps.isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
|
||||||
|
|
@ -558,9 +596,11 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
? deps.resolveDebtRoleSwapFollowupIntent(String(alternateMessage ?? ""), sourceIntentHint)
|
? deps.resolveDebtRoleSwapFollowupIntent(String(alternateMessage ?? ""), sourceIntentHint)
|
||||||
: null;
|
: null;
|
||||||
const debtRoleSwapIntent = debtRoleSwapPrimary ?? debtRoleSwapAlternate ?? null;
|
const debtRoleSwapIntent = debtRoleSwapPrimary ?? debtRoleSwapAlternate ?? null;
|
||||||
const shortValueFlowRetargetPrimary = hasValueFlowCarryoverSourceHint && hasShortValueFlowRetargetCue(userMessage);
|
const shortValueFlowRetargetPrimary = hasValueFlowCarryoverSourceHint &&
|
||||||
|
(hasShortValueFlowRetargetCue(userMessage) || hasCompactCashflowFollowupCue(userMessage));
|
||||||
const shortValueFlowRetargetAlternate = hasValueFlowCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
|
const shortValueFlowRetargetAlternate = hasValueFlowCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
|
||||||
? hasShortValueFlowRetargetCue(String(alternateMessage ?? ""))
|
? hasShortValueFlowRetargetCue(String(alternateMessage ?? "")) ||
|
||||||
|
hasCompactCashflowFollowupCue(String(alternateMessage ?? ""))
|
||||||
: false;
|
: false;
|
||||||
const businessOverviewBoundaryFollowupPrimary = hasBusinessOverviewCarryoverSourceHint && hasBusinessOverviewBoundaryFollowupCue(userMessage);
|
const businessOverviewBoundaryFollowupPrimary = hasBusinessOverviewCarryoverSourceHint && hasBusinessOverviewBoundaryFollowupCue(userMessage);
|
||||||
const businessOverviewBoundaryFollowupAlternate = hasBusinessOverviewCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
|
const businessOverviewBoundaryFollowupAlternate = hasBusinessOverviewCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
|
||||||
|
|
@ -573,6 +613,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
businessOverviewBoundaryFollowupPrimary ||
|
businessOverviewBoundaryFollowupPrimary ||
|
||||||
inventoryShortFollowupPrimary ||
|
inventoryShortFollowupPrimary ||
|
||||||
inventoryPurchaseDateVatBridge ||
|
inventoryPurchaseDateVatBridge ||
|
||||||
|
inventoryMarginRankingFollowup ||
|
||||||
explicitSummaryBundleReuseSignal ||
|
explicitSummaryBundleReuseSignal ||
|
||||||
mcpDiscoveryOrganizationClarificationContinuation;
|
mcpDiscoveryOrganizationClarificationContinuation;
|
||||||
let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||||
|
|
@ -582,6 +623,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
businessOverviewBoundaryFollowupAlternate ||
|
businessOverviewBoundaryFollowupAlternate ||
|
||||||
inventoryShortFollowupAlternate ||
|
inventoryShortFollowupAlternate ||
|
||||||
inventoryPurchaseDateVatBridge ||
|
inventoryPurchaseDateVatBridge ||
|
||||||
|
inventoryMarginRankingFollowup ||
|
||||||
explicitSummaryBundleReuseSignal ||
|
explicitSummaryBundleReuseSignal ||
|
||||||
mcpDiscoveryOrganizationClarificationContinuation
|
mcpDiscoveryOrganizationClarificationContinuation
|
||||||
: false;
|
: false;
|
||||||
|
|
@ -622,6 +664,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
shortValueFlowRetargetAlternate ||
|
shortValueFlowRetargetAlternate ||
|
||||||
businessOverviewBoundaryFollowupPrimary ||
|
businessOverviewBoundaryFollowupPrimary ||
|
||||||
businessOverviewBoundaryFollowupAlternate ||
|
businessOverviewBoundaryFollowupAlternate ||
|
||||||
|
inventoryMarginRankingFollowup ||
|
||||||
deps.hasFollowupMarker(userMessage) ||
|
deps.hasFollowupMarker(userMessage) ||
|
||||||
deps.hasReferentialPointer(userMessage) ||
|
deps.hasReferentialPointer(userMessage) ||
|
||||||
(deps.toNonEmptyString(alternateMessage)
|
(deps.toNonEmptyString(alternateMessage)
|
||||||
|
|
@ -645,6 +688,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
shortValueFlowRetargetAlternate ||
|
shortValueFlowRetargetAlternate ||
|
||||||
businessOverviewBoundaryFollowupPrimary ||
|
businessOverviewBoundaryFollowupPrimary ||
|
||||||
businessOverviewBoundaryFollowupAlternate ||
|
businessOverviewBoundaryFollowupAlternate ||
|
||||||
|
inventoryMarginRankingFollowup ||
|
||||||
deps.hasFollowupMarker(userMessage) ||
|
deps.hasFollowupMarker(userMessage) ||
|
||||||
deps.hasReferentialPointer(userMessage) ||
|
deps.hasReferentialPointer(userMessage) ||
|
||||||
(deps.toNonEmptyString(alternateMessage)
|
(deps.toNonEmptyString(alternateMessage)
|
||||||
|
|
@ -757,6 +801,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
!inventoryShortFollowupAlternate &&
|
!inventoryShortFollowupAlternate &&
|
||||||
!businessOverviewBoundaryFollowupPrimary &&
|
!businessOverviewBoundaryFollowupPrimary &&
|
||||||
!businessOverviewBoundaryFollowupAlternate &&
|
!businessOverviewBoundaryFollowupAlternate &&
|
||||||
|
!inventoryMarginRankingFollowup &&
|
||||||
!foreignAccountingPivotOverInventory &&
|
!foreignAccountingPivotOverInventory &&
|
||||||
!deps.hasFollowupMarker(userMessage) &&
|
!deps.hasFollowupMarker(userMessage) &&
|
||||||
!deps.hasReferentialPointer(userMessage) &&
|
!deps.hasReferentialPointer(userMessage) &&
|
||||||
|
|
@ -816,6 +861,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
businessOverviewBoundaryFollowupPrimary ||
|
businessOverviewBoundaryFollowupPrimary ||
|
||||||
inventoryShortFollowupPrimary ||
|
inventoryShortFollowupPrimary ||
|
||||||
inventoryPurchaseDateVatBridge ||
|
inventoryPurchaseDateVatBridge ||
|
||||||
|
inventoryMarginRankingFollowup ||
|
||||||
explicitSummaryBundleReuseSignal ||
|
explicitSummaryBundleReuseSignal ||
|
||||||
hasInventoryRootTemporalFollowupPrimary ||
|
hasInventoryRootTemporalFollowupPrimary ||
|
||||||
mcpDiscoveryOrganizationClarificationContinuation;
|
mcpDiscoveryOrganizationClarificationContinuation;
|
||||||
|
|
@ -827,6 +873,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
businessOverviewBoundaryFollowupAlternate ||
|
businessOverviewBoundaryFollowupAlternate ||
|
||||||
inventoryShortFollowupAlternate ||
|
inventoryShortFollowupAlternate ||
|
||||||
inventoryPurchaseDateVatBridge ||
|
inventoryPurchaseDateVatBridge ||
|
||||||
|
inventoryMarginRankingFollowup ||
|
||||||
explicitSummaryBundleReuseSignal ||
|
explicitSummaryBundleReuseSignal ||
|
||||||
hasInventoryRootTemporalFollowupAlternate ||
|
hasInventoryRootTemporalFollowupAlternate ||
|
||||||
mcpDiscoveryOrganizationClarificationContinuation
|
mcpDiscoveryOrganizationClarificationContinuation
|
||||||
|
|
@ -849,6 +896,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
shortValueFlowRetargetAlternate ||
|
shortValueFlowRetargetAlternate ||
|
||||||
businessOverviewBoundaryFollowupPrimary ||
|
businessOverviewBoundaryFollowupPrimary ||
|
||||||
businessOverviewBoundaryFollowupAlternate ||
|
businessOverviewBoundaryFollowupAlternate ||
|
||||||
|
inventoryMarginRankingFollowup ||
|
||||||
deps.hasFollowupMarker(userMessage) ||
|
deps.hasFollowupMarker(userMessage) ||
|
||||||
deps.hasReferentialPointer(userMessage) ||
|
deps.hasReferentialPointer(userMessage) ||
|
||||||
(deps.toNonEmptyString(alternateMessage)
|
(deps.toNonEmptyString(alternateMessage)
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ const SUPPORTED_ADDRESS_INTENTS = new Set([
|
||||||
"inventory_purchase_documents_for_item",
|
"inventory_purchase_documents_for_item",
|
||||||
"inventory_supplier_stock_overlap_as_of_date",
|
"inventory_supplier_stock_overlap_as_of_date",
|
||||||
"inventory_sale_trace_for_item",
|
"inventory_sale_trace_for_item",
|
||||||
|
"inventory_margin_ranking_for_nomenclature",
|
||||||
"inventory_profitability_for_item",
|
"inventory_profitability_for_item",
|
||||||
"inventory_purchase_to_sale_chain",
|
"inventory_purchase_to_sale_chain",
|
||||||
"inventory_aging_by_purchase_date",
|
"inventory_aging_by_purchase_date",
|
||||||
|
|
@ -133,6 +134,72 @@ function detectCounterpartyTurnoverFamily(text) {
|
||||||
entity
|
entity
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function hasCompactOrganizationCashflowDisplaySignal(text) {
|
||||||
|
const normalized = String(text ?? "");
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasCompactCue = /(?:\u043a\u043e\u0440\u043e\u0442\u043a\w*|\u043e\u0434\u043d\u043e\u0439\s+\u0441\u0442\u0440\u043e\u043a\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0438\u0442\u043e\u0433\p{L}*|\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}*|\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?\b|\u0431\u0435\u0437\s+\u0441\u043f\u0438\u0441(?:\u043a\p{L}*)?|\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0431\u0438\u0432\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0440\u0435\u0437\p{L}*|\u0431\u0435\u0437\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\p{L}*)/iu.test(normalized);
|
||||||
|
if (!hasCompactCue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasMoneyCue = /(?:\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\u0435\u0436\p{L}*|\u043f\u0440\u0438\u0448\p{L}*|\u0443\u0448\p{L}*|\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*|\u0441\u043f\u0438\u0441\u0430\p{L}*|\u043d\u0435\u0442\u0442\u043e|cash|money|incoming|outgoing|net)/iu.test(normalized);
|
||||||
|
const hasOrganizationEarningsCue = /(?:\u0434\u0430\u0439|\u043f\u043e\u043a\u0430\u0436\p{L}*|\u0438\u0442\u043e\u0433\p{L}*|\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|\u0437\u0430\u0440\u0430\u0431\u043e\u0442\p{L}*|\u043f\u0440\u0438\u0448\p{L}*|\u0443\u0448\p{L}*|\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*|\u0434\u0435\u043d\u0435\u0436\p{L}*\s+\u043d\u0435\u0442\u0442\u043e|how\s+much|show|give|earned|received|paid)/iu.test(normalized);
|
||||||
|
const hasExplicitExclusionCue = /(?:\u0438\u0441\u043a\u043b\u044e\u0447\p{L}*|\u0443\u0431\u0435\u0440\p{L}*|\u043a\u0440\u043e\u043c\u0435|exclude|excluding|without)[\s\S]{0,80}(?:\u043a\u0440\u0443\u043f\u043d\p{L}*|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\p{L}*|\u043a\u043b\u0438\u0435\u043d\u0442\p{L}*|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\p{L}*|\u043e\u043f\u0435\u0440\u0430\u0446\p{L}*|counterpart|customer|supplier|operation)/iu.test(normalized);
|
||||||
|
const hasSpecificCounterpartyScope = detectScopedCounterpartyEntity(normalized) !== null;
|
||||||
|
return hasMoneyCue && hasOrganizationEarningsCue && !hasExplicitExclusionCue && !hasSpecificCounterpartyScope;
|
||||||
|
}
|
||||||
|
function detectScopedCounterpartyEntity(text) {
|
||||||
|
const patterns = [
|
||||||
|
/(?:^|[\s,.;:!?])(?:\u043f\u043e|\u0443|\u0434\u043b\u044f|by|for)\s+(.+?)(?=$|[,.;:!?]|\s+(?:\u0437\u0430|\u043d\u0430|\u0432|\u0432\u043e|\u043a|\u043f\u043e|\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a|\u043a\u0430\u043a|\u043a\u0430\u043a\u043e\u0435|\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0430\u044f|\u043a\u0430\u043a\u0438\u0435|\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*|\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|\u0434\u0435\u043d\u0435\u0433|\u0434\u0435\u043d\u0435\u0436\p{L}*|\u043f\u043b\u0430\u0442[\u0435\u0451]\u0436\p{L}*|\u0438\u0441\u0445\u043e\u0434\p{L}*|\u0432\u0445\u043e\u0434\p{L}*)(?=$|[\s,.;:!?]))/iu,
|
||||||
|
/(?:^|[\s,.;:!?])(?:\u043f\u043e|\u0443|\u0434\u043b\u044f|by|for)\s+([\p{L}\d._-]{2,})(?=$|[\s,.;:!?])/iu
|
||||||
|
];
|
||||||
|
const ignored = new Set([
|
||||||
|
"\u0433\u043e\u0434",
|
||||||
|
"\u0433\u043e\u0434\u0430",
|
||||||
|
"\u043f\u0435\u0440\u0438\u043e\u0434",
|
||||||
|
"\u043f\u0435\u0440\u0438\u043e\u0434\u0430",
|
||||||
|
"\u043c\u0435\u0441\u044f\u0446",
|
||||||
|
"\u043c\u0435\u0441\u044f\u0446\u0430",
|
||||||
|
"\u043a\u0432\u0430\u0440\u0442\u0430\u043b",
|
||||||
|
"\u043a\u0432\u0430\u0440\u0442\u0430\u043b\u0430",
|
||||||
|
"\u0434\u0435\u043d\u044c\u0433\u0438",
|
||||||
|
"\u043d\u0435\u0442\u0442\u043e",
|
||||||
|
"\u0441\u0430\u043b\u044c\u0434\u043e",
|
||||||
|
"year",
|
||||||
|
"period",
|
||||||
|
"month",
|
||||||
|
"quarter",
|
||||||
|
"net"
|
||||||
|
]);
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const rawEntity = text.match(pattern)?.[1]?.trim() ?? "";
|
||||||
|
if (!rawEntity) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const entity = rawEntity.replace(/^["'«»]+|["'«»]+$/gu, "").trim();
|
||||||
|
if (entity.length >= 2 && !ignored.has(entity)) {
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function detectCounterpartyBidirectionalValueFlowFamily(text) {
|
||||||
|
const hasNetCue = /(?:\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|net\s+(?:flow|cash|payment)|cash\s+net)/iu.test(text);
|
||||||
|
const hasIncomingCue = /(?:\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0432\u0445\u043e\u0434\p{L}*|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*|received|incoming)/iu.test(text);
|
||||||
|
const hasOutgoingCue = /(?:\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*|\u0438\u0441\u0445\u043e\u0434\p{L}*|\u0441\u043f\u0438\u0441\u0430\u043d\p{L}*|paid|outgoing|payment)/iu.test(text);
|
||||||
|
if (!(hasNetCue || (hasIncomingCue && hasOutgoingCue))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const entity = detectScopedCounterpartyEntity(text);
|
||||||
|
if (!entity) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
family: "counterparty_bidirectional_value_flow_or_netting",
|
||||||
|
entity
|
||||||
|
};
|
||||||
|
}
|
||||||
function hasExplicitCounterpartyValueObject(text) {
|
function hasExplicitCounterpartyValueObject(text) {
|
||||||
return /(?:\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b|\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442|\u0442\u043e\u0432\u0430\u0440|\u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442\u0443\u0440|\u0441\u0434\u0435\u043b\u043a|customer|client|counterparty|supplier|vendor|contract|item|product|deal)/iu.test(text);
|
return /(?:\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b|\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442|\u0442\u043e\u0432\u0430\u0440|\u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442\u0443\u0440|\u0441\u0434\u0435\u043b\u043a|customer|client|counterparty|supplier|vendor|contract|item|product|deal)/iu.test(text);
|
||||||
}
|
}
|
||||||
|
|
@ -279,14 +346,14 @@ function detectBroadBusinessEvaluation(text) {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
function buildEntityCandidates(counterpartyTurnover) {
|
function buildEntityCandidates(entityFamily) {
|
||||||
if (!counterpartyTurnover?.entity) {
|
if (!entityFamily?.entity) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
type: "counterparty",
|
type: "counterparty",
|
||||||
value: counterpartyTurnover.entity,
|
value: entityFamily.entity,
|
||||||
source: "current_turn_loose_entity_tail"
|
source: "current_turn_loose_entity_tail"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
@ -298,29 +365,43 @@ function createAssistantTurnMeaningPolicy(deps = {}) {
|
||||||
const rawText = normalizeTurnText(rawMessage, deps);
|
const rawText = normalizeTurnText(rawMessage, deps);
|
||||||
const effectiveText = normalizeTurnText(effectiveMessage, deps);
|
const effectiveText = normalizeTurnText(effectiveMessage, deps);
|
||||||
const joinedText = fallbackCompactWhitespace(`${rawText} ${effectiveText}`);
|
const joinedText = fallbackCompactWhitespace(`${rawText} ${effectiveText}`);
|
||||||
const supportedIntent = detectSupportedIntent(joinedText, deps);
|
const compactOrganizationCashflowDisplay = hasCompactOrganizationCashflowDisplaySignal(rawText);
|
||||||
const counterpartyTurnover = detectCounterpartyTurnoverFamily(joinedText);
|
const supportedIntent = compactOrganizationCashflowDisplay ? null : detectSupportedIntent(joinedText, deps);
|
||||||
|
const counterpartyBidirectionalValueFlow = compactOrganizationCashflowDisplay ? null : detectCounterpartyBidirectionalValueFlowFamily(joinedText);
|
||||||
|
const counterpartyTurnover = compactOrganizationCashflowDisplay ? null : detectCounterpartyTurnoverFamily(joinedText);
|
||||||
const selectedObjectInventoryExact = hasSelectedObjectInventoryExactSignal(joinedText);
|
const selectedObjectInventoryExact = hasSelectedObjectInventoryExactSignal(joinedText);
|
||||||
const broadBusinessEvaluation = selectedObjectInventoryExact ? null : detectBroadBusinessEvaluation(joinedText);
|
const broadBusinessEvaluation = compactOrganizationCashflowDisplay
|
||||||
|
? { family: "broad_business_evaluation" }
|
||||||
|
: selectedObjectInventoryExact || counterpartyBidirectionalValueFlow?.family
|
||||||
|
? null
|
||||||
|
: detectBroadBusinessEvaluation(joinedText);
|
||||||
const llmIntent = toNonEmptyString(input?.llmPreDecomposeMeta?.predecomposeContract?.intent, deps);
|
const llmIntent = toNonEmptyString(input?.llmPreDecomposeMeta?.predecomposeContract?.intent, deps);
|
||||||
const explicitIntentCandidate = broadBusinessEvaluation?.family
|
const explicitIntentCandidate = broadBusinessEvaluation?.family
|
||||||
? null
|
? null
|
||||||
: supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null);
|
: supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null);
|
||||||
const unsupportedFamily = broadBusinessEvaluation?.family
|
const unsupportedFamily = broadBusinessEvaluation?.family
|
||||||
? broadBusinessEvaluation.family
|
? broadBusinessEvaluation.family
|
||||||
: !explicitIntentCandidate && counterpartyTurnover?.family
|
: !explicitIntentCandidate && counterpartyBidirectionalValueFlow?.family
|
||||||
? counterpartyTurnover.family
|
? counterpartyBidirectionalValueFlow.family
|
||||||
: null;
|
: !explicitIntentCandidate && counterpartyTurnover?.family
|
||||||
|
? counterpartyTurnover.family
|
||||||
|
: null;
|
||||||
const reasonCodes = [];
|
const reasonCodes = [];
|
||||||
if (supportedIntent?.reason) {
|
if (supportedIntent?.reason) {
|
||||||
reasonCodes.push(supportedIntent.reason);
|
reasonCodes.push(supportedIntent.reason);
|
||||||
}
|
}
|
||||||
|
if (counterpartyBidirectionalValueFlow?.family) {
|
||||||
|
reasonCodes.push("counterparty_bidirectional_value_flow_current_turn_signal");
|
||||||
|
}
|
||||||
if (counterpartyTurnover?.family) {
|
if (counterpartyTurnover?.family) {
|
||||||
reasonCodes.push("counterparty_turnover_current_turn_signal");
|
reasonCodes.push("counterparty_turnover_current_turn_signal");
|
||||||
}
|
}
|
||||||
if (broadBusinessEvaluation?.family) {
|
if (broadBusinessEvaluation?.family) {
|
||||||
reasonCodes.push("broad_business_evaluation_current_turn_signal");
|
reasonCodes.push("broad_business_evaluation_current_turn_signal");
|
||||||
}
|
}
|
||||||
|
if (compactOrganizationCashflowDisplay) {
|
||||||
|
reasonCodes.push("compact_cashflow_display_current_turn_signal");
|
||||||
|
}
|
||||||
if (rawText !== normalizeTurnText(rawMessage, { ...deps, repairAddressMojibake: (value) => String(value ?? "") })) {
|
if (rawText !== normalizeTurnText(rawMessage, { ...deps, repairAddressMojibake: (value) => String(value ?? "") })) {
|
||||||
reasonCodes.push("mojibake_repair_applied");
|
reasonCodes.push("mojibake_repair_applied");
|
||||||
}
|
}
|
||||||
|
|
@ -338,32 +419,39 @@ function createAssistantTurnMeaningPolicy(deps = {}) {
|
||||||
? "inventory"
|
? "inventory"
|
||||||
: broadBusinessEvaluation?.family
|
: broadBusinessEvaluation?.family
|
||||||
? "business_summary"
|
? "business_summary"
|
||||||
: explicitIntentCandidate?.includes("counterparty")
|
: counterpartyBidirectionalValueFlow?.family
|
||||||
? "counterparty"
|
? "counterparty_value"
|
||||||
: counterpartyTurnover?.family
|
: explicitIntentCandidate?.includes("counterparty")
|
||||||
? "counterparty"
|
? "counterparty"
|
||||||
: null;
|
: counterpartyTurnover?.family
|
||||||
|
? "counterparty"
|
||||||
|
: null;
|
||||||
const askedActionFamily = explicitIntentCandidate === "receivables_confirmed_as_of_date" ||
|
const askedActionFamily = explicitIntentCandidate === "receivables_confirmed_as_of_date" ||
|
||||||
explicitIntentCandidate === "payables_confirmed_as_of_date" ||
|
explicitIntentCandidate === "payables_confirmed_as_of_date" ||
|
||||||
explicitIntentCandidate === "inventory_on_hand_as_of_date"
|
explicitIntentCandidate === "inventory_on_hand_as_of_date"
|
||||||
? "confirmed_snapshot"
|
? "confirmed_snapshot"
|
||||||
: broadBusinessEvaluation?.family
|
: broadBusinessEvaluation?.family
|
||||||
? "broad_evaluation"
|
? "broad_evaluation"
|
||||||
: explicitIntentCandidate === "customer_revenue_and_payments" ||
|
: counterpartyBidirectionalValueFlow?.family
|
||||||
explicitIntentCandidate === "supplier_payouts_profile"
|
? "net_value_flow"
|
||||||
? "counterparty_value_or_turnover"
|
: explicitIntentCandidate === "customer_revenue_and_payments" ||
|
||||||
: explicitIntentCandidate === "vat_liability_confirmed_for_tax_period"
|
explicitIntentCandidate === "supplier_payouts_profile"
|
||||||
? "confirmed_tax_period"
|
? "counterparty_value_or_turnover"
|
||||||
: explicitIntentCandidate === "vat_payable_confirmed_as_of_date"
|
: explicitIntentCandidate === "vat_liability_confirmed_for_tax_period"
|
||||||
? "confirmed_snapshot"
|
? "confirmed_tax_period"
|
||||||
: explicitIntentCandidate === "vat_payable_forecast"
|
: explicitIntentCandidate === "vat_payable_confirmed_as_of_date"
|
||||||
? "forecast"
|
? "confirmed_snapshot"
|
||||||
: explicitIntentCandidate === "list_documents_by_counterparty"
|
: explicitIntentCandidate === "vat_payable_forecast"
|
||||||
? "list_documents"
|
? "forecast"
|
||||||
: counterpartyTurnover?.family
|
: explicitIntentCandidate === "list_documents_by_counterparty"
|
||||||
? "counterparty_value_or_turnover"
|
? "list_documents"
|
||||||
: null;
|
: counterpartyTurnover?.family
|
||||||
const staleReplayForbidden = Boolean(unsupportedFamily || broadBusinessEvaluation?.family || (counterpartyTurnover?.entity && !explicitIntentCandidate));
|
? "counterparty_value_or_turnover"
|
||||||
|
: null;
|
||||||
|
const staleReplayForbidden = Boolean(unsupportedFamily ||
|
||||||
|
broadBusinessEvaluation?.family ||
|
||||||
|
(counterpartyBidirectionalValueFlow?.entity && !explicitIntentCandidate) ||
|
||||||
|
(counterpartyTurnover?.entity && !explicitIntentCandidate));
|
||||||
return {
|
return {
|
||||||
schema_version: "assistant_turn_meaning_v1",
|
schema_version: "assistant_turn_meaning_v1",
|
||||||
raw_message: rawMessage,
|
raw_message: rawMessage,
|
||||||
|
|
@ -373,10 +461,13 @@ function createAssistantTurnMeaningPolicy(deps = {}) {
|
||||||
asked_domain_family: askedDomainFamily,
|
asked_domain_family: askedDomainFamily,
|
||||||
asked_action_family: askedActionFamily,
|
asked_action_family: askedActionFamily,
|
||||||
explicit_intent_candidate: explicitIntentCandidate,
|
explicit_intent_candidate: explicitIntentCandidate,
|
||||||
explicit_entity_candidates: broadBusinessEvaluation?.family ? [] : buildEntityCandidates(counterpartyTurnover),
|
explicit_entity_candidates: broadBusinessEvaluation?.family
|
||||||
|
? []
|
||||||
|
: buildEntityCandidates(counterpartyBidirectionalValueFlow ?? counterpartyTurnover),
|
||||||
meaning_confidence: broadBusinessEvaluation?.family
|
meaning_confidence: broadBusinessEvaluation?.family
|
||||||
? "medium"
|
? "medium"
|
||||||
: supportedIntent?.confidence ?? (counterpartyTurnover?.family ? "medium" : "low"),
|
: supportedIntent?.confidence ??
|
||||||
|
(counterpartyBidirectionalValueFlow?.family || counterpartyTurnover?.family ? "medium" : "low"),
|
||||||
intent_override_strength: explicitIntentCandidate
|
intent_override_strength: explicitIntentCandidate
|
||||||
? "explicit_current_turn_intent"
|
? "explicit_current_turn_intent"
|
||||||
: staleReplayForbidden
|
: staleReplayForbidden
|
||||||
|
|
|
||||||
|
|
@ -198,21 +198,23 @@ function loadCapabilitiesRegistry() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function buildCapabilityContractReplyFromRegistry() {
|
function buildCapabilityContractReplyFromRegistry() {
|
||||||
const registry = loadCapabilitiesRegistry();
|
|
||||||
const topGroups = registry.groups.filter((group) => group.group_code !== "boundaries").slice(0, 6);
|
|
||||||
const groupLines = topGroups.map((group, index) => {
|
|
||||||
const examples = group.typical_queries
|
|
||||||
.slice(0, 2)
|
|
||||||
.map((query) => query.trim())
|
|
||||||
.filter((query) => query.length > 0)
|
|
||||||
.join("; ");
|
|
||||||
return `${index + 1}. ${group.group_title}: ${group.description}${examples ? `. Например: ${examples}` : "."}`;
|
|
||||||
});
|
|
||||||
return [
|
return [
|
||||||
"Могу помочь с вопросами по данным 1С в режиме чтения: НДС, контрагенты, долги, деньги и склад.",
|
"Могу быстро смотреть управленческие вещи по данным 1С в режиме чтения.",
|
||||||
"По основным группам:",
|
"",
|
||||||
...groupLines,
|
"- кто должен денег и кому должны;",
|
||||||
"Если нужно, подскажу, как лучше сформулировать запрос под вашу задачу.",
|
"- какой год или месяц был самым денежным;",
|
||||||
|
"- какие контрагенты дают основной поток;",
|
||||||
|
"- что лежит на складе и какие остатки стареют;",
|
||||||
|
"- сколько НДС к уплате за период;",
|
||||||
|
"- какие документы, оплаты и договоры есть по контрагенту.",
|
||||||
|
"",
|
||||||
|
"Примеры запросов:",
|
||||||
|
"- кто самый доходный клиент за все время",
|
||||||
|
"- что зависло на складе",
|
||||||
|
"- кому мы должны на сегодня",
|
||||||
|
"- какое нетто по СВК за 2020",
|
||||||
|
"- сколько НДС к уплате за 4 квартал 2019",
|
||||||
|
"",
|
||||||
"Что не делаю: не настраиваю 1С, не меняю конфигурацию, не создаю и не провожу документы, не выполняю админ-действия на сервере."
|
"Что не делаю: не настраиваю 1С, не меняю конфигурацию, не создаю и не провожу документы, не выполняю админ-действия на сервере."
|
||||||
].join("\n");
|
].join("\n");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ function toRouteHintSummaryV1(normalized) {
|
||||||
}
|
}
|
||||||
const ACCOUNT_HINT_PATTERN = /(?:\b(?:account|acct|schet|счет|сч)\s*[:#]?\s*(?:[1-9][0-9](?:[./-][0-9]{1,2})?)|\b(?:19|20|21|23|25|26|28|29|44|51|60|62|68)\b)/i;
|
const ACCOUNT_HINT_PATTERN = /(?:\b(?:account|acct|schet|счет|сч)\s*[:#]?\s*(?:[1-9][0-9](?:[./-][0-9]{1,2})?)|\b(?:19|20|21|23|25|26|28|29|44|51|60|62|68)\b)/i;
|
||||||
const PERIOD_PATTERN = /\b20\d{2}(?:[-./](?:0[1-9]|1[0-2]))?\b/i;
|
const PERIOD_PATTERN = /\b20\d{2}(?:[-./](?:0[1-9]|1[0-2]))?\b/i;
|
||||||
|
const BIDIRECTIONAL_VALUE_FLOW_PATTERN = /(?:\b(?:receive(?:d)?|received|get|got|incoming|inflow|paid|payment|payments|outgoing|outflow|net|netto|cash\s*flow)\b|получ(?:ить|ил[аи]?|ено|аем|или)|поступ(?:ил[аи]?|ление|ления)|заплат(?:ить|ил[аи]?|или)|оплат(?:ить|ил[аи]?|ы|или)|входящ(?:ий|ие|их)|исходящ(?:ий|ие|их)|нетто|сальдо)/iu;
|
||||||
|
const COUNTERPARTY_SCOPE_PATTERN = /(?:\b(?:counterparty|supplier|customer|vendor|client|bank)\b|контрагент|поставщик|покупател|клиент|заказчик|банк|сбербанк|по\s+(?:ип|ооо|пао|зао|оао|группа)\b)/iu;
|
||||||
const SYMPTOM_MARKER_PATTERN = /(?:\bsymptom\b|\banomaly\b|\bproblem\b|\bissue\b|\btail\b|\bhanging\b|\bblocked\b|\bincomplete\b|remains?\s+open|not\s+(?:confirmed|observed|resolved|closed)|не\s+(?:подтвержден|закрыт|наблюдается)|хвост|сбой|проблем)/i;
|
const SYMPTOM_MARKER_PATTERN = /(?:\bsymptom\b|\banomaly\b|\bproblem\b|\bissue\b|\btail\b|\bhanging\b|\bblocked\b|\bincomplete\b|remains?\s+open|not\s+(?:confirmed|observed|resolved|closed)|не\s+(?:подтвержден|закрыт|наблюдается)|хвост|сбой|проблем)/i;
|
||||||
const LIFECYCLE_MARKER_PATTERN = /(?:\blifecycle\b|\bchain\b|\btransition\b|\bstep\b|\btrace\b|цепоч|этап|переход|связк|где\s+разрыв)/i;
|
const LIFECYCLE_MARKER_PATTERN = /(?:\blifecycle\b|\bchain\b|\btransition\b|\bstep\b|\btrace\b|цепоч|этап|переход|связк|где\s+разрыв)/i;
|
||||||
const CHAIN_BREAK_PATTERN = /(?:\bbreak\b|\bbroken\b|\bgap\b|missing\s+(?:transition|step|link)|chain\s+break|разрыв|обрыв|нет\s+переход|не\s+дошл|не\s+наблюд)/i;
|
const CHAIN_BREAK_PATTERN = /(?:\bbreak\b|\bbroken\b|\bgap\b|missing\s+(?:transition|step|link)|chain\s+break|разрыв|обрыв|нет\s+переход|не\s+дошл|не\s+наблюд)/i;
|
||||||
|
|
@ -54,6 +56,13 @@ exports.ROUTE_DISCIPLINE_RULE_TABLE = [
|
||||||
forbidden_fallback: ["store_canonical", "hybrid_store_plus_live"],
|
forbidden_fallback: ["store_canonical", "hybrid_store_plus_live"],
|
||||||
description: "Ranking and period summary queries require analytical batch path."
|
description: "Ranking and period summary queries require analytical batch path."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
query_class: "bidirectional_value_flow",
|
||||||
|
required_route: "hybrid_store_plus_live",
|
||||||
|
allowed_fallback: ["no_route"],
|
||||||
|
forbidden_fallback: ["store_canonical"],
|
||||||
|
description: "Scoped bidirectional value-flow questions require hybrid evidence path."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
query_class: "symptom_first",
|
query_class: "symptom_first",
|
||||||
required_route: "hybrid_store_plus_live",
|
required_route: "hybrid_store_plus_live",
|
||||||
|
|
@ -155,6 +164,17 @@ function hasAmbiguitySignal(fragment, lowerText) {
|
||||||
function hasAccountOrPeriodAnchor(fragment, lowerText) {
|
function hasAccountOrPeriodAnchor(fragment, lowerText) {
|
||||||
return fragment.account_hints.length > 0 || ACCOUNT_HINT_PATTERN.test(lowerText) || PERIOD_PATTERN.test(lowerText);
|
return fragment.account_hints.length > 0 || ACCOUNT_HINT_PATTERN.test(lowerText) || PERIOD_PATTERN.test(lowerText);
|
||||||
}
|
}
|
||||||
|
function hasBidirectionalValueFlowSignal(fragment, lowerText) {
|
||||||
|
if (fragment.flags.asks_for_ranking_or_top || fragment.flags.asks_for_period_summary) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return BIDIRECTIONAL_VALUE_FLOW_PATTERN.test(lowerText);
|
||||||
|
}
|
||||||
|
function hasCounterpartyScopeSignal(fragment, lowerText) {
|
||||||
|
return (COUNTERPARTY_SCOPE_PATTERN.test(lowerText) ||
|
||||||
|
fragment.entity_hints.some((hint) => hint.trim().length > 0) ||
|
||||||
|
fragment.candidate_labels.includes("cross_entity"));
|
||||||
|
}
|
||||||
function resolveRouteClass(fragment) {
|
function resolveRouteClass(fragment) {
|
||||||
const lowerText = mergedFragmentText(fragment);
|
const lowerText = mergedFragmentText(fragment);
|
||||||
const symptomSignal = hasSymptomSignal(fragment, lowerText);
|
const symptomSignal = hasSymptomSignal(fragment, lowerText);
|
||||||
|
|
@ -164,12 +184,17 @@ function resolveRouteClass(fragment) {
|
||||||
const causalSignal = hasCausalSignal(lowerText);
|
const causalSignal = hasCausalSignal(lowerText);
|
||||||
const ambiguitySignal = hasAmbiguitySignal(fragment, lowerText);
|
const ambiguitySignal = hasAmbiguitySignal(fragment, lowerText);
|
||||||
const accountOrPeriodAnchor = hasAccountOrPeriodAnchor(fragment, lowerText);
|
const accountOrPeriodAnchor = hasAccountOrPeriodAnchor(fragment, lowerText);
|
||||||
|
const bidirectionalValueFlowSignal = hasBidirectionalValueFlowSignal(fragment, lowerText);
|
||||||
|
const counterpartyScopeSignal = hasCounterpartyScopeSignal(fragment, lowerText);
|
||||||
if (fragment.flags.asks_for_exact_object_trace) {
|
if (fragment.flags.asks_for_exact_object_trace) {
|
||||||
return ROUTE_DISCIPLINE_RULE_MAP.get("exact_object_trace");
|
return ROUTE_DISCIPLINE_RULE_MAP.get("exact_object_trace");
|
||||||
}
|
}
|
||||||
if (fragment.flags.asks_for_ranking_or_top || fragment.flags.asks_for_period_summary) {
|
if (fragment.flags.asks_for_ranking_or_top || fragment.flags.asks_for_period_summary) {
|
||||||
return ROUTE_DISCIPLINE_RULE_MAP.get("ranking_or_period_summary");
|
return ROUTE_DISCIPLINE_RULE_MAP.get("ranking_or_period_summary");
|
||||||
}
|
}
|
||||||
|
if (bidirectionalValueFlowSignal && counterpartyScopeSignal) {
|
||||||
|
return ROUTE_DISCIPLINE_RULE_MAP.get("bidirectional_value_flow");
|
||||||
|
}
|
||||||
if (ambiguitySignal && (symptomSignal || lifecycleSignal || chainBreakSignal || periodImpactSignal || causalSignal)) {
|
if (ambiguitySignal && (symptomSignal || lifecycleSignal || chainBreakSignal || periodImpactSignal || causalSignal)) {
|
||||||
return ROUTE_DISCIPLINE_RULE_MAP.get("mixed_ambiguity");
|
return ROUTE_DISCIPLINE_RULE_MAP.get("mixed_ambiguity");
|
||||||
}
|
}
|
||||||
|
|
@ -205,7 +230,8 @@ function shouldPromoteFromNoRoute(fragment, rule) {
|
||||||
hasLifecycleSignal(fragment, lowerText) ||
|
hasLifecycleSignal(fragment, lowerText) ||
|
||||||
hasChainBreakSignal(lowerText) ||
|
hasChainBreakSignal(lowerText) ||
|
||||||
hasPeriodImpactSignal(lowerText) ||
|
hasPeriodImpactSignal(lowerText) ||
|
||||||
hasCausalSignal(lowerText);
|
hasCausalSignal(lowerText) ||
|
||||||
|
(hasBidirectionalValueFlowSignal(fragment, lowerText) && hasCounterpartyScopeSignal(fragment, lowerText));
|
||||||
const hasAnchor = hasAccountOrPeriodAnchor(fragment, lowerText) ||
|
const hasAnchor = hasAccountOrPeriodAnchor(fragment, lowerText) ||
|
||||||
fragment.candidate_labels.includes("cross_entity") ||
|
fragment.candidate_labels.includes("cross_entity") ||
|
||||||
DOMAIN_LEXICAL_ANCHOR_PATTERN.test(lowerText);
|
DOMAIN_LEXICAL_ANCHOR_PATTERN.test(lowerText);
|
||||||
|
|
|
||||||
|
|
@ -1287,7 +1287,8 @@ function loadSessionDialog(runId: string, caseId: string): {
|
||||||
text: toStringSafe(item.text) ?? "",
|
text: toStringSafe(item.text) ?? "",
|
||||||
created_at: toStringSafe(item.created_at),
|
created_at: toStringSafe(item.created_at),
|
||||||
trace_id: toStringSafe(item.trace_id),
|
trace_id: toStringSafe(item.trace_id),
|
||||||
reply_type: toStringSafe(item.reply_type)
|
reply_type: toStringSafe(item.reply_type),
|
||||||
|
debug: item.debug ?? null
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const turns = toArray(record.turns)
|
const turns = toArray(record.turns)
|
||||||
|
|
@ -1364,7 +1365,8 @@ function buildFallbackDialog(run: IndexedRun, caseId: string): {
|
||||||
text: userText,
|
text: userText,
|
||||||
created_at: null,
|
created_at: null,
|
||||||
trace_id: null,
|
trace_id: null,
|
||||||
reply_type: null
|
reply_type: null,
|
||||||
|
debug: null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message_id: null,
|
message_id: null,
|
||||||
|
|
@ -1372,7 +1374,8 @@ function buildFallbackDialog(run: IndexedRun, caseId: string): {
|
||||||
text: assistantSummaryParts.join("\n"),
|
text: assistantSummaryParts.join("\n"),
|
||||||
created_at: null,
|
created_at: null,
|
||||||
trace_id: toStringSafe(targetCase.trace_id),
|
trace_id: toStringSafe(targetCase.trace_id),
|
||||||
reply_type: toStringSafe(targetCase.reply_type)
|
reply_type: toStringSafe(targetCase.reply_type),
|
||||||
|
debug: null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
decomposition: [],
|
decomposition: [],
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ const COMPUTE_EXACT_INTENTS = new Set<AddressIntent>([
|
||||||
"inventory_purchase_documents_for_item",
|
"inventory_purchase_documents_for_item",
|
||||||
"inventory_supplier_stock_overlap_as_of_date",
|
"inventory_supplier_stock_overlap_as_of_date",
|
||||||
"inventory_sale_trace_for_item",
|
"inventory_sale_trace_for_item",
|
||||||
|
"inventory_margin_ranking_for_nomenclature",
|
||||||
"inventory_profitability_for_item",
|
"inventory_profitability_for_item",
|
||||||
"inventory_purchase_to_sale_chain",
|
"inventory_purchase_to_sale_chain",
|
||||||
"inventory_aging_by_purchase_date",
|
"inventory_aging_by_purchase_date",
|
||||||
|
|
@ -92,6 +93,7 @@ function defaultCapabilityId(intent: AddressIntent): string {
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date"
|
intent === "inventory_aging_by_purchase_date"
|
||||||
|
|
@ -178,13 +180,17 @@ function resolveCapabilityEnabled(intent: AddressIntent): { enabled: boolean; re
|
||||||
intent === "inventory_purchase_provenance_for_item" ||
|
intent === "inventory_purchase_provenance_for_item" ||
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain"
|
intent === "inventory_purchase_to_sale_chain"
|
||||||
) {
|
) {
|
||||||
if (intent === "inventory_profitability_for_item") {
|
if (intent === "inventory_profitability_for_item" || intent === "inventory_margin_ranking_for_nomenclature") {
|
||||||
return {
|
return {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
reason: "inventory_profitability_route_enabled"
|
reason:
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature"
|
||||||
|
? "inventory_margin_ranking_route_enabled"
|
||||||
|
: "inventory_profitability_route_enabled"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (intent === "inventory_purchase_to_sale_chain") {
|
if (intent === "inventory_purchase_to_sale_chain") {
|
||||||
|
|
@ -284,6 +290,7 @@ export function resolveShadowRouteIntent(
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date"
|
intent === "inventory_aging_by_purchase_date"
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,7 @@ export function isConfirmedBalanceIntent(intent: AddressIntent): boolean {
|
||||||
intent === "inventory_purchase_provenance_for_item" ||
|
intent === "inventory_purchase_provenance_for_item" ||
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "open_contracts_confirmed_as_of_date" ||
|
intent === "open_contracts_confirmed_as_of_date" ||
|
||||||
|
|
|
||||||
|
|
@ -1111,6 +1111,7 @@ function isInventoryTraceIntent(intent: AddressIntent): boolean {
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date"
|
intent === "inventory_aging_by_purchase_date"
|
||||||
|
|
@ -1135,6 +1136,7 @@ function usesRecipeDefaultLimit(intent: AddressIntent): boolean {
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date"
|
intent === "inventory_aging_by_purchase_date"
|
||||||
|
|
@ -1628,6 +1630,9 @@ function requiredFiltersByIntent(intent: AddressIntent): Array<keyof AddressFilt
|
||||||
) {
|
) {
|
||||||
return ["item"];
|
return ["item"];
|
||||||
}
|
}
|
||||||
|
if (intent === "inventory_margin_ranking_for_nomenclature") {
|
||||||
|
return ["period_from", "period_to"];
|
||||||
|
}
|
||||||
if (intent === "payables_confirmed_as_of_date") {
|
if (intent === "payables_confirmed_as_of_date") {
|
||||||
return ["as_of_date"];
|
return ["as_of_date"];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2152,6 +2152,28 @@ function hasBidirectionalValueFlowComparisonSignal(text: string): boolean {
|
||||||
return hasIncomingCue && hasOutgoingCue && hasComparisonCue && (hasValueFlowCue || hasNetAmountCue);
|
return hasIncomingCue && hasOutgoingCue && hasComparisonCue && (hasValueFlowCue || hasNetAmountCue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasNomenclatureMarginRankingSignal(text: string): boolean {
|
||||||
|
const normalized = String(text ?? "").trim().toLowerCase();
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasNomenclatureCue =
|
||||||
|
/(?:номенклатур|товар|позици|ассортимент|sku|item|product|goods)/iu.test(normalized);
|
||||||
|
const hasRealizationCue =
|
||||||
|
/(?:реализован|реализац|продан|продаж|отгруж|41(?:[.,]0?1)?|90(?:[.,]\d{1,2})?|sales?|sold)/iu.test(
|
||||||
|
normalized
|
||||||
|
);
|
||||||
|
const hasMarginCue =
|
||||||
|
/(?:прибыл|марж|рентаб|наценк|себестоим|выручк|profit|margin|profitability|gross\s+spread|cogs)/iu.test(
|
||||||
|
normalized
|
||||||
|
);
|
||||||
|
const hasRankingCue =
|
||||||
|
/(?:высок|низк|топ|сам(?:ая|ый|ое|ые)|больш|меньш|ранж|рейтинг|high|low|top|rank|best|worst)/iu.test(
|
||||||
|
normalized
|
||||||
|
);
|
||||||
|
return hasNomenclatureCue && hasRealizationCue && hasMarginCue && hasRankingCue;
|
||||||
|
}
|
||||||
|
|
||||||
function hasVatPeriodInspectionBridgeSignal(text: string): boolean {
|
function hasVatPeriodInspectionBridgeSignal(text: string): boolean {
|
||||||
const normalized = String(text ?? "").trim().toLowerCase();
|
const normalized = String(text ?? "").trim().toLowerCase();
|
||||||
if (!/(?:ндс|vat)/iu.test(normalized)) {
|
if (!/(?:ндс|vat)/iu.test(normalized)) {
|
||||||
|
|
@ -2263,6 +2285,14 @@ function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolutio
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasNomenclatureMarginRankingSignal(normalized)) {
|
||||||
|
return unicodeBridgeResolution(
|
||||||
|
"inventory_margin_ranking_for_nomenclature",
|
||||||
|
"high",
|
||||||
|
"unicode_nomenclature_margin_ranking_bridge_signal_detected"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const hasOpenItemsAccountCue =
|
const hasOpenItemsAccountCue =
|
||||||
/(?:хвост|долг|незакрыт|вис)/iu.test(normalized) &&
|
/(?:хвост|долг|незакрыт|вис)/iu.test(normalized) &&
|
||||||
/(?:сч(?:е|ё)т(?:а|у|ом|е|ов)?\s*(?:№|#)?\s*(?:60|62|76)(?:[.,]\d{1,2})?|\b(?:60|62|76)(?:[.,]\d{1,2})?\b\s*сч(?:е|ё)т)/iu.test(
|
/(?:сч(?:е|ё)т(?:а|у|ом|е|ов)?\s*(?:№|#)?\s*(?:60|62|76)(?:[.,]\d{1,2})?|\b(?:60|62|76)(?:[.,]\d{1,2})?\b\s*сч(?:е|ё)т)/iu.test(
|
||||||
|
|
|
||||||
|
|
@ -2034,6 +2034,7 @@ function isOrganizationScopedInventoryIntent(intent: AddressIntent): boolean {
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date"
|
intent === "inventory_aging_by_purchase_date"
|
||||||
|
|
@ -2085,6 +2086,7 @@ function shouldDeferInventoryOrganizationClarification(
|
||||||
intent === "inventory_purchase_provenance_for_item" ||
|
intent === "inventory_purchase_provenance_for_item" ||
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date"
|
intent === "inventory_aging_by_purchase_date"
|
||||||
|
|
@ -2455,6 +2457,7 @@ function canAutoBroadenPeriodWindow(intent: AddressIntent, filters: AddressFilte
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date"
|
intent === "inventory_aging_by_purchase_date"
|
||||||
|
|
@ -2467,6 +2470,7 @@ function shouldBoostAutoBroadenedLimit(intent: AddressIntent): boolean {
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date"
|
intent === "inventory_aging_by_purchase_date"
|
||||||
|
|
@ -3132,6 +3136,9 @@ async function tryComposeLlmLimitedReply(input: {
|
||||||
if (process.env.VITEST === "true" || process.env.NODE_ENV === "test") {
|
if (process.env.VITEST === "true" || process.env.NODE_ENV === "test") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (input.intent === "inventory_margin_ranking_for_nomenclature" && input.category === "missing_anchor") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (!shouldUseLlmLimitedReply(input.category)) {
|
if (!shouldUseLlmLimitedReply(input.category)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -3206,6 +3213,13 @@ function composeLimitedReply(input: {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const missingAnchorPhrase = missingAnchorLabels.length > 0 ? missingAnchorLabels.join(", ") : "контрагент, договор, счет или период";
|
const missingAnchorPhrase = missingAnchorLabels.length > 0 ? missingAnchorLabels.join(", ") : "контрагент, договор, счет или период";
|
||||||
|
if (input.intent === "inventory_margin_ranking_for_nomenclature" && input.category === "missing_anchor") {
|
||||||
|
return [
|
||||||
|
"Для рейтинга прибыльности номенклатуры нужен период.",
|
||||||
|
"Могу посчитать по номенклатуре: выручку без НДС, себестоимость реализации, валовую прибыль и маржинальность.",
|
||||||
|
"Уточните период: месяц, квартал, год или весь доступный период."
|
||||||
|
].join("\n\n");
|
||||||
|
}
|
||||||
const heading =
|
const heading =
|
||||||
input.category === "empty_match"
|
input.category === "empty_match"
|
||||||
? pickDeterministicVariant(headingSeed, [
|
? pickDeterministicVariant(headingSeed, [
|
||||||
|
|
|
||||||
|
|
@ -992,6 +992,17 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
|
||||||
account_scope_mode: "strict",
|
account_scope_mode: "strict",
|
||||||
query_template: "inventory_trading_margin_proxy_profile"
|
query_template: "inventory_trading_margin_proxy_profile"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
recipe_id: "address_inventory_margin_ranking_for_nomenclature_v1",
|
||||||
|
intent: "inventory_margin_ranking_for_nomenclature",
|
||||||
|
purpose: "Rank realized nomenclature by bounded gross margin proxy for an explicit period using 41.01 purchase and sale document rows",
|
||||||
|
required_filters: ["period_from", "period_to"],
|
||||||
|
optional_filters: ["organization", "warehouse", "limit", "sort"],
|
||||||
|
default_limit: 800,
|
||||||
|
account_scope: ["41.01"],
|
||||||
|
account_scope_mode: "strict",
|
||||||
|
query_template: "inventory_margin_ranking_profile"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
recipe_id: "address_inventory_purchase_to_sale_chain_v1",
|
recipe_id: "address_inventory_purchase_to_sale_chain_v1",
|
||||||
intent: "inventory_purchase_to_sale_chain",
|
intent: "inventory_purchase_to_sale_chain",
|
||||||
|
|
@ -1928,6 +1939,7 @@ function maxLimitForIntent(intent: AddressIntent): number {
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
intent === "inventory_trading_margin_proxy_for_organization" ||
|
intent === "inventory_trading_margin_proxy_for_organization" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date" ||
|
intent === "inventory_aging_by_purchase_date" ||
|
||||||
|
|
@ -2182,6 +2194,8 @@ export function buildAddressRecipePlan(
|
||||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||||
: recipe.query_template === "inventory_trading_margin_proxy_profile"
|
: recipe.query_template === "inventory_trading_margin_proxy_profile"
|
||||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||||
|
: recipe.query_template === "inventory_margin_ranking_profile"
|
||||||
|
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||||
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
||||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||||
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
||||||
|
|
|
||||||
|
|
@ -237,16 +237,6 @@ function formatMoneyRub(value: number): string {
|
||||||
return `${formatNumberWithDots(value, 2)} ₽`;
|
return `${formatNumberWithDots(value, 2)} ₽`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatVatProbeStatusRu(status: VatDirectSourceProbeItem["status"]): string {
|
|
||||||
if (status === "ok") {
|
|
||||||
return "есть движения";
|
|
||||||
}
|
|
||||||
if (status === "empty") {
|
|
||||||
return "движения не найдены";
|
|
||||||
}
|
|
||||||
return "ошибка запроса";
|
|
||||||
}
|
|
||||||
|
|
||||||
function emphasizeNumericTokens(line: string): string {
|
function emphasizeNumericTokens(line: string): string {
|
||||||
if (!line) {
|
if (!line) {
|
||||||
return line;
|
return line;
|
||||||
|
|
@ -524,6 +514,73 @@ function bankOperationEvidenceLine(
|
||||||
return `Основание 1С: ${parts.join("; ")}.`;
|
return `Основание 1С: ${parts.join("; ")}.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BankOperationSemanticBucket =
|
||||||
|
| "commission"
|
||||||
|
| "deposit_or_credit"
|
||||||
|
| "tax_or_budget"
|
||||||
|
| "transfer_or_return"
|
||||||
|
| "other";
|
||||||
|
|
||||||
|
function classifyBankOperationSemanticBucket(row: ComposeStageRow): BankOperationSemanticBucket {
|
||||||
|
const text = [
|
||||||
|
row.registrator,
|
||||||
|
row.operation_kind,
|
||||||
|
row.payment_purpose,
|
||||||
|
row.contract,
|
||||||
|
row.comment
|
||||||
|
]
|
||||||
|
.map((item) => String(item ?? "").toLowerCase())
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
if (/(?:комисс|тариф|эквайр|обслуживан)/iu.test(text)) {
|
||||||
|
return "commission";
|
||||||
|
}
|
||||||
|
if (/(?:депозит|кредит|займ|овердрафт|процент|ссуд)/iu.test(text)) {
|
||||||
|
return "deposit_or_credit";
|
||||||
|
}
|
||||||
|
if (/(?:налог|ндс|взнос|бюджет|фнс|пфр|страхов)/iu.test(text)) {
|
||||||
|
return "tax_or_budget";
|
||||||
|
}
|
||||||
|
if (/(?:возврат|перевод|перечислен|переброс|пополн|инкасс|перенос)/iu.test(text)) {
|
||||||
|
return "transfer_or_return";
|
||||||
|
}
|
||||||
|
return "other";
|
||||||
|
}
|
||||||
|
|
||||||
|
function bankOperationSemanticBucketLabel(bucket: BankOperationSemanticBucket): string {
|
||||||
|
if (bucket === "commission") {
|
||||||
|
return "комиссии и банковское обслуживание";
|
||||||
|
}
|
||||||
|
if (bucket === "deposit_or_credit") {
|
||||||
|
return "депозиты, кредиты или проценты";
|
||||||
|
}
|
||||||
|
if (bucket === "tax_or_budget") {
|
||||||
|
return "налоги и бюджетные платежи";
|
||||||
|
}
|
||||||
|
if (bucket === "transfer_or_return") {
|
||||||
|
return "переводы, возвраты или перебросы";
|
||||||
|
}
|
||||||
|
return "прочие банковские операции";
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeBankOperationSemantics(rows: ComposeStageRow[]): string | null {
|
||||||
|
if (rows.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const counts = new Map<BankOperationSemanticBucket, number>();
|
||||||
|
for (const row of rows) {
|
||||||
|
const bucket = classifyBankOperationSemanticBucket(row);
|
||||||
|
counts.set(bucket, (counts.get(bucket) ?? 0) + 1);
|
||||||
|
}
|
||||||
|
const ranked = Array.from(counts.entries())
|
||||||
|
.sort((left, right) => right[1] - left[1])
|
||||||
|
.slice(0, 3);
|
||||||
|
if (ranked.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const parts = ranked.map(([bucket, count]) => `${bankOperationSemanticBucketLabel(bucket)} — ${count}`);
|
||||||
|
return `По смыслу это скорее финансовый/банковский контур: ${parts.join("; ")}.`;
|
||||||
|
}
|
||||||
function bankRoleBoundaryLine(userMessage: string | null | undefined, rows: ComposeStageRow[]): string | null {
|
function bankRoleBoundaryLine(userMessage: string | null | undefined, rows: ComposeStageRow[]): string | null {
|
||||||
const incomingBoundary = hasBankIncomingRoleBoundaryQuestion(userMessage);
|
const incomingBoundary = hasBankIncomingRoleBoundaryQuestion(userMessage);
|
||||||
const outgoingBoundary = hasBankOutgoingRoleBoundaryQuestion(userMessage);
|
const outgoingBoundary = hasBankOutgoingRoleBoundaryQuestion(userMessage);
|
||||||
|
|
@ -2666,29 +2723,24 @@ function formatPayablesEvidenceSuffix(item: PayablesConfirmedBalanceAggregate):
|
||||||
const suffix = item.documents.length > 1 ? ` (+${item.documents.length - 1})` : "";
|
const suffix = item.documents.length > 1 ? ` (+${item.documents.length - 1})` : "";
|
||||||
parts.push(`документ: ${item.documents[0]}${suffix}`);
|
parts.push(`документ: ${item.documents[0]}${suffix}`);
|
||||||
}
|
}
|
||||||
if (item.sourceRefs.length > 0) {
|
|
||||||
parts.push(`source refs: ${item.sourceRefs.slice(0, 2).join("; ")}`);
|
|
||||||
}
|
|
||||||
return parts.length > 0 ? ` | ${parts.join(" | ")}` : "";
|
return parts.length > 0 ? ` | ${parts.join(" | ")}` : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDebtMirrorGroupLine(item: DebtMirrorBalanceGroup): string {
|
function formatDebtMirrorGroupLine(item: DebtMirrorBalanceGroup): string {
|
||||||
const details = [
|
|
||||||
item.account ? `счет ${item.account}` : null,
|
|
||||||
item.contract ? `договор/аналитика: ${item.contract}` : null,
|
|
||||||
item.organization ? `организация: ${item.organization}` : null
|
|
||||||
].filter((part): part is string => Boolean(part));
|
|
||||||
const netText =
|
const netText =
|
||||||
Math.abs(item.netAmount) <= 0.005
|
Math.abs(item.netAmount) <= 0.005
|
||||||
? "чисто: 0 ₽"
|
? "чисто: 0 ₽"
|
||||||
: item.netAmount > 0
|
: item.netAmount > 0
|
||||||
? `чисто к получению: ${formatMoneyRub(item.netAmount)}`
|
? `чисто к получению: ${formatMoneyRub(item.netAmount)}`
|
||||||
: `чисто к оплате: ${formatMoneyRub(Math.abs(item.netAmount))}`;
|
: `чисто к оплате: ${formatMoneyRub(Math.abs(item.netAmount))}`;
|
||||||
return `${item.name}${details.length > 0 ? ` (${details.join(", ")})` : ""}: дебет ${formatMoneyRub(item.debitAmount)} / кредит ${formatMoneyRub(item.creditAmount)}, ${netText}.`;
|
return `${item.name}: встречная часть ${formatMoneyRub(Math.min(item.debitAmount, item.creditAmount))}, ${netText}.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function debtMirrorCleanScopeLabel(kind: "payables" | "receivables"): string {
|
function debtMirrorCleanScopeLabel(kind: "payables" | "receivables", grammaticalCase: "accusative" | "genitive"): string {
|
||||||
return kind === "payables" ? "чистый долг к оплате" : "чистую дебиторку к получению";
|
if (kind === "payables") {
|
||||||
|
return grammaticalCase === "genitive" ? "чистого долга к оплате" : "чистый долг к оплате";
|
||||||
|
}
|
||||||
|
return grammaticalCase === "genitive" ? "чистой дебиторки к получению" : "чистую дебиторку к получению";
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendDebtMirrorCompactDisclosure(
|
function appendDebtMirrorCompactDisclosure(
|
||||||
|
|
@ -2699,12 +2751,13 @@ function appendDebtMirrorCompactDisclosure(
|
||||||
if (snapshot.mirroredOffsetAmount <= 0.005) {
|
if (snapshot.mirroredOffsetAmount <= 0.005) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
lines.push("", "Для сверки:");
|
||||||
lines.push(
|
lines.push(
|
||||||
`Отдельно сверено встречных остатков: ${formatMoneyRub(snapshot.mirroredOffsetAmount)}; они не включены в ${debtMirrorCleanScopeLabel(kind)}.`
|
`- Встречная часть: ${formatMoneyRub(snapshot.mirroredOffsetAmount)}; она не включена в ${debtMirrorCleanScopeLabel(kind, "accusative")}.`
|
||||||
);
|
);
|
||||||
const leadingMirror = snapshot.mirrorGroups[0] ?? null;
|
const leadingMirror = snapshot.mirrorGroups[0] ?? null;
|
||||||
if (leadingMirror) {
|
if (leadingMirror) {
|
||||||
lines.push(`Крупнейший встречный хвост: ${formatDebtMirrorGroupLine(leadingMirror)}`);
|
lines.push(`- Крупнейший встречный контрагент: ${formatDebtMirrorGroupLine(leadingMirror)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2719,9 +2772,12 @@ function appendDebtMirrorDisclosure(
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push("Встречные остатки к сверке");
|
lines.push("Встречные остатки к сверке");
|
||||||
lines.push(
|
lines.push(
|
||||||
`- Встречная часть: ${formatMoneyRub(snapshot.mirroredOffsetAmount)}; она исключена из ${debtMirrorCleanScopeLabel(kind)}.`
|
`- Встречная часть: ${formatMoneyRub(snapshot.mirroredOffsetAmount)}; она исключена из ${debtMirrorCleanScopeLabel(kind, "genitive")}.`
|
||||||
);
|
);
|
||||||
lines.push(...snapshot.mirrorGroups.slice(0, 3).map((item, index) => `${index + 1}. ${formatDebtMirrorGroupLine(item)}`));
|
const leadingMirror = snapshot.mirrorGroups[0] ?? null;
|
||||||
|
if (leadingMirror) {
|
||||||
|
lines.push(`- Крупнейший встречный контрагент: ${formatDebtMirrorGroupLine(leadingMirror)}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function deriveOperationalYearWindow(
|
function deriveOperationalYearWindow(
|
||||||
|
|
@ -3734,7 +3790,6 @@ function composeFactualReplyBody(
|
||||||
|
|
||||||
const netVat = turnover68Credit - turnover68Debit;
|
const netVat = turnover68Credit - turnover68Debit;
|
||||||
const vatToPay = Math.max(0, netVat);
|
const vatToPay = Math.max(0, netVat);
|
||||||
const carryoverOrOverpayment = Math.max(0, -netVat);
|
|
||||||
const totalVatTurnoverAbs =
|
const totalVatTurnoverAbs =
|
||||||
Math.abs(turnover68Credit) + Math.abs(turnover68Debit) + Math.abs(turnover19Debit) + Math.abs(turnover19Credit);
|
Math.abs(turnover68Credit) + Math.abs(turnover68Debit) + Math.abs(turnover19Debit) + Math.abs(turnover19Credit);
|
||||||
const vatActivityDetected = totalVatTurnoverAbs > 0.0000001;
|
const vatActivityDetected = totalVatTurnoverAbs > 0.0000001;
|
||||||
|
|
@ -3743,67 +3798,20 @@ function composeFactualReplyBody(
|
||||||
const shouldShowCalendarDetails = needsVatCalendarDetails(options.userMessage);
|
const shouldShowCalendarDetails = needsVatCalendarDetails(options.userMessage);
|
||||||
const vatCalendar = deriveVatDeadlineCalendar(options.periodFrom, options.periodTo);
|
const vatCalendar = deriveVatDeadlineCalendar(options.periodFrom, options.periodTo);
|
||||||
const formatForecastMoney = (value: number): string => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
const formatForecastMoney = (value: number): string => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
||||||
const vatProbe = options.vatDirectSourceProbe ?? null;
|
|
||||||
const periodWindowLabel =
|
const periodWindowLabel =
|
||||||
options.periodFrom && options.periodTo ? `${formatDateRu(options.periodFrom)}..${formatDateRu(options.periodTo)}` : null;
|
options.periodFrom && options.periodTo ? `${formatDateRu(options.periodFrom)}..${formatDateRu(options.periodTo)}` : null;
|
||||||
|
|
||||||
const lines = [
|
const lines = [
|
||||||
`Коротко: ориентир по НДС к уплате за ${periodWindowLabel ?? "доступный срез"} — ${formatForecastMoney(vatToPay)}.`,
|
`Коротко: ориентир по НДС к уплате за ${periodWindowLabel ?? "доступный срез"} — ${formatForecastMoney(vatToPay)}.`,
|
||||||
`Если смотреть на возможный перенос или переплату, получается ${formatForecastMoney(carryoverOrOverpayment)}.`,
|
"Это предварительная оценка по бухгалтерским оборотам НДС, а не подтвержденная сумма налога по декларации.",
|
||||||
"Это предварительная оценка по оборотам 68.02*/19*, а не подтвержденная сумма налога по декларации.",
|
|
||||||
"",
|
"",
|
||||||
"Что вошло в расчет:",
|
"Что вошло в расчет:",
|
||||||
`- Период оценки: ${periodWindowLabel ?? "не задан (использован доступный срез)"}.`,
|
`- Период оценки: ${periodWindowLabel ?? "не задан (использован доступный срез)"}.`,
|
||||||
`- Оборот по кредиту 68*: ${formatForecastMoney(turnover68Credit)}.`,
|
`- НДС начисленный: ${formatForecastMoney(turnover68Credit)}.`,
|
||||||
`- Оборот по дебету 68*: ${formatForecastMoney(turnover68Debit)}.`,
|
`- Уменьшение/вычеты по НДС: ${formatForecastMoney(turnover68Debit)}.`,
|
||||||
`- Нетто НДС (68 Кт - 68 Дт): ${formatForecastMoney(netVat)}.`,
|
`- Нетто НДС: ${formatForecastMoney(netVat)}.`
|
||||||
`- Справочно по 19*: дебет ${formatForecastMoney(turnover19Debit)}, кредит ${formatForecastMoney(turnover19Credit)}.`
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if (vatProbe && vatProbe.status === "ok") {
|
|
||||||
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
|
||||||
const statusRank = (status: VatDirectSourceProbeItem["status"]): number =>
|
|
||||||
status === "ok" ? 0 : status === "empty" ? 1 : 2;
|
|
||||||
const orderedProbeRows = [...vatProbe.probedSources].sort(
|
|
||||||
(a, b) =>
|
|
||||||
statusRank(a.status) - statusRank(b.status) ||
|
|
||||||
a.fullName.localeCompare(b.fullName, "ru")
|
|
||||||
);
|
|
||||||
const nonErrorProbeRows = orderedProbeRows.filter((item) => item.status !== "error");
|
|
||||||
const visibleProbeRows = (nonErrorProbeRows.length > 0 ? nonErrorProbeRows : orderedProbeRows).slice(0, 6);
|
|
||||||
const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length;
|
|
||||||
lines.push(
|
|
||||||
"",
|
|
||||||
"Покрытие VAT-источников в 1С:",
|
|
||||||
`- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`,
|
|
||||||
`- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`,
|
|
||||||
`- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`,
|
|
||||||
`- Источников с ошибкой запроса: ${formatNumberWithDots(erroredSources)}.`
|
|
||||||
);
|
|
||||||
if (visibleProbeRows.length > 0) {
|
|
||||||
lines.push(
|
|
||||||
...visibleProbeRows.map((item, index) => {
|
|
||||||
const name = item.synonym ? `${item.fullName} (${item.synonym})` : item.fullName;
|
|
||||||
const extra =
|
|
||||||
item.status === "ok"
|
|
||||||
? item.lastPeriod
|
|
||||||
? ` | последнее движение: ${item.lastPeriod}`
|
|
||||||
: ""
|
|
||||||
: item.status === "error" && item.error
|
|
||||||
? ` | ошибка: ${item.error}`
|
|
||||||
: "";
|
|
||||||
return `${index + 1}. ${name} | ${formatVatProbeStatusRu(item.status)}${extra}`;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (vatProbe.errors.length > 0) {
|
|
||||||
lines.push(`- Ограничения probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
|
||||||
}
|
|
||||||
lines.push("- Сумма прогноза выше рассчитана строго по оборотам 68.02*/19*; прямые VAT-источники показаны для проверки покрытия.");
|
|
||||||
} else if (vatProbe && vatProbe.status === "error") {
|
|
||||||
lines.push("", "Покрытие VAT-источников в 1С: дополнительная проверка завершилась ошибкой, поэтому использован только базовый контур 68.02*/19*.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!vatActivityDetected) {
|
if (!vatActivityDetected) {
|
||||||
lines.push(
|
lines.push(
|
||||||
`В выбранном окне не найдено движений по НДС-субсчетам 68.02*/19*; поэтому оперативный прогноз к уплате равен ${formatForecastMoney(
|
`В выбранном окне не найдено движений по НДС-субсчетам 68.02*/19*; поэтому оперативный прогноз к уплате равен ${formatForecastMoney(
|
||||||
|
|
@ -3883,18 +3891,15 @@ function composeFactualReplyBody(
|
||||||
const purchaseVat = rowsByMarker.get("VAT_BOOK_PURCHASES") ?? 0;
|
const purchaseVat = rowsByMarker.get("VAT_BOOK_PURCHASES") ?? 0;
|
||||||
const netVat = salesVat - purchaseVat;
|
const netVat = salesVat - purchaseVat;
|
||||||
const vatToPay = Math.max(0, netVat);
|
const vatToPay = Math.max(0, netVat);
|
||||||
const carryoverOrOverpayment = Math.max(0, -netVat);
|
|
||||||
const periodWindowLabel =
|
const periodWindowLabel =
|
||||||
options.periodFrom && options.periodTo ? `${formatDateRu(options.periodFrom)}..${formatDateRu(options.periodTo)}` : null;
|
options.periodFrom && options.periodTo ? `${formatDateRu(options.periodFrom)}..${formatDateRu(options.periodTo)}` : null;
|
||||||
const formatConfirmedMoney = (value: number): string => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
const formatConfirmedMoney = (value: number): string => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
||||||
const vatProbe = options.vatDirectSourceProbe ?? null;
|
|
||||||
const organizationLabel = normalizeOrganizationScopeValue(options.organizationHint);
|
const organizationLabel = normalizeOrganizationScopeValue(options.organizationHint);
|
||||||
const organizationScopeLabel = organizationLabel ? ` по организации ${organizationLabel}` : "";
|
const organizationScopeLabel = organizationLabel ? ` по организации ${organizationLabel}` : "";
|
||||||
|
|
||||||
const lines = [
|
const lines = [
|
||||||
`Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`,
|
`Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`,
|
||||||
`Если смотреть на возможный перенос или переплату, получается ${formatConfirmedMoney(carryoverOrOverpayment)}.`,
|
"Расчет сделан по книгам продаж и покупок.",
|
||||||
"Это подтвержденный расчет по регистрам книг продаж и покупок, без surrogate-формулы 68/19.",
|
|
||||||
"",
|
"",
|
||||||
"Что вошло в расчет:",
|
"Что вошло в расчет:",
|
||||||
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
|
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
|
||||||
|
|
@ -3904,32 +3909,6 @@ function composeFactualReplyBody(
|
||||||
`- Нетто НДС (книга продаж - книга покупок): ${formatConfirmedMoney(netVat)}.`
|
`- Нетто НДС (книга продаж - книга покупок): ${formatConfirmedMoney(netVat)}.`
|
||||||
];
|
];
|
||||||
|
|
||||||
if (vatProbe && vatProbe.status === "ok") {
|
|
||||||
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
|
||||||
const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length;
|
|
||||||
lines.push(
|
|
||||||
"",
|
|
||||||
"Покрытие VAT-источников в 1С:",
|
|
||||||
`- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`,
|
|
||||||
`- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`,
|
|
||||||
`- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`,
|
|
||||||
`- Источников с ошибкой запроса: ${formatNumberWithDots(erroredSources)}.`
|
|
||||||
);
|
|
||||||
if (vatProbe.errors.length > 0) {
|
|
||||||
lines.push(`- Ограничения probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
|
||||||
}
|
|
||||||
lines.push("- Сумма расчета выше получена по книгам продаж/покупок; дополнительная проверка использована для контроля полноты VAT-источников.");
|
|
||||||
} else if (vatProbe && vatProbe.status === "error") {
|
|
||||||
lines.push(
|
|
||||||
"",
|
|
||||||
"Покрытие VAT-источников в 1С: дополнительная проверка недоступна, поэтому использован основной бухгалтерский срез.",
|
|
||||||
"Итоговая сумма НДС выше рассчитана по основному маршруту книг продаж/покупок; probe влияет только на диагностику покрытия."
|
|
||||||
);
|
|
||||||
if (vatProbe.errors.length > 0) {
|
|
||||||
lines.push(`- Детали probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
lines.push(
|
lines.push(
|
||||||
"",
|
"",
|
||||||
|
|
@ -4003,63 +3982,24 @@ function composeFactualReplyBody(
|
||||||
const lines: string[] = [
|
const lines: string[] = [
|
||||||
`Итого подтвержденный НДС к уплате на ${formatDateRu(asOfDate)}: ${formatMoneyRub(totalVatPayable)}.`,
|
`Итого подтвержденный НДС к уплате на ${formatDateRu(asOfDate)}: ${formatMoneyRub(totalVatPayable)}.`,
|
||||||
"",
|
"",
|
||||||
"Блок 1. Статус результата",
|
"Что учтено:",
|
||||||
"- Результат: подтвержденный срез НДС к уплате по состоянию на дату.",
|
|
||||||
"",
|
|
||||||
"Блок 2. Что учтено",
|
|
||||||
`- Дата среза: ${formatDateRu(asOfDate)}.`,
|
`- Дата среза: ${formatDateRu(asOfDate)}.`,
|
||||||
"- Контур: остатки по счетам НДС к уплате (68*)."
|
"- Контур: остатки по счетам НДС к уплате."
|
||||||
];
|
];
|
||||||
|
|
||||||
const vatProbe = options.vatDirectSourceProbe ?? null;
|
|
||||||
if (vatProbe && vatProbe.status === "ok") {
|
|
||||||
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
|
||||||
lines.push(
|
|
||||||
"",
|
|
||||||
"Блок 2.1. Проверка VAT-источников в 1С",
|
|
||||||
`- VAT-объектов в метаданных 1С: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`,
|
|
||||||
`- Пробных прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`,
|
|
||||||
`- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`
|
|
||||||
);
|
|
||||||
if (vatProbe.probedSources.length > 0) {
|
|
||||||
lines.push(
|
|
||||||
...vatProbe.probedSources.slice(0, 4).map((item, index) => {
|
|
||||||
const name = item.synonym ? `${item.fullName} (${item.synonym})` : item.fullName;
|
|
||||||
const suffix =
|
|
||||||
item.status === "ok"
|
|
||||||
? `${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.sampleRegistrator ? ` | пример: ${item.sampleRegistrator}` : ""}`
|
|
||||||
: item.status === "error" && item.error
|
|
||||||
? ` | ошибка: ${item.error}`
|
|
||||||
: "";
|
|
||||||
return `${index + 1}. ${name} | ${formatVatProbeStatusRu(item.status)}${suffix}`;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (vatProbe.errors.length > 0) {
|
|
||||||
lines.push(`- Ограничения probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
|
||||||
}
|
|
||||||
} else if (vatProbe && vatProbe.status === "error") {
|
|
||||||
lines.push(
|
|
||||||
"",
|
|
||||||
"Блок 2.1. Проверка VAT-источников в 1С",
|
|
||||||
"- Дополнительная проверка VAT-источников завершилась ошибкой, поэтому срез подтвержден по доступному бухгалтерскому источнику (68*)."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
lines.push(
|
lines.push(
|
||||||
"",
|
"",
|
||||||
"Блок 3. Сводка",
|
"Сводка:",
|
||||||
`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`,
|
`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`,
|
||||||
`- Подтвержденных позиций по НДС: ${formatNumberWithDots(accountRows.length)}.`,
|
`- Подтвержденных позиций по НДС: ${formatNumberWithDots(accountRows.length)}.`,
|
||||||
"",
|
"",
|
||||||
"Блок 4. Подтвержденные позиции"
|
"Подтвержденные позиции:"
|
||||||
);
|
);
|
||||||
|
|
||||||
if (accountRows.length > 0) {
|
if (accountRows.length > 0) {
|
||||||
lines.push(
|
lines.push(
|
||||||
...accountRows.slice(0, 12).map((item, index) => {
|
...accountRows.slice(0, 12).map((item, index) => {
|
||||||
const refs = Array.from(item.refs).slice(0, 2).join("; ");
|
return `${index + 1}. ${item.account} — остаток НДС к уплате: ${formatMoneyRub(item.total)}, операций: ${formatNumberWithDots(item.operations)}.`;
|
||||||
return `${index + 1}. ${item.account} | остаток НДС к уплате: ${formatMoneyRub(item.total)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${refs ? ` | source refs: ${refs}` : ""}`;
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -4386,7 +4326,6 @@ function composeFactualReplyBody(
|
||||||
},
|
},
|
||||||
{ supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 }
|
{ supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isDirectBalanceQuestion(options.userMessage)) {
|
if (isDirectBalanceQuestion(options.userMessage)) {
|
||||||
const leading = confirmedBalances[0] ?? null;
|
const leading = confirmedBalances[0] ?? null;
|
||||||
const compactLines: string[] = leading
|
const compactLines: string[] = leading
|
||||||
|
|
@ -4399,8 +4338,7 @@ function composeFactualReplyBody(
|
||||||
if (leading) {
|
if (leading) {
|
||||||
compactLines.push(
|
compactLines.push(
|
||||||
...confirmedBalances.slice(0, 5).map((item, index) => {
|
...confirmedBalances.slice(0, 5).map((item, index) => {
|
||||||
const lastPeriod = item.lastPeriod ? `, последнее движение: ${item.lastPeriod}` : "";
|
return `${index + 1}. ${item.name} — ${formatMoneyRub(item.outstandingAmount)} (${formatNumberWithDots(item.operations)} опер.).`;
|
||||||
return `${index + 1}. ${item.name} — ${formatMoneyRub(item.outstandingAmount)} (${formatNumberWithDots(item.operations)} опер.${lastPeriod}).`;
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
if (confirmedBalances.length > 5) {
|
if (confirmedBalances.length > 5) {
|
||||||
|
|
@ -4454,7 +4392,7 @@ function composeFactualReplyBody(
|
||||||
if (confirmedBalances.length > 0) {
|
if (confirmedBalances.length > 0) {
|
||||||
lines.push(
|
lines.push(
|
||||||
...confirmedBalances.slice(0, 10).flatMap((item, index) => [
|
...confirmedBalances.slice(0, 10).flatMap((item, index) => [
|
||||||
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`,
|
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`,
|
||||||
""
|
""
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
@ -4512,8 +4450,7 @@ function composeFactualReplyBody(
|
||||||
if (leading) {
|
if (leading) {
|
||||||
compactLines.push(
|
compactLines.push(
|
||||||
...confirmedBalances.slice(0, 5).map((item, index) => {
|
...confirmedBalances.slice(0, 5).map((item, index) => {
|
||||||
const lastPeriod = item.lastPeriod ? `, последнее движение: ${item.lastPeriod}` : "";
|
return `${index + 1}. ${item.name} — ${formatMoneyRub(item.outstandingAmount)} (${formatNumberWithDots(item.operations)} опер.).`;
|
||||||
return `${index + 1}. ${item.name} — ${formatMoneyRub(item.outstandingAmount)} (${formatNumberWithDots(item.operations)} опер.${lastPeriod}).`;
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
if (confirmedBalances.length > 5) {
|
if (confirmedBalances.length > 5) {
|
||||||
|
|
@ -4534,49 +4471,31 @@ function composeFactualReplyBody(
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines: string[] = [
|
const lines: string[] = [
|
||||||
`Коротко: подтвержденная дебиторская задолженность на ${formatDateRu(receivablesAsOfDate)} — ${formatMoneyRub(totalOutstandingAmount)}.`,
|
`Коротко: на ${formatDateRu(receivablesAsOfDate)} нам должны ${formatMoneyRub(totalOutstandingAmount)}.`
|
||||||
"Это подтвержденный срез дебиторской задолженности, а не эвристический shortlist."
|
|
||||||
];
|
];
|
||||||
|
if (periodScopeLine || carryoverLine) {
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push("Что учтено");
|
lines.push("Граница ответа:");
|
||||||
lines.push(`- Дата среза: ${formatDateRu(receivablesAsOfDate)}.`);
|
if (periodScopeLine) {
|
||||||
if (periodScopeLine) {
|
lines.push(periodScopeLine);
|
||||||
lines.push(periodScopeLine);
|
}
|
||||||
}
|
if (carryoverLine) {
|
||||||
lines.push("- Контур: дебиторская задолженность по счетам 62/76.");
|
lines.push(carryoverLine);
|
||||||
if (carryoverLine) {
|
}
|
||||||
lines.push(carryoverLine);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push("Сводка");
|
lines.push("К получению:");
|
||||||
lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`);
|
|
||||||
lines.push(`- Контрагентов с подтвержденным остатком к получению: ${formatNumberWithDots(confirmedBalances.length)}.`);
|
|
||||||
appendDebtMirrorDisclosure(lines, balanceSnapshot, "receivables");
|
|
||||||
|
|
||||||
lines.push("");
|
|
||||||
lines.push("Категории дебиторской задолженности");
|
|
||||||
lines.push(`- ${receivablesCategoryLabel("supplier_or_contractor")}: ${formatNumberWithDots(categoryCounts.supplier_or_contractor)}.`);
|
|
||||||
lines.push(`- ${receivablesCategoryLabel("bank_or_credit")}: ${formatNumberWithDots(categoryCounts.bank_or_credit)}.`);
|
|
||||||
lines.push(`- ${receivablesCategoryLabel("tax_or_state")}: ${formatNumberWithDots(categoryCounts.tax_or_state)}.`);
|
|
||||||
lines.push(`- ${receivablesCategoryLabel("other")}: ${formatNumberWithDots(categoryCounts.other)}.`);
|
|
||||||
|
|
||||||
lines.push("");
|
|
||||||
lines.push("Подтвержденные позиции к получению");
|
|
||||||
if (confirmedBalances.length > 0) {
|
if (confirmedBalances.length > 0) {
|
||||||
lines.push(
|
lines.push(
|
||||||
...confirmedBalances.slice(0, 10).flatMap((item, index) => [
|
...confirmedBalances.slice(0, 10).map(
|
||||||
`${index + 1}. ${item.name} | категория: ${receivablesCategoryLabel(item.category)} | остаток к получению: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`,
|
(item, index) => `${index + 1}. ${item.name} — ${formatMoneyRub(item.outstandingAmount)} (${formatNumberWithDots(item.operations)} опер.).`
|
||||||
""
|
)
|
||||||
])
|
|
||||||
);
|
);
|
||||||
if (lines[lines.length - 1] === "") {
|
|
||||||
lines.pop();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
lines.push("- Подтвержденной открытой дебиторской задолженности на дату среза не найдено.");
|
lines.push("- Подтвержденной открытой дебиторской задолженности на дату среза не найдено.");
|
||||||
}
|
}
|
||||||
|
appendDebtMirrorDisclosure(lines, balanceSnapshot, "receivables");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
responseType: confirmedBalances.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
|
responseType: confirmedBalances.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
|
||||||
|
|
@ -4606,7 +4525,7 @@ function composeFactualReplyBody(
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const formatHeuristicItem = (item: PayablesCounterpartyRiskAggregate, index: number): string =>
|
const formatHeuristicItem = (item: PayablesCounterpartyRiskAggregate, index: number): string =>
|
||||||
`${index + 1}. ${item.name} | сумма к проверке: ${formatMoneyRub(item.totalAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}`;
|
`${index + 1}. ${item.name} | сумма к проверке: ${formatMoneyRub(item.totalAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}`;
|
||||||
|
|
||||||
const pushCategorySlice = (
|
const pushCategorySlice = (
|
||||||
lines: string[],
|
lines: string[],
|
||||||
|
|
@ -4723,7 +4642,7 @@ function composeFactualReplyBody(
|
||||||
"Блок 5. Крупнейшие подтвержденные позиции к оплате (по сумме остатка):",
|
"Блок 5. Крупнейшие подтвержденные позиции к оплате (по сумме остатка):",
|
||||||
...confirmedBalances.slice(0, 10).map(
|
...confirmedBalances.slice(0, 10).map(
|
||||||
(item, index) =>
|
(item, index) =>
|
||||||
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток к оплате: ${formatMoneyRub(item.outstandingAmount)} | операций в срезе: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`
|
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток к оплате: ${formatMoneyRub(item.outstandingAmount)} | операций в срезе: ${formatNumberWithDots(item.operations)}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
appendDebtMirrorDisclosure(lines, balanceSnapshot, "payables");
|
appendDebtMirrorDisclosure(lines, balanceSnapshot, "payables");
|
||||||
|
|
@ -5013,15 +4932,41 @@ function composeFactualReplyBody(
|
||||||
);
|
);
|
||||||
const counterparty = resolvePreferredCounterpartyDisplayLabel(options.counterpartyHint, rowCounterparties);
|
const counterparty = resolvePreferredCounterpartyDisplayLabel(options.counterpartyHint, rowCounterparties);
|
||||||
const roleBoundary = bankRoleBoundaryLine(options.userMessage, rows);
|
const roleBoundary = bankRoleBoundaryLine(options.userMessage, rows);
|
||||||
const visibleRows = rows.slice(0, Math.min(rows.length, 5));
|
const visibleRows = [...rows]
|
||||||
|
.sort(
|
||||||
|
(left, right) =>
|
||||||
|
Math.abs(right.amount ?? 0) - Math.abs(left.amount ?? 0) ||
|
||||||
|
(String(right.period ?? "").localeCompare(String(left.period ?? ""), "ru"))
|
||||||
|
)
|
||||||
|
.slice(0, Math.min(rows.length, 5));
|
||||||
|
const semanticSummary = summarizeBankOperationSemantics(rows);
|
||||||
|
const compactRoleAnswer = /(?:клиент|поставщик|финансов|роль|коротко)/iu.test(String(options.userMessage ?? ""));
|
||||||
|
const compactEvidenceRows = visibleRows.map((row, index) => {
|
||||||
|
const direction = bankOperationDirectionLabel(bankOperationDirection(row));
|
||||||
|
const amount = formatMoneyRub(row.amount ?? 0);
|
||||||
|
const period = row.period ? formatDateRu(row.period) : "дата не указана";
|
||||||
|
const operationKind = String(row.operation_kind ?? "").trim();
|
||||||
|
const paymentPurpose = String(row.payment_purpose ?? "").trim();
|
||||||
|
const detail = operationKind || paymentPurpose
|
||||||
|
? ` | ${[operationKind, paymentPurpose].filter(Boolean).join("; ")}`
|
||||||
|
: "";
|
||||||
|
return `${index + 1}. ${period} | ${direction} | ${amount}${detail}`;
|
||||||
|
});
|
||||||
const lines = [
|
const lines = [
|
||||||
`Коротко: найдено банковских операций${counterparty ? ` по ${counterparty}` : " по контрагенту"} — ${rows.length}.`,
|
`Коротко: найдено банковских операций${counterparty ? ` по ${counterparty}` : " по контрагенту"} — ${rows.length}.`,
|
||||||
summarizeBankOperationDirections(rows),
|
summarizeBankOperationDirections(rows),
|
||||||
roleBoundary ?? "Показываю подтвержденные банковские операции из текущего среза.",
|
roleBoundary ?? "Показываю подтвержденные банковские операции из текущего среза.",
|
||||||
bankOperationEvidenceLine(rows, preferredBankEvidenceDirection(options.userMessage)),
|
...(semanticSummary ? [semanticSummary] : [])
|
||||||
...formatTopRows(visibleRows, visibleRows.length)
|
|
||||||
];
|
];
|
||||||
if (rows.length > visibleRows.length) {
|
if (!compactRoleAnswer) {
|
||||||
|
lines.push(
|
||||||
|
bankOperationEvidenceLine(rows, preferredBankEvidenceDirection(options.userMessage)),
|
||||||
|
"Примеры строк 1С:",
|
||||||
|
...compactEvidenceRows
|
||||||
|
);
|
||||||
|
}
|
||||||
|
lines.push("Следующий шаг: могу отдельно разложить назначения платежа, договоры или отделить банковский контур от клиентского/поставщицкого.");
|
||||||
|
if (!compactRoleAnswer && rows.length > visibleRows.length) {
|
||||||
lines.push(`Показаны первые ${visibleRows.length} из ${rows.length}; полный список остается в подтвержденном срезе.`);
|
lines.push(`Показаны первые ${visibleRows.length} из ${rows.length}; полный список остается в подтвержденном срезе.`);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,26 @@ function findFocusedCounterpartyValuePoint(
|
||||||
return profileRows.length === 1 ? profileRows[0] : null;
|
return profileRows.length === 1 ? profileRows[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasProfitAmbiguityCue(normalizedQuestion: string): boolean {
|
||||||
|
return /(?:заработ|прибыл|прибыль|доход|выручк)/iu.test(normalizedQuestion);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCashflowBoundaryLine(isSupplier: boolean): string {
|
||||||
|
return isSupplier
|
||||||
|
? "Граница ответа: это подтвержденный денежный поток по поставщику, а не итоговая задолженность."
|
||||||
|
: "Граница ответа: это подтвержденный денежный поток по поступлениям, а не чистая прибыль.";
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCashflowNextStepLine(isSupplier: boolean, normalizedQuestion: string): string | null {
|
||||||
|
if (isSupplier) {
|
||||||
|
return "Следующий шаг: могу отдельно показать остаток долга, просрочку или расшифровку по документам.";
|
||||||
|
}
|
||||||
|
if (hasProfitAmbiguityCue(normalizedQuestion)) {
|
||||||
|
return "Следующий шаг: могу отдельно проверить чистую прибыль по закрытию 90/91/99.";
|
||||||
|
}
|
||||||
|
return "Следующий шаг: могу разложить поток по месяцам, документам или контрагентам.";
|
||||||
|
}
|
||||||
|
|
||||||
export function composeCounterpartyAnalyticsReply(
|
export function composeCounterpartyAnalyticsReply(
|
||||||
intent: AddressIntent,
|
intent: AddressIntent,
|
||||||
rows: ComposeStageRow[],
|
rows: ComposeStageRow[],
|
||||||
|
|
@ -546,6 +566,8 @@ export function composeCounterpartyAnalyticsReply(
|
||||||
const semanticSingleBestCounterparty =
|
const semanticSingleBestCounterparty =
|
||||||
focus === "top_by_total" && hasSingleBestCounterpartyCue && !asksExplicitRankingList;
|
focus === "top_by_total" && hasSingleBestCounterpartyCue && !asksExplicitRankingList;
|
||||||
const effectiveLimit = asksSingleBestCounterparty || semanticSingleBestCounterparty ? 1 : limit;
|
const effectiveLimit = asksSingleBestCounterparty || semanticSingleBestCounterparty ? 1 : limit;
|
||||||
|
const cashflowBoundaryLine = buildCashflowBoundaryLine(isSupplier);
|
||||||
|
const cashflowNextStepLine = buildCashflowNextStepLine(isSupplier, normalizedQuestion);
|
||||||
|
|
||||||
const byCounterparty = new Map<string, CounterpartyValuePoint>();
|
const byCounterparty = new Map<string, CounterpartyValuePoint>();
|
||||||
const byYear = new Map<number, CounterpartyYearPoint>();
|
const byYear = new Map<number, CounterpartyYearPoint>();
|
||||||
|
|
@ -655,10 +677,12 @@ export function composeCounterpartyAnalyticsReply(
|
||||||
? `за период ${deps.formatDateRu(options.periodFrom)}..${deps.formatDateRu(options.periodTo)}`
|
? `за период ${deps.formatDateRu(options.periodFrom)}..${deps.formatDateRu(options.periodTo)}`
|
||||||
: "за доступное время";
|
: "за доступное время";
|
||||||
const directAnswerLine = isSupplier
|
const directAnswerLine = isSupplier
|
||||||
? `Оборот по ${focusedCounterparty.name} ${periodLabel}: ${deps.formatMoneyRub(focusedCounterparty.total)} по ${focusedCounterparty.ops} подтвержденным исходящим операциям. Это денежный поток по поставщику, а не итоговая задолженность.`
|
? `Оборот по ${focusedCounterparty.name} ${periodLabel}: ${deps.formatMoneyRub(focusedCounterparty.total)} по ${focusedCounterparty.ops} подтвержденным исходящим операциям.`
|
||||||
: `Оборот по ${focusedCounterparty.name} ${periodLabel}: ${deps.formatMoneyRub(focusedCounterparty.total)} по ${focusedCounterparty.ops} подтвержденным входящим операциям. Это денежный поток от клиента, а не чистая прибыль.`;
|
: `Оборот по ${focusedCounterparty.name} ${periodLabel}: ${deps.formatMoneyRub(focusedCounterparty.total)} по ${focusedCounterparty.ops} подтвержденным входящим операциям.`;
|
||||||
const summaryLines = [
|
const summaryLines = [
|
||||||
directAnswerLine,
|
directAnswerLine,
|
||||||
|
cashflowBoundaryLine,
|
||||||
|
...(cashflowNextStepLine ? [cashflowNextStepLine] : []),
|
||||||
"",
|
"",
|
||||||
"Подтверждение:",
|
"Подтверждение:",
|
||||||
`- Контрагент в выборке: ${focusedCounterparty.name}.`,
|
`- Контрагент в выборке: ${focusedCounterparty.name}.`,
|
||||||
|
|
@ -678,11 +702,10 @@ export function composeCounterpartyAnalyticsReply(
|
||||||
options.periodFrom && options.periodTo
|
options.periodFrom && options.periodTo
|
||||||
? `За период ${deps.formatDateRu(options.periodFrom)}..${deps.formatDateRu(options.periodTo)} подтверждено ${deps.formatMoneyRub(totalFlow)} ${isSupplier ? "исходящих выплат" : "входящих поступлений"}.`
|
? `За период ${deps.formatDateRu(options.periodFrom)}..${deps.formatDateRu(options.periodTo)} подтверждено ${deps.formatMoneyRub(totalFlow)} ${isSupplier ? "исходящих выплат" : "входящих поступлений"}.`
|
||||||
: `За все доступное время подтверждено ${deps.formatMoneyRub(totalFlow)} ${isSupplier ? "исходящих выплат" : "входящих поступлений"}.`;
|
: `За все доступное время подтверждено ${deps.formatMoneyRub(totalFlow)} ${isSupplier ? "исходящих выплат" : "входящих поступлений"}.`;
|
||||||
const directAnswerLine = isSupplier
|
|
||||||
? periodLine
|
|
||||||
: `${periodLine} Это денежный поток от клиентов, а не чистая прибыль.`;
|
|
||||||
const summaryLines = [
|
const summaryLines = [
|
||||||
directAnswerLine,
|
periodLine,
|
||||||
|
cashflowBoundaryLine,
|
||||||
|
...(cashflowNextStepLine ? [cashflowNextStepLine] : []),
|
||||||
"",
|
"",
|
||||||
"Подтверждение:",
|
"Подтверждение:",
|
||||||
`- Операций в выборке: ${totalOperations}.`,
|
`- Операций в выборке: ${totalOperations}.`,
|
||||||
|
|
@ -709,11 +732,17 @@ export function composeCounterpartyAnalyticsReply(
|
||||||
const strongestYear = visible[0];
|
const strongestYear = visible[0];
|
||||||
const directAnswerLine = isSupplier
|
const directAnswerLine = isSupplier
|
||||||
? `Самый крупный год по подтвержденным выплатам: ${strongestYear.year} (${deps.formatMoneyRub(strongestYear.total)} по ${strongestYear.ops} операциям).`
|
? `Самый крупный год по подтвержденным выплатам: ${strongestYear.year} (${deps.formatMoneyRub(strongestYear.total)} по ${strongestYear.ops} операциям).`
|
||||||
: `Самый доходный год по подтвержденным поступлениям: ${strongestYear.year} (${deps.formatMoneyRub(strongestYear.total)} по ${strongestYear.ops} операциям). Это денежный поток, а не чистая прибыль.`;
|
: `Самый доходный год по подтвержденным поступлениям: ${strongestYear.year} (${deps.formatMoneyRub(strongestYear.total)} по ${strongestYear.ops} операциям).`;
|
||||||
const heading = isSupplier
|
const heading = isSupplier
|
||||||
? `Топ-${visible.length} лет по сумме выплат:`
|
? `Топ-${visible.length} лет по сумме выплат:`
|
||||||
: `Топ-${visible.length} лет по сумме поступлений:`;
|
: `Топ-${visible.length} лет по сумме поступлений:`;
|
||||||
lines.unshift(heading);
|
lines.unshift(heading);
|
||||||
|
if (!isSupplier) {
|
||||||
|
lines.unshift(cashflowBoundaryLine);
|
||||||
|
if (cashflowNextStepLine) {
|
||||||
|
lines.unshift(cashflowNextStepLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
lines.unshift(directAnswerLine);
|
lines.unshift(directAnswerLine);
|
||||||
lines.push(
|
lines.push(
|
||||||
...visible.map(
|
...visible.map(
|
||||||
|
|
@ -829,11 +858,17 @@ export function composeCounterpartyAnalyticsReply(
|
||||||
const directAnswerLine = singleCandidateOnly
|
const directAnswerLine = singleCandidateOnly
|
||||||
? isSupplier
|
? isSupplier
|
||||||
? `В выбранном срезе найден один поставщик: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
? `В выбранном срезе найден один поставщик: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
||||||
: `В выбранном срезе найден один клиент: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг; сумма является денежным потоком, а не чистой прибылью.`
|
: `В выбранном срезе найден один клиент: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
||||||
: isSupplier
|
: isSupplier
|
||||||
? `Крупнейший поставщик по подтвержденным выплатам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
? `Крупнейший поставщик по подтвержденным выплатам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
||||||
: `Самый доходный клиент ${rankingPeriodLabel} по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это денежный поток, а не чистая прибыль.`;
|
: `Самый доходный клиент ${rankingPeriodLabel} по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`;
|
||||||
lines.unshift(directAnswerLine);
|
lines.unshift(directAnswerLine);
|
||||||
|
if (!isSupplier) {
|
||||||
|
lines.splice(1, 0, cashflowBoundaryLine);
|
||||||
|
if (cashflowNextStepLine) {
|
||||||
|
lines.splice(2, 0, cashflowNextStepLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
lines.push(
|
lines.push(
|
||||||
...visible.map((item, index) => {
|
...visible.map((item, index) => {
|
||||||
|
|
|
||||||
|
|
@ -423,6 +423,7 @@ function isInventoryIntent(intent: AddressIntent | undefined): boolean {
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date"
|
intent === "inventory_aging_by_purchase_date"
|
||||||
|
|
@ -438,6 +439,7 @@ function isInventoryDrilldownFrameIntent(intent: AddressIntent | undefined): boo
|
||||||
intent === "inventory_purchase_provenance_for_item" ||
|
intent === "inventory_purchase_provenance_for_item" ||
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date"
|
intent === "inventory_aging_by_purchase_date"
|
||||||
|
|
@ -449,6 +451,7 @@ function isInventoryLifecycleHistoryIntent(intent: AddressIntent | undefined): b
|
||||||
intent === "inventory_purchase_provenance_for_item" ||
|
intent === "inventory_purchase_provenance_for_item" ||
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain"
|
intent === "inventory_purchase_to_sale_chain"
|
||||||
);
|
);
|
||||||
|
|
@ -798,11 +801,32 @@ export function hasInventoryPurchaseDateVatBridgeCue(text: string): boolean {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hasInventoryMarginRankingFollowupCue(text: string): boolean {
|
||||||
|
const normalized = textWithRepairedVariant(String(text ?? ""))
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ё/g, "е");
|
||||||
|
if (!normalized.trim()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const wantsFoundRows =
|
||||||
|
/(?:покажи|показать|выведи|дай|раскрой|show|list|покажи|показать|выведи|дай|раскрой)/iu.test(normalized) &&
|
||||||
|
/(?:найденн|строк|реализац|себестоимостн|баз|найденн|строк|реализац|себестоимостн|баз)/iu.test(normalized) &&
|
||||||
|
/(?:себестоимостн|реализац|марж|прибыл|номенклатур|себестоимостн|реализац|марж|прибыл|номенклат)/iu.test(normalized);
|
||||||
|
const account41Not01 =
|
||||||
|
/\b41(?:[.,]\d{1,2})?\b/iu.test(normalized) &&
|
||||||
|
/\b01(?:[.,]\d{1,2})?\b/iu.test(normalized) &&
|
||||||
|
/(?:\bне\b|вместо|а\s+не|not|instead|РЅРµ|вместо|Р°\s+РЅРµ)/iu.test(normalized);
|
||||||
|
return wantsFoundRows || account41Not01;
|
||||||
|
}
|
||||||
|
|
||||||
export function hasAddressFollowupContextSignal(text: string): boolean {
|
export function hasAddressFollowupContextSignal(text: string): boolean {
|
||||||
const normalized = String(text ?? "").trim();
|
const normalized = String(text ?? "").trim();
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (hasInventoryMarginRankingFollowupCue(normalized)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
/(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(
|
/(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(
|
||||||
normalized
|
normalized
|
||||||
|
|
@ -1115,6 +1139,7 @@ function mergeFollowupFilters(
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date" ||
|
intent === "inventory_aging_by_purchase_date" ||
|
||||||
|
|
@ -1175,6 +1200,7 @@ function mergeFollowupFilters(
|
||||||
(intent === "inventory_purchase_provenance_for_item" ||
|
(intent === "inventory_purchase_provenance_for_item" ||
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date")
|
intent === "inventory_aging_by_purchase_date")
|
||||||
|
|
@ -1412,6 +1438,7 @@ function mergeFollowupFilters(
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date" ||
|
intent === "inventory_aging_by_purchase_date" ||
|
||||||
|
|
@ -1424,6 +1451,9 @@ function mergeFollowupFilters(
|
||||||
const currentContractExplicit = toNonEmptyString(merged.contract);
|
const currentContractExplicit = toNonEmptyString(merged.contract);
|
||||||
const currentItemExplicit = toNonEmptyString(merged.item);
|
const currentItemExplicit = toNonEmptyString(merged.item);
|
||||||
const currentAccountExplicit = toNonEmptyString(merged.account);
|
const currentAccountExplicit = toNonEmptyString(merged.account);
|
||||||
|
const currentAccountRefinesMarginDomain =
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" &&
|
||||||
|
hasInventoryMarginRankingFollowupCue(userMessage);
|
||||||
const shouldSuppressGenericPeriodCarryover =
|
const shouldSuppressGenericPeriodCarryover =
|
||||||
(Boolean(currentCounterpartyExplicit) &&
|
(Boolean(currentCounterpartyExplicit) &&
|
||||||
!isLowQualityCounterpartyAnchor(currentCounterpartyExplicit) &&
|
!isLowQualityCounterpartyAnchor(currentCounterpartyExplicit) &&
|
||||||
|
|
@ -1432,7 +1462,7 @@ function mergeFollowupFilters(
|
||||||
!isLowQualityContractAnchor(currentContractExplicit) &&
|
!isLowQualityContractAnchor(currentContractExplicit) &&
|
||||||
currentContractExplicit !== previousContract) ||
|
currentContractExplicit !== previousContract) ||
|
||||||
(Boolean(currentItemExplicit) && currentItemExplicit !== previousItem) ||
|
(Boolean(currentItemExplicit) && currentItemExplicit !== previousItem) ||
|
||||||
(Boolean(currentAccountExplicit) && currentAccountExplicit !== previousAccount);
|
(Boolean(currentAccountExplicit) && currentAccountExplicit !== previousAccount && !currentAccountRefinesMarginDomain);
|
||||||
const vatRelativeMonthFollowup =
|
const vatRelativeMonthFollowup =
|
||||||
relativeMonthFromFollowupYear &&
|
relativeMonthFromFollowupYear &&
|
||||||
(intent === "vat_payable_confirmed_as_of_date" ||
|
(intent === "vat_payable_confirmed_as_of_date" ||
|
||||||
|
|
@ -1488,6 +1518,22 @@ function mergeFollowupFilters(
|
||||||
reasons.push("period_from_followup_context");
|
reasons.push("period_from_followup_context");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" &&
|
||||||
|
previousHasPeriod &&
|
||||||
|
hasInventoryMarginRankingFollowupCue(userMessage) &&
|
||||||
|
!hasExplicitPeriodInMessage &&
|
||||||
|
!hasExplicitCurrentDateInMessage
|
||||||
|
) {
|
||||||
|
if (previousPeriodFrom && merged.period_from !== previousPeriodFrom) {
|
||||||
|
merged.period_from = previousPeriodFrom;
|
||||||
|
}
|
||||||
|
if (previousPeriodTo && merged.period_to !== previousPeriodTo) {
|
||||||
|
merged.period_to = previousPeriodTo;
|
||||||
|
}
|
||||||
|
reasons.push("period_from_followup_context");
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!currentHasPeriod &&
|
!currentHasPeriod &&
|
||||||
previousHasPeriod &&
|
previousHasPeriod &&
|
||||||
|
|
@ -1563,6 +1609,7 @@ function resolveMissingRequiredFilters(intent: AddressIntent, filters: AddressFi
|
||||||
account_balance_snapshot: ["account", "as_of_date"],
|
account_balance_snapshot: ["account", "as_of_date"],
|
||||||
documents_forming_balance: ["account", "as_of_date"],
|
documents_forming_balance: ["account", "as_of_date"],
|
||||||
inventory_on_hand_as_of_date: ["as_of_date"],
|
inventory_on_hand_as_of_date: ["as_of_date"],
|
||||||
|
inventory_margin_ranking_for_nomenclature: ["period_from", "period_to"],
|
||||||
inventory_profitability_for_item: ["item"],
|
inventory_profitability_for_item: ["item"],
|
||||||
open_contracts_confirmed_as_of_date: ["as_of_date"],
|
open_contracts_confirmed_as_of_date: ["as_of_date"],
|
||||||
payables_confirmed_as_of_date: ["as_of_date"],
|
payables_confirmed_as_of_date: ["as_of_date"],
|
||||||
|
|
@ -1648,6 +1695,26 @@ function deriveIntentWithFollowupContext(
|
||||||
previousCounterpartyLaneActive && !hasExplicitInventoryItemReference;
|
previousCounterpartyLaneActive && !hasExplicitInventoryItemReference;
|
||||||
const inventoryPurchaseDateVatBridge =
|
const inventoryPurchaseDateVatBridge =
|
||||||
inventorySelectedObjectFollowup && hasInventoryPurchaseDateVatBridgeCue(normalizedMessage);
|
inventorySelectedObjectFollowup && hasInventoryPurchaseDateVatBridgeCue(normalizedMessage);
|
||||||
|
const marginRankingLineageActive =
|
||||||
|
sourceIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
|
fallbackIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
|
followupContext.root_intent === "inventory_margin_ranking_for_nomenclature";
|
||||||
|
|
||||||
|
if (
|
||||||
|
marginRankingLineageActive &&
|
||||||
|
hasInventoryMarginRankingFollowupCue(normalizedMessage) &&
|
||||||
|
(detectedIntent.intent === "unknown" ||
|
||||||
|
detectedIntent.intent === "account_balance_snapshot" ||
|
||||||
|
detectedIntent.intent === "documents_forming_balance" ||
|
||||||
|
detectedIntent.intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
|
detectedIntent.intent === sourceIntent)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
intent: "inventory_margin_ranking_for_nomenclature",
|
||||||
|
confidence: "low",
|
||||||
|
reasons: [...detectedIntent.reasons, "intent_adjusted_to_inventory_margin_ranking_followup_context"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
inventoryPurchaseDateVatBridge &&
|
inventoryPurchaseDateVatBridge &&
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,81 @@ function inventoryProfitabilityPeriodLabel(options: InventoryComposeOptions, dep
|
||||||
return asOfDate ? `до ${deps.formatDateRu(asOfDate)}` : "по доступной выборке";
|
return asOfDate ? `до ${deps.formatDateRu(asOfDate)}` : "по доступной выборке";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function asksForInventoryCostBaseRows(userMessage: string | null | undefined): boolean {
|
||||||
|
const text = String(userMessage ?? "").toLowerCase();
|
||||||
|
if (!/(?:покажи|показать|выведи|вывести|дай|дать|раскрой|раскрыть|строк|строки|строку|баз)/iu.test(text)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(?:себестоимостн|себестоимост|себестоим|закупочн|закупк|90\.02|\b41\b|баз)/iu.test(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InventoryMarginRankingEntry {
|
||||||
|
item: string;
|
||||||
|
revenue: number;
|
||||||
|
costProxy: number;
|
||||||
|
spread: number;
|
||||||
|
marginPct: number | null;
|
||||||
|
saleQuantity: number;
|
||||||
|
purchaseQuantity: number;
|
||||||
|
saleDocuments: number;
|
||||||
|
purchaseDocuments: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function inventoryRowItemLabel(row: ComposeStageRow, deps: InventoryReplyDeps): string | null {
|
||||||
|
return deps.summarizeInventoryTraceRows([row]).item;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildInventoryMarginRankingEntries(rows: ComposeStageRow[], deps: InventoryReplyDeps): InventoryMarginRankingEntry[] {
|
||||||
|
const byItem = new Map<string, { item: string; saleRows: ComposeStageRow[]; purchaseRows: ComposeStageRow[] }>();
|
||||||
|
for (const row of rows) {
|
||||||
|
const item = inventoryRowItemLabel(row, deps);
|
||||||
|
if (!item) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const key = item.trim().toLocaleLowerCase("ru");
|
||||||
|
const current = byItem.get(key) ?? { item, saleRows: [], purchaseRows: [] };
|
||||||
|
if (deps.isInventorySaleMovement(row)) {
|
||||||
|
current.saleRows.push(row);
|
||||||
|
}
|
||||||
|
if (deps.isInventoryPurchaseMovement(row)) {
|
||||||
|
current.purchaseRows.push(row);
|
||||||
|
}
|
||||||
|
byItem.set(key, current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(byItem.values())
|
||||||
|
.map((entry) => {
|
||||||
|
const revenue = sumInventoryRowAmount(entry.saleRows);
|
||||||
|
const costProxy = sumInventoryRowAmount(entry.purchaseRows);
|
||||||
|
const spread = revenue - costProxy;
|
||||||
|
return {
|
||||||
|
item: entry.item,
|
||||||
|
revenue,
|
||||||
|
costProxy,
|
||||||
|
spread,
|
||||||
|
marginPct: revenue > 0 ? (spread / revenue) * 100 : null,
|
||||||
|
saleQuantity: sumInventoryRowQuantity(entry.saleRows),
|
||||||
|
purchaseQuantity: sumInventoryRowQuantity(entry.purchaseRows),
|
||||||
|
saleDocuments: entry.saleRows.length,
|
||||||
|
purchaseDocuments: entry.purchaseRows.length
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((entry) => entry.revenue > 0 || entry.costProxy > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatInventoryMarginRankingLine(
|
||||||
|
entry: InventoryMarginRankingEntry,
|
||||||
|
index: number,
|
||||||
|
deps: InventoryReplyDeps
|
||||||
|
): string {
|
||||||
|
return `${index + 1}. ${entry.item} — выручка ${deps.formatMoneyRub(entry.revenue)}, себестоимостная база ${deps.formatMoneyRub(
|
||||||
|
entry.costProxy
|
||||||
|
)}, валовая разница ${deps.formatMoneyRub(entry.spread)}, маржа ${formatInventoryPercent(
|
||||||
|
entry.marginPct,
|
||||||
|
deps.formatNumberWithDots
|
||||||
|
)}.`;
|
||||||
|
}
|
||||||
|
|
||||||
export function composeInventoryReply(
|
export function composeInventoryReply(
|
||||||
intent: AddressIntent,
|
intent: AddressIntent,
|
||||||
rows: ComposeStageRow[],
|
rows: ComposeStageRow[],
|
||||||
|
|
@ -175,17 +250,23 @@ export function composeInventoryReply(
|
||||||
const uniqueWarehouses = deps.uniqueStrings(
|
const uniqueWarehouses = deps.uniqueStrings(
|
||||||
positions.map((item) => String(item.warehouse ?? "").trim()).filter((item) => item.length > 0)
|
positions.map((item) => String(item.warehouse ?? "").trim()).filter((item) => item.length > 0)
|
||||||
);
|
);
|
||||||
const totalQuantity = positions.reduce((sum, item) => sum + item.quantity, 0);
|
|
||||||
const totalAmount = positions.reduce((sum, item) => sum + item.amount, 0);
|
const totalAmount = positions.reduce((sum, item) => sum + item.amount, 0);
|
||||||
const directAnswerLine =
|
const directAnswerLine =
|
||||||
positions.length > 0
|
positions.length > 0
|
||||||
? `На ${deps.formatDateRu(asOfDate)} на складе подтверждено ${deps.formatNumberWithDots(positions.length)} позиций с остатком на ${deps.formatMoneyRub(totalAmount)}.`
|
? `На ${deps.formatDateRu(asOfDate)} на складе подтверждено ${deps.formatNumberWithDots(positions.length)} позиций на ${deps.formatMoneyRub(totalAmount)}.`
|
||||||
: `На ${deps.formatDateRu(asOfDate)} подтвержденных товарных остатков по счету 41.01 не найдено.`;
|
: `На ${deps.formatDateRu(asOfDate)} подтвержденных товарных остатков по счету 41.01 не найдено.`;
|
||||||
const lines: string[] = [directAnswerLine];
|
const lines: string[] = [directAnswerLine];
|
||||||
|
|
||||||
if (positions.length > 0) {
|
if (positions.length > 0) {
|
||||||
const visiblePositionsLimit = 6;
|
const visiblePositionsLimit = 6;
|
||||||
const visiblePositions = positions.slice(0, visiblePositionsLimit);
|
const positionsByAmount = [...positions].sort((left, right) => {
|
||||||
|
const amountDelta = right.amount - left.amount;
|
||||||
|
if (amountDelta !== 0) {
|
||||||
|
return amountDelta;
|
||||||
|
}
|
||||||
|
return left.item.localeCompare(right.item, "ru");
|
||||||
|
});
|
||||||
|
const visiblePositions = positionsByAmount.slice(0, visiblePositionsLimit);
|
||||||
appendInventorySection(
|
appendInventorySection(
|
||||||
lines,
|
lines,
|
||||||
"Позиции:",
|
"Позиции:",
|
||||||
|
|
@ -213,11 +294,14 @@ export function composeInventoryReply(
|
||||||
`Позиции с остатком: ${deps.formatNumberWithDots(positions.length)}.`,
|
`Позиции с остатком: ${deps.formatNumberWithDots(positions.length)}.`,
|
||||||
`Уникальных товаров: ${deps.formatNumberWithDots(uniqueItems.length)}.`,
|
`Уникальных товаров: ${deps.formatNumberWithDots(uniqueItems.length)}.`,
|
||||||
`Уникальных складов: ${deps.formatNumberWithDots(uniqueWarehouses.length)}.`,
|
`Уникальных складов: ${deps.formatNumberWithDots(uniqueWarehouses.length)}.`,
|
||||||
`Суммарное количество: ${deps.formatNumberWithDots(totalQuantity, 3)}.`
|
"Общее количество не свожу в один управленческий показатель, потому что в остатках смешаны разнородные позиции."
|
||||||
]);
|
]);
|
||||||
if (rows.length !== positions.length) {
|
if (rows.length !== positions.length) {
|
||||||
lines.push(`- Проверенных строк движения: ${deps.formatNumberWithDots(rows.length)}.`);
|
lines.push(`- Проверенных строк движения: ${deps.formatNumberWithDots(rows.length)}.`);
|
||||||
}
|
}
|
||||||
|
if (positions.length > 0) {
|
||||||
|
lines.push("- Следующий шаг: могу раскрыть полный список, разложить остатки по складам или сравнить с другой датой.");
|
||||||
|
}
|
||||||
|
|
||||||
return positions.length > 0
|
return positions.length > 0
|
||||||
? buildFactualListReply(lines, buildConfirmedBalanceSemantics("strong"))
|
? buildFactualListReply(lines, buildConfirmedBalanceSemantics("strong"))
|
||||||
|
|
@ -539,6 +623,129 @@ export function composeInventoryReply(
|
||||||
: buildFactualSummaryReply(lines, buildConfirmedBalanceSemantics("medium", false));
|
: buildFactualSummaryReply(lines, buildConfirmedBalanceSemantics("medium", false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (intent === "inventory_margin_ranking_for_nomenclature") {
|
||||||
|
const entries = buildInventoryMarginRankingEntries(rows, deps);
|
||||||
|
const confirmedEntries = entries.filter((entry) => entry.revenue > 0 && entry.costProxy > 0);
|
||||||
|
const highMargin = [...confirmedEntries]
|
||||||
|
.sort((left, right) => right.spread - left.spread || (right.marginPct ?? -Infinity) - (left.marginPct ?? -Infinity))
|
||||||
|
.slice(0, 5);
|
||||||
|
const lowMargin = [...confirmedEntries]
|
||||||
|
.sort((left, right) => left.spread - right.spread || (left.marginPct ?? Infinity) - (right.marginPct ?? Infinity))
|
||||||
|
.slice(0, 5);
|
||||||
|
const salesWithoutCost = entries.filter((entry) => entry.revenue > 0 && entry.costProxy <= 0);
|
||||||
|
const purchasesWithoutSales = entries.filter((entry) => entry.costProxy > 0 && entry.revenue <= 0);
|
||||||
|
const periodLabel = inventoryProfitabilityPeriodLabel(options, deps);
|
||||||
|
const totalRevenue = entries.reduce((sum, entry) => sum + entry.revenue, 0);
|
||||||
|
const totalCostProxy = entries.reduce((sum, entry) => sum + entry.costProxy, 0);
|
||||||
|
const totalSpread = totalRevenue - totalCostProxy;
|
||||||
|
if (confirmedEntries.length === 0) {
|
||||||
|
const costBaseRowsRequested = asksForInventoryCostBaseRows(options.userMessage);
|
||||||
|
const lines: string[] = [
|
||||||
|
costBaseRowsRequested && purchasesWithoutSales.length === 0
|
||||||
|
? `За период ${periodLabel} подтвержденных строк себестоимостной базы по реализованной номенклатуре не найдено.`
|
||||||
|
: `За период ${periodLabel} рейтинг прибыльности номенклатуры построить нельзя.`
|
||||||
|
];
|
||||||
|
const findings: string[] = [];
|
||||||
|
if (salesWithoutCost.length > 0) {
|
||||||
|
const salesCount = deps.formatNumberWithDots(salesWithoutCost.length);
|
||||||
|
const salesItemPhrase =
|
||||||
|
salesWithoutCost.length === 1 ? "1 номенклатурной позиции" : `${salesCount} номенклатурным позициям`;
|
||||||
|
findings.push(
|
||||||
|
`Есть реализация по ${salesItemPhrase}.`
|
||||||
|
);
|
||||||
|
findings.push(
|
||||||
|
salesWithoutCost.length === 1
|
||||||
|
? "Подтвержденной себестоимости реализации по этой позиции не найдено."
|
||||||
|
: "Подтвержденной себестоимости реализации по этим позициям не найдено."
|
||||||
|
);
|
||||||
|
findings.push("Поэтому валовую прибыль и маржинальность честно посчитать нельзя.");
|
||||||
|
}
|
||||||
|
if (costBaseRowsRequested && purchasesWithoutSales.length === 0) {
|
||||||
|
findings.push("Строк себестоимости реализации / себестоимостной базы для показа нет.");
|
||||||
|
}
|
||||||
|
if (purchasesWithoutSales.length > 0) {
|
||||||
|
const purchaseCount = deps.formatNumberWithDots(purchasesWithoutSales.length);
|
||||||
|
const purchaseItemPhrase =
|
||||||
|
purchasesWithoutSales.length === 1 ? "1 позиции" : `${purchaseCount} позициям`;
|
||||||
|
findings.push(
|
||||||
|
purchasesWithoutSales.length === 1
|
||||||
|
? `Есть себестоимостная база по ${purchaseItemPhrase}, но реализации по ней в периоде не найдено.`
|
||||||
|
: `Есть себестоимостная база по ${purchaseItemPhrase}, но реализации по ним в периоде не найдено.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (entries.length === 0) {
|
||||||
|
findings.push("В доступной выборке нет достаточных строк реализации и себестоимости по номенклатуре.");
|
||||||
|
}
|
||||||
|
appendInventoryBulletSection(lines, "Что нашлось:", findings);
|
||||||
|
lines.push(
|
||||||
|
`Вывод: за период ${periodLabel} нет достаточной базы для рейтинга «высокая / низкая прибыль» по номенклатуре.`
|
||||||
|
);
|
||||||
|
const nextActions: string[] = [];
|
||||||
|
if (salesWithoutCost.length > 0) {
|
||||||
|
nextActions.push("показать найденные реализации за этот период;");
|
||||||
|
}
|
||||||
|
if (purchasesWithoutSales.length > 0) {
|
||||||
|
nextActions.push("показать найденные строки себестоимостной базы за этот период;");
|
||||||
|
}
|
||||||
|
nextActions.push(
|
||||||
|
"расширить период до квартала или года;",
|
||||||
|
"попробовать строгий расчет по проводкам 90.01 / 90.02;",
|
||||||
|
"построить управленческий proxy по закупочным документам, если такой способ допустим для вашей проверки."
|
||||||
|
);
|
||||||
|
appendInventoryBulletSection(lines, "Что можно сделать дальше:", nextActions);
|
||||||
|
appendInventoryBulletSection(lines, "Граница ответа:", [
|
||||||
|
"Прибыльность номенклатуры считаю только когда есть реализация и подтвержденная себестоимость реализации.",
|
||||||
|
"Это не показатель чистой прибыли и не замена закрытию месяца."
|
||||||
|
]);
|
||||||
|
return buildFactualSummaryReply(lines, buildConfirmedBalanceSemantics(entries.length > 0 ? "medium" : "weak", false));
|
||||||
|
}
|
||||||
|
const directAnswerLine =
|
||||||
|
confirmedEntries.length > 0
|
||||||
|
? `За период ${periodLabel} собран рейтинг реализованной номенклатуры по валовой маржинальности: выручка ${deps.formatMoneyRub(
|
||||||
|
totalRevenue
|
||||||
|
)}, себестоимостная база ${deps.formatMoneyRub(totalCostProxy)}, расчетная валовая разница ${deps.formatMoneyRub(
|
||||||
|
totalSpread
|
||||||
|
)}.`
|
||||||
|
: `За период ${periodLabel} не удалось подтвердить рейтинг прибыльности номенклатуры: нужны одновременно строки реализации и закупочного/себестоимостного следа по товарам.`;
|
||||||
|
const lines: string[] = [directAnswerLine];
|
||||||
|
|
||||||
|
if (highMargin.length > 0) {
|
||||||
|
appendInventorySection(
|
||||||
|
lines,
|
||||||
|
"Высокая валовая маржинальность:",
|
||||||
|
highMargin.map((entry, index) => formatInventoryMarginRankingLine(entry, index, deps))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (lowMargin.length > 0) {
|
||||||
|
appendInventorySection(
|
||||||
|
lines,
|
||||||
|
"Низкая или отрицательная валовая маржинальность:",
|
||||||
|
lowMargin.map((entry, index) => formatInventoryMarginRankingLine(entry, index, deps))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const boundaryLines = [
|
||||||
|
"Это управленческий расчет валовой маржинальности по реализации и доступной себестоимостной базе, не показатель чистой прибыли.",
|
||||||
|
"Для строгого бухгалтерского расчета нужны проводки 90.01 / 90.02 и закрытие себестоимости; этот ответ не подменяет закрытие месяца."
|
||||||
|
];
|
||||||
|
if (salesWithoutCost.length > 0) {
|
||||||
|
boundaryLines.push(
|
||||||
|
`По ${deps.formatNumberWithDots(salesWithoutCost.length)} позициям есть продажи, но нет подтвержденной себестоимости реализации — их нельзя честно ранжировать по прибыли.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (purchasesWithoutSales.length > 0) {
|
||||||
|
boundaryLines.push(
|
||||||
|
`По ${deps.formatNumberWithDots(purchasesWithoutSales.length)} позициям есть себестоимостная база без реализации в этом периоде.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
appendInventoryBulletSection(lines, "Граница ответа:", boundaryLines);
|
||||||
|
|
||||||
|
return buildFactualSummaryReply(
|
||||||
|
lines,
|
||||||
|
buildConfirmedBalanceSemantics(confirmedEntries.length > 0 ? "strong" : entries.length > 0 ? "medium" : "weak", confirmedEntries.length > 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (intent === "inventory_profitability_for_item") {
|
if (intent === "inventory_profitability_for_item") {
|
||||||
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
|
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
|
||||||
const saleRows = rows.filter((row) => deps.isInventorySaleMovement(row));
|
const saleRows = rows.filter((row) => deps.isInventorySaleMovement(row));
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,14 @@ function sessionOrganizationName(
|
||||||
return toNonEmptyString(scope?.selectedOrganization) ?? toNonEmptyString(scope?.activeOrganization);
|
return toNonEmptyString(scope?.selectedOrganization) ?? toNonEmptyString(scope?.activeOrganization);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sessionKnownOrganizations(
|
||||||
|
sessionOrganizationScope: BuildAssistantAddressOrchestrationRuntimeInput["sessionOrganizationScope"]
|
||||||
|
): unknown[] {
|
||||||
|
const scope = toRecordObject(sessionOrganizationScope);
|
||||||
|
const knownOrganizations = scope?.knownOrganizations;
|
||||||
|
return Array.isArray(knownOrganizations) ? knownOrganizations : [];
|
||||||
|
}
|
||||||
|
|
||||||
function predecomposeOrganizationName(
|
function predecomposeOrganizationName(
|
||||||
predecomposeContract: Record<string, unknown> | null,
|
predecomposeContract: Record<string, unknown> | null,
|
||||||
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]
|
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]
|
||||||
|
|
@ -130,6 +138,122 @@ function mergeOrganizationIntoDiscoveryFollowupContext(
|
||||||
return base;
|
return base;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function compactLower(value: unknown): string {
|
||||||
|
return String(value ?? "")
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasCompactCashflowFollowupSignal(text: string | null): boolean {
|
||||||
|
const value = compactLower(text);
|
||||||
|
if (!value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(?:\u0431\u0435\u0437\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0431\u0438\u0432\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0440\u0435\u0437\p{L}*|\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b\p{L}*|\u043e\u0434\u043d\u043e\u0439\s+\u0441\u0442\u0440\u043e\u043a\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0438\u0442\u043e\u0433|\u043f\u0440\u0438\u0448\p{L}*[\s\S]{0,80}\u0443\u0448\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|\u043f\u043e\u043b\u0443\u0447\p{L}*[\s\S]{0,80}\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*[\s\S]{0,80}\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b)/iu.test(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dateScopeToFilterWindow(dateScope: string | null): Record<string, string> | null {
|
||||||
|
if (!dateScope) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (/^(?:19|20)\d{2}$/.test(dateScope)) {
|
||||||
|
return {
|
||||||
|
period_from: `${dateScope}-01-01`,
|
||||||
|
period_to: `${dateScope}-12-31`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (/^(?:19|20)\d{2}-\d{2}-\d{2}$/.test(dateScope)) {
|
||||||
|
return { as_of_date: dateScope };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function looksLikeReliableOrganizationScope(value: string | null): boolean {
|
||||||
|
const text = compactLower(value);
|
||||||
|
if (!text) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (/(?:\u0440\u0430\u0437\u0431\u0438\u0432|\u0440\u0430\u0437\u0440\u0435\u0437|\u0432\u0445\u043e\u0434\u044f\u0449|\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u043d\u0435\u0442\u0442\u043e|\u0442\u043e\u043f)/iu.test(text)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(?:^|[\s"'\u00ab])(?:\u043e\u043e\u043e|\u0438\u043f|\u043f\u0430\u043e|\u0430\u043e|\u0437\u0430\u043e)(?:[\s"'\u00bb]|$)|\u043e\u0431\u0449\u0435\u0441\u0442\u0432\p{L}*\s+\u0441\s+\u043e\u0433\u0440\u0430\u043d\u0438\u0447\p{L}*\s+\u043e\u0442\u0432\p{L}*|\b(?:llc|inc|corp)\b/iu.test(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function inferBusinessOverviewDiscoveryContextFromSessionItems(
|
||||||
|
sessionItems: unknown[],
|
||||||
|
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]
|
||||||
|
): Record<string, unknown> | null {
|
||||||
|
for (let index = sessionItems.length - 1; index >= 0; index -= 1) {
|
||||||
|
const item = toRecordObject(sessionItems[index]);
|
||||||
|
if (toNonEmptyString(item?.role) !== "assistant") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const debug = toRecordObject(item?.debug);
|
||||||
|
const entryPoint = toRecordObject(debug?.assistant_mcp_discovery_entry_point_v1);
|
||||||
|
const turnInput = toRecordObject(entryPoint?.turn_input);
|
||||||
|
const dataNeedGraph = toRecordObject(turnInput?.data_need_graph);
|
||||||
|
if (toNonEmptyString(dataNeedGraph?.business_fact_family) !== "business_overview") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const turnMeaningRef = toRecordObject(turnInput?.turn_meaning_ref);
|
||||||
|
const filterWindow = dateScopeToFilterWindow(toNonEmptyString(turnMeaningRef?.explicit_date_scope));
|
||||||
|
if (!filterWindow) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const previousFilters: Record<string, unknown> = { ...filterWindow };
|
||||||
|
const organization = toNonEmptyString(turnMeaningRef?.explicit_organization_scope);
|
||||||
|
if (looksLikeReliableOrganizationScope(organization)) {
|
||||||
|
previousFilters.organization = organization;
|
||||||
|
}
|
||||||
|
const rankingNeed = toNonEmptyString(dataNeedGraph?.ranking_need);
|
||||||
|
return {
|
||||||
|
previous_discovery_pilot_scope: "business_overview_route_template_v1",
|
||||||
|
previous_filters: previousFilters,
|
||||||
|
root_filters: { ...previousFilters },
|
||||||
|
previous_seeded_ranking_need: rankingNeed,
|
||||||
|
previous_intent: "business_overview",
|
||||||
|
root_intent: "business_overview"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeBusinessOverviewDateContextForCompactCashflow(input: {
|
||||||
|
userMessage: string;
|
||||||
|
followupContext: Record<string, unknown> | null;
|
||||||
|
sessionItems: unknown[];
|
||||||
|
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"];
|
||||||
|
}): Record<string, unknown> | null {
|
||||||
|
if (!hasCompactCashflowFollowupSignal(input.userMessage)) {
|
||||||
|
return input.followupContext;
|
||||||
|
}
|
||||||
|
const existingFilters = toRecordObject(input.followupContext?.previous_filters);
|
||||||
|
if (
|
||||||
|
input.toNonEmptyString(existingFilters?.period_from) ||
|
||||||
|
input.toNonEmptyString(existingFilters?.period_to) ||
|
||||||
|
input.toNonEmptyString(existingFilters?.as_of_date)
|
||||||
|
) {
|
||||||
|
return input.followupContext;
|
||||||
|
}
|
||||||
|
const inferred = inferBusinessOverviewDiscoveryContextFromSessionItems(input.sessionItems, input.toNonEmptyString);
|
||||||
|
if (!inferred) {
|
||||||
|
return input.followupContext;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...inferred,
|
||||||
|
...(input.followupContext ?? {}),
|
||||||
|
previous_filters: {
|
||||||
|
...(toRecordObject(inferred.previous_filters) ?? {}),
|
||||||
|
...(toRecordObject(input.followupContext?.previous_filters) ?? {})
|
||||||
|
},
|
||||||
|
root_filters: {
|
||||||
|
...(toRecordObject(inferred.root_filters) ?? {}),
|
||||||
|
...(toRecordObject(input.followupContext?.root_filters) ?? {})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function hasSelectedObjectInventorySignal(text: string | null): boolean {
|
function hasSelectedObjectInventorySignal(text: string | null): boolean {
|
||||||
return /(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ним|selected\s+object)/iu.test(
|
return /(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ним|selected\s+object)/iu.test(
|
||||||
String(text ?? "")
|
String(text ?? "")
|
||||||
|
|
@ -157,6 +281,7 @@ function isInventorySelectedObjectOrRootIntent(intent: string | null): boolean {
|
||||||
intent === "inventory_purchase_provenance_for_item" ||
|
intent === "inventory_purchase_provenance_for_item" ||
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date"
|
intent === "inventory_aging_by_purchase_date"
|
||||||
|
|
@ -175,6 +300,18 @@ function isGenericCanonicalDriftIntent(intent: string | null): boolean {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasInventoryMarginRankingAccountCorrectionCue(text: string | null): boolean {
|
||||||
|
const value = String(text ?? "").toLowerCase();
|
||||||
|
if (!value.trim()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
/\b41(?:[.,]\d{1,2})?\b/iu.test(value) &&
|
||||||
|
/\b01(?:[.,]\d{1,2})?\b/iu.test(value) &&
|
||||||
|
/(?:\u0430\s+\u043d\u0435|\u043d\u0435|\u0432\u043c\u0435\u0441\u0442\u043e|not|instead)/iu.test(value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function hasSameDateFollowupSignal(text: string | null): boolean {
|
function hasSameDateFollowupSignal(text: string | null): boolean {
|
||||||
return /(?:эту\s+же\s+дат(?:у|е|ой)|ту\s+же\s+дат(?:у|е|ой)|same\s+date)/iu.test(String(text ?? ""));
|
return /(?:эту\s+же\s+дат(?:у|е|ой)|ту\s+же\s+дат(?:у|е|ой)|same\s+date)/iu.test(String(text ?? ""));
|
||||||
}
|
}
|
||||||
|
|
@ -240,6 +377,12 @@ function shouldPreferRawFollowupMessage(
|
||||||
const hasInventoryFrameCarryover =
|
const hasInventoryFrameCarryover =
|
||||||
isInventorySelectedObjectOrRootIntent(previousIntent) ||
|
isInventorySelectedObjectOrRootIntent(previousIntent) ||
|
||||||
isInventorySelectedObjectOrRootIntent(rootIntent);
|
isInventorySelectedObjectOrRootIntent(rootIntent);
|
||||||
|
const hasInventoryMarginRankingCarryover =
|
||||||
|
previousIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
|
rootIntent === "inventory_margin_ranking_for_nomenclature";
|
||||||
|
const hasInventoryMarginRankingAccountCorrection =
|
||||||
|
hasInventoryMarginRankingCarryover &&
|
||||||
|
[rawMessage, canonicalMessage].some((message) => hasInventoryMarginRankingAccountCorrectionCue(message));
|
||||||
const hasDocumentCarryover =
|
const hasDocumentCarryover =
|
||||||
previousIntent === "list_documents_by_counterparty" || previousIntent === "list_documents_by_contract";
|
previousIntent === "list_documents_by_counterparty" || previousIntent === "list_documents_by_contract";
|
||||||
|
|
||||||
|
|
@ -263,6 +406,13 @@ function shouldPreferRawFollowupMessage(
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
hasInventoryMarginRankingAccountCorrection &&
|
||||||
|
(intent === "account_balance_snapshot" || intent === "documents_forming_balance" || intent === "unknown")
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
(hasSelectedObjectInventorySignal(rawMessage) || hasInventoryItemCarryover) &&
|
(hasSelectedObjectInventorySignal(rawMessage) || hasInventoryItemCarryover) &&
|
||||||
(hasSelectedObjectInventoryActionCue(rawMessage) || hasShortInventoryPurchaseFollowupCue(rawMessage)) &&
|
(hasSelectedObjectInventoryActionCue(rawMessage) || hasShortInventoryPurchaseFollowupCue(rawMessage)) &&
|
||||||
|
|
@ -363,12 +513,18 @@ export async function buildAssistantAddressOrchestrationRuntime(
|
||||||
const orchestrationContract = toRecordObject(orchestrationDecision.orchestrationContract);
|
const orchestrationContract = toRecordObject(orchestrationDecision.orchestrationContract);
|
||||||
const predecomposeContract = toRecordObject(addressPreDecompose.predecomposeContract);
|
const predecomposeContract = toRecordObject(addressPreDecompose.predecomposeContract);
|
||||||
const explicitPredecomposeOrganization = predecomposeOrganizationName(predecomposeContract, input.toNonEmptyString);
|
const explicitPredecomposeOrganization = predecomposeOrganizationName(predecomposeContract, input.toNonEmptyString);
|
||||||
const discoveryFollowupContext = mergeOrganizationIntoDiscoveryFollowupContext(
|
const discoveryFollowupContextWithOrganization = mergeOrganizationIntoDiscoveryFollowupContext(
|
||||||
followupContext,
|
followupContext,
|
||||||
explicitPredecomposeOrganization
|
explicitPredecomposeOrganization
|
||||||
? null
|
? null
|
||||||
: sessionOrganizationName(input.sessionOrganizationScope ?? null, input.toNonEmptyString)
|
: sessionOrganizationName(input.sessionOrganizationScope ?? null, input.toNonEmptyString)
|
||||||
);
|
);
|
||||||
|
const discoveryFollowupContext = mergeBusinessOverviewDateContextForCompactCashflow({
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
followupContext: discoveryFollowupContextWithOrganization,
|
||||||
|
sessionItems: input.sessionItems,
|
||||||
|
toNonEmptyString: input.toNonEmptyString
|
||||||
|
});
|
||||||
const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(
|
const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(
|
||||||
input.userMessage,
|
input.userMessage,
|
||||||
addressInputMessage,
|
addressInputMessage,
|
||||||
|
|
@ -384,7 +540,8 @@ export async function buildAssistantAddressOrchestrationRuntime(
|
||||||
effectiveMessage: addressInputMessage,
|
effectiveMessage: addressInputMessage,
|
||||||
assistantTurnMeaning: toRecordObject(orchestrationContract?.assistant_turn_meaning),
|
assistantTurnMeaning: toRecordObject(orchestrationContract?.assistant_turn_meaning),
|
||||||
predecomposeContract,
|
predecomposeContract,
|
||||||
followupContext: discoveryFollowupContext
|
followupContext: discoveryFollowupContext,
|
||||||
|
knownOrganizations: sessionKnownOrganizations(input.sessionOrganizationScope ?? null)
|
||||||
})) as Record<string, unknown>;
|
})) as Record<string, unknown>;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
mcpDiscoveryRuntimeEntryPointError = String(error instanceof Error ? error.message : error ?? "unknown_error").slice(0, 280);
|
mcpDiscoveryRuntimeEntryPointError = String(error instanceof Error ? error.message : error ?? "unknown_error").slice(0, 280);
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,13 @@ function formatMissingAnchors(anchors: string[]): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildClarificationReply(binding: AssistantCapabilityRuntimeBindingContract): string {
|
function buildClarificationReply(binding: AssistantCapabilityRuntimeBindingContract): string {
|
||||||
|
if (binding.capability_contract_id === "inventory_inventory_margin_ranking_for_nomenclature") {
|
||||||
|
return [
|
||||||
|
"Для рейтинга прибыльности номенклатуры нужен период.",
|
||||||
|
"Могу посчитать по номенклатуре: выручку без НДС, себестоимость реализации, валовую прибыль и маржинальность.",
|
||||||
|
"Уточните период: месяц, квартал, год или весь доступный период."
|
||||||
|
].join("\n\n");
|
||||||
|
}
|
||||||
return [
|
return [
|
||||||
"Нужно уточнение, чтобы не подставить неподтвержденный объект в расчет.",
|
"Нужно уточнение, чтобы не подставить неподтвержденный объект в расчет.",
|
||||||
`Не хватает: ${formatMissingAnchors(binding.missing_anchors)}.`,
|
`Не хватает: ${formatMissingAnchors(binding.missing_anchors)}.`,
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,15 @@ function anchorSatisfied(requiredAnchor: string, providedAnchors: string[], debu
|
||||||
if (providedAnchors.includes(requiredAnchor)) {
|
if (providedAnchors.includes(requiredAnchor)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (requiredAnchor === "period") {
|
||||||
|
return (
|
||||||
|
(hasValue(filters?.period_from) && hasValue(filters?.period_to)) ||
|
||||||
|
hasValue(filters?.as_of_date) ||
|
||||||
|
providedAnchors.includes("period_from") ||
|
||||||
|
providedAnchors.includes("period_to") ||
|
||||||
|
providedAnchors.includes("as_of_date")
|
||||||
|
);
|
||||||
|
}
|
||||||
if (requiredAnchor === "item") {
|
if (requiredAnchor === "item") {
|
||||||
return (
|
return (
|
||||||
providedAnchors.includes("selected_object") ||
|
providedAnchors.includes("selected_object") ||
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,278 @@
|
||||||
|
import type { AssistantMcpDiscoveryDataNeedGraphContract } from "./assistantMcpDiscoveryDataNeedGraph";
|
||||||
|
import type {
|
||||||
|
AssistantMcpDiscoveryPlanContract,
|
||||||
|
AssistantMcpDiscoveryPrimitive,
|
||||||
|
AssistantMcpDiscoveryTurnMeaningRef
|
||||||
|
} from "./assistantMcpDiscoveryPolicy";
|
||||||
|
|
||||||
|
export const ASSISTANT_EVIDENCE_PLANNER_SCHEMA_VERSION = "assistant_evidence_planner_v1" as const;
|
||||||
|
|
||||||
|
export type AssistantEvidencePlannerStatus = "ready_for_execution" | "needs_clarification" | "blocked";
|
||||||
|
export type AssistantEvidenceCoverageExpectation = "confirmed_coverage" | "bounded_inference" | "clarification";
|
||||||
|
export type AssistantEvidenceAnswerMode =
|
||||||
|
| "confirmed_business_answer"
|
||||||
|
| "bounded_business_inference"
|
||||||
|
| "clarification_required"
|
||||||
|
| "checked_sources_only";
|
||||||
|
|
||||||
|
export interface AssistantEvidenceDataNeedContract {
|
||||||
|
business_fact_family: string | null;
|
||||||
|
action_family: string | null;
|
||||||
|
aggregation_need: string | null;
|
||||||
|
comparison_need: string | null;
|
||||||
|
ranking_need: string | null;
|
||||||
|
time_scope_need: string | null;
|
||||||
|
proof_expectation: string | null;
|
||||||
|
subject_candidates: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantEvidenceAxesContract {
|
||||||
|
required_axes: string[];
|
||||||
|
provided_axes: string[];
|
||||||
|
missing_axes: string[];
|
||||||
|
user_actionable_missing_axes: string[];
|
||||||
|
clarification_gaps: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantEvidencePrimitivePlanContract {
|
||||||
|
selected_chain_id: string;
|
||||||
|
allowed_primitives: AssistantMcpDiscoveryPrimitive[];
|
||||||
|
rejected_primitives: string[];
|
||||||
|
execution_budget: AssistantMcpDiscoveryPlanContract["execution_budget"];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantEvidenceCoverageGateContract {
|
||||||
|
requires_evidence_gate: true;
|
||||||
|
expected_coverage: AssistantEvidenceCoverageExpectation;
|
||||||
|
answer_permission_if_satisfied: AssistantEvidenceAnswerMode;
|
||||||
|
answer_may_use_raw_model_claims: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantEvidenceAnswerContract {
|
||||||
|
answer_mode: AssistantEvidenceAnswerMode;
|
||||||
|
required_user_layers: string[];
|
||||||
|
forbidden_overclaim_flags: string[];
|
||||||
|
must_keep_internal_mechanics_hidden: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantEvidencePlannerContract {
|
||||||
|
schema_version: typeof ASSISTANT_EVIDENCE_PLANNER_SCHEMA_VERSION;
|
||||||
|
policy_owner: "assistantEvidencePlanner";
|
||||||
|
planner_status: AssistantEvidencePlannerStatus;
|
||||||
|
semantic_data_need: string | null;
|
||||||
|
selected_chain_id: string;
|
||||||
|
data_need: AssistantEvidenceDataNeedContract;
|
||||||
|
evidence_axes: AssistantEvidenceAxesContract;
|
||||||
|
primitive_plan: AssistantEvidencePrimitivePlanContract;
|
||||||
|
coverage_gate: AssistantEvidenceCoverageGateContract;
|
||||||
|
answer_contract: AssistantEvidenceAnswerContract;
|
||||||
|
reason_codes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BuildAssistantEvidencePlannerInput {
|
||||||
|
selectedChainId: string;
|
||||||
|
plannerStatus: AssistantEvidencePlannerStatus;
|
||||||
|
semanticDataNeed?: string | null;
|
||||||
|
dataNeedGraph?: AssistantMcpDiscoveryDataNeedGraphContract | null;
|
||||||
|
discoveryPlan: AssistantMcpDiscoveryPlanContract;
|
||||||
|
additionalMissingAxes?: string[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toNonEmptyString(value: unknown): string | null {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const text = String(value).trim();
|
||||||
|
return text.length > 0 ? text : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeReasonCode(value: string): string | null {
|
||||||
|
const normalized = value
|
||||||
|
.trim()
|
||||||
|
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
|
||||||
|
.replace(/^_+|_+$/g, "")
|
||||||
|
.toLowerCase();
|
||||||
|
return normalized.length > 0 ? normalized.slice(0, 120) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushReason(target: string[], value: string): void {
|
||||||
|
const normalized = normalizeReasonCode(value);
|
||||||
|
if (normalized && !target.includes(normalized)) {
|
||||||
|
target.push(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function uniqueStrings(values: unknown[]): string[] {
|
||||||
|
const result: string[] = [];
|
||||||
|
for (const value of values) {
|
||||||
|
const text = toNonEmptyString(value);
|
||||||
|
if (text && !result.includes(text)) {
|
||||||
|
result.push(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function providedAxesFromMeaning(meaning: AssistantMcpDiscoveryTurnMeaningRef | null): string[] {
|
||||||
|
const result: string[] = [];
|
||||||
|
if ((meaning?.explicit_entity_candidates?.length ?? 0) > 0) {
|
||||||
|
result.push("counterparty");
|
||||||
|
result.push("business_entity");
|
||||||
|
}
|
||||||
|
if (toNonEmptyString(meaning?.explicit_organization_scope)) {
|
||||||
|
result.push("organization");
|
||||||
|
}
|
||||||
|
if (toNonEmptyString(meaning?.explicit_date_scope)) {
|
||||||
|
result.push("period");
|
||||||
|
}
|
||||||
|
if (toNonEmptyString(meaning?.asked_aggregation_axis)) {
|
||||||
|
result.push("aggregate_axis");
|
||||||
|
}
|
||||||
|
if (toNonEmptyString(meaning?.metadata_scope_hint)) {
|
||||||
|
result.push("metadata_scope");
|
||||||
|
}
|
||||||
|
return uniqueStrings(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
function missingAxes(requiredAxes: string[], providedAxes: string[]): string[] {
|
||||||
|
return requiredAxes.filter((axis) => !providedAxes.includes(axis));
|
||||||
|
}
|
||||||
|
|
||||||
|
const USER_ACTIONABLE_AXIS_SET = new Set([
|
||||||
|
"counterparty",
|
||||||
|
"business_entity",
|
||||||
|
"organization",
|
||||||
|
"period",
|
||||||
|
"as_of_date",
|
||||||
|
"item",
|
||||||
|
"supplier",
|
||||||
|
"buyer",
|
||||||
|
"warehouse",
|
||||||
|
"document",
|
||||||
|
"contract",
|
||||||
|
"metadata_scope",
|
||||||
|
"lane_family_choice"
|
||||||
|
]);
|
||||||
|
|
||||||
|
function userActionableMissingAxes(axes: string[]): string[] {
|
||||||
|
return axes.filter((axis) => USER_ACTIONABLE_AXIS_SET.has(axis));
|
||||||
|
}
|
||||||
|
|
||||||
|
function coverageExpectationFor(
|
||||||
|
graph: AssistantMcpDiscoveryDataNeedGraphContract | null
|
||||||
|
): AssistantEvidenceCoverageExpectation {
|
||||||
|
if (graph?.proof_expectation === "bounded_inference") {
|
||||||
|
return "bounded_inference";
|
||||||
|
}
|
||||||
|
if (graph?.proof_expectation === "clarification_required") {
|
||||||
|
return "clarification";
|
||||||
|
}
|
||||||
|
return "confirmed_coverage";
|
||||||
|
}
|
||||||
|
|
||||||
|
function answerModeFor(input: {
|
||||||
|
plannerStatus: AssistantEvidencePlannerStatus;
|
||||||
|
coverageExpectation: AssistantEvidenceCoverageExpectation;
|
||||||
|
}): AssistantEvidenceAnswerMode {
|
||||||
|
if (input.plannerStatus === "needs_clarification") {
|
||||||
|
return "clarification_required";
|
||||||
|
}
|
||||||
|
if (input.plannerStatus === "blocked") {
|
||||||
|
return "checked_sources_only";
|
||||||
|
}
|
||||||
|
if (input.coverageExpectation === "bounded_inference") {
|
||||||
|
return "bounded_business_inference";
|
||||||
|
}
|
||||||
|
if (input.coverageExpectation === "clarification") {
|
||||||
|
return "clarification_required";
|
||||||
|
}
|
||||||
|
return "confirmed_business_answer";
|
||||||
|
}
|
||||||
|
|
||||||
|
function requiredUserLayersFor(answerMode: AssistantEvidenceAnswerMode): string[] {
|
||||||
|
if (answerMode === "clarification_required") {
|
||||||
|
return ["clarifying_question", "why_needed", "available_calculation"];
|
||||||
|
}
|
||||||
|
if (answerMode === "bounded_business_inference") {
|
||||||
|
return ["business_conclusion", "key_figures", "evidence_boundary", "next_step"];
|
||||||
|
}
|
||||||
|
if (answerMode === "checked_sources_only") {
|
||||||
|
return ["checked_sources_boundary", "unknowns", "next_probe"];
|
||||||
|
}
|
||||||
|
return ["direct_business_answer", "key_figures", "evidence_boundary", "next_step"];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAssistantEvidencePlanner(
|
||||||
|
input: BuildAssistantEvidencePlannerInput
|
||||||
|
): AssistantEvidencePlannerContract {
|
||||||
|
const graph = input.dataNeedGraph ?? null;
|
||||||
|
const plan = input.discoveryPlan;
|
||||||
|
const turnMeaning = plan.turn_meaning_ref;
|
||||||
|
const requiredAxes = uniqueStrings(plan.required_axes);
|
||||||
|
const providedAxes = providedAxesFromMeaning(turnMeaning);
|
||||||
|
const graphClarificationGaps = uniqueStrings(graph?.clarification_gaps ?? []);
|
||||||
|
const additionalAxisGaps = uniqueStrings(input.additionalMissingAxes ?? []).filter(
|
||||||
|
(axis) => !providedAxes.includes(axis) && (requiredAxes.includes(axis) || USER_ACTIONABLE_AXIS_SET.has(axis)),
|
||||||
|
);
|
||||||
|
const axisGaps = uniqueStrings([...additionalAxisGaps, ...missingAxes(requiredAxes, providedAxes)]);
|
||||||
|
const actionableAxisGaps = userActionableMissingAxes(axisGaps);
|
||||||
|
const clarificationGaps = uniqueStrings([...graphClarificationGaps, ...axisGaps]);
|
||||||
|
const coverageExpectation = coverageExpectationFor(graph);
|
||||||
|
const answerMode = answerModeFor({
|
||||||
|
plannerStatus: input.plannerStatus,
|
||||||
|
coverageExpectation
|
||||||
|
});
|
||||||
|
const reasonCodes = uniqueStrings(plan.reason_codes);
|
||||||
|
|
||||||
|
pushReason(reasonCodes, "evidence_planner_contract_built");
|
||||||
|
if (graph) {
|
||||||
|
pushReason(reasonCodes, "evidence_planner_consumed_data_need_graph");
|
||||||
|
}
|
||||||
|
if (clarificationGaps.length > 0) {
|
||||||
|
pushReason(reasonCodes, "evidence_planner_has_missing_axes_or_gaps");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
schema_version: ASSISTANT_EVIDENCE_PLANNER_SCHEMA_VERSION,
|
||||||
|
policy_owner: "assistantEvidencePlanner",
|
||||||
|
planner_status: input.plannerStatus,
|
||||||
|
semantic_data_need: toNonEmptyString(input.semanticDataNeed),
|
||||||
|
selected_chain_id: input.selectedChainId,
|
||||||
|
data_need: {
|
||||||
|
business_fact_family: graph?.business_fact_family ?? null,
|
||||||
|
action_family: graph?.action_family ?? null,
|
||||||
|
aggregation_need: graph?.aggregation_need ?? null,
|
||||||
|
comparison_need: graph?.comparison_need ?? null,
|
||||||
|
ranking_need: graph?.ranking_need ?? null,
|
||||||
|
time_scope_need: graph?.time_scope_need ?? null,
|
||||||
|
proof_expectation: graph?.proof_expectation ?? null,
|
||||||
|
subject_candidates: uniqueStrings(graph?.subject_candidates ?? [])
|
||||||
|
},
|
||||||
|
evidence_axes: {
|
||||||
|
required_axes: requiredAxes,
|
||||||
|
provided_axes: providedAxes,
|
||||||
|
missing_axes: axisGaps,
|
||||||
|
user_actionable_missing_axes: actionableAxisGaps,
|
||||||
|
clarification_gaps: clarificationGaps
|
||||||
|
},
|
||||||
|
primitive_plan: {
|
||||||
|
selected_chain_id: input.selectedChainId,
|
||||||
|
allowed_primitives: plan.allowed_primitives,
|
||||||
|
rejected_primitives: plan.rejected_primitives,
|
||||||
|
execution_budget: plan.execution_budget
|
||||||
|
},
|
||||||
|
coverage_gate: {
|
||||||
|
requires_evidence_gate: true,
|
||||||
|
expected_coverage: coverageExpectation,
|
||||||
|
answer_permission_if_satisfied: answerMode,
|
||||||
|
answer_may_use_raw_model_claims: false
|
||||||
|
},
|
||||||
|
answer_contract: {
|
||||||
|
answer_mode: answerMode,
|
||||||
|
required_user_layers: requiredUserLayersFor(answerMode),
|
||||||
|
forbidden_overclaim_flags: uniqueStrings(graph?.forbidden_overclaim_flags ?? []),
|
||||||
|
must_keep_internal_mechanics_hidden: true
|
||||||
|
},
|
||||||
|
reason_codes: reasonCodes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1392,26 +1392,18 @@ function derivedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutio
|
||||||
const organization = organizationScope ? ` по организации ${organizationScope}` : "";
|
const organization = organizationScope ? ` по организации ${organizationScope}` : "";
|
||||||
const counterparty = flow.counterparty ? ` по контрагенту ${flow.counterparty}` : "";
|
const counterparty = flow.counterparty ? ` по контрагенту ${flow.counterparty}` : "";
|
||||||
const period = flow.period_scope ? ` за период ${flow.period_scope}` : " в проверенном окне";
|
const period = flow.period_scope ? ` за период ${flow.period_scope}` : " в проверенном окне";
|
||||||
const movementLabel =
|
|
||||||
flow.value_flow_direction === "outgoing_supplier_payout"
|
|
||||||
? "исходящих платежей/списаний"
|
|
||||||
: "входящих денежных поступлений";
|
|
||||||
const totalLabel =
|
const totalLabel =
|
||||||
flow.value_flow_direction === "outgoing_supplier_payout"
|
flow.value_flow_direction === "outgoing_supplier_payout"
|
||||||
? "сумма исходящих платежей/списаний составляет"
|
? "исходящие платежи/списания"
|
||||||
: "сумма входящих денежных поступлений составляет";
|
: "входящие денежные поступления";
|
||||||
const caveat =
|
const caveat =
|
||||||
flow.value_flow_direction === "outgoing_supplier_payout"
|
flow.value_flow_direction === "outgoing_supplier_payout"
|
||||||
? "Это расчет по найденным строкам 1С, а не подтверждение полного объема платежей вне проверенного окна."
|
? "Граница: это проверенный денежный срез по найденным списаниям в 1С, не внешний аудит всех платежей вне этого окна."
|
||||||
: "Это расчет по найденным строкам 1С, а не подтверждение полного объема поступлений вне проверенного окна.";
|
: "Граница: это проверенный денежный срез по найденным поступлениям в 1С, не внешний аудит всех поступлений вне этого окна.";
|
||||||
const dates =
|
|
||||||
flow.first_movement_date && flow.latest_movement_date
|
|
||||||
? ` Первая найденная дата движения: ${flow.first_movement_date}; последняя: ${flow.latest_movement_date}.`
|
|
||||||
: "";
|
|
||||||
const limitCaveat = flow.coverage_limited_by_probe_limit
|
const limitCaveat = flow.coverage_limited_by_probe_limit
|
||||||
? " Лимит строк проверки достигнут; полный запрошенный период может быть покрыт не полностью."
|
? " Проверка уперлась в лимит строк, поэтому полный период может быть покрыт не полностью."
|
||||||
: "";
|
: "";
|
||||||
return `По найденным строкам ${movementLabel} в 1С${counterparty}${period} ${totalLabel} ${flow.total_amount_human_ru} Учтено строк с суммой: ${flow.rows_with_amount} из ${flow.rows_matched}.${dates}${limitCaveat} ${caveat}`;
|
return `${totalLabel}${counterparty || organization}${period}: ${flow.total_amount_human_ru}.${limitCaveat} ${caveat}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function derivedValueFlowMonthlyLines(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] {
|
function derivedValueFlowMonthlyLines(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] {
|
||||||
|
|
@ -2109,7 +2101,7 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
|
||||||
: pilot.derived_ranked_value_flow && derivedValueLine
|
: pilot.derived_ranked_value_flow && derivedValueLine
|
||||||
? [derivedValueLine]
|
? [derivedValueLine]
|
||||||
: derivedValueLine
|
: derivedValueLine
|
||||||
? [...pilot.evidence.confirmed_facts, derivedValueLine, ...monthlyConfirmedLines]
|
? [derivedValueLine, ...monthlyConfirmedLines]
|
||||||
: valueFlowZeroResultConfirmedLine(pilot)
|
: valueFlowZeroResultConfirmedLine(pilot)
|
||||||
? [valueFlowZeroResultConfirmedLine(pilot)!]
|
? [valueFlowZeroResultConfirmedLine(pilot)!]
|
||||||
: derivedEntityResolutionLine
|
: derivedEntityResolutionLine
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,18 @@ function hasBusinessOverviewDirectMoneyAnswerHint(input: {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const text = input.rawUtterance;
|
const text = input.rawUtterance;
|
||||||
|
if (
|
||||||
|
/(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)[\s\S]{0,80}(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\p{L}*|\u0443\u0448\p{L}*|\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*)|(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e)[\s\S]{0,80}(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)/iu.test(
|
||||||
|
text
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
/(?:\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}+|\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?\b|\u0431\u0435\u0437\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0431\u0438\u0432\p{L}*|\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b\p{L}*|\u043e\u0434\u043d\u043e\u0439\s+\u0441\u0442\u0440\u043e\u043a\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0438\u0442\u043e\u0433|\u0434\u0435\u043d\p{L}{0,20}\s+\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\u043b\p{L}*[\s\S]{0,80}\u0443\u0448\u043b\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|\u0432\u0445\u043e\u0434\u044f\u0449\p{L}*[\s\S]{0,80}\u0438\u0441\u0445\u043e\u0434\u044f\u0449\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e)/iu.test(text)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|how\s+much)[\s\S]{0,120}(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u0432\u044b\u0440\u0443\u0447|\u0434\u0435\u043d\p{L}*|\u043f\u043e\u043b\u0443\u0447|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*)|(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u0432\u044b\u0440\u0443\u0447)[\s\S]{0,120}(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|\u0432\u0441\u0435\u0433\u043e|\u0432\u043e\u043e\u0431\u0449\u0435|(?:19|20)\d{2}|all\s+time)|(?:\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0430\u044f|\u043a\u0430\u043a\u0438\u0435|which|what)[\s\S]{0,80}(?:\u0441\u0430\u043c\p{L}*|top|best|most)[\s\S]{0,80}(?:\u0434\u043e\u0445\u043e\u0434\u043d|\u0432\u044b\u0440\u0443\u0447|\u043e\u0431\u043e\u0440\u043e\u0442|revenue|turnover)[\s\S]{0,40}(?:\u0433\u043e\u0434|year)/iu.test(
|
return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|how\s+much)[\s\S]{0,120}(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u0432\u044b\u0440\u0443\u0447|\u0434\u0435\u043d\p{L}*|\u043f\u043e\u043b\u0443\u0447|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*)|(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u0432\u044b\u0440\u0443\u0447)[\s\S]{0,120}(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|\u0432\u0441\u0435\u0433\u043e|\u0432\u043e\u043e\u0431\u0449\u0435|(?:19|20)\d{2}|all\s+time)|(?:\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0430\u044f|\u043a\u0430\u043a\u0438\u0435|which|what)[\s\S]{0,80}(?:\u0441\u0430\u043c\p{L}*|top|best|most)[\s\S]{0,80}(?:\u0434\u043e\u0445\u043e\u0434\u043d|\u0432\u044b\u0440\u0443\u0447|\u043e\u0431\u043e\u0440\u043e\u0442|revenue|turnover)[\s\S]{0,40}(?:\u0433\u043e\u0434|year)/iu.test(
|
||||||
text
|
text
|
||||||
);
|
);
|
||||||
|
|
@ -293,6 +305,12 @@ function rankingNeedFromRawUtterance(value: string): string | null {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (/\u0438\u0441\u043a\u043b\u044e\u0447[\p{L}\p{N}_]*\s+\u0442\u043e\u043f/iu.test(text)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (/(?:\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?\b|\u043d\u0435\s+\u0442\u043e\u043f\b|\u0438\u0441\u043a\u043b\u044e\u0447\w*\s+\u0442\u043e\u043f|\u0431\u0435\u0437\s+\u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0430\b|без\s+топ(?:ов|а)?\b|не\s+топ\b|исключ\S*\s+топ|без\s+рейтинга\b)/iu.test(text)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (/(?:\u0442\u043e\u043f[-\s]?\d*|\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435|\u044b\u0435)|\u0431\u043e\u043b\u044c\u0448\u0435\s+\u0432\u0441\u0435\u0433\u043e|\u043d\u0430\u0438\u0431\u043e\u043b[\u0435\u0451]\u0435|\u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d|\u043c\u0430\u043a\u0441\u0438\u043c\u0443\u043c|\u043a\u0440\u0443\u043f\u043d\u0435\u0439\u0448|\u043b\u0443\u0447\u0448\u0438\u0439)/iu.test(text)) {
|
if (/(?:\u0442\u043e\u043f[-\s]?\d*|\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435|\u044b\u0435)|\u0431\u043e\u043b\u044c\u0448\u0435\s+\u0432\u0441\u0435\u0433\u043e|\u043d\u0430\u0438\u0431\u043e\u043b[\u0435\u0451]\u0435|\u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d|\u043c\u0430\u043a\u0441\u0438\u043c\u0443\u043c|\u043a\u0440\u0443\u043f\u043d\u0435\u0439\u0448|\u043b\u0443\u0447\u0448\u0438\u0439)/iu.test(text)) {
|
||||||
return "top_desc";
|
return "top_desc";
|
||||||
}
|
}
|
||||||
|
|
@ -324,6 +342,16 @@ function rankingNeedFromRawUtterance(value: string): string | null {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function suppressRankingNeedFromRawUtterance(value: string): boolean {
|
||||||
|
const text = lower(value);
|
||||||
|
if (!text) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(?:\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?\b|\u043d\u0435\s+\u0442\u043e\u043f\b|\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440\b|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}+|\u0438\u0441\u043a\u043b\u044e\u0447\w*\s+\u0442\u043e\u043f|\u0431\u0435\u0437\s+\u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0430\b|без\s+топ(?:ов|а)?\b|не\s+топ\b|не\s+обзор\b|просто\s+ден\p{L}+|исключ\S*\s+топ|без\s+рейтинга\b)/iu.test(
|
||||||
|
text
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function proofExpectationFor(input: {
|
function proofExpectationFor(input: {
|
||||||
family: string | null;
|
family: string | null;
|
||||||
clarificationGaps: string[];
|
clarificationGaps: string[];
|
||||||
|
|
@ -497,6 +525,7 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
|
||||||
const action = lower(turnMeaning?.asked_action_family);
|
const action = lower(turnMeaning?.asked_action_family);
|
||||||
const unsupported = lower(turnMeaning?.unsupported_but_understood_family);
|
const unsupported = lower(turnMeaning?.unsupported_but_understood_family);
|
||||||
const rawUtterance = lower(input.rawUtterance);
|
const rawUtterance = lower(input.rawUtterance);
|
||||||
|
const rawQuestionSignal = lower([input.rawUtterance, turnMeaning?.raw_message, turnMeaning?.effective_message].join(" "));
|
||||||
const aggregationAxis = lower(turnMeaning?.asked_aggregation_axis);
|
const aggregationAxis = lower(turnMeaning?.asked_aggregation_axis);
|
||||||
const seededRankingNeed = toNonEmptyString(turnMeaning?.seeded_ranking_need);
|
const seededRankingNeed = toNonEmptyString(turnMeaning?.seeded_ranking_need);
|
||||||
const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope);
|
const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope);
|
||||||
|
|
@ -514,16 +543,22 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
|
||||||
});
|
});
|
||||||
const aggregationNeed = aggregationNeedFor(aggregationAxis);
|
const aggregationNeed = aggregationNeedFor(aggregationAxis);
|
||||||
const comparisonNeed = comparisonNeedFor(action);
|
const comparisonNeed = comparisonNeedFor(action);
|
||||||
const rankingNeed = rankingNeedFromRawUtterance(rawUtterance) ?? seededRankingNeed;
|
|
||||||
const allTimeScopeHint = hasAllTimeScopeHint(rawUtterance);
|
const allTimeScopeHint = hasAllTimeScopeHint(rawUtterance);
|
||||||
const subjectScopedBidirectionalAllTime =
|
const subjectScopedBidirectionalAllTime =
|
||||||
businessFactFamily === "value_flow" &&
|
businessFactFamily === "value_flow" &&
|
||||||
comparisonNeed === "incoming_vs_outgoing" &&
|
comparisonNeed === "incoming_vs_outgoing" &&
|
||||||
subjectCandidates.length > 0 &&
|
subjectCandidates.length > 0 &&
|
||||||
!explicitDateScope;
|
!explicitDateScope;
|
||||||
|
const suppressRankingNeed =
|
||||||
|
suppressRankingNeedFromRawUtterance(rawQuestionSignal) ||
|
||||||
|
/(?:\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?|\u043d\u0435\s+\u0442\u043e\u043f|\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}+|\u0438\u0441\u043a\u043b\u044e\u0447\w*\s+\u0442\u043e\u043f|\u0431\u0435\u0437\s+\u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0430)/iu.test(
|
||||||
|
rawQuestionSignal
|
||||||
|
);
|
||||||
|
const rawRankingNeed = rankingNeedFromRawUtterance(rawQuestionSignal);
|
||||||
|
const rankingNeed = suppressRankingNeed ? null : rawRankingNeed ?? seededRankingNeed;
|
||||||
const directBusinessOverviewMoneyAnswerHint = hasBusinessOverviewDirectMoneyAnswerHint({
|
const directBusinessOverviewMoneyAnswerHint = hasBusinessOverviewDirectMoneyAnswerHint({
|
||||||
family: businessFactFamily,
|
family: businessFactFamily,
|
||||||
rawUtterance,
|
rawUtterance: rawQuestionSignal,
|
||||||
rankingNeed
|
rankingNeed
|
||||||
});
|
});
|
||||||
const oneSidedOpenScopeTotalHint = hasOpenScopeOneSidedValueTotalHintUtf8Safe(rawUtterance, action);
|
const oneSidedOpenScopeTotalHint = hasOpenScopeOneSidedValueTotalHintUtf8Safe(rawUtterance, action);
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,20 @@
|
||||||
import type { AssistantMcpDiscoveryRuntimeEntryPointContract } from "./assistantMcpDiscoveryRuntimeEntryPoint";
|
import type { AssistantMcpDiscoveryRuntimeEntryPointContract } from "./assistantMcpDiscoveryRuntimeEntryPoint";
|
||||||
import type { AssistantMcpRouteCandidateContract } from "./assistantMcpDiscoveryRuntimeBridge";
|
import type { AssistantMcpRouteCandidateContract } from "./assistantMcpDiscoveryRuntimeBridge";
|
||||||
|
import type { AssistantMcpDiscoveryExecutionHandoffContract } from "./assistantMcpDiscoveryExecutionHandoff";
|
||||||
|
import type { AssistantEvidencePlannerContract } from "./assistantEvidencePlanner";
|
||||||
|
|
||||||
export interface AssistantMcpDiscoveryDebugAttachmentFields {
|
export interface AssistantMcpDiscoveryDebugAttachmentFields {
|
||||||
assistant_mcp_discovery_entry_point_v1: AssistantMcpDiscoveryRuntimeEntryPointContract | null;
|
assistant_mcp_discovery_entry_point_v1: AssistantMcpDiscoveryRuntimeEntryPointContract | null;
|
||||||
mcp_discovery_entry_status: string | null;
|
mcp_discovery_entry_status: string | null;
|
||||||
mcp_discovery_attempted: boolean;
|
mcp_discovery_attempted: boolean;
|
||||||
mcp_discovery_hot_runtime_wired: false;
|
mcp_discovery_hot_runtime_wired: boolean;
|
||||||
mcp_discovery_bridge_status: string | null;
|
mcp_discovery_bridge_status: string | null;
|
||||||
mcp_discovery_selected_chain_id: string | null;
|
mcp_discovery_selected_chain_id: string | null;
|
||||||
|
mcp_discovery_evidence_plan_v1: AssistantEvidencePlannerContract | null;
|
||||||
|
mcp_discovery_evidence_plan_status: string | null;
|
||||||
|
mcp_discovery_evidence_plan_answer_mode: string | null;
|
||||||
|
mcp_discovery_evidence_plan_missing_axes: string[];
|
||||||
|
mcp_discovery_evidence_plan_expected_coverage: string | null;
|
||||||
mcp_discovery_catalog_chain_template_matches: string[];
|
mcp_discovery_catalog_chain_template_matches: string[];
|
||||||
mcp_discovery_catalog_chain_alignment_status: string | null;
|
mcp_discovery_catalog_chain_alignment_status: string | null;
|
||||||
mcp_discovery_catalog_chain_top_match: string | null;
|
mcp_discovery_catalog_chain_top_match: string | null;
|
||||||
|
|
@ -17,11 +24,18 @@ export interface AssistantMcpDiscoveryDebugAttachmentFields {
|
||||||
mcp_discovery_route_candidate_fact_family: string | null;
|
mcp_discovery_route_candidate_fact_family: string | null;
|
||||||
mcp_discovery_route_candidate_action_family: string | null;
|
mcp_discovery_route_candidate_action_family: string | null;
|
||||||
mcp_discovery_route_candidate_proof_expectation: string | null;
|
mcp_discovery_route_candidate_proof_expectation: string | null;
|
||||||
|
mcp_discovery_route_candidate_evidence_plan_status: string | null;
|
||||||
|
mcp_discovery_route_candidate_evidence_answer_mode: string | null;
|
||||||
|
mcp_discovery_route_candidate_evidence_expected_coverage: string | null;
|
||||||
mcp_discovery_route_candidate_missing_axes: string[];
|
mcp_discovery_route_candidate_missing_axes: string[];
|
||||||
mcp_discovery_route_candidate_provided_axes: string[];
|
mcp_discovery_route_candidate_provided_axes: string[];
|
||||||
mcp_discovery_route_candidate_executable_now: boolean;
|
mcp_discovery_route_candidate_executable_now: boolean;
|
||||||
mcp_discovery_route_candidate_enablement_reason: string | null;
|
mcp_discovery_route_candidate_enablement_reason: string | null;
|
||||||
mcp_discovery_route_candidate_next_action: string | null;
|
mcp_discovery_route_candidate_next_action: string | null;
|
||||||
|
mcp_discovery_execution_handoff_v1: AssistantMcpDiscoveryExecutionHandoffContract | null;
|
||||||
|
mcp_discovery_execution_handoff_status: string | null;
|
||||||
|
mcp_discovery_execution_handoff_allowed_hot_chain: boolean;
|
||||||
|
mcp_discovery_execution_handoff_can_use_guarded_response: boolean;
|
||||||
mcp_discovery_answer_mode: string | null;
|
mcp_discovery_answer_mode: string | null;
|
||||||
mcp_discovery_business_fact_answer_allowed: boolean;
|
mcp_discovery_business_fact_answer_allowed: boolean;
|
||||||
mcp_discovery_user_facing_response_allowed: boolean;
|
mcp_discovery_user_facing_response_allowed: boolean;
|
||||||
|
|
@ -78,6 +92,19 @@ function isRouteCandidateContract(value: unknown): value is AssistantMcpRouteCan
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isEvidencePlannerContract(value: unknown): value is AssistantEvidencePlannerContract {
|
||||||
|
const record = toRecordObject(value);
|
||||||
|
return record?.schema_version === "assistant_evidence_planner_v1" && record?.policy_owner === "assistantEvidencePlanner";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExecutionHandoffContract(value: unknown): value is AssistantMcpDiscoveryExecutionHandoffContract {
|
||||||
|
const record = toRecordObject(value);
|
||||||
|
return (
|
||||||
|
record?.schema_version === "assistant_mcp_discovery_execution_handoff_v1" &&
|
||||||
|
record?.policy_owner === "assistantMcpDiscoveryExecutionHandoff"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function resolveEntryPoint(input: AttachAssistantMcpDiscoveryDebugInput): AssistantMcpDiscoveryRuntimeEntryPointContract | null {
|
function resolveEntryPoint(input: AttachAssistantMcpDiscoveryDebugInput): AssistantMcpDiscoveryRuntimeEntryPointContract | null {
|
||||||
if (isMcpDiscoveryEntryPointContract(input.entryPoint)) {
|
if (isMcpDiscoveryEntryPointContract(input.entryPoint)) {
|
||||||
return input.entryPoint;
|
return input.entryPoint;
|
||||||
|
|
@ -95,17 +122,31 @@ export function buildAssistantMcpDiscoveryDebugAttachmentFields(
|
||||||
const entryPoint = resolveEntryPoint(input);
|
const entryPoint = resolveEntryPoint(input);
|
||||||
const bridge = toRecordObject(entryPoint?.bridge);
|
const bridge = toRecordObject(entryPoint?.bridge);
|
||||||
const planner = toRecordObject(bridge?.planner);
|
const planner = toRecordObject(bridge?.planner);
|
||||||
|
const evidencePlan = isEvidencePlannerContract(planner?.evidence_plan) ? planner.evidence_plan : null;
|
||||||
|
const evidencePlanAxes = toRecordObject(evidencePlan?.evidence_axes);
|
||||||
|
const evidencePlanCoverageGate = toRecordObject(evidencePlan?.coverage_gate);
|
||||||
|
const evidencePlanAnswerContract = toRecordObject(evidencePlan?.answer_contract);
|
||||||
const chainAlignment = toRecordObject(planner?.catalog_chain_template_alignment);
|
const chainAlignment = toRecordObject(planner?.catalog_chain_template_alignment);
|
||||||
const routeCandidate = isRouteCandidateContract(bridge?.route_candidate) ? bridge.route_candidate : null;
|
const routeCandidate = isRouteCandidateContract(bridge?.route_candidate) ? bridge.route_candidate : null;
|
||||||
|
const executionHandoff = isExecutionHandoffContract(bridge?.execution_handoff) ? bridge.execution_handoff : null;
|
||||||
const answerDraft = toRecordObject(bridge?.answer_draft);
|
const answerDraft = toRecordObject(bridge?.answer_draft);
|
||||||
|
const hotRuntimeWired =
|
||||||
|
entryPoint?.hot_runtime_wired === true ||
|
||||||
|
bridge?.hot_runtime_wired === true ||
|
||||||
|
executionHandoff?.can_use_guarded_response === true;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
assistant_mcp_discovery_entry_point_v1: entryPoint,
|
assistant_mcp_discovery_entry_point_v1: entryPoint,
|
||||||
mcp_discovery_entry_status: toNonEmptyString(entryPoint?.entry_status),
|
mcp_discovery_entry_status: toNonEmptyString(entryPoint?.entry_status),
|
||||||
mcp_discovery_attempted: Boolean(entryPoint?.discovery_attempted),
|
mcp_discovery_attempted: Boolean(entryPoint?.discovery_attempted),
|
||||||
mcp_discovery_hot_runtime_wired: false,
|
mcp_discovery_hot_runtime_wired: hotRuntimeWired,
|
||||||
mcp_discovery_bridge_status: toNonEmptyString(bridge?.bridge_status),
|
mcp_discovery_bridge_status: toNonEmptyString(bridge?.bridge_status),
|
||||||
mcp_discovery_selected_chain_id: toNonEmptyString(planner?.selected_chain_id),
|
mcp_discovery_selected_chain_id: toNonEmptyString(planner?.selected_chain_id),
|
||||||
|
mcp_discovery_evidence_plan_v1: evidencePlan,
|
||||||
|
mcp_discovery_evidence_plan_status: toNonEmptyString(evidencePlan?.planner_status),
|
||||||
|
mcp_discovery_evidence_plan_answer_mode: toNonEmptyString(evidencePlanAnswerContract?.answer_mode),
|
||||||
|
mcp_discovery_evidence_plan_missing_axes: toStringArray(evidencePlanAxes?.missing_axes),
|
||||||
|
mcp_discovery_evidence_plan_expected_coverage: toNonEmptyString(evidencePlanCoverageGate?.expected_coverage),
|
||||||
mcp_discovery_catalog_chain_template_matches: toStringArray(planner?.catalog_chain_template_matches),
|
mcp_discovery_catalog_chain_template_matches: toStringArray(planner?.catalog_chain_template_matches),
|
||||||
mcp_discovery_catalog_chain_alignment_status: toNonEmptyString(chainAlignment?.alignment_status),
|
mcp_discovery_catalog_chain_alignment_status: toNonEmptyString(chainAlignment?.alignment_status),
|
||||||
mcp_discovery_catalog_chain_top_match: toNonEmptyString(chainAlignment?.top_chain_template_match),
|
mcp_discovery_catalog_chain_top_match: toNonEmptyString(chainAlignment?.top_chain_template_match),
|
||||||
|
|
@ -115,11 +156,18 @@ export function buildAssistantMcpDiscoveryDebugAttachmentFields(
|
||||||
mcp_discovery_route_candidate_fact_family: toNonEmptyString(routeCandidate?.business_fact_family),
|
mcp_discovery_route_candidate_fact_family: toNonEmptyString(routeCandidate?.business_fact_family),
|
||||||
mcp_discovery_route_candidate_action_family: toNonEmptyString(routeCandidate?.action_family),
|
mcp_discovery_route_candidate_action_family: toNonEmptyString(routeCandidate?.action_family),
|
||||||
mcp_discovery_route_candidate_proof_expectation: toNonEmptyString(routeCandidate?.proof_expectation),
|
mcp_discovery_route_candidate_proof_expectation: toNonEmptyString(routeCandidate?.proof_expectation),
|
||||||
|
mcp_discovery_route_candidate_evidence_plan_status: toNonEmptyString(routeCandidate?.evidence_plan_status),
|
||||||
|
mcp_discovery_route_candidate_evidence_answer_mode: toNonEmptyString(routeCandidate?.evidence_answer_mode),
|
||||||
|
mcp_discovery_route_candidate_evidence_expected_coverage: toNonEmptyString(routeCandidate?.evidence_expected_coverage),
|
||||||
mcp_discovery_route_candidate_missing_axes: toStringArray(routeCandidate?.missing_axes),
|
mcp_discovery_route_candidate_missing_axes: toStringArray(routeCandidate?.missing_axes),
|
||||||
mcp_discovery_route_candidate_provided_axes: toStringArray(routeCandidate?.provided_axes),
|
mcp_discovery_route_candidate_provided_axes: toStringArray(routeCandidate?.provided_axes),
|
||||||
mcp_discovery_route_candidate_executable_now: routeCandidate?.executable_now === true,
|
mcp_discovery_route_candidate_executable_now: routeCandidate?.executable_now === true,
|
||||||
mcp_discovery_route_candidate_enablement_reason: toNonEmptyString(routeCandidate?.enablement_reason),
|
mcp_discovery_route_candidate_enablement_reason: toNonEmptyString(routeCandidate?.enablement_reason),
|
||||||
mcp_discovery_route_candidate_next_action: toNonEmptyString(routeCandidate?.recommended_next_action),
|
mcp_discovery_route_candidate_next_action: toNonEmptyString(routeCandidate?.recommended_next_action),
|
||||||
|
mcp_discovery_execution_handoff_v1: executionHandoff,
|
||||||
|
mcp_discovery_execution_handoff_status: toNonEmptyString(executionHandoff?.handoff_status),
|
||||||
|
mcp_discovery_execution_handoff_allowed_hot_chain: executionHandoff?.allowed_hot_chain === true,
|
||||||
|
mcp_discovery_execution_handoff_can_use_guarded_response: executionHandoff?.can_use_guarded_response === true,
|
||||||
mcp_discovery_answer_mode: toNonEmptyString(answerDraft?.answer_mode),
|
mcp_discovery_answer_mode: toNonEmptyString(answerDraft?.answer_mode),
|
||||||
mcp_discovery_business_fact_answer_allowed: bridge?.business_fact_answer_allowed === true,
|
mcp_discovery_business_fact_answer_allowed: bridge?.business_fact_answer_allowed === true,
|
||||||
mcp_discovery_user_facing_response_allowed: bridge?.user_facing_response_allowed === true,
|
mcp_discovery_user_facing_response_allowed: bridge?.user_facing_response_allowed === true,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
import type { AssistantMcpDiscoveryAnswerDraftContract } from "./assistantMcpDiscoveryAnswerAdapter";
|
||||||
|
import type { AssistantMcpDiscoveryPilotExecutionContract } from "./assistantMcpDiscoveryPilotExecutor";
|
||||||
|
import type { AssistantMcpDiscoveryChainId } from "./assistantMcpDiscoveryPlanner";
|
||||||
|
import type {
|
||||||
|
AssistantMcpDiscoveryRuntimeBridgeStatus,
|
||||||
|
AssistantMcpRouteCandidateContract
|
||||||
|
} from "./assistantMcpDiscoveryRuntimeBridge";
|
||||||
|
|
||||||
|
export const ASSISTANT_MCP_DISCOVERY_EXECUTION_HANDOFF_SCHEMA_VERSION =
|
||||||
|
"assistant_mcp_discovery_execution_handoff_v1" as const;
|
||||||
|
|
||||||
|
export type AssistantMcpDiscoveryExecutionHandoffStatus =
|
||||||
|
| "ready_for_guarded_response"
|
||||||
|
| "awaiting_user_scope"
|
||||||
|
| "checked_sources_only"
|
||||||
|
| "not_enabled_for_chain"
|
||||||
|
| "blocked";
|
||||||
|
|
||||||
|
export interface AssistantMcpDiscoveryExecutionHandoffInput {
|
||||||
|
bridgeStatus: AssistantMcpDiscoveryRuntimeBridgeStatus;
|
||||||
|
routeCandidate: AssistantMcpRouteCandidateContract;
|
||||||
|
pilot: AssistantMcpDiscoveryPilotExecutionContract;
|
||||||
|
answerDraft: AssistantMcpDiscoveryAnswerDraftContract;
|
||||||
|
businessFactAnswerAllowed: boolean;
|
||||||
|
userFacingResponseAllowed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantMcpDiscoveryExecutionHandoffContract {
|
||||||
|
schema_version: typeof ASSISTANT_MCP_DISCOVERY_EXECUTION_HANDOFF_SCHEMA_VERSION;
|
||||||
|
policy_owner: "assistantMcpDiscoveryExecutionHandoff";
|
||||||
|
handoff_status: AssistantMcpDiscoveryExecutionHandoffStatus;
|
||||||
|
selected_chain_id: AssistantMcpDiscoveryChainId;
|
||||||
|
route_candidate_status: AssistantMcpRouteCandidateContract["candidate_status"];
|
||||||
|
evidence_answer_mode: string | null;
|
||||||
|
evidence_expected_coverage: string | null;
|
||||||
|
pilot_status: AssistantMcpDiscoveryPilotExecutionContract["pilot_status"];
|
||||||
|
answer_mode: AssistantMcpDiscoveryAnswerDraftContract["answer_mode"];
|
||||||
|
mcp_execution_performed: boolean;
|
||||||
|
allowed_hot_chain: boolean;
|
||||||
|
can_use_guarded_response: boolean;
|
||||||
|
must_keep_internal_mechanics_hidden: true;
|
||||||
|
reason_codes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const HOT_HANDOFF_CHAIN_ALLOWLIST: AssistantMcpDiscoveryChainId[] = ["value_flow"];
|
||||||
|
|
||||||
|
function uniqueStrings(values: string[]): string[] {
|
||||||
|
const result: string[] = [];
|
||||||
|
for (const value of values) {
|
||||||
|
const text = String(value ?? "").trim();
|
||||||
|
if (text && !result.includes(text)) {
|
||||||
|
result.push(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handoffStatusFor(
|
||||||
|
input: AssistantMcpDiscoveryExecutionHandoffInput,
|
||||||
|
allowedHotChain: boolean
|
||||||
|
): AssistantMcpDiscoveryExecutionHandoffStatus {
|
||||||
|
if (input.bridgeStatus === "needs_clarification" || input.routeCandidate.candidate_status === "needs_user_scope") {
|
||||||
|
return "awaiting_user_scope";
|
||||||
|
}
|
||||||
|
if (input.bridgeStatus === "checked_sources_only") {
|
||||||
|
return "checked_sources_only";
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
input.bridgeStatus === "blocked" ||
|
||||||
|
input.bridgeStatus === "unsupported" ||
|
||||||
|
input.routeCandidate.candidate_status === "blocked" ||
|
||||||
|
input.routeCandidate.candidate_status === "needs_route_enablement"
|
||||||
|
) {
|
||||||
|
return "blocked";
|
||||||
|
}
|
||||||
|
if (!allowedHotChain) {
|
||||||
|
return "not_enabled_for_chain";
|
||||||
|
}
|
||||||
|
return "ready_for_guarded_response";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAssistantMcpDiscoveryExecutionHandoff(
|
||||||
|
input: AssistantMcpDiscoveryExecutionHandoffInput
|
||||||
|
): AssistantMcpDiscoveryExecutionHandoffContract {
|
||||||
|
const allowedHotChain = HOT_HANDOFF_CHAIN_ALLOWLIST.includes(input.routeCandidate.selected_chain_id);
|
||||||
|
const baseStatus = handoffStatusFor(input, allowedHotChain);
|
||||||
|
const readinessChecksPassed =
|
||||||
|
baseStatus === "ready_for_guarded_response" &&
|
||||||
|
input.bridgeStatus === "answer_draft_ready" &&
|
||||||
|
input.routeCandidate.candidate_status === "ready_for_reviewed_execution" &&
|
||||||
|
input.routeCandidate.executable_now === true &&
|
||||||
|
input.pilot.pilot_status === "executed" &&
|
||||||
|
input.pilot.mcp_execution_performed === true &&
|
||||||
|
input.businessFactAnswerAllowed === true &&
|
||||||
|
input.userFacingResponseAllowed === true &&
|
||||||
|
input.answerDraft.internal_mechanics_allowed === false;
|
||||||
|
const handoffStatus = readinessChecksPassed ? "ready_for_guarded_response" : baseStatus;
|
||||||
|
const reasonCodes = uniqueStrings([
|
||||||
|
`execution_handoff_status_${handoffStatus}`,
|
||||||
|
allowedHotChain ? "execution_handoff_chain_allowlisted" : "execution_handoff_chain_not_allowlisted",
|
||||||
|
input.routeCandidate.executable_now
|
||||||
|
? "execution_handoff_route_candidate_executable"
|
||||||
|
: "execution_handoff_route_candidate_not_executable",
|
||||||
|
input.pilot.mcp_execution_performed
|
||||||
|
? "execution_handoff_mcp_execution_performed"
|
||||||
|
: "execution_handoff_mcp_execution_not_performed",
|
||||||
|
input.businessFactAnswerAllowed
|
||||||
|
? "execution_handoff_business_fact_allowed"
|
||||||
|
: "execution_handoff_business_fact_not_allowed",
|
||||||
|
input.userFacingResponseAllowed
|
||||||
|
? "execution_handoff_user_facing_allowed"
|
||||||
|
: "execution_handoff_user_facing_not_allowed",
|
||||||
|
readinessChecksPassed
|
||||||
|
? "execution_handoff_guarded_response_ready"
|
||||||
|
: "execution_handoff_guarded_response_not_ready"
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
schema_version: ASSISTANT_MCP_DISCOVERY_EXECUTION_HANDOFF_SCHEMA_VERSION,
|
||||||
|
policy_owner: "assistantMcpDiscoveryExecutionHandoff",
|
||||||
|
handoff_status: handoffStatus,
|
||||||
|
selected_chain_id: input.routeCandidate.selected_chain_id,
|
||||||
|
route_candidate_status: input.routeCandidate.candidate_status,
|
||||||
|
evidence_answer_mode: input.routeCandidate.evidence_answer_mode,
|
||||||
|
evidence_expected_coverage: input.routeCandidate.evidence_expected_coverage,
|
||||||
|
pilot_status: input.pilot.pilot_status,
|
||||||
|
answer_mode: input.answerDraft.answer_mode,
|
||||||
|
mcp_execution_performed: input.pilot.mcp_execution_performed,
|
||||||
|
allowed_hot_chain: allowedHotChain,
|
||||||
|
can_use_guarded_response: readinessChecksPassed,
|
||||||
|
must_keep_internal_mechanics_hidden: true,
|
||||||
|
reason_codes: reasonCodes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -717,15 +717,59 @@ function aggregationAxisForPlanner(
|
||||||
return axis === "month" ? "month" : null;
|
return axis === "month" ? "month" : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cleanExplicitEntityCandidate(value: string): string | null {
|
||||||
|
let text = value.replace(/\s+/g, " ").trim();
|
||||||
|
if (!text) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
text = text
|
||||||
|
.replace(
|
||||||
|
/^(?:по\s+)?(?:деньгам|деньги|денежн(?:ый|ые|ого|ому|ым|ом)?\s+поток(?:у|ом|а|и)?|нетто|расчет(?:ам|ы)?|получили|получено|заплатили|уплачено|сколько)\s+(?:с|по)\s+/iu,
|
||||||
|
""
|
||||||
|
)
|
||||||
|
.replace(/^(?:по\s+)?(?:контрагент(?:у|ом)?|поставщик(?:у|ом)?|клиент(?:у|ом)?)\s+/iu, "")
|
||||||
|
.replace(/\s+за\s+\d{4}(?:\s+год)?$/iu, "")
|
||||||
|
.trim();
|
||||||
|
return text || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scoreExplicitEntityCandidate(raw: string, cleaned: string, index: number): number {
|
||||||
|
let score = 100 - index;
|
||||||
|
if (raw.trim() === cleaned) {
|
||||||
|
score += 20;
|
||||||
|
}
|
||||||
|
if (/^(?:по\s+)?(?:деньгам|деньги|денежн(?:ый|ые|ого|ому|ым|ом)?\s+поток(?:у|ом|а|и)?|нетто|расчет(?:ам|ы)?|получили|получено|заплатили|уплачено|сколько)\s+(?:с|по)\s+/iu.test(raw)) {
|
||||||
|
score -= 40;
|
||||||
|
}
|
||||||
|
if (/[А-ЯЁA-Z]/u.test(cleaned)) {
|
||||||
|
score += 8;
|
||||||
|
}
|
||||||
|
if (/(?:^|\s)(?:ООО|АО|ПАО|ИП|Группа|Комитет|Департамент)(?:\s|$)/u.test(cleaned)) {
|
||||||
|
score += 6;
|
||||||
|
}
|
||||||
|
score -= Math.max(0, cleaned.length - 60);
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
function firstEntityCandidate(planner: AssistantMcpDiscoveryPlannerContract): string | null {
|
function firstEntityCandidate(planner: AssistantMcpDiscoveryPlannerContract): string | null {
|
||||||
const candidates = planner.discovery_plan.turn_meaning_ref?.explicit_entity_candidates ?? [];
|
const candidates = planner.discovery_plan.turn_meaning_ref?.explicit_entity_candidates ?? [];
|
||||||
for (const candidate of candidates) {
|
let best: { text: string; score: number } | null = null;
|
||||||
const text = toNonEmptyString(candidate);
|
for (let index = 0; index < candidates.length; index += 1) {
|
||||||
if (text) {
|
const candidate = candidates[index];
|
||||||
return text;
|
const raw = toNonEmptyString(candidate);
|
||||||
|
if (!raw) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const cleaned = cleanExplicitEntityCandidate(raw);
|
||||||
|
if (!cleaned) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const score = scoreExplicitEntityCandidate(raw, cleaned, index);
|
||||||
|
if (!best || score > best.score) {
|
||||||
|
best = { text: cleaned, score };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return best?.text ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function dateScopeToFilters(dateScope: string | null): Pick<AddressFilterSet, "period_from" | "period_to"> {
|
function dateScopeToFilters(dateScope: string | null): Pick<AddressFilterSet, "period_from" | "period_to"> {
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,10 @@ import {
|
||||||
type AssistantMcpCatalogPlanReview
|
type AssistantMcpCatalogPlanReview
|
||||||
} from "./assistantMcpCatalogIndex";
|
} from "./assistantMcpCatalogIndex";
|
||||||
import type { AssistantMcpDiscoveryDataNeedGraphContract } from "./assistantMcpDiscoveryDataNeedGraph";
|
import type { AssistantMcpDiscoveryDataNeedGraphContract } from "./assistantMcpDiscoveryDataNeedGraph";
|
||||||
|
import {
|
||||||
|
buildAssistantEvidencePlanner,
|
||||||
|
type AssistantEvidencePlannerContract
|
||||||
|
} from "./assistantEvidencePlanner";
|
||||||
|
|
||||||
export const ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION = "assistant_mcp_discovery_planner_v1" as const;
|
export const ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION = "assistant_mcp_discovery_planner_v1" as const;
|
||||||
|
|
||||||
|
|
@ -91,6 +95,7 @@ export interface AssistantMcpDiscoveryPlannerContract {
|
||||||
catalog_chain_template_alignment: AssistantMcpDiscoveryCatalogChainTemplateAlignment;
|
catalog_chain_template_alignment: AssistantMcpDiscoveryCatalogChainTemplateAlignment;
|
||||||
proposed_primitives: AssistantMcpDiscoveryPrimitive[];
|
proposed_primitives: AssistantMcpDiscoveryPrimitive[];
|
||||||
required_axes: string[];
|
required_axes: string[];
|
||||||
|
evidence_plan: AssistantEvidencePlannerContract;
|
||||||
discovery_plan: AssistantMcpDiscoveryPlanContract;
|
discovery_plan: AssistantMcpDiscoveryPlanContract;
|
||||||
catalog_review: AssistantMcpCatalogPlanReview;
|
catalog_review: AssistantMcpCatalogPlanReview;
|
||||||
reason_codes: string[];
|
reason_codes: string[];
|
||||||
|
|
@ -153,6 +158,17 @@ function pushAllUnique(target: string[], values: string[]): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function uniqueStringList(values: unknown[]): string[] {
|
||||||
|
const result: string[] = [];
|
||||||
|
for (const value of values) {
|
||||||
|
const text = toNonEmptyString(value);
|
||||||
|
if (text) {
|
||||||
|
pushUnique(result, text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
function pushCatalogChainTemplateAlignmentReasons(
|
function pushCatalogChainTemplateAlignmentReasons(
|
||||||
target: string[],
|
target: string[],
|
||||||
alignment: AssistantMcpDiscoveryCatalogChainTemplateAlignment
|
alignment: AssistantMcpDiscoveryCatalogChainTemplateAlignment
|
||||||
|
|
@ -1355,6 +1371,69 @@ function statusFrom(
|
||||||
return "ready_for_execution";
|
return "ready_for_execution";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function providedAxesFromPlanTurnMeaning(plan: AssistantMcpDiscoveryPlanContract): string[] {
|
||||||
|
const result: string[] = [];
|
||||||
|
const meaning = plan.turn_meaning_ref;
|
||||||
|
if ((meaning?.explicit_entity_candidates?.length ?? 0) > 0) {
|
||||||
|
pushUnique(result, "counterparty");
|
||||||
|
pushUnique(result, "business_entity");
|
||||||
|
}
|
||||||
|
if (toNonEmptyString(meaning?.explicit_organization_scope)) {
|
||||||
|
pushUnique(result, "organization");
|
||||||
|
}
|
||||||
|
if (toNonEmptyString(meaning?.explicit_date_scope)) {
|
||||||
|
pushUnique(result, "period");
|
||||||
|
}
|
||||||
|
if (toNonEmptyString(meaning?.asked_aggregation_axis)) {
|
||||||
|
pushUnique(result, "aggregate_axis");
|
||||||
|
}
|
||||||
|
if (toNonEmptyString(meaning?.metadata_scope_hint)) {
|
||||||
|
pushUnique(result, "metadata_scope");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function preferredClarificationAxesForRecipe(input: {
|
||||||
|
recipe: PlannerRecipe;
|
||||||
|
dataNeedGraph: AssistantMcpDiscoveryDataNeedGraphContract | null;
|
||||||
|
}): string[] {
|
||||||
|
if (
|
||||||
|
input.recipe.chainId === "value_flow_ranking" ||
|
||||||
|
input.recipe.chainId === "value_flow_comparison" ||
|
||||||
|
input.recipe.chainId === "business_overview"
|
||||||
|
) {
|
||||||
|
return ["organization"];
|
||||||
|
}
|
||||||
|
if (input.dataNeedGraph?.business_fact_family === "value_flow" && !hasSubjectCandidates(input.dataNeedGraph)) {
|
||||||
|
return ["organization"];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function missingAxesFromCatalogReview(
|
||||||
|
review: AssistantMcpCatalogPlanReview,
|
||||||
|
input: {
|
||||||
|
providedAxes: string[];
|
||||||
|
preferredAxes: string[];
|
||||||
|
}
|
||||||
|
): string[] {
|
||||||
|
const result: string[] = [];
|
||||||
|
for (const options of Object.values(review.missing_axes_by_primitive)) {
|
||||||
|
const candidates = options
|
||||||
|
.map((option) => uniqueStringList(option).filter((axis) => !input.providedAxes.includes(axis)))
|
||||||
|
.filter((option) => option.length > 0);
|
||||||
|
const preferredCandidate = candidates.find((option) => option.some((axis) => input.preferredAxes.includes(axis)));
|
||||||
|
const candidate = preferredCandidate ?? candidates.sort((left, right) => left.length - right.length)[0];
|
||||||
|
if (!candidate) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const axis of candidate) {
|
||||||
|
pushUnique(result, axis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
export function planAssistantMcpDiscovery(
|
export function planAssistantMcpDiscovery(
|
||||||
input: AssistantMcpDiscoveryPlannerInput
|
input: AssistantMcpDiscoveryPlannerInput
|
||||||
): AssistantMcpDiscoveryPlannerContract {
|
): AssistantMcpDiscoveryPlannerContract {
|
||||||
|
|
@ -1422,6 +1501,17 @@ export function planAssistantMcpDiscovery(
|
||||||
} else {
|
} else {
|
||||||
pushReason(reasonCodes, "planner_needs_more_user_or_scope_context");
|
pushReason(reasonCodes, "planner_needs_more_user_or_scope_context");
|
||||||
}
|
}
|
||||||
|
const evidencePlan = buildAssistantEvidencePlanner({
|
||||||
|
selectedChainId: recipe.chainId,
|
||||||
|
plannerStatus,
|
||||||
|
semanticDataNeed,
|
||||||
|
dataNeedGraph,
|
||||||
|
discoveryPlan: plan,
|
||||||
|
additionalMissingAxes: missingAxesFromCatalogReview(adjustedReview, {
|
||||||
|
providedAxes: providedAxesFromPlanTurnMeaning(plan),
|
||||||
|
preferredAxes: preferredClarificationAxesForRecipe({ recipe, dataNeedGraph })
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
schema_version: ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION,
|
schema_version: ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION,
|
||||||
|
|
@ -1436,6 +1526,7 @@ export function planAssistantMcpDiscovery(
|
||||||
catalog_chain_template_alignment: catalogChainTemplateAlignment,
|
catalog_chain_template_alignment: catalogChainTemplateAlignment,
|
||||||
proposed_primitives: recipe.primitives,
|
proposed_primitives: recipe.primitives,
|
||||||
required_axes: recipe.axes,
|
required_axes: recipe.axes,
|
||||||
|
evidence_plan: evidencePlan,
|
||||||
discovery_plan: plan,
|
discovery_plan: plan,
|
||||||
catalog_review: adjustedReview,
|
catalog_review: adjustedReview,
|
||||||
reason_codes: reasonCodes
|
reason_codes: reasonCodes
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export interface AssistantMcpDiscoveryResponseCandidateContract {
|
||||||
schema_version: typeof ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION;
|
schema_version: typeof ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION;
|
||||||
policy_owner: "assistantMcpDiscoveryResponseCandidate";
|
policy_owner: "assistantMcpDiscoveryResponseCandidate";
|
||||||
candidate_status: AssistantMcpDiscoveryResponseCandidateStatus;
|
candidate_status: AssistantMcpDiscoveryResponseCandidateStatus;
|
||||||
hot_runtime_wired: false;
|
hot_runtime_wired: boolean;
|
||||||
reply_type: "partial_coverage" | "clarification_required" | "no_grounded_answer";
|
reply_type: "partial_coverage" | "clarification_required" | "no_grounded_answer";
|
||||||
reply_text: string | null;
|
reply_text: string | null;
|
||||||
eligible_for_future_hot_runtime: boolean;
|
eligible_for_future_hot_runtime: boolean;
|
||||||
|
|
@ -60,6 +60,65 @@ function requestsFinancialCounterpartyBoundary(turnMeaning: Record<string, unkno
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function requestsCompactCashflowAnswer(
|
||||||
|
turnMeaning: Record<string, unknown> | null,
|
||||||
|
graph: Record<string, unknown> | null
|
||||||
|
): boolean {
|
||||||
|
const text = normalizeQuestionText([
|
||||||
|
turnMeaning?.raw_message,
|
||||||
|
turnMeaning?.effective_message,
|
||||||
|
graph?.source_message,
|
||||||
|
graph?.question
|
||||||
|
].join(" "));
|
||||||
|
if (!text) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
/(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)[\s\S]{0,80}(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\p{L}*|\u0443\u0448\p{L}*|\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*)|(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e)[\s\S]{0,80}(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)/iu.test(
|
||||||
|
text
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return /(?:\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}+|\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?\b|\u0431\u0435\u0437\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0431\u0438\u0432\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0440\u0435\u0437\p{L}*|\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b\p{L}*|\u043e\u0434\u043d\u043e\u0439\s+\u0441\u0442\u0440\u043e\u043a\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0438\u0442\u043e\u0433|\u0434\u0435\u043d\p{L}{0,20}\s+\u043d\u0435\u0442\u0442\u043e|\u043f\u043e\u043b\u0443\u0447\p{L}*[\s\S]{0,80}\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\u043b\p{L}*[\s\S]{0,80}\u0443\u0448\u043b\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*)[\s\S]{0,120}(?:\u0434\u0435\u043d\p{L}*|\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u043f\u043e\u043b\u0443\u0447|\u043f\u0440\u0438\u0448\u043b))/iu.test(
|
||||||
|
text
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestsCashflowPolarityAnswer(
|
||||||
|
turnMeaning: Record<string, unknown> | null,
|
||||||
|
graph: Record<string, unknown> | null
|
||||||
|
): boolean {
|
||||||
|
const text = normalizeQuestionText([
|
||||||
|
turnMeaning?.raw_message,
|
||||||
|
turnMeaning?.effective_message,
|
||||||
|
graph?.source_message,
|
||||||
|
graph?.question
|
||||||
|
].join(" "));
|
||||||
|
return /(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)[\s\S]{0,80}(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441)|(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441)[\s\S]{0,80}(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)/iu.test(
|
||||||
|
text
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestsDirectAccountingProfitAnswer(
|
||||||
|
turnMeaning: Record<string, unknown> | null,
|
||||||
|
graph: Record<string, unknown> | null
|
||||||
|
): boolean {
|
||||||
|
const text = normalizeQuestionText([
|
||||||
|
turnMeaning?.raw_message,
|
||||||
|
turnMeaning?.effective_message,
|
||||||
|
graph?.source_message,
|
||||||
|
graph?.question
|
||||||
|
].join(" "));
|
||||||
|
if (!/(?:чист\p{L}{0,8}\s+прибыл|финрезультат|финансов\p{L}{0,12}\s+результат)/iu.test(text)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (/(?:^|\s)(?:а\s+)?это\s+чист\p{L}{0,8}\s+прибыл|это\s+прибыл\p{L}{0,4}\s+или\s+нет/iu.test(text)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(?:какая|какой|сколько|посчитай|рассчитай|покажи|дай|определить|определи|найди|итог|за\s+\d{4})/iu.test(text);
|
||||||
|
}
|
||||||
|
|
||||||
function toStringList(value: unknown): string[] {
|
function toStringList(value: unknown): string[] {
|
||||||
if (!Array.isArray(value)) {
|
if (!Array.isArray(value)) {
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -262,7 +321,7 @@ function localizeLine(value: string): string {
|
||||||
return "Сумма исходящих платежей рассчитана только по подтвержденным строкам списаний в 1С.";
|
return "Сумма исходящих платежей рассчитана только по подтвержденным строкам списаний в 1С.";
|
||||||
}
|
}
|
||||||
if (/^Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows$/i.test(value)) {
|
if (/^Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows$/i.test(value)) {
|
||||||
return "Нетто денежного потока рассчитано только как входящие подтвержденные строки 1С минус исходящие подтвержденные строки 1С.";
|
return "Нетто денежного потока рассчитано как подтвержденные входящие платежи минус подтвержденные исходящие платежи в 1С.";
|
||||||
}
|
}
|
||||||
if (/^Counterparty monthly net value-flow breakdown was grouped by month over confirmed incoming and outgoing 1C rows$/i.test(value)) {
|
if (/^Counterparty monthly net value-flow breakdown was grouped by month over confirmed incoming and outgoing 1C rows$/i.test(value)) {
|
||||||
return "Помесячная нетто-раскладка сгруппирована только по подтвержденным входящим и исходящим строкам 1С.";
|
return "Помесячная нетто-раскладка сгруппирована только по подтвержденным входящим и исходящим строкам 1С.";
|
||||||
|
|
@ -472,9 +531,9 @@ function businessOverviewCoverageLimitLine(overview: Record<string, unknown>): s
|
||||||
limited.push("исходящие");
|
limited.push("исходящие");
|
||||||
}
|
}
|
||||||
const continuation =
|
const continuation =
|
||||||
"Если нужен полный сквозной ответ, безопасный следующий шаг — выбрать конкретный год или квартал для дозапроса: тогда широкий срез можно собрать частями без выдачи непроверенного итога.";
|
"Для полного сквозного итога лучше разбить проверку по годам или кварталам.";
|
||||||
return limited.length > 0
|
return limited.length > 0
|
||||||
? `Важно: по направлению ${limited.join(" и ")} проверка достигла лимита строк; это расширенный проверенный срез найденных строк, но не гарантия полного бухгалтерского оборота без отдельной полной выгрузки. ${continuation}`
|
? `Ограничение: широкий денежный срез по направлению ${limited.join(" и ")} не считаю полным бухгалтерским оборотом без отдельной полной выгрузки. ${continuation}`
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -602,41 +661,16 @@ function businessOverviewInventoryLine(overview: Record<string, unknown>): strin
|
||||||
}
|
}
|
||||||
const amount = moneyText(inventory.total_amount_human_ru);
|
const amount = moneyText(inventory.total_amount_human_ru);
|
||||||
const rows = Number(inventory.rows_matched);
|
const rows = Number(inventory.rows_matched);
|
||||||
const quantity = Number(inventory.total_quantity);
|
|
||||||
if (!amount && !Number.isFinite(rows)) {
|
if (!amount && !Number.isFinite(rows)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const pieces = [
|
const pieces = [
|
||||||
Number.isFinite(rows) ? `${rows} позиций` : null,
|
Number.isFinite(rows) ? `${rows} позиций` : null,
|
||||||
amount ? `на ${sentenceAmount(amount) ?? amount}` : null,
|
amount ? `на ${sentenceAmount(amount) ?? amount}` : null
|
||||||
Number.isFinite(quantity) && quantity > 0 ? `количество ${quantity}` : null
|
|
||||||
].filter((item): item is string => Boolean(item));
|
].filter((item): item is string => Boolean(item));
|
||||||
return pieces.length > 0 ? `Склад: ${pieces.join(", ")}.` : null;
|
return pieces.length > 0 ? `Склад: ${pieces.join(", ")}.` : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function rowCountText(value: unknown): string | null {
|
|
||||||
const count = Number(value);
|
|
||||||
return Number.isFinite(count) ? String(count) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sideRowsText(side: Record<string, unknown> | null): string | null {
|
|
||||||
const rowsWithAmount = rowCountText(side?.rows_with_amount);
|
|
||||||
const rowsMatched = rowCountText(side?.rows_matched);
|
|
||||||
if (rowsWithAmount && rowsMatched) {
|
|
||||||
return `${rowsWithAmount} из ${rowsMatched}`;
|
|
||||||
}
|
|
||||||
return rowsWithAmount ?? rowsMatched;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sideDateText(side: Record<string, unknown> | null): string | null {
|
|
||||||
const first = toNonEmptyString(side?.first_movement_date);
|
|
||||||
const latest = toNonEmptyString(side?.latest_movement_date);
|
|
||||||
if (first && latest) {
|
|
||||||
return first === latest ? `дата ${first}` : `даты ${first}..${latest}`;
|
|
||||||
}
|
|
||||||
return first ? `первая дата ${first}` : latest ? `последняя дата ${latest}` : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function bidirectionalNetLabel(direction: unknown): string {
|
function bidirectionalNetLabel(direction: unknown): string {
|
||||||
if (direction === "net_outgoing") {
|
if (direction === "net_outgoing") {
|
||||||
return "нетто в сторону контрагента";
|
return "нетто в сторону контрагента";
|
||||||
|
|
@ -678,29 +712,25 @@ function buildCompactBidirectionalValueFlowReply(
|
||||||
: "по выбранному контуру";
|
: "по выбранному контуру";
|
||||||
const period = toNonEmptyString(flow.period_scope);
|
const period = toNonEmptyString(flow.period_scope);
|
||||||
const periodText = period ? ` за период ${period}` : " в проверенном окне";
|
const periodText = period ? ` за период ${period}` : " в проверенном окне";
|
||||||
const incomingRows = sideRowsText(incoming);
|
|
||||||
const outgoingRows = sideRowsText(outgoing);
|
|
||||||
const incomingDates = sideDateText(incoming);
|
|
||||||
const outgoingDates = sideDateText(outgoing);
|
|
||||||
const netLabel = bidirectionalNetLabel(flow.net_direction);
|
const netLabel = bidirectionalNetLabel(flow.net_direction);
|
||||||
const lines = [
|
const lines = [
|
||||||
`Коротко: ${subjectLead}${periodText} по найденным строкам 1С получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}; расчетное ${netLabel}: ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`
|
`Коротко: ${subjectLead}${periodText} по найденным строкам 1С получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}; расчетное ${netLabel}: ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`
|
||||||
];
|
];
|
||||||
|
|
||||||
const basis: string[] = [];
|
const basis: string[] = [];
|
||||||
if (incomingRows) {
|
if (incomingAmount) {
|
||||||
basis.push(`входящих строк с суммой ${incomingRows}${incomingDates ? ` (${incomingDates})` : ""}`);
|
basis.push("входящие платежи");
|
||||||
}
|
}
|
||||||
if (outgoingRows) {
|
if (outgoingAmount) {
|
||||||
basis.push(`исходящих строк с суммой ${outgoingRows}${outgoingDates ? ` (${outgoingDates})` : ""}`);
|
basis.push("исходящие платежи");
|
||||||
}
|
}
|
||||||
if (basis.length > 0) {
|
if (basis.length > 0) {
|
||||||
lines.push(`Основа: ${basis.join("; ")}.`);
|
lines.push(`Основа: проверенные ${basis.join(" и ")} по этому контрагенту в 1С.`);
|
||||||
}
|
}
|
||||||
if (flow.coverage_limited_by_probe_limit === true) {
|
if (flow.coverage_limited_by_probe_limit === true) {
|
||||||
lines.push("Важно: часть проверки достигла лимита строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода.");
|
lines.push("Важно: часть проверки достигла лимита строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода.");
|
||||||
}
|
}
|
||||||
lines.push("Метод: нетто рассчитано как подтвержденные входящие строки 1С минус подтвержденные исходящие строки; это не полное бухгалтерское сальдо вне проверенного окна.");
|
lines.push("Метод: нетто рассчитано как подтвержденные входящие платежи минус подтвержденные исходящие платежи; это не полное бухгалтерское сальдо вне проверенного окна.");
|
||||||
|
|
||||||
const fallbackNextStep = toNonEmptyString(draft.next_step_line);
|
const fallbackNextStep = toNonEmptyString(draft.next_step_line);
|
||||||
if (fallbackNextStep) {
|
if (fallbackNextStep) {
|
||||||
|
|
@ -718,6 +748,21 @@ function compactComparable(value: string | null): string {
|
||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function businessOverviewOrganizationScopeLabel(value: unknown): string | null {
|
||||||
|
const text = toNonEmptyString(value);
|
||||||
|
if (!text) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const comparable = compactComparable(text);
|
||||||
|
if (
|
||||||
|
/^(?:с|без)\s+разбивк/.test(comparable) ||
|
||||||
|
/\b(?:входящ|исходящ|нетто|топ|контрагент|платеж|поступлен)\b/.test(comparable)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
function businessOverviewSeparateSubjectLabel(
|
function businessOverviewSeparateSubjectLabel(
|
||||||
graph: Record<string, unknown> | null,
|
graph: Record<string, unknown> | null,
|
||||||
turnMeaning: Record<string, unknown> | null,
|
turnMeaning: Record<string, unknown> | null,
|
||||||
|
|
@ -785,21 +830,17 @@ function buildPreviousCounterpartyValueFlowSummary(
|
||||||
`; отдельно по ${counterparty}: получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}, ` +
|
`; отдельно по ${counterparty}: получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}, ` +
|
||||||
`${netLabel} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}`;
|
`${netLabel} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}`;
|
||||||
const basis: string[] = [];
|
const basis: string[] = [];
|
||||||
const incomingRows = sideRowsText(incoming);
|
if (incomingAmount) {
|
||||||
const outgoingRows = sideRowsText(outgoing);
|
basis.push("входящие платежи");
|
||||||
const incomingDates = sideDateText(incoming);
|
|
||||||
const outgoingDates = sideDateText(outgoing);
|
|
||||||
if (incomingRows) {
|
|
||||||
basis.push(`входящие строки ${incomingRows}${incomingDates ? ` (${incomingDates})` : ""}`);
|
|
||||||
}
|
}
|
||||||
if (outgoingRows) {
|
if (outgoingAmount) {
|
||||||
basis.push(`исходящие строки ${outgoingRows}${outgoingDates ? ` (${outgoingDates})` : ""}`);
|
basis.push("исходящие платежи");
|
||||||
}
|
}
|
||||||
const documents = previousDocumentSummaryLine(documentBundle, counterparty);
|
const documents = previousDocumentSummaryLine(documentBundle, counterparty);
|
||||||
if (documents) {
|
if (documents) {
|
||||||
basis.push(documents);
|
basis.push(documents);
|
||||||
}
|
}
|
||||||
const basisText = basis.length > 0 ? ` Основа: ${basis.join("; ")}.` : "";
|
const basisText = basis.length > 0 ? ` Основа: проверенные ${basis.join(" и ")}.` : "";
|
||||||
return {
|
return {
|
||||||
lead,
|
lead,
|
||||||
line:
|
line:
|
||||||
|
|
@ -835,7 +876,7 @@ function buildCompactBusinessOverviewReply(
|
||||||
const netDirection = overview.net_direction === "net_outgoing" ? "операционное нетто в минус" : "расчетное операционное нетто";
|
const netDirection = overview.net_direction === "net_outgoing" ? "операционное нетто в минус" : "расчетное операционное нетто";
|
||||||
const period = businessOverviewPeriodText(overview);
|
const period = businessOverviewPeriodText(overview);
|
||||||
const limitLine = businessOverviewCoverageLimitLine(overview);
|
const limitLine = businessOverviewCoverageLimitLine(overview);
|
||||||
const organizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope);
|
const organizationScope = businessOverviewOrganizationScopeLabel(turnMeaning?.explicit_organization_scope);
|
||||||
const separateSubject = businessOverviewSeparateSubjectLabel(graph, turnMeaning, organizationScope);
|
const separateSubject = businessOverviewSeparateSubjectLabel(graph, turnMeaning, organizationScope);
|
||||||
const previousCounterpartySummary = buildPreviousCounterpartyValueFlowSummary(
|
const previousCounterpartySummary = buildPreviousCounterpartyValueFlowSummary(
|
||||||
toRecordObject(turnMeaning?.previous_counterparty_value_flow_bundle),
|
toRecordObject(turnMeaning?.previous_counterparty_value_flow_bundle),
|
||||||
|
|
@ -890,6 +931,29 @@ function buildCompactBusinessOverviewReply(
|
||||||
actionFamily === "vendor_risk_procurement_boundary" || unsupportedFamily === "vendor_risk_procurement_boundary";
|
actionFamily === "vendor_risk_procurement_boundary" || unsupportedFamily === "vendor_risk_procurement_boundary";
|
||||||
const inventoryReserveBoundary =
|
const inventoryReserveBoundary =
|
||||||
actionFamily === "inventory_reserve_boundary" || unsupportedFamily === "inventory_reserve_liquidation_boundary";
|
actionFamily === "inventory_reserve_boundary" || unsupportedFamily === "inventory_reserve_liquidation_boundary";
|
||||||
|
const compactCashflowRequested = directMoneyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph);
|
||||||
|
const cashflowPolarityRequested = compactCashflowRequested && requestsCashflowPolarityAnswer(turnMeaning, graph);
|
||||||
|
const directAccountingProfitRequested = requestsDirectAccountingProfitAnswer(turnMeaning, graph);
|
||||||
|
|
||||||
|
if (compactCashflowRequested && !rankingNeed && (incomingAmount || outgoingAmount || netAmount)) {
|
||||||
|
const netDisplay = sentenceAmount(netAmount) ?? netAmount ?? "0 \u0440\u0443\u0431.";
|
||||||
|
const signedNetDisplay =
|
||||||
|
cashflowPolarityRequested && netDisplay && !String(netDisplay).trim().startsWith("-")
|
||||||
|
? `+${netDisplay}`
|
||||||
|
: netDisplay;
|
||||||
|
const polarityLead = cashflowPolarityRequested
|
||||||
|
? `\u041a\u043e\u0440\u043e\u0442\u043a\u043e: \u043f\u043e \u0434\u0435\u043d\u044c\u0433\u0430\u043c \u043f\u043b\u044e\u0441. `
|
||||||
|
: "\u041a\u043e\u0440\u043e\u0442\u043a\u043e: ";
|
||||||
|
lines.push(
|
||||||
|
`${polarityLead}${organizationPrefix}${period} \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438 ${incomingAmount ?? "0 \u0440\u0443\u0431."}; \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438/\u0441\u043f\u0438\u0441\u0430\u043b\u0438 ${outgoingAmount ?? "0 \u0440\u0443\u0431."}; \u0434\u0435\u043d\u0435\u0436\u043d\u043e\u0435 \u043d\u0435\u0442\u0442\u043e ${signedNetDisplay}.`
|
||||||
|
);
|
||||||
|
lines.push(
|
||||||
|
cashflowPolarityRequested
|
||||||
|
? "\u042d\u0442\u043e \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0439 \u043f\u043e\u0442\u043e\u043a \u043f\u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u043c \u0441\u0442\u0440\u043e\u043a\u0430\u043c 1\u0421, \u043d\u0435 \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0441\u043a\u0438\u0439 \u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442."
|
||||||
|
: "\u042d\u0442\u043e \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0439 \u043f\u043e\u0442\u043e\u043a \u043f\u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u043c \u0441\u0442\u0440\u043e\u043a\u0430\u043c 1\u0421, \u043d\u0435 \u0447\u0438\u0441\u0442\u0430\u044f \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u0438 \u043d\u0435 \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0441\u043a\u0438\u0439 \u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442."
|
||||||
|
);
|
||||||
|
return joinBusinessReplyLines(lines);
|
||||||
|
}
|
||||||
|
|
||||||
if (profitMarginBoundary) {
|
if (profitMarginBoundary) {
|
||||||
const accountingFinancialResult = toRecordObject(overview.accounting_financial_result);
|
const accountingFinancialResult = toRecordObject(overview.accounting_financial_result);
|
||||||
|
|
@ -914,10 +978,14 @@ function buildCompactBusinessOverviewReply(
|
||||||
: amount
|
: amount
|
||||||
: "сумма не распознана";
|
: "сумма не распознана";
|
||||||
lines.push(
|
lines.push(
|
||||||
`Коротко: нет, денежное операционное нетто не стоит считать чистой прибылью. Отдельно по закрытию счетов 90/91/99 в 1С за ${periodScope} подтвержден ${directionText}: ${amountText}${marginPct ? `; маржа к подтвержденной выручке ${marginPct}` : "; маржа к подтвержденной выручке не рассчитана"}.`
|
directAccountingProfitRequested
|
||||||
|
? `Коротко: за ${periodScope} по закрытию счетов 90/91/99 в 1С подтвержден ${directionText}: ${amountText}${marginPct ? `; маржа к подтвержденной выручке ${marginPct}` : "; маржа к подтвержденной выручке не рассчитана"}.`
|
||||||
|
: `Коротко: нет, денежное операционное нетто не стоит считать чистой прибылью. Отдельно по закрытию счетов 90/91/99 в 1С за ${periodScope} подтвержден ${directionText}: ${amountText}${marginPct ? `; маржа к подтвержденной выручке ${marginPct}` : "; маржа к подтвержденной выручке не рассчитана"}.`
|
||||||
);
|
);
|
||||||
lines.push(
|
lines.push(
|
||||||
"Это учетный финрезультат по найденным строкам закрытия периода в 1С, а не внешний аудит и не юридически подтвержденная отчетность."
|
directAccountingProfitRequested
|
||||||
|
? "Это учетный финрезультат по найденным строкам закрытия периода в 1С, не денежный поток, не внешний аудит и не юридически подтвержденная отчетность."
|
||||||
|
: "Это учетный финрезультат по найденным строкам закрытия периода в 1С, а не внешний аудит и не юридически подтвержденная отчетность."
|
||||||
);
|
);
|
||||||
return joinBusinessReplyLines(lines);
|
return joinBusinessReplyLines(lines);
|
||||||
}
|
}
|
||||||
|
|
@ -1110,6 +1178,69 @@ function buildCompactBusinessOverviewReply(
|
||||||
return joinBusinessReplyLines(lines);
|
return joinBusinessReplyLines(lines);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!separateSubject && !crossScopeExecutiveSummary && (actionFamily === "broad_evaluation" || unsupportedFamily === "broad_business_evaluation")) {
|
||||||
|
const subject = organizationScope ?? "компания";
|
||||||
|
const periodWithoutPrefix = period.replace(/^за\s+/iu, "");
|
||||||
|
lines.push(
|
||||||
|
`Коротко: по доступным данным ${subject} выглядит как бизнес с крупными контрактными денежными потоками и заметной зависимостью от нескольких крупных контрагентов, а не как равномерный поток мелких продаж.`
|
||||||
|
);
|
||||||
|
lines.push("Что видно:");
|
||||||
|
if (incomingAmount) {
|
||||||
|
lines.push(`- входящие деньги за ${periodWithoutPrefix}: ${incomingAmount};`);
|
||||||
|
}
|
||||||
|
if (outgoingAmount) {
|
||||||
|
lines.push(`- исходящие платежи/списания: ${outgoingAmount};`);
|
||||||
|
}
|
||||||
|
if (netAmount) {
|
||||||
|
lines.push(`- ${netDirection}: ${sentenceAmount(netAmount) ?? netAmount};`);
|
||||||
|
}
|
||||||
|
if (customerName && customerAmount) {
|
||||||
|
lines.push(
|
||||||
|
topCustomerLooksFinancial
|
||||||
|
? `- крупнейший входящий источник: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}; это похоже на финансовый контур, не на обычную клиентскую выручку;${nonFinancialCustomer ? ` крупнейший небанковский входящий контрагент: ${nonFinancialCustomer};` : ""}`
|
||||||
|
: `- крупнейший источник денег: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}; это сигнал концентрации на крупном заказчике;`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (topSupplier) {
|
||||||
|
lines.push(
|
||||||
|
topSupplierLooksFinancial
|
||||||
|
? `- крупнейший получатель исходящих денег: ${topSupplier}; это похоже на финансовый контур, не на обычного поставщика;`
|
||||||
|
: `- крупнейший получатель исходящих денег: ${topSupplier};`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const inventoryLine = businessOverviewInventoryLine(overview);
|
||||||
|
if (inventoryLine) {
|
||||||
|
lines.push(`- ${localizeLine(inventoryLine)}`);
|
||||||
|
}
|
||||||
|
const debtLine = businessOverviewDebtLine(overview);
|
||||||
|
if (debtLine) {
|
||||||
|
lines.push(`- ${localizeLine(debtLine)}`);
|
||||||
|
}
|
||||||
|
lines.push(
|
||||||
|
"Ограничение: это оценка по денежным потокам и найденным срезам 1С, не аудиторское заключение и не подтвержденная чистая прибыль."
|
||||||
|
);
|
||||||
|
const missingOverviewFamilies: string[] = [];
|
||||||
|
if (!businessOverviewTaxLine(overview)) {
|
||||||
|
missingOverviewFamilies.push("НДС/налоговая позиция без отдельного точного расчета");
|
||||||
|
}
|
||||||
|
if (!debtLine) {
|
||||||
|
missingOverviewFamilies.push("долги без даты среза");
|
||||||
|
}
|
||||||
|
if (!inventoryLine) {
|
||||||
|
missingOverviewFamilies.push("склад без даты среза");
|
||||||
|
}
|
||||||
|
if (missingOverviewFamilies.length > 0) {
|
||||||
|
lines.push(`Что не подтверждено в этом срезе: ${missingOverviewFamilies.join(", ")}.`);
|
||||||
|
}
|
||||||
|
if (limitLine) {
|
||||||
|
lines.push(limitLine);
|
||||||
|
}
|
||||||
|
lines.push(
|
||||||
|
"Что проверить дальше: чистую прибыль через 90/91/99, маржинальность по проектам, зависимость от топ-3 контрагентов, старые складские остатки и зависшие расчеты."
|
||||||
|
);
|
||||||
|
return joinBusinessReplyLines(lines);
|
||||||
|
}
|
||||||
|
|
||||||
if (crossScopeExecutiveSummary && separateSubject && previousCounterpartySummary && (incomingAmount || outgoingAmount || netAmount)) {
|
if (crossScopeExecutiveSummary && separateSubject && previousCounterpartySummary && (incomingAmount || outgoingAmount || netAmount)) {
|
||||||
lines.push(
|
lines.push(
|
||||||
`Коротко: по компании ${organizationScope ?? "в выбранном контуре"} ${period} подтвержден денежный срез: получили ${incomingAmount ?? "0 руб."}, исходящие платежи/списания ${outgoingAmount ?? "0 руб."}, ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}${previousCounterpartySummary.lead}; можно утверждать только эти подтвержденные срезы, нельзя называть это чистой прибылью, полным оборотом или доказанной ролью главного клиента/поставщика.`
|
`Коротко: по компании ${organizationScope ?? "в выбранном контуре"} ${period} подтвержден денежный срез: получили ${incomingAmount ?? "0 руб."}, исходящие платежи/списания ${outgoingAmount ?? "0 руб."}, ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}${previousCounterpartySummary.lead}; можно утверждать только эти подтвержденные срезы, нельзя называть это чистой прибылью, полным оборотом или доказанной ролью главного клиента/поставщика.`
|
||||||
|
|
@ -1128,6 +1259,40 @@ function buildCompactBusinessOverviewReply(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rankingNeed) {
|
if (rankingNeed) {
|
||||||
|
const explicitPeriodRankingOverview =
|
||||||
|
period &&
|
||||||
|
!/(?:все\s+доступное|все\s+время|all\s+time)/iu.test(period) &&
|
||||||
|
(incomingAmount || outgoingAmount || netAmount);
|
||||||
|
if (explicitPeriodRankingOverview) {
|
||||||
|
lines.push(
|
||||||
|
`Коротко: ${organizationPrefix}${period} денежная картина подтверждена по найденным строкам 1С.`
|
||||||
|
);
|
||||||
|
lines.push(
|
||||||
|
`Деньги: входящие ${incomingAmount ?? "0 руб."}, исходящие ${outgoingAmount ?? "0 руб."}, расчетное операционное нетто ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`
|
||||||
|
);
|
||||||
|
if (customerName && customerAmount) {
|
||||||
|
lines.push(
|
||||||
|
topCustomerLooksFinancial
|
||||||
|
? `Топ входящих: 1. ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}. Это финансовый/банковский контур, не считаю его клиентской выручкой без назначения платежа.${nonFinancialCustomer ? ` 2. Крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}.` : ""}`
|
||||||
|
: `Крупнейший входящий контрагент: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (topSupplier) {
|
||||||
|
lines.push(
|
||||||
|
topSupplierLooksFinancial
|
||||||
|
? `Топ исходящих: 1. ${topSupplier}. Это финансовый/банковский контур, не считаю его обычным поставщиком без назначения платежа и договора.${nonFinancialSupplier ? ` 2. Крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}.` : ""}`
|
||||||
|
: `Крупнейший получатель исходящих денег: ${topSupplier}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
lines.push(
|
||||||
|
`Вывод: по движению денег период ${netDirection}; это не чистая прибыль и не бухгалтерский финрезультат.`
|
||||||
|
);
|
||||||
|
if (requestedFinancialBoundaryLine) {
|
||||||
|
lines.push(requestedFinancialBoundaryLine);
|
||||||
|
}
|
||||||
|
lines.push("Следующий шаг: могу отдельно посчитать чистую прибыль через закрытие 90/91/99 или разложить этот период по контрагентам.");
|
||||||
|
return joinBusinessReplyLines(lines);
|
||||||
|
}
|
||||||
const incomingLeader = strongestIncomingYear(overview);
|
const incomingLeader = strongestIncomingYear(overview);
|
||||||
const canRankYearlyNet = !limitLine;
|
const canRankYearlyNet = !limitLine;
|
||||||
const netLeader = canRankYearlyNet ? strongestNetYear(overview) : null;
|
const netLeader = canRankYearlyNet ? strongestNetYear(overview) : null;
|
||||||
|
|
@ -1268,6 +1433,17 @@ function statusFrom(entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract |
|
||||||
return "not_applicable";
|
return "not_applicable";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasGuardedHotHandoff(entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null): boolean {
|
||||||
|
const bridge = toRecordObject(entryPoint?.bridge);
|
||||||
|
const handoff = toRecordObject(bridge?.execution_handoff);
|
||||||
|
return (
|
||||||
|
entryPoint?.hot_runtime_wired === true &&
|
||||||
|
bridge?.hot_runtime_wired === true &&
|
||||||
|
handoff?.can_use_guarded_response === true &&
|
||||||
|
handoff?.must_keep_internal_mechanics_hidden === true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function replyTypeFor(
|
function replyTypeFor(
|
||||||
status: AssistantMcpDiscoveryResponseCandidateStatus
|
status: AssistantMcpDiscoveryResponseCandidateStatus
|
||||||
): AssistantMcpDiscoveryResponseCandidateContract["reply_type"] {
|
): AssistantMcpDiscoveryResponseCandidateContract["reply_type"] {
|
||||||
|
|
@ -1318,9 +1494,15 @@ export function buildAssistantMcpDiscoveryResponseCandidate(
|
||||||
): AssistantMcpDiscoveryResponseCandidateContract {
|
): AssistantMcpDiscoveryResponseCandidateContract {
|
||||||
const entry = entryPoint ?? null;
|
const entry = entryPoint ?? null;
|
||||||
const status = statusFrom(entry);
|
const status = statusFrom(entry);
|
||||||
|
const guardedHotHandoff = hasGuardedHotHandoff(entry);
|
||||||
const reasonCodes = uniqueStrings(entry?.reason_codes ?? []);
|
const reasonCodes = uniqueStrings(entry?.reason_codes ?? []);
|
||||||
pushReason(reasonCodes, `mcp_discovery_response_candidate_${status}`);
|
pushReason(reasonCodes, `mcp_discovery_response_candidate_${status}`);
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_candidate_not_hot_wired");
|
pushReason(
|
||||||
|
reasonCodes,
|
||||||
|
guardedHotHandoff
|
||||||
|
? "mcp_discovery_response_candidate_guarded_hot_wired"
|
||||||
|
: "mcp_discovery_response_candidate_not_hot_wired"
|
||||||
|
);
|
||||||
|
|
||||||
const replyText =
|
const replyText =
|
||||||
entry && (status === "ready_for_guarded_use" || status === "checked_sources_only_candidate" || status === "clarification_candidate")
|
entry && (status === "ready_for_guarded_use" || status === "checked_sources_only_candidate" || status === "clarification_candidate")
|
||||||
|
|
@ -1331,11 +1513,12 @@ export function buildAssistantMcpDiscoveryResponseCandidate(
|
||||||
schema_version: ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION,
|
schema_version: ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION,
|
||||||
policy_owner: "assistantMcpDiscoveryResponseCandidate",
|
policy_owner: "assistantMcpDiscoveryResponseCandidate",
|
||||||
candidate_status: replyText ? status : status === "clarification_candidate" ? status : status,
|
candidate_status: replyText ? status : status === "clarification_candidate" ? status : status,
|
||||||
hot_runtime_wired: false,
|
hot_runtime_wired: guardedHotHandoff,
|
||||||
reply_type: replyTypeFor(status),
|
reply_type: replyTypeFor(status),
|
||||||
reply_text: replyText,
|
reply_text: replyText,
|
||||||
eligible_for_future_hot_runtime:
|
eligible_for_future_hot_runtime:
|
||||||
Boolean(replyText) && (status === "ready_for_guarded_use" || status === "checked_sources_only_candidate" || status === "clarification_candidate"),
|
Boolean(replyText) &&
|
||||||
|
(status === "ready_for_guarded_use" || status === "checked_sources_only_candidate" || status === "clarification_candidate"),
|
||||||
must_keep_internal_mechanics_hidden: true,
|
must_keep_internal_mechanics_hidden: true,
|
||||||
reason_codes: reasonCodes
|
reason_codes: reasonCodes
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,7 @@ function isDetectedIntentAlignedWithTurnMeaning(
|
||||||
normalizedIntent === "inventory_purchase_provenance_for_item" ||
|
normalizedIntent === "inventory_purchase_provenance_for_item" ||
|
||||||
normalizedIntent === "inventory_purchase_documents_for_item" ||
|
normalizedIntent === "inventory_purchase_documents_for_item" ||
|
||||||
normalizedIntent === "inventory_sale_trace_for_item" ||
|
normalizedIntent === "inventory_sale_trace_for_item" ||
|
||||||
|
normalizedIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
normalizedIntent === "inventory_profitability_for_item" ||
|
normalizedIntent === "inventory_profitability_for_item" ||
|
||||||
normalizedIntent === "inventory_purchase_to_sale_chain"
|
normalizedIntent === "inventory_purchase_to_sale_chain"
|
||||||
) {
|
) {
|
||||||
|
|
@ -276,7 +277,7 @@ function isExplicitMetadataDiscoveryTurn(
|
||||||
}
|
}
|
||||||
|
|
||||||
function isInventoryExactAddressIntent(intent: string | null): boolean {
|
function isInventoryExactAddressIntent(intent: string | null): boolean {
|
||||||
return /^(?:inventory_purchase_provenance_for_item|inventory_purchase_documents_for_item|inventory_sale_trace_for_item|inventory_profitability_for_item|inventory_purchase_to_sale_chain|inventory_aging_by_purchase_date|inventory_on_hand_as_of_date)$/u.test(
|
return /^(?:inventory_purchase_provenance_for_item|inventory_purchase_documents_for_item|inventory_sale_trace_for_item|inventory_margin_ranking_for_nomenclature|inventory_profitability_for_item|inventory_purchase_to_sale_chain|inventory_aging_by_purchase_date|inventory_on_hand_as_of_date)$/u.test(
|
||||||
String(intent ?? "")
|
String(intent ?? "")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -305,13 +306,17 @@ function isOpenScopeValueFlowWithoutSubject(
|
||||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||||
): boolean {
|
): boolean {
|
||||||
const graph = readDiscoveryDataNeedGraph(entryPoint);
|
const graph = readDiscoveryDataNeedGraph(entryPoint);
|
||||||
|
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
|
||||||
const businessFactFamily = toNonEmptyString(graph?.business_fact_family);
|
const businessFactFamily = toNonEmptyString(graph?.business_fact_family);
|
||||||
const subjectCandidates = Array.isArray(graph?.subject_candidates) ? graph.subject_candidates : [];
|
const subjectCandidates = Array.isArray(graph?.subject_candidates) ? graph.subject_candidates : [];
|
||||||
const reasonCodes = Array.isArray(graph?.reason_codes) ? graph.reason_codes : [];
|
const reasonCodes = readStringArray(graph?.reason_codes);
|
||||||
|
const clarificationGaps = readStringArray(graph?.clarification_gaps);
|
||||||
|
const explicitOrganizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope);
|
||||||
return (
|
return (
|
||||||
businessFactFamily === "value_flow" &&
|
businessFactFamily === "value_flow" &&
|
||||||
subjectCandidates.length === 0 &&
|
subjectCandidates.length === 0 &&
|
||||||
reasonCodes.some((reason) => toNonEmptyString(reason) === "data_need_graph_open_scope_total_without_subject")
|
(reasonCodes.includes("data_need_graph_open_scope_total_without_subject") ||
|
||||||
|
(Boolean(explicitOrganizationScope) && clarificationGaps.includes("subject")))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -491,6 +496,31 @@ function hasExactBankOperationsAddressReply(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasInventoryMarginRankingAddressReply(
|
||||||
|
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
||||||
|
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||||
|
): boolean {
|
||||||
|
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!toNonEmptyString(input.currentReply)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (hasMetadataDiscoveryPriority(input, entryPoint)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||||
|
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
|
||||||
|
const capabilityId =
|
||||||
|
toNonEmptyString(input.addressRuntimeMeta?.capability_id) ??
|
||||||
|
toNonEmptyString(input.addressRuntimeMeta?.capability_contract_id);
|
||||||
|
return Boolean(
|
||||||
|
detectedIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
|
selectedRecipe === "address_inventory_margin_ranking_for_nomenclature_v1" ||
|
||||||
|
capabilityId === "inventory_inventory_margin_ranking_for_nomenclature"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function hasValueFlowActionConflictWithDiscoveryTurnMeaning(
|
function hasValueFlowActionConflictWithDiscoveryTurnMeaning(
|
||||||
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
||||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||||
|
|
@ -608,6 +638,9 @@ function hasRuntimeAdjustedExactReply(
|
||||||
if (hasMetadataDiscoveryPriority(input, entryPoint)) {
|
if (hasMetadataDiscoveryPriority(input, entryPoint)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (hasEvidenceLaneConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
|
if (hasEvidenceLaneConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -637,6 +670,9 @@ function hasRuntimeMatchedExactReply(
|
||||||
if (hasMetadataDiscoveryPriority(input, entryPoint)) {
|
if (hasMetadataDiscoveryPriority(input, entryPoint)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (hasEvidenceLaneConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
|
if (hasEvidenceLaneConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -663,6 +699,9 @@ function hasAlignedFactualAddressReply(
|
||||||
if (hasMetadataDiscoveryPriority(input, entryPoint)) {
|
if (hasMetadataDiscoveryPriority(input, entryPoint)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
|
if (hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -728,6 +767,9 @@ function hasMatchedFactualAddressContinuationTarget(
|
||||||
if (hasMetadataDiscoveryPriority(input, entryPoint)) {
|
if (hasMetadataDiscoveryPriority(input, entryPoint)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||||
const dialogContinuationContract =
|
const dialogContinuationContract =
|
||||||
toRecordObject(input.addressRuntimeMeta?.dialogContinuationContract) ??
|
toRecordObject(input.addressRuntimeMeta?.dialogContinuationContract) ??
|
||||||
|
|
@ -780,6 +822,9 @@ function hasFullConfirmedFactualAddressReply(
|
||||||
if (hasMetadataDiscoveryPriority(input, entryPoint)) {
|
if (hasMetadataDiscoveryPriority(input, entryPoint)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return hasFullConfirmedTruth(input);
|
return hasFullConfirmedTruth(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -814,6 +859,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||||
entryPoint
|
entryPoint
|
||||||
);
|
);
|
||||||
const exactBankOperationsAddressReply = hasExactBankOperationsAddressReply(input, entryPoint);
|
const exactBankOperationsAddressReply = hasExactBankOperationsAddressReply(input, entryPoint);
|
||||||
|
const inventoryMarginRankingAddressReply = hasInventoryMarginRankingAddressReply(input, entryPoint);
|
||||||
const openScopeValueFlowDiscoveryPriority = hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint);
|
const openScopeValueFlowDiscoveryPriority = hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint);
|
||||||
const metadataDiscoveryPriority = hasMetadataDiscoveryPriority(input, entryPoint);
|
const metadataDiscoveryPriority = hasMetadataDiscoveryPriority(input, entryPoint);
|
||||||
const valueFlowActionConflictWithDiscoveryTurnMeaning = hasValueFlowActionConflictWithDiscoveryTurnMeaning(
|
const valueFlowActionConflictWithDiscoveryTurnMeaning = hasValueFlowActionConflictWithDiscoveryTurnMeaning(
|
||||||
|
|
@ -897,6 +943,9 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||||
if (exactBankOperationsAddressReply) {
|
if (exactBankOperationsAddressReply) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_exact_bank_operations_address_reply");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_exact_bank_operations_address_reply");
|
||||||
}
|
}
|
||||||
|
if (inventoryMarginRankingAddressReply) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_inventory_margin_ranking_address_reply");
|
||||||
|
}
|
||||||
if (deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") {
|
if (deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") {
|
||||||
pushReason(
|
pushReason(
|
||||||
reasonCodes,
|
reasonCodes,
|
||||||
|
|
@ -909,6 +958,9 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||||
if (!candidate.eligible_for_future_hot_runtime) {
|
if (!candidate.eligible_for_future_hot_runtime) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_not_eligible");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_not_eligible");
|
||||||
}
|
}
|
||||||
|
if (candidate.hot_runtime_wired) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_guarded_hot_wired");
|
||||||
|
}
|
||||||
if (!toNonEmptyString(candidate.reply_text)) {
|
if (!toNonEmptyString(candidate.reply_text)) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_missing_reply_text");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_missing_reply_text");
|
||||||
}
|
}
|
||||||
|
|
@ -929,6 +981,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||||
!staleMetadataDiscoveryFallbackAgainstExactAddressReply &&
|
!staleMetadataDiscoveryFallbackAgainstExactAddressReply &&
|
||||||
!exactValueFlowReplyForBusinessOverviewDirectMoneyNeed &&
|
!exactValueFlowReplyForBusinessOverviewDirectMoneyNeed &&
|
||||||
!exactBankOperationsAddressReply &&
|
!exactBankOperationsAddressReply &&
|
||||||
|
!inventoryMarginRankingAddressReply &&
|
||||||
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
|
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
|
||||||
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
|
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
|
||||||
candidate.eligible_for_future_hot_runtime &&
|
candidate.eligible_for_future_hot_runtime &&
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,10 @@ import {
|
||||||
type AssistantMcpDiscoveryPilotExecutionContract,
|
type AssistantMcpDiscoveryPilotExecutionContract,
|
||||||
type AssistantMcpDiscoveryPilotExecutorDeps
|
type AssistantMcpDiscoveryPilotExecutorDeps
|
||||||
} from "./assistantMcpDiscoveryPilotExecutor";
|
} from "./assistantMcpDiscoveryPilotExecutor";
|
||||||
|
import {
|
||||||
|
buildAssistantMcpDiscoveryExecutionHandoff,
|
||||||
|
type AssistantMcpDiscoveryExecutionHandoffContract
|
||||||
|
} from "./assistantMcpDiscoveryExecutionHandoff";
|
||||||
import {
|
import {
|
||||||
planAssistantMcpDiscovery,
|
planAssistantMcpDiscovery,
|
||||||
type AssistantMcpDiscoveryChainId,
|
type AssistantMcpDiscoveryChainId,
|
||||||
|
|
@ -79,6 +83,9 @@ export interface AssistantMcpRouteCandidateContract {
|
||||||
business_fact_family: string | null;
|
business_fact_family: string | null;
|
||||||
action_family: string | null;
|
action_family: string | null;
|
||||||
proof_expectation: string | null;
|
proof_expectation: string | null;
|
||||||
|
evidence_plan_status: string | null;
|
||||||
|
evidence_answer_mode: string | null;
|
||||||
|
evidence_expected_coverage: string | null;
|
||||||
required_axes: string[];
|
required_axes: string[];
|
||||||
provided_axes: string[];
|
provided_axes: string[];
|
||||||
missing_axes: string[];
|
missing_axes: string[];
|
||||||
|
|
@ -92,12 +99,13 @@ export interface AssistantMcpDiscoveryRuntimeBridgeContract {
|
||||||
schema_version: typeof ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION;
|
schema_version: typeof ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION;
|
||||||
policy_owner: "assistantMcpDiscoveryRuntimeBridge";
|
policy_owner: "assistantMcpDiscoveryRuntimeBridge";
|
||||||
bridge_status: AssistantMcpDiscoveryRuntimeBridgeStatus;
|
bridge_status: AssistantMcpDiscoveryRuntimeBridgeStatus;
|
||||||
hot_runtime_wired: false;
|
hot_runtime_wired: boolean;
|
||||||
planner: AssistantMcpDiscoveryPlannerContract;
|
planner: AssistantMcpDiscoveryPlannerContract;
|
||||||
pilot: AssistantMcpDiscoveryPilotExecutionContract;
|
pilot: AssistantMcpDiscoveryPilotExecutionContract;
|
||||||
answer_draft: AssistantMcpDiscoveryAnswerDraftContract;
|
answer_draft: AssistantMcpDiscoveryAnswerDraftContract;
|
||||||
loop_state: AssistantMcpDiscoveryLoopStateContract;
|
loop_state: AssistantMcpDiscoveryLoopStateContract;
|
||||||
route_candidate: AssistantMcpRouteCandidateContract;
|
route_candidate: AssistantMcpRouteCandidateContract;
|
||||||
|
execution_handoff: AssistantMcpDiscoveryExecutionHandoffContract;
|
||||||
user_facing_response_allowed: boolean;
|
user_facing_response_allowed: boolean;
|
||||||
business_fact_answer_allowed: boolean;
|
business_fact_answer_allowed: boolean;
|
||||||
requires_user_clarification: boolean;
|
requires_user_clarification: boolean;
|
||||||
|
|
@ -326,8 +334,14 @@ function buildRouteCandidate(
|
||||||
bridgeStatus: AssistantMcpDiscoveryRuntimeBridgeStatus
|
bridgeStatus: AssistantMcpDiscoveryRuntimeBridgeStatus
|
||||||
): AssistantMcpRouteCandidateContract {
|
): AssistantMcpRouteCandidateContract {
|
||||||
const plannerClarificationGaps = planner.discovery_plan.clarification_gaps ?? [];
|
const plannerClarificationGaps = planner.discovery_plan.clarification_gaps ?? [];
|
||||||
const providedAxes = flattenAxes(pilot, "provided_axes");
|
const evidenceAxes = planner.evidence_plan.evidence_axes;
|
||||||
const missingAxes = plannerClarificationGaps.length > 0 ? plannerClarificationGaps : flattenAxes(pilot, "missing_axis_options");
|
const providedAxes = uniqueStrings([...evidenceAxes.provided_axes, ...flattenAxes(pilot, "provided_axes")]);
|
||||||
|
const missingAxes =
|
||||||
|
plannerClarificationGaps.length > 0
|
||||||
|
? plannerClarificationGaps
|
||||||
|
: evidenceAxes.user_actionable_missing_axes.length > 0
|
||||||
|
? evidenceAxes.user_actionable_missing_axes
|
||||||
|
: flattenAxes(pilot, "missing_axis_options");
|
||||||
const missingProofFamily = routeCandidateMissingProofFamily(planner, pilot);
|
const missingProofFamily = routeCandidateMissingProofFamily(planner, pilot);
|
||||||
const candidateStatus = routeCandidateStatusFor(bridgeStatus, pilot, missingProofFamily);
|
const candidateStatus = routeCandidateStatusFor(bridgeStatus, pilot, missingProofFamily);
|
||||||
return {
|
return {
|
||||||
|
|
@ -338,9 +352,12 @@ function buildRouteCandidate(
|
||||||
selected_chain_summary: planner.selected_chain_summary,
|
selected_chain_summary: planner.selected_chain_summary,
|
||||||
nearest_catalog_chain_template: planner.catalog_chain_template_alignment.top_chain_template_match,
|
nearest_catalog_chain_template: planner.catalog_chain_template_alignment.top_chain_template_match,
|
||||||
catalog_alignment_status: planner.catalog_chain_template_alignment.alignment_status,
|
catalog_alignment_status: planner.catalog_chain_template_alignment.alignment_status,
|
||||||
business_fact_family: planner.data_need_graph?.business_fact_family ?? null,
|
business_fact_family: planner.evidence_plan.data_need.business_fact_family,
|
||||||
action_family: planner.data_need_graph?.action_family ?? null,
|
action_family: planner.evidence_plan.data_need.action_family,
|
||||||
proof_expectation: planner.data_need_graph?.proof_expectation ?? null,
|
proof_expectation: planner.evidence_plan.data_need.proof_expectation,
|
||||||
|
evidence_plan_status: planner.evidence_plan.planner_status,
|
||||||
|
evidence_answer_mode: planner.evidence_plan.answer_contract.answer_mode,
|
||||||
|
evidence_expected_coverage: planner.evidence_plan.coverage_gate.expected_coverage,
|
||||||
required_axes: [...planner.required_axes],
|
required_axes: [...planner.required_axes],
|
||||||
provided_axes: providedAxes,
|
provided_axes: providedAxes,
|
||||||
missing_axes: missingAxes,
|
missing_axes: missingAxes,
|
||||||
|
|
@ -348,7 +365,7 @@ function buildRouteCandidate(
|
||||||
enablement_reason: routeCandidateEnablementReason(candidateStatus, pilot, missingAxes, missingProofFamily),
|
enablement_reason: routeCandidateEnablementReason(candidateStatus, pilot, missingAxes, missingProofFamily),
|
||||||
recommended_next_action: routeCandidateNextAction(candidateStatus),
|
recommended_next_action: routeCandidateNextAction(candidateStatus),
|
||||||
forbidden_overclaim_flags: uniqueStrings([
|
forbidden_overclaim_flags: uniqueStrings([
|
||||||
...(planner.data_need_graph?.forbidden_overclaim_flags ?? []),
|
...planner.evidence_plan.answer_contract.forbidden_overclaim_flags,
|
||||||
...(missingProofFamily ? [missingProofFamily.must_not_claim] : [])
|
...(missingProofFamily ? [missingProofFamily.must_not_claim] : [])
|
||||||
])
|
])
|
||||||
};
|
};
|
||||||
|
|
@ -402,6 +419,16 @@ export async function runAssistantMcpDiscoveryRuntimeBridge(
|
||||||
const bridgeStatus = bridgeStatusFor(pilot, answerDraft);
|
const bridgeStatus = bridgeStatusFor(pilot, answerDraft);
|
||||||
const loopState = buildLoopState(planner, pilot, bridgeStatus);
|
const loopState = buildLoopState(planner, pilot, bridgeStatus);
|
||||||
const routeCandidate = buildRouteCandidate(planner, pilot, bridgeStatus);
|
const routeCandidate = buildRouteCandidate(planner, pilot, bridgeStatus);
|
||||||
|
const userFacingResponseAllowed = bridgeStatus !== "blocked";
|
||||||
|
const businessFactAllowed = businessFactAnswerAllowed(answerDraft);
|
||||||
|
const executionHandoff = buildAssistantMcpDiscoveryExecutionHandoff({
|
||||||
|
bridgeStatus,
|
||||||
|
routeCandidate,
|
||||||
|
pilot,
|
||||||
|
answerDraft,
|
||||||
|
businessFactAnswerAllowed: businessFactAllowed,
|
||||||
|
userFacingResponseAllowed
|
||||||
|
});
|
||||||
const reasonCodes = uniqueStrings([...planner.reason_codes, ...pilot.reason_codes, ...answerDraft.reason_codes]);
|
const reasonCodes = uniqueStrings([...planner.reason_codes, ...pilot.reason_codes, ...answerDraft.reason_codes]);
|
||||||
|
|
||||||
pushReason(reasonCodes, `runtime_bridge_status_${bridgeStatus}`);
|
pushReason(reasonCodes, `runtime_bridge_status_${bridgeStatus}`);
|
||||||
|
|
@ -409,19 +436,24 @@ export async function runAssistantMcpDiscoveryRuntimeBridge(
|
||||||
pushReason(reasonCodes, `runtime_bridge_loop_state_${loopState.loop_status}`);
|
pushReason(reasonCodes, `runtime_bridge_loop_state_${loopState.loop_status}`);
|
||||||
pushReason(reasonCodes, "runtime_bridge_route_candidate_built");
|
pushReason(reasonCodes, "runtime_bridge_route_candidate_built");
|
||||||
pushReason(reasonCodes, `runtime_bridge_route_candidate_${routeCandidate.candidate_status}`);
|
pushReason(reasonCodes, `runtime_bridge_route_candidate_${routeCandidate.candidate_status}`);
|
||||||
|
pushReason(reasonCodes, `runtime_bridge_execution_handoff_${executionHandoff.handoff_status}`);
|
||||||
|
for (const reasonCode of executionHandoff.reason_codes) {
|
||||||
|
pushReason(reasonCodes, reasonCode);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
schema_version: ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION,
|
schema_version: ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION,
|
||||||
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
|
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
|
||||||
bridge_status: bridgeStatus,
|
bridge_status: bridgeStatus,
|
||||||
hot_runtime_wired: false,
|
hot_runtime_wired: executionHandoff.can_use_guarded_response,
|
||||||
planner,
|
planner,
|
||||||
pilot,
|
pilot,
|
||||||
answer_draft: answerDraft,
|
answer_draft: answerDraft,
|
||||||
loop_state: loopState,
|
loop_state: loopState,
|
||||||
route_candidate: routeCandidate,
|
route_candidate: routeCandidate,
|
||||||
user_facing_response_allowed: bridgeStatus !== "blocked",
|
execution_handoff: executionHandoff,
|
||||||
business_fact_answer_allowed: businessFactAnswerAllowed(answerDraft),
|
user_facing_response_allowed: userFacingResponseAllowed,
|
||||||
|
business_fact_answer_allowed: businessFactAllowed,
|
||||||
requires_user_clarification: bridgeStatus === "needs_clarification",
|
requires_user_clarification: bridgeStatus === "needs_clarification",
|
||||||
reason_codes: reasonCodes
|
reason_codes: reasonCodes
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export interface AssistantMcpDiscoveryRuntimeEntryPointContract {
|
||||||
schema_version: typeof ASSISTANT_MCP_DISCOVERY_RUNTIME_ENTRY_POINT_SCHEMA_VERSION;
|
schema_version: typeof ASSISTANT_MCP_DISCOVERY_RUNTIME_ENTRY_POINT_SCHEMA_VERSION;
|
||||||
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint";
|
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint";
|
||||||
entry_status: AssistantMcpDiscoveryRuntimeEntryPointStatus;
|
entry_status: AssistantMcpDiscoveryRuntimeEntryPointStatus;
|
||||||
hot_runtime_wired: false;
|
hot_runtime_wired: boolean;
|
||||||
discovery_attempted: boolean;
|
discovery_attempted: boolean;
|
||||||
turn_input: AssistantMcpDiscoveryTurnInputContract;
|
turn_input: AssistantMcpDiscoveryTurnInputContract;
|
||||||
bridge: AssistantMcpDiscoveryRuntimeBridgeContract | null;
|
bridge: AssistantMcpDiscoveryRuntimeBridgeContract | null;
|
||||||
|
|
@ -108,13 +108,18 @@ export async function runAssistantMcpDiscoveryRuntimeEntryPoint(
|
||||||
});
|
});
|
||||||
const reasonCodes = uniqueStrings([...turnInput.reason_codes, ...bridge.reason_codes]);
|
const reasonCodes = uniqueStrings([...turnInput.reason_codes, ...bridge.reason_codes]);
|
||||||
pushReason(reasonCodes, "runtime_entry_point_bridge_executed");
|
pushReason(reasonCodes, "runtime_entry_point_bridge_executed");
|
||||||
pushReason(reasonCodes, "runtime_entry_point_not_wired_to_hot_assistant_answer");
|
pushReason(
|
||||||
|
reasonCodes,
|
||||||
|
bridge.hot_runtime_wired
|
||||||
|
? "runtime_entry_point_wired_to_guarded_hot_assistant_answer"
|
||||||
|
: "runtime_entry_point_not_wired_to_hot_assistant_answer"
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
schema_version: ASSISTANT_MCP_DISCOVERY_RUNTIME_ENTRY_POINT_SCHEMA_VERSION,
|
schema_version: ASSISTANT_MCP_DISCOVERY_RUNTIME_ENTRY_POINT_SCHEMA_VERSION,
|
||||||
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
|
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
|
||||||
entry_status: "bridge_executed",
|
entry_status: "bridge_executed",
|
||||||
hot_runtime_wired: false,
|
hot_runtime_wired: bridge.hot_runtime_wired,
|
||||||
discovery_attempted: true,
|
discovery_attempted: true,
|
||||||
turn_input: turnInput,
|
turn_input: turnInput,
|
||||||
bridge,
|
bridge,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import {
|
||||||
buildAssistantMcpDiscoveryDataNeedGraph,
|
buildAssistantMcpDiscoveryDataNeedGraph,
|
||||||
type AssistantMcpDiscoveryDataNeedGraphContract
|
type AssistantMcpDiscoveryDataNeedGraphContract
|
||||||
} from "./assistantMcpDiscoveryDataNeedGraph";
|
} from "./assistantMcpDiscoveryDataNeedGraph";
|
||||||
|
import { resolveOrganizationSelectionFromMessage } from "./assistantOrganizationMatcher";
|
||||||
import { normalizeRussianComparableText, repairAddressMojibakeText } from "./addressTextRepair";
|
import { normalizeRussianComparableText, repairAddressMojibakeText } from "./addressTextRepair";
|
||||||
import type {
|
import type {
|
||||||
AssistantMcpDiscoveryMetadataRecommendedPrimitive,
|
AssistantMcpDiscoveryMetadataRecommendedPrimitive,
|
||||||
|
|
@ -27,6 +28,7 @@ export interface BuildAssistantMcpDiscoveryTurnInputAdapterInput {
|
||||||
followupContext?: Record<string, unknown> | null;
|
followupContext?: Record<string, unknown> | null;
|
||||||
userMessage?: string | null;
|
userMessage?: string | null;
|
||||||
effectiveMessage?: string | null;
|
effectiveMessage?: string | null;
|
||||||
|
knownOrganizations?: unknown[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AssistantMcpDiscoveryTurnInputContract {
|
export interface AssistantMcpDiscoveryTurnInputContract {
|
||||||
|
|
@ -351,6 +353,18 @@ function collectEntityCandidates(value: unknown): string[] {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function collectReasonCodes(value: unknown): string[] {
|
||||||
|
const result: string[] = [];
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
const reason = toNonEmptyString(value);
|
||||||
|
return reason ? [reason] : result;
|
||||||
|
}
|
||||||
|
for (const item of value) {
|
||||||
|
pushUnique(result, item);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
function collectPredecomposeEntities(predecompose: Record<string, unknown> | null): {
|
function collectPredecomposeEntities(predecompose: Record<string, unknown> | null): {
|
||||||
counterparty: string | null;
|
counterparty: string | null;
|
||||||
organization: string | null;
|
organization: string | null;
|
||||||
|
|
@ -1044,12 +1058,17 @@ function hasBusinessOverviewContinuationSignal(text: string): boolean {
|
||||||
/(?:\u043f\u043e\u043b\u0443\u0447|\u0437\u0430\u043f\u043b\u0430\u0442|\u043d\u0435\u0442\u0442\u043e|\u0434\u0435\u043d\p{L}*|\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|received|paid|net|cash|customer|supplier)/iu.test(
|
/(?:\u043f\u043e\u043b\u0443\u0447|\u0437\u0430\u043f\u043b\u0430\u0442|\u043d\u0435\u0442\u0442\u043e|\u0434\u0435\u043d\p{L}*|\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|received|paid|net|cash|customer|supplier)/iu.test(
|
||||||
normalized
|
normalized
|
||||||
);
|
);
|
||||||
|
const hasCashflowPolarityCue =
|
||||||
|
/(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)[\s\S]{0,80}(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\p{L}*|\u0443\u0448\p{L}*|\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*)|(?:\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u043d\u0435\u0442\u0442\u043e)[\s\S]{0,80}(?:\u043f\u043e\s+\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\p{L}*)/iu.test(
|
||||||
|
normalized
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
hasEvidenceContinuationCue ||
|
hasEvidenceContinuationCue ||
|
||||||
hasAnalystContinuationCue ||
|
hasAnalystContinuationCue ||
|
||||||
hasTaxContinuationCue ||
|
hasTaxContinuationCue ||
|
||||||
hasFinalSummaryCue ||
|
hasFinalSummaryCue ||
|
||||||
hasMoneyBreakdownCue
|
hasMoneyBreakdownCue ||
|
||||||
|
hasCashflowPolarityCue
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1114,6 +1133,13 @@ function hasBusinessOverviewFollowupSeed(followupSeed: ReturnType<typeof collect
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasValueFlowSignal(text: string): boolean {
|
function hasValueFlowSignal(text: string): boolean {
|
||||||
|
if (
|
||||||
|
/(?:\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\u043a|\u043e\u043f\u043b\u0430\u0442|\u043f\u043b\u0430\u0442[\u0435\u0451]\u0436|\u0437\u0430\u043f\u043b\u0430\u0442|\u043f\u0435\u0440\u0435\u0447\u0438\u0441\u043b|\u0441\u043f\u0438\u0441\u0430\u043d|\u0440\u0430\u0441\u0445\u043e\u0434|\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u0432\u0445\u043e\u0434\u044f\u0449|\u043f\u043e\u043b\u0443\u0447(?:\u0438\u043b|\u0435\u043d\u043e|\u0435\u043d)|\u043f\u043e\u0441\u0442\u0443\u043f\u0438\u043b|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d|\u0434\u0435\u043d\p{L}*|\u0437\u0430\u0440\u0430\u0431\u043e\u0442|supplier|value[-\s]?flow|turnover|revenue|payment|payout|outflow|cash\s+flow|\bearn(?:ed|ing|ings)?\b)/iu.test(
|
||||||
|
text
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return /(?:оборот|выручк|оплат|плат[её]ж|заплат|перечисл|списан|расход|исходящ|входящ|получ(?:ил|ено|ен)|поступил|поступлен|денежн[а-яёa-z0-9_-]*\s+поток|(?<!\p{L})заработ(?:ал|али|ало|аем|ает|ать|ано|ок)(?!\p{L})|supplier|value[-\s]?flow|turnover|revenue|payment|payout|outflow|cash\s+flow|\bearn(?:ed|ing|ings)?\b)/iu.test(
|
return /(?:оборот|выручк|оплат|плат[её]ж|заплат|перечисл|списан|расход|исходящ|входящ|получ(?:ил|ено|ен)|поступил|поступлен|денежн[а-яёa-z0-9_-]*\s+поток|(?<!\p{L})заработ(?:ал|али|ало|аем|ает|ать|ано|ок)(?!\p{L})|supplier|value[-\s]?flow|turnover|revenue|payment|payout|outflow|cash\s+flow|\bearn(?:ed|ing|ings)?\b)/iu.test(
|
||||||
text
|
text
|
||||||
);
|
);
|
||||||
|
|
@ -1132,6 +1158,13 @@ function hasPayoutSignal(text: string): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasBidirectionalValueFlowSignal(text: string): boolean {
|
function hasBidirectionalValueFlowSignal(text: string): boolean {
|
||||||
|
if (
|
||||||
|
/(?:\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|\u0431\u0430\u043b\u0430\u043d\u0441\s+(?:\u043f\u043b\u0430\u0442|\u0434\u0435\u043d\u0435\u0433|\u0434\u0435\u043d\u0435\u0436)|\u0432\u0437\u0430\u0438\u043c\u043e\u0440\u0430\u0441\u0447[\u0435\u0451]\u0442|\u043f\u043e\u043b\u0443\u0447\p{L}*.*(?:\u0437\u0430)?\u043f\u043b\u0430\u0442\p{L}*|(?:\u0437\u0430)?\u043f\u043b\u0430\u0442\p{L}*.*\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0432\u0445\u043e\u0434\u044f\u0449.*\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u0438\u0441\u0445\u043e\u0434\u044f\u0449.*\u0432\u0445\u043e\u0434\u044f\u0449|\u043f\u0440\u0438\u0448\p{L}*.*\u0443\u0448\p{L}*|\u0443\u0448\p{L}*.*\u043f\u0440\u0438\u0448\p{L}*|\u043f\u043b\u044e\u0441.*\u043c\u0438\u043d\u0443\u0441|\u043c\u0438\u043d\u0443\u0441.*\u043f\u043b\u044e\u0441|net\s+(?:flow|cash|payment)|cash\s+net|incoming\s+and\s+outgoing|received\s+and\s+paid|paid\s+and\s+received)/iu.test(
|
||||||
|
text
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return /(?:нетто|сальдо|баланс\s+(?:плат|денег|денеж)|взаиморасч[её]т|получил[иа]?.*(?:за)?платил|(?:за)?платил[иа]?.*получил|входящ.*исходящ|исходящ.*входящ|дебет.*кредит|кредит.*дебет|net\s+(?:flow|cash|payment)|cash\s+net|incoming\s+and\s+outgoing|received\s+and\s+paid|paid\s+and\s+received)/iu.test(
|
return /(?:нетто|сальдо|баланс\s+(?:плат|денег|денеж)|взаиморасч[её]т|получил[иа]?.*(?:за)?платил|(?:за)?платил[иа]?.*получил|входящ.*исходящ|исходящ.*входящ|дебет.*кредит|кредит.*дебет|net\s+(?:flow|cash|payment)|cash\s+net|incoming\s+and\s+outgoing|received\s+and\s+paid|paid\s+and\s+received)/iu.test(
|
||||||
text
|
text
|
||||||
);
|
);
|
||||||
|
|
@ -1178,6 +1211,46 @@ function extractOrganizationScopeFromRawText(value: unknown): string | null {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractOrganizationScopeFromSemanticText(value: unknown): string | null {
|
||||||
|
const text = toNonEmptyString(value);
|
||||||
|
if (!text) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const match = text.match(
|
||||||
|
/(?:^|[\s,;:])(?:\u043a\u043e\u043c\u043f\u0430\u043d(?:\u0438\u0438|\u0438\u044f)|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446(?:\u0438\u0438|\u0438\u044f))\s+(.+?)(?=$|[\n,.;:!?]|\s+\u0437\u0430\s+(?:\d{4}|\d{2}\s*\u0433|\d{2}\s+\u0433\u043e\u0434)|\s+\u043d\u0430\s+(?:\d{4}|\d{2}\s*\u0433|\d{2}\s+\u0433\u043e\u0434))/iu
|
||||||
|
);
|
||||||
|
return toNonEmptyString(match?.[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeLooseOrganizationAlias(value: string | null): string | null {
|
||||||
|
const text = toNonEmptyString(value);
|
||||||
|
if (!text) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const normalized = text
|
||||||
|
.replace(/^[«"'“”]+|[»"'“”]+$/gu, "")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
if (!normalized) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const comparable = normalizeRussianComparableText(normalized);
|
||||||
|
const normalizedTokens = normalized.split(/\s+/u).filter(Boolean);
|
||||||
|
const hasYearOnlyTimeTail =
|
||||||
|
/\b(?:19|20)\d{2}\b/u.test(normalized) &&
|
||||||
|
normalizedTokens.length <= 4 &&
|
||||||
|
!/(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e|llc|inc|corp)/iu.test(
|
||||||
|
comparable
|
||||||
|
);
|
||||||
|
if (hasYearOnlyTimeTail) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (/^(?:\u0438|\u0432|\u0432\u043e|\u0437\u0430|\u043d\u0430|\u043f\u043e|\u043a\u0442\u043e|\u0447\u0442\u043e|\u043a\u0430\u043a(?:\u043e\u0439|\u0430\u044f|\u0438\u0435)?|\u0433\u043b\u0430\u0432\u043d\p{L}*)\b/iu.test(comparable)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
function hasMonthlyAggregationSignal(text: string): boolean {
|
function hasMonthlyAggregationSignal(text: string): boolean {
|
||||||
return /(?:\u043f\u043e\s+\u043c\u0435\u0441\u044f\u0446\u0430\u043c|\u043f\u043e\u043c\u0435\u0441\u044f\u0447\u043d\u043e|\u0435\u0436\u0435\u043c\u0435\u0441\u044f\u0447\u043d\u043e|month\s+by\s+month|by\s+month|monthly)/iu.test(
|
return /(?:\u043f\u043e\s+\u043c\u0435\u0441\u044f\u0446\u0430\u043c|\u043f\u043e\u043c\u0435\u0441\u044f\u0447\u043d\u043e|\u0435\u0436\u0435\u043c\u0435\u0441\u044f\u0447\u043d\u043e|month\s+by\s+month|by\s+month|monthly)/iu.test(
|
||||||
text
|
text
|
||||||
|
|
@ -1451,7 +1524,10 @@ function metadataScopeHintFromRawText(text: string): string | null {
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasExplicitDateScopeLiteral(text: string): boolean {
|
function hasExplicitDateScopeLiteral(text: string): boolean {
|
||||||
return /(?:\b(?:19|20)\d{2}\b|\b\d{4}-\d{2}-\d{2}\b|\b\d{4}-\d{2}\b)/iu.test(text);
|
if (/\b\d{2}\s*(?:\u0433(?:\u043e\u0434(?:\u0430|\u0443)?)?\.?)\b/iu.test(text)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return /(?:\b(?:19|20)\d{2}\b|\b\d{4}-\d{2}-\d{2}\b|\b\d{4}-\d{2}\b|\b\d{2}\s*(?:г(?:од(?:а|у)?)?\.?))\b/iu.test(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
function stripNegatedTaxDateScopeClauses(text: string): string {
|
function stripNegatedTaxDateScopeClauses(text: string): string {
|
||||||
|
|
@ -1483,6 +1559,20 @@ function collectDateScopeFromRawText(text: string): string | null {
|
||||||
if (year?.[1]) {
|
if (year?.[1]) {
|
||||||
return year[1];
|
return year[1];
|
||||||
}
|
}
|
||||||
|
const utf8ShortYear = text.match(/\b(\d{2})\s*(?:\u0433(?:\u043e\u0434(?:\u0430|\u0443)?)?\.?)\b/iu);
|
||||||
|
if (utf8ShortYear?.[1]) {
|
||||||
|
const numericYear = Number.parseInt(utf8ShortYear[1], 10);
|
||||||
|
if (Number.isFinite(numericYear)) {
|
||||||
|
return String(numericYear <= 30 ? 2000 + numericYear : 1900 + numericYear);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const shortYear = text.match(/\b(\d{2})\s*(?:г(?:од(?:а|у)?)?\.?)\b/iu);
|
||||||
|
if (shortYear?.[1]) {
|
||||||
|
const numericYear = Number.parseInt(shortYear[1], 10);
|
||||||
|
if (Number.isFinite(numericYear)) {
|
||||||
|
return String(numericYear <= 30 ? 2000 + numericYear : 1900 + numericYear);
|
||||||
|
}
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1598,17 +1688,51 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
const reasonCodes: string[] = [];
|
const reasonCodes: string[] = [];
|
||||||
const rawUserText = toNonEmptyString(input.userMessage);
|
const rawUserText = toNonEmptyString(input.userMessage);
|
||||||
const rawEffectiveText = toNonEmptyString(input.effectiveMessage);
|
const rawEffectiveText = toNonEmptyString(input.effectiveMessage);
|
||||||
|
const knownOrganizations = Array.isArray(input.knownOrganizations) ? input.knownOrganizations : [];
|
||||||
const repairedUserText = rawUserText ? repairAddressMojibakeText(rawUserText) : null;
|
const repairedUserText = rawUserText ? repairAddressMojibakeText(rawUserText) : null;
|
||||||
const repairedEffectiveText = rawEffectiveText ? repairAddressMojibakeText(rawEffectiveText) : null;
|
const repairedEffectiveText = rawEffectiveText ? repairAddressMojibakeText(rawEffectiveText) : null;
|
||||||
|
const rawUserSignalSourceText = repairedUserText ?? rawUserText ?? "";
|
||||||
const rawSignalSourceText = `${repairedUserText ?? rawUserText ?? ""} ${repairedEffectiveText ?? rawEffectiveText ?? ""}`.trim();
|
const rawSignalSourceText = `${repairedUserText ?? rawUserText ?? ""} ${repairedEffectiveText ?? rawEffectiveText ?? ""}`.trim();
|
||||||
const rawEntitySourceText = repairedUserText ?? rawUserText ?? repairedEffectiveText ?? rawEffectiveText ?? rawSignalSourceText;
|
const rawEntitySourceText = repairedUserText ?? rawUserText ?? repairedEffectiveText ?? rawEffectiveText ?? rawSignalSourceText;
|
||||||
|
const rawUserEntitySourceText = rawUserSignalSourceText || rawEntitySourceText;
|
||||||
|
const rawUserTextOnly = compactLower(rawUserSignalSourceText);
|
||||||
|
const rawAssistantEntityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates);
|
||||||
|
const rawUserPrimaryBusinessOverviewSignal = hasBusinessOverviewSignal(rawUserTextOnly);
|
||||||
|
const rawUserLifecyclePivotTextSignal =
|
||||||
|
!rawUserPrimaryBusinessOverviewSignal && hasLifecycleSignal(rawUserTextOnly);
|
||||||
|
const rawUserBidirectionalValueFlowPivotTextSignal =
|
||||||
|
!rawUserPrimaryBusinessOverviewSignal &&
|
||||||
|
!rawUserLifecyclePivotTextSignal &&
|
||||||
|
hasBidirectionalValueFlowSignal(rawUserTextOnly);
|
||||||
|
const rawUserScopedEntityCandidate = rawUserSignalSourceText
|
||||||
|
? rawScopedEntityCandidateFromText(rawUserEntitySourceText)
|
||||||
|
: null;
|
||||||
|
const rawUserCounterpartyBidirectionalOverride = Boolean(
|
||||||
|
rawUserBidirectionalValueFlowPivotTextSignal &&
|
||||||
|
(rawUserScopedEntityCandidate ||
|
||||||
|
predecomposeEntities.counterparty ||
|
||||||
|
rawAssistantEntityCandidates.find((candidate) => !isInvalidEntityCandidate(candidate)))
|
||||||
|
);
|
||||||
const rawText = compactLower(rawSignalSourceText);
|
const rawText = compactLower(rawSignalSourceText);
|
||||||
const rawReferentialDocumentExclusionSignal = hasReferentialDocumentExclusionFollowupSignal(
|
const rawReferentialDocumentExclusionSignal = hasReferentialDocumentExclusionFollowupSignal(
|
||||||
repairedUserText ?? rawUserText ?? ""
|
repairedUserText ?? rawUserText ?? ""
|
||||||
);
|
);
|
||||||
const rawPrimaryBusinessOverviewSignal = hasBusinessOverviewSignal(rawText);
|
const rawPrimaryBusinessOverviewSignal =
|
||||||
|
hasBusinessOverviewSignal(rawText) && !rawUserCounterpartyBidirectionalOverride;
|
||||||
const explicitVatQuestionSignal = hasExplicitVatQuestionSignal(rawText);
|
const explicitVatQuestionSignal = hasExplicitVatQuestionSignal(rawText);
|
||||||
const explicitVatMovementEvidenceSignal = hasExplicitVatMovementEvidenceSignal(rawText);
|
const explicitVatMovementEvidenceSignal = hasExplicitVatMovementEvidenceSignal(rawText);
|
||||||
|
const rawLifecyclePivotTextSignal =
|
||||||
|
!rawPrimaryBusinessOverviewSignal && hasLifecycleSignal(rawText);
|
||||||
|
const rawBidirectionalValueFlowPivotTextSignal =
|
||||||
|
!rawPrimaryBusinessOverviewSignal &&
|
||||||
|
!rawLifecyclePivotTextSignal &&
|
||||||
|
hasBidirectionalValueFlowSignal(rawText);
|
||||||
|
const rawValueFlowPivotTextSignal =
|
||||||
|
!rawPrimaryBusinessOverviewSignal &&
|
||||||
|
!rawLifecyclePivotTextSignal &&
|
||||||
|
(hasValueFlowSignal(rawText) ||
|
||||||
|
hasValueRankingSignal(rawText) ||
|
||||||
|
rawBidirectionalValueFlowPivotTextSignal);
|
||||||
const explicitVatSuppressesBusinessOverviewContinuation = Boolean(
|
const explicitVatSuppressesBusinessOverviewContinuation = Boolean(
|
||||||
explicitVatQuestionSignal && !rawPrimaryBusinessOverviewSignal
|
explicitVatQuestionSignal && !rawPrimaryBusinessOverviewSignal
|
||||||
);
|
);
|
||||||
|
|
@ -1634,7 +1758,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
!rawLifecycleSignal && !rawValueFlowSignal && !rawMetadataSignal && hasEntityResolutionSignal(rawText);
|
!rawLifecycleSignal && !rawValueFlowSignal && !rawMetadataSignal && hasEntityResolutionSignal(rawText);
|
||||||
const rawPayoutSignal = rawValueFlowSignal && !rawBidirectionalValueFlowSignal && hasPayoutSignal(rawText);
|
const rawPayoutSignal = rawValueFlowSignal && !rawBidirectionalValueFlowSignal && hasPayoutSignal(rawText);
|
||||||
const rawValueFlowAggregateQuestionSignal =
|
const rawValueFlowAggregateQuestionSignal =
|
||||||
rawValueFlowSignal && hasValueFlowAggregateQuestionSignal(rawText);
|
(rawValueFlowSignal || rawValueFlowPivotTextSignal) && hasValueFlowAggregateQuestionSignal(rawText);
|
||||||
const monthlyAggregationSignal = hasMonthlyAggregationSignal(rawText);
|
const monthlyAggregationSignal = hasMonthlyAggregationSignal(rawText);
|
||||||
const rawAllTimeScopeSignal = hasAllTimeScopeHint(rawText);
|
const rawAllTimeScopeSignal = hasAllTimeScopeHint(rawText);
|
||||||
const dateScopeSignalText = stripNegatedTaxDateScopeClauses(rawText);
|
const dateScopeSignalText = stripNegatedTaxDateScopeClauses(rawText);
|
||||||
|
|
@ -1670,6 +1794,10 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family);
|
const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family);
|
||||||
const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis);
|
const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis);
|
||||||
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
|
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
|
||||||
|
const assistantTurnMeaningReasonCodes = collectReasonCodes(assistantTurnMeaning?.reason_codes);
|
||||||
|
const compactCashflowDisplayFollowupSignal = assistantTurnMeaningReasonCodes.includes(
|
||||||
|
"compact_cashflow_display_current_turn_signal"
|
||||||
|
);
|
||||||
const broadBusinessEvaluationUnsupported = unsupported === "broad_business_evaluation";
|
const broadBusinessEvaluationUnsupported = unsupported === "broad_business_evaluation";
|
||||||
const seededBusinessOverviewSignal =
|
const seededBusinessOverviewSignal =
|
||||||
broadBusinessEvaluationUnsupported ||
|
broadBusinessEvaluationUnsupported ||
|
||||||
|
|
@ -1702,6 +1830,74 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
: profitMarginBusinessOverviewSignal
|
: profitMarginBusinessOverviewSignal
|
||||||
? "profit_margin_boundary"
|
? "profit_margin_boundary"
|
||||||
: "broad_evaluation";
|
: "broad_evaluation";
|
||||||
|
const assistantTurnMeaningDateScope = toNonEmptyString(assistantTurnMeaning?.explicit_date_scope);
|
||||||
|
const rawAssistantTurnMeaningOrganizationScope = toNonEmptyString(assistantTurnMeaning?.explicit_organization_scope);
|
||||||
|
const assistantTurnMeaningOrganizationScope = isReferentialOrganizationPlaceholder(
|
||||||
|
rawAssistantTurnMeaningOrganizationScope
|
||||||
|
)
|
||||||
|
? null
|
||||||
|
: rawAssistantTurnMeaningOrganizationScope;
|
||||||
|
const organizationSelectionFromKnown =
|
||||||
|
resolveOrganizationSelectionFromMessage(rawUserText ?? rawEffectiveText ?? rawSignalSourceText, knownOrganizations) ??
|
||||||
|
resolveOrganizationSelectionFromMessage(rawSignalSourceText, knownOrganizations) ??
|
||||||
|
null;
|
||||||
|
const rawOrganizationScope =
|
||||||
|
extractOrganizationScopeFromRawText(rawUserText) ??
|
||||||
|
extractOrganizationScopeFromRawText(rawEffectiveText) ??
|
||||||
|
extractOrganizationScopeFromRawText(rawSignalSourceText);
|
||||||
|
const semanticOrganizationScope =
|
||||||
|
normalizeLooseOrganizationAlias(
|
||||||
|
extractOrganizationScopeFromSemanticText(rawEffectiveText) ??
|
||||||
|
extractOrganizationScopeFromSemanticText(rawUserText) ??
|
||||||
|
extractOrganizationScopeFromSemanticText(rawSignalSourceText)
|
||||||
|
);
|
||||||
|
const predecomposeCounterpartyAsOrganizationScope =
|
||||||
|
(rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
|
||||||
|
semanticOrganizationScope &&
|
||||||
|
!rawScopedEntityCandidate &&
|
||||||
|
!predecomposeEntities.organization
|
||||||
|
? normalizeLooseOrganizationAlias(predecomposeEntities.counterparty)
|
||||||
|
: null;
|
||||||
|
const rawOrganizationMentionSignal =
|
||||||
|
hasOrganizationScopeSignalUtf8(rawText) ||
|
||||||
|
Boolean(organizationSelectionFromKnown || rawOrganizationScope || semanticOrganizationScope || predecomposeCounterpartyAsOrganizationScope);
|
||||||
|
const currentTurnFreshOrganizationScope =
|
||||||
|
organizationSelectionFromKnown ??
|
||||||
|
predecomposeEntities.organization ??
|
||||||
|
rawOrganizationScope ??
|
||||||
|
semanticOrganizationScope ??
|
||||||
|
predecomposeCounterpartyAsOrganizationScope;
|
||||||
|
const currentTurnOrganizationScope =
|
||||||
|
currentTurnFreshOrganizationScope ?? assistantTurnMeaningOrganizationScope;
|
||||||
|
const predecomposeOrganizationMirrorsCounterparty = sameScopedName(
|
||||||
|
predecomposeEntities.counterparty,
|
||||||
|
predecomposeEntities.organization
|
||||||
|
);
|
||||||
|
const organizationMirrorsPredecomposeCounterpartyForPivot = Boolean(
|
||||||
|
sameScopedName(predecomposeEntities.counterparty, assistantTurnMeaningOrganizationScope) ||
|
||||||
|
sameScopedName(predecomposeEntities.counterparty, currentTurnOrganizationScope) ||
|
||||||
|
predecomposeOrganizationMirrorsCounterparty
|
||||||
|
);
|
||||||
|
const normalizedPredecomposeCounterpartyForPivot =
|
||||||
|
organizationMirrorsPredecomposeCounterpartyForPivot
|
||||||
|
? null
|
||||||
|
: normalizeFollowupCounterpartyCandidate(predecomposeEntities.counterparty);
|
||||||
|
const rawExplicitCounterpartyPivotCandidate =
|
||||||
|
rawScopedEntityCandidate ??
|
||||||
|
rawAssistantEntityCandidates.find(
|
||||||
|
(candidate) =>
|
||||||
|
!isInvalidEntityCandidate(candidate) &&
|
||||||
|
!sameScopedName(candidate, currentTurnOrganizationScope)
|
||||||
|
) ??
|
||||||
|
normalizedPredecomposeCounterpartyForPivot ??
|
||||||
|
null;
|
||||||
|
const businessOverviewCounterpartyValueFlowPivot = Boolean(
|
||||||
|
businessOverviewContinuationSignal &&
|
||||||
|
!rawPrimaryBusinessOverviewSignal &&
|
||||||
|
rawValueFlowPivotTextSignal &&
|
||||||
|
rawExplicitCounterpartyPivotCandidate &&
|
||||||
|
(rawTopicSwitchSignal || rawValueFlowAggregateQuestionSignal)
|
||||||
|
);
|
||||||
const businessOverviewUnsupportedFamily = inventoryReserveBusinessOverviewSignal
|
const businessOverviewUnsupportedFamily = inventoryReserveBusinessOverviewSignal
|
||||||
? "inventory_reserve_liquidation_boundary"
|
? "inventory_reserve_liquidation_boundary"
|
||||||
: debtDueDateBusinessOverviewSignal
|
: debtDueDateBusinessOverviewSignal
|
||||||
|
|
@ -1712,8 +1908,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
? "profit_margin_boundary"
|
? "profit_margin_boundary"
|
||||||
: "broad_business_evaluation";
|
: "broad_business_evaluation";
|
||||||
const businessOverviewSignal =
|
const businessOverviewSignal =
|
||||||
rawBusinessOverviewSignal ||
|
!businessOverviewCounterpartyValueFlowPivot &&
|
||||||
seededBusinessOverviewSignal;
|
(rawBusinessOverviewSignal || seededBusinessOverviewSignal);
|
||||||
const businessOverviewSeparateCounterpartySignal = Boolean(
|
const businessOverviewSeparateCounterpartySignal = Boolean(
|
||||||
businessOverviewSignal && hasBusinessOverviewSeparateCounterpartySignal(rawText)
|
businessOverviewSignal && hasBusinessOverviewSeparateCounterpartySignal(rawText)
|
||||||
);
|
);
|
||||||
|
|
@ -1735,18 +1931,6 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
hasSimpleMovementLanePivotSignal(rawText) ||
|
hasSimpleMovementLanePivotSignal(rawText) ||
|
||||||
hasMovementEvidenceFollowupSignal(rawText) ||
|
hasMovementEvidenceFollowupSignal(rawText) ||
|
||||||
hasPronounMovementEvidenceFollowupSignal(rawText);
|
hasPronounMovementEvidenceFollowupSignal(rawText);
|
||||||
const assistantTurnMeaningDateScope = toNonEmptyString(assistantTurnMeaning?.explicit_date_scope);
|
|
||||||
const rawAssistantTurnMeaningOrganizationScope = toNonEmptyString(assistantTurnMeaning?.explicit_organization_scope);
|
|
||||||
const assistantTurnMeaningOrganizationScope = isReferentialOrganizationPlaceholder(
|
|
||||||
rawAssistantTurnMeaningOrganizationScope
|
|
||||||
)
|
|
||||||
? null
|
|
||||||
: rawAssistantTurnMeaningOrganizationScope;
|
|
||||||
const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText);
|
|
||||||
const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText ?? rawEffectiveText ?? rawSignalSourceText);
|
|
||||||
const currentTurnFreshOrganizationScope = predecomposeEntities.organization ?? rawOrganizationScope;
|
|
||||||
const currentTurnOrganizationScope =
|
|
||||||
currentTurnFreshOrganizationScope ?? assistantTurnMeaningOrganizationScope;
|
|
||||||
const followupCounterpartyIsMetadataOrganizationScope = Boolean(
|
const followupCounterpartyIsMetadataOrganizationScope = Boolean(
|
||||||
followupSeed.subjectResolutionOptional &&
|
followupSeed.subjectResolutionOptional &&
|
||||||
followupSeed.counterparty &&
|
followupSeed.counterparty &&
|
||||||
|
|
@ -1792,10 +1976,6 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
!rawBidirectionalValueFlowSignal &&
|
!rawBidirectionalValueFlowSignal &&
|
||||||
explicitOrganizationScopeSignal
|
explicitOrganizationScopeSignal
|
||||||
);
|
);
|
||||||
const predecomposeOrganizationMirrorsCounterparty = sameScopedName(
|
|
||||||
predecomposeEntities.counterparty,
|
|
||||||
predecomposeEntities.organization
|
|
||||||
);
|
|
||||||
const organizationMirrorsPredecomposeCounterparty = Boolean(
|
const organizationMirrorsPredecomposeCounterparty = Boolean(
|
||||||
(rawBidirectionalValueFlowSignal ||
|
(rawBidirectionalValueFlowSignal ||
|
||||||
hasValueRankingSignal(rawText) ||
|
hasValueRankingSignal(rawText) ||
|
||||||
|
|
@ -2130,16 +2310,27 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
const bidirectionalValueFlowSignal =
|
const bidirectionalValueFlowSignal =
|
||||||
!businessOverviewSignal &&
|
!businessOverviewSignal &&
|
||||||
!lifecycleSignal &&
|
!lifecycleSignal &&
|
||||||
(rawBidirectionalValueFlowSignal || seededAction === "net_value_flow");
|
((businessOverviewCounterpartyValueFlowPivot
|
||||||
|
? rawBidirectionalValueFlowPivotTextSignal
|
||||||
|
: rawBidirectionalValueFlowSignal) ||
|
||||||
|
seededAction === "net_value_flow");
|
||||||
const valueFlowSignal =
|
const valueFlowSignal =
|
||||||
!businessOverviewSignal &&
|
!businessOverviewSignal &&
|
||||||
!lifecycleSignal &&
|
!lifecycleSignal &&
|
||||||
!metadataGroundedMovementLaneApplicable &&
|
!metadataGroundedMovementLaneApplicable &&
|
||||||
(rawValueFlowSignal || seededDomain === "counterparty_value");
|
((businessOverviewCounterpartyValueFlowPivot
|
||||||
|
? rawValueFlowPivotTextSignal
|
||||||
|
: rawValueFlowSignal) ||
|
||||||
|
seededDomain === "counterparty_value");
|
||||||
const payoutSignal =
|
const payoutSignal =
|
||||||
valueFlowSignal &&
|
valueFlowSignal &&
|
||||||
!bidirectionalValueFlowSignal &&
|
!bidirectionalValueFlowSignal &&
|
||||||
(rawPayoutSignal || seededAction === "payout");
|
((businessOverviewCounterpartyValueFlowPivot
|
||||||
|
? rawValueFlowPivotTextSignal &&
|
||||||
|
!rawBidirectionalValueFlowPivotTextSignal &&
|
||||||
|
hasPayoutSignal(rawText)
|
||||||
|
: rawPayoutSignal) ||
|
||||||
|
seededAction === "payout");
|
||||||
const semanticDataNeed = metadataAmbiguityLaneClarificationApplicable
|
const semanticDataNeed = metadataAmbiguityLaneClarificationApplicable
|
||||||
? "metadata lane clarification"
|
? "metadata lane clarification"
|
||||||
: semanticNeedFor({
|
: semanticNeedFor({
|
||||||
|
|
@ -2147,17 +2338,37 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
? "movements"
|
? "movements"
|
||||||
: businessOverviewSignal
|
: businessOverviewSignal
|
||||||
? "business_overview"
|
? "business_overview"
|
||||||
|
: lifecycleSignal
|
||||||
|
? "counterparty_lifecycle"
|
||||||
|
: valueFlowSignal
|
||||||
|
? "counterparty_value"
|
||||||
: rawDomain ?? seededDomain,
|
: rawDomain ?? seededDomain,
|
||||||
action: explicitVatMovementEvidenceSignal
|
action: explicitVatMovementEvidenceSignal
|
||||||
? "list_movements"
|
? "list_movements"
|
||||||
: businessOverviewSignal
|
: businessOverviewSignal
|
||||||
? businessOverviewActionFamily
|
? businessOverviewActionFamily
|
||||||
|
: lifecycleSignal
|
||||||
|
? "activity_duration"
|
||||||
|
: valueFlowSignal
|
||||||
|
? bidirectionalValueFlowSignal
|
||||||
|
? "net_value_flow"
|
||||||
|
: payoutSignal
|
||||||
|
? "payout"
|
||||||
|
: rawAction ?? seededAction ?? "turnover"
|
||||||
: rawAction ?? seededAction,
|
: rawAction ?? seededAction,
|
||||||
unsupported: explicitVatMovementEvidenceSignal
|
unsupported: explicitVatMovementEvidenceSignal
|
||||||
? "movement_evidence"
|
? "movement_evidence"
|
||||||
: businessOverviewSignal
|
: businessOverviewSignal
|
||||||
? businessOverviewUnsupportedFamily
|
? businessOverviewUnsupportedFamily
|
||||||
: unsupported ?? seededUnsupported,
|
: lifecycleSignal
|
||||||
|
? "counterparty_lifecycle"
|
||||||
|
: valueFlowSignal
|
||||||
|
? bidirectionalValueFlowSignal
|
||||||
|
? "counterparty_bidirectional_value_flow_or_netting"
|
||||||
|
: payoutSignal
|
||||||
|
? "counterparty_payouts_or_outflow"
|
||||||
|
: seededUnsupported ?? "counterparty_value_or_turnover"
|
||||||
|
: unsupported ?? seededUnsupported,
|
||||||
lifecycleSignal,
|
lifecycleSignal,
|
||||||
valueFlowSignal,
|
valueFlowSignal,
|
||||||
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
|
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
|
||||||
|
|
@ -2322,11 +2533,17 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
const clarificationLoopStillNeedsPeriod = Boolean(
|
const clarificationLoopStillNeedsPeriod = Boolean(
|
||||||
followupSeed.loopStatus === "awaiting_clarification" && followupSeed.loopPendingAxes.includes("period")
|
followupSeed.loopStatus === "awaiting_clarification" && followupSeed.loopPendingAxes.includes("period")
|
||||||
);
|
);
|
||||||
|
const rawTwoDigitPredecomposeYearHint = Boolean(
|
||||||
|
predecomposeDateScope &&
|
||||||
|
/^\d{4}$/.test(predecomposeDateScope) &&
|
||||||
|
/\b\d{2}\b/u.test(rawText)
|
||||||
|
);
|
||||||
const businessOverviewRawWithoutDateScope = Boolean(
|
const businessOverviewRawWithoutDateScope = Boolean(
|
||||||
businessOverviewSignal &&
|
businessOverviewSignal &&
|
||||||
!rawAllTimeScopeSignal &&
|
!rawAllTimeScopeSignal &&
|
||||||
!explicitDateScopeLiteralDetected &&
|
!explicitDateScopeLiteralDetected &&
|
||||||
!rawDateScope &&
|
!rawDateScope &&
|
||||||
|
!rawTwoDigitPredecomposeYearHint &&
|
||||||
!relativeCurrentDateHintDetected
|
!relativeCurrentDateHintDetected
|
||||||
);
|
);
|
||||||
const predecomposeDateScopeCountsAsCurrentTurnPeriod = Boolean(
|
const predecomposeDateScopeCountsAsCurrentTurnPeriod = Boolean(
|
||||||
|
|
@ -2352,6 +2569,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
const suppressNegatedTaxOnlyDateScope = Boolean(businessOverviewSignal && negatedTaxDateScopeOnlySignal);
|
const suppressNegatedTaxOnlyDateScope = Boolean(businessOverviewSignal && negatedTaxDateScopeOnlySignal);
|
||||||
const topicSwitchSuppressesFollowupScope = Boolean(
|
const topicSwitchSuppressesFollowupScope = Boolean(
|
||||||
rawTopicSwitchSignal &&
|
rawTopicSwitchSignal &&
|
||||||
|
!compactCashflowDisplayFollowupSignal &&
|
||||||
(rawMetadataSignal ||
|
(rawMetadataSignal ||
|
||||||
businessOverviewSignal ||
|
businessOverviewSignal ||
|
||||||
rawEntitySearchOverridesStaleScope ||
|
rawEntitySearchOverridesStaleScope ||
|
||||||
|
|
@ -2469,30 +2687,30 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
unsupported_but_understood_family:
|
unsupported_but_understood_family:
|
||||||
businessOverviewSignal
|
businessOverviewSignal
|
||||||
? businessOverviewUnsupportedFamily
|
? businessOverviewUnsupportedFamily
|
||||||
: unsupported ??
|
: lifecycleSignal
|
||||||
(lifecycleSignal
|
? "counterparty_lifecycle"
|
||||||
? "counterparty_lifecycle"
|
|
||||||
: valueFlowSignal
|
: valueFlowSignal
|
||||||
? bidirectionalValueFlowSignal
|
? bidirectionalValueFlowSignal
|
||||||
? "counterparty_bidirectional_value_flow_or_netting"
|
? "counterparty_bidirectional_value_flow_or_netting"
|
||||||
: payoutSignal
|
: payoutSignal
|
||||||
? "counterparty_payouts_or_outflow"
|
? "counterparty_payouts_or_outflow"
|
||||||
: seededUnsupported ?? "counterparty_value_or_turnover"
|
: seededUnsupported ?? "counterparty_value_or_turnover"
|
||||||
: metadataGroundedMovementLaneApplicable
|
: unsupported ??
|
||||||
? "movement_evidence"
|
(metadataGroundedMovementLaneApplicable
|
||||||
: metadataGroundedDocumentLaneApplicable
|
? "movement_evidence"
|
||||||
? "document_evidence"
|
: metadataGroundedDocumentLaneApplicable
|
||||||
: explicitVatMovementEvidenceSignal
|
? "document_evidence"
|
||||||
? "movement_evidence"
|
: explicitVatMovementEvidenceSignal
|
||||||
: metadataAmbiguityLaneClarificationApplicable
|
? "movement_evidence"
|
||||||
? "metadata_lane_choice_clarification"
|
: metadataAmbiguityLaneClarificationApplicable
|
||||||
: entityResolutionSignal
|
? "metadata_lane_choice_clarification"
|
||||||
? "entity_resolution"
|
: entityResolutionSignal
|
||||||
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
? "entity_resolution"
|
||||||
? "1c_metadata_surface"
|
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
||||||
: followupDiscoverySeedApplicable
|
? "1c_metadata_surface"
|
||||||
? seededUnsupported
|
: followupDiscoverySeedApplicable
|
||||||
: null),
|
? seededUnsupported
|
||||||
|
: null),
|
||||||
stale_replay_forbidden: Boolean(
|
stale_replay_forbidden: Boolean(
|
||||||
assistantTurnMeaning?.stale_replay_forbidden ||
|
assistantTurnMeaning?.stale_replay_forbidden ||
|
||||||
businessOverviewSignal ||
|
businessOverviewSignal ||
|
||||||
|
|
@ -2763,6 +2981,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
if (businessOverviewSignal) {
|
if (businessOverviewSignal) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_broad_business_evaluation_route_candidate");
|
pushReason(reasonCodes, "mcp_discovery_broad_business_evaluation_route_candidate");
|
||||||
}
|
}
|
||||||
|
if (businessOverviewCounterpartyValueFlowPivot) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_business_overview_followup_pivoted_to_counterparty_value_flow");
|
||||||
|
}
|
||||||
if (businessOverviewContinuationSignal) {
|
if (businessOverviewContinuationSignal) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_from_followup_context");
|
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_from_followup_context");
|
||||||
}
|
}
|
||||||
|
|
@ -2787,6 +3008,15 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
if (businessOverviewRawYearOverridesPredecomposeAsOf) {
|
if (businessOverviewRawYearOverridesPredecomposeAsOf) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_business_overview_raw_year_overrode_predecompose_as_of_scope");
|
pushReason(reasonCodes, "mcp_discovery_business_overview_raw_year_overrode_predecompose_as_of_scope");
|
||||||
}
|
}
|
||||||
|
if (organizationSelectionFromKnown) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_organization_scope_from_known_organizations");
|
||||||
|
}
|
||||||
|
if (semanticOrganizationScope) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_organization_scope_from_semantic_text");
|
||||||
|
}
|
||||||
|
if (predecomposeCounterpartyAsOrganizationScope) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_counterparty_reinterpreted_as_organization_scope");
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
!(valueFlowOrganizationStaysScope && normalizedPredecomposeCounterparty === explicitOrganizationScope) &&
|
!(valueFlowOrganizationStaysScope && normalizedPredecomposeCounterparty === explicitOrganizationScope) &&
|
||||||
normalizedPredecomposeCounterparty
|
normalizedPredecomposeCounterparty
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([
|
||||||
"inventory_purchase_documents_for_item",
|
"inventory_purchase_documents_for_item",
|
||||||
"inventory_supplier_stock_overlap_as_of_date",
|
"inventory_supplier_stock_overlap_as_of_date",
|
||||||
"inventory_sale_trace_for_item",
|
"inventory_sale_trace_for_item",
|
||||||
|
"inventory_margin_ranking_for_nomenclature",
|
||||||
"inventory_profitability_for_item",
|
"inventory_profitability_for_item",
|
||||||
"inventory_purchase_to_sale_chain",
|
"inventory_purchase_to_sale_chain",
|
||||||
"inventory_aging_by_purchase_date",
|
"inventory_aging_by_purchase_date",
|
||||||
|
|
@ -41,6 +42,7 @@ const ADDRESS_INTENTS_ALLOW_STRICT_DEEP_INVESTIGATION_BYPASS = new Set([
|
||||||
"inventory_purchase_provenance_for_item",
|
"inventory_purchase_provenance_for_item",
|
||||||
"inventory_purchase_documents_for_item",
|
"inventory_purchase_documents_for_item",
|
||||||
"inventory_sale_trace_for_item",
|
"inventory_sale_trace_for_item",
|
||||||
|
"inventory_margin_ranking_for_nomenclature",
|
||||||
"inventory_profitability_for_item",
|
"inventory_profitability_for_item",
|
||||||
"inventory_purchase_to_sale_chain"
|
"inventory_purchase_to_sale_chain"
|
||||||
]);
|
]);
|
||||||
|
|
@ -286,7 +288,7 @@ export function createAssistantRoutePolicy(deps) {
|
||||||
: null;
|
: null;
|
||||||
const semanticCanonicalRecommended = semanticExtractionContract?.apply_canonical_recommended !== false;
|
const semanticCanonicalRecommended = semanticExtractionContract?.apply_canonical_recommended !== false;
|
||||||
const llmSupportedDeepAddressIntentSignal = llmContractMode === "deep_analysis" &&
|
const llmSupportedDeepAddressIntentSignal = llmContractMode === "deep_analysis" &&
|
||||||
/^(?:inventory_purchase_provenance_for_item|inventory_purchase_documents_for_item|inventory_sale_trace_for_item|inventory_profitability_for_item|inventory_purchase_to_sale_chain)$/u.test(llmContractIntent ?? "") &&
|
/^(?:inventory_purchase_provenance_for_item|inventory_purchase_documents_for_item|inventory_sale_trace_for_item|inventory_margin_ranking_for_nomenclature|inventory_profitability_for_item|inventory_purchase_to_sale_chain)$/u.test(llmContractIntent ?? "") &&
|
||||||
semanticCanonicalRecommended;
|
semanticCanonicalRecommended;
|
||||||
const llmCanonicalEntitySignal = /(?:заказчик|поставщик|контрагент|компан|customer|supplier|counterparty|company|vendor|client)/iu.test(compactWhitespace(repairedInputMessage.toLowerCase()));
|
const llmCanonicalEntitySignal = /(?:заказчик|поставщик|контрагент|компан|customer|supplier|counterparty|company|vendor|client)/iu.test(compactWhitespace(repairedInputMessage.toLowerCase()));
|
||||||
const llmCanonicalAppliedSignal = Boolean(llmPreDecomposeMeta?.applied) && llmContractMode !== "deep_analysis";
|
const llmCanonicalAppliedSignal = Boolean(llmPreDecomposeMeta?.applied) && llmContractMode !== "deep_analysis";
|
||||||
|
|
@ -395,6 +397,24 @@ export function createAssistantRoutePolicy(deps) {
|
||||||
const hasTemporalCue = /(?:на\s+эту\s+же\s+дат[ауеы]|на\s+тот\s+же\s+период|за\s+этот\s+же\s+период|за\s+этот\s+период|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр|\b(?:19|20)\d{2}\b)/iu.test(normalized);
|
const hasTemporalCue = /(?:на\s+эту\s+же\s+дат[ауеы]|на\s+тот\s+же\s+период|за\s+этот\s+же\s+период|за\s+этот\s+период|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр|\b(?:19|20)\d{2}\b)/iu.test(normalized);
|
||||||
return hasRequestCue && hasTemporalCue;
|
return hasRequestCue && hasTemporalCue;
|
||||||
}
|
}
|
||||||
|
function hasInventoryMarginRankingContinuationSignal(text) {
|
||||||
|
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase()).replace(/ё/g, "е");
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const wantsFoundRows =
|
||||||
|
/(?:покажи|показать|выведи|дай|раскрой|show|list)/iu.test(normalized) &&
|
||||||
|
/(?:найденн|строк|реализац|себестоимостн|баз)/iu.test(normalized) &&
|
||||||
|
/(?:себестоимостн|реализац|марж|прибыл|номенклатур)/iu.test(normalized);
|
||||||
|
const account41Not01 =
|
||||||
|
/\b41(?:[.,]\d{1,2})?\b/iu.test(normalized) &&
|
||||||
|
/\b01(?:[.,]\d{1,2})?\b/iu.test(normalized) &&
|
||||||
|
/(?:\bне\b|вместо|а\s+не|not|instead)/iu.test(normalized);
|
||||||
|
const periodExpansion =
|
||||||
|
/(?:расширь|расширить|возьми|давай|покажи|за|на|до|весь|год|квартал|месяц|expand|period)/iu.test(normalized) &&
|
||||||
|
/(?:январ|феврал|март|апрел|ма[йяе]|июн|июл|август|сентябр|октябр|ноябр|декабр|\b(?:19|20)\d{2}\b)/iu.test(normalized);
|
||||||
|
return wantsFoundRows || account41Not01 || periodExpansion;
|
||||||
|
}
|
||||||
function hasOrganizationClarificationTextCue(text) {
|
function hasOrganizationClarificationTextCue(text) {
|
||||||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
|
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
|
|
@ -563,6 +583,7 @@ export function createAssistantRoutePolicy(deps) {
|
||||||
hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) ||
|
hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) ||
|
||||||
hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage);
|
hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage);
|
||||||
const followupPreviousIntent = toNonEmptyString(followupContext?.previous_intent);
|
const followupPreviousIntent = toNonEmptyString(followupContext?.previous_intent);
|
||||||
|
const followupRootIntent = toNonEmptyString(followupContext?.root_intent);
|
||||||
const followupPreviousFilters = followupContext?.previous_filters && typeof followupContext.previous_filters === "object"
|
const followupPreviousFilters = followupContext?.previous_filters && typeof followupContext.previous_filters === "object"
|
||||||
? followupContext.previous_filters
|
? followupContext.previous_filters
|
||||||
: null;
|
: null;
|
||||||
|
|
@ -597,6 +618,16 @@ export function createAssistantRoutePolicy(deps) {
|
||||||
hasShortInventoryObjectFollowupSignal(repairedRawUserMessage) ||
|
hasShortInventoryObjectFollowupSignal(repairedRawUserMessage) ||
|
||||||
hasShortInventoryObjectFollowupSignal(effectiveAddressUserMessage) ||
|
hasShortInventoryObjectFollowupSignal(effectiveAddressUserMessage) ||
|
||||||
hasShortInventoryObjectFollowupSignal(repairedEffectiveAddressUserMessage)));
|
hasShortInventoryObjectFollowupSignal(repairedEffectiveAddressUserMessage)));
|
||||||
|
const protectedInventoryMarginRankingFollowup = Boolean(followupContext &&
|
||||||
|
(followupPreviousIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
|
followupRootIntent === "inventory_margin_ranking_for_nomenclature") &&
|
||||||
|
!dataScopeMetaQuery &&
|
||||||
|
!capabilityMetaQuery &&
|
||||||
|
!dangerOrCoercionSignal &&
|
||||||
|
(hasInventoryMarginRankingContinuationSignal(rawUserMessage) ||
|
||||||
|
hasInventoryMarginRankingContinuationSignal(repairedRawUserMessage) ||
|
||||||
|
hasInventoryMarginRankingContinuationSignal(effectiveAddressUserMessage) ||
|
||||||
|
hasInventoryMarginRankingContinuationSignal(repairedEffectiveAddressUserMessage)));
|
||||||
const organizationClarificationContinuationDetected = Boolean((followupContext || continuitySnapshot.hasGroundedAddressContext) &&
|
const organizationClarificationContinuationDetected = Boolean((followupContext || continuitySnapshot.hasGroundedAddressContext) &&
|
||||||
lastOrganizationClarificationDebug &&
|
lastOrganizationClarificationDebug &&
|
||||||
explicitOrganizationClarificationSelection &&
|
explicitOrganizationClarificationSelection &&
|
||||||
|
|
@ -654,6 +685,7 @@ export function createAssistantRoutePolicy(deps) {
|
||||||
!baseToolGatePreservesAddressLane &&
|
!baseToolGatePreservesAddressLane &&
|
||||||
!effectiveGroundedValueFlowFollowupContextDetected &&
|
!effectiveGroundedValueFlowFollowupContextDetected &&
|
||||||
!protectedInventoryShortFollowup &&
|
!protectedInventoryShortFollowup &&
|
||||||
|
!protectedInventoryMarginRankingFollowup &&
|
||||||
!organizationClarificationContinuationDetected &&
|
!organizationClarificationContinuationDetected &&
|
||||||
!routeCandidateOrganizationClarificationDetected);
|
!routeCandidateOrganizationClarificationDetected);
|
||||||
const lastAddressAssistantDebug = sessionItems
|
const lastAddressAssistantDebug = sessionItems
|
||||||
|
|
@ -1165,6 +1197,7 @@ export function createAssistantRoutePolicy(deps) {
|
||||||
hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) ||
|
hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) ||
|
||||||
hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) ||
|
hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) ||
|
||||||
hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage) ||
|
hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage) ||
|
||||||
|
protectedInventoryMarginRankingFollowup ||
|
||||||
inventoryRootRestatementFollowupDetected);
|
inventoryRootRestatementFollowupDetected);
|
||||||
const deepAnalysisPreferenceDetected = Boolean(hasDeepAnalysisPreferenceSignal(rawUserMessage) ||
|
const deepAnalysisPreferenceDetected = Boolean(hasDeepAnalysisPreferenceSignal(rawUserMessage) ||
|
||||||
hasDeepAnalysisPreferenceSignal(repairedRawUserMessage) ||
|
hasDeepAnalysisPreferenceSignal(repairedRawUserMessage) ||
|
||||||
|
|
@ -1202,7 +1235,9 @@ export function createAssistantRoutePolicy(deps) {
|
||||||
(!llmContractIntent || llmContractIntent === "unknown"));
|
(!llmContractIntent || llmContractIntent === "unknown"));
|
||||||
const exactAddressIntentProtectedFromSemanticDeepHint = laneProtectionArbitration.exactAddressIntentProtectedFromSemanticDeepHint;
|
const exactAddressIntentProtectedFromSemanticDeepHint = laneProtectionArbitration.exactAddressIntentProtectedFromSemanticDeepHint;
|
||||||
const protectAddressLaneFromFallback = Boolean(
|
const protectAddressLaneFromFallback = Boolean(
|
||||||
laneProtectionArbitration.protectAddressLaneFromFallback || customerValueRankingAddressSignal
|
laneProtectionArbitration.protectAddressLaneFromFallback ||
|
||||||
|
customerValueRankingAddressSignal ||
|
||||||
|
protectedInventoryMarginRankingFollowup
|
||||||
);
|
);
|
||||||
const vatExplainFollowupSignal = Boolean(followupContext &&
|
const vatExplainFollowupSignal = Boolean(followupContext &&
|
||||||
toNonEmptyString(followupContext.previous_intent) === "vat_payable_forecast" &&
|
toNonEmptyString(followupContext.previous_intent) === "vat_payable_forecast" &&
|
||||||
|
|
@ -1283,7 +1318,7 @@ export function createAssistantRoutePolicy(deps) {
|
||||||
let toolGateDecision = String(baseToolGate?.decision ?? "skip_address_lane");
|
let toolGateDecision = String(baseToolGate?.decision ?? "skip_address_lane");
|
||||||
let toolGateReason = String(baseToolGate?.reason ?? "no_address_signal_after_l0");
|
let toolGateReason = String(baseToolGate?.reason ?? "no_address_signal_after_l0");
|
||||||
const semanticAddressLaneRecovery = Boolean(!runAddressLane &&
|
const semanticAddressLaneRecovery = Boolean(!runAddressLane &&
|
||||||
supportedAddressRouteCandidateDetected &&
|
(supportedAddressRouteCandidateDetected || protectedInventoryMarginRankingFollowup) &&
|
||||||
!deepAnalysisPreferenceDetected &&
|
!deepAnalysisPreferenceDetected &&
|
||||||
!unsupportedAddressIntentFallbackToDeep &&
|
!unsupportedAddressIntentFallbackToDeep &&
|
||||||
!deepAnalysisSignalFallbackToDeep &&
|
!deepAnalysisSignalFallbackToDeep &&
|
||||||
|
|
@ -1292,7 +1327,9 @@ export function createAssistantRoutePolicy(deps) {
|
||||||
if (semanticAddressLaneRecovery) {
|
if (semanticAddressLaneRecovery) {
|
||||||
runAddressLane = true;
|
runAddressLane = true;
|
||||||
toolGateDecision = "run_address_lane";
|
toolGateDecision = "run_address_lane";
|
||||||
toolGateReason = resolvedIntentResolution.intent !== "unknown" || llmContractIntent
|
toolGateReason = protectedInventoryMarginRankingFollowup
|
||||||
|
? "followup_context_detected"
|
||||||
|
: resolvedIntentResolution.intent !== "unknown" || llmContractIntent
|
||||||
? "address_intent_resolver_detected"
|
? "address_intent_resolver_detected"
|
||||||
: "address_signal_detected";
|
: "address_signal_detected";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -337,6 +337,18 @@ export const INVENTORY_CAPABILITY_CONTRACTS: readonly AssistantCapabilityContrac
|
||||||
answerObjectShape: "inventory_profitability_bundle",
|
answerObjectShape: "inventory_profitability_bundle",
|
||||||
bundleReusePolicy: "sale_trace_bundle_preferred"
|
bundleReusePolicy: "sale_trace_bundle_preferred"
|
||||||
}),
|
}),
|
||||||
|
inventoryExactCapability({
|
||||||
|
capability_id: "inventory_inventory_margin_ranking_for_nomenclature",
|
||||||
|
intent_ids: ["inventory_margin_ranking_for_nomenclature"],
|
||||||
|
entry_modes: ["root_entry", "root_followup", "clarification_resume"],
|
||||||
|
transitions: ["T1", "T2", "T7"],
|
||||||
|
requiresFocusObject: false,
|
||||||
|
requiredAnchors: ["period"],
|
||||||
|
resultShape: "nomenclature_margin_ranking",
|
||||||
|
answerObjectShape: "inventory_margin_ranking",
|
||||||
|
bundleReusePolicy: "none",
|
||||||
|
scenarioFamilies: ["canonical", "colloquial", "account_41_correction"]
|
||||||
|
}),
|
||||||
inventoryExactCapability({
|
inventoryExactCapability({
|
||||||
capability_id: "inventory_inventory_purchase_to_sale_chain",
|
capability_id: "inventory_inventory_purchase_to_sale_chain",
|
||||||
intent_ids: ["inventory_purchase_to_sale_chain"],
|
intent_ids: ["inventory_purchase_to_sale_chain"],
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,31 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
return deps.compactWhitespace(deps.repairAddressMojibake(String(value ?? "")).toLowerCase()).replace(/ё/g, "е");
|
return deps.compactWhitespace(deps.repairAddressMojibake(String(value ?? "")).toLowerCase()).replace(/ё/g, "е");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasInventoryMarginRankingFollowupSignal(userMessage, alternateMessage = null, sourceIntentHint = null) {
|
||||||
|
if (sourceIntentHint !== "inventory_margin_ranking_for_nomenclature") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return [userMessage, alternateMessage]
|
||||||
|
.filter((value) => deps.toNonEmptyString(value))
|
||||||
|
.map((value) =>
|
||||||
|
deps.compactWhitespace(deps.repairAddressMojibake(String(value ?? "")).toLowerCase()).replace(/ё/g, "е")
|
||||||
|
)
|
||||||
|
.some((normalized) => {
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const wantsFoundRows =
|
||||||
|
/(?:покажи|показать|выведи|дай|раскрой|show|list|покажи|показать|выведи|дай|раскрой)/iu.test(normalized) &&
|
||||||
|
/(?:найденн|строк|реализац|себестоимостн|баз|найденн|строк|реализац|себестоимостн|баз)/iu.test(normalized) &&
|
||||||
|
/(?:себестоимостн|реализац|марж|прибыл|номенклатур|себестоимостн|реализац|марж|прибыл|номенклат)/iu.test(normalized);
|
||||||
|
const account41Not01 =
|
||||||
|
/\b41(?:[.,]\d{1,2})?\b/iu.test(normalized) &&
|
||||||
|
/\b01(?:[.,]\d{1,2})?\b/iu.test(normalized) &&
|
||||||
|
/(?:\bне\b|вместо|а\s+не|not|instead|РЅРµ|вместо|Р°\s+РЅРµ)/iu.test(normalized);
|
||||||
|
return wantsFoundRows || account41Not01;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function hasSamePeriodReferenceCue(...values) {
|
function hasSamePeriodReferenceCue(...values) {
|
||||||
return values
|
return values
|
||||||
.map((value) => normalizeFollowupText(value))
|
.map((value) => normalizeFollowupText(value))
|
||||||
|
|
@ -640,9 +665,26 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
llmPreDecomposeMeta
|
llmPreDecomposeMeta
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
const hasCompactCashflowFollowupCue = (value) => {
|
||||||
|
const normalized = normalizeFollowupText(value);
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasCompactCue = /(?:\u043a\u043e\u0440\u043e\u0442\p{L}*|\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u0431\u0435\u0437\s+\u0442\u043e\u043f\p{L}*)/iu.test(normalized);
|
||||||
|
const hasCashflowCue = /(?:\u0434\u0435\u043d\p{L}*|\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u043f\u043e\u043b\u0443\u0447|\u0437\u0430\u043f\u043b\u0430\u0442|\u043f\u0440\u0438\u0448\p{L}*|\u0443\u0448\p{L}*|\u043d\u0435\u0442\u0442\u043e)/iu.test(normalized);
|
||||||
|
return hasCompactCue && hasCashflowCue;
|
||||||
|
};
|
||||||
|
const compactCashflowFollowupSignal =
|
||||||
|
hasShortValueFlowRetargetCue(userMessage) ||
|
||||||
|
hasCompactCashflowFollowupCue(userMessage) ||
|
||||||
|
(deps.toNonEmptyString(alternateMessage)
|
||||||
|
? hasShortValueFlowRetargetCue(String(alternateMessage ?? "")) ||
|
||||||
|
hasCompactCashflowFollowupCue(String(alternateMessage ?? ""))
|
||||||
|
: false);
|
||||||
if (
|
if (
|
||||||
assistantTurnMeaning?.stale_replay_forbidden === true &&
|
assistantTurnMeaning?.stale_replay_forbidden === true &&
|
||||||
!hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage)
|
!hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage) &&
|
||||||
|
!compactCashflowFollowupSignal
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -738,7 +780,8 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
sourceIntentHint === "customer_revenue_and_payments" ||
|
sourceIntentHint === "customer_revenue_and_payments" ||
|
||||||
sourceDiscoveryPilotScopeHint === "counterparty_value_flow_query_movements_v1" ||
|
sourceDiscoveryPilotScopeHint === "counterparty_value_flow_query_movements_v1" ||
|
||||||
sourceDiscoveryPilotScopeHint === "counterparty_supplier_payout_query_movements_v1" ||
|
sourceDiscoveryPilotScopeHint === "counterparty_supplier_payout_query_movements_v1" ||
|
||||||
sourceDiscoveryPilotScopeHint === "counterparty_bidirectional_value_flow_query_movements_v1";
|
sourceDiscoveryPilotScopeHint === "counterparty_bidirectional_value_flow_query_movements_v1" ||
|
||||||
|
sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
|
||||||
const hasBusinessOverviewCarryoverSourceHint =
|
const hasBusinessOverviewCarryoverSourceHint =
|
||||||
sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
|
sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
|
||||||
const navigationSessionState = resolveNavigationSessionContextState(
|
const navigationSessionState = resolveNavigationSessionContextState(
|
||||||
|
|
@ -760,6 +803,11 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
sourceIntentHint,
|
sourceIntentHint,
|
||||||
hasNavigationInventoryItemFocusHint
|
hasNavigationInventoryItemFocusHint
|
||||||
);
|
);
|
||||||
|
const inventoryMarginRankingFollowup = hasInventoryMarginRankingFollowupSignal(
|
||||||
|
userMessage,
|
||||||
|
alternateMessage,
|
||||||
|
sourceIntentHint
|
||||||
|
);
|
||||||
let inventoryShortFollowupPrimary =
|
let inventoryShortFollowupPrimary =
|
||||||
(deps.isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
|
(deps.isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
|
||||||
deps.hasShortInventoryObjectFollowupSignal(userMessage);
|
deps.hasShortInventoryObjectFollowupSignal(userMessage);
|
||||||
|
|
@ -777,10 +825,12 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
: null;
|
: null;
|
||||||
const debtRoleSwapIntent = debtRoleSwapPrimary ?? debtRoleSwapAlternate ?? null;
|
const debtRoleSwapIntent = debtRoleSwapPrimary ?? debtRoleSwapAlternate ?? null;
|
||||||
const shortValueFlowRetargetPrimary =
|
const shortValueFlowRetargetPrimary =
|
||||||
hasValueFlowCarryoverSourceHint && hasShortValueFlowRetargetCue(userMessage);
|
hasValueFlowCarryoverSourceHint &&
|
||||||
|
(hasShortValueFlowRetargetCue(userMessage) || hasCompactCashflowFollowupCue(userMessage));
|
||||||
const shortValueFlowRetargetAlternate =
|
const shortValueFlowRetargetAlternate =
|
||||||
hasValueFlowCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
|
hasValueFlowCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
|
||||||
? hasShortValueFlowRetargetCue(String(alternateMessage ?? ""))
|
? hasShortValueFlowRetargetCue(String(alternateMessage ?? "")) ||
|
||||||
|
hasCompactCashflowFollowupCue(String(alternateMessage ?? ""))
|
||||||
: false;
|
: false;
|
||||||
const businessOverviewBoundaryFollowupPrimary =
|
const businessOverviewBoundaryFollowupPrimary =
|
||||||
hasBusinessOverviewCarryoverSourceHint && hasBusinessOverviewBoundaryFollowupCue(userMessage);
|
hasBusinessOverviewCarryoverSourceHint && hasBusinessOverviewBoundaryFollowupCue(userMessage);
|
||||||
|
|
@ -796,6 +846,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
businessOverviewBoundaryFollowupPrimary ||
|
businessOverviewBoundaryFollowupPrimary ||
|
||||||
inventoryShortFollowupPrimary ||
|
inventoryShortFollowupPrimary ||
|
||||||
inventoryPurchaseDateVatBridge ||
|
inventoryPurchaseDateVatBridge ||
|
||||||
|
inventoryMarginRankingFollowup ||
|
||||||
explicitSummaryBundleReuseSignal ||
|
explicitSummaryBundleReuseSignal ||
|
||||||
mcpDiscoveryOrganizationClarificationContinuation;
|
mcpDiscoveryOrganizationClarificationContinuation;
|
||||||
let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||||
|
|
@ -805,6 +856,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
businessOverviewBoundaryFollowupAlternate ||
|
businessOverviewBoundaryFollowupAlternate ||
|
||||||
inventoryShortFollowupAlternate ||
|
inventoryShortFollowupAlternate ||
|
||||||
inventoryPurchaseDateVatBridge ||
|
inventoryPurchaseDateVatBridge ||
|
||||||
|
inventoryMarginRankingFollowup ||
|
||||||
explicitSummaryBundleReuseSignal ||
|
explicitSummaryBundleReuseSignal ||
|
||||||
mcpDiscoveryOrganizationClarificationContinuation
|
mcpDiscoveryOrganizationClarificationContinuation
|
||||||
: false;
|
: false;
|
||||||
|
|
@ -862,6 +914,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
shortValueFlowRetargetAlternate ||
|
shortValueFlowRetargetAlternate ||
|
||||||
businessOverviewBoundaryFollowupPrimary ||
|
businessOverviewBoundaryFollowupPrimary ||
|
||||||
businessOverviewBoundaryFollowupAlternate ||
|
businessOverviewBoundaryFollowupAlternate ||
|
||||||
|
inventoryMarginRankingFollowup ||
|
||||||
deps.hasFollowupMarker(userMessage) ||
|
deps.hasFollowupMarker(userMessage) ||
|
||||||
deps.hasReferentialPointer(userMessage) ||
|
deps.hasReferentialPointer(userMessage) ||
|
||||||
(deps.toNonEmptyString(alternateMessage)
|
(deps.toNonEmptyString(alternateMessage)
|
||||||
|
|
@ -886,6 +939,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
shortValueFlowRetargetAlternate ||
|
shortValueFlowRetargetAlternate ||
|
||||||
businessOverviewBoundaryFollowupPrimary ||
|
businessOverviewBoundaryFollowupPrimary ||
|
||||||
businessOverviewBoundaryFollowupAlternate ||
|
businessOverviewBoundaryFollowupAlternate ||
|
||||||
|
inventoryMarginRankingFollowup ||
|
||||||
deps.hasFollowupMarker(userMessage) ||
|
deps.hasFollowupMarker(userMessage) ||
|
||||||
deps.hasReferentialPointer(userMessage) ||
|
deps.hasReferentialPointer(userMessage) ||
|
||||||
(deps.toNonEmptyString(alternateMessage)
|
(deps.toNonEmptyString(alternateMessage)
|
||||||
|
|
@ -1054,8 +1108,9 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
!inventoryShortFollowupPrimary &&
|
!inventoryShortFollowupPrimary &&
|
||||||
!inventoryShortFollowupAlternate &&
|
!inventoryShortFollowupAlternate &&
|
||||||
!businessOverviewBoundaryFollowupPrimary &&
|
!businessOverviewBoundaryFollowupPrimary &&
|
||||||
!businessOverviewBoundaryFollowupAlternate &&
|
!businessOverviewBoundaryFollowupAlternate &&
|
||||||
!foreignAccountingPivotOverInventory &&
|
!inventoryMarginRankingFollowup &&
|
||||||
|
!foreignAccountingPivotOverInventory &&
|
||||||
!deps.hasFollowupMarker(userMessage) &&
|
!deps.hasFollowupMarker(userMessage) &&
|
||||||
!deps.hasReferentialPointer(userMessage) &&
|
!deps.hasReferentialPointer(userMessage) &&
|
||||||
(!deps.toNonEmptyString(alternateMessage)
|
(!deps.toNonEmptyString(alternateMessage)
|
||||||
|
|
@ -1118,6 +1173,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
businessOverviewBoundaryFollowupPrimary ||
|
businessOverviewBoundaryFollowupPrimary ||
|
||||||
inventoryShortFollowupPrimary ||
|
inventoryShortFollowupPrimary ||
|
||||||
inventoryPurchaseDateVatBridge ||
|
inventoryPurchaseDateVatBridge ||
|
||||||
|
inventoryMarginRankingFollowup ||
|
||||||
explicitSummaryBundleReuseSignal ||
|
explicitSummaryBundleReuseSignal ||
|
||||||
hasInventoryRootTemporalFollowupPrimary ||
|
hasInventoryRootTemporalFollowupPrimary ||
|
||||||
mcpDiscoveryOrganizationClarificationContinuation;
|
mcpDiscoveryOrganizationClarificationContinuation;
|
||||||
|
|
@ -1129,6 +1185,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
businessOverviewBoundaryFollowupAlternate ||
|
businessOverviewBoundaryFollowupAlternate ||
|
||||||
inventoryShortFollowupAlternate ||
|
inventoryShortFollowupAlternate ||
|
||||||
inventoryPurchaseDateVatBridge ||
|
inventoryPurchaseDateVatBridge ||
|
||||||
|
inventoryMarginRankingFollowup ||
|
||||||
explicitSummaryBundleReuseSignal ||
|
explicitSummaryBundleReuseSignal ||
|
||||||
hasInventoryRootTemporalFollowupAlternate ||
|
hasInventoryRootTemporalFollowupAlternate ||
|
||||||
mcpDiscoveryOrganizationClarificationContinuation
|
mcpDiscoveryOrganizationClarificationContinuation
|
||||||
|
|
@ -1151,6 +1208,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
shortValueFlowRetargetAlternate ||
|
shortValueFlowRetargetAlternate ||
|
||||||
businessOverviewBoundaryFollowupPrimary ||
|
businessOverviewBoundaryFollowupPrimary ||
|
||||||
businessOverviewBoundaryFollowupAlternate ||
|
businessOverviewBoundaryFollowupAlternate ||
|
||||||
|
inventoryMarginRankingFollowup ||
|
||||||
deps.hasFollowupMarker(userMessage) ||
|
deps.hasFollowupMarker(userMessage) ||
|
||||||
deps.hasReferentialPointer(userMessage) ||
|
deps.hasReferentialPointer(userMessage) ||
|
||||||
(deps.toNonEmptyString(alternateMessage)
|
(deps.toNonEmptyString(alternateMessage)
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ const SUPPORTED_ADDRESS_INTENTS = new Set([
|
||||||
"inventory_purchase_documents_for_item",
|
"inventory_purchase_documents_for_item",
|
||||||
"inventory_supplier_stock_overlap_as_of_date",
|
"inventory_supplier_stock_overlap_as_of_date",
|
||||||
"inventory_sale_trace_for_item",
|
"inventory_sale_trace_for_item",
|
||||||
|
"inventory_margin_ranking_for_nomenclature",
|
||||||
"inventory_profitability_for_item",
|
"inventory_profitability_for_item",
|
||||||
"inventory_purchase_to_sale_chain",
|
"inventory_purchase_to_sale_chain",
|
||||||
"inventory_aging_by_purchase_date",
|
"inventory_aging_by_purchase_date",
|
||||||
|
|
@ -138,6 +139,90 @@ function detectCounterpartyTurnoverFamily(text) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasCompactOrganizationCashflowDisplaySignal(text) {
|
||||||
|
const normalized = String(text ?? "");
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasCompactCue =
|
||||||
|
/(?:\u043a\u043e\u0440\u043e\u0442\u043a\w*|\u043e\u0434\u043d\u043e\u0439\s+\u0441\u0442\u0440\u043e\u043a\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0438\u0442\u043e\u0433\p{L}*|\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}*|\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?\b|\u0431\u0435\u0437\s+\u0441\u043f\u0438\u0441(?:\u043a\p{L}*)?|\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0431\u0438\u0432\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0440\u0435\u0437\p{L}*|\u0431\u0435\u0437\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\p{L}*)/iu.test(
|
||||||
|
normalized
|
||||||
|
);
|
||||||
|
if (!hasCompactCue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasMoneyCue =
|
||||||
|
/(?:\u0434\u0435\u043d\p{L}*|\u0434\u0435\u043d\u0435\u0436\p{L}*|\u043f\u0440\u0438\u0448\p{L}*|\u0443\u0448\p{L}*|\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*|\u0441\u043f\u0438\u0441\u0430\p{L}*|\u043d\u0435\u0442\u0442\u043e|cash|money|incoming|outgoing|net)/iu.test(
|
||||||
|
normalized
|
||||||
|
);
|
||||||
|
const hasOrganizationEarningsCue =
|
||||||
|
/(?:\u0434\u0430\u0439|\u043f\u043e\u043a\u0430\u0436\p{L}*|\u0438\u0442\u043e\u0433\p{L}*|\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|\u0437\u0430\u0440\u0430\u0431\u043e\u0442\p{L}*|\u043f\u0440\u0438\u0448\p{L}*|\u0443\u0448\p{L}*|\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*|\u0434\u0435\u043d\u0435\u0436\p{L}*\s+\u043d\u0435\u0442\u0442\u043e|how\s+much|show|give|earned|received|paid)/iu.test(
|
||||||
|
normalized
|
||||||
|
);
|
||||||
|
const hasExplicitExclusionCue =
|
||||||
|
/(?:\u0438\u0441\u043a\u043b\u044e\u0447\p{L}*|\u0443\u0431\u0435\u0440\p{L}*|\u043a\u0440\u043e\u043c\u0435|exclude|excluding|without)[\s\S]{0,80}(?:\u043a\u0440\u0443\u043f\u043d\p{L}*|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\p{L}*|\u043a\u043b\u0438\u0435\u043d\u0442\p{L}*|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\p{L}*|\u043e\u043f\u0435\u0440\u0430\u0446\p{L}*|counterpart|customer|supplier|operation)/iu.test(
|
||||||
|
normalized
|
||||||
|
);
|
||||||
|
const hasSpecificCounterpartyScope = detectScopedCounterpartyEntity(normalized) !== null;
|
||||||
|
return hasMoneyCue && hasOrganizationEarningsCue && !hasExplicitExclusionCue && !hasSpecificCounterpartyScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectScopedCounterpartyEntity(text) {
|
||||||
|
const patterns = [
|
||||||
|
/(?:^|[\s,.;:!?])(?:\u043f\u043e|\u0443|\u0434\u043b\u044f|by|for)\s+(.+?)(?=$|[,.;:!?]|\s+(?:\u0437\u0430|\u043d\u0430|\u0432|\u0432\u043e|\u043a|\u043f\u043e|\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a|\u043a\u0430\u043a|\u043a\u0430\u043a\u043e\u0435|\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0430\u044f|\u043a\u0430\u043a\u0438\u0435|\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*|\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|\u0434\u0435\u043d\u0435\u0433|\u0434\u0435\u043d\u0435\u0436\p{L}*|\u043f\u043b\u0430\u0442[\u0435\u0451]\u0436\p{L}*|\u0438\u0441\u0445\u043e\u0434\p{L}*|\u0432\u0445\u043e\u0434\p{L}*)(?=$|[\s,.;:!?]))/iu,
|
||||||
|
/(?:^|[\s,.;:!?])(?:\u043f\u043e|\u0443|\u0434\u043b\u044f|by|for)\s+([\p{L}\d._-]{2,})(?=$|[\s,.;:!?])/iu
|
||||||
|
];
|
||||||
|
const ignored = new Set([
|
||||||
|
"\u0433\u043e\u0434",
|
||||||
|
"\u0433\u043e\u0434\u0430",
|
||||||
|
"\u043f\u0435\u0440\u0438\u043e\u0434",
|
||||||
|
"\u043f\u0435\u0440\u0438\u043e\u0434\u0430",
|
||||||
|
"\u043c\u0435\u0441\u044f\u0446",
|
||||||
|
"\u043c\u0435\u0441\u044f\u0446\u0430",
|
||||||
|
"\u043a\u0432\u0430\u0440\u0442\u0430\u043b",
|
||||||
|
"\u043a\u0432\u0430\u0440\u0442\u0430\u043b\u0430",
|
||||||
|
"\u0434\u0435\u043d\u044c\u0433\u0438",
|
||||||
|
"\u043d\u0435\u0442\u0442\u043e",
|
||||||
|
"\u0441\u0430\u043b\u044c\u0434\u043e",
|
||||||
|
"year",
|
||||||
|
"period",
|
||||||
|
"month",
|
||||||
|
"quarter",
|
||||||
|
"net"
|
||||||
|
]);
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const rawEntity = text.match(pattern)?.[1]?.trim() ?? "";
|
||||||
|
if (!rawEntity) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const entity = rawEntity.replace(/^["'«»]+|["'«»]+$/gu, "").trim();
|
||||||
|
if (entity.length >= 2 && !ignored.has(entity)) {
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectCounterpartyBidirectionalValueFlowFamily(text) {
|
||||||
|
const hasNetCue =
|
||||||
|
/(?:\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|net\s+(?:flow|cash|payment)|cash\s+net)/iu.test(text);
|
||||||
|
const hasIncomingCue =
|
||||||
|
/(?:\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0432\u0445\u043e\u0434\p{L}*|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*|received|incoming)/iu.test(text);
|
||||||
|
const hasOutgoingCue =
|
||||||
|
/(?:\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*|\u0438\u0441\u0445\u043e\u0434\p{L}*|\u0441\u043f\u0438\u0441\u0430\u043d\p{L}*|paid|outgoing|payment)/iu.test(text);
|
||||||
|
if (!(hasNetCue || (hasIncomingCue && hasOutgoingCue))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const entity = detectScopedCounterpartyEntity(text);
|
||||||
|
if (!entity) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
family: "counterparty_bidirectional_value_flow_or_netting",
|
||||||
|
entity
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function hasExplicitCounterpartyValueObject(text) {
|
function hasExplicitCounterpartyValueObject(text) {
|
||||||
return /(?:\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b|\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442|\u0442\u043e\u0432\u0430\u0440|\u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442\u0443\u0440|\u0441\u0434\u0435\u043b\u043a|customer|client|counterparty|supplier|vendor|contract|item|product|deal)/iu.test(
|
return /(?:\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b|\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442|\u0442\u043e\u0432\u0430\u0440|\u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442\u0443\u0440|\u0441\u0434\u0435\u043b\u043a|customer|client|counterparty|supplier|vendor|contract|item|product|deal)/iu.test(
|
||||||
text
|
text
|
||||||
|
|
@ -379,14 +464,14 @@ function detectBroadBusinessEvaluation(text) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildEntityCandidates(counterpartyTurnover) {
|
function buildEntityCandidates(entityFamily) {
|
||||||
if (!counterpartyTurnover?.entity) {
|
if (!entityFamily?.entity) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
type: "counterparty",
|
type: "counterparty",
|
||||||
value: counterpartyTurnover.entity,
|
value: entityFamily.entity,
|
||||||
source: "current_turn_loose_entity_tail"
|
source: "current_turn_loose_entity_tail"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
@ -399,10 +484,17 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
|
||||||
const rawText = normalizeTurnText(rawMessage, deps);
|
const rawText = normalizeTurnText(rawMessage, deps);
|
||||||
const effectiveText = normalizeTurnText(effectiveMessage, deps);
|
const effectiveText = normalizeTurnText(effectiveMessage, deps);
|
||||||
const joinedText = fallbackCompactWhitespace(`${rawText} ${effectiveText}`);
|
const joinedText = fallbackCompactWhitespace(`${rawText} ${effectiveText}`);
|
||||||
const supportedIntent = detectSupportedIntent(joinedText, deps);
|
const compactOrganizationCashflowDisplay = hasCompactOrganizationCashflowDisplaySignal(rawText);
|
||||||
const counterpartyTurnover = detectCounterpartyTurnoverFamily(joinedText);
|
const supportedIntent = compactOrganizationCashflowDisplay ? null : detectSupportedIntent(joinedText, deps);
|
||||||
|
const counterpartyBidirectionalValueFlow = compactOrganizationCashflowDisplay ? null : detectCounterpartyBidirectionalValueFlowFamily(joinedText);
|
||||||
|
const counterpartyTurnover = compactOrganizationCashflowDisplay ? null : detectCounterpartyTurnoverFamily(joinedText);
|
||||||
const selectedObjectInventoryExact = hasSelectedObjectInventoryExactSignal(joinedText);
|
const selectedObjectInventoryExact = hasSelectedObjectInventoryExactSignal(joinedText);
|
||||||
const broadBusinessEvaluation = selectedObjectInventoryExact ? null : detectBroadBusinessEvaluation(joinedText);
|
const broadBusinessEvaluation =
|
||||||
|
compactOrganizationCashflowDisplay
|
||||||
|
? { family: "broad_business_evaluation" }
|
||||||
|
: selectedObjectInventoryExact || counterpartyBidirectionalValueFlow?.family
|
||||||
|
? null
|
||||||
|
: detectBroadBusinessEvaluation(joinedText);
|
||||||
const llmIntent = toNonEmptyString(input?.llmPreDecomposeMeta?.predecomposeContract?.intent, deps);
|
const llmIntent = toNonEmptyString(input?.llmPreDecomposeMeta?.predecomposeContract?.intent, deps);
|
||||||
const explicitIntentCandidate =
|
const explicitIntentCandidate =
|
||||||
broadBusinessEvaluation?.family
|
broadBusinessEvaluation?.family
|
||||||
|
|
@ -410,6 +502,8 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
|
||||||
: supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null);
|
: supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null);
|
||||||
const unsupportedFamily = broadBusinessEvaluation?.family
|
const unsupportedFamily = broadBusinessEvaluation?.family
|
||||||
? broadBusinessEvaluation.family
|
? broadBusinessEvaluation.family
|
||||||
|
: !explicitIntentCandidate && counterpartyBidirectionalValueFlow?.family
|
||||||
|
? counterpartyBidirectionalValueFlow.family
|
||||||
: !explicitIntentCandidate && counterpartyTurnover?.family
|
: !explicitIntentCandidate && counterpartyTurnover?.family
|
||||||
? counterpartyTurnover.family
|
? counterpartyTurnover.family
|
||||||
: null;
|
: null;
|
||||||
|
|
@ -417,12 +511,18 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
|
||||||
if (supportedIntent?.reason) {
|
if (supportedIntent?.reason) {
|
||||||
reasonCodes.push(supportedIntent.reason);
|
reasonCodes.push(supportedIntent.reason);
|
||||||
}
|
}
|
||||||
|
if (counterpartyBidirectionalValueFlow?.family) {
|
||||||
|
reasonCodes.push("counterparty_bidirectional_value_flow_current_turn_signal");
|
||||||
|
}
|
||||||
if (counterpartyTurnover?.family) {
|
if (counterpartyTurnover?.family) {
|
||||||
reasonCodes.push("counterparty_turnover_current_turn_signal");
|
reasonCodes.push("counterparty_turnover_current_turn_signal");
|
||||||
}
|
}
|
||||||
if (broadBusinessEvaluation?.family) {
|
if (broadBusinessEvaluation?.family) {
|
||||||
reasonCodes.push("broad_business_evaluation_current_turn_signal");
|
reasonCodes.push("broad_business_evaluation_current_turn_signal");
|
||||||
}
|
}
|
||||||
|
if (compactOrganizationCashflowDisplay) {
|
||||||
|
reasonCodes.push("compact_cashflow_display_current_turn_signal");
|
||||||
|
}
|
||||||
if (rawText !== normalizeTurnText(rawMessage, { ...deps, repairAddressMojibake: (value) => String(value ?? "") })) {
|
if (rawText !== normalizeTurnText(rawMessage, { ...deps, repairAddressMojibake: (value) => String(value ?? "") })) {
|
||||||
reasonCodes.push("mojibake_repair_applied");
|
reasonCodes.push("mojibake_repair_applied");
|
||||||
}
|
}
|
||||||
|
|
@ -443,6 +543,8 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
|
||||||
? "inventory"
|
? "inventory"
|
||||||
: broadBusinessEvaluation?.family
|
: broadBusinessEvaluation?.family
|
||||||
? "business_summary"
|
? "business_summary"
|
||||||
|
: counterpartyBidirectionalValueFlow?.family
|
||||||
|
? "counterparty_value"
|
||||||
: explicitIntentCandidate?.includes("counterparty")
|
: explicitIntentCandidate?.includes("counterparty")
|
||||||
? "counterparty"
|
? "counterparty"
|
||||||
: counterpartyTurnover?.family
|
: counterpartyTurnover?.family
|
||||||
|
|
@ -455,6 +557,8 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
|
||||||
? "confirmed_snapshot"
|
? "confirmed_snapshot"
|
||||||
: broadBusinessEvaluation?.family
|
: broadBusinessEvaluation?.family
|
||||||
? "broad_evaluation"
|
? "broad_evaluation"
|
||||||
|
: counterpartyBidirectionalValueFlow?.family
|
||||||
|
? "net_value_flow"
|
||||||
: explicitIntentCandidate === "customer_revenue_and_payments" ||
|
: explicitIntentCandidate === "customer_revenue_and_payments" ||
|
||||||
explicitIntentCandidate === "supplier_payouts_profile"
|
explicitIntentCandidate === "supplier_payouts_profile"
|
||||||
? "counterparty_value_or_turnover"
|
? "counterparty_value_or_turnover"
|
||||||
|
|
@ -470,7 +574,10 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
|
||||||
? "counterparty_value_or_turnover"
|
? "counterparty_value_or_turnover"
|
||||||
: null;
|
: null;
|
||||||
const staleReplayForbidden = Boolean(
|
const staleReplayForbidden = Boolean(
|
||||||
unsupportedFamily || broadBusinessEvaluation?.family || (counterpartyTurnover?.entity && !explicitIntentCandidate)
|
unsupportedFamily ||
|
||||||
|
broadBusinessEvaluation?.family ||
|
||||||
|
(counterpartyBidirectionalValueFlow?.entity && !explicitIntentCandidate) ||
|
||||||
|
(counterpartyTurnover?.entity && !explicitIntentCandidate)
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
schema_version: "assistant_turn_meaning_v1",
|
schema_version: "assistant_turn_meaning_v1",
|
||||||
|
|
@ -481,10 +588,13 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
|
||||||
asked_domain_family: askedDomainFamily,
|
asked_domain_family: askedDomainFamily,
|
||||||
asked_action_family: askedActionFamily,
|
asked_action_family: askedActionFamily,
|
||||||
explicit_intent_candidate: explicitIntentCandidate,
|
explicit_intent_candidate: explicitIntentCandidate,
|
||||||
explicit_entity_candidates: broadBusinessEvaluation?.family ? [] : buildEntityCandidates(counterpartyTurnover),
|
explicit_entity_candidates: broadBusinessEvaluation?.family
|
||||||
|
? []
|
||||||
|
: buildEntityCandidates(counterpartyBidirectionalValueFlow ?? counterpartyTurnover),
|
||||||
meaning_confidence: broadBusinessEvaluation?.family
|
meaning_confidence: broadBusinessEvaluation?.family
|
||||||
? "medium"
|
? "medium"
|
||||||
: supportedIntent?.confidence ?? (counterpartyTurnover?.family ? "medium" : "low"),
|
: supportedIntent?.confidence ??
|
||||||
|
(counterpartyBidirectionalValueFlow?.family || counterpartyTurnover?.family ? "medium" : "low"),
|
||||||
intent_override_strength: explicitIntentCandidate
|
intent_override_strength: explicitIntentCandidate
|
||||||
? "explicit_current_turn_intent"
|
? "explicit_current_turn_intent"
|
||||||
: staleReplayForbidden
|
: staleReplayForbidden
|
||||||
|
|
|
||||||
|
|
@ -219,22 +219,23 @@ export function loadCapabilitiesRegistry(): CapabilityRegistry {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildCapabilityContractReplyFromRegistry(): string {
|
export function buildCapabilityContractReplyFromRegistry(): string {
|
||||||
const registry = loadCapabilitiesRegistry();
|
|
||||||
const topGroups = registry.groups.filter((group) => group.group_code !== "boundaries").slice(0, 6);
|
|
||||||
const groupLines = topGroups.map((group, index) => {
|
|
||||||
const examples = group.typical_queries
|
|
||||||
.slice(0, 2)
|
|
||||||
.map((query) => query.trim())
|
|
||||||
.filter((query) => query.length > 0)
|
|
||||||
.join("; ");
|
|
||||||
return `${index + 1}. ${group.group_title}: ${group.description}${examples ? `. Например: ${examples}` : "."}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
"Могу помочь с вопросами по данным 1С в режиме чтения: НДС, контрагенты, долги, деньги и склад.",
|
"Могу быстро смотреть управленческие вещи по данным 1С в режиме чтения.",
|
||||||
"По основным группам:",
|
"",
|
||||||
...groupLines,
|
"- кто должен денег и кому должны;",
|
||||||
"Если нужно, подскажу, как лучше сформулировать запрос под вашу задачу.",
|
"- какой год или месяц был самым денежным;",
|
||||||
|
"- какие контрагенты дают основной поток;",
|
||||||
|
"- что лежит на складе и какие остатки стареют;",
|
||||||
|
"- сколько НДС к уплате за период;",
|
||||||
|
"- какие документы, оплаты и договоры есть по контрагенту.",
|
||||||
|
"",
|
||||||
|
"Примеры запросов:",
|
||||||
|
"- кто самый доходный клиент за все время",
|
||||||
|
"- что зависло на складе",
|
||||||
|
"- кому мы должны на сегодня",
|
||||||
|
"- какое нетто по СВК за 2020",
|
||||||
|
"- сколько НДС к уплате за 4 квартал 2019",
|
||||||
|
"",
|
||||||
"Что не делаю: не настраиваю 1С, не меняю конфигурацию, не создаю и не провожу документы, не выполняю админ-действия на сервере."
|
"Что не делаю: не настраиваю 1С, не меняю конфигурацию, не создаю и не провожу документы, не выполняю админ-действия на сервере."
|
||||||
].join("\n");
|
].join("\n");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ type V2FamilyFragment = V2Family["fragments"][number];
|
||||||
type RouteQueryClass =
|
type RouteQueryClass =
|
||||||
| "exact_object_trace"
|
| "exact_object_trace"
|
||||||
| "ranking_or_period_summary"
|
| "ranking_or_period_summary"
|
||||||
|
| "bidirectional_value_flow"
|
||||||
| "symptom_first"
|
| "symptom_first"
|
||||||
| "lifecycle_first"
|
| "lifecycle_first"
|
||||||
| "chain_break"
|
| "chain_break"
|
||||||
|
|
@ -66,6 +67,10 @@ interface RouteDisciplineRule {
|
||||||
const ACCOUNT_HINT_PATTERN =
|
const ACCOUNT_HINT_PATTERN =
|
||||||
/(?:\b(?:account|acct|schet|счет|сч)\s*[:#]?\s*(?:[1-9][0-9](?:[./-][0-9]{1,2})?)|\b(?:19|20|21|23|25|26|28|29|44|51|60|62|68)\b)/i;
|
/(?:\b(?:account|acct|schet|счет|сч)\s*[:#]?\s*(?:[1-9][0-9](?:[./-][0-9]{1,2})?)|\b(?:19|20|21|23|25|26|28|29|44|51|60|62|68)\b)/i;
|
||||||
const PERIOD_PATTERN = /\b20\d{2}(?:[-./](?:0[1-9]|1[0-2]))?\b/i;
|
const PERIOD_PATTERN = /\b20\d{2}(?:[-./](?:0[1-9]|1[0-2]))?\b/i;
|
||||||
|
const BIDIRECTIONAL_VALUE_FLOW_PATTERN =
|
||||||
|
/(?:\b(?:receive(?:d)?|received|get|got|incoming|inflow|paid|payment|payments|outgoing|outflow|net|netto|cash\s*flow)\b|получ(?:ить|ил[аи]?|ено|аем|или)|поступ(?:ил[аи]?|ление|ления)|заплат(?:ить|ил[аи]?|или)|оплат(?:ить|ил[аи]?|ы|или)|входящ(?:ий|ие|их)|исходящ(?:ий|ие|их)|нетто|сальдо)/iu;
|
||||||
|
const COUNTERPARTY_SCOPE_PATTERN =
|
||||||
|
/(?:\b(?:counterparty|supplier|customer|vendor|client|bank)\b|контрагент|поставщик|покупател|клиент|заказчик|банк|сбербанк|по\s+(?:ип|ооо|пао|зао|оао|группа)\b)/iu;
|
||||||
const SYMPTOM_MARKER_PATTERN =
|
const SYMPTOM_MARKER_PATTERN =
|
||||||
/(?:\bsymptom\b|\banomaly\b|\bproblem\b|\bissue\b|\btail\b|\bhanging\b|\bblocked\b|\bincomplete\b|remains?\s+open|not\s+(?:confirmed|observed|resolved|closed)|не\s+(?:подтвержден|закрыт|наблюдается)|хвост|сбой|проблем)/i;
|
/(?:\bsymptom\b|\banomaly\b|\bproblem\b|\bissue\b|\btail\b|\bhanging\b|\bblocked\b|\bincomplete\b|remains?\s+open|not\s+(?:confirmed|observed|resolved|closed)|не\s+(?:подтвержден|закрыт|наблюдается)|хвост|сбой|проблем)/i;
|
||||||
const LIFECYCLE_MARKER_PATTERN =
|
const LIFECYCLE_MARKER_PATTERN =
|
||||||
|
|
@ -96,6 +101,13 @@ export const ROUTE_DISCIPLINE_RULE_TABLE: RouteDisciplineRule[] = [
|
||||||
forbidden_fallback: ["store_canonical", "hybrid_store_plus_live"],
|
forbidden_fallback: ["store_canonical", "hybrid_store_plus_live"],
|
||||||
description: "Ranking and period summary queries require analytical batch path."
|
description: "Ranking and period summary queries require analytical batch path."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
query_class: "bidirectional_value_flow",
|
||||||
|
required_route: "hybrid_store_plus_live",
|
||||||
|
allowed_fallback: ["no_route"],
|
||||||
|
forbidden_fallback: ["store_canonical"],
|
||||||
|
description: "Scoped bidirectional value-flow questions require hybrid evidence path."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
query_class: "symptom_first",
|
query_class: "symptom_first",
|
||||||
required_route: "hybrid_store_plus_live",
|
required_route: "hybrid_store_plus_live",
|
||||||
|
|
@ -218,6 +230,21 @@ function hasAccountOrPeriodAnchor(fragment: V2FamilyFragment, lowerText: string)
|
||||||
return fragment.account_hints.length > 0 || ACCOUNT_HINT_PATTERN.test(lowerText) || PERIOD_PATTERN.test(lowerText);
|
return fragment.account_hints.length > 0 || ACCOUNT_HINT_PATTERN.test(lowerText) || PERIOD_PATTERN.test(lowerText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasBidirectionalValueFlowSignal(fragment: V2FamilyFragment, lowerText: string): boolean {
|
||||||
|
if (fragment.flags.asks_for_ranking_or_top || fragment.flags.asks_for_period_summary) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return BIDIRECTIONAL_VALUE_FLOW_PATTERN.test(lowerText);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasCounterpartyScopeSignal(fragment: V2FamilyFragment, lowerText: string): boolean {
|
||||||
|
return (
|
||||||
|
COUNTERPARTY_SCOPE_PATTERN.test(lowerText) ||
|
||||||
|
fragment.entity_hints.some((hint) => hint.trim().length > 0) ||
|
||||||
|
fragment.candidate_labels.includes("cross_entity")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function resolveRouteClass(fragment: V2FamilyFragment): RouteDisciplineRule {
|
function resolveRouteClass(fragment: V2FamilyFragment): RouteDisciplineRule {
|
||||||
const lowerText = mergedFragmentText(fragment);
|
const lowerText = mergedFragmentText(fragment);
|
||||||
const symptomSignal = hasSymptomSignal(fragment, lowerText);
|
const symptomSignal = hasSymptomSignal(fragment, lowerText);
|
||||||
|
|
@ -227,6 +254,8 @@ function resolveRouteClass(fragment: V2FamilyFragment): RouteDisciplineRule {
|
||||||
const causalSignal = hasCausalSignal(lowerText);
|
const causalSignal = hasCausalSignal(lowerText);
|
||||||
const ambiguitySignal = hasAmbiguitySignal(fragment, lowerText);
|
const ambiguitySignal = hasAmbiguitySignal(fragment, lowerText);
|
||||||
const accountOrPeriodAnchor = hasAccountOrPeriodAnchor(fragment, lowerText);
|
const accountOrPeriodAnchor = hasAccountOrPeriodAnchor(fragment, lowerText);
|
||||||
|
const bidirectionalValueFlowSignal = hasBidirectionalValueFlowSignal(fragment, lowerText);
|
||||||
|
const counterpartyScopeSignal = hasCounterpartyScopeSignal(fragment, lowerText);
|
||||||
|
|
||||||
if (fragment.flags.asks_for_exact_object_trace) {
|
if (fragment.flags.asks_for_exact_object_trace) {
|
||||||
return ROUTE_DISCIPLINE_RULE_MAP.get("exact_object_trace")!;
|
return ROUTE_DISCIPLINE_RULE_MAP.get("exact_object_trace")!;
|
||||||
|
|
@ -234,6 +263,9 @@ function resolveRouteClass(fragment: V2FamilyFragment): RouteDisciplineRule {
|
||||||
if (fragment.flags.asks_for_ranking_or_top || fragment.flags.asks_for_period_summary) {
|
if (fragment.flags.asks_for_ranking_or_top || fragment.flags.asks_for_period_summary) {
|
||||||
return ROUTE_DISCIPLINE_RULE_MAP.get("ranking_or_period_summary")!;
|
return ROUTE_DISCIPLINE_RULE_MAP.get("ranking_or_period_summary")!;
|
||||||
}
|
}
|
||||||
|
if (bidirectionalValueFlowSignal && counterpartyScopeSignal) {
|
||||||
|
return ROUTE_DISCIPLINE_RULE_MAP.get("bidirectional_value_flow")!;
|
||||||
|
}
|
||||||
if (ambiguitySignal && (symptomSignal || lifecycleSignal || chainBreakSignal || periodImpactSignal || causalSignal)) {
|
if (ambiguitySignal && (symptomSignal || lifecycleSignal || chainBreakSignal || periodImpactSignal || causalSignal)) {
|
||||||
return ROUTE_DISCIPLINE_RULE_MAP.get("mixed_ambiguity")!;
|
return ROUTE_DISCIPLINE_RULE_MAP.get("mixed_ambiguity")!;
|
||||||
}
|
}
|
||||||
|
|
@ -272,7 +304,8 @@ function shouldPromoteFromNoRoute(fragment: V2FamilyFragment, rule: RouteDiscipl
|
||||||
hasLifecycleSignal(fragment, lowerText) ||
|
hasLifecycleSignal(fragment, lowerText) ||
|
||||||
hasChainBreakSignal(lowerText) ||
|
hasChainBreakSignal(lowerText) ||
|
||||||
hasPeriodImpactSignal(lowerText) ||
|
hasPeriodImpactSignal(lowerText) ||
|
||||||
hasCausalSignal(lowerText);
|
hasCausalSignal(lowerText) ||
|
||||||
|
(hasBidirectionalValueFlowSignal(fragment, lowerText) && hasCounterpartyScopeSignal(fragment, lowerText));
|
||||||
|
|
||||||
const hasAnchor =
|
const hasAnchor =
|
||||||
hasAccountOrPeriodAnchor(fragment, lowerText) ||
|
hasAccountOrPeriodAnchor(fragment, lowerText) ||
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ export type AddressIntent =
|
||||||
| "inventory_supplier_stock_overlap_as_of_date"
|
| "inventory_supplier_stock_overlap_as_of_date"
|
||||||
| "inventory_sale_trace_for_item"
|
| "inventory_sale_trace_for_item"
|
||||||
| "inventory_trading_margin_proxy_for_organization"
|
| "inventory_trading_margin_proxy_for_organization"
|
||||||
|
| "inventory_margin_ranking_for_nomenclature"
|
||||||
| "inventory_profitability_for_item"
|
| "inventory_profitability_for_item"
|
||||||
| "inventory_purchase_to_sale_chain"
|
| "inventory_purchase_to_sale_chain"
|
||||||
| "inventory_aging_by_purchase_date"
|
| "inventory_aging_by_purchase_date"
|
||||||
|
|
@ -203,6 +204,7 @@ export interface AddressRecipeDefinition {
|
||||||
| "inventory_supplier_stock_overlap_profile"
|
| "inventory_supplier_stock_overlap_profile"
|
||||||
| "inventory_sale_trace_profile"
|
| "inventory_sale_trace_profile"
|
||||||
| "inventory_trading_margin_proxy_profile"
|
| "inventory_trading_margin_proxy_profile"
|
||||||
|
| "inventory_margin_ranking_profile"
|
||||||
| "inventory_profitability_profile"
|
| "inventory_profitability_profile"
|
||||||
| "inventory_purchase_to_sale_chain_profile"
|
| "inventory_purchase_to_sale_chain_profile"
|
||||||
| "inventory_aging_by_purchase_date_profile"
|
| "inventory_aging_by_purchase_date_profile"
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,15 @@ describe("addressIntentResolver regression bridges", () => {
|
||||||
|
|
||||||
expect(result.intent).toBe("inventory_aging_by_purchase_date");
|
expect(result.intent).toBe("inventory_aging_by_purchase_date");
|
||||||
});
|
});
|
||||||
|
it("routes nomenclature margin ranking away from OS, bank, and settlement fallbacks", () => {
|
||||||
|
const result = resolveAddressIntent(
|
||||||
|
"\u041a\u0430\u043a\u0430\u044f \u043d\u043e\u043c\u0435\u043a\u043b\u0430\u0442\u0443\u0440\u0430 \u0442\u043e\u0432\u0430\u0440\u0430 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u0430 \u0441 \u0432\u044b\u0441\u043e\u043a\u043e\u0439 \u043f\u0440\u0438\u0431\u044b\u043b\u044c\u044e \u043a\u0430\u043a\u0430\u044f \u0441 \u043d\u0438\u0437\u043a\u043e\u0439"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.intent).toBe("inventory_margin_ranking_for_nomenclature");
|
||||||
|
expect(result.reasons).toContain("unicode_nomenclature_margin_ranking_bridge_signal_detected");
|
||||||
|
});
|
||||||
|
|
||||||
it("detects bare historical inventory root with explicit month-year", () => {
|
it("detects bare historical inventory root with explicit month-year", () => {
|
||||||
const result = resolveAddressIntent("остатки на март 2016");
|
const result = resolveAddressIntent("остатки на март 2016");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,4 +104,292 @@ describe("inventory profitability selected-object regressions", () => {
|
||||||
expect(reply).toContain("не чистая прибыль компании");
|
expect(reply).toContain("не чистая прибыль компании");
|
||||||
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("asks for a period before ranking nomenclature by margin", async () => {
|
||||||
|
const service = new AddressQueryService();
|
||||||
|
const result = await service.tryHandle(
|
||||||
|
"\u041a\u0430\u043a\u0430\u044f \u043d\u043e\u043c\u0435\u043a\u043b\u0430\u0442\u0443\u0440\u0430 \u0442\u043e\u0432\u0430\u0440\u0430 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u0430 \u0441 \u0432\u044b\u0441\u043e\u043a\u043e\u0439 \u043f\u0440\u0438\u0431\u044b\u043b\u044c\u044e \u043a\u0430\u043a\u0430\u044f \u0441 \u043d\u0438\u0437\u043a\u043e\u0439"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result?.handled).toBe(true);
|
||||||
|
expect(result?.response_type).toBe("LIMITED_WITH_REASON");
|
||||||
|
expect(result?.debug.detected_intent).toBe("inventory_margin_ranking_for_nomenclature");
|
||||||
|
expect(result?.debug.selected_recipe).toBe("address_inventory_margin_ranking_for_nomenclature_v1");
|
||||||
|
expect(result?.debug.capability_id).toBe("inventory_inventory_margin_ranking_for_nomenclature");
|
||||||
|
expect(result?.debug.missing_required_filters).toEqual(["period_from", "period_to"]);
|
||||||
|
const reply = String(result?.reply_text ?? "");
|
||||||
|
expect(reply).toContain("\u043d\u0443\u0436\u0435\u043d \u043f\u0435\u0440\u0438\u043e\u0434");
|
||||||
|
expect(reply).toContain("\u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438");
|
||||||
|
expect(reply).not.toContain("\u041e\u0421");
|
||||||
|
expect(reply).not.toContain("\u0430\u043c\u043e\u0440\u0442\u0438\u0437");
|
||||||
|
expect(reply).not.toContain("\u0437\u0430\u043a\u0443\u043f\u043e\u0447\u043d\u044b\u0439/\u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u043d\u044b\u0439 \u0441\u043b\u0435\u0434");
|
||||||
|
expect(reply).not.toContain("settlement");
|
||||||
|
expect(executeAddressMcpQueryMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gives a useful accounting limited answer when sales exist but cost is missing", async () => {
|
||||||
|
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||||
|
fetched_rows: 1,
|
||||||
|
matched_rows: 1,
|
||||||
|
raw_rows: [
|
||||||
|
{
|
||||||
|
Period: "2020-05-20T00:00:00Z",
|
||||||
|
Registrator: "Sales document 1",
|
||||||
|
AccountDt: "62.01",
|
||||||
|
AccountKt: "41.01",
|
||||||
|
Amount: 1500,
|
||||||
|
Quantity: 10,
|
||||||
|
SubcontoKt1: "Item A",
|
||||||
|
Organization: "OOO Alternative Plus"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
rows: [],
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const service = new AddressQueryService();
|
||||||
|
const result = await service.tryHandle(
|
||||||
|
"\u041a\u0430\u043a\u0430\u044f \u043d\u043e\u043c\u0435\u043a\u043b\u0430\u0442\u0443\u0440\u0430 \u0442\u043e\u0432\u0430\u0440\u0430 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u0430 \u0441 \u0432\u044b\u0441\u043e\u043a\u043e\u0439 \u043f\u0440\u0438\u0431\u044b\u043b\u044c\u044e \u043a\u0430\u043a\u0430\u044f \u0441 \u043d\u0438\u0437\u043a\u043e\u0439 \u0437\u0430 \u043c\u0430\u0439 2020"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result?.handled).toBe(true);
|
||||||
|
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
|
||||||
|
expect(result?.debug.detected_intent).toBe("inventory_margin_ranking_for_nomenclature");
|
||||||
|
expect(result?.debug.mcp_call_status).toBe("matched_non_empty");
|
||||||
|
const reply = String(result?.reply_text ?? "");
|
||||||
|
expect(reply).toContain("\u0440\u0435\u0439\u0442\u0438\u043d\u0433 \u043f\u0440\u0438\u0431\u044b\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442\u0443\u0440\u044b \u043f\u043e\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043d\u0435\u043b\u044c\u0437\u044f");
|
||||||
|
expect(reply).toContain("\u0415\u0441\u0442\u044c \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f");
|
||||||
|
expect(reply).toContain("\u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u0438 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438");
|
||||||
|
expect(reply).toContain("\u0447\u0435\u0441\u0442\u043d\u043e \u043f\u043e\u0441\u0447\u0438\u0442\u0430\u0442\u044c \u043d\u0435\u043b\u044c\u0437\u044f");
|
||||||
|
expect(reply).toContain("\u0427\u0442\u043e \u043c\u043e\u0436\u043d\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0434\u0430\u043b\u044c\u0448\u0435");
|
||||||
|
expect(reply).toContain("90.01 / 90.02");
|
||||||
|
expect(reply).not.toContain("\u041e\u0421");
|
||||||
|
expect(reply).not.toContain("\u0430\u043c\u043e\u0440\u0442\u0438\u0437");
|
||||||
|
expect(reply).not.toContain("\u0437\u0430\u043a\u0443\u043f\u043e\u0447\u043d\u044b\u0439/\u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u043d\u044b\u0439 \u0441\u043b\u0435\u0434");
|
||||||
|
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not offer to show realizations when only cost base exists in the period", async () => {
|
||||||
|
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||||
|
fetched_rows: 1,
|
||||||
|
matched_rows: 1,
|
||||||
|
raw_rows: [
|
||||||
|
{
|
||||||
|
Period: "2017-09-12T00:00:00Z",
|
||||||
|
Registrator: "Purchase document 1",
|
||||||
|
AccountDt: "41.01",
|
||||||
|
AccountKt: "60.01",
|
||||||
|
Amount: 700,
|
||||||
|
Quantity: 2,
|
||||||
|
SubcontoDt1: "Item C",
|
||||||
|
Organization: "OOO Alternative Plus"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
rows: [],
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const service = new AddressQueryService();
|
||||||
|
const result = await service.tryHandle(
|
||||||
|
"\u041a\u0430\u043a\u0430\u044f \u043d\u043e\u043c\u0435\u043a\u043b\u0430\u0442\u0443\u0440\u0430 \u0442\u043e\u0432\u0430\u0440\u0430 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u0430 \u0441 \u0432\u044b\u0441\u043e\u043a\u043e\u0439 \u043f\u0440\u0438\u0431\u044b\u043b\u044c\u044e \u043a\u0430\u043a\u0430\u044f \u0441 \u043d\u0438\u0437\u043a\u043e\u0439 \u0437\u0430 \u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044c 2017"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result?.handled).toBe(true);
|
||||||
|
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
|
||||||
|
const reply = String(result?.reply_text ?? "");
|
||||||
|
expect(reply).toContain("\u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u043d\u0430\u044f \u0431\u0430\u0437\u0430");
|
||||||
|
expect(reply).toContain("\u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u043f\u043e \u043d\u0435\u0439 \u0432 \u043f\u0435\u0440\u0438\u043e\u0434\u0435 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e");
|
||||||
|
expect(reply).toContain("\u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u0435 \u0441\u0442\u0440\u043e\u043a\u0438 \u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u043d\u043e\u0439 \u0431\u0430\u0437\u044b");
|
||||||
|
expect(reply).not.toContain("\u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u0435 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438");
|
||||||
|
expect(reply).not.toContain("\u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u043d\u0430\u044f/\u0437\u0430\u043a\u0443\u043f\u043e\u0447\u043d\u0430\u044f");
|
||||||
|
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps cost-base line drilldown inside the nomenclature margin route", () => {
|
||||||
|
const marginFollowupContext = {
|
||||||
|
previous_intent: "inventory_margin_ranking_for_nomenclature" as const,
|
||||||
|
target_intent: "inventory_margin_ranking_for_nomenclature" as const,
|
||||||
|
root_intent: "inventory_margin_ranking_for_nomenclature" as const,
|
||||||
|
previous_filters: {
|
||||||
|
organization: "OOO Alternative Plus",
|
||||||
|
period_from: "2017-09-01",
|
||||||
|
period_to: "2017-09-30"
|
||||||
|
},
|
||||||
|
previous_anchor_type: "unknown" as const,
|
||||||
|
previous_anchor_value: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = runAddressDecomposeStage(
|
||||||
|
"\u043f\u043e\u043a\u0430\u0436\u0438 \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u0435 \u0441\u0442\u0440\u043e\u043a\u0438 \u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u043d\u043e\u0439 \u0431\u0430\u0437\u044b",
|
||||||
|
marginFollowupContext
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result?.intent.intent).toBe("inventory_margin_ranking_for_nomenclature");
|
||||||
|
expect(result?.filters.extracted_filters.period_from).toBe("2017-09-01");
|
||||||
|
expect(result?.filters.extracted_filters.period_to).toBe("2017-09-30");
|
||||||
|
expect(result?.filters.missing_required_filters).toEqual([]);
|
||||||
|
expect(result?.intent.reasons).toContain("intent_adjusted_to_inventory_margin_ranking_followup_context");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not pivot margin follow-up account-41 correction into a balance snapshot", () => {
|
||||||
|
const marginFollowupContext = {
|
||||||
|
previous_intent: "inventory_margin_ranking_for_nomenclature" as const,
|
||||||
|
target_intent: "inventory_margin_ranking_for_nomenclature" as const,
|
||||||
|
root_intent: "inventory_margin_ranking_for_nomenclature" as const,
|
||||||
|
previous_filters: {
|
||||||
|
organization: "OOO Alternative Plus",
|
||||||
|
period_from: "2017-09-01",
|
||||||
|
period_to: "2017-09-30"
|
||||||
|
},
|
||||||
|
previous_anchor_type: "unknown" as const,
|
||||||
|
previous_anchor_value: null
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const message of [
|
||||||
|
"\u0430\u043d\u0430\u043b\u0438\u0437 \u043f\u043e 41 \u0441\u0447\u0435\u0442\u0443 \u0430 \u043d\u0435 01",
|
||||||
|
"\u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0430\u043d\u0430\u043b\u0438\u0437 \u043f\u043e \u0441\u0447\u0435\u0442\u0443 41 \u0432\u043c\u0435\u0441\u0442\u043e \u0441\u0447\u0435\u0442\u0430 01"
|
||||||
|
]) {
|
||||||
|
const result = runAddressDecomposeStage(message, marginFollowupContext);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result?.intent.intent).toBe("inventory_margin_ranking_for_nomenclature");
|
||||||
|
expect(result?.intent.intent).not.toBe("account_balance_snapshot");
|
||||||
|
expect(result?.filters.extracted_filters.period_from).toBe("2017-09-01");
|
||||||
|
expect(result?.filters.extracted_filters.period_to).toBe("2017-09-30");
|
||||||
|
expect(result?.filters.missing_required_filters).toEqual([]);
|
||||||
|
expect(result?.intent.reasons).toContain("intent_adjusted_to_inventory_margin_ranking_followup_context");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps carried period when executing margin account-41 correction", async () => {
|
||||||
|
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||||
|
fetched_rows: 2,
|
||||||
|
matched_rows: 2,
|
||||||
|
raw_rows: [
|
||||||
|
{
|
||||||
|
Period: "2017-02-10T00:00:00Z",
|
||||||
|
Registrator: "Purchase document 2017",
|
||||||
|
AccountDt: "41.01",
|
||||||
|
AccountKt: "60.01",
|
||||||
|
Amount: 500,
|
||||||
|
Quantity: 1,
|
||||||
|
SubcontoDt1: "Item A",
|
||||||
|
Organization: "OOO Alternative Plus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Period: "2017-03-10T00:00:00Z",
|
||||||
|
Registrator: "Sales document 2017",
|
||||||
|
AccountDt: "62.01",
|
||||||
|
AccountKt: "41.01",
|
||||||
|
Amount: 900,
|
||||||
|
Quantity: 1,
|
||||||
|
SubcontoKt1: "Item A",
|
||||||
|
Organization: "OOO Alternative Plus"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
rows: [],
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
const marginFollowupContext = {
|
||||||
|
previous_intent: "inventory_margin_ranking_for_nomenclature" as const,
|
||||||
|
target_intent: "inventory_margin_ranking_for_nomenclature" as const,
|
||||||
|
root_intent: "inventory_margin_ranking_for_nomenclature" as const,
|
||||||
|
previous_filters: {
|
||||||
|
organization: "OOO Alternative Plus",
|
||||||
|
period_from: "2017-01-01",
|
||||||
|
period_to: "2017-12-31"
|
||||||
|
},
|
||||||
|
previous_anchor_type: "organization" as const,
|
||||||
|
previous_anchor_value: "OOO Alternative Plus"
|
||||||
|
};
|
||||||
|
|
||||||
|
const service = new AddressQueryService();
|
||||||
|
const result = await service.tryHandle(
|
||||||
|
"\u0430\u043d\u0430\u043b\u0438\u0437 \u043f\u043e 41 \u0441\u0447\u0435\u0442\u0443 \u0430 \u043d\u0435 01",
|
||||||
|
{ followupContext: marginFollowupContext }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result?.handled).toBe(true);
|
||||||
|
expect(result?.debug.detected_intent).toBe("inventory_margin_ranking_for_nomenclature");
|
||||||
|
expect(result?.debug.selected_recipe).toBe("address_inventory_margin_ranking_for_nomenclature_v1");
|
||||||
|
expect(result?.debug.capability_id).toBe("inventory_inventory_margin_ranking_for_nomenclature");
|
||||||
|
expect(result?.debug.extracted_filters?.period_from).toBe("2017-01-01");
|
||||||
|
expect(result?.debug.extracted_filters?.period_to).toBe("2017-12-31");
|
||||||
|
expect(result?.debug.missing_required_filters).toEqual([]);
|
||||||
|
expect(result?.debug.mcp_call_status).toBe("matched_non_empty");
|
||||||
|
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("answers period-scoped nomenclature margin ranking with high and low gross-margin buckets", async () => {
|
||||||
|
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||||
|
fetched_rows: 4,
|
||||||
|
matched_rows: 4,
|
||||||
|
raw_rows: [
|
||||||
|
{
|
||||||
|
Period: "2020-01-10T00:00:00Z",
|
||||||
|
Registrator: "Purchase document 1",
|
||||||
|
AccountDt: "41.01",
|
||||||
|
AccountKt: "60.01",
|
||||||
|
Amount: 500,
|
||||||
|
Quantity: 10,
|
||||||
|
SubcontoDt1: "Item A",
|
||||||
|
Organization: "OOO Alternative Plus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Period: "2020-02-10T00:00:00Z",
|
||||||
|
Registrator: "Sales document 1",
|
||||||
|
AccountDt: "62.01",
|
||||||
|
AccountKt: "41.01",
|
||||||
|
Amount: 1500,
|
||||||
|
Quantity: 10,
|
||||||
|
SubcontoKt1: "Item A",
|
||||||
|
Organization: "OOO Alternative Plus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Period: "2020-03-10T00:00:00Z",
|
||||||
|
Registrator: "Purchase document 2",
|
||||||
|
AccountDt: "41.01",
|
||||||
|
AccountKt: "60.01",
|
||||||
|
Amount: 1000,
|
||||||
|
Quantity: 5,
|
||||||
|
SubcontoDt1: "Item B",
|
||||||
|
Organization: "OOO Alternative Plus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Period: "2020-04-10T00:00:00Z",
|
||||||
|
Registrator: "Sales document 2",
|
||||||
|
AccountDt: "62.01",
|
||||||
|
AccountKt: "41.01",
|
||||||
|
Amount: 900,
|
||||||
|
Quantity: 5,
|
||||||
|
SubcontoKt1: "Item B",
|
||||||
|
Organization: "OOO Alternative Plus"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
rows: [],
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const service = new AddressQueryService();
|
||||||
|
const result = await service.tryHandle(
|
||||||
|
"\u041a\u0430\u043a\u0430\u044f \u043d\u043e\u043c\u0435\u043a\u043b\u0430\u0442\u0443\u0440\u0430 \u0442\u043e\u0432\u0430\u0440\u0430 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u0430 \u0441 \u0432\u044b\u0441\u043e\u043a\u043e\u0439 \u043f\u0440\u0438\u0431\u044b\u043b\u044c\u044e \u043a\u0430\u043a\u0430\u044f \u0441 \u043d\u0438\u0437\u043a\u043e\u0439 \u0437\u0430 2020 \u0433\u043e\u0434"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result?.handled).toBe(true);
|
||||||
|
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
|
||||||
|
expect(result?.debug.detected_intent).toBe("inventory_margin_ranking_for_nomenclature");
|
||||||
|
expect(result?.debug.selected_recipe).toBe("address_inventory_margin_ranking_for_nomenclature_v1");
|
||||||
|
expect(result?.debug.capability_id).toBe("inventory_inventory_margin_ranking_for_nomenclature");
|
||||||
|
expect(result?.debug.mcp_call_status).toBe("matched_non_empty");
|
||||||
|
const reply = String(result?.reply_text ?? "");
|
||||||
|
expect(reply).toContain("\u0412\u044b\u0441\u043e\u043a\u0430\u044f \u0432\u0430\u043b\u043e\u0432\u0430\u044f \u043c\u0430\u0440\u0436\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u044c");
|
||||||
|
expect(reply).toContain("\u041d\u0438\u0437\u043a\u0430\u044f \u0438\u043b\u0438 \u043e\u0442\u0440\u0438\u0446\u0430\u0442\u0435\u043b\u044c\u043d\u0430\u044f");
|
||||||
|
expect(reply).toContain("Item A");
|
||||||
|
expect(reply).toContain("Item B");
|
||||||
|
expect(reply).toContain("\u043d\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u0435\u043b\u044c \u0447\u0438\u0441\u0442\u043e\u0439 \u043f\u0440\u0438\u0431\u044b\u043b\u0438");
|
||||||
|
expect(reply).not.toContain("\u041e\u0421");
|
||||||
|
expect(reply).not.toContain("\u0430\u043c\u043e\u0440\u0442\u0438\u0437");
|
||||||
|
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -587,6 +587,7 @@ describe("address compose stage utf8 headers", () => {
|
||||||
expect(reply.text).toContain("не подтвержденная клиентская выручка");
|
expect(reply.text).toContain("не подтвержденная клиентская выручка");
|
||||||
expect(reply.text).toContain("Сводка по направлению");
|
expect(reply.text).toContain("Сводка по направлению");
|
||||||
expect(reply.text).toContain("Основание 1С");
|
expect(reply.text).toContain("Основание 1С");
|
||||||
|
expect(reply.text).toContain("По смыслу это скорее финансовый/банковский контур");
|
||||||
expect(reply.text).toContain("вид операции/назначение платежа/договор");
|
expect(reply.text).toContain("вид операции/назначение платежа/договор");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -613,6 +614,8 @@ describe("address compose stage utf8 headers", () => {
|
||||||
|
|
||||||
expect(reply.text).toContain("Сводка по направлению");
|
expect(reply.text).toContain("Сводка по направлению");
|
||||||
expect(reply.text).toContain("Это не обычный поставщик автоматически");
|
expect(reply.text).toContain("Это не обычный поставщик автоматически");
|
||||||
|
expect(reply.text).toContain("Примеры строк 1С:");
|
||||||
|
expect(reply.text).toContain("Следующий шаг: могу отдельно разложить назначения платежа");
|
||||||
expect(reply.text).toContain("Показаны первые 5 из 8");
|
expect(reply.text).toContain("Показаны первые 5 из 8");
|
||||||
expect(reply.text).not.toContain("00000000007");
|
expect(reply.text).not.toContain("00000000007");
|
||||||
});
|
});
|
||||||
|
|
@ -793,12 +796,11 @@ describe("address compose stage utf8 headers", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(reply.responseType).toBe("FACTUAL_LIST");
|
expect(reply.responseType).toBe("FACTUAL_LIST");
|
||||||
expect(reply.text).toContain("Коротко: подтвержденная дебиторская задолженность на 31.05.2020");
|
expect(reply.text).toContain("Коротко: на 31.05.2020 нам должны");
|
||||||
expect(reply.text).toContain("Это подтвержденный срез дебиторской задолженности");
|
expect(reply.text).toContain("К получению:");
|
||||||
expect(reply.text).toContain("Что учтено");
|
expect(reply.text).not.toContain("эвристический shortlist");
|
||||||
expect(reply.text).toContain("Сводка");
|
expect(reply.text).not.toContain("Строк в выборке");
|
||||||
expect(reply.text).toContain("Категории дебиторской задолженности");
|
expect(reply.text).not.toContain("Категории дебиторской задолженности");
|
||||||
expect(reply.text).toContain("Подтвержденные позиции к получению");
|
|
||||||
expect(reply.text).not.toContain("Блок 1");
|
expect(reply.text).not.toContain("Блок 1");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -2089,9 +2091,11 @@ describe("address compose stage utf8 headers", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
||||||
expect(reply.text).toContain("Покрытие VAT-источников в 1С");
|
expect(reply.text).toContain("предварительная оценка по бухгалтерским оборотам НДС");
|
||||||
expect(reply.text).toContain("Найдено VAT-объектов: 5");
|
expect(reply.text).toContain("НДС начисленный");
|
||||||
expect(reply.text).toContain("РегистрНакопления.НДСПродажи");
|
expect(reply.text).not.toContain("Покрытие VAT-источников в 1С");
|
||||||
|
expect(reply.text).not.toContain("VAT-объектов");
|
||||||
|
expect(reply.text).not.toContain("РегистрНакопления.НДСПродажи");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds confirmed VAT tax-period reply from sales and purchase book markers", () => {
|
it("builds confirmed VAT tax-period reply from sales and purchase book markers", () => {
|
||||||
|
|
@ -2191,7 +2195,7 @@ describe("address compose stage utf8 headers", () => {
|
||||||
expect(reply.text).not.toContain("**1**)");
|
expect(reply.text).not.toContain("**1**)");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps VAT probe timestamps intact when numeric emphasis is enabled", () => {
|
it("keeps VAT probe diagnostics out of the user-facing estimate when numeric emphasis is enabled", () => {
|
||||||
const reply = composeFactualReply(
|
const reply = composeFactualReply(
|
||||||
"vat_payable_forecast",
|
"vat_payable_forecast",
|
||||||
[
|
[
|
||||||
|
|
@ -2226,11 +2230,12 @@ describe("address compose stage utf8 headers", () => {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(reply.text).toContain("последнее движение: 2019-12-31T23:59:59Z");
|
expect(reply.text).toContain("предварительная оценка по бухгалтерским оборотам НДС");
|
||||||
|
expect(reply.text).not.toContain("последнее движение: 2019-12-31T23:59:59Z");
|
||||||
expect(reply.text).not.toContain("2019****-12**-31T23:**59**:59Z");
|
expect(reply.text).not.toContain("2019****-12**-31T23:**59**:59Z");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("adds MCP VAT source probe block for confirmed VAT as-of response", () => {
|
it("hides MCP VAT source probe diagnostics for confirmed VAT as-of response", () => {
|
||||||
const reply = composeFactualReply(
|
const reply = composeFactualReply(
|
||||||
"vat_payable_confirmed_as_of_date",
|
"vat_payable_confirmed_as_of_date",
|
||||||
[
|
[
|
||||||
|
|
@ -2272,10 +2277,10 @@ describe("address compose stage utf8 headers", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(reply.responseType).toBe("FACTUAL_LIST");
|
expect(reply.responseType).toBe("FACTUAL_LIST");
|
||||||
expect(reply.text).toContain("Блок 2.1. Проверка VAT-источников в 1С");
|
expect(reply.text).toContain("Что учтено:");
|
||||||
expect(reply.text).toContain("VAT-объектов в метаданных 1С: 3");
|
expect(reply.text).toContain("Подтвержденные позиции:");
|
||||||
expect(reply.text).toContain("Источников с движениями до даты среза: 1");
|
expect(reply.text).not.toContain("VAT-объектов");
|
||||||
expect(reply.text).toContain("РегистрНакопления.НДСНачисленный");
|
expect(reply.text).not.toContain("РегистрНакопления.НДСНачисленный");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("adds VAT probe error note for confirmed VAT as-of response", () => {
|
it("adds VAT probe error note for confirmed VAT as-of response", () => {
|
||||||
|
|
@ -2305,7 +2310,8 @@ describe("address compose stage utf8 headers", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(reply.responseType).toBe("FACTUAL_LIST");
|
expect(reply.responseType).toBe("FACTUAL_LIST");
|
||||||
expect(reply.text).toContain("Дополнительная проверка VAT-источников завершилась ошибкой");
|
expect(reply.text).toContain("Подтвержденные позиции:");
|
||||||
|
expect(reply.text).not.toContain("Дополнительная проверка VAT-источников завершилась ошибкой");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -5724,6 +5730,47 @@ it("routes old purchase residue questions to aging-by-purchase-date", () => {
|
||||||
expect(reply.semantics?.balance_confirmed).toBe(true);
|
expect(reply.semantics?.balance_confirmed).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("sorts inventory-on-hand positions by amount before rendering the top list", () => {
|
||||||
|
const reply = composeFactualReply(
|
||||||
|
"inventory_on_hand_as_of_date",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
period: "2020-03-31T23:59:59Z",
|
||||||
|
registrator: "Остатки на дату",
|
||||||
|
account_dt: "41.01",
|
||||||
|
account_kt: "",
|
||||||
|
amount: 148261.67,
|
||||||
|
analytics: ["Модуль прямоугольный", "Основной склад", "ООО Ромашка"],
|
||||||
|
item: "Модуль прямоугольный",
|
||||||
|
warehouse: "Основной склад",
|
||||||
|
organization: "ООО Ромашка",
|
||||||
|
quantity: 22
|
||||||
|
},
|
||||||
|
{
|
||||||
|
period: "2020-03-31T23:59:59Z",
|
||||||
|
registrator: "Остатки на дату",
|
||||||
|
account_dt: "41.01",
|
||||||
|
account_kt: "",
|
||||||
|
amount: 498472.5,
|
||||||
|
analytics: ["Конструкция трансформер рабочей станции", "Основной склад", "ООО Ромашка"],
|
||||||
|
item: "Конструкция трансформер рабочей станции",
|
||||||
|
warehouse: "Основной склад",
|
||||||
|
organization: "ООО Ромашка",
|
||||||
|
quantity: 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
asOfDate: "2020-03-31",
|
||||||
|
useRubCurrency: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(reply.responseType).toBe("FACTUAL_LIST");
|
||||||
|
expect(reply.text.indexOf("Конструкция трансформер рабочей станции")).toBeLessThan(
|
||||||
|
reply.text.indexOf("Модуль прямоугольный")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps supplier-overlap reply business-first without exact contour leakage", () => {
|
it("keeps supplier-overlap reply business-first without exact contour leakage", () => {
|
||||||
const reply = composeFactualReply(
|
const reply = composeFactualReply(
|
||||||
"inventory_supplier_stock_overlap_as_of_date",
|
"inventory_supplier_stock_overlap_as_of_date",
|
||||||
|
|
|
||||||
|
|
@ -329,9 +329,9 @@ describe("address reply builders regressions", () => {
|
||||||
expect(firstLine).toContain("7.271,20");
|
expect(firstLine).toContain("7.271,20");
|
||||||
expect(firstLine).toContain("Авант мебель");
|
expect(firstLine).toContain("Авант мебель");
|
||||||
expect(firstLine).not.toContain("3.677.454,14");
|
expect(firstLine).not.toContain("3.677.454,14");
|
||||||
expect(result.text).toContain("встречных остатков");
|
expect(result.text).toContain("Встречная часть");
|
||||||
expect(result.text).toContain("Комитет государственных услуг г. Москвы");
|
expect(result.text).toContain("Комитет государственных услуг г. Москвы");
|
||||||
expect(result.text).toContain("Финансовое обеспечение заявки");
|
expect(result.text).not.toContain("Финансовое обеспечение заявки");
|
||||||
expect(result.text).not.toContain("договор/аналитика: ООО \\Альтернатива Плюс\\");
|
expect(result.text).not.toContain("договор/аналитика: ООО \\Альтернатива Плюс\\");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -380,7 +380,7 @@ describe("address reply builders regressions", () => {
|
||||||
expect(firstLine).toContain("9.612.904,90");
|
expect(firstLine).toContain("9.612.904,90");
|
||||||
expect(firstLine).toContain("Департамент капитального ремонта города Москвы.");
|
expect(firstLine).toContain("Департамент капитального ремонта города Москвы.");
|
||||||
expect(firstLine).not.toContain("13.290.359,04");
|
expect(firstLine).not.toContain("13.290.359,04");
|
||||||
expect(result.text).toContain("встречных остатков");
|
expect(result.text).toContain("Встречная часть");
|
||||||
expect(result.text).toContain("Комитет государственных услуг г. Москвы");
|
expect(result.text).toContain("Комитет государственных услуг г. Москвы");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -443,4 +443,140 @@ describe("address reply builders regressions", () => {
|
||||||
expect(result.text.split("\n")[0]).toContain("1.000,00");
|
expect(result.text.split("\n")[0]).toContain("1.000,00");
|
||||||
expect(result.text).not.toContain("встречных остатков");
|
expect(result.text).not.toContain("встречных остатков");
|
||||||
});
|
});
|
||||||
|
it("avoids mixed stock quantity as a fake management KPI in on-hand snapshot answers", () => {
|
||||||
|
const result = composeInventoryReply(
|
||||||
|
"inventory_on_hand_as_of_date",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
amount: 695360,
|
||||||
|
quantity: 3,
|
||||||
|
item: "Рабочая станция",
|
||||||
|
warehouse: "Основной склад",
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
period: "2017-06-30",
|
||||||
|
registrator: "Остатки"
|
||||||
|
} as any,
|
||||||
|
{
|
||||||
|
amount: 295526.51,
|
||||||
|
quantity: 317000,
|
||||||
|
item: "Лифлеты",
|
||||||
|
warehouse: "Основной склад",
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
period: "2017-06-30",
|
||||||
|
registrator: "Остатки"
|
||||||
|
} as any
|
||||||
|
],
|
||||||
|
{
|
||||||
|
userMessage: "какие остатки на июнь 2017",
|
||||||
|
asOfDate: "2017-06-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resolvePayablesAsOfDate: () => "2017-06-30",
|
||||||
|
buildInventoryOnHandAggregate: () => [
|
||||||
|
{
|
||||||
|
item: "Рабочая станция",
|
||||||
|
warehouse: "Основной склад",
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
quantity: 3,
|
||||||
|
amount: 695360,
|
||||||
|
operations: 1,
|
||||||
|
firstPeriod: "2017-06-30",
|
||||||
|
lastPeriod: "2017-06-30",
|
||||||
|
sourceRefs: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
item: "Лифлеты",
|
||||||
|
warehouse: "Основной склад",
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
quantity: 317000,
|
||||||
|
amount: 295526.51,
|
||||||
|
operations: 1,
|
||||||
|
firstPeriod: "2017-06-30",
|
||||||
|
lastPeriod: "2017-06-30",
|
||||||
|
sourceRefs: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
uniqueStrings: (values: string[]) => Array.from(new Set(values)),
|
||||||
|
formatDateRu: (value: string) => value,
|
||||||
|
formatNumberWithDots: (value: number, fractionDigits = 0) => value.toFixed(fractionDigits),
|
||||||
|
formatMoneyRub: (value: number) => `${value} ₽`,
|
||||||
|
isInventoryPurchaseMovement: () => false,
|
||||||
|
summarizeInventoryTraceRows: () => ({
|
||||||
|
item: null,
|
||||||
|
warehouses: [],
|
||||||
|
organizations: [],
|
||||||
|
counterparties: [],
|
||||||
|
documents: [],
|
||||||
|
firstPeriod: null,
|
||||||
|
lastPeriod: null,
|
||||||
|
totalAmount: 0
|
||||||
|
}),
|
||||||
|
formatInventoryTraceRows: () => [],
|
||||||
|
hasInventoryPurchaseDateActionFocus: () => false,
|
||||||
|
inventoryTraceDateLabel: () => "",
|
||||||
|
extractInventoryCounterpartyCandidates: () => [],
|
||||||
|
buildInventoryAgingByItemAggregate: () => [],
|
||||||
|
formatInventoryAgingRows: () => [],
|
||||||
|
isInventorySaleMovement: () => false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result?.text).not.toContain("Суммарное количество");
|
||||||
|
expect(result?.text).toContain("Общее количество не свожу в один управленческий показатель");
|
||||||
|
expect(result?.text).toContain("Следующий шаг: могу раскрыть полный список");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("answers cost-base evidence follow-up directly when margin ranking has sales without confirmed cost", () => {
|
||||||
|
const result = composeInventoryReply(
|
||||||
|
"inventory_margin_ranking_for_nomenclature",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
amount: 120000,
|
||||||
|
quantity: 2,
|
||||||
|
item: "Рабочая станция",
|
||||||
|
period: "2020-05-20",
|
||||||
|
registrator: "Реализация"
|
||||||
|
} as any
|
||||||
|
],
|
||||||
|
{
|
||||||
|
userMessage: "покажи найденные строки себестоимостной базы",
|
||||||
|
periodFrom: "2020-05-01",
|
||||||
|
periodTo: "2020-05-31"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resolvePayablesAsOfDate: () => "2020-05-31",
|
||||||
|
buildInventoryOnHandAggregate: () => [],
|
||||||
|
uniqueStrings: (values: string[]) => Array.from(new Set(values)),
|
||||||
|
formatDateRu: (value: string) => value,
|
||||||
|
formatNumberWithDots: (value: number, fractionDigits = 0) => value.toFixed(fractionDigits),
|
||||||
|
formatMoneyRub: (value: number) => `${value} ₽`,
|
||||||
|
isInventoryPurchaseMovement: () => false,
|
||||||
|
summarizeInventoryTraceRows: (rows: any[]) => ({
|
||||||
|
item: rows[0]?.item ?? null,
|
||||||
|
warehouses: [],
|
||||||
|
organizations: [],
|
||||||
|
counterparties: [],
|
||||||
|
documents: [],
|
||||||
|
firstPeriod: null,
|
||||||
|
lastPeriod: null,
|
||||||
|
totalAmount: 0
|
||||||
|
}),
|
||||||
|
formatInventoryTraceRows: () => [],
|
||||||
|
hasInventoryPurchaseDateActionFocus: () => false,
|
||||||
|
inventoryTraceDateLabel: () => "",
|
||||||
|
extractInventoryCounterpartyCandidates: () => [],
|
||||||
|
buildInventoryAgingByItemAggregate: () => [],
|
||||||
|
formatInventoryAgingRows: () => [],
|
||||||
|
isInventorySaleMovement: () => true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result?.text.split("\n")[0]).toContain(
|
||||||
|
"подтвержденных строк себестоимостной базы по реализованной номенклатуре не найдено"
|
||||||
|
);
|
||||||
|
expect(result?.text).toContain("Есть реализация по 1 номенклатурной позиции");
|
||||||
|
expect(result?.text).toContain("Строк себестоимости реализации / себестоимостной базы для показа нет");
|
||||||
|
expect(result?.text).not.toContain("входящих денежных поступлений");
|
||||||
|
expect(result?.text).not.toContain("амортизац");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -218,6 +218,90 @@ describe("assistant address orchestration runtime adapter", () => {
|
||||||
root_filters: expect.objectContaining({
|
root_filters: expect.objectContaining({
|
||||||
organization: "Org A"
|
organization: "Org A"
|
||||||
})
|
})
|
||||||
|
}),
|
||||||
|
knownOrganizations: ["Org A"]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rebuilds business overview period carryover for compact cashflow follow-up when address carryover is absent", async () => {
|
||||||
|
const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({
|
||||||
|
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||||
|
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
|
||||||
|
entry_status: "bridge_executed",
|
||||||
|
hot_runtime_wired: false,
|
||||||
|
discovery_attempted: true
|
||||||
|
}));
|
||||||
|
const input = buildInput({
|
||||||
|
userMessage:
|
||||||
|
"\u0434\u0430\u0439 \u043e\u0434\u043d\u043e\u0439 \u0441\u0442\u0440\u043e\u043a\u043e\u0439 \u0434\u0435\u043d\u044c\u0433\u0438 \u0431\u0435\u0437 \u0440\u0430\u0437\u0431\u0438\u0432\u043a\u0438",
|
||||||
|
sessionItems: [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
debug: {
|
||||||
|
assistant_mcp_discovery_entry_point_v1: {
|
||||||
|
turn_input: {
|
||||||
|
data_need_graph: {
|
||||||
|
business_fact_family: "business_overview",
|
||||||
|
ranking_need: "top_desc"
|
||||||
|
},
|
||||||
|
turn_meaning_ref: {
|
||||||
|
explicit_date_scope: "2020",
|
||||||
|
explicit_organization_scope:
|
||||||
|
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
runAddressLlmPreDecompose: vi.fn(async () => ({
|
||||||
|
attempted: true,
|
||||||
|
applied: false,
|
||||||
|
effectiveMessage:
|
||||||
|
"\u0434\u0430\u0439 \u043e\u0434\u043d\u043e\u0439 \u0441\u0442\u0440\u043e\u043a\u043e\u0439 \u0434\u0435\u043d\u044c\u0433\u0438 \u0431\u0435\u0437 \u0440\u0430\u0437\u0431\u0438\u0432\u043a\u0438",
|
||||||
|
reason: "raw_kept",
|
||||||
|
predecomposeContract: {
|
||||||
|
mode: "unsupported",
|
||||||
|
intent: "unknown",
|
||||||
|
period: {}
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
resolveAddressFollowupCarryoverContext: vi.fn(() => null),
|
||||||
|
resolveAssistantOrchestrationDecision: vi.fn(() => ({
|
||||||
|
runAddressLane: false,
|
||||||
|
livingMode: "chat",
|
||||||
|
livingReason: "unsupported_current_turn_meaning_boundary",
|
||||||
|
toolGateDecision: "skip_address_lane",
|
||||||
|
toolGateReason: "unsupported_current_turn_meaning_boundary",
|
||||||
|
orchestrationContract: {
|
||||||
|
schema_version: "assistant_orchestration_contract_v1",
|
||||||
|
assistant_turn_meaning: {
|
||||||
|
schema_version: "assistant_turn_meaning_v1",
|
||||||
|
asked_domain_family: "business_summary",
|
||||||
|
asked_action_family: "broad_evaluation",
|
||||||
|
unsupported_but_understood_family: "broad_business_evaluation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
runMcpDiscoveryRuntimeEntryPoint
|
||||||
|
});
|
||||||
|
|
||||||
|
await buildAssistantAddressOrchestrationRuntime(input);
|
||||||
|
|
||||||
|
expect(runMcpDiscoveryRuntimeEntryPoint).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
followupContext: expect.objectContaining({
|
||||||
|
previous_discovery_pilot_scope: "business_overview_route_template_v1",
|
||||||
|
previous_seeded_ranking_need: "top_desc",
|
||||||
|
previous_filters: expect.objectContaining({
|
||||||
|
period_from: "2020-01-01",
|
||||||
|
period_to: "2020-12-31"
|
||||||
|
}),
|
||||||
|
root_filters: expect.objectContaining({
|
||||||
|
period_from: "2020-01-01",
|
||||||
|
period_to: "2020-12-31"
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -499,6 +583,70 @@ describe("assistant address orchestration runtime adapter", () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("prefers raw margin-ranking account correction over account-balance canonical drift", async () => {
|
||||||
|
const resolveAddressFollowupCarryoverContext = vi.fn(() => ({
|
||||||
|
followupContext: {
|
||||||
|
previous_intent: "inventory_margin_ranking_for_nomenclature",
|
||||||
|
target_intent: "inventory_margin_ranking_for_nomenclature",
|
||||||
|
root_intent: "inventory_margin_ranking_for_nomenclature",
|
||||||
|
previous_filters: {
|
||||||
|
period_from: "2017-01-01",
|
||||||
|
period_to: "2017-12-31",
|
||||||
|
organization: "OOO Alternative Plus"
|
||||||
|
},
|
||||||
|
previous_anchor_type: "organization",
|
||||||
|
previous_anchor_value: "OOO Alternative Plus"
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
const resolveAssistantOrchestrationDecision = vi.fn(() => ({
|
||||||
|
runAddressLane: true,
|
||||||
|
livingMode: "address_data",
|
||||||
|
livingReason: "address_lane_triggered",
|
||||||
|
toolGateDecision: "run_address_lane",
|
||||||
|
toolGateReason: "followup_context_detected",
|
||||||
|
orchestrationContract: { schema_version: "assistant_orchestration_contract_v1" }
|
||||||
|
}));
|
||||||
|
const buildAddressLlmPredecomposeContractV1 = vi.fn(({ sourceMessage, canonicalMessage }: { sourceMessage: string; canonicalMessage: string }) => ({
|
||||||
|
schema_version: "address_llm_predecompose_contract_v1",
|
||||||
|
source_message: sourceMessage,
|
||||||
|
canonical_message: canonicalMessage,
|
||||||
|
mode: canonicalMessage === sourceMessage ? "unsupported" : "address_query",
|
||||||
|
intent: canonicalMessage === sourceMessage ? "unknown" : "account_balance_snapshot"
|
||||||
|
}));
|
||||||
|
const rawMessage = "\u0430\u043d\u0430\u043b\u0438\u0437 \u043f\u043e 41 \u0441\u0447\u0435\u0442\u0443 \u0430 \u043d\u0435 01";
|
||||||
|
|
||||||
|
const output = await buildAssistantAddressOrchestrationRuntime(
|
||||||
|
buildInput({
|
||||||
|
userMessage: rawMessage,
|
||||||
|
runAddressLlmPreDecompose: vi.fn(async () => ({
|
||||||
|
attempted: true,
|
||||||
|
applied: true,
|
||||||
|
effectiveMessage:
|
||||||
|
"\u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0430\u043d\u0430\u043b\u0438\u0437 \u043f\u043e \u0441\u0447\u0435\u0442\u0443 41 \u0432\u043c\u0435\u0441\u0442\u043e \u0441\u0447\u0435\u0442\u0430 01",
|
||||||
|
reason: "normalized_fragment_applied",
|
||||||
|
predecomposeContract: {
|
||||||
|
mode: "address_query",
|
||||||
|
intent: "account_balance_snapshot"
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
buildAddressLlmPredecomposeContractV1,
|
||||||
|
resolveAddressFollowupCarryoverContext,
|
||||||
|
resolveAssistantOrchestrationDecision
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(output.addressInputMessage).toBe(rawMessage);
|
||||||
|
expect(output.addressPreDecompose.applied).toBe(false);
|
||||||
|
expect(output.addressPreDecompose.reason).toBe("followup_raw_message_preferred_over_llm_rewrite");
|
||||||
|
expect(resolveAddressFollowupCarryoverContext).toHaveBeenCalledTimes(2);
|
||||||
|
expect(resolveAssistantOrchestrationDecision).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
rawUserMessage: rawMessage,
|
||||||
|
effectiveAddressUserMessage: rawMessage
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("prefers raw selected-object inventory action over generic canonical drift intent", async () => {
|
it("prefers raw selected-object inventory action over generic canonical drift intent", async () => {
|
||||||
const resolveAddressFollowupCarryoverContext = vi.fn(() => ({
|
const resolveAddressFollowupCarryoverContext = vi.fn(() => ({
|
||||||
followupContext: {
|
followupContext: {
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,35 @@ describe("assistant capability binding response guard", () => {
|
||||||
expect(output.audit.reason_codes).toContain("capability_binding_guard_clarification_reply");
|
expect(output.audit.reason_codes).toContain("capability_binding_guard_clarification_reply");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses business clarification for nomenclature margin ranking period", () => {
|
||||||
|
const output = applyAssistantCapabilityBindingResponseGuard({
|
||||||
|
assistantReply: "unsafe answer",
|
||||||
|
replyType: "partial_coverage",
|
||||||
|
capabilityBinding: binding({
|
||||||
|
capability_id: "inventory_inventory_margin_ranking_for_nomenclature",
|
||||||
|
capability_contract_id: "inventory_inventory_margin_ranking_for_nomenclature",
|
||||||
|
binding_status: "blocked",
|
||||||
|
binding_action: "clarify",
|
||||||
|
required_anchors: ["period"],
|
||||||
|
provided_anchors: [],
|
||||||
|
missing_anchors: ["period"],
|
||||||
|
requires_focus_object: false,
|
||||||
|
focus_object_binding_status: "not_required",
|
||||||
|
result_shape: "nomenclature_margin_ranking",
|
||||||
|
answer_object_shape: "inventory_margin_ranking",
|
||||||
|
violations: ["required_anchor_missing"],
|
||||||
|
reason_codes: ["required_anchor_missing"]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output.replyType).toBe("partial_coverage");
|
||||||
|
expect(output.assistantReply).toContain("Для рейтинга прибыльности номенклатуры нужен период");
|
||||||
|
expect(output.assistantReply).toContain("выручку");
|
||||||
|
expect(output.assistantReply).toContain("себестоимость реализации");
|
||||||
|
expect(output.assistantReply).not.toContain("period");
|
||||||
|
expect(output.audit.applied).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("turns blocked incompatible transitions into bounded replies", () => {
|
it("turns blocked incompatible transitions into bounded replies", () => {
|
||||||
const output = applyAssistantCapabilityBindingResponseGuard({
|
const output = applyAssistantCapabilityBindingResponseGuard({
|
||||||
assistantReply: "unsafe answer",
|
assistantReply: "unsafe answer",
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,34 @@ describe("assistant capability runtime binding adapter", () => {
|
||||||
expect(binding.violations).toEqual([]);
|
expect(binding.violations).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("treats period_from and period_to as satisfying period anchor", () => {
|
||||||
|
const binding = resolveAssistantCapabilityRuntimeBinding({
|
||||||
|
addressDebug: {
|
||||||
|
capability_id: "inventory_inventory_margin_ranking_for_nomenclature",
|
||||||
|
detected_intent: "inventory_margin_ranking_for_nomenclature",
|
||||||
|
detected_mode: "address_query",
|
||||||
|
capability_layer: "compute",
|
||||||
|
capability_route_mode: "exact",
|
||||||
|
extracted_filters: {
|
||||||
|
period_from: "2020-05-01",
|
||||||
|
period_to: "2020-05-31",
|
||||||
|
organization: "ООО Альтернатива Плюс"
|
||||||
|
},
|
||||||
|
rows_matched: 1,
|
||||||
|
route_expectation_status: "matched"
|
||||||
|
},
|
||||||
|
groundingStatus: "grounded",
|
||||||
|
replyType: "factual"
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(binding.binding_status).toBe("bound");
|
||||||
|
expect(binding.binding_action).toBe("allow");
|
||||||
|
expect(binding.required_anchors).toEqual(["period"]);
|
||||||
|
expect(binding.provided_anchors).toEqual(expect.arrayContaining(["period_from", "period_to"]));
|
||||||
|
expect(binding.missing_anchors).toEqual([]);
|
||||||
|
expect(binding.violations).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
it("blocks selected-object capabilities when required anchors are missing", () => {
|
it("blocks selected-object capabilities when required anchors are missing", () => {
|
||||||
const binding = resolveAssistantCapabilityRuntimeBinding({
|
const binding = resolveAssistantCapabilityRuntimeBinding({
|
||||||
addressDebug: {
|
addressDebug: {
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,25 @@ describe("assistant MCP discovery data need graph", () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not turn explicit no-top wording into a value-flow ranking", () => {
|
||||||
|
const result = buildAssistantMcpDiscoveryDataNeedGraph({
|
||||||
|
semanticDataNeed: "counterparty value-flow evidence",
|
||||||
|
rawUtterance:
|
||||||
|
"Определить общую сумму поступлений в ООО Альтернатива Плюс за 2020 год, исключая топ-контрагентов и детализацию по контрагентам",
|
||||||
|
turnMeaning: {
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "counterparty_value_or_turnover",
|
||||||
|
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||||
|
explicit_date_scope: "2020"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.business_fact_family).toBe("value_flow");
|
||||||
|
expect(result.ranking_need).toBeNull();
|
||||||
|
expect(result.decomposition_candidates).toContain("aggregate_checked_amounts");
|
||||||
|
expect(result.decomposition_candidates).not.toContain("aggregate_ranked_axis_values");
|
||||||
|
});
|
||||||
|
|
||||||
it("marks metadata lane choice as a clarification-required graph", () => {
|
it("marks metadata lane choice as a clarification-required graph", () => {
|
||||||
const result = buildAssistantMcpDiscoveryDataNeedGraph({
|
const result = buildAssistantMcpDiscoveryDataNeedGraph({
|
||||||
semanticDataNeed: "metadata lane clarification",
|
semanticDataNeed: "metadata lane clarification",
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,26 @@ function entryPointContract(overrides: Record<string, unknown> = {}) {
|
||||||
business_fact_answer_allowed: true,
|
business_fact_answer_allowed: true,
|
||||||
requires_user_clarification: false,
|
requires_user_clarification: false,
|
||||||
planner: {
|
planner: {
|
||||||
selected_chain_id: "value_flow_ranking",
|
selected_chain_id: "value_flow",
|
||||||
catalog_chain_template_matches: ["value_flow_ranking", "value_flow"],
|
evidence_plan: {
|
||||||
|
schema_version: "assistant_evidence_planner_v1",
|
||||||
|
policy_owner: "assistantEvidencePlanner",
|
||||||
|
planner_status: "ready_for_execution",
|
||||||
|
selected_chain_id: "value_flow",
|
||||||
|
evidence_axes: {
|
||||||
|
missing_axes: []
|
||||||
|
},
|
||||||
|
coverage_gate: {
|
||||||
|
expected_coverage: "confirmed_coverage"
|
||||||
|
},
|
||||||
|
answer_contract: {
|
||||||
|
answer_mode: "confirmed_business_answer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
catalog_chain_template_matches: ["value_flow"],
|
||||||
catalog_chain_template_alignment: {
|
catalog_chain_template_alignment: {
|
||||||
alignment_status: "selected_matches_top",
|
alignment_status: "selected_matches_top",
|
||||||
top_chain_template_match: "value_flow_ranking",
|
top_chain_template_match: "value_flow",
|
||||||
selected_chain_template_rank: 1,
|
selected_chain_template_rank: 1,
|
||||||
selected_chain_is_catalog_template: true,
|
selected_chain_is_catalog_template: true,
|
||||||
selected_chain_in_catalog_matches: true,
|
selected_chain_in_catalog_matches: true,
|
||||||
|
|
@ -30,13 +45,16 @@ function entryPointContract(overrides: Record<string, unknown> = {}) {
|
||||||
schema_version: "assistant_mcp_route_candidate_v1",
|
schema_version: "assistant_mcp_route_candidate_v1",
|
||||||
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
|
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
|
||||||
candidate_status: "ready_for_reviewed_execution",
|
candidate_status: "ready_for_reviewed_execution",
|
||||||
selected_chain_id: "value_flow_ranking",
|
selected_chain_id: "value_flow",
|
||||||
selected_chain_summary: "Rank value flow",
|
selected_chain_summary: "Measure value flow",
|
||||||
nearest_catalog_chain_template: "value_flow_ranking",
|
nearest_catalog_chain_template: "value_flow",
|
||||||
catalog_alignment_status: "selected_matches_top",
|
catalog_alignment_status: "selected_matches_top",
|
||||||
business_fact_family: "value_flow",
|
business_fact_family: "value_flow",
|
||||||
action_family: "turnover",
|
action_family: "turnover",
|
||||||
proof_expectation: "coverage_checked_fact",
|
proof_expectation: "coverage_checked_fact",
|
||||||
|
evidence_plan_status: "ready_for_execution",
|
||||||
|
evidence_answer_mode: "confirmed_business_answer",
|
||||||
|
evidence_expected_coverage: "confirmed_coverage",
|
||||||
required_axes: ["organization", "period"],
|
required_axes: ["organization", "period"],
|
||||||
provided_axes: ["organization", "period"],
|
provided_axes: ["organization", "period"],
|
||||||
missing_axes: [],
|
missing_axes: [],
|
||||||
|
|
@ -45,6 +63,22 @@ function entryPointContract(overrides: Record<string, unknown> = {}) {
|
||||||
recommended_next_action: "Execute through the reviewed runtime bridge and truth gate.",
|
recommended_next_action: "Execute through the reviewed runtime bridge and truth gate.",
|
||||||
forbidden_overclaim_flags: ["no_unchecked_fact_totals"]
|
forbidden_overclaim_flags: ["no_unchecked_fact_totals"]
|
||||||
},
|
},
|
||||||
|
execution_handoff: {
|
||||||
|
schema_version: "assistant_mcp_discovery_execution_handoff_v1",
|
||||||
|
policy_owner: "assistantMcpDiscoveryExecutionHandoff",
|
||||||
|
handoff_status: "ready_for_guarded_response",
|
||||||
|
selected_chain_id: "value_flow",
|
||||||
|
route_candidate_status: "ready_for_reviewed_execution",
|
||||||
|
evidence_answer_mode: "confirmed_business_answer",
|
||||||
|
evidence_expected_coverage: "confirmed_coverage",
|
||||||
|
pilot_status: "executed",
|
||||||
|
answer_mode: "confirmed_with_bounded_inference",
|
||||||
|
mcp_execution_performed: true,
|
||||||
|
allowed_hot_chain: true,
|
||||||
|
can_use_guarded_response: true,
|
||||||
|
must_keep_internal_mechanics_hidden: true,
|
||||||
|
reason_codes: ["execution_handoff_guarded_response_ready"]
|
||||||
|
},
|
||||||
answer_draft: {
|
answer_draft: {
|
||||||
answer_mode: "confirmed_with_bounded_inference"
|
answer_mode: "confirmed_with_bounded_inference"
|
||||||
}
|
}
|
||||||
|
|
@ -66,22 +100,32 @@ describe("assistant MCP discovery debug attachment", () => {
|
||||||
);
|
);
|
||||||
expect(debug.mcp_discovery_entry_status).toBe("bridge_executed");
|
expect(debug.mcp_discovery_entry_status).toBe("bridge_executed");
|
||||||
expect(debug.mcp_discovery_attempted).toBe(true);
|
expect(debug.mcp_discovery_attempted).toBe(true);
|
||||||
expect(debug.mcp_discovery_hot_runtime_wired).toBe(false);
|
expect(debug.mcp_discovery_hot_runtime_wired).toBe(true);
|
||||||
expect(debug.mcp_discovery_bridge_status).toBe("answer_draft_ready");
|
expect(debug.mcp_discovery_bridge_status).toBe("answer_draft_ready");
|
||||||
expect(debug.mcp_discovery_selected_chain_id).toBe("value_flow_ranking");
|
expect(debug.mcp_discovery_selected_chain_id).toBe("value_flow");
|
||||||
expect(debug.mcp_discovery_catalog_chain_template_matches).toEqual(["value_flow_ranking", "value_flow"]);
|
expect(debug.mcp_discovery_evidence_plan_status).toBe("ready_for_execution");
|
||||||
|
expect(debug.mcp_discovery_evidence_plan_answer_mode).toBe("confirmed_business_answer");
|
||||||
|
expect(debug.mcp_discovery_evidence_plan_expected_coverage).toBe("confirmed_coverage");
|
||||||
|
expect(debug.mcp_discovery_evidence_plan_missing_axes).toEqual([]);
|
||||||
|
expect(debug.mcp_discovery_catalog_chain_template_matches).toEqual(["value_flow"]);
|
||||||
expect(debug.mcp_discovery_catalog_chain_alignment_status).toBe("selected_matches_top");
|
expect(debug.mcp_discovery_catalog_chain_alignment_status).toBe("selected_matches_top");
|
||||||
expect(debug.mcp_discovery_catalog_chain_top_match).toBe("value_flow_ranking");
|
expect(debug.mcp_discovery_catalog_chain_top_match).toBe("value_flow");
|
||||||
expect(debug.mcp_discovery_catalog_chain_selected_matches_top).toBe(true);
|
expect(debug.mcp_discovery_catalog_chain_selected_matches_top).toBe(true);
|
||||||
expect(debug.mcp_discovery_route_candidate_status).toBe("ready_for_reviewed_execution");
|
expect(debug.mcp_discovery_route_candidate_status).toBe("ready_for_reviewed_execution");
|
||||||
expect(debug.mcp_discovery_route_candidate_fact_family).toBe("value_flow");
|
expect(debug.mcp_discovery_route_candidate_fact_family).toBe("value_flow");
|
||||||
expect(debug.mcp_discovery_route_candidate_action_family).toBe("turnover");
|
expect(debug.mcp_discovery_route_candidate_action_family).toBe("turnover");
|
||||||
|
expect(debug.mcp_discovery_route_candidate_evidence_plan_status).toBe("ready_for_execution");
|
||||||
|
expect(debug.mcp_discovery_route_candidate_evidence_answer_mode).toBe("confirmed_business_answer");
|
||||||
|
expect(debug.mcp_discovery_route_candidate_evidence_expected_coverage).toBe("confirmed_coverage");
|
||||||
expect(debug.mcp_discovery_route_candidate_missing_axes).toEqual([]);
|
expect(debug.mcp_discovery_route_candidate_missing_axes).toEqual([]);
|
||||||
expect(debug.mcp_discovery_route_candidate_provided_axes).toEqual(["organization", "period"]);
|
expect(debug.mcp_discovery_route_candidate_provided_axes).toEqual(["organization", "period"]);
|
||||||
expect(debug.mcp_discovery_route_candidate_executable_now).toBe(true);
|
expect(debug.mcp_discovery_route_candidate_executable_now).toBe(true);
|
||||||
expect(debug.mcp_discovery_route_candidate_next_action).toBe(
|
expect(debug.mcp_discovery_route_candidate_next_action).toBe(
|
||||||
"Execute through the reviewed runtime bridge and truth gate."
|
"Execute through the reviewed runtime bridge and truth gate."
|
||||||
);
|
);
|
||||||
|
expect(debug.mcp_discovery_execution_handoff_status).toBe("ready_for_guarded_response");
|
||||||
|
expect(debug.mcp_discovery_execution_handoff_allowed_hot_chain).toBe(true);
|
||||||
|
expect(debug.mcp_discovery_execution_handoff_can_use_guarded_response).toBe(true);
|
||||||
expect(debug.mcp_discovery_answer_mode).toBe("confirmed_with_bounded_inference");
|
expect(debug.mcp_discovery_answer_mode).toBe("confirmed_with_bounded_inference");
|
||||||
expect(debug.mcp_discovery_business_fact_answer_allowed).toBe(true);
|
expect(debug.mcp_discovery_business_fact_answer_allowed).toBe(true);
|
||||||
expect(debug.mcp_discovery_user_facing_response_allowed).toBe(true);
|
expect(debug.mcp_discovery_user_facing_response_allowed).toBe(true);
|
||||||
|
|
@ -100,15 +144,27 @@ describe("assistant MCP discovery debug attachment", () => {
|
||||||
expect(debug.mcp_discovery_hot_runtime_wired).toBe(false);
|
expect(debug.mcp_discovery_hot_runtime_wired).toBe(false);
|
||||||
expect(debug.mcp_discovery_bridge_status).toBeNull();
|
expect(debug.mcp_discovery_bridge_status).toBeNull();
|
||||||
expect(debug.mcp_discovery_selected_chain_id).toBeNull();
|
expect(debug.mcp_discovery_selected_chain_id).toBeNull();
|
||||||
|
expect(debug.mcp_discovery_evidence_plan_v1).toBeNull();
|
||||||
|
expect(debug.mcp_discovery_evidence_plan_status).toBeNull();
|
||||||
|
expect(debug.mcp_discovery_evidence_plan_answer_mode).toBeNull();
|
||||||
|
expect(debug.mcp_discovery_evidence_plan_expected_coverage).toBeNull();
|
||||||
|
expect(debug.mcp_discovery_evidence_plan_missing_axes).toEqual([]);
|
||||||
expect(debug.mcp_discovery_catalog_chain_template_matches).toEqual([]);
|
expect(debug.mcp_discovery_catalog_chain_template_matches).toEqual([]);
|
||||||
expect(debug.mcp_discovery_catalog_chain_alignment_status).toBeNull();
|
expect(debug.mcp_discovery_catalog_chain_alignment_status).toBeNull();
|
||||||
expect(debug.mcp_discovery_catalog_chain_top_match).toBeNull();
|
expect(debug.mcp_discovery_catalog_chain_top_match).toBeNull();
|
||||||
expect(debug.mcp_discovery_catalog_chain_selected_matches_top).toBe(false);
|
expect(debug.mcp_discovery_catalog_chain_selected_matches_top).toBe(false);
|
||||||
expect(debug.mcp_discovery_route_candidate_v1).toBeNull();
|
expect(debug.mcp_discovery_route_candidate_v1).toBeNull();
|
||||||
expect(debug.mcp_discovery_route_candidate_status).toBeNull();
|
expect(debug.mcp_discovery_route_candidate_status).toBeNull();
|
||||||
|
expect(debug.mcp_discovery_route_candidate_evidence_plan_status).toBeNull();
|
||||||
|
expect(debug.mcp_discovery_route_candidate_evidence_answer_mode).toBeNull();
|
||||||
|
expect(debug.mcp_discovery_route_candidate_evidence_expected_coverage).toBeNull();
|
||||||
expect(debug.mcp_discovery_route_candidate_missing_axes).toEqual([]);
|
expect(debug.mcp_discovery_route_candidate_missing_axes).toEqual([]);
|
||||||
expect(debug.mcp_discovery_route_candidate_provided_axes).toEqual([]);
|
expect(debug.mcp_discovery_route_candidate_provided_axes).toEqual([]);
|
||||||
expect(debug.mcp_discovery_route_candidate_executable_now).toBe(false);
|
expect(debug.mcp_discovery_route_candidate_executable_now).toBe(false);
|
||||||
|
expect(debug.mcp_discovery_execution_handoff_v1).toBeNull();
|
||||||
|
expect(debug.mcp_discovery_execution_handoff_status).toBeNull();
|
||||||
|
expect(debug.mcp_discovery_execution_handoff_allowed_hot_chain).toBe(false);
|
||||||
|
expect(debug.mcp_discovery_execution_handoff_can_use_guarded_response).toBe(false);
|
||||||
expect(debug.mcp_discovery_answer_mode).toBeNull();
|
expect(debug.mcp_discovery_answer_mode).toBeNull();
|
||||||
expect(debug.mcp_discovery_business_fact_answer_allowed).toBe(false);
|
expect(debug.mcp_discovery_business_fact_answer_allowed).toBe(false);
|
||||||
expect(debug.mcp_discovery_user_facing_response_allowed).toBe(false);
|
expect(debug.mcp_discovery_user_facing_response_allowed).toBe(false);
|
||||||
|
|
|
||||||
|
|
@ -1750,6 +1750,51 @@ describe("assistant MCP discovery pilot executor", () => {
|
||||||
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(2);
|
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("prefers clean counterparty candidates for bidirectional net value-flow probes", async () => {
|
||||||
|
const planner = planAssistantMcpDiscovery({
|
||||||
|
turnMeaning: {
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "net_value_flow",
|
||||||
|
explicit_entity_candidates: ["деньгам с группа свк", "Группа СВК"],
|
||||||
|
explicit_date_scope: "2020",
|
||||||
|
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const deps = buildSequentialDeps([
|
||||||
|
{
|
||||||
|
rows: [{ Period: "2020-06-30T00:00:00", Amount: 12093465, Counterparty: "Группа СВК" }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rows: []
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
|
||||||
|
|
||||||
|
expect(result.derived_bidirectional_value_flow).toMatchObject({
|
||||||
|
counterparty: "Группа СВК",
|
||||||
|
period_scope: "2020",
|
||||||
|
net_amount: 12093465,
|
||||||
|
net_direction: "net_incoming",
|
||||||
|
incoming_customer_revenue: {
|
||||||
|
rows_matched: 1,
|
||||||
|
total_amount: 12093465
|
||||||
|
},
|
||||||
|
outgoing_supplier_payout: {
|
||||||
|
rows_matched: 0,
|
||||||
|
total_amount: 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(result.reason_codes).toContain("pilot_derived_bidirectional_value_flow_from_confirmed_rows");
|
||||||
|
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(2);
|
||||||
|
const serializedCalls = deps.executeAddressMcpQuery.mock.calls
|
||||||
|
.map((call) => JSON.stringify(call[0]))
|
||||||
|
.join("\n");
|
||||||
|
expect(serializedCalls).toContain("%Группа%");
|
||||||
|
expect(serializedCalls).toContain("%СВК%");
|
||||||
|
expect(serializedCalls).not.toContain("деньгам с группа свк");
|
||||||
|
});
|
||||||
|
|
||||||
it("preserves explicit date ranges when building bidirectional value-flow probes", async () => {
|
it("preserves explicit date ranges when building bidirectional value-flow probes", async () => {
|
||||||
const planner = planAssistantMcpDiscovery({
|
const planner = planAssistantMcpDiscovery({
|
||||||
turnMeaning: {
|
turnMeaning: {
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,35 @@ describe("assistant MCP discovery planner", () => {
|
||||||
expect(result.required_axes).toEqual(["counterparty", "period", "aggregate_axis", "amount", "coverage_target"]);
|
expect(result.required_axes).toEqual(["counterparty", "period", "aggregate_axis", "amount", "coverage_target"]);
|
||||||
expect(result.catalog_review.review_status).toBe("catalog_compatible");
|
expect(result.catalog_review.review_status).toBe("catalog_compatible");
|
||||||
expect(result.discovery_plan.answer_may_use_raw_model_claims).toBe(false);
|
expect(result.discovery_plan.answer_may_use_raw_model_claims).toBe(false);
|
||||||
|
expect(result.evidence_plan).toMatchObject({
|
||||||
|
schema_version: "assistant_evidence_planner_v1",
|
||||||
|
planner_status: "ready_for_execution",
|
||||||
|
selected_chain_id: "value_flow",
|
||||||
|
data_need: {
|
||||||
|
business_fact_family: "value_flow",
|
||||||
|
action_family: "turnover",
|
||||||
|
proof_expectation: "coverage_checked_fact"
|
||||||
|
},
|
||||||
|
coverage_gate: {
|
||||||
|
expected_coverage: "confirmed_coverage",
|
||||||
|
answer_permission_if_satisfied: "confirmed_business_answer",
|
||||||
|
answer_may_use_raw_model_claims: false
|
||||||
|
},
|
||||||
|
answer_contract: {
|
||||||
|
answer_mode: "confirmed_business_answer",
|
||||||
|
must_keep_internal_mechanics_hidden: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(result.evidence_plan.evidence_axes.required_axes).toEqual([
|
||||||
|
"counterparty",
|
||||||
|
"period",
|
||||||
|
"aggregate_axis",
|
||||||
|
"amount",
|
||||||
|
"coverage_target"
|
||||||
|
]);
|
||||||
|
expect(result.evidence_plan.evidence_axes.provided_axes).toEqual(["counterparty", "business_entity", "period"]);
|
||||||
|
expect(result.evidence_plan.evidence_axes.missing_axes).toEqual(["aggregate_axis", "amount", "coverage_target"]);
|
||||||
|
expect(result.evidence_plan.evidence_axes.user_actionable_missing_axes).toEqual([]);
|
||||||
expect(result.data_need_graph?.business_fact_family).toBe("value_flow");
|
expect(result.data_need_graph?.business_fact_family).toBe("value_flow");
|
||||||
expect(result.catalog_chain_template_matches[0]).toBe("value_flow");
|
expect(result.catalog_chain_template_matches[0]).toBe("value_flow");
|
||||||
expect(result.catalog_chain_template_alignment).toMatchObject({
|
expect(result.catalog_chain_template_alignment).toMatchObject({
|
||||||
|
|
@ -284,6 +313,17 @@ describe("assistant MCP discovery planner", () => {
|
||||||
selected_chain_matches_top: true
|
selected_chain_matches_top: true
|
||||||
});
|
});
|
||||||
expect(result.catalog_review.review_status).toBe("catalog_compatible");
|
expect(result.catalog_review.review_status).toBe("catalog_compatible");
|
||||||
|
expect(result.evidence_plan.coverage_gate.expected_coverage).toBe("bounded_inference");
|
||||||
|
expect(result.evidence_plan.answer_contract.answer_mode).toBe("bounded_business_inference");
|
||||||
|
expect(result.evidence_plan.answer_contract.required_user_layers).toEqual([
|
||||||
|
"business_conclusion",
|
||||||
|
"key_figures",
|
||||||
|
"evidence_boundary",
|
||||||
|
"next_step"
|
||||||
|
]);
|
||||||
|
expect(result.evidence_plan.answer_contract.forbidden_overclaim_flags).toContain(
|
||||||
|
"no_unchecked_business_health_claim"
|
||||||
|
);
|
||||||
expect(result.reason_codes).toContain("planner_selected_business_overview_from_data_need_graph");
|
expect(result.reason_codes).toContain("planner_selected_business_overview_from_data_need_graph");
|
||||||
expect(result.reason_codes).toContain("planner_instantiated_catalog_chain_template_business_overview");
|
expect(result.reason_codes).toContain("planner_instantiated_catalog_chain_template_business_overview");
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,45 @@ function entryPoint(overrides: Record<string, unknown> = {}) {
|
||||||
} as any;
|
} as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hotValueFlowEntryPoint(overrides: Record<string, unknown> = {}) {
|
||||||
|
const base = entryPoint({
|
||||||
|
hot_runtime_wired: true,
|
||||||
|
bridge: {
|
||||||
|
bridge_status: "answer_draft_ready",
|
||||||
|
hot_runtime_wired: true,
|
||||||
|
user_facing_response_allowed: true,
|
||||||
|
business_fact_answer_allowed: true,
|
||||||
|
requires_user_clarification: false,
|
||||||
|
execution_handoff: {
|
||||||
|
schema_version: "assistant_mcp_discovery_execution_handoff_v1",
|
||||||
|
policy_owner: "assistantMcpDiscoveryExecutionHandoff",
|
||||||
|
handoff_status: "ready_for_guarded_response",
|
||||||
|
selected_chain_id: "value_flow",
|
||||||
|
route_candidate_status: "ready_for_reviewed_execution",
|
||||||
|
evidence_answer_mode: "confirmed_business_answer",
|
||||||
|
evidence_expected_coverage: "confirmed_coverage",
|
||||||
|
pilot_status: "executed",
|
||||||
|
answer_mode: "confirmed_with_bounded_inference",
|
||||||
|
mcp_execution_performed: true,
|
||||||
|
allowed_hot_chain: true,
|
||||||
|
can_use_guarded_response: true,
|
||||||
|
must_keep_internal_mechanics_hidden: true,
|
||||||
|
reason_codes: ["execution_handoff_guarded_response_ready"]
|
||||||
|
},
|
||||||
|
answer_draft: {
|
||||||
|
answer_mode: "confirmed_with_bounded_inference",
|
||||||
|
headline: "Confirmed value flow.",
|
||||||
|
confirmed_lines: ["1C value-flow rows were found for counterparty SVK", "Confirmed scoped amount: 3 750 руб."],
|
||||||
|
inference_lines: ["Counterparty value-flow total was calculated from confirmed 1C movement rows"],
|
||||||
|
unknown_lines: [],
|
||||||
|
limitation_lines: [],
|
||||||
|
next_step_line: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { ...base, ...overrides } as any;
|
||||||
|
}
|
||||||
|
|
||||||
describe("assistant MCP discovery response candidate", () => {
|
describe("assistant MCP discovery response candidate", () => {
|
||||||
it("builds a Russian guarded candidate from a confirmed discovery draft", () => {
|
it("builds a Russian guarded candidate from a confirmed discovery draft", () => {
|
||||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(entryPoint());
|
const candidate = buildAssistantMcpDiscoveryResponseCandidate(entryPoint());
|
||||||
|
|
@ -44,6 +83,16 @@ describe("assistant MCP discovery response candidate", () => {
|
||||||
expect(candidate.reply_text).not.toContain("primitive");
|
expect(candidate.reply_text).not.toContain("primitive");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("marks an allowlisted value-flow handoff as hot-wired for guarded runtime use", () => {
|
||||||
|
const candidate = buildAssistantMcpDiscoveryResponseCandidate(hotValueFlowEntryPoint());
|
||||||
|
|
||||||
|
expect(candidate.candidate_status).toBe("ready_for_guarded_use");
|
||||||
|
expect(candidate.hot_runtime_wired).toBe(true);
|
||||||
|
expect(candidate.eligible_for_future_hot_runtime).toBe(true);
|
||||||
|
expect(candidate.reason_codes).toContain("mcp_discovery_response_candidate_guarded_hot_wired");
|
||||||
|
expect(candidate.reply_text).toContain("3 750");
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps inventory reserve boundary answers direct instead of compacting into a money overview", () => {
|
it("keeps inventory reserve boundary answers direct instead of compacting into a money overview", () => {
|
||||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||||
entryPoint({
|
entryPoint({
|
||||||
|
|
@ -213,6 +262,62 @@ describe("assistant MCP discovery response candidate", () => {
|
||||||
expect(candidate.reply_text).not.toContain("маржа к выручке 90.01");
|
expect(candidate.reply_text).not.toContain("маржа к выручке 90.01");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("answers direct net-profit questions with the accounting result first instead of saying no", () => {
|
||||||
|
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||||
|
entryPoint({
|
||||||
|
turn_input: {
|
||||||
|
adapter_status: "ready",
|
||||||
|
turn_meaning_ref: {
|
||||||
|
raw_message: "какая чистая прибыль по Альтернативе за 2020?",
|
||||||
|
effective_message: "Определить чистую прибыль компании Альтернатива за 2020 год",
|
||||||
|
asked_domain_family: "business_overview",
|
||||||
|
asked_action_family: "profit_margin_boundary",
|
||||||
|
unsupported_but_understood_family: "profit_margin_boundary",
|
||||||
|
explicit_date_scope: "2020"
|
||||||
|
},
|
||||||
|
data_need_graph: {
|
||||||
|
business_fact_family: "business_overview",
|
||||||
|
ranking_need: null,
|
||||||
|
reason_codes: ["data_need_graph_family_business_overview"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bridge: {
|
||||||
|
bridge_status: "answer_draft_ready",
|
||||||
|
user_facing_response_allowed: true,
|
||||||
|
business_fact_answer_allowed: true,
|
||||||
|
requires_user_clarification: false,
|
||||||
|
pilot: {
|
||||||
|
pilot_scope: "business_overview_route_template_v1",
|
||||||
|
derived_business_overview: {
|
||||||
|
accounting_financial_result: {
|
||||||
|
period_scope: "2020",
|
||||||
|
final_result_direction: "loss",
|
||||||
|
final_result_amount_human_ru: "7 136 815,85 руб.",
|
||||||
|
net_margin_to_revenue_pct: -59.41
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
answer_draft: {
|
||||||
|
answer_mode: "confirmed_with_bounded_inference",
|
||||||
|
headline: "Коротко: по бухгалтерскому маршруту 90/91/99 за 2020 подтвержден учетный убыток.",
|
||||||
|
confirmed_lines: [],
|
||||||
|
inference_lines: [],
|
||||||
|
unknown_lines: [],
|
||||||
|
limitation_lines: [],
|
||||||
|
next_step_line: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const firstLine = String(candidate.reply_text ?? "").split("\n")[0] ?? "";
|
||||||
|
expect(firstLine).toContain("за 2020 по закрытию счетов 90/91/99");
|
||||||
|
expect(firstLine).toContain("учетный убыток");
|
||||||
|
expect(firstLine).toContain("минус 7 136 815,85 руб.");
|
||||||
|
expect(firstLine).not.toContain("нет, денежное операционное нетто");
|
||||||
|
expect(candidate.reply_text).toContain("не денежный поток");
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps vendor-risk boundary answers direct instead of compacting into a money overview", () => {
|
it("keeps vendor-risk boundary answers direct instead of compacting into a money overview", () => {
|
||||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||||
entryPoint({
|
entryPoint({
|
||||||
|
|
@ -446,11 +551,11 @@ describe("assistant MCP discovery response candidate", () => {
|
||||||
expect(candidate.reply_text).toContain("не полный бухгалтерский рейтинг доходности");
|
expect(candidate.reply_text).toContain("не полный бухгалтерский рейтинг доходности");
|
||||||
expect(candidate.reply_text).toContain("не как чистую бухгалтерскую прибыль");
|
expect(candidate.reply_text).toContain("не как чистую бухгалтерскую прибыль");
|
||||||
expect(candidate.reply_text).toContain("Годовое операционное нетто в широком срезе не ранжирую");
|
expect(candidate.reply_text).toContain("Годовое операционное нетто в широком срезе не ранжирую");
|
||||||
expect(candidate.reply_text).toContain("проверка достигла лимита строк");
|
expect(candidate.reply_text).toContain("лучше разбить проверку по годам или кварталам");
|
||||||
expect(candidate.reply_text).toContain("выбрать конкретный год или квартал для дозапроса");
|
expect(candidate.reply_text).toContain("не считаю полным бухгалтерским оборотом");
|
||||||
expect(candidate.reply_text).toContain("без выдачи непроверенного итога");
|
|
||||||
expect(candidate.reply_text).not.toContain("По расчетному операционному нетто лучший год");
|
expect(candidate.reply_text).not.toContain("По расчетному операционному нетто лучший год");
|
||||||
expect(candidate.reply_text).not.toContain("По годам:");
|
expect(candidate.reply_text).not.toContain("По годам:");
|
||||||
|
expect(candidate.reply_text).not.toContain("проверка достигла лимита строк");
|
||||||
expect(candidate.reply_text).not.toContain("лимит выборки MCP");
|
expect(candidate.reply_text).not.toContain("лимит выборки MCP");
|
||||||
expect(candidate.reply_text).not.toContain("MCP-срез");
|
expect(candidate.reply_text).not.toContain("MCP-срез");
|
||||||
expect(candidate.reply_text).not.toContain("Что подтверждено:");
|
expect(candidate.reply_text).not.toContain("Что подтверждено:");
|
||||||
|
|
@ -529,6 +634,150 @@ describe("assistant MCP discovery response candidate", () => {
|
||||||
expect(candidate.reply_text).not.toContain("Складской срез");
|
expect(candidate.reply_text).not.toContain("Складской срез");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("lets compact cashflow wording override profit-boundary and overview prose", () => {
|
||||||
|
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||||
|
entryPoint({
|
||||||
|
turn_input: {
|
||||||
|
adapter_status: "ready",
|
||||||
|
turn_meaning_ref: {
|
||||||
|
raw_message:
|
||||||
|
"\u0421\u043a\u043e\u043b\u044c\u043a\u043e \u0434\u0435\u043d\u0435\u0433 \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0430 \u0437\u0430 2020 \u0433\u043e\u0434? \u041e\u0442\u0432\u0435\u0442\u044c \u043a\u043e\u0440\u043e\u0442\u043a\u043e: \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438, \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438, \u0434\u0435\u043d\u0435\u0436\u043d\u043e\u0435 \u043d\u0435\u0442\u0442\u043e, \u044d\u0442\u043e \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u0438\u043b\u0438 \u043d\u0435\u0442.",
|
||||||
|
asked_action_family: "profit_margin_boundary",
|
||||||
|
unsupported_but_understood_family: "profit_margin_boundary",
|
||||||
|
explicit_organization_scope:
|
||||||
|
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
|
||||||
|
explicit_date_scope: "2020"
|
||||||
|
},
|
||||||
|
data_need_graph: {
|
||||||
|
business_fact_family: "business_overview",
|
||||||
|
ranking_need: null,
|
||||||
|
reason_codes: [
|
||||||
|
"data_need_graph_family_business_overview",
|
||||||
|
"data_need_graph_business_overview_direct_money_answer"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bridge: {
|
||||||
|
bridge_status: "answer_draft_ready",
|
||||||
|
user_facing_response_allowed: true,
|
||||||
|
business_fact_answer_allowed: true,
|
||||||
|
requires_user_clarification: false,
|
||||||
|
pilot: {
|
||||||
|
pilot_scope: "business_overview_route_template_v1",
|
||||||
|
derived_business_overview: {
|
||||||
|
period_scope: "2020",
|
||||||
|
incoming_customer_revenue: {
|
||||||
|
total_amount_human_ru: "47 628 853,03 руб."
|
||||||
|
},
|
||||||
|
outgoing_supplier_payout: {
|
||||||
|
total_amount_human_ru: "43 763 351,53 руб."
|
||||||
|
},
|
||||||
|
net_amount_human_ru: "3 865 501,50 руб.",
|
||||||
|
net_direction: "net_incoming",
|
||||||
|
top_customers: [{ axis_value: "СБЕРБАНК", total_amount_human_ru: "12 792 194,31 руб." }],
|
||||||
|
top_suppliers: [],
|
||||||
|
accounting_financial_result: {
|
||||||
|
final_result_direction: "loss",
|
||||||
|
final_result_amount_human_ru: "7 136 815,85 руб.",
|
||||||
|
period_scope: "2020"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
answer_draft: {
|
||||||
|
answer_mode: "confirmed_with_bounded_inference",
|
||||||
|
headline: "wide overview should not leak",
|
||||||
|
confirmed_lines: [],
|
||||||
|
inference_lines: [],
|
||||||
|
unknown_lines: [],
|
||||||
|
limitation_lines: [],
|
||||||
|
next_step_line: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(candidate.reply_text).toContain("47 628 853,03");
|
||||||
|
expect(candidate.reply_text).toContain("43 763 351,53");
|
||||||
|
expect(candidate.reply_text).toContain("3 865 501,50");
|
||||||
|
expect(candidate.reply_text).not.toContain("СБЕРБАНК");
|
||||||
|
expect(candidate.reply_text).not.toContain("7 136 815,85");
|
||||||
|
expect(candidate.reply_text).not.toContain("wide overview");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses compact cashflow output for one-line no-breakdown wording", () => {
|
||||||
|
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||||
|
entryPoint({
|
||||||
|
turn_input: {
|
||||||
|
adapter_status: "ready",
|
||||||
|
turn_meaning_ref: {
|
||||||
|
raw_message:
|
||||||
|
"\u0434\u0430\u0439 \u043e\u0434\u043d\u043e\u0439 \u0441\u0442\u0440\u043e\u043a\u043e\u0439 \u0434\u0435\u043d\u044c\u0433\u0438 \u0431\u0435\u0437 \u0440\u0430\u0437\u0431\u0438\u0432\u043a\u0438",
|
||||||
|
asked_action_family: "broad_evaluation",
|
||||||
|
unsupported_but_understood_family: "broad_business_evaluation",
|
||||||
|
explicit_date_scope: "2020"
|
||||||
|
},
|
||||||
|
data_need_graph: {
|
||||||
|
business_fact_family: "business_overview",
|
||||||
|
ranking_need: null,
|
||||||
|
reason_codes: [
|
||||||
|
"data_need_graph_family_business_overview",
|
||||||
|
"data_need_graph_business_overview_direct_money_answer"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bridge: {
|
||||||
|
bridge_status: "answer_draft_ready",
|
||||||
|
user_facing_response_allowed: true,
|
||||||
|
business_fact_answer_allowed: true,
|
||||||
|
requires_user_clarification: false,
|
||||||
|
pilot: {
|
||||||
|
pilot_scope: "business_overview_route_template_v1",
|
||||||
|
derived_business_overview: {
|
||||||
|
period_scope: "2020",
|
||||||
|
incoming_customer_revenue: {
|
||||||
|
total_amount_human_ru: "47 628 853,03 \u0440\u0443\u0431."
|
||||||
|
},
|
||||||
|
outgoing_supplier_payout: {
|
||||||
|
total_amount_human_ru: "43 763 351,53 \u0440\u0443\u0431."
|
||||||
|
},
|
||||||
|
net_amount_human_ru: "3 865 501,50 \u0440\u0443\u0431.",
|
||||||
|
net_direction: "net_incoming",
|
||||||
|
top_customers: [
|
||||||
|
{
|
||||||
|
axis_value: "\u0421\u0411\u0415\u0420\u0411\u0410\u041d\u041a, \u041f\u0410\u041e",
|
||||||
|
total_amount_human_ru: "12 792 194,31 \u0440\u0443\u0431."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
top_suppliers: [
|
||||||
|
{
|
||||||
|
axis_value: "\u0414\u0435\u043f\u0430\u0440\u0442\u0430\u043c\u0435\u043d\u0442",
|
||||||
|
total_amount_human_ru: "9 612 904,90 \u0440\u0443\u0431."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
answer_draft: {
|
||||||
|
answer_mode: "confirmed_with_bounded_inference",
|
||||||
|
headline: "wide overview should not leak",
|
||||||
|
confirmed_lines: [],
|
||||||
|
inference_lines: [],
|
||||||
|
unknown_lines: [],
|
||||||
|
limitation_lines: [],
|
||||||
|
next_step_line: "\u0427\u0442\u043e \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u0434\u0430\u043b\u044c\u0448\u0435"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(candidate.reply_text).toContain("\u0437\u0430 2020");
|
||||||
|
expect(candidate.reply_text).toContain("47 628 853,03");
|
||||||
|
expect(candidate.reply_text).toContain("43 763 351,53");
|
||||||
|
expect(candidate.reply_text).toContain("3 865 501,50");
|
||||||
|
expect(candidate.reply_text).not.toContain("\u0421\u0411\u0415\u0420\u0411\u0410\u041d\u041a");
|
||||||
|
expect(candidate.reply_text).not.toContain("\u0427\u0442\u043e \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c");
|
||||||
|
expect(candidate.reply_text).not.toContain("wide overview");
|
||||||
|
});
|
||||||
|
|
||||||
it("labels organization-scoped bidirectional value-flow continuations as company scope", () => {
|
it("labels organization-scoped bidirectional value-flow continuations as company scope", () => {
|
||||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||||
entryPoint({
|
entryPoint({
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,42 @@ describe("assistant MCP discovery response policy", () => {
|
||||||
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_not_discovery_ready_address_candidate");
|
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_not_discovery_ready_address_candidate");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps inventory margin-ranking exact reply over stale value-flow discovery candidate", () => {
|
||||||
|
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||||
|
currentReply:
|
||||||
|
"За период 2017 рейтинг прибыльности номенклатуры построить нельзя: нет подтвержденной себестоимости реализации.",
|
||||||
|
currentReplySource: "address_query_runtime_v1",
|
||||||
|
currentReplyType: "partial_coverage",
|
||||||
|
addressRuntimeMeta: {
|
||||||
|
detected_intent: "inventory_margin_ranking_for_nomenclature",
|
||||||
|
selected_recipe: "address_inventory_margin_ranking_for_nomenclature_v1",
|
||||||
|
capability_id: "inventory_inventory_margin_ranking_for_nomenclature",
|
||||||
|
assistant_mcp_discovery_entry_point_v1: entryPoint({
|
||||||
|
turn_input: {
|
||||||
|
adapter_status: "ready",
|
||||||
|
should_run_discovery: true,
|
||||||
|
turn_meaning_ref: {
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "turnover"
|
||||||
|
},
|
||||||
|
data_need_graph: {
|
||||||
|
business_fact_family: "value_flow",
|
||||||
|
clarification_gaps: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.applied).toBe(false);
|
||||||
|
expect(result.decision).toBe("keep_current_reply");
|
||||||
|
expect(result.reply_text).toContain("рейтинг прибыльности номенклатуры");
|
||||||
|
expect(result.reason_codes).toContain(
|
||||||
|
"mcp_discovery_response_policy_keep_inventory_margin_ranking_address_reply"
|
||||||
|
);
|
||||||
|
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_candidate_applied");
|
||||||
|
});
|
||||||
|
|
||||||
it("lets a grounded business overview candidate override a semantically wrong exact address recipe", () => {
|
it("lets a grounded business overview candidate override a semantically wrong exact address recipe", () => {
|
||||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||||
currentReply: "Supplier and stock overlap was confirmed for 2020.",
|
currentReply: "Supplier and stock overlap was confirmed for 2020.",
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,11 @@ describe("assistant MCP discovery runtime bridge", () => {
|
||||||
expect(result.answer_draft.answer_mode).toBe("confirmed_with_bounded_inference");
|
expect(result.answer_draft.answer_mode).toBe("confirmed_with_bounded_inference");
|
||||||
expect(result.business_fact_answer_allowed).toBe(true);
|
expect(result.business_fact_answer_allowed).toBe(true);
|
||||||
expect(result.user_facing_response_allowed).toBe(true);
|
expect(result.user_facing_response_allowed).toBe(true);
|
||||||
|
expect(result.execution_handoff).toMatchObject({
|
||||||
|
handoff_status: "not_enabled_for_chain",
|
||||||
|
allowed_hot_chain: false,
|
||||||
|
can_use_guarded_response: false
|
||||||
|
});
|
||||||
expect(result.reason_codes).toContain("runtime_bridge_not_wired_to_hot_assistant_answer");
|
expect(result.reason_codes).toContain("runtime_bridge_not_wired_to_hot_assistant_answer");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -88,6 +93,10 @@ describe("assistant MCP discovery runtime bridge", () => {
|
||||||
expect(result.requires_user_clarification).toBe(true);
|
expect(result.requires_user_clarification).toBe(true);
|
||||||
expect(result.pilot.mcp_execution_performed).toBe(false);
|
expect(result.pilot.mcp_execution_performed).toBe(false);
|
||||||
expect(result.business_fact_answer_allowed).toBe(false);
|
expect(result.business_fact_answer_allowed).toBe(false);
|
||||||
|
expect(result.execution_handoff).toMatchObject({
|
||||||
|
handoff_status: "awaiting_user_scope",
|
||||||
|
can_use_guarded_response: false
|
||||||
|
});
|
||||||
expect(result.answer_draft.next_step_line).toContain("Уточните контрагента");
|
expect(result.answer_draft.next_step_line).toContain("Уточните контрагента");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -163,6 +172,13 @@ describe("assistant MCP discovery runtime bridge", () => {
|
||||||
});
|
});
|
||||||
expect(result.loop_state.pending_axes).toContain("organization");
|
expect(result.loop_state.pending_axes).toContain("organization");
|
||||||
expect(result.loop_state.provided_axes).toContain("aggregate_axis");
|
expect(result.loop_state.provided_axes).toContain("aggregate_axis");
|
||||||
|
expect(result.planner.evidence_plan.evidence_axes.user_actionable_missing_axes).toEqual(["organization"]);
|
||||||
|
expect(result.planner.evidence_plan.evidence_axes.missing_axes).toEqual([
|
||||||
|
"organization",
|
||||||
|
"aggregate_axis",
|
||||||
|
"amount",
|
||||||
|
"coverage_target"
|
||||||
|
]);
|
||||||
expect(result.loop_state.catalog_chain_template_matches[0]).toBe("value_flow_ranking");
|
expect(result.loop_state.catalog_chain_template_matches[0]).toBe("value_flow_ranking");
|
||||||
expect(result.loop_state.catalog_chain_template_alignment.alignment_status).toBe("selected_matches_top");
|
expect(result.loop_state.catalog_chain_template_alignment.alignment_status).toBe("selected_matches_top");
|
||||||
expect(result.loop_state.catalog_chain_template_alignment.selected_chain_matches_top).toBe(true);
|
expect(result.loop_state.catalog_chain_template_alignment.selected_chain_matches_top).toBe(true);
|
||||||
|
|
@ -174,10 +190,15 @@ describe("assistant MCP discovery runtime bridge", () => {
|
||||||
catalog_alignment_status: "selected_matches_top",
|
catalog_alignment_status: "selected_matches_top",
|
||||||
business_fact_family: "value_flow",
|
business_fact_family: "value_flow",
|
||||||
action_family: "turnover",
|
action_family: "turnover",
|
||||||
|
evidence_plan_status: "needs_clarification",
|
||||||
|
evidence_answer_mode: "clarification_required",
|
||||||
|
evidence_expected_coverage: "confirmed_coverage",
|
||||||
executable_now: false
|
executable_now: false
|
||||||
});
|
});
|
||||||
expect(result.route_candidate.missing_axes).toContain("organization");
|
expect(result.route_candidate.missing_axes).toContain("organization");
|
||||||
expect(result.route_candidate.provided_axes).toContain("aggregate_axis");
|
expect(result.route_candidate.provided_axes).toContain("aggregate_axis");
|
||||||
|
expect(result.route_candidate.missing_axes).not.toContain("amount");
|
||||||
|
expect(result.route_candidate.missing_axes).not.toContain("coverage_target");
|
||||||
expect(result.route_candidate.recommended_next_action).toBe(
|
expect(result.route_candidate.recommended_next_action).toBe(
|
||||||
"Ask the user for the missing scope axes before MCP execution."
|
"Ask the user for the missing scope axes before MCP execution."
|
||||||
);
|
);
|
||||||
|
|
@ -230,6 +251,11 @@ describe("assistant MCP discovery runtime bridge", () => {
|
||||||
executable_now: true,
|
executable_now: true,
|
||||||
enablement_reason: null
|
enablement_reason: null
|
||||||
});
|
});
|
||||||
|
expect(result.execution_handoff).toMatchObject({
|
||||||
|
handoff_status: "not_enabled_for_chain",
|
||||||
|
allowed_hot_chain: false,
|
||||||
|
can_use_guarded_response: false
|
||||||
|
});
|
||||||
expect(result.answer_draft.confirmed_lines.join("\n")).toContain("СВК-А");
|
expect(result.answer_draft.confirmed_lines.join("\n")).toContain("СВК-А");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1053,6 +1079,19 @@ describe("assistant MCP discovery runtime bridge", () => {
|
||||||
period_scope: "2020",
|
period_scope: "2020",
|
||||||
total_amount: 5000
|
total_amount: 5000
|
||||||
});
|
});
|
||||||
|
expect(result.execution_handoff).toMatchObject({
|
||||||
|
schema_version: "assistant_mcp_discovery_execution_handoff_v1",
|
||||||
|
handoff_status: "ready_for_guarded_response",
|
||||||
|
selected_chain_id: "value_flow",
|
||||||
|
route_candidate_status: "ready_for_reviewed_execution",
|
||||||
|
pilot_status: "executed",
|
||||||
|
mcp_execution_performed: true,
|
||||||
|
allowed_hot_chain: true,
|
||||||
|
can_use_guarded_response: true,
|
||||||
|
must_keep_internal_mechanics_hidden: true
|
||||||
|
});
|
||||||
|
expect(result.reason_codes).toContain("runtime_bridge_execution_handoff_ready_for_guarded_response");
|
||||||
|
expect(result.reason_codes).toContain("execution_handoff_guarded_response_ready");
|
||||||
expect(result.answer_draft.confirmed_lines.join("\n")).toContain("5 000");
|
expect(result.answer_draft.confirmed_lines.join("\n")).toContain("5 000");
|
||||||
expect(result.answer_draft.confirmed_lines.join("\n")).not.toContain("контрагенту");
|
expect(result.answer_draft.confirmed_lines.join("\n")).not.toContain("контрагенту");
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,10 @@ describe("assistant MCP discovery runtime entry point", () => {
|
||||||
expect(result.bridge?.bridge_status).toBe("answer_draft_ready");
|
expect(result.bridge?.bridge_status).toBe("answer_draft_ready");
|
||||||
expect(result.bridge?.pilot.pilot_scope).toBe("counterparty_value_flow_query_movements_v1");
|
expect(result.bridge?.pilot.pilot_scope).toBe("counterparty_value_flow_query_movements_v1");
|
||||||
expect(result.bridge?.pilot.derived_value_flow?.total_amount).toBe(3750);
|
expect(result.bridge?.pilot.derived_value_flow?.total_amount).toBe(3750);
|
||||||
expect(result.bridge?.hot_runtime_wired).toBe(false);
|
expect(result.bridge?.hot_runtime_wired).toBe(true);
|
||||||
|
expect(result.bridge?.execution_handoff.can_use_guarded_response).toBe(true);
|
||||||
|
expect(result.hot_runtime_wired).toBe(true);
|
||||||
|
expect(result.reason_codes).toContain("runtime_entry_point_wired_to_guarded_hot_assistant_answer");
|
||||||
expect(result.reason_codes).toContain("mcp_discovery_unsupported_but_understood_turn");
|
expect(result.reason_codes).toContain("mcp_discovery_unsupported_but_understood_turn");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1789,6 +1789,56 @@ describe("assistant MCP discovery turn input adapter", () => {
|
||||||
expect(result.reason_codes).not.toContain("mcp_discovery_counterparty_from_predecompose");
|
expect(result.reason_codes).not.toContain("mcp_discovery_counterparty_from_predecompose");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("grounds organization aliases from known organizations for clean-session earnings questions", () => {
|
||||||
|
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||||
|
userMessage: "скока денег альтернатива заработала за 20 год?",
|
||||||
|
effectiveMessage: "скока денег альтернатива заработала за 20 год?",
|
||||||
|
knownOrganizations: ["ООО Альтернатива Плюс", "ООО Лайсвуд", "РАЙМ"],
|
||||||
|
predecomposeContract: {
|
||||||
|
period: {
|
||||||
|
period_from: "2020-01-01",
|
||||||
|
period_to: "2020-12-31",
|
||||||
|
has_explicit_period: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.adapter_status).toBe("ready");
|
||||||
|
expect(result.should_run_discovery).toBe(true);
|
||||||
|
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
|
||||||
|
expect(result.turn_meaning_ref?.explicit_organization_scope).toBe("ООО Альтернатива Плюс");
|
||||||
|
expect(result.turn_meaning_ref?.explicit_date_scope).toBe("2020");
|
||||||
|
expect(result.reason_codes).toContain("mcp_discovery_organization_scope_from_known_organizations");
|
||||||
|
expect(result.data_need_graph?.clarification_gaps ?? []).not.toContain("organization");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats a business-overview company alias misread as counterparty as organization scope", () => {
|
||||||
|
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||||
|
userMessage: "Сколько денег Альтернатива заработала за 2020 год?",
|
||||||
|
effectiveMessage: "Определить финансовый результат компании Альтернатива за 2020 год",
|
||||||
|
assistantTurnMeaning: {
|
||||||
|
asked_domain_family: "business_summary",
|
||||||
|
asked_action_family: "broad_evaluation",
|
||||||
|
unsupported_but_understood_family: "broad_business_evaluation"
|
||||||
|
},
|
||||||
|
predecomposeContract: {
|
||||||
|
entities: { counterparty: "Альтернатива", organization: null },
|
||||||
|
period: {
|
||||||
|
period_from: "2020-01-01",
|
||||||
|
period_to: "2020-12-31",
|
||||||
|
has_explicit_period: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.adapter_status).toBe("ready");
|
||||||
|
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
|
||||||
|
expect(result.turn_meaning_ref?.explicit_organization_scope).toBe("Альтернатива");
|
||||||
|
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
|
||||||
|
expect(result.data_need_graph?.clarification_gaps ?? []).not.toContain("organization");
|
||||||
|
expect(result.reason_codes).toContain("mcp_discovery_counterparty_reinterpreted_as_organization_scope");
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps all-time business overview from reusing a negated VAT period as active scope", () => {
|
it("keeps all-time business overview from reusing a negated VAT period as active scope", () => {
|
||||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||||
userMessage:
|
userMessage:
|
||||||
|
|
@ -2308,6 +2358,69 @@ describe("assistant MCP discovery turn input adapter", () => {
|
||||||
expect(result.data_need_graph?.reason_codes).toContain("data_need_graph_business_overview_direct_money_answer");
|
expect(result.data_need_graph?.reason_codes).toContain("data_need_graph_business_overview_direct_money_answer");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not inherit ranking when a cashflow follow-up explicitly says no overview or tops", () => {
|
||||||
|
const orgName = "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
||||||
|
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||||
|
userMessage:
|
||||||
|
"\u043d\u0435 \u043e\u0431\u0437\u043e\u0440, \u043f\u0440\u043e\u0441\u0442\u043e \u0434\u0435\u043d\u044c\u0433\u0438: \u043f\u0440\u0438\u0448\u043b\u043e, \u0443\u0448\u043b\u043e, \u043d\u0435\u0442\u0442\u043e \u0437\u0430 2020 \u0431\u0435\u0437 \u0442\u043e\u043f\u043e\u0432",
|
||||||
|
followupContext: {
|
||||||
|
previous_discovery_pilot_scope: "business_overview_route_template_v1",
|
||||||
|
previous_filters: {
|
||||||
|
organization: orgName
|
||||||
|
},
|
||||||
|
previous_seeded_ranking_need: "top_desc"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.adapter_status).toBe("ready");
|
||||||
|
expect(result.should_run_discovery).toBe(true);
|
||||||
|
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
|
||||||
|
expect(result.data_need_graph?.ranking_need).toBeNull();
|
||||||
|
expect(result.turn_meaning_ref?.explicit_date_scope).toBe("2020");
|
||||||
|
expect(result.data_need_graph?.reason_codes).toContain("data_need_graph_business_overview_direct_money_answer");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves previous overview period for compact cashflow display follow-ups", () => {
|
||||||
|
const orgName = "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
||||||
|
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||||
|
userMessage:
|
||||||
|
"\u0434\u0430\u0439 \u043e\u0434\u043d\u043e\u0439 \u0441\u0442\u0440\u043e\u043a\u043e\u0439 \u0434\u0435\u043d\u044c\u0433\u0438 \u0431\u0435\u0437 \u0440\u0430\u0437\u0431\u0438\u0432\u043a\u0438",
|
||||||
|
assistantTurnMeaning: {
|
||||||
|
asked_domain_family: "business_summary",
|
||||||
|
asked_action_family: "broad_evaluation",
|
||||||
|
unsupported_but_understood_family: "broad_business_evaluation",
|
||||||
|
stale_replay_forbidden: true,
|
||||||
|
reason_codes: ["compact_cashflow_display_current_turn_signal"]
|
||||||
|
},
|
||||||
|
followupContext: {
|
||||||
|
previous_discovery_pilot_scope: "business_overview_route_template_v1",
|
||||||
|
previous_filters: {
|
||||||
|
organization: orgName,
|
||||||
|
period_from: "2020-01-01",
|
||||||
|
period_to: "2020-12-31"
|
||||||
|
},
|
||||||
|
previous_seeded_ranking_need: "top_desc"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.adapter_status).toBe("ready");
|
||||||
|
expect(result.should_run_discovery).toBe(true);
|
||||||
|
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
|
||||||
|
expect(result.data_need_graph?.ranking_need).toBeNull();
|
||||||
|
expect(result.turn_meaning_ref).toMatchObject({
|
||||||
|
asked_domain_family: "business_overview",
|
||||||
|
asked_action_family: "broad_evaluation",
|
||||||
|
explicit_organization_scope: orgName,
|
||||||
|
explicit_date_scope: "2020",
|
||||||
|
unsupported_but_understood_family: "broad_business_evaluation",
|
||||||
|
stale_replay_forbidden: true
|
||||||
|
});
|
||||||
|
expect(result.reason_codes).toContain("mcp_discovery_date_scope_from_followup_context");
|
||||||
|
expect(result.reason_codes).not.toContain("mcp_discovery_topic_switch_suppressed_followup_scope");
|
||||||
|
expect(result.data_need_graph?.reason_codes).toContain("data_need_graph_business_overview_direct_money_answer");
|
||||||
|
expect(result.data_need_graph?.reason_codes).not.toContain("data_need_graph_all_time_scope_hint");
|
||||||
|
});
|
||||||
|
|
||||||
it("routes organization-level profit and margin wording to business overview instead of exact value recipes", () => {
|
it("routes organization-level profit and margin wording to business overview instead of exact value recipes", () => {
|
||||||
const orgName = "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
const orgName = "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
||||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||||
|
|
@ -3342,6 +3455,107 @@ describe("assistant MCP discovery turn input adapter", () => {
|
||||||
expect(result.reason_codes).not.toContain("mcp_discovery_counterparty_from_followup_context");
|
expect(result.reason_codes).not.toContain("mcp_discovery_counterparty_from_followup_context");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("pivots a business-overview follow-up with explicit counterparty net-flow wording back to counterparty value flow", () => {
|
||||||
|
const orgName =
|
||||||
|
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
||||||
|
const counterpartyName = "\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a";
|
||||||
|
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||||
|
userMessage:
|
||||||
|
"\u0410 \u0442\u0435\u043f\u0435\u0440\u044c \u043f\u043e \u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a \u0437\u0430 2020: \u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0434\u0435\u043d\u0435\u0433 \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438, \u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438 \u0438 \u043a\u0430\u043a\u043e\u0435 \u043d\u0435\u0442\u0442\u043e?",
|
||||||
|
assistantTurnMeaning: {
|
||||||
|
asked_domain_family: "unknown",
|
||||||
|
asked_action_family: "unknown"
|
||||||
|
},
|
||||||
|
followupContext: {
|
||||||
|
previous_discovery_pilot_scope: "business_overview_route_template_v1",
|
||||||
|
previous_filters: {
|
||||||
|
organization: orgName,
|
||||||
|
period_from: "2020-01-01",
|
||||||
|
period_to: "2020-12-31"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.adapter_status).toBe("ready");
|
||||||
|
expect(result.should_run_discovery).toBe(true);
|
||||||
|
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
|
||||||
|
expect(result.data_need_graph?.business_fact_family).toBe("value_flow");
|
||||||
|
expect(result.data_need_graph?.subject_candidates).toEqual([counterpartyName]);
|
||||||
|
expect(result.turn_meaning_ref).toMatchObject({
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "net_value_flow",
|
||||||
|
explicit_entity_candidates: [counterpartyName],
|
||||||
|
explicit_organization_scope: orgName,
|
||||||
|
explicit_date_scope: "2020",
|
||||||
|
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting",
|
||||||
|
stale_replay_forbidden: true
|
||||||
|
});
|
||||||
|
expect(result.reason_codes).toContain(
|
||||||
|
"mcp_discovery_business_overview_followup_pivoted_to_counterparty_value_flow"
|
||||||
|
);
|
||||||
|
expect(result.reason_codes).toContain("mcp_discovery_business_overview_continuation_from_followup_context");
|
||||||
|
expect(result.reason_codes).toContain("mcp_discovery_counterparty_from_raw_scope");
|
||||||
|
expect(result.reason_codes).not.toContain("mcp_discovery_broad_business_evaluation_route_candidate");
|
||||||
|
expect(result.reason_codes).not.toContain("mcp_discovery_business_overview_suppressed_stale_counterparty");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("survives poisoned business-overview turn meaning when predecompose still carries the counterparty", () => {
|
||||||
|
const orgName =
|
||||||
|
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
||||||
|
const counterpartyName = "\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a";
|
||||||
|
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||||
|
userMessage:
|
||||||
|
"\u0410 \u0442\u0435\u043f\u0435\u0440\u044c \u043f\u043e \u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a \u0437\u0430 2020: \u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0434\u0435\u043d\u0435\u0433 \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438, \u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438 \u0438 \u043a\u0430\u043a\u043e\u0435 \u043d\u0435\u0442\u0442\u043e?",
|
||||||
|
effectiveMessage:
|
||||||
|
"\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\u0443 \u0432\u044b\u043f\u043b\u0430\u0447\u0435\u043d\u043d\u044b\u0445 \u0441\u0440\u0435\u0434\u0441\u0442\u0432 \u0438 \u0447\u0438\u0441\u0442\u0443\u044e \u043f\u0440\u0438\u0431\u044b\u043b\u044c (\u043d\u0435\u0442\u0442\u043e) \u0434\u043b\u044f \u0433\u0440\u0443\u043f\u043f\u044b \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0439 \u0421\u0412\u041a \u0437\u0430 2020 \u0433\u043e\u0434.",
|
||||||
|
assistantTurnMeaning: {
|
||||||
|
asked_domain_family: "business_overview",
|
||||||
|
asked_action_family: "profit_margin_boundary",
|
||||||
|
explicit_date_scope: "2020",
|
||||||
|
unsupported_but_understood_family: "profit_margin_boundary",
|
||||||
|
stale_replay_forbidden: true
|
||||||
|
},
|
||||||
|
predecomposeContract: {
|
||||||
|
entities: {
|
||||||
|
counterparty: counterpartyName
|
||||||
|
},
|
||||||
|
period: {
|
||||||
|
scope: "range",
|
||||||
|
period_from: "2020-01-01",
|
||||||
|
period_to: "2020-12-31"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
followupContext: {
|
||||||
|
previous_discovery_pilot_scope: "business_overview_route_template_v1",
|
||||||
|
previous_filters: {
|
||||||
|
organization: orgName,
|
||||||
|
period_from: "2020-01-01",
|
||||||
|
period_to: "2020-12-31"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.adapter_status).toBe("ready");
|
||||||
|
expect(result.should_run_discovery).toBe(true);
|
||||||
|
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
|
||||||
|
expect(result.data_need_graph?.business_fact_family).toBe("value_flow");
|
||||||
|
expect(result.data_need_graph?.subject_candidates).toEqual([counterpartyName]);
|
||||||
|
expect(result.turn_meaning_ref).toMatchObject({
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "net_value_flow",
|
||||||
|
explicit_entity_candidates: [counterpartyName],
|
||||||
|
explicit_organization_scope: orgName,
|
||||||
|
explicit_date_scope: "2020",
|
||||||
|
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting",
|
||||||
|
stale_replay_forbidden: true
|
||||||
|
});
|
||||||
|
expect(result.reason_codes).toContain(
|
||||||
|
"mcp_discovery_business_overview_followup_pivoted_to_counterparty_value_flow"
|
||||||
|
);
|
||||||
|
expect(result.reason_codes).toContain("mcp_discovery_counterparty_from_predecompose");
|
||||||
|
expect(result.reason_codes).not.toContain("mcp_discovery_broad_business_evaluation_route_candidate");
|
||||||
|
});
|
||||||
|
|
||||||
it("lets an explicit VAT follow-up stay on the exact VAT route instead of stale business overview", () => {
|
it("lets an explicit VAT follow-up stay on the exact VAT route instead of stale business overview", () => {
|
||||||
const orgName =
|
const orgName =
|
||||||
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
||||||
|
|
|
||||||
|
|
@ -183,6 +183,75 @@ describe("assistantRoutePolicy", () => {
|
||||||
expect(decision.livingMode).toBe("address_data");
|
expect(decision.livingMode).toBe("address_data");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps margin-ranking found-rows follow-up in address lane", () => {
|
||||||
|
const policy = buildPolicy();
|
||||||
|
|
||||||
|
const decision = policy.resolveAssistantOrchestrationDecision({
|
||||||
|
rawUserMessage: "покажи найденные строки себестоимостной базы",
|
||||||
|
effectiveAddressUserMessage: "покажи найденные строки себестоимостной базы",
|
||||||
|
followupContext: {
|
||||||
|
previous_intent: "inventory_margin_ranking_for_nomenclature",
|
||||||
|
root_intent: "inventory_margin_ranking_for_nomenclature",
|
||||||
|
previous_filters: {
|
||||||
|
organization: "ООО Альтернатива Плюс",
|
||||||
|
period_from: "2020-05-01",
|
||||||
|
period_to: "2020-05-31"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
llmPreDecomposeMeta: {
|
||||||
|
applied: false,
|
||||||
|
predecomposeContract: {
|
||||||
|
mode: "unsupported",
|
||||||
|
mode_confidence: "low",
|
||||||
|
intent: "unknown",
|
||||||
|
intent_confidence: "low"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
useMock: false
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(decision.runAddressLane).toBe(true);
|
||||||
|
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||||||
|
expect(decision.toolGateReason).toBe("followup_context_detected");
|
||||||
|
expect(decision.livingMode).toBe("address_data");
|
||||||
|
expect(decision.orchestrationContract?.hard_meta_mode).toBe(null);
|
||||||
|
expect(decision.orchestrationContract?.final_decision?.tool_gate_reason).toBe("followup_context_detected");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps margin-ranking period expansion follow-up in address lane", () => {
|
||||||
|
const policy = buildPolicy();
|
||||||
|
|
||||||
|
const decision = policy.resolveAssistantOrchestrationDecision({
|
||||||
|
rawUserMessage: "расширь до 2017 года",
|
||||||
|
effectiveAddressUserMessage: "расширь до 2017 года",
|
||||||
|
followupContext: {
|
||||||
|
previous_intent: "inventory_margin_ranking_for_nomenclature",
|
||||||
|
root_intent: "inventory_margin_ranking_for_nomenclature",
|
||||||
|
previous_filters: {
|
||||||
|
organization: "ООО Альтернатива Плюс",
|
||||||
|
period_from: "2020-05-01",
|
||||||
|
period_to: "2020-05-31"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
llmPreDecomposeMeta: {
|
||||||
|
applied: false,
|
||||||
|
predecomposeContract: {
|
||||||
|
mode: "unsupported",
|
||||||
|
mode_confidence: "low",
|
||||||
|
intent: "unknown",
|
||||||
|
intent_confidence: "low"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
useMock: false
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(decision.runAddressLane).toBe(true);
|
||||||
|
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||||||
|
expect(decision.toolGateReason).toBe("followup_context_detected");
|
||||||
|
expect(decision.livingMode).toBe("address_data");
|
||||||
|
expect(decision.orchestrationContract?.hard_meta_mode).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
it("does not let deep session continuation override an exact VAT period route", () => {
|
it("does not let deep session continuation override an exact VAT period route", () => {
|
||||||
const policy = buildPolicy({
|
const policy = buildPolicy({
|
||||||
detectAddressQuestionMode: () => ({ mode: "address_query", confidence: "high" }),
|
detectAddressQuestionMode: () => ({ mode: "address_query", confidence: "high" }),
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,34 @@ describe("assistantTurnMeaningPolicy", () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps counterparty net-flow wording in counterparty semantics even if effective text mentions profit", () => {
|
||||||
|
const policy = buildPolicy({
|
||||||
|
resolveAddressIntent: () => ({ intent: "unknown", confidence: "low" })
|
||||||
|
});
|
||||||
|
|
||||||
|
const meaning = policy.resolveAssistantTurnMeaning({
|
||||||
|
rawUserMessage:
|
||||||
|
"\u0410 \u0442\u0435\u043f\u0435\u0440\u044c \u043f\u043e \u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a \u0437\u0430 2020: \u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0434\u0435\u043d\u0435\u0433 \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438, \u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438 \u0438 \u043a\u0430\u043a\u043e\u0435 \u043d\u0435\u0442\u0442\u043e?",
|
||||||
|
effectiveAddressUserMessage:
|
||||||
|
"\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\u0443 \u0432\u044b\u043f\u043b\u0430\u0447\u0435\u043d\u043d\u044b\u0445 \u0441\u0440\u0435\u0434\u0441\u0442\u0432 \u0438 \u0447\u0438\u0441\u0442\u0443\u044e \u043f\u0440\u0438\u0431\u044b\u043b\u044c (\u043d\u0435\u0442\u0442\u043e) \u0434\u043b\u044f \u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a \u0437\u0430 2020 \u0433\u043e\u0434."
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(meaning.explicit_intent_candidate).toBeNull();
|
||||||
|
expect(meaning.asked_domain_family).toBe("counterparty_value");
|
||||||
|
expect(meaning.asked_action_family).toBe("net_value_flow");
|
||||||
|
expect(meaning.unsupported_but_understood_family).toBe("counterparty_bidirectional_value_flow_or_netting");
|
||||||
|
expect(meaning.explicit_entity_candidates).toEqual([
|
||||||
|
{
|
||||||
|
type: "counterparty",
|
||||||
|
value: "\u0433\u0440\u0443\u043f\u043f\u0430 \u0441\u0432\u043a",
|
||||||
|
source: "current_turn_loose_entity_tail"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
expect(meaning.reason_codes).toContain("counterparty_bidirectional_value_flow_current_turn_signal");
|
||||||
|
expect(meaning.reason_codes).not.toContain("broad_business_evaluation_current_turn_signal");
|
||||||
|
expect(meaning.stale_replay_forbidden).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("ignores temporal tail words in all-time revenue ranking questions", () => {
|
it("ignores temporal tail words in all-time revenue ranking questions", () => {
|
||||||
const policy = buildPolicy({
|
const policy = buildPolicy({
|
||||||
resolveAddressIntent: (text: string) =>
|
resolveAddressIntent: (text: string) =>
|
||||||
|
|
@ -165,6 +193,55 @@ describe("assistantTurnMeaningPolicy", () => {
|
||||||
expect(meaning.reason_codes).toContain("broad_business_evaluation_current_turn_signal");
|
expect(meaning.reason_codes).toContain("broad_business_evaluation_current_turn_signal");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("treats no-tops compact money wording as display modifier, not counterparty exclusion", () => {
|
||||||
|
const policy = buildPolicy({
|
||||||
|
resolveAddressIntent: () => ({ intent: "customer_revenue_and_payments", confidence: "high" })
|
||||||
|
});
|
||||||
|
|
||||||
|
const meaning = policy.resolveAssistantTurnMeaning({
|
||||||
|
rawUserMessage:
|
||||||
|
"\u0430 \u0435\u0441\u043b\u0438 \u043a\u043e\u0440\u043e\u0442\u043a\u043e, \u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438 \u0434\u0435\u043d\u044c\u0433\u0430\u043c\u0438 \u0431\u0435\u0437 \u0442\u043e\u043f\u043e\u0432?",
|
||||||
|
effectiveAddressUserMessage:
|
||||||
|
"\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0438\u0442\u043e\u0433\u043e\u0432\u044b\u0439 \u0444\u0438\u043d\u0430\u043d\u0441\u043e\u0432\u044b\u0439 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 \u043f\u043e \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438 \u0441 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435\u043c \u043a\u0440\u0443\u043f\u043d\u0435\u0439\u0448\u0438\u0445 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u0439 \u0438\u043b\u0438 \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u043e\u0432."
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(meaning.explicit_intent_candidate).toBeNull();
|
||||||
|
expect(meaning.asked_domain_family).toBe("business_summary");
|
||||||
|
expect(meaning.asked_action_family).toBe("broad_evaluation");
|
||||||
|
expect(meaning.explicit_entity_candidates).toEqual([]);
|
||||||
|
expect(meaning.unsupported_but_understood_family).toBe("broad_business_evaluation");
|
||||||
|
expect(meaning.stale_replay_forbidden).toBe(true);
|
||||||
|
expect(meaning.reason_codes).toContain("compact_cashflow_display_current_turn_signal");
|
||||||
|
expect(meaning.reason_codes).toContain("broad_business_evaluation_current_turn_signal");
|
||||||
|
expect(meaning.reason_codes).not.toContain("counterparty_turnover_current_turn_signal");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
"\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438 \u0434\u0435\u043d\u044c\u0433\u0430\u043c\u0438 \u0431\u0435\u0437 \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u043e\u0432?",
|
||||||
|
"\u0442\u043e\u043b\u044c\u043a\u043e \u0438\u0442\u043e\u0433: \u043f\u0440\u0438\u0448\u043b\u043e, \u0443\u0448\u043b\u043e, \u043d\u0435\u0442\u0442\u043e",
|
||||||
|
"\u0434\u0430\u0439 \u043e\u0434\u043d\u043e\u0439 \u0441\u0442\u0440\u043e\u043a\u043e\u0439 \u0434\u0435\u043d\u044c\u0433\u0438 \u0431\u0435\u0437 \u0440\u0430\u0437\u0431\u0438\u0432\u043a\u0438",
|
||||||
|
"\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438 \u0438 \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438 \u0431\u0435\u0437 \u0434\u0435\u0442\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438"
|
||||||
|
])("treats compact cashflow wording '%s' as business-summary display request", (rawUserMessage) => {
|
||||||
|
const policy = buildPolicy({
|
||||||
|
resolveAddressIntent: () => ({ intent: "customer_revenue_and_payments", confidence: "high" })
|
||||||
|
});
|
||||||
|
|
||||||
|
const meaning = policy.resolveAssistantTurnMeaning({
|
||||||
|
rawUserMessage,
|
||||||
|
effectiveAddressUserMessage:
|
||||||
|
"\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u043e\u0431\u043e\u0440\u043e\u0442 \u0438 \u0432\u044b\u0440\u0443\u0447\u043a\u0443 \u043f\u043e \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438 \u0431\u0435\u0437 \u0440\u0430\u0437\u0440\u0435\u0437\u0430 \u043f\u043e \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430\u043c."
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(meaning.explicit_intent_candidate).toBeNull();
|
||||||
|
expect(meaning.asked_domain_family).toBe("business_summary");
|
||||||
|
expect(meaning.asked_action_family).toBe("broad_evaluation");
|
||||||
|
expect(meaning.explicit_entity_candidates).toEqual([]);
|
||||||
|
expect(meaning.unsupported_but_understood_family).toBe("broad_business_evaluation");
|
||||||
|
expect(meaning.stale_replay_forbidden).toBe(true);
|
||||||
|
expect(meaning.reason_codes).toContain("compact_cashflow_display_current_turn_signal");
|
||||||
|
expect(meaning.reason_codes).not.toContain("counterparty_turnover_current_turn_signal");
|
||||||
|
});
|
||||||
|
|
||||||
it("treats organization-level earnings and best-year wording as business overview", () => {
|
it("treats organization-level earnings and best-year wording as business overview", () => {
|
||||||
const policy = buildPolicy({
|
const policy = buildPolicy({
|
||||||
resolveAddressIntent: () => ({ intent: "customer_revenue_and_payments", confidence: "high" })
|
resolveAddressIntent: () => ({ intent: "customer_revenue_and_payments", confidence: "high" })
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,8 @@ describe("counterparty analytics reply builders", () => {
|
||||||
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
||||||
expect(reply.text).toContain("Оборот по СВК за доступное время: 3.500,00");
|
expect(reply.text).toContain("Оборот по СВК за доступное время: 3.500,00");
|
||||||
expect(reply.text).toContain("по 2 подтвержденным входящим операциям");
|
expect(reply.text).toContain("по 2 подтвержденным входящим операциям");
|
||||||
expect(reply.text).toContain("Это денежный поток от клиента, а не чистая прибыль");
|
expect(reply.text).toContain("Граница ответа: это подтвержденный денежный поток по поступлениям, а не чистая прибыль.");
|
||||||
|
expect(reply.text).toContain("Следующий шаг: могу разложить поток по месяцам, документам или контрагентам.");
|
||||||
expect(reply.text).not.toContain("Самый доходный клиент");
|
expect(reply.text).not.toContain("Самый доходный клиент");
|
||||||
expect(reply.text).not.toContain("Топ-");
|
expect(reply.text).not.toContain("Топ-");
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -225,4 +225,63 @@ describe("routeHintAdapter", () => {
|
||||||
}
|
}
|
||||||
expect(summary.decisions[0]?.route).toBe("store_canonical");
|
expect(summary.decisions[0]?.route).toBe("store_canonical");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("routes counterparty received-paid-net wording to hybrid instead of canonical fact lookup", () => {
|
||||||
|
const summary = toRouteHintSummary({
|
||||||
|
schema_version: "normalized_query_v2_0_2",
|
||||||
|
user_message_raw: "А теперь по Группа СВК за 2020: сколько денег получили, сколько заплатили и какое нетто?",
|
||||||
|
message_in_scope: true,
|
||||||
|
scope_confidence: "high",
|
||||||
|
contains_multiple_tasks: false,
|
||||||
|
fragments: [
|
||||||
|
{
|
||||||
|
fragment_id: "F1",
|
||||||
|
raw_fragment_text: "А теперь по Группа СВК за 2020: сколько денег получили, сколько заплатили и какое нетто?",
|
||||||
|
normalized_fragment_text:
|
||||||
|
"Определить сумму полученных средств, сумму выплаченных средств и чистый остаток (нетто) для контрагента Группа СВК за период 2020 года.",
|
||||||
|
domain_relevance: "in_scope",
|
||||||
|
business_scope: "company_specific_accounting",
|
||||||
|
entity_hints: ["Группа СВК"],
|
||||||
|
account_hints: [],
|
||||||
|
document_hints: [],
|
||||||
|
register_hints: [],
|
||||||
|
time_scope: {
|
||||||
|
type: "explicit",
|
||||||
|
value: "2020",
|
||||||
|
confidence: "high"
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
has_multi_entity_scope: false,
|
||||||
|
asks_for_chain_explanation: false,
|
||||||
|
asks_for_ranking_or_top: false,
|
||||||
|
asks_for_period_summary: false,
|
||||||
|
asks_for_rule_check: false,
|
||||||
|
asks_for_anomaly_scan: false,
|
||||||
|
asks_for_exact_object_trace: false,
|
||||||
|
asks_for_evidence: false,
|
||||||
|
mentions_period_close_context: false
|
||||||
|
},
|
||||||
|
candidate_labels: ["simple_factual"],
|
||||||
|
confidence: "high",
|
||||||
|
execution_readiness: "executable",
|
||||||
|
clarification_reason: null,
|
||||||
|
soft_assumption_used: [],
|
||||||
|
route_status: "routed",
|
||||||
|
no_route_reason: null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
discarded_fragments: [],
|
||||||
|
global_notes: {
|
||||||
|
needs_clarification: false,
|
||||||
|
clarification_reason: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(summary.mode).toBe("deterministic_v2");
|
||||||
|
if (summary.mode !== "deterministic_v2") {
|
||||||
|
throw new Error("Expected deterministic_v2 summary");
|
||||||
|
}
|
||||||
|
expect(summary.decisions[0]?.route).toBe("hybrid_store_plus_live");
|
||||||
|
expect(summary.decisions[0]?.reason).toContain("bidirectional_value_flow");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue