АРЧ АП11 - Архитектура после регресса: + Архитектура: восстановить bridge от provenance выбранной позиции к VAT-периоду и закрыть phase10 replay
This commit is contained in:
parent
9872ef5446
commit
31cb4ccbbb
|
|
@ -193,6 +193,24 @@ Still open after the accepted phase9 replay:
|
||||||
- business answers are now semantically correct on this path, but some inventory list formatting still feels heavier and more mechanical than the target human style;
|
- business answers are now semantically correct on this path, but some inventory list formatting still feels heavier and more mechanical than the target human style;
|
||||||
- the next architecture slice should keep expanding saved-session proof across additional real user chains, while separately tightening answer presentation so exact routes do not feel template-driven even when the truth path is already correct.
|
- the next architecture slice should keep expanding saved-session proof across additional real user chains, while separately tightening answer presentation so exact routes do not feel template-driven even when the truth path is already correct.
|
||||||
|
|
||||||
|
Latest phase10 bridge-and-aggregate evidence after the manual replay recovery:
|
||||||
|
|
||||||
|
- live replay `address_truth_harness_phase10_manual_bridge_and_aggregate_mix_live_20260418_rerun8` is accepted end-to-end with `9/9` steps green;
|
||||||
|
- the previously broken bridge `selected item purchase provenance -> VAT on purchase date` is now explicit instead of implicit:
|
||||||
|
- the continuity layer derives the purchase month from the grounded provenance evidence;
|
||||||
|
- the same session keeps `selected object` continuity instead of collapsing into generic root-only VAT arbitration;
|
||||||
|
- the runtime now routes this follow-up as `vat_liability_confirmed_for_tax_period`, not as `forecast`, `unknown`, or generic clarification;
|
||||||
|
- the same replay also proves that the neighboring aggregate fixes are live on the real assistant path:
|
||||||
|
- top-customer-all-time now returns a direct business answer first;
|
||||||
|
- top-year aggregate now returns a direct business answer first;
|
||||||
|
- very-old-stock now prefers `inventory_aging_by_purchase_date` over a generic inventory snapshot;
|
||||||
|
- this matters architecturally because the seam that used to exist only as ambient monolith behavior is now protected as an explicit carryover contract plus replay-backed acceptance path.
|
||||||
|
|
||||||
|
Still open after the accepted phase10 replay:
|
||||||
|
|
||||||
|
- the user-facing VAT explanation block is now correct and grounded, but some long exact answers still feel heavier than the target human product tone;
|
||||||
|
- the next architecture slice should keep moving from repaired bridge authority into answer-shaping cleanup and broader saved-session replay coverage, not back into isolated wording tweaks.
|
||||||
|
|
||||||
## Ready Signal
|
## Ready Signal
|
||||||
|
|
||||||
The project can leave the current breakpoint when:
|
The project can leave the current breakpoint when:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,240 @@
|
||||||
|
# 12 - Manual Run System Analysis `assistant-stage1-3NilqwT1G2` (2026-04-18)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This note analyzes the manual saved-session run `assistant-stage1-3NilqwT1G2`.
|
||||||
|
|
||||||
|
The goal is not to review isolated prompts.
|
||||||
|
|
||||||
|
The goal is to determine:
|
||||||
|
|
||||||
|
- which failures should improve as orchestration / continuity authority is repaired;
|
||||||
|
- which failures are separate route or capability gaps;
|
||||||
|
- which failures come from answer shaping and presentation rather than from routing.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
Primary artifacts:
|
||||||
|
|
||||||
|
- `llm_normalizer/reports/assistant-stage1-3NilqwT1G2.md`
|
||||||
|
- `llm_normalizer/reports/assistant-stage1-3NilqwT1G2.json`
|
||||||
|
- `llm_normalizer/data/assistant_sessions/assistant-stage1-3NilqwT1G2-SAVED-001.json`
|
||||||
|
|
||||||
|
Relevant architecture context:
|
||||||
|
|
||||||
|
- `10 - regression_breakpoint_analysis_2026-04-17.md`
|
||||||
|
- `11 - continuity_stabilization_plan_2026-04-17.md`
|
||||||
|
- `docs/orchestration/active_domain_contract.json`
|
||||||
|
|
||||||
|
## High-Level Verdict
|
||||||
|
|
||||||
|
This run is bad for more than one reason.
|
||||||
|
|
||||||
|
It is not a single orchestration bug.
|
||||||
|
|
||||||
|
It is also not a pure capability-gap run.
|
||||||
|
|
||||||
|
The failure pattern is mixed:
|
||||||
|
|
||||||
|
1. Some turns fail because the right contour already exists, but orchestration or intent selection does not reach it.
|
||||||
|
2. Some turns fail because extracted neighboring entities no longer share an explicit bridge contract.
|
||||||
|
3. Some turns fail because the needed business capability family is only partially modeled.
|
||||||
|
4. Some turns are technically routed but still feel bad because answer shape and presentation are too template-like.
|
||||||
|
|
||||||
|
## What Still Works
|
||||||
|
|
||||||
|
The following contours are still alive in this run:
|
||||||
|
|
||||||
|
- inventory root snapshot and historical date carryover;
|
||||||
|
- selected-item supplier provenance;
|
||||||
|
- selected-item sale trace;
|
||||||
|
- confirmed receivables snapshot on a date;
|
||||||
|
- VAT confirmed for the carried tax period after a debt-date question;
|
||||||
|
- documents by counterparty;
|
||||||
|
- inventory root return on later turns.
|
||||||
|
|
||||||
|
This matters because it means the runtime foundation is not dead.
|
||||||
|
|
||||||
|
The project did not lose all exact routes.
|
||||||
|
|
||||||
|
The main problem is that the system no longer enters the correct contour reliably under mixed real-session wording.
|
||||||
|
|
||||||
|
## Failure Families
|
||||||
|
|
||||||
|
### A. Contour exists, but the runtime does not enter it
|
||||||
|
|
||||||
|
These are the clearest orchestration / intent-resolution failures.
|
||||||
|
|
||||||
|
#### `Q12` - `прикинь какой ндс нам надо заплатить на февраль 2017`
|
||||||
|
|
||||||
|
- Actual behavior: fell into `living_chat`, `living_chat_response_source = llm_chat`.
|
||||||
|
- Why this is bad: the system already has `vat_liability_confirmed_for_tax_period` as a real contour.
|
||||||
|
- Why this is architecture, not wording only: the right family exists, but the lane authority does not protect it under colloquial phrasing.
|
||||||
|
|
||||||
|
#### `Q16` - `мы должны комуто денег на сегодня?`
|
||||||
|
|
||||||
|
- Actual behavior: `detected_intent = unknown`, partial refusal.
|
||||||
|
- Why this is bad: `payables_confirmed_as_of_date` exists in recipe, capability, filter, compose, and navigation layers.
|
||||||
|
- Why this is architecture: the contour is present, but the resolver / route arbitration fails to bind the phrase to the payables family.
|
||||||
|
|
||||||
|
#### `Q29` - `Есть ли остатки товара, которые закупались очень давно`
|
||||||
|
|
||||||
|
- Actual behavior: routed as `inventory_on_hand_as_of_date`, returned a generic current stock snapshot.
|
||||||
|
- Why this is bad: the active domain contract explicitly contains the old-stock / aging path, and the runtime has `inventory_aging_by_purchase_date`.
|
||||||
|
- Why this is architecture: generic inventory root wins over the more specific aging branch because precedence is wrong.
|
||||||
|
|
||||||
|
### B. Cross-domain bridge disappeared after extraction
|
||||||
|
|
||||||
|
These are the most important "neighbor entities used to live together" failures.
|
||||||
|
|
||||||
|
#### `Q10` - `ндс можешь прикинуть на дату покупки рабочей станции?`
|
||||||
|
|
||||||
|
- Actual behavior: `detected_intent = unknown`, no route, organization only, selected-item / purchase-date context dropped.
|
||||||
|
- What this should have used: selected item -> purchase provenance bundle -> purchase date / period -> VAT contour.
|
||||||
|
- Why this is architecture: the system can answer selected-item provenance and can answer VAT periods, but the bridge between those adjacent contours is not explicit.
|
||||||
|
|
||||||
|
#### `Q17` - `а нам?`
|
||||||
|
|
||||||
|
- Actual behavior: rewritten into a generic "our company / our base" question and lost the debt polarity follow-up.
|
||||||
|
- What this should have used: `payables -> receivables` role-swap follow-up over the same date scope.
|
||||||
|
- Why this is architecture: short polarity pivots need an explicit follow-up contract, not free-form reinterpretation.
|
||||||
|
|
||||||
|
#### `Q23` - `а по свк`
|
||||||
|
|
||||||
|
- Actual behavior: stayed inside `list_documents_by_counterparty`, but the short alias retarget is semantically weak and the answer quality is poor.
|
||||||
|
- Why this is architecture: short retargets over an existing counterparty lane need a dedicated alias / retarget seam.
|
||||||
|
|
||||||
|
### C. Business family partially exists, but not at the right grain
|
||||||
|
|
||||||
|
These are not fixed by continuity alone.
|
||||||
|
|
||||||
|
#### `Q13` - `кто у нас самый доходный клиент за все время`
|
||||||
|
|
||||||
|
- Actual behavior: `detected_intent = unknown`, partial refusal.
|
||||||
|
- What exists nearby: `customer_revenue_and_payments`.
|
||||||
|
- What is missing in practice: stable ranking / all-time aggregate handling for company-level customer revenue questions under colloquial wording.
|
||||||
|
|
||||||
|
#### `Q18` - `какой у нас самый доходный год`
|
||||||
|
|
||||||
|
#### `Q19` - `а за 2017 мы скок заработали?`
|
||||||
|
|
||||||
|
#### `Q20` - `сколько вообще денег мы заработали за все время?`
|
||||||
|
|
||||||
|
- Actual behavior: broad refusal or partial refusal.
|
||||||
|
- Why this matters: these are not random questions; they are business analytics asks at company/year/all-time aggregate grain.
|
||||||
|
- Why this is not only orchestration: current nearby revenue contour is not enough to honestly cover company-year and all-time aggregate semantics.
|
||||||
|
|
||||||
|
### D. Route exists nearby, but wording-to-intent binding regressed
|
||||||
|
|
||||||
|
#### `Q11` - `а какой ндс мы должны сгрузить на март 2020?`
|
||||||
|
|
||||||
|
- Actual behavior: `detected_intent = unknown`.
|
||||||
|
- What exists nearby: VAT confirmed liability contour.
|
||||||
|
- Why this matters: colloquial accounting wording like `сгрузить НДС` should not fall out of contour if the product claims live accountant dialogue.
|
||||||
|
|
||||||
|
#### `Q25` - `что нам отгружать чепурнов? какой товар или услугу?`
|
||||||
|
|
||||||
|
- Actual behavior: partial refusal.
|
||||||
|
- What exists nearby: counterparty documents and related shipment evidence.
|
||||||
|
- Why this matters: the system has adjacent evidence contours but lacks a stable "counterparty -> shipped items/services" interpretation.
|
||||||
|
|
||||||
|
### E. User-facing quality defects even when routing is not fully broken
|
||||||
|
|
||||||
|
#### `Q2` - `расскажи что можешь интересного`
|
||||||
|
|
||||||
|
- The answer is cleaner than before, but still catalog-like and mechanically enumerated.
|
||||||
|
|
||||||
|
#### `Q14` / `Q15`
|
||||||
|
|
||||||
|
- These turns route correctly, but the answer shape still exposes internal "block" thinking and feels less human than the target product tone.
|
||||||
|
|
||||||
|
#### `Q28`
|
||||||
|
|
||||||
|
- The route works, but the result relies on fallback reasoning (`confirmed_balance_unavailable_fallback_to_heuristic_candidates` / `open_items_account_query_override_to_movements`), so business confidence should stay explicit.
|
||||||
|
|
||||||
|
## System Classification
|
||||||
|
|
||||||
|
This run should be read as:
|
||||||
|
|
||||||
|
- `orchestration / continuity authority failures`: `Q10`, `Q12`, `Q16`, `Q17`, `Q23`, `Q29`
|
||||||
|
- `resolver / wording-to-intent failures`: `Q11`, `Q13`, `Q25`
|
||||||
|
- `aggregate business contour gaps`: `Q18`, `Q19`, `Q20`
|
||||||
|
- `answer-shape / presentation defects`: `Q2`, `Q14`, `Q15`, `Q28`
|
||||||
|
|
||||||
|
The important conclusion:
|
||||||
|
|
||||||
|
- a large share of the bad run is still architecture and orchestration;
|
||||||
|
- but not all of it will heal automatically just by continuing continuity fixes.
|
||||||
|
|
||||||
|
## What The Old Monolith Likely Had
|
||||||
|
|
||||||
|
Before extraction, several neighboring decisions lived in one ambient runtime region:
|
||||||
|
|
||||||
|
- intent guess;
|
||||||
|
- follow-up carryover;
|
||||||
|
- selected-object reuse;
|
||||||
|
- organization/date reuse;
|
||||||
|
- recipe selection;
|
||||||
|
- answer packaging.
|
||||||
|
|
||||||
|
That old shape was fragile, but it allowed adjacent capabilities to "borrow" context from one another without explicit contracts.
|
||||||
|
|
||||||
|
After extraction, those same borrow paths became seams.
|
||||||
|
|
||||||
|
The system now needs those seams to be first-class contracts.
|
||||||
|
|
||||||
|
Without that, optimization does not produce a stronger stone.
|
||||||
|
|
||||||
|
It produces split pieces whose joints are filled by ad hoc glue.
|
||||||
|
|
||||||
|
## Architecture Requirement
|
||||||
|
|
||||||
|
The extraction path is still the right direction only if every broken adjacency becomes explicit.
|
||||||
|
|
||||||
|
The missing explicit seams now visible in this run are:
|
||||||
|
|
||||||
|
- `inventory root <-> inventory aging`
|
||||||
|
- `selected item / provenance bundle <-> VAT period ask`
|
||||||
|
- `payables <-> receivables polarity flip`
|
||||||
|
- `counterparty documents <-> short alias retarget`
|
||||||
|
- `counterparty evidence <-> shipped goods/services summary`
|
||||||
|
- `organization scope <-> company analytics aggregates`
|
||||||
|
|
||||||
|
Each seam needs:
|
||||||
|
|
||||||
|
1. an explicit transition / carryover contract;
|
||||||
|
2. one clear intent-family arbitration rule;
|
||||||
|
3. answer-shape expectations for the user-facing reply;
|
||||||
|
4. replay coverage inside a saved-session scenario, not only in isolated unit cases.
|
||||||
|
|
||||||
|
## Final Conclusion
|
||||||
|
|
||||||
|
Yes, the architecture can become better than the old monolith.
|
||||||
|
|
||||||
|
But only under one condition:
|
||||||
|
|
||||||
|
- every extracted neighbor relationship that used to work implicitly must now be reintroduced as an explicit contract and guarded by scenario-level acceptance.
|
||||||
|
|
||||||
|
If that is done, the stone becomes better:
|
||||||
|
|
||||||
|
- less magical;
|
||||||
|
- more inspectable;
|
||||||
|
- safer to expand across domains.
|
||||||
|
|
||||||
|
If that is not done, the stone does not become better.
|
||||||
|
|
||||||
|
It becomes fragmented and cosmetically re-glued.
|
||||||
|
|
||||||
|
## Next Recommended Slice
|
||||||
|
|
||||||
|
The next phase should not be a random patch sweep.
|
||||||
|
|
||||||
|
It should be one focused replay pack built from this exact saved session with priority order:
|
||||||
|
|
||||||
|
1. `inventory -> selected item -> VAT purchase-date bridge`
|
||||||
|
2. `direct VAT colloquial liability wording`
|
||||||
|
3. `payables / receivables polarity swap`
|
||||||
|
4. `inventory aging vs generic inventory precedence`
|
||||||
|
5. `company analytics aggregates`
|
||||||
|
|
||||||
|
That pack should become the next acceptance gate before wider domain expansion.
|
||||||
|
|
@ -0,0 +1,265 @@
|
||||||
|
{
|
||||||
|
"schema_version": "domain_truth_harness_spec_v1",
|
||||||
|
"scenario_id": "address_truth_harness_phase10_manual_bridge_and_aggregate_mix",
|
||||||
|
"domain": "address_phase10_manual_bridge_and_aggregate_mix",
|
||||||
|
"title": "Phase 10 manual bridge and aggregate replay for orchestration recovery",
|
||||||
|
"description": "Focused AGENT replay built from the manual saved session assistant-stage1-3NilqwT1G2. The scenario validates broken seams where neighboring contours used to cooperate implicitly: inventory -> selected item -> VAT purchase-date bridge, colloquial VAT liability wording, payables/receivables polarity flip, company-level revenue aggregates, and inventory aging versus generic stock snapshot precedence.",
|
||||||
|
"bindings": {},
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step_id": "step_01_inventory_historical_anchor",
|
||||||
|
"title": "Historical inventory anchor on March 2016",
|
||||||
|
"question": "остатки на март 2016",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"expected_intents": [
|
||||||
|
"inventory_on_hand_as_of_date"
|
||||||
|
],
|
||||||
|
"expected_recipe": "address_inventory_on_hand_as_of_date_v1",
|
||||||
|
"required_filters": {
|
||||||
|
"as_of_date": "2016-03-31",
|
||||||
|
"period_from": "2016-03-01",
|
||||||
|
"period_to": "2016-03-31"
|
||||||
|
},
|
||||||
|
"required_direct_answer_patterns_any": [
|
||||||
|
"31\\.03\\.2016",
|
||||||
|
"(?i)на складе"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"inventory_root",
|
||||||
|
"historical_date_anchor"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_02_selected_item_purchase_provenance",
|
||||||
|
"title": "Selected workstation supplier / purchase provenance on the same date",
|
||||||
|
"question": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual",
|
||||||
|
"partial_coverage"
|
||||||
|
],
|
||||||
|
"expected_intents": [
|
||||||
|
"inventory_purchase_provenance_for_item"
|
||||||
|
],
|
||||||
|
"expected_recipe": "address_inventory_purchase_provenance_for_item_v1",
|
||||||
|
"required_filters": {
|
||||||
|
"item": "Рабочая станция универсального специалиста (индивидуальное изготовление)",
|
||||||
|
"as_of_date": "{{step_01_inventory_historical_anchor.filters.as_of_date}}"
|
||||||
|
},
|
||||||
|
"required_direct_answer_patterns_any": [
|
||||||
|
"(?i)поставщик|закуп",
|
||||||
|
"(?i)рабочая станция"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"selected_object",
|
||||||
|
"inventory_provenance",
|
||||||
|
"date_scope"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_03_vat_on_purchase_date_bridge",
|
||||||
|
"title": "VAT approximate on the purchase date of the selected item",
|
||||||
|
"question": "ндс можешь прикинуть на дату покупки рабочей станции?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"partial_coverage"
|
||||||
|
],
|
||||||
|
"expected_intents": [
|
||||||
|
"vat_liability_confirmed_for_tax_period"
|
||||||
|
],
|
||||||
|
"required_direct_answer_patterns_any": [
|
||||||
|
"(?i)ндс",
|
||||||
|
"(?i)дата покупки|период|налоговый период|не удается точно"
|
||||||
|
],
|
||||||
|
"forbidden_direct_answer_patterns": [
|
||||||
|
"(?i)mcp",
|
||||||
|
"(?i)address_mode",
|
||||||
|
"(?i)tool_gate_reason"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"bridge_inventory_to_vat",
|
||||||
|
"selected_object",
|
||||||
|
"purchase_date_bundle"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_04_colloquial_vat_tax_period",
|
||||||
|
"title": "Colloquial VAT liability wording on February 2017",
|
||||||
|
"question": "прикинь какой ндс нам надо заплатить на февраль 2017",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"partial_coverage"
|
||||||
|
],
|
||||||
|
"expected_intents": [
|
||||||
|
"vat_liability_confirmed_for_tax_period"
|
||||||
|
],
|
||||||
|
"expected_recipe": "address_vat_liability_confirmed_tax_period_v1",
|
||||||
|
"required_filters": {
|
||||||
|
"period_from": "2017-02-01",
|
||||||
|
"period_to": "2017-02-28"
|
||||||
|
},
|
||||||
|
"required_direct_answer_patterns_any": [
|
||||||
|
"(?i)ндс",
|
||||||
|
"(?i)2017"
|
||||||
|
],
|
||||||
|
"forbidden_direct_answer_patterns": [
|
||||||
|
"(?i)mcp",
|
||||||
|
"(?i)address_mode",
|
||||||
|
"(?i)tool_gate_reason"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"vat_colloquial_wording",
|
||||||
|
"tax_period"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_05_payables_today",
|
||||||
|
"title": "Payables snapshot for today",
|
||||||
|
"question": "мы должны комуто денег на сегодня?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual",
|
||||||
|
"partial_coverage"
|
||||||
|
],
|
||||||
|
"expected_intents": [
|
||||||
|
"payables_confirmed_as_of_date",
|
||||||
|
"list_payables_counterparties"
|
||||||
|
],
|
||||||
|
"required_direct_answer_patterns_any": [
|
||||||
|
"(?i)кредитор|обязатель|долж",
|
||||||
|
"(?i)сегодня|на .*20"
|
||||||
|
],
|
||||||
|
"forbidden_direct_answer_patterns": [
|
||||||
|
"(?i)mcp",
|
||||||
|
"(?i)address_mode",
|
||||||
|
"(?i)tool_gate_reason"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"payables",
|
||||||
|
"debt_polarity"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_06_receivables_role_flip",
|
||||||
|
"title": "Short polarity flip from payables to receivables",
|
||||||
|
"question": "а нам?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual",
|
||||||
|
"partial_coverage"
|
||||||
|
],
|
||||||
|
"expected_intents": [
|
||||||
|
"receivables_confirmed_as_of_date",
|
||||||
|
"list_receivables_counterparties"
|
||||||
|
],
|
||||||
|
"required_direct_answer_patterns_any": [
|
||||||
|
"(?i)дебитор|нам должны|должны нам",
|
||||||
|
"(?i)сегодня|на .*20|не удается точно"
|
||||||
|
],
|
||||||
|
"forbidden_direct_answer_patterns": [
|
||||||
|
"(?i)что относится к вашей компании",
|
||||||
|
"(?i)mcp",
|
||||||
|
"(?i)address_mode",
|
||||||
|
"(?i)tool_gate_reason"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"receivables",
|
||||||
|
"polarity_flip",
|
||||||
|
"followup_short"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_07_top_customer_all_time",
|
||||||
|
"title": "Top customer by all-time revenue",
|
||||||
|
"question": "кто у нас самый доходный клиент за все время",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"partial_coverage",
|
||||||
|
"clarification_required"
|
||||||
|
],
|
||||||
|
"expected_intents": [
|
||||||
|
"customer_revenue_and_payments"
|
||||||
|
],
|
||||||
|
"required_direct_answer_patterns_any": [
|
||||||
|
"(?i)клиент|контрагент|доход|выруч",
|
||||||
|
"(?i)не удается точно|за все время|самый доходный"
|
||||||
|
],
|
||||||
|
"forbidden_direct_answer_patterns": [
|
||||||
|
"(?i)mcp",
|
||||||
|
"(?i)address_mode",
|
||||||
|
"(?i)tool_gate_reason"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"customer_analytics",
|
||||||
|
"aggregate_all_time",
|
||||||
|
"ranking"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_08_top_year_revenue",
|
||||||
|
"title": "Most profitable year",
|
||||||
|
"question": "какой у нас самый доходный год",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"partial_coverage",
|
||||||
|
"clarification_required"
|
||||||
|
],
|
||||||
|
"expected_intents": [
|
||||||
|
"customer_revenue_and_payments"
|
||||||
|
],
|
||||||
|
"required_direct_answer_patterns_any": [
|
||||||
|
"(?i)год|доход|выруч|прибыл",
|
||||||
|
"(?i)не удается точно|самый доходный"
|
||||||
|
],
|
||||||
|
"forbidden_direct_answer_patterns": [
|
||||||
|
"(?i)mcp",
|
||||||
|
"(?i)address_mode",
|
||||||
|
"(?i)tool_gate_reason"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"company_analytics",
|
||||||
|
"aggregate_year",
|
||||||
|
"ranking"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_09_very_old_stock",
|
||||||
|
"title": "Very old stock should prefer aging contour over generic inventory snapshot",
|
||||||
|
"question": "Есть ли остатки товара, которые закупались очень давно",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"partial_coverage"
|
||||||
|
],
|
||||||
|
"expected_intents": [
|
||||||
|
"inventory_aging_by_purchase_date"
|
||||||
|
],
|
||||||
|
"expected_recipe": "address_inventory_aging_by_purchase_date_v1",
|
||||||
|
"required_direct_answer_patterns_any": [
|
||||||
|
"(?i)стар|давно|закуп",
|
||||||
|
"(?i)остат"
|
||||||
|
],
|
||||||
|
"forbidden_direct_answer_patterns": [
|
||||||
|
"(?i)18\\.04\\.2026",
|
||||||
|
"(?i)на складе подтверждено \\d+ позиц"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"inventory_aging",
|
||||||
|
"very_old_stock",
|
||||||
|
"precedence"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -1598,7 +1598,11 @@ function extractAddressFilters(userMessage, intent) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (intent === "vat_liability_confirmed_for_tax_period" && !periodRange.period_from && !periodRange.period_to) {
|
const monthPeriodWasDerived = warnings.includes("period_derived_from_month_phrase");
|
||||||
|
if (intent === "vat_liability_confirmed_for_tax_period" &&
|
||||||
|
!periodRange.period_from &&
|
||||||
|
!periodRange.period_to &&
|
||||||
|
!monthPeriodWasDerived) {
|
||||||
const periodToForQuarter = filters.period_to ?? vatAsOfDate ?? null;
|
const periodToForQuarter = filters.period_to ?? vatAsOfDate ?? null;
|
||||||
if (periodToForQuarter) {
|
if (periodToForQuarter) {
|
||||||
const quarterWindow = deriveQuarterWindowForDate(periodToForQuarter);
|
const quarterWindow = deriveQuarterWindowForDate(periodToForQuarter);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.hasCompactAccountCodeToken = hasCompactAccountCodeToken;
|
||||||
|
exports.hasAccountNumberAnchor = hasAccountNumberAnchor;
|
||||||
exports.resolveAddressIntent = resolveAddressIntent;
|
exports.resolveAddressIntent = resolveAddressIntent;
|
||||||
const addressCounterpartyIntentSignals_1 = require("./addressCounterpartyIntentSignals");
|
const addressCounterpartyIntentSignals_1 = require("./addressCounterpartyIntentSignals");
|
||||||
const addressInventoryIntentSignals_1 = require("./addressInventoryIntentSignals");
|
const addressInventoryIntentSignals_1 = require("./addressInventoryIntentSignals");
|
||||||
|
|
@ -1449,11 +1451,21 @@ function hasCustomerRevenueRankingBridgeSignal(text) {
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
const hasSupplierCue = /(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0432\u0435\u043d\u0434\u043e\u0440|supplier|vendor)/iu.test(normalized);
|
||||||
|
if (hasSupplierCue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasDirectRankingCue = /(?:\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0434\u043e\u0445\u043e\u0434\u043d(?:\u044b\u0439|\u0430\u044f|\u043e\u0435|\u044b\u0435|\u043e\u0433\u043e|\u043e\u043c\u0443|\u044b\u043c|\u044b\u0445)?\s+(?:\u043a\u043b\u0438\u0435\u043d\u0442|customer|client)|(?:\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0430\u044f)\s+(?:\u0443\s+\u043d\u0430\u0441\s+)?\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0434\u043e\u0445\u043e\u0434\u043d(?:\u044b\u0439|\u0430\u044f|\u043e\u0435|\u044b\u0435|\u043e\u0433\u043e|\u043e\u043c\u0443|\u044b\u043c|\u044b\u0445)?\s+\u0433\u043e\u0434|(?:\u0430\s+)?(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a)\s+(?:\u0432\u043e\u043e\u0431\u0449\u0435\s+)?(?:\u0434\u0435\u043d\u0435\u0433\s+)?\u043c\u044b\s+\u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438(?:\s+\u0437\u0430\s+\u0432\u0441\u0435\s+\u0432\u0440\u0435\u043c\u044f)?|(?:\u0430\s+)?(?:\u0437\u0430|for)\s+\d{4}\s+\u043c\u044b\s+(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a)\s+\u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438|(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a)\s+\u043c\u044b\s+\u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438\s+\u0437\u0430\s+\d{4}|(?:\u043e\u0431\u0449\u0430\u044f\s+)?\u0432\u044b\u0440\u0443\u0447\u043a\u0430\s+\u0437\u0430\s+\d{4})/iu.test(normalized);
|
||||||
|
if (hasDirectRankingCue) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const hasMoneyCue = /(?:\u0434\u0435\u043d\u044c\u0433|\u0434\u0435\u043d\u0435\u0433|\u0432\u044b\u0440\u0443\u0447|\u0434\u043e\u0445\u043e\u0434|\u043e\u0431\u043e\u0440\u043e\u0442|revenue|turnover|money|inflow)/iu.test(normalized);
|
const hasMoneyCue = /(?:\u0434\u0435\u043d\u044c\u0433|\u0434\u0435\u043d\u0435\u0433|\u0432\u044b\u0440\u0443\u0447|\u0434\u043e\u0445\u043e\u0434|\u043e\u0431\u043e\u0440\u043e\u0442|revenue|turnover|money|inflow)/iu.test(normalized);
|
||||||
if (!hasMoneyCue) {
|
if (!hasMoneyCue) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return /(?:\u043a\u0442\u043e\s+(?:\u043d\u0430\u043c\s+)?(?:\u0431\u043e\u043b\u044c\u0448\u0435(?:\s+\u0432\u0441\u0435\u0433\u043e)?\s+\u043f\u0440\u0438\u043d\u0435\u0441(?:\s+\u0434\u0435\u043d\u0435\u0433)?|\u043f\u0440\u0438\u043d\u0435\u0441\s+\u0431\u043e\u043b\u044c\u0448\u0435(?:\s+\u0432\u0441\u0435\u0433\u043e)?\s+\u0434\u0435\u043d\u0435\u0433)|who\s+brought\s+(?:us\s+)?(?:the\s+)?most\s+money)/iu.test(normalized);
|
const hasCustomerRankingCue = /(?:\u043a\u0442\u043e\s+(?:\u043d\u0430\u043c\s+)?(?:\u0431\u043e\u043b\u044c\u0448\u0435(?:\s+\u0432\u0441\u0435\u0433\u043e)?\s+\u043f\u0440\u0438\u043d\u0435\u0441(?:\s+\u0434\u0435\u043d\u0435\u0433)?|\u043f\u0440\u0438\u043d\u0435\u0441\s+\u0431\u043e\u043b\u044c\u0448\u0435(?:\s+\u0432\u0441\u0435\u0433\u043e)?\s+\u0434\u0435\u043d\u0435\u0433)|who\s+brought\s+(?:us\s+)?(?:the\s+)?most\s+money|(?:\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0434\u043e\u0445\u043e\u0434\u043d\w*\s+(?:\u043a\u043b\u0438\u0435\u043d\u0442|customer|client)))/iu.test(normalized);
|
||||||
|
const hasRevenueAggregateCue = /(?:(?:\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0430\u044f)\s+(?:\u0443\s+\u043d\u0430\u0441\s+)?\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0434\u043e\u0445\u043e\u0434\u043d\w*\s+\u0433\u043e\u0434|(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|скок)\s+(?:\u0432\u043e\u043e\u0431\u0449\u0435\s+)?(?:\u0434\u0435\u043d\u0435\u0433\s+)?\u043c\u044b\s+\u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438|(?:\u0437\u0430|for)\s+\d{4}\s+\u043c\u044b\s+(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|скок)\s+\u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438|\u0432\u044b\u0440\u0443\u0447\u043a\w*\s+\u0437\u0430\s+\d{4})/iu.test(normalized);
|
||||||
|
return hasCustomerRankingCue || hasRevenueAggregateCue;
|
||||||
}
|
}
|
||||||
function hasInventoryProvenanceBridgeSignal(text) {
|
function hasInventoryProvenanceBridgeSignal(text) {
|
||||||
const normalized = String(text ?? "").trim().toLowerCase();
|
const normalized = String(text ?? "").trim().toLowerCase();
|
||||||
|
|
@ -1481,6 +1493,10 @@ function hasColloquialInventoryOnHandBridgeSignal(text) {
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
const hasInventoryAgingCue = /(?:\u043e\u0447\u0435\u043d\u044c\s+\u0434\u0430\u0432\u043d\u043e|\u0434\u0430\u0432\u043d\u043e\s+\u043a\u0443\u043f\u043b|\u0434\u0430\u0432\u043d\u043e\s+\u043f\u0440\u0438\u043e\u0431\u0440\u0435\u0442|\u0441\u0442\u0430\u0440(?:\u044b\u0435|\u044b\u043c|\u044b\u0445)?\s+\u0437\u0430\u043a\u0443\u043f|\u0441\u0442\u0430\u0440\u044b\u0439\s+\u0442\u043e\u0432\u0430\u0440|old\s+stock|old\s+purchase|aging\s+by\s+purchase\s+date)/iu.test(normalized);
|
||||||
|
if (hasInventoryAgingCue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const tokenCount = normalized.split(/\s+/u).filter(Boolean).length;
|
const tokenCount = normalized.split(/\s+/u).filter(Boolean).length;
|
||||||
const hasWarehouseCue = /(?:\u0441\u043a\u043b\u0430\u0434(?:\u0430\u0445|\u0435|\u0443|\u043e\u043c|\u044b)?|\u043e\u0441\u0442\u0430\u0442|warehouse|stock|inventory)/iu.test(normalized);
|
const hasWarehouseCue = /(?:\u0441\u043a\u043b\u0430\u0434(?:\u0430\u0445|\u0435|\u0443|\u043e\u043c|\u044b)?|\u043e\u0441\u0442\u0430\u0442|warehouse|stock|inventory)/iu.test(normalized);
|
||||||
if (!hasWarehouseCue) {
|
if (!hasWarehouseCue) {
|
||||||
|
|
@ -1489,15 +1505,74 @@ function hasColloquialInventoryOnHandBridgeSignal(text) {
|
||||||
const hasQuestionCue = /(?:\u0447\u0442\u043e|\u0447\u0435|\u0447\u0451|\u043a\u0430\u043a\u0438\u0435|\u043f\u043e\u043a\u0430\u0436\u0438|\u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c|show|list|what)/iu.test(normalized);
|
const hasQuestionCue = /(?:\u0447\u0442\u043e|\u0447\u0435|\u0447\u0451|\u043a\u0430\u043a\u0438\u0435|\u043f\u043e\u043a\u0430\u0436\u0438|\u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c|show|list|what)/iu.test(normalized);
|
||||||
return hasQuestionCue && tokenCount <= 8;
|
return hasQuestionCue && tokenCount <= 8;
|
||||||
}
|
}
|
||||||
|
function repairLikelyUtf8Mojibake(text) {
|
||||||
|
const raw = String(text ?? "");
|
||||||
|
if (!raw) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const repaired = Buffer.from(raw, "latin1").toString("utf8");
|
||||||
|
return repaired || raw;
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
function resolveAddressIntent(userMessage) {
|
function resolveAddressIntent(userMessage) {
|
||||||
const text = String(userMessage ?? "").trim().toLowerCase();
|
const text = String(userMessage ?? "").trim().toLowerCase();
|
||||||
if (hasCustomerRevenueRankingBridgeSignal(text)) {
|
const repairedText = repairLikelyUtf8Mojibake(text).trim().toLowerCase();
|
||||||
|
const bridgeText = repairedText && repairedText !== text ? `${text} ${repairedText}` : text;
|
||||||
|
const hasLooseVatPayableBridge = /(?:\u043d\u0434\u0441|vat)/iu.test(text) &&
|
||||||
|
/(?:\u043a\u0430\u043a\u043e\u0439\s+\u043d\u0434\u0441\s+(?:\u043d\u0430\u043c\s+)?(?:\u043d\u0430\u0434\u043e|\u043d\u0443\u0436\u043d\u043e)|\u043d\u0430\u043c\s+\u043d\u0430\u0434\u043e\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|\u043d\u0430\u043c\s+\u043d\u0443\u0436\u043d\u043e\s+\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u043d\u0434\u0441\s+\u043a\s+\u0443\u043f\u043b\u0430\u0442\u0435)/iu.test(text) &&
|
||||||
|
/(?:\u0437\u0430\s+(?:\d{4}|(?:\u044f\u043d\u0432\u0430\u0440|\u0444\u0435\u0432\u0440\u0430\u043b|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d|\u0438\u044e\u043b|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440|\u043e\u043a\u0442\u044f\u0431\u0440|\u043d\u043e\u044f\u0431\u0440|\u0434\u0435\u043a\u0430\u0431\u0440)\S*(?:\s+(?:19|20)\d{2})?)|\u043d\u0430\s+(?:\u044f\u043d\u0432\u0430\u0440|\u0444\u0435\u0432\u0440\u0430\u043b|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d|\u0438\u044e\u043b|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440|\u043e\u043a\u0442\u044f\u0431\u0440|\u043d\u043e\u044f\u0431\u0440|\u0434\u0435\u043a\u0430\u0431\u0440)\S*(?:\s+(?:19|20)\d{2})?|\b[1-4]\s*(?:\u043a\u0432\u0430\u0440\u0442\u0430\u043b|\u043a\u0432\.?)\b)/iu.test(text);
|
||||||
|
if (hasLooseVatPayableBridge) {
|
||||||
|
return {
|
||||||
|
intent: "vat_liability_confirmed_for_tax_period",
|
||||||
|
confidence: "high",
|
||||||
|
reasons: ["vat_liability_colloquial_bridge_signal_detected"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const hasExplicitReceivablesSnapshotBridge = /(?:\u043d\u0430\u043c\s+\u043a\u0442\u043e-\u0442\u043e\s+\u0434\u043e\u043b\u0436\u0435\u043d|\u043d\u0430\u043c\s+\u043a\u0442\u043e\s+\u0434\u043e\u043b\u0436\u0435\u043d|\u043a\u0442\u043e\s+\u043d\u0430\u043c\s+\u0434\u043e\u043b\u0436\u0435\u043d|\u0435\u0441\u0442\u044c\s+\u043b\u0438\s+\u0434\u0435\u0431\u0438\u0442\u043e\u0440\u0441\u043a\w+\s+\u0437\u0430\u0434\u043e\u043b\u0436\u0435\u043d\u043d\u043e\u0441\u0442\u044c|\u0437\u0430\u0434\u043e\u043b\u0436\u0435\u043d\u043d\u043e\u0441\u0442\u044c\s+\u043d\u0430\u043c|receivables?)/iu.test(text);
|
||||||
|
if (hasExplicitReceivablesSnapshotBridge) {
|
||||||
|
return {
|
||||||
|
intent: "receivables_confirmed_as_of_date",
|
||||||
|
confidence: "high",
|
||||||
|
reasons: ["receivables_snapshot_bridge_signal_detected"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const hasExplicitPayablesSnapshotBridge = /(?:\u043c\u044b\s+\u0434\u043e\u043b\u0436\u043d\u044b\s+\u043a\u043e\u043c\u0443-\u0442\u043e\s+\u0434\u0435\u043d\u0435\u0433|\u043c\u044b\s+\u0434\u043e\u043b\u0436\u043d\u044b\s+\u043a\u043e\u043c\u0443\u0442\u043e\s+\u0434\u0435\u043d\u0435\u0433|\u043c\u044b\s+\u043a\u043e\u043c\u0443-\u0442\u043e\s+\u0434\u043e\u043b\u0436\u043d\u044b|\u043a\u043e\u043c\u0443\s+\u043c\u044b\s+\u0434\u043e\u043b\u0436\u043d\u044b|\u0435\u0441\u0442\u044c\s+\u043b\u0438\s+\u0437\u0430\u0434\u043e\u043b\u0436\u0435\u043d\u043d\u043e\u0441\u0442\u044c\s+\u043f\u0435\u0440\u0435\u0434\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430\u043c\u0438|\u0437\u0430\u0434\u043e\u043b\u0436\u0435\u043d\u043d\u043e\u0441\u0442\u044c\s+\u043f\u0435\u0440\u0435\u0434\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430\u043c\u0438|payables?)/iu.test(text);
|
||||||
|
if (hasExplicitPayablesSnapshotBridge) {
|
||||||
|
return {
|
||||||
|
intent: "payables_confirmed_as_of_date",
|
||||||
|
confidence: "high",
|
||||||
|
reasons: ["payables_snapshot_bridge_signal_detected"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const hasDirectInventoryAgingBridge = /(?:\u043e\u0447\u0435\u043d\u044c\s+\u0434\u0430\u0432\u043d\u043e|\u0434\u0430\u0432\u043d\u043e\s+\u043a\u0443\u043f\u043b|\u0434\u0430\u0432\u043d\u043e\s+\u043f\u0440\u0438\u043e\u0431\u0440\u0435\u0442|\u0441\u0442\u0430\u0440(?:\u044b\u0435|\u044b\u043c|\u044b\u0445)?\s+\u0437\u0430\u043a\u0443\u043f|\u0441\u0442\u0430\u0440(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0442\u043e\u0432\u0430\u0440|old\s+stock|old\s+purchase|very\s+old\s+stock|aging\s+by\s+purchase\s+date)/iu.test(bridgeText);
|
||||||
|
if (hasDirectInventoryAgingBridge || hasInventoryAgingSignal(text) || hasInventoryAgingSignal(repairedText)) {
|
||||||
|
return {
|
||||||
|
intent: "inventory_aging_by_purchase_date",
|
||||||
|
confidence: "high",
|
||||||
|
reasons: ["inventory_aging_bridge_signal_detected"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const hasDirectRevenueAggregateBridge = /(?:\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0434\u043e\u0445\u043e\u0434\u043d(?:\u044b\u0439|\u0430\u044f|\u043e\u0435|\u044b\u0435|\u043e\u0433\u043e|\u043e\u043c\u0443|\u044b\u043c|\u044b\u0445)?\s+\u043a\u043b\u0438\u0435\u043d\u0442|(?:\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0430\u044f)\s+(?:\u0443\s+\u043d\u0430\u0441\s+)?\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0434\u043e\u0445\u043e\u0434\u043d(?:\u044b\u0439|\u0430\u044f|\u043e\u0435|\u044b\u0435|\u043e\u0433\u043e|\u043e\u043c\u0443|\u044b\u043c|\u044b\u0445)?\s+\u0433\u043e\u0434|(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a).*(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438|\u0432\u044b\u0440\u0443\u0447\u0438\u043b\u0438)|\u0432\u044b\u0440\u0443\u0447\u043a\u0430\s+\u0437\u0430\s+\d{4})/iu.test(bridgeText);
|
||||||
|
if (hasDirectRevenueAggregateBridge || hasCustomerRevenueRankingBridgeSignal(bridgeText)) {
|
||||||
return {
|
return {
|
||||||
intent: "customer_revenue_and_payments",
|
intent: "customer_revenue_and_payments",
|
||||||
confidence: "medium",
|
confidence: "medium",
|
||||||
reasons: ["customer_revenue_ranking_bridge_signal_detected"]
|
reasons: ["customer_revenue_ranking_bridge_signal_detected"]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const hasHistoricalInventorySnapshotBridge = [text, repairedText, bridgeText].some((sample) => /(?:\u043e\u0441\u0442\u0430\u0442|inventory|stock|\u0441\u043a\u043b\u0430\u0434|остат|склад)/iu.test(sample) &&
|
||||||
|
/(?:(?:\u043d\u0430|\u0437\u0430|на|за)\s+(?:\u044f\u043d\u0432\u0430\u0440|\u0444\u0435\u0432\u0440\u0430\u043b|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d|\u0438\u044e\u043b|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440|\u043e\u043a\u0442\u044f\u0431\u0440|\u043d\u043e\u044f\u0431\u0440|\u0434\u0435\u043a\u0430\u0431\u0440|март|апрел|май|июн|июл|август|сентябр|октябр|ноябр|декабр)\S*(?:\s+(?:19|20)\d{2})?|(?:\u043d\u0430|\u0437\u0430|на|за)\s+\d{4}|\b(?:19|20)\d{2}\b)/iu.test(sample));
|
||||||
|
if (hasHistoricalInventorySnapshotBridge) {
|
||||||
|
return {
|
||||||
|
intent: "inventory_on_hand_as_of_date",
|
||||||
|
confidence: "medium",
|
||||||
|
reasons: ["inventory_historical_snapshot_bridge_signal_detected"]
|
||||||
|
};
|
||||||
|
}
|
||||||
if (hasInventoryDocumentaryChainBridgeSignal(text)) {
|
if (hasInventoryDocumentaryChainBridgeSignal(text)) {
|
||||||
return {
|
return {
|
||||||
intent: "inventory_purchase_to_sale_chain",
|
intent: "inventory_purchase_to_sale_chain",
|
||||||
|
|
|
||||||
|
|
@ -487,14 +487,19 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
||||||
}
|
}
|
||||||
if (focus === "top_years_by_total") {
|
if (focus === "top_years_by_total") {
|
||||||
const visible = rankedByYearTotal.slice(0, limit);
|
const visible = rankedByYearTotal.slice(0, limit);
|
||||||
const heading = isSupplier
|
|
||||||
? `Топ-${visible.length} лет по сумме выплат:`
|
|
||||||
: `Топ-${visible.length} лет по сумме поступлений:`;
|
|
||||||
lines.unshift(heading);
|
|
||||||
if (visible.length === 0) {
|
if (visible.length === 0) {
|
||||||
lines.push("По доступному окну не удалось собрать годовые агрегаты по суммам.");
|
lines.push("По доступному окну не удалось собрать годовые агрегаты по суммам.");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
const strongestYear = visible[0];
|
||||||
|
const directAnswerLine = isSupplier
|
||||||
|
? `Самый крупный год по подтвержденным выплатам: ${strongestYear.year} (${deps.formatMoneyRub(strongestYear.total)} по ${strongestYear.ops} операциям).`
|
||||||
|
: `Самый доходный год по подтвержденным поступлениям: ${strongestYear.year} (${deps.formatMoneyRub(strongestYear.total)} по ${strongestYear.ops} операциям). Это денежный поток, а не чистая прибыль.`;
|
||||||
|
const heading = isSupplier
|
||||||
|
? `Топ-${visible.length} лет по сумме выплат:`
|
||||||
|
: `Топ-${visible.length} лет по сумме поступлений:`;
|
||||||
|
lines.unshift(heading);
|
||||||
|
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)}`));
|
||||||
}
|
}
|
||||||
return (0, replyContracts_1.buildFactualListReply)(lines);
|
return (0, replyContracts_1.buildFactualListReply)(lines);
|
||||||
|
|
@ -556,7 +561,14 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
||||||
const heading = isSupplier
|
const heading = isSupplier
|
||||||
? `Топ-${visible.length} поставщиков по сумме выплат:`
|
? `Топ-${visible.length} поставщиков по сумме выплат:`
|
||||||
: `Топ-${visible.length} заказчиков по сумме поступлений:`;
|
: `Топ-${visible.length} заказчиков по сумме поступлений:`;
|
||||||
|
const leadingCounterparty = visible[0] ?? null;
|
||||||
lines.unshift(heading);
|
lines.unshift(heading);
|
||||||
|
if (leadingCounterparty) {
|
||||||
|
const directAnswerLine = isSupplier
|
||||||
|
? `Крупнейший поставщик по подтвержденным выплатам за доступное время: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
||||||
|
: `Самый доходный клиент за доступное время по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это денежный поток, а не чистая прибыль.`;
|
||||||
|
lines.unshift(directAnswerLine);
|
||||||
|
}
|
||||||
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;
|
||||||
return `${index + 1}. ${item.name} | сумма: ${deps.formatMoneyRub(item.total)} | операций: ${item.ops} | средний чек: ${deps.formatMoneyRub(avgCheck)} | максимальная разовая сумма: ${deps.formatMoneyRub(item.maxSingle)}`;
|
return `${index + 1}. ${item.name} | сумма: ${deps.formatMoneyRub(item.total)} | операций: ${item.ops} | средний чек: ${deps.formatMoneyRub(avgCheck)} | максимальная разовая сумма: ${deps.formatMoneyRub(item.maxSingle)}`;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ exports.hasInventoryPurchaseDateFollowupCue = hasInventoryPurchaseDateFollowupCu
|
||||||
exports.hasBareInventoryPurchaseDateFollowupCue = hasBareInventoryPurchaseDateFollowupCue;
|
exports.hasBareInventoryPurchaseDateFollowupCue = hasBareInventoryPurchaseDateFollowupCue;
|
||||||
exports.hasInventorySaleFollowupCue = hasInventorySaleFollowupCue;
|
exports.hasInventorySaleFollowupCue = hasInventorySaleFollowupCue;
|
||||||
exports.hasInventoryPurchaseToSaleChainFollowupCue = hasInventoryPurchaseToSaleChainFollowupCue;
|
exports.hasInventoryPurchaseToSaleChainFollowupCue = hasInventoryPurchaseToSaleChainFollowupCue;
|
||||||
|
exports.hasInventoryPurchaseDateVatBridgeCue = hasInventoryPurchaseDateVatBridgeCue;
|
||||||
exports.hasAddressFollowupContextSignal = hasAddressFollowupContextSignal;
|
exports.hasAddressFollowupContextSignal = hasAddressFollowupContextSignal;
|
||||||
exports.runAddressDecomposeStage = runAddressDecomposeStage;
|
exports.runAddressDecomposeStage = runAddressDecomposeStage;
|
||||||
const addressQueryClassifier_1 = require("../addressQueryClassifier");
|
const addressQueryClassifier_1 = require("../addressQueryClassifier");
|
||||||
|
|
@ -537,6 +538,14 @@ function hasInventorySaleFollowupCue(text) {
|
||||||
function hasInventoryPurchaseToSaleChainFollowupCue(text) {
|
function hasInventoryPurchaseToSaleChainFollowupCue(text) {
|
||||||
return /(?:через\s+какие\s+документы\s+прош[её]л\s+путь\s+товара|закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*(?:warehouse|stock)\s*->\s*sale)/iu.test(String(text ?? ""));
|
return /(?:через\s+какие\s+документы\s+прош[её]л\s+путь\s+товара|закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*(?:warehouse|stock)\s*->\s*sale)/iu.test(String(text ?? ""));
|
||||||
}
|
}
|
||||||
|
function hasInventoryPurchaseDateVatBridgeCue(text) {
|
||||||
|
const normalized = String(text ?? "").trim();
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (/(?:ндс|vat)/iu.test(normalized) &&
|
||||||
|
/(?:на\s+дат[ауеы]\s+покупк|на\s+дат[ауеы]\s+закупк|по\s+дат[еу]\s+покупк|по\s+дат[еу]\s+закупк|дата\s+покупк|дата\s+закупк|purchase\s+date)/iu.test(normalized));
|
||||||
|
}
|
||||||
function hasAddressFollowupContextSignal(text) {
|
function hasAddressFollowupContextSignal(text) {
|
||||||
const normalized = String(text ?? "").trim();
|
const normalized = String(text ?? "").trim();
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
|
|
@ -563,6 +572,9 @@ function hasAddressFollowupContextSignal(text) {
|
||||||
/(?:ндс|vat|прогноз|к\s+уплате|нул|ноль|\b0(?:[.,]0+)?\b)/iu.test(normalized)) {
|
/(?:ндс|vat|прогноз|к\s+уплате|нул|ноль|\b0(?:[.,]0+)?\b)/iu.test(normalized)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (tokenCount <= 14 && hasInventoryPurchaseDateVatBridgeCue(normalized)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const hasPeriodLiteral = /\b(?:19|20)\d{2}(?:[./-](?:0?[1-9]|1[0-2]))?\b/.test(normalized);
|
const hasPeriodLiteral = /\b(?:19|20)\d{2}(?:[./-](?:0?[1-9]|1[0-2]))?\b/.test(normalized);
|
||||||
if (tokenCount <= 8 && hasPeriodLiteral) {
|
if (tokenCount <= 8 && hasPeriodLiteral) {
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -1056,6 +1068,17 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
||||||
const isVatFollowup = hasVatCue(normalizedMessage);
|
const isVatFollowup = hasVatCue(normalizedMessage);
|
||||||
const previousIsInventoryFamily = isInventoryIntent(sourceIntent ?? undefined);
|
const previousIsInventoryFamily = isInventoryIntent(sourceIntent ?? undefined);
|
||||||
const inventorySelectedObjectFollowup = hasSelectedObjectInventorySignal(normalizedMessage) || (previousIsInventoryFamily && hasFollowupSignal);
|
const inventorySelectedObjectFollowup = hasSelectedObjectInventorySignal(normalizedMessage) || (previousIsInventoryFamily && hasFollowupSignal);
|
||||||
|
const inventoryPurchaseDateVatBridge = inventorySelectedObjectFollowup && hasInventoryPurchaseDateVatBridgeCue(normalizedMessage);
|
||||||
|
if (inventoryPurchaseDateVatBridge &&
|
||||||
|
(detectedIntent.intent === "unknown" ||
|
||||||
|
detectedIntent.intent === sourceIntent ||
|
||||||
|
detectedIntent.intent === "vat_payable_confirmed_as_of_date")) {
|
||||||
|
return {
|
||||||
|
intent: "vat_liability_confirmed_for_tax_period",
|
||||||
|
confidence: "low",
|
||||||
|
reasons: [...detectedIntent.reasons, "intent_adjusted_to_inventory_purchase_date_vat_bridge"]
|
||||||
|
};
|
||||||
|
}
|
||||||
if (detectedIntent.intent === "unknown" && isVatFollowup) {
|
if (detectedIntent.intent === "unknown" && isVatFollowup) {
|
||||||
const vatIntent = hasVatTaxPaymentCue(normalizedMessage)
|
const vatIntent = hasVatTaxPaymentCue(normalizedMessage)
|
||||||
? "vat_liability_confirmed_for_tax_period"
|
? "vat_liability_confirmed_for_tax_period"
|
||||||
|
|
|
||||||
|
|
@ -209,8 +209,8 @@ function composeInventoryReply(intent, rows, options, deps) {
|
||||||
.map((item) => `${item.item} (${deps.inventoryTraceDateLabel(item.firstPurchasePeriod)})`)
|
.map((item) => `${item.item} (${deps.inventoryTraceDateLabel(item.firstPurchasePeriod)})`)
|
||||||
.join("; ");
|
.join("; ");
|
||||||
const directAnswerLine = agingItems.length > 0
|
const directAnswerLine = agingItems.length > 0
|
||||||
? `К старым закупкам на ${deps.formatDateRu(asOfDate)} в первую очередь относятся позиции с самой ранней первой закупкой: ${oldestAnswerPreview}.`
|
? `К самым старым закупкам в текущем подтвержденном срезе относятся позиции с самой ранней первой закупкой: ${oldestAnswerPreview}.`
|
||||||
: `По доступному закупочному следу на ${deps.formatDateRu(asOfDate)} позиции старых закупок не материализованы.`;
|
: "По доступному закупочному следу позиции со старыми закупками не материализованы.";
|
||||||
const lines = [directAnswerLine];
|
const lines = [directAnswerLine];
|
||||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Сводка:", [
|
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Сводка:", [
|
||||||
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||||
|
|
|
||||||
|
|
@ -683,6 +683,7 @@ function createAssistantRoutePolicy(deps) {
|
||||||
const deepSessionContinuationFallbackToDeep = Boolean(!followupContext &&
|
const deepSessionContinuationFallbackToDeep = Boolean(!followupContext &&
|
||||||
baseToolGate?.runAddressLane &&
|
baseToolGate?.runAddressLane &&
|
||||||
!llmRuntimeUnavailableDetected &&
|
!llmRuntimeUnavailableDetected &&
|
||||||
|
!protectAddressLaneFromFallback &&
|
||||||
hasDeepSessionContinuationSignal({
|
hasDeepSessionContinuationSignal({
|
||||||
rawUserMessage,
|
rawUserMessage,
|
||||||
repairedRawUserMessage,
|
repairedRawUserMessage,
|
||||||
|
|
|
||||||
|
|
@ -2642,6 +2642,9 @@ function hasAddressFollowupContextSignal(userMessage) {
|
||||||
!hasAny(/(?:по\s+этому|по\s+тому|по\s+нему|по\s+ней|по\s+ним)/iu)) {
|
!hasAny(/(?:по\s+этому|по\s+тому|по\s+нему|по\s+ней|по\s+ним)/iu)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (shortFollowup && samples.some((sample) => (0, decomposeStage_1.hasInventoryPurchaseDateVatBridgeCue)(sample))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (shortFollowup && samples.some((sample) => hasPeriodLiteral(sample))) {
|
if (shortFollowup && samples.some((sample) => hasPeriodLiteral(sample))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -3458,6 +3461,31 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
|
||||||
semanticHints: candidateMeta?.semanticHints ?? null
|
semanticHints: candidateMeta?.semanticHints ?? null
|
||||||
}, userMessage);
|
}, userMessage);
|
||||||
}
|
}
|
||||||
|
const candidatePredecomposeContract = (0, predecomposeContract_1.buildAddressLlmPredecomposeContractV1)({
|
||||||
|
sourceMessage: String(userMessage ?? ""),
|
||||||
|
canonicalMessage: candidate,
|
||||||
|
semanticHints: candidateMeta?.semanticHints ?? null
|
||||||
|
});
|
||||||
|
const sourceHasExplicitAccountAnchor = (0, addressIntentResolver_1.hasAccountNumberAnchor)(repairedSourceMessage || userMessage) ||
|
||||||
|
(0, addressIntentResolver_1.hasCompactAccountCodeToken)(repairedSourceMessage || userMessage);
|
||||||
|
const candidateInjectsAccountAnchor = Boolean(toNonEmptyString(candidatePredecomposeContract?.entities?.account));
|
||||||
|
if (sourceIntentResolution.intent === "inventory_on_hand_as_of_date" &&
|
||||||
|
candidateIntentResolution.intent === "inventory_on_hand_as_of_date" &&
|
||||||
|
!sourceHasExplicitAccountAnchor &&
|
||||||
|
candidateInjectsAccountAnchor) {
|
||||||
|
return attachAddressPredecomposeContract({
|
||||||
|
...baseMeta,
|
||||||
|
attempted: true,
|
||||||
|
applied: false,
|
||||||
|
traceId: normalized?.trace_id ?? null,
|
||||||
|
llmCanonicalCandidateDetected: true,
|
||||||
|
effectiveMessage: userMessage,
|
||||||
|
reason: "normalized_fragment_rejected_anchor_injection",
|
||||||
|
fallbackRuleHit: null,
|
||||||
|
sanitizedUserMessage,
|
||||||
|
semanticHints: candidateMeta?.semanticHints ?? null
|
||||||
|
}, userMessage);
|
||||||
|
}
|
||||||
const sourceAnchorQuality = evaluateAddressAnchorQuality(repairedSourceMessage || userMessage);
|
const sourceAnchorQuality = evaluateAddressAnchorQuality(repairedSourceMessage || userMessage);
|
||||||
const candidateAnchorQuality = evaluateAddressAnchorQuality(candidate);
|
const candidateAnchorQuality = evaluateAddressAnchorQuality(candidate);
|
||||||
const sameIntentForAnchorSafety = sourceAnchorQuality.intent !== "unknown" && sourceAnchorQuality.intent === candidateAnchorQuality.intent;
|
const sameIntentForAnchorSafety = sourceAnchorQuality.intent !== "unknown" && sourceAnchorQuality.intent === candidateAnchorQuality.intent;
|
||||||
|
|
@ -3554,11 +3582,7 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
|
||||||
const semanticContractForCandidate = (0, predecomposeContract_1.buildAddressSemanticExtractionContractV1)({
|
const semanticContractForCandidate = (0, predecomposeContract_1.buildAddressSemanticExtractionContractV1)({
|
||||||
sourceMessage: String(userMessage ?? ""),
|
sourceMessage: String(userMessage ?? ""),
|
||||||
canonicalMessage: candidate,
|
canonicalMessage: candidate,
|
||||||
predecomposeContract: (0, predecomposeContract_1.buildAddressLlmPredecomposeContractV1)({
|
predecomposeContract: candidatePredecomposeContract
|
||||||
sourceMessage: String(userMessage ?? ""),
|
|
||||||
canonicalMessage: candidate,
|
|
||||||
semanticHints: candidateMeta?.semanticHints ?? null
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
if (!semanticContractForCandidate.apply_canonical_recommended) {
|
if (!semanticContractForCandidate.apply_canonical_recommended) {
|
||||||
const sourceDataSignalDetected = Boolean(semanticContractForCandidate?.guard_hints?.source_data_signal_detected);
|
const sourceDataSignalDetected = Boolean(semanticContractForCandidate?.guard_hints?.source_data_signal_detected);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,101 @@
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.createAssistantTransitionPolicy = createAssistantTransitionPolicy;
|
exports.createAssistantTransitionPolicy = createAssistantTransitionPolicy;
|
||||||
function createAssistantTransitionPolicy(deps) {
|
function createAssistantTransitionPolicy(deps) {
|
||||||
|
function parseDmyDateToIso(value) {
|
||||||
|
const match = String(value ?? "").trim().match(/^(\d{2})\.(\d{2})\.(\d{4})$/);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return `${match[3]}-${match[2]}-${match[1]}`;
|
||||||
|
}
|
||||||
|
function computeMonthWindowFromIso(isoDate) {
|
||||||
|
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const year = Number(match[1]);
|
||||||
|
const month = Number(match[2]);
|
||||||
|
if (!Number.isFinite(year) || !Number.isFinite(month) || month < 1 || month > 12) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
||||||
|
const mm = String(month).padStart(2, "0");
|
||||||
|
const dd = String(lastDay).padStart(2, "0");
|
||||||
|
return {
|
||||||
|
purchase_date: isoDate,
|
||||||
|
period_from: `${year}-${mm}-01`,
|
||||||
|
period_to: `${year}-${mm}-${dd}`,
|
||||||
|
as_of_date: `${year}-${mm}-${dd}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function extractEarliestDmyDateFromEntityRefs(entityRefs) {
|
||||||
|
const dates = [];
|
||||||
|
for (const entityRef of Array.isArray(entityRefs) ? entityRefs : []) {
|
||||||
|
const value = deps.toNonEmptyString(entityRef?.value);
|
||||||
|
if (!value) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const matches = String(value).match(/\b(\d{2}\.\d{2}\.\d{4})\b/g);
|
||||||
|
if (!matches) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const token of matches) {
|
||||||
|
const isoDate = parseDmyDateToIso(token);
|
||||||
|
if (isoDate) {
|
||||||
|
dates.push(isoDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dates.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return dates.sort()[0] ?? null;
|
||||||
|
}
|
||||||
|
function extractPurchaseDateBridgeWindow(previousAddressItem, addressNavigationState) {
|
||||||
|
const replyText = deps.toNonEmptyString(previousAddressItem?.text);
|
||||||
|
if (replyText) {
|
||||||
|
const repairedText = deps.repairAddressMojibake(replyText);
|
||||||
|
const explicitFirstDateMatch = repairedText.match(/первая\s+найденная\s+дата\s+закупки:\s*(\d{2}\.\d{2}\.\d{4})/iu);
|
||||||
|
const explicitFirstDateIso = explicitFirstDateMatch ? parseDmyDateToIso(explicitFirstDateMatch[1]) : null;
|
||||||
|
if (explicitFirstDateIso) {
|
||||||
|
return computeMonthWindowFromIso(explicitFirstDateIso);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const sessionContext = addressNavigationState &&
|
||||||
|
typeof addressNavigationState === "object" &&
|
||||||
|
addressNavigationState.session_context &&
|
||||||
|
typeof addressNavigationState.session_context === "object"
|
||||||
|
? addressNavigationState.session_context
|
||||||
|
: null;
|
||||||
|
const focusObject = sessionContext && typeof sessionContext.active_focus_object === "object"
|
||||||
|
? sessionContext.active_focus_object
|
||||||
|
: null;
|
||||||
|
const preferredResultSetId = deps.toNonEmptyString(focusObject?.provenance_result_set_id) ??
|
||||||
|
deps.toNonEmptyString(sessionContext?.active_result_set_id);
|
||||||
|
const resultSets = Array.isArray(addressNavigationState?.result_sets) ? addressNavigationState.result_sets : [];
|
||||||
|
const preferredResultSet = (preferredResultSetId
|
||||||
|
? resultSets.find((item) => deps.toNonEmptyString(item?.result_set_id) === preferredResultSetId) ?? null
|
||||||
|
: null) ??
|
||||||
|
resultSets.find((item) => deps.toNonEmptyString(item?.intent) === "inventory_purchase_provenance_for_item") ??
|
||||||
|
null;
|
||||||
|
const earliestIsoDate = extractEarliestDmyDateFromEntityRefs(preferredResultSet?.entity_refs);
|
||||||
|
return earliestIsoDate ? computeMonthWindowFromIso(earliestIsoDate) : null;
|
||||||
|
}
|
||||||
|
function hasInventoryPurchaseDateVatBridgeSignal(userMessage, alternateMessage, sourceIntentHint, hasInventoryItemFocusHint) {
|
||||||
|
if (sourceIntentHint !== "inventory_purchase_provenance_for_item" &&
|
||||||
|
!hasInventoryItemFocusHint &&
|
||||||
|
!deps.isInventorySelectedObjectIntent(sourceIntentHint)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const samples = [userMessage, alternateMessage]
|
||||||
|
.map((item) => deps.compactWhitespace(deps.repairAddressMojibake(String(item ?? "")).toLowerCase()))
|
||||||
|
.filter((item) => item.length > 0);
|
||||||
|
if (samples.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return samples.some((sample) => /(?:ндс|vat)/iu.test(sample) &&
|
||||||
|
/(?:на\s+дат[ауеы]\s+покупк|на\s+дат[ауеы]\s+закупк|по\s+дат[еу]\s+покупк|по\s+дат[еу]\s+закупк|дата\s+покупк|дата\s+закупк|purchase\s+date)/iu.test(sample));
|
||||||
|
}
|
||||||
function hasInventoryRootRestatementLikeSignal(userMessage, sourceIntentHint, hasInventoryRootFrame) {
|
function hasInventoryRootRestatementLikeSignal(userMessage, sourceIntentHint, hasInventoryRootFrame) {
|
||||||
if (!hasInventoryRootFrame) {
|
if (!hasInventoryRootFrame) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -159,6 +254,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||||
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);
|
||||||
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) &&
|
||||||
|
|
@ -172,11 +268,15 @@ 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;
|
||||||
let hasPrimaryFollowupSignal = deps.hasAddressFollowupContextSignal(userMessage) || Boolean(debtRoleSwapPrimary) || inventoryShortFollowupPrimary;
|
let hasPrimaryFollowupSignal = deps.hasAddressFollowupContextSignal(userMessage) ||
|
||||||
|
Boolean(debtRoleSwapPrimary) ||
|
||||||
|
inventoryShortFollowupPrimary ||
|
||||||
|
inventoryPurchaseDateVatBridge;
|
||||||
let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||||
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
||||||
Boolean(debtRoleSwapAlternate) ||
|
Boolean(debtRoleSwapAlternate) ||
|
||||||
inventoryShortFollowupAlternate
|
inventoryShortFollowupAlternate ||
|
||||||
|
inventoryPurchaseDateVatBridge
|
||||||
: false;
|
: false;
|
||||||
const hasPrimaryIndexReferenceSignal = deps.extractDisplayedEntityIndexMention(userMessage) !== null;
|
const hasPrimaryIndexReferenceSignal = deps.extractDisplayedEntityIndexMention(userMessage) !== null;
|
||||||
const hasAlternateIndexReferenceSignal = deps.toNonEmptyString(alternateMessage)
|
const hasAlternateIndexReferenceSignal = deps.toNonEmptyString(alternateMessage)
|
||||||
|
|
@ -206,6 +306,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
hasInventoryRootTemporalFollowupAlternate ||
|
hasInventoryRootTemporalFollowupAlternate ||
|
||||||
hasInventoryRootRestatementPrimary ||
|
hasInventoryRootRestatementPrimary ||
|
||||||
hasInventoryRootRestatementAlternate ||
|
hasInventoryRootRestatementAlternate ||
|
||||||
|
inventoryPurchaseDateVatBridge ||
|
||||||
Boolean(debtRoleSwapIntent) ||
|
Boolean(debtRoleSwapIntent) ||
|
||||||
deps.hasFollowupMarker(userMessage) ||
|
deps.hasFollowupMarker(userMessage) ||
|
||||||
deps.hasReferentialPointer(userMessage) ||
|
deps.hasReferentialPointer(userMessage) ||
|
||||||
|
|
@ -340,11 +441,13 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
deps.hasAddressFollowupContextSignal(userMessage) ||
|
deps.hasAddressFollowupContextSignal(userMessage) ||
|
||||||
Boolean(debtRoleSwapPrimary) ||
|
Boolean(debtRoleSwapPrimary) ||
|
||||||
inventoryShortFollowupPrimary ||
|
inventoryShortFollowupPrimary ||
|
||||||
|
inventoryPurchaseDateVatBridge ||
|
||||||
hasInventoryRootTemporalFollowupPrimary;
|
hasInventoryRootTemporalFollowupPrimary;
|
||||||
hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||||
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
||||||
Boolean(debtRoleSwapAlternate) ||
|
Boolean(debtRoleSwapAlternate) ||
|
||||||
inventoryShortFollowupAlternate ||
|
inventoryShortFollowupAlternate ||
|
||||||
|
inventoryPurchaseDateVatBridge ||
|
||||||
hasInventoryRootTemporalFollowupAlternate
|
hasInventoryRootTemporalFollowupAlternate
|
||||||
: false;
|
: false;
|
||||||
hasStrongFollowupReference =
|
hasStrongFollowupReference =
|
||||||
|
|
@ -356,6 +459,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
inventoryShortFollowupAlternate ||
|
inventoryShortFollowupAlternate ||
|
||||||
hasInventoryRootTemporalFollowupPrimary ||
|
hasInventoryRootTemporalFollowupPrimary ||
|
||||||
hasInventoryRootTemporalFollowupAlternate ||
|
hasInventoryRootTemporalFollowupAlternate ||
|
||||||
|
inventoryPurchaseDateVatBridge ||
|
||||||
Boolean(debtRoleSwapIntent) ||
|
Boolean(debtRoleSwapIntent) ||
|
||||||
deps.hasFollowupMarker(userMessage) ||
|
deps.hasFollowupMarker(userMessage) ||
|
||||||
deps.hasReferentialPointer(userMessage) ||
|
deps.hasReferentialPointer(userMessage) ||
|
||||||
|
|
@ -435,6 +539,13 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
if (!deps.toNonEmptyString(previousFilters.organization) && organizationClarificationSelection) {
|
if (!deps.toNonEmptyString(previousFilters.organization) && organizationClarificationSelection) {
|
||||||
previousFilters.organization = organizationClarificationSelection;
|
previousFilters.organization = organizationClarificationSelection;
|
||||||
}
|
}
|
||||||
|
if (inventoryPurchaseDateVatBridge) {
|
||||||
|
const purchaseBridgeWindow = extractPurchaseDateBridgeWindow(previousAddressItem, addressNavigationState);
|
||||||
|
if (purchaseBridgeWindow) {
|
||||||
|
previousFilters.period_from = purchaseBridgeWindow.period_from;
|
||||||
|
previousFilters.period_to = purchaseBridgeWindow.period_to;
|
||||||
|
}
|
||||||
|
}
|
||||||
const shouldBackfillPreviousDateScopeFromNavigation = sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
const shouldBackfillPreviousDateScopeFromNavigation = sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||||
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
sourceIntentHint === "inventory_purchase_provenance_for_item" ||
|
sourceIntentHint === "inventory_purchase_provenance_for_item" ||
|
||||||
|
|
@ -461,7 +572,8 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
previousFilters.period_to = deps.toNonEmptyString(navigationDateScope?.period_to);
|
previousFilters.period_to = deps.toNonEmptyString(navigationDateScope?.period_to);
|
||||||
}
|
}
|
||||||
const rootContextOnlyPivot = Boolean((deps.isInventorySelectedObjectIntent(sourceIntentHint) || currentFrameKind === "inventory_drilldown") &&
|
const rootContextOnlyPivot = Boolean((deps.isInventorySelectedObjectIntent(sourceIntentHint) || currentFrameKind === "inventory_drilldown") &&
|
||||||
deps.hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage));
|
deps.hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage) &&
|
||||||
|
!inventoryPurchaseDateVatBridge);
|
||||||
const inventoryRootTemporalPivot = Boolean(inventoryRootFrame &&
|
const inventoryRootTemporalPivot = Boolean(inventoryRootFrame &&
|
||||||
(deps.isInventorySelectedObjectIntent(sourceIntentHint) ||
|
(deps.isInventorySelectedObjectIntent(sourceIntentHint) ||
|
||||||
deps.isInventoryRootFrameIntent(sourceIntentHint) ||
|
deps.isInventoryRootFrameIntent(sourceIntentHint) ||
|
||||||
|
|
@ -567,11 +679,13 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
hasInventoryRootRestatementAlternate ||
|
hasInventoryRootRestatementAlternate ||
|
||||||
hasSelectedObjectInventorySignalPrimary ||
|
hasSelectedObjectInventorySignalPrimary ||
|
||||||
hasSelectedObjectInventorySignalAlternate));
|
hasSelectedObjectInventorySignalAlternate));
|
||||||
const carryoverTargetIntent = followupSelectionMode === "carry_root_context"
|
const carryoverTargetIntent = inventoryPurchaseDateVatBridge
|
||||||
? inventoryRootFrame?.intent ?? displayedEntityTargetIntent ?? explicitIntent ?? previousIntent ?? undefined
|
? "vat_liability_confirmed_for_tax_period"
|
||||||
: explicitInventorySameDatePivot
|
: followupSelectionMode === "carry_root_context"
|
||||||
? "inventory_on_hand_as_of_date"
|
? inventoryRootFrame?.intent ?? displayedEntityTargetIntent ?? explicitIntent ?? previousIntent ?? undefined
|
||||||
: displayedEntityTargetIntent ?? explicitIntent ?? previousIntent ?? undefined;
|
: explicitInventorySameDatePivot
|
||||||
|
? "inventory_on_hand_as_of_date"
|
||||||
|
: displayedEntityTargetIntent ?? explicitIntent ?? previousIntent ?? undefined;
|
||||||
return {
|
return {
|
||||||
followupContext: {
|
followupContext: {
|
||||||
previous_intent: previousIntent ?? undefined,
|
previous_intent: previousIntent ?? undefined,
|
||||||
|
|
|
||||||
|
|
@ -1848,7 +1848,13 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (intent === "vat_liability_confirmed_for_tax_period" && !periodRange.period_from && !periodRange.period_to) {
|
const monthPeriodWasDerived = warnings.includes("period_derived_from_month_phrase");
|
||||||
|
if (
|
||||||
|
intent === "vat_liability_confirmed_for_tax_period" &&
|
||||||
|
!periodRange.period_from &&
|
||||||
|
!periodRange.period_to &&
|
||||||
|
!monthPeriodWasDerived
|
||||||
|
) {
|
||||||
const periodToForQuarter = filters.period_to ?? vatAsOfDate ?? null;
|
const periodToForQuarter = filters.period_to ?? vatAsOfDate ?? null;
|
||||||
if (periodToForQuarter) {
|
if (periodToForQuarter) {
|
||||||
const quarterWindow = deriveQuarterWindowForDate(periodToForQuarter);
|
const quarterWindow = deriveQuarterWindowForDate(periodToForQuarter);
|
||||||
|
|
|
||||||
|
|
@ -534,7 +534,7 @@ function hasFuzzyLexeme(text: string, lexemeRoots: string[]): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasCompactAccountCodeToken(text: string): boolean {
|
export function hasCompactAccountCodeToken(text: string): boolean {
|
||||||
// Match compact account tokens while reducing false positives on short-year literals like "22 РіРѕРґ".
|
// Match compact account tokens while reducing false positives on short-year literals like "22 РіРѕРґ".
|
||||||
const source = String(text ?? "");
|
const source = String(text ?? "");
|
||||||
if (!source) {
|
if (!source) {
|
||||||
|
|
@ -1599,7 +1599,7 @@ function hasGenericAddressLookupSignal(text: string): boolean {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasAccountNumberAnchor(text: string): boolean {
|
export function hasAccountNumberAnchor(text: string): boolean {
|
||||||
return /(?:account|сч[её]т|счет)\D{0,12}\d{2}(?:[.,]\d{1,2})?/i.test(text);
|
return /(?:account|сч[её]т|счет)\D{0,12}\d{2}(?:[.,]\d{1,2})?/i.test(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1790,6 +1790,20 @@ function hasCustomerRevenueRankingBridgeSignal(text: string): boolean {
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
const hasSupplierCue =
|
||||||
|
/(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0432\u0435\u043d\u0434\u043e\u0440|supplier|vendor)/iu.test(
|
||||||
|
normalized
|
||||||
|
);
|
||||||
|
if (hasSupplierCue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasDirectRankingCue =
|
||||||
|
/(?:\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0434\u043e\u0445\u043e\u0434\u043d(?:\u044b\u0439|\u0430\u044f|\u043e\u0435|\u044b\u0435|\u043e\u0433\u043e|\u043e\u043c\u0443|\u044b\u043c|\u044b\u0445)?\s+(?:\u043a\u043b\u0438\u0435\u043d\u0442|customer|client)|(?:\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0430\u044f)\s+(?:\u0443\s+\u043d\u0430\u0441\s+)?\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0434\u043e\u0445\u043e\u0434\u043d(?:\u044b\u0439|\u0430\u044f|\u043e\u0435|\u044b\u0435|\u043e\u0433\u043e|\u043e\u043c\u0443|\u044b\u043c|\u044b\u0445)?\s+\u0433\u043e\u0434|(?:\u0430\s+)?(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a)\s+(?:\u0432\u043e\u043e\u0431\u0449\u0435\s+)?(?:\u0434\u0435\u043d\u0435\u0433\s+)?\u043c\u044b\s+\u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438(?:\s+\u0437\u0430\s+\u0432\u0441\u0435\s+\u0432\u0440\u0435\u043c\u044f)?|(?:\u0430\s+)?(?:\u0437\u0430|for)\s+\d{4}\s+\u043c\u044b\s+(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a)\s+\u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438|(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a)\s+\u043c\u044b\s+\u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438\s+\u0437\u0430\s+\d{4}|(?:\u043e\u0431\u0449\u0430\u044f\s+)?\u0432\u044b\u0440\u0443\u0447\u043a\u0430\s+\u0437\u0430\s+\d{4})/iu.test(
|
||||||
|
normalized
|
||||||
|
);
|
||||||
|
if (hasDirectRankingCue) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const hasMoneyCue =
|
const hasMoneyCue =
|
||||||
/(?:\u0434\u0435\u043d\u044c\u0433|\u0434\u0435\u043d\u0435\u0433|\u0432\u044b\u0440\u0443\u0447|\u0434\u043e\u0445\u043e\u0434|\u043e\u0431\u043e\u0440\u043e\u0442|revenue|turnover|money|inflow)/iu.test(
|
/(?:\u0434\u0435\u043d\u044c\u0433|\u0434\u0435\u043d\u0435\u0433|\u0432\u044b\u0440\u0443\u0447|\u0434\u043e\u0445\u043e\u0434|\u043e\u0431\u043e\u0440\u043e\u0442|revenue|turnover|money|inflow)/iu.test(
|
||||||
normalized
|
normalized
|
||||||
|
|
@ -1797,9 +1811,15 @@ function hasCustomerRevenueRankingBridgeSignal(text: string): boolean {
|
||||||
if (!hasMoneyCue) {
|
if (!hasMoneyCue) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return /(?:\u043a\u0442\u043e\s+(?:\u043d\u0430\u043c\s+)?(?:\u0431\u043e\u043b\u044c\u0448\u0435(?:\s+\u0432\u0441\u0435\u0433\u043e)?\s+\u043f\u0440\u0438\u043d\u0435\u0441(?:\s+\u0434\u0435\u043d\u0435\u0433)?|\u043f\u0440\u0438\u043d\u0435\u0441\s+\u0431\u043e\u043b\u044c\u0448\u0435(?:\s+\u0432\u0441\u0435\u0433\u043e)?\s+\u0434\u0435\u043d\u0435\u0433)|who\s+brought\s+(?:us\s+)?(?:the\s+)?most\s+money)/iu.test(
|
const hasCustomerRankingCue =
|
||||||
normalized
|
/(?:\u043a\u0442\u043e\s+(?:\u043d\u0430\u043c\s+)?(?:\u0431\u043e\u043b\u044c\u0448\u0435(?:\s+\u0432\u0441\u0435\u0433\u043e)?\s+\u043f\u0440\u0438\u043d\u0435\u0441(?:\s+\u0434\u0435\u043d\u0435\u0433)?|\u043f\u0440\u0438\u043d\u0435\u0441\s+\u0431\u043e\u043b\u044c\u0448\u0435(?:\s+\u0432\u0441\u0435\u0433\u043e)?\s+\u0434\u0435\u043d\u0435\u0433)|who\s+brought\s+(?:us\s+)?(?:the\s+)?most\s+money|(?:\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0434\u043e\u0445\u043e\u0434\u043d\w*\s+(?:\u043a\u043b\u0438\u0435\u043d\u0442|customer|client)))/iu.test(
|
||||||
);
|
normalized
|
||||||
|
);
|
||||||
|
const hasRevenueAggregateCue =
|
||||||
|
/(?:(?:\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0430\u044f)\s+(?:\u0443\s+\u043d\u0430\u0441\s+)?\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0434\u043e\u0445\u043e\u0434\u043d\w*\s+\u0433\u043e\u0434|(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|скок)\s+(?:\u0432\u043e\u043e\u0431\u0449\u0435\s+)?(?:\u0434\u0435\u043d\u0435\u0433\s+)?\u043c\u044b\s+\u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438|(?:\u0437\u0430|for)\s+\d{4}\s+\u043c\u044b\s+(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|скок)\s+\u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438|\u0432\u044b\u0440\u0443\u0447\u043a\w*\s+\u0437\u0430\s+\d{4})/iu.test(
|
||||||
|
normalized
|
||||||
|
);
|
||||||
|
return hasCustomerRankingCue || hasRevenueAggregateCue;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasInventoryProvenanceBridgeSignal(text: string): boolean {
|
function hasInventoryProvenanceBridgeSignal(text: string): boolean {
|
||||||
|
|
@ -1845,6 +1865,13 @@ function hasColloquialInventoryOnHandBridgeSignal(text: string): boolean {
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
const hasInventoryAgingCue =
|
||||||
|
/(?:\u043e\u0447\u0435\u043d\u044c\s+\u0434\u0430\u0432\u043d\u043e|\u0434\u0430\u0432\u043d\u043e\s+\u043a\u0443\u043f\u043b|\u0434\u0430\u0432\u043d\u043e\s+\u043f\u0440\u0438\u043e\u0431\u0440\u0435\u0442|\u0441\u0442\u0430\u0440(?:\u044b\u0435|\u044b\u043c|\u044b\u0445)?\s+\u0437\u0430\u043a\u0443\u043f|\u0441\u0442\u0430\u0440\u044b\u0439\s+\u0442\u043e\u0432\u0430\u0440|old\s+stock|old\s+purchase|aging\s+by\s+purchase\s+date)/iu.test(
|
||||||
|
normalized
|
||||||
|
);
|
||||||
|
if (hasInventoryAgingCue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const tokenCount = normalized.split(/\s+/u).filter(Boolean).length;
|
const tokenCount = normalized.split(/\s+/u).filter(Boolean).length;
|
||||||
const hasWarehouseCue =
|
const hasWarehouseCue =
|
||||||
/(?:\u0441\u043a\u043b\u0430\u0434(?:\u0430\u0445|\u0435|\u0443|\u043e\u043c|\u044b)?|\u043e\u0441\u0442\u0430\u0442|warehouse|stock|inventory)/iu.test(
|
/(?:\u0441\u043a\u043b\u0430\u0434(?:\u0430\u0445|\u0435|\u0443|\u043e\u043c|\u044b)?|\u043e\u0441\u0442\u0430\u0442|warehouse|stock|inventory)/iu.test(
|
||||||
|
|
@ -1860,10 +1887,81 @@ function hasColloquialInventoryOnHandBridgeSignal(text: string): boolean {
|
||||||
return hasQuestionCue && tokenCount <= 8;
|
return hasQuestionCue && tokenCount <= 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function repairLikelyUtf8Mojibake(text: string): string {
|
||||||
|
const raw = String(text ?? "");
|
||||||
|
if (!raw) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const repaired = Buffer.from(raw, "latin1").toString("utf8");
|
||||||
|
return repaired || raw;
|
||||||
|
} catch {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveAddressIntent(userMessage: string): AddressIntentResolution {
|
export function resolveAddressIntent(userMessage: string): AddressIntentResolution {
|
||||||
const text = String(userMessage ?? "").trim().toLowerCase();
|
const text = String(userMessage ?? "").trim().toLowerCase();
|
||||||
|
const repairedText = repairLikelyUtf8Mojibake(text).trim().toLowerCase();
|
||||||
|
const bridgeText = repairedText && repairedText !== text ? `${text} ${repairedText}` : text;
|
||||||
|
|
||||||
if (hasCustomerRevenueRankingBridgeSignal(text)) {
|
const hasLooseVatPayableBridge =
|
||||||
|
/(?:\u043d\u0434\u0441|vat)/iu.test(text) &&
|
||||||
|
/(?:\u043a\u0430\u043a\u043e\u0439\s+\u043d\u0434\u0441\s+(?:\u043d\u0430\u043c\s+)?(?:\u043d\u0430\u0434\u043e|\u043d\u0443\u0436\u043d\u043e)|\u043d\u0430\u043c\s+\u043d\u0430\u0434\u043e\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|\u043d\u0430\u043c\s+\u043d\u0443\u0436\u043d\u043e\s+\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u043d\u0434\u0441\s+\u043a\s+\u0443\u043f\u043b\u0430\u0442\u0435)/iu.test(
|
||||||
|
text
|
||||||
|
) &&
|
||||||
|
/(?:\u0437\u0430\s+(?:\d{4}|(?:\u044f\u043d\u0432\u0430\u0440|\u0444\u0435\u0432\u0440\u0430\u043b|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d|\u0438\u044e\u043b|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440|\u043e\u043a\u0442\u044f\u0431\u0440|\u043d\u043e\u044f\u0431\u0440|\u0434\u0435\u043a\u0430\u0431\u0440)\S*(?:\s+(?:19|20)\d{2})?)|\u043d\u0430\s+(?:\u044f\u043d\u0432\u0430\u0440|\u0444\u0435\u0432\u0440\u0430\u043b|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d|\u0438\u044e\u043b|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440|\u043e\u043a\u0442\u044f\u0431\u0440|\u043d\u043e\u044f\u0431\u0440|\u0434\u0435\u043a\u0430\u0431\u0440)\S*(?:\s+(?:19|20)\d{2})?|\b[1-4]\s*(?:\u043a\u0432\u0430\u0440\u0442\u0430\u043b|\u043a\u0432\.?)\b)/iu.test(
|
||||||
|
text
|
||||||
|
);
|
||||||
|
if (hasLooseVatPayableBridge) {
|
||||||
|
return {
|
||||||
|
intent: "vat_liability_confirmed_for_tax_period",
|
||||||
|
confidence: "high",
|
||||||
|
reasons: ["vat_liability_colloquial_bridge_signal_detected"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasExplicitReceivablesSnapshotBridge =
|
||||||
|
/(?:\u043d\u0430\u043c\s+\u043a\u0442\u043e-\u0442\u043e\s+\u0434\u043e\u043b\u0436\u0435\u043d|\u043d\u0430\u043c\s+\u043a\u0442\u043e\s+\u0434\u043e\u043b\u0436\u0435\u043d|\u043a\u0442\u043e\s+\u043d\u0430\u043c\s+\u0434\u043e\u043b\u0436\u0435\u043d|\u0435\u0441\u0442\u044c\s+\u043b\u0438\s+\u0434\u0435\u0431\u0438\u0442\u043e\u0440\u0441\u043a\w+\s+\u0437\u0430\u0434\u043e\u043b\u0436\u0435\u043d\u043d\u043e\u0441\u0442\u044c|\u0437\u0430\u0434\u043e\u043b\u0436\u0435\u043d\u043d\u043e\u0441\u0442\u044c\s+\u043d\u0430\u043c|receivables?)/iu.test(
|
||||||
|
text
|
||||||
|
);
|
||||||
|
if (hasExplicitReceivablesSnapshotBridge) {
|
||||||
|
return {
|
||||||
|
intent: "receivables_confirmed_as_of_date",
|
||||||
|
confidence: "high",
|
||||||
|
reasons: ["receivables_snapshot_bridge_signal_detected"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasExplicitPayablesSnapshotBridge =
|
||||||
|
/(?:\u043c\u044b\s+\u0434\u043e\u043b\u0436\u043d\u044b\s+\u043a\u043e\u043c\u0443-\u0442\u043e\s+\u0434\u0435\u043d\u0435\u0433|\u043c\u044b\s+\u0434\u043e\u043b\u0436\u043d\u044b\s+\u043a\u043e\u043c\u0443\u0442\u043e\s+\u0434\u0435\u043d\u0435\u0433|\u043c\u044b\s+\u043a\u043e\u043c\u0443-\u0442\u043e\s+\u0434\u043e\u043b\u0436\u043d\u044b|\u043a\u043e\u043c\u0443\s+\u043c\u044b\s+\u0434\u043e\u043b\u0436\u043d\u044b|\u0435\u0441\u0442\u044c\s+\u043b\u0438\s+\u0437\u0430\u0434\u043e\u043b\u0436\u0435\u043d\u043d\u043e\u0441\u0442\u044c\s+\u043f\u0435\u0440\u0435\u0434\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430\u043c\u0438|\u0437\u0430\u0434\u043e\u043b\u0436\u0435\u043d\u043d\u043e\u0441\u0442\u044c\s+\u043f\u0435\u0440\u0435\u0434\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430\u043c\u0438|payables?)/iu.test(
|
||||||
|
text
|
||||||
|
);
|
||||||
|
if (hasExplicitPayablesSnapshotBridge) {
|
||||||
|
return {
|
||||||
|
intent: "payables_confirmed_as_of_date",
|
||||||
|
confidence: "high",
|
||||||
|
reasons: ["payables_snapshot_bridge_signal_detected"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasDirectInventoryAgingBridge =
|
||||||
|
/(?:\u043e\u0447\u0435\u043d\u044c\s+\u0434\u0430\u0432\u043d\u043e|\u0434\u0430\u0432\u043d\u043e\s+\u043a\u0443\u043f\u043b|\u0434\u0430\u0432\u043d\u043e\s+\u043f\u0440\u0438\u043e\u0431\u0440\u0435\u0442|\u0441\u0442\u0430\u0440(?:\u044b\u0435|\u044b\u043c|\u044b\u0445)?\s+\u0437\u0430\u043a\u0443\u043f|\u0441\u0442\u0430\u0440(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0442\u043e\u0432\u0430\u0440|old\s+stock|old\s+purchase|very\s+old\s+stock|aging\s+by\s+purchase\s+date)/iu.test(
|
||||||
|
bridgeText
|
||||||
|
);
|
||||||
|
if (hasDirectInventoryAgingBridge || hasInventoryAgingSignal(text) || hasInventoryAgingSignal(repairedText)) {
|
||||||
|
return {
|
||||||
|
intent: "inventory_aging_by_purchase_date",
|
||||||
|
confidence: "high",
|
||||||
|
reasons: ["inventory_aging_bridge_signal_detected"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasDirectRevenueAggregateBridge =
|
||||||
|
/(?:\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0434\u043e\u0445\u043e\u0434\u043d(?:\u044b\u0439|\u0430\u044f|\u043e\u0435|\u044b\u0435|\u043e\u0433\u043e|\u043e\u043c\u0443|\u044b\u043c|\u044b\u0445)?\s+\u043a\u043b\u0438\u0435\u043d\u0442|(?:\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0430\u044f)\s+(?:\u0443\s+\u043d\u0430\u0441\s+)?\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0434\u043e\u0445\u043e\u0434\u043d(?:\u044b\u0439|\u0430\u044f|\u043e\u0435|\u044b\u0435|\u043e\u0433\u043e|\u043e\u043c\u0443|\u044b\u043c|\u044b\u0445)?\s+\u0433\u043e\u0434|(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a).*(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438|\u0432\u044b\u0440\u0443\u0447\u0438\u043b\u0438)|\u0432\u044b\u0440\u0443\u0447\u043a\u0430\s+\u0437\u0430\s+\d{4})/iu.test(
|
||||||
|
bridgeText
|
||||||
|
);
|
||||||
|
if (hasDirectRevenueAggregateBridge || hasCustomerRevenueRankingBridgeSignal(bridgeText)) {
|
||||||
return {
|
return {
|
||||||
intent: "customer_revenue_and_payments",
|
intent: "customer_revenue_and_payments",
|
||||||
confidence: "medium",
|
confidence: "medium",
|
||||||
|
|
@ -1871,6 +1969,19 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasHistoricalInventorySnapshotBridge = [text, repairedText, bridgeText].some(
|
||||||
|
(sample) =>
|
||||||
|
/(?:\u043e\u0441\u0442\u0430\u0442|inventory|stock|\u0441\u043a\u043b\u0430\u0434|остат|склад)/iu.test(sample) &&
|
||||||
|
/(?:(?:\u043d\u0430|\u0437\u0430|на|за)\s+(?:\u044f\u043d\u0432\u0430\u0440|\u0444\u0435\u0432\u0440\u0430\u043b|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d|\u0438\u044e\u043b|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440|\u043e\u043a\u0442\u044f\u0431\u0440|\u043d\u043e\u044f\u0431\u0440|\u0434\u0435\u043a\u0430\u0431\u0440|март|апрел|май|июн|июл|август|сентябр|октябр|ноябр|декабр)\S*(?:\s+(?:19|20)\d{2})?|(?:\u043d\u0430|\u0437\u0430|на|за)\s+\d{4}|\b(?:19|20)\d{2}\b)/iu.test(sample)
|
||||||
|
);
|
||||||
|
if (hasHistoricalInventorySnapshotBridge) {
|
||||||
|
return {
|
||||||
|
intent: "inventory_on_hand_as_of_date",
|
||||||
|
confidence: "medium",
|
||||||
|
reasons: ["inventory_historical_snapshot_bridge_signal_detected"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (hasInventoryDocumentaryChainBridgeSignal(text)) {
|
if (hasInventoryDocumentaryChainBridgeSignal(text)) {
|
||||||
return {
|
return {
|
||||||
intent: "inventory_purchase_to_sale_chain",
|
intent: "inventory_purchase_to_sale_chain",
|
||||||
|
|
|
||||||
|
|
@ -639,13 +639,18 @@ export function composeCounterpartyAnalyticsReply(
|
||||||
|
|
||||||
if (focus === "top_years_by_total") {
|
if (focus === "top_years_by_total") {
|
||||||
const visible = rankedByYearTotal.slice(0, limit);
|
const visible = rankedByYearTotal.slice(0, limit);
|
||||||
const heading = isSupplier
|
|
||||||
? `Топ-${visible.length} лет по сумме выплат:`
|
|
||||||
: `Топ-${visible.length} лет по сумме поступлений:`;
|
|
||||||
lines.unshift(heading);
|
|
||||||
if (visible.length === 0) {
|
if (visible.length === 0) {
|
||||||
lines.push("По доступному окну не удалось собрать годовые агрегаты по суммам.");
|
lines.push("По доступному окну не удалось собрать годовые агрегаты по суммам.");
|
||||||
} else {
|
} else {
|
||||||
|
const strongestYear = visible[0];
|
||||||
|
const directAnswerLine = isSupplier
|
||||||
|
? `Самый крупный год по подтвержденным выплатам: ${strongestYear.year} (${deps.formatMoneyRub(strongestYear.total)} по ${strongestYear.ops} операциям).`
|
||||||
|
: `Самый доходный год по подтвержденным поступлениям: ${strongestYear.year} (${deps.formatMoneyRub(strongestYear.total)} по ${strongestYear.ops} операциям). Это денежный поток, а не чистая прибыль.`;
|
||||||
|
const heading = isSupplier
|
||||||
|
? `Топ-${visible.length} лет по сумме выплат:`
|
||||||
|
: `Топ-${visible.length} лет по сумме поступлений:`;
|
||||||
|
lines.unshift(heading);
|
||||||
|
lines.unshift(directAnswerLine);
|
||||||
lines.push(
|
lines.push(
|
||||||
...visible.map(
|
...visible.map(
|
||||||
(item, index) =>
|
(item, index) =>
|
||||||
|
|
@ -742,7 +747,14 @@ export function composeCounterpartyAnalyticsReply(
|
||||||
const heading = isSupplier
|
const heading = isSupplier
|
||||||
? `Топ-${visible.length} поставщиков по сумме выплат:`
|
? `Топ-${visible.length} поставщиков по сумме выплат:`
|
||||||
: `Топ-${visible.length} заказчиков по сумме поступлений:`;
|
: `Топ-${visible.length} заказчиков по сумме поступлений:`;
|
||||||
|
const leadingCounterparty = visible[0] ?? null;
|
||||||
lines.unshift(heading);
|
lines.unshift(heading);
|
||||||
|
if (leadingCounterparty) {
|
||||||
|
const directAnswerLine = isSupplier
|
||||||
|
? `Крупнейший поставщик по подтвержденным выплатам за доступное время: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
||||||
|
: `Самый доходный клиент за доступное время по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это денежный поток, а не чистая прибыль.`;
|
||||||
|
lines.unshift(directAnswerLine);
|
||||||
|
}
|
||||||
lines.push(
|
lines.push(
|
||||||
...visible.map((item, index) => {
|
...visible.map((item, index) => {
|
||||||
const avgCheck = item.ops > 0 ? item.total / item.ops : 0;
|
const avgCheck = item.ops > 0 ? item.total / item.ops : 0;
|
||||||
|
|
|
||||||
|
|
@ -691,6 +691,19 @@ export function hasInventoryPurchaseToSaleChainFollowupCue(text: string): boolea
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hasInventoryPurchaseDateVatBridgeCue(text: string): boolean {
|
||||||
|
const normalized = String(text ?? "").trim();
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
/(?:ндс|vat)/iu.test(normalized) &&
|
||||||
|
/(?:на\s+дат[ауеы]\s+покупк|на\s+дат[ауеы]\s+закупк|по\s+дат[еу]\s+покупк|по\s+дат[еу]\s+закупк|дата\s+покупк|дата\s+закупк|purchase\s+date)/iu.test(
|
||||||
|
normalized
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
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) {
|
||||||
|
|
@ -728,6 +741,9 @@ export function hasAddressFollowupContextSignal(text: string): boolean {
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (tokenCount <= 14 && hasInventoryPurchaseDateVatBridgeCue(normalized)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const hasPeriodLiteral = /\b(?:19|20)\d{2}(?:[./-](?:0?[1-9]|1[0-2]))?\b/.test(normalized);
|
const hasPeriodLiteral = /\b(?:19|20)\d{2}(?:[./-](?:0?[1-9]|1[0-2]))?\b/.test(normalized);
|
||||||
if (tokenCount <= 8 && hasPeriodLiteral) {
|
if (tokenCount <= 8 && hasPeriodLiteral) {
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -1314,6 +1330,21 @@ function deriveIntentWithFollowupContext(
|
||||||
const previousIsInventoryFamily = isInventoryIntent(sourceIntent ?? undefined);
|
const previousIsInventoryFamily = isInventoryIntent(sourceIntent ?? undefined);
|
||||||
const inventorySelectedObjectFollowup =
|
const inventorySelectedObjectFollowup =
|
||||||
hasSelectedObjectInventorySignal(normalizedMessage) || (previousIsInventoryFamily && hasFollowupSignal);
|
hasSelectedObjectInventorySignal(normalizedMessage) || (previousIsInventoryFamily && hasFollowupSignal);
|
||||||
|
const inventoryPurchaseDateVatBridge =
|
||||||
|
inventorySelectedObjectFollowup && hasInventoryPurchaseDateVatBridgeCue(normalizedMessage);
|
||||||
|
|
||||||
|
if (
|
||||||
|
inventoryPurchaseDateVatBridge &&
|
||||||
|
(detectedIntent.intent === "unknown" ||
|
||||||
|
detectedIntent.intent === sourceIntent ||
|
||||||
|
detectedIntent.intent === "vat_payable_confirmed_as_of_date")
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
intent: "vat_liability_confirmed_for_tax_period",
|
||||||
|
confidence: "low",
|
||||||
|
reasons: [...detectedIntent.reasons, "intent_adjusted_to_inventory_purchase_date_vat_bridge"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (detectedIntent.intent === "unknown" && isVatFollowup) {
|
if (detectedIntent.intent === "unknown" && isVatFollowup) {
|
||||||
const vatIntent: AddressIntent = hasVatTaxPaymentCue(normalizedMessage)
|
const vatIntent: AddressIntent = hasVatTaxPaymentCue(normalizedMessage)
|
||||||
|
|
|
||||||
|
|
@ -316,8 +316,8 @@ export function composeInventoryReply(
|
||||||
.join("; ");
|
.join("; ");
|
||||||
const directAnswerLine =
|
const directAnswerLine =
|
||||||
agingItems.length > 0
|
agingItems.length > 0
|
||||||
? `К старым закупкам на ${deps.formatDateRu(asOfDate)} в первую очередь относятся позиции с самой ранней первой закупкой: ${oldestAnswerPreview}.`
|
? `К самым старым закупкам в текущем подтвержденном срезе относятся позиции с самой ранней первой закупкой: ${oldestAnswerPreview}.`
|
||||||
: `По доступному закупочному следу на ${deps.formatDateRu(asOfDate)} позиции старых закупок не материализованы.`;
|
: "По доступному закупочному следу позиции со старыми закупками не материализованы.";
|
||||||
const lines: string[] = [directAnswerLine];
|
const lines: string[] = [directAnswerLine];
|
||||||
appendInventoryBulletSection(lines, "Сводка:", [
|
appendInventoryBulletSection(lines, "Сводка:", [
|
||||||
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||||
|
|
|
||||||
|
|
@ -719,6 +719,7 @@ export function createAssistantRoutePolicy(deps) {
|
||||||
const deepSessionContinuationFallbackToDeep = Boolean(!followupContext &&
|
const deepSessionContinuationFallbackToDeep = Boolean(!followupContext &&
|
||||||
baseToolGate?.runAddressLane &&
|
baseToolGate?.runAddressLane &&
|
||||||
!llmRuntimeUnavailableDetected &&
|
!llmRuntimeUnavailableDetected &&
|
||||||
|
!protectAddressLaneFromFallback &&
|
||||||
hasDeepSessionContinuationSignal({
|
hasDeepSessionContinuationSignal({
|
||||||
rawUserMessage,
|
rawUserMessage,
|
||||||
repairedRawUserMessage,
|
repairedRawUserMessage,
|
||||||
|
|
|
||||||
|
|
@ -2597,6 +2597,9 @@ function hasAddressFollowupContextSignal(userMessage) {
|
||||||
!hasAny(/(?:по\s+этому|по\s+тому|по\s+нему|по\s+ней|по\s+ним)/iu)) {
|
!hasAny(/(?:по\s+этому|по\s+тому|по\s+нему|по\s+ней|по\s+ним)/iu)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (shortFollowup && samples.some((sample) => (0, decomposeStage_1.hasInventoryPurchaseDateVatBridgeCue)(sample))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (shortFollowup && samples.some((sample) => hasPeriodLiteral(sample))) {
|
if (shortFollowup && samples.some((sample) => hasPeriodLiteral(sample))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -3413,6 +3416,31 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
|
||||||
semanticHints: candidateMeta?.semanticHints ?? null
|
semanticHints: candidateMeta?.semanticHints ?? null
|
||||||
}, userMessage);
|
}, userMessage);
|
||||||
}
|
}
|
||||||
|
const candidatePredecomposeContract = (0, predecomposeContract_1.buildAddressLlmPredecomposeContractV1)({
|
||||||
|
sourceMessage: String(userMessage ?? ""),
|
||||||
|
canonicalMessage: candidate,
|
||||||
|
semanticHints: candidateMeta?.semanticHints ?? null
|
||||||
|
});
|
||||||
|
const sourceHasExplicitAccountAnchor = (0, addressIntentResolver_1.hasAccountNumberAnchor)(repairedSourceMessage || userMessage) ||
|
||||||
|
(0, addressIntentResolver_1.hasCompactAccountCodeToken)(repairedSourceMessage || userMessage);
|
||||||
|
const candidateInjectsAccountAnchor = Boolean(toNonEmptyString(candidatePredecomposeContract?.entities?.account));
|
||||||
|
if (sourceIntentResolution.intent === "inventory_on_hand_as_of_date" &&
|
||||||
|
candidateIntentResolution.intent === "inventory_on_hand_as_of_date" &&
|
||||||
|
!sourceHasExplicitAccountAnchor &&
|
||||||
|
candidateInjectsAccountAnchor) {
|
||||||
|
return attachAddressPredecomposeContract({
|
||||||
|
...baseMeta,
|
||||||
|
attempted: true,
|
||||||
|
applied: false,
|
||||||
|
traceId: normalized?.trace_id ?? null,
|
||||||
|
llmCanonicalCandidateDetected: true,
|
||||||
|
effectiveMessage: userMessage,
|
||||||
|
reason: "normalized_fragment_rejected_anchor_injection",
|
||||||
|
fallbackRuleHit: null,
|
||||||
|
sanitizedUserMessage,
|
||||||
|
semanticHints: candidateMeta?.semanticHints ?? null
|
||||||
|
}, userMessage);
|
||||||
|
}
|
||||||
const sourceAnchorQuality = evaluateAddressAnchorQuality(repairedSourceMessage || userMessage);
|
const sourceAnchorQuality = evaluateAddressAnchorQuality(repairedSourceMessage || userMessage);
|
||||||
const candidateAnchorQuality = evaluateAddressAnchorQuality(candidate);
|
const candidateAnchorQuality = evaluateAddressAnchorQuality(candidate);
|
||||||
const sameIntentForAnchorSafety = sourceAnchorQuality.intent !== "unknown" && sourceAnchorQuality.intent === candidateAnchorQuality.intent;
|
const sameIntentForAnchorSafety = sourceAnchorQuality.intent !== "unknown" && sourceAnchorQuality.intent === candidateAnchorQuality.intent;
|
||||||
|
|
@ -3509,11 +3537,7 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
|
||||||
const semanticContractForCandidate = (0, predecomposeContract_1.buildAddressSemanticExtractionContractV1)({
|
const semanticContractForCandidate = (0, predecomposeContract_1.buildAddressSemanticExtractionContractV1)({
|
||||||
sourceMessage: String(userMessage ?? ""),
|
sourceMessage: String(userMessage ?? ""),
|
||||||
canonicalMessage: candidate,
|
canonicalMessage: candidate,
|
||||||
predecomposeContract: (0, predecomposeContract_1.buildAddressLlmPredecomposeContractV1)({
|
predecomposeContract: candidatePredecomposeContract
|
||||||
sourceMessage: String(userMessage ?? ""),
|
|
||||||
canonicalMessage: candidate,
|
|
||||||
semanticHints: candidateMeta?.semanticHints ?? null
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
if (!semanticContractForCandidate.apply_canonical_recommended) {
|
if (!semanticContractForCandidate.apply_canonical_recommended) {
|
||||||
const sourceDataSignalDetected = Boolean(semanticContractForCandidate?.guard_hints?.source_data_signal_detected);
|
const sourceDataSignalDetected = Boolean(semanticContractForCandidate?.guard_hints?.source_data_signal_detected);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,118 @@
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
|
||||||
export function createAssistantTransitionPolicy(deps) {
|
export function createAssistantTransitionPolicy(deps) {
|
||||||
|
function parseDmyDateToIso(value) {
|
||||||
|
const match = String(value ?? "").trim().match(/^(\d{2})\.(\d{2})\.(\d{4})$/);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return `${match[3]}-${match[2]}-${match[1]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeMonthWindowFromIso(isoDate) {
|
||||||
|
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const year = Number(match[1]);
|
||||||
|
const month = Number(match[2]);
|
||||||
|
if (!Number.isFinite(year) || !Number.isFinite(month) || month < 1 || month > 12) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
||||||
|
const mm = String(month).padStart(2, "0");
|
||||||
|
const dd = String(lastDay).padStart(2, "0");
|
||||||
|
return {
|
||||||
|
purchase_date: isoDate,
|
||||||
|
period_from: `${year}-${mm}-01`,
|
||||||
|
period_to: `${year}-${mm}-${dd}`,
|
||||||
|
as_of_date: `${year}-${mm}-${dd}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractEarliestDmyDateFromEntityRefs(entityRefs) {
|
||||||
|
const dates = [];
|
||||||
|
for (const entityRef of Array.isArray(entityRefs) ? entityRefs : []) {
|
||||||
|
const value = deps.toNonEmptyString(entityRef?.value);
|
||||||
|
if (!value) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const matches = String(value).match(/\b(\d{2}\.\d{2}\.\d{4})\b/g);
|
||||||
|
if (!matches) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const token of matches) {
|
||||||
|
const isoDate = parseDmyDateToIso(token);
|
||||||
|
if (isoDate) {
|
||||||
|
dates.push(isoDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dates.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return dates.sort()[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractPurchaseDateBridgeWindow(previousAddressItem, addressNavigationState) {
|
||||||
|
const replyText = deps.toNonEmptyString(previousAddressItem?.text);
|
||||||
|
if (replyText) {
|
||||||
|
const repairedText = deps.repairAddressMojibake(replyText);
|
||||||
|
const explicitFirstDateMatch = repairedText.match(/первая\s+найденная\s+дата\s+закупки:\s*(\d{2}\.\d{2}\.\d{4})/iu);
|
||||||
|
const explicitFirstDateIso = explicitFirstDateMatch ? parseDmyDateToIso(explicitFirstDateMatch[1]) : null;
|
||||||
|
if (explicitFirstDateIso) {
|
||||||
|
return computeMonthWindowFromIso(explicitFirstDateIso);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionContext =
|
||||||
|
addressNavigationState &&
|
||||||
|
typeof addressNavigationState === "object" &&
|
||||||
|
addressNavigationState.session_context &&
|
||||||
|
typeof addressNavigationState.session_context === "object"
|
||||||
|
? addressNavigationState.session_context
|
||||||
|
: null;
|
||||||
|
const focusObject =
|
||||||
|
sessionContext && typeof sessionContext.active_focus_object === "object"
|
||||||
|
? sessionContext.active_focus_object
|
||||||
|
: null;
|
||||||
|
const preferredResultSetId =
|
||||||
|
deps.toNonEmptyString(focusObject?.provenance_result_set_id) ??
|
||||||
|
deps.toNonEmptyString(sessionContext?.active_result_set_id);
|
||||||
|
const resultSets = Array.isArray(addressNavigationState?.result_sets) ? addressNavigationState.result_sets : [];
|
||||||
|
const preferredResultSet =
|
||||||
|
(preferredResultSetId
|
||||||
|
? resultSets.find((item) => deps.toNonEmptyString(item?.result_set_id) === preferredResultSetId) ?? null
|
||||||
|
: null) ??
|
||||||
|
resultSets.find((item) => deps.toNonEmptyString(item?.intent) === "inventory_purchase_provenance_for_item") ??
|
||||||
|
null;
|
||||||
|
const earliestIsoDate = extractEarliestDmyDateFromEntityRefs(preferredResultSet?.entity_refs);
|
||||||
|
return earliestIsoDate ? computeMonthWindowFromIso(earliestIsoDate) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasInventoryPurchaseDateVatBridgeSignal(userMessage, alternateMessage, sourceIntentHint, hasInventoryItemFocusHint) {
|
||||||
|
if (
|
||||||
|
sourceIntentHint !== "inventory_purchase_provenance_for_item" &&
|
||||||
|
!hasInventoryItemFocusHint &&
|
||||||
|
!deps.isInventorySelectedObjectIntent(sourceIntentHint)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const samples = [userMessage, alternateMessage]
|
||||||
|
.map((item) => deps.compactWhitespace(deps.repairAddressMojibake(String(item ?? "")).toLowerCase()))
|
||||||
|
.filter((item) => item.length > 0);
|
||||||
|
if (samples.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return samples.some(
|
||||||
|
(sample) =>
|
||||||
|
/(?:ндс|vat)/iu.test(sample) &&
|
||||||
|
/(?:на\s+дат[ауеы]\s+покупк|на\s+дат[ауеы]\s+закупк|по\s+дат[еу]\s+покупк|по\s+дат[еу]\s+закупк|дата\s+покупк|дата\s+закупк|purchase\s+date)/iu.test(
|
||||||
|
sample
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function hasInventoryRootRestatementLikeSignal(userMessage, sourceIntentHint, hasInventoryRootFrame) {
|
function hasInventoryRootRestatementLikeSignal(userMessage, sourceIntentHint, hasInventoryRootFrame) {
|
||||||
if (!hasInventoryRootFrame) {
|
if (!hasInventoryRootFrame) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -197,6 +309,12 @@ export 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
|
||||||
|
);
|
||||||
let inventoryShortFollowupPrimary =
|
let inventoryShortFollowupPrimary =
|
||||||
(deps.isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
|
(deps.isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
|
||||||
deps.hasShortInventoryObjectFollowupSignal(userMessage);
|
deps.hasShortInventoryObjectFollowupSignal(userMessage);
|
||||||
|
|
@ -214,11 +332,15 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
: null;
|
: null;
|
||||||
const debtRoleSwapIntent = debtRoleSwapPrimary ?? debtRoleSwapAlternate ?? null;
|
const debtRoleSwapIntent = debtRoleSwapPrimary ?? debtRoleSwapAlternate ?? null;
|
||||||
let hasPrimaryFollowupSignal =
|
let hasPrimaryFollowupSignal =
|
||||||
deps.hasAddressFollowupContextSignal(userMessage) || Boolean(debtRoleSwapPrimary) || inventoryShortFollowupPrimary;
|
deps.hasAddressFollowupContextSignal(userMessage) ||
|
||||||
|
Boolean(debtRoleSwapPrimary) ||
|
||||||
|
inventoryShortFollowupPrimary ||
|
||||||
|
inventoryPurchaseDateVatBridge;
|
||||||
let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||||
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
||||||
Boolean(debtRoleSwapAlternate) ||
|
Boolean(debtRoleSwapAlternate) ||
|
||||||
inventoryShortFollowupAlternate
|
inventoryShortFollowupAlternate ||
|
||||||
|
inventoryPurchaseDateVatBridge
|
||||||
: false;
|
: false;
|
||||||
const hasPrimaryIndexReferenceSignal = deps.extractDisplayedEntityIndexMention(userMessage) !== null;
|
const hasPrimaryIndexReferenceSignal = deps.extractDisplayedEntityIndexMention(userMessage) !== null;
|
||||||
const hasAlternateIndexReferenceSignal = deps.toNonEmptyString(alternateMessage)
|
const hasAlternateIndexReferenceSignal = deps.toNonEmptyString(alternateMessage)
|
||||||
|
|
@ -265,6 +387,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
hasInventoryRootTemporalFollowupAlternate ||
|
hasInventoryRootTemporalFollowupAlternate ||
|
||||||
hasInventoryRootRestatementPrimary ||
|
hasInventoryRootRestatementPrimary ||
|
||||||
hasInventoryRootRestatementAlternate ||
|
hasInventoryRootRestatementAlternate ||
|
||||||
|
inventoryPurchaseDateVatBridge ||
|
||||||
Boolean(debtRoleSwapIntent) ||
|
Boolean(debtRoleSwapIntent) ||
|
||||||
deps.hasFollowupMarker(userMessage) ||
|
deps.hasFollowupMarker(userMessage) ||
|
||||||
deps.hasReferentialPointer(userMessage) ||
|
deps.hasReferentialPointer(userMessage) ||
|
||||||
|
|
@ -418,11 +541,13 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
deps.hasAddressFollowupContextSignal(userMessage) ||
|
deps.hasAddressFollowupContextSignal(userMessage) ||
|
||||||
Boolean(debtRoleSwapPrimary) ||
|
Boolean(debtRoleSwapPrimary) ||
|
||||||
inventoryShortFollowupPrimary ||
|
inventoryShortFollowupPrimary ||
|
||||||
|
inventoryPurchaseDateVatBridge ||
|
||||||
hasInventoryRootTemporalFollowupPrimary;
|
hasInventoryRootTemporalFollowupPrimary;
|
||||||
hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||||
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
||||||
Boolean(debtRoleSwapAlternate) ||
|
Boolean(debtRoleSwapAlternate) ||
|
||||||
inventoryShortFollowupAlternate ||
|
inventoryShortFollowupAlternate ||
|
||||||
|
inventoryPurchaseDateVatBridge ||
|
||||||
hasInventoryRootTemporalFollowupAlternate
|
hasInventoryRootTemporalFollowupAlternate
|
||||||
: false;
|
: false;
|
||||||
hasStrongFollowupReference =
|
hasStrongFollowupReference =
|
||||||
|
|
@ -434,6 +559,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
inventoryShortFollowupAlternate ||
|
inventoryShortFollowupAlternate ||
|
||||||
hasInventoryRootTemporalFollowupPrimary ||
|
hasInventoryRootTemporalFollowupPrimary ||
|
||||||
hasInventoryRootTemporalFollowupAlternate ||
|
hasInventoryRootTemporalFollowupAlternate ||
|
||||||
|
inventoryPurchaseDateVatBridge ||
|
||||||
Boolean(debtRoleSwapIntent) ||
|
Boolean(debtRoleSwapIntent) ||
|
||||||
deps.hasFollowupMarker(userMessage) ||
|
deps.hasFollowupMarker(userMessage) ||
|
||||||
deps.hasReferentialPointer(userMessage) ||
|
deps.hasReferentialPointer(userMessage) ||
|
||||||
|
|
@ -522,6 +648,13 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
if (!deps.toNonEmptyString(previousFilters.organization) && organizationClarificationSelection) {
|
if (!deps.toNonEmptyString(previousFilters.organization) && organizationClarificationSelection) {
|
||||||
previousFilters.organization = organizationClarificationSelection;
|
previousFilters.organization = organizationClarificationSelection;
|
||||||
}
|
}
|
||||||
|
if (inventoryPurchaseDateVatBridge) {
|
||||||
|
const purchaseBridgeWindow = extractPurchaseDateBridgeWindow(previousAddressItem, addressNavigationState);
|
||||||
|
if (purchaseBridgeWindow) {
|
||||||
|
previousFilters.period_from = purchaseBridgeWindow.period_from;
|
||||||
|
previousFilters.period_to = purchaseBridgeWindow.period_to;
|
||||||
|
}
|
||||||
|
}
|
||||||
const shouldBackfillPreviousDateScopeFromNavigation =
|
const shouldBackfillPreviousDateScopeFromNavigation =
|
||||||
sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||||
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
|
|
@ -556,7 +689,8 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
}
|
}
|
||||||
const rootContextOnlyPivot = Boolean(
|
const rootContextOnlyPivot = Boolean(
|
||||||
(deps.isInventorySelectedObjectIntent(sourceIntentHint) || currentFrameKind === "inventory_drilldown") &&
|
(deps.isInventorySelectedObjectIntent(sourceIntentHint) || currentFrameKind === "inventory_drilldown") &&
|
||||||
deps.hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage)
|
deps.hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage) &&
|
||||||
|
!inventoryPurchaseDateVatBridge
|
||||||
);
|
);
|
||||||
const inventoryRootTemporalPivot = Boolean(
|
const inventoryRootTemporalPivot = Boolean(
|
||||||
inventoryRootFrame &&
|
inventoryRootFrame &&
|
||||||
|
|
@ -681,7 +815,9 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
hasSelectedObjectInventorySignalAlternate)
|
hasSelectedObjectInventorySignalAlternate)
|
||||||
);
|
);
|
||||||
const carryoverTargetIntent =
|
const carryoverTargetIntent =
|
||||||
followupSelectionMode === "carry_root_context"
|
inventoryPurchaseDateVatBridge
|
||||||
|
? "vat_liability_confirmed_for_tax_period"
|
||||||
|
: followupSelectionMode === "carry_root_context"
|
||||||
? inventoryRootFrame?.intent ?? displayedEntityTargetIntent ?? explicitIntent ?? previousIntent ?? undefined
|
? inventoryRootFrame?.intent ?? displayedEntityTargetIntent ?? explicitIntent ?? previousIntent ?? undefined
|
||||||
: explicitInventorySameDatePivot
|
: explicitInventorySameDatePivot
|
||||||
? "inventory_on_hand_as_of_date"
|
? "inventory_on_hand_as_of_date"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { extractAddressFilters } from "../src/services/addressFilterExtractor";
|
||||||
|
|
||||||
|
describe("address filter extractor regressions", () => {
|
||||||
|
it("keeps explicit month window for confirmed VAT tax-period intent", () => {
|
||||||
|
const extracted = extractAddressFilters(
|
||||||
|
"сколько ндс надо заплатить в налоговую за декабрь 2019",
|
||||||
|
"vat_liability_confirmed_for_tax_period"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(extracted.extracted_filters.period_from).toBe("2019-12-01");
|
||||||
|
expect(extracted.extracted_filters.period_to).toBe("2019-12-31");
|
||||||
|
expect(extracted.warnings).toContain("period_derived_from_month_phrase");
|
||||||
|
expect(extracted.warnings).not.toContain("period_derived_from_tax_quarter_for_confirmed_vat_liability");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -55,7 +55,7 @@ describe("address follow-up temporal regressions", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
expect(result?.intent.intent).toBe("vat_payable_confirmed_as_of_date");
|
expect(result?.intent.intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||||
expect(result?.filters.extracted_filters.period_from).toBe("2017-05-01");
|
expect(result?.filters.extracted_filters.period_from).toBe("2017-05-01");
|
||||||
expect(result?.filters.extracted_filters.period_to).toBe("2017-05-31");
|
expect(result?.filters.extracted_filters.period_to).toBe("2017-05-31");
|
||||||
expect(result?.filters.extracted_filters.as_of_date).toBe("2017-05-31");
|
expect(result?.filters.extracted_filters.as_of_date).toBe("2017-05-31");
|
||||||
|
|
@ -76,7 +76,7 @@ describe("address follow-up temporal regressions", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
expect(result?.intent.intent).toBe("vat_payable_confirmed_as_of_date");
|
expect(result?.intent.intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||||
expect(result?.filters.extracted_filters.period_from).toBe("2017-05-01");
|
expect(result?.filters.extracted_filters.period_from).toBe("2017-05-01");
|
||||||
expect(result?.filters.extracted_filters.period_to).toBe("2017-05-31");
|
expect(result?.filters.extracted_filters.period_to).toBe("2017-05-31");
|
||||||
expect(result?.filters.extracted_filters.as_of_date).toBe("2017-05-31");
|
expect(result?.filters.extracted_filters.as_of_date).toBe("2017-05-31");
|
||||||
|
|
@ -116,4 +116,26 @@ describe("address follow-up temporal regressions", () => {
|
||||||
expect(result?.filters.extracted_filters.warehouse).toBeUndefined();
|
expect(result?.filters.extracted_filters.warehouse).toBeUndefined();
|
||||||
expect(result?.baseReasons).toContain("as_of_date_from_followup_context");
|
expect(result?.baseReasons).toContain("as_of_date_from_followup_context");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("retargets inventory purchase-date VAT bridge into confirmed VAT period with inherited purchase month", () => {
|
||||||
|
const result = runAddressDecomposeStage("ндс можешь прикинуть на дату покупки рабочей станции?", {
|
||||||
|
previous_intent: "inventory_purchase_provenance_for_item",
|
||||||
|
target_intent: "vat_liability_confirmed_for_tax_period",
|
||||||
|
previous_filters: {
|
||||||
|
item: "Рабочая станция универсального специалиста",
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
period_from: "2015-02-01",
|
||||||
|
period_to: "2015-02-28",
|
||||||
|
as_of_date: "2016-03-31"
|
||||||
|
},
|
||||||
|
previous_anchor_type: "item",
|
||||||
|
previous_anchor_value: "Рабочая станция универсального специалиста"
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result?.intent.intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||||
|
expect(result?.filters.extracted_filters.period_from).toBe("2015-02-01");
|
||||||
|
expect(result?.filters.extracted_filters.period_to).toBe("2015-02-28");
|
||||||
|
expect(result?.baseReasons).toContain("period_from_followup_context");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { resolveAddressIntent } from "../src/services/addressIntentResolver";
|
||||||
|
|
||||||
|
describe("addressIntentResolver regression bridges", () => {
|
||||||
|
it("detects colloquial VAT liability for a month period", () => {
|
||||||
|
const result = resolveAddressIntent("прикинь какой ндс нам надо заплатить на февраль 2017");
|
||||||
|
|
||||||
|
expect(result.intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects payables snapshot wording in plain human form", () => {
|
||||||
|
const result = resolveAddressIntent("мы должны комуто денег на сегодня?");
|
||||||
|
|
||||||
|
expect(result.intent).toBe("payables_confirmed_as_of_date");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects top customer all-time revenue wording", () => {
|
||||||
|
const result = resolveAddressIntent("кто у нас самый доходный клиент за все время");
|
||||||
|
|
||||||
|
expect(result.intent).toBe("customer_revenue_and_payments");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects top-year company revenue wording", () => {
|
||||||
|
const result = resolveAddressIntent("какой у нас самый доходный год");
|
||||||
|
|
||||||
|
expect(result.intent).toBe("customer_revenue_and_payments");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not collapse very old stock request into generic inventory snapshot", () => {
|
||||||
|
const result = resolveAddressIntent("Есть ли остатки товара, которые закупались очень давно");
|
||||||
|
|
||||||
|
expect(result.intent).toBe("inventory_aging_by_purchase_date");
|
||||||
|
});
|
||||||
|
it("detects bare historical inventory root with explicit month-year", () => {
|
||||||
|
const result = resolveAddressIntent("остатки на март 2016");
|
||||||
|
|
||||||
|
expect(result.intent).toBe("inventory_on_hand_as_of_date");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -2747,14 +2747,15 @@ describe("address filter extraction for balance drilldown", () => {
|
||||||
expect(extracted.warnings).toContain("period_to_derived_from_as_of_date_for_vat_forecast");
|
expect(extracted.warnings).toContain("period_to_derived_from_as_of_date_for_vat_forecast");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("derives full tax quarter window for confirmed VAT tax-period intent from month phrase", () => {
|
it("keeps explicit month window for confirmed VAT tax-period intent from month phrase", () => {
|
||||||
const extracted = extractAddressFilters(
|
const extracted = extractAddressFilters(
|
||||||
"сколько ндс надо заплатить в налоговую за декабрь 2019",
|
"сколько ндс надо заплатить в налоговую за декабрь 2019",
|
||||||
"vat_liability_confirmed_for_tax_period"
|
"vat_liability_confirmed_for_tax_period"
|
||||||
);
|
);
|
||||||
expect(extracted.extracted_filters.period_from).toBe("2019-10-01");
|
expect(extracted.extracted_filters.period_from).toBe("2019-12-01");
|
||||||
expect(extracted.extracted_filters.period_to).toBe("2019-12-31");
|
expect(extracted.extracted_filters.period_to).toBe("2019-12-31");
|
||||||
expect(extracted.warnings).toContain("period_derived_from_tax_quarter_for_confirmed_vat_liability");
|
expect(extracted.warnings).toContain("period_derived_from_month_phrase");
|
||||||
|
expect(extracted.warnings).not.toContain("period_derived_from_tax_quarter_for_confirmed_vat_liability");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("derives VAT forecast quarter-to-date window for explicit day+month+year phrase", () => {
|
it("derives VAT forecast quarter-to-date window for explicit day+month+year phrase", () => {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,145 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { composeCounterpartyAnalyticsReply } from "../src/services/address_runtime/counterpartyAnalyticsReplyBuilders";
|
||||||
|
import { composeInventoryReply } from "../src/services/address_runtime/inventoryReplyBuilders";
|
||||||
|
|
||||||
|
describe("address reply builders regressions", () => {
|
||||||
|
it("starts top customer aggregate reply with a direct business answer", () => {
|
||||||
|
const result = composeCounterpartyAnalyticsReply(
|
||||||
|
"customer_revenue_and_payments",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
amount: 250000,
|
||||||
|
period: "2020-03-31",
|
||||||
|
registrator: "Поступление 1"
|
||||||
|
} as any
|
||||||
|
],
|
||||||
|
{
|
||||||
|
userMessage: "кто у нас самый доходный клиент за все время?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
formatPercent: () => null,
|
||||||
|
formatDateRu: (value: string) => value,
|
||||||
|
formatMoneyRub: (value: number) => `${value} ₽`,
|
||||||
|
extractYearFromIso: (value: string | null) => (value ? Number(value.slice(0, 4)) : null),
|
||||||
|
detectCounterpartyProfileFocus: () => "full_profile",
|
||||||
|
detectCounterpartyLifecycleFocus: () => "active_customers_all_time",
|
||||||
|
hasCounterpartyLifecycleLongevityQuestion: () => false,
|
||||||
|
hasCounterpartyActivityAgeQuestion: () => false,
|
||||||
|
detectRankingLimit: () => 5,
|
||||||
|
detectValueRankingFocus: () => "top_by_total",
|
||||||
|
detectContractValueFocus: () => "top_by_turnover",
|
||||||
|
detectMinOpsForAvgCheck: () => 1,
|
||||||
|
extractRequestedYearFromQuestion: () => null,
|
||||||
|
extractCounterpartyName: () => "Чапурнов",
|
||||||
|
extractContractName: () => null,
|
||||||
|
counterpartyLookupMatches: () => false,
|
||||||
|
toUtcDayTimestamp: () => null,
|
||||||
|
formatAgeYearsMonthsDays: () => "0 дней",
|
||||||
|
normalizeQuestionText: (value: string | null | undefined) => String(value ?? "")
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result?.text.split("\n")[0]).toContain("Самый доходный клиент");
|
||||||
|
expect(result?.text.split("\n")[0]).toContain("Чапурнов");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("starts top year aggregate reply with a direct business answer", () => {
|
||||||
|
const result = composeCounterpartyAnalyticsReply(
|
||||||
|
"customer_revenue_and_payments",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
amount: 320000,
|
||||||
|
period: "2021-05-12",
|
||||||
|
registrator: "Поступление 2"
|
||||||
|
} as any
|
||||||
|
],
|
||||||
|
{
|
||||||
|
userMessage: "какой у нас самый доходный год"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
formatPercent: () => null,
|
||||||
|
formatDateRu: (value: string) => value,
|
||||||
|
formatMoneyRub: (value: number) => `${value} ₽`,
|
||||||
|
extractYearFromIso: (value: string | null) => (value ? Number(value.slice(0, 4)) : null),
|
||||||
|
detectCounterpartyProfileFocus: () => "full_profile",
|
||||||
|
detectCounterpartyLifecycleFocus: () => "active_customers_all_time",
|
||||||
|
hasCounterpartyLifecycleLongevityQuestion: () => false,
|
||||||
|
hasCounterpartyActivityAgeQuestion: () => false,
|
||||||
|
detectRankingLimit: () => 5,
|
||||||
|
detectValueRankingFocus: () => "top_years_by_total",
|
||||||
|
detectContractValueFocus: () => "top_by_turnover",
|
||||||
|
detectMinOpsForAvgCheck: () => 1,
|
||||||
|
extractRequestedYearFromQuestion: () => null,
|
||||||
|
extractCounterpartyName: () => "Чапурнов",
|
||||||
|
extractContractName: () => null,
|
||||||
|
counterpartyLookupMatches: () => false,
|
||||||
|
toUtcDayTimestamp: () => null,
|
||||||
|
formatAgeYearsMonthsDays: () => "0 дней",
|
||||||
|
normalizeQuestionText: (value: string | null | undefined) => String(value ?? "")
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result?.text.split("\n")[0]).toContain("Самый доходный год");
|
||||||
|
expect(result?.text.split("\n")[0]).toContain("2021");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps very old stock answer free of explicit as-of date in the first line", () => {
|
||||||
|
const result = composeInventoryReply(
|
||||||
|
"inventory_aging_by_purchase_date",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
amount: 1000,
|
||||||
|
period: "2015-02-05",
|
||||||
|
registrator: "Поступление 3"
|
||||||
|
} as any
|
||||||
|
],
|
||||||
|
{
|
||||||
|
userMessage: "Есть ли остатки товара, которые закупались очень давно",
|
||||||
|
asOfDate: "2026-04-18"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resolvePayablesAsOfDate: () => "2026-04-18",
|
||||||
|
buildInventoryOnHandAggregate: () => [],
|
||||||
|
uniqueStrings: (values: string[]) => values,
|
||||||
|
formatDateRu: (value: string) => value,
|
||||||
|
formatNumberWithDots: (value: number) => String(value),
|
||||||
|
formatMoneyRub: (value: number) => `${value} ₽`,
|
||||||
|
isInventoryPurchaseMovement: () => true,
|
||||||
|
summarizeInventoryTraceRows: () => ({
|
||||||
|
item: "Рабочая станция",
|
||||||
|
warehouses: ["Основной склад"],
|
||||||
|
organizations: ["ООО Альтернатива Плюс"],
|
||||||
|
counterparties: ["Чапурнов"],
|
||||||
|
documents: ["Поступление 3"],
|
||||||
|
firstPeriod: "2015-02-05",
|
||||||
|
lastPeriod: "2015-02-05",
|
||||||
|
totalAmount: 1000
|
||||||
|
}),
|
||||||
|
formatInventoryTraceRows: () => [],
|
||||||
|
hasInventoryPurchaseDateActionFocus: () => false,
|
||||||
|
inventoryTraceDateLabel: (value: string | null) => value ?? "дата не указана",
|
||||||
|
extractInventoryCounterpartyCandidates: () => ["Чапурнов"],
|
||||||
|
buildInventoryAgingByItemAggregate: () => [
|
||||||
|
{
|
||||||
|
item: "Рабочая станция",
|
||||||
|
warehouse: "Основной склад",
|
||||||
|
organization: "ООО Альтернатива Плюс",
|
||||||
|
firstPurchasePeriod: "2015-02-05",
|
||||||
|
lastPurchasePeriod: "2015-02-05",
|
||||||
|
operations: 1,
|
||||||
|
documentCount: 1,
|
||||||
|
counterparties: ["Чапурнов"],
|
||||||
|
ageDays: 4089
|
||||||
|
}
|
||||||
|
],
|
||||||
|
formatInventoryAgingRows: () => ["1. Рабочая станция | первая закупка: 2015-02-05"],
|
||||||
|
isInventorySaleMovement: () => false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const firstLine = result?.text.split("\n")[0] ?? "";
|
||||||
|
expect(firstLine).toContain("К самым старым закупкам");
|
||||||
|
expect(firstLine).not.toContain("2026-04-18");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -157,6 +157,53 @@ describe("assistantRoutePolicy", () => {
|
||||||
expect(decision.orchestrationContract?.semantic_route_arbitration?.supported_address_intent_detected).toBe(true);
|
expect(decision.orchestrationContract?.semantic_route_arbitration?.supported_address_intent_detected).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not let deep session continuation override an exact VAT period route", () => {
|
||||||
|
const policy = buildPolicy({
|
||||||
|
detectAddressQuestionMode: () => ({ mode: "address_query", confidence: "high" }),
|
||||||
|
resolveAddressIntent: () => ({ intent: "vat_liability_confirmed_for_tax_period", confidence: "high" }),
|
||||||
|
resolveAddressToolGateDecision: () => ({
|
||||||
|
runAddressLane: true,
|
||||||
|
decision: "run_address_lane",
|
||||||
|
reason: "address_mode_classifier_detected"
|
||||||
|
}),
|
||||||
|
hasDeepSessionContinuationSignal: () => true
|
||||||
|
});
|
||||||
|
|
||||||
|
const decision = policy.resolveAssistantOrchestrationDecision({
|
||||||
|
rawUserMessage: "а какой НДС мы должны сгрузить на март 2020?",
|
||||||
|
effectiveAddressUserMessage: "а какой НДС мы должны сгрузить на март 2020?",
|
||||||
|
followupContext: null,
|
||||||
|
llmPreDecomposeMeta: {
|
||||||
|
applied: true,
|
||||||
|
llmCanonicalCandidateDetected: true,
|
||||||
|
predecomposeContract: {
|
||||||
|
mode: "address_query",
|
||||||
|
mode_confidence: "high",
|
||||||
|
intent: "vat_liability_confirmed_for_tax_period",
|
||||||
|
intent_confidence: "high"
|
||||||
|
},
|
||||||
|
semanticExtractionContract: {
|
||||||
|
valid: true,
|
||||||
|
apply_canonical_recommended: true,
|
||||||
|
extraction: {
|
||||||
|
query_shape: "UNKNOWN",
|
||||||
|
aggregation_profile: "unknown"
|
||||||
|
},
|
||||||
|
guard_hints: {
|
||||||
|
deep_investigation_signal_detected: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as any,
|
||||||
|
useMock: false
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(decision.runAddressLane).toBe(true);
|
||||||
|
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||||||
|
expect(decision.toolGateReason).toBe("address_mode_classifier_detected");
|
||||||
|
expect(decision.livingMode).toBe("address_data");
|
||||||
|
expect(decision.orchestrationContract?.deep_session_continuation_fallback_to_deep).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it("routes memory recap follow-up over grounded answer to chat", () => {
|
it("routes memory recap follow-up over grounded answer to chat", () => {
|
||||||
const policy = buildPolicy({
|
const policy = buildPolicy({
|
||||||
resolveRouteMemorySignals: () => ({
|
resolveRouteMemorySignals: () => ({
|
||||||
|
|
|
||||||
|
|
@ -254,6 +254,133 @@ describe("assistantTransitionPolicy", () => {
|
||||||
expect(carryover?.followupContext?.root_context_only).toBeUndefined();
|
expect(carryover?.followupContext?.root_context_only).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("bridges selected-item purchase provenance into a VAT period follow-up", () => {
|
||||||
|
const item = "Рабочая станция универсального специалиста";
|
||||||
|
const policy = buildPolicy({
|
||||||
|
findLastAddressAssistantItem: () => ({
|
||||||
|
text: [
|
||||||
|
`По позиции ${item} однозначный поставщик не подтвержден.`,
|
||||||
|
"Подтверждение:",
|
||||||
|
"- Первая найденная дата закупки: 05.02.2015.",
|
||||||
|
"- Последняя найденная дата закупки: 22.07.2015."
|
||||||
|
].join("\n"),
|
||||||
|
debug: {
|
||||||
|
detected_intent: "inventory_purchase_provenance_for_item",
|
||||||
|
extracted_filters: {
|
||||||
|
item,
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
as_of_date: "2016-03-31"
|
||||||
|
},
|
||||||
|
anchor_type: "item",
|
||||||
|
anchor_value_resolved: item
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
hasAddressFollowupContextSignal: () => false,
|
||||||
|
findRecentInventoryRootFrame: () => null,
|
||||||
|
resolveAddressIntent: () => ({ intent: "unknown" })
|
||||||
|
});
|
||||||
|
|
||||||
|
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||||||
|
"ндс можешь прикинуть на дату покупки рабочей станции?",
|
||||||
|
[],
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
session_context: {
|
||||||
|
active_focus_object: {
|
||||||
|
object_type: "item",
|
||||||
|
label: item,
|
||||||
|
provenance_result_set_id: "rs-provenance"
|
||||||
|
},
|
||||||
|
active_result_set_id: "rs-provenance"
|
||||||
|
},
|
||||||
|
result_sets: [
|
||||||
|
{
|
||||||
|
result_set_id: "rs-provenance",
|
||||||
|
intent: "inventory_purchase_provenance_for_item",
|
||||||
|
entity_refs: [
|
||||||
|
{
|
||||||
|
index: 1,
|
||||||
|
entity_type: "item",
|
||||||
|
value: "Поступление товаров и услуг 00000000023 от 05.02.2015 0:00:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(carryover?.followupContext?.previous_intent).toBe("inventory_purchase_provenance_for_item");
|
||||||
|
expect(carryover?.followupContext?.target_intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||||
|
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||||||
|
item,
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
period_from: "2015-02-01",
|
||||||
|
period_to: "2015-02-28"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps selected-object continuity for purchase-date VAT bridge even when an inventory root frame exists", () => {
|
||||||
|
const item = "Рабочая станция универсального специалиста";
|
||||||
|
const policy = buildPolicy({
|
||||||
|
findLastAddressAssistantItem: () => ({
|
||||||
|
text: [
|
||||||
|
`По позиции ${item} однозначный поставщик не подтвержден.`,
|
||||||
|
"Подтверждение:",
|
||||||
|
"- Первая найденная дата закупки: 05.02.2015."
|
||||||
|
].join("\n"),
|
||||||
|
debug: {
|
||||||
|
detected_intent: "inventory_purchase_provenance_for_item",
|
||||||
|
extracted_filters: {
|
||||||
|
item,
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
as_of_date: "2016-03-31"
|
||||||
|
},
|
||||||
|
anchor_type: "item",
|
||||||
|
anchor_value_resolved: item
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
hasAddressFollowupContextSignal: () => false,
|
||||||
|
hasForeignAccountingPivotOverInventoryMessage: () => true,
|
||||||
|
resolveAddressIntent: () => ({ intent: "unknown" })
|
||||||
|
});
|
||||||
|
|
||||||
|
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||||||
|
"ндс можешь прикинуть на дату покупки рабочей станции?",
|
||||||
|
[],
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
session_context: {
|
||||||
|
active_focus_object: {
|
||||||
|
object_type: "item",
|
||||||
|
label: item,
|
||||||
|
provenance_result_set_id: "rs-provenance"
|
||||||
|
},
|
||||||
|
active_result_set_id: "rs-provenance"
|
||||||
|
},
|
||||||
|
result_sets: [
|
||||||
|
{
|
||||||
|
result_set_id: "rs-provenance",
|
||||||
|
intent: "inventory_purchase_provenance_for_item",
|
||||||
|
entity_refs: [
|
||||||
|
{
|
||||||
|
index: 1,
|
||||||
|
entity_type: "item",
|
||||||
|
value: "Поступление товаров и услуг 00000000023 от 05.02.2015 0:00:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(carryover?.followupSelectionMode).toBe("carry_previous_intent");
|
||||||
|
expect(carryover?.followupContext?.root_context_only).toBeUndefined();
|
||||||
|
expect(carryover?.followupContext?.previous_intent).toBe("inventory_purchase_provenance_for_item");
|
||||||
|
expect(carryover?.followupContext?.target_intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||||
|
});
|
||||||
|
|
||||||
it("drops stale carryover for a fresh standalone topic from another intent family", () => {
|
it("drops stale carryover for a fresh standalone topic from another intent family", () => {
|
||||||
const policy = buildPolicy({
|
const policy = buildPolicy({
|
||||||
findLastAddressAssistantItem: () => ({
|
findLastAddressAssistantItem: () => ({
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
{
|
||||||
|
"suite_id": "assistant_saved_session_runtime_job-vc0r4pNwB-",
|
||||||
|
"suite_version": "0.1.0",
|
||||||
|
"schema_version": "assistant_saved_session_runtime_v0_1",
|
||||||
|
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
|
||||||
|
"scenario_count": 1,
|
||||||
|
"case_ids": [
|
||||||
|
"SAVED-001"
|
||||||
|
],
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"case_id": "SAVED-001",
|
||||||
|
"scenario_tag": "saved_user_sessions_runtime",
|
||||||
|
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
|
||||||
|
"question_type": "followup",
|
||||||
|
"broadness_level": "medium",
|
||||||
|
"turns": [
|
||||||
|
{
|
||||||
|
"user_message": "приветик - че как там дела"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "расскажи что можешь интересного"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "кайф - что там на складе по остаткам?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "а исторические остатки на другие даты умеешь?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "давай на июль 2017"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "март 2016"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "а кому продали?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "у тебя написано кто контрагент: рабочая станция - это ошибка?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "ндс можешь прикинуть на дату покупки рабочей станции?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "а какой ндс мы должны сгрузить на март 2020?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "прикинь какой ндс нам надо заплатить на февраль 2017"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "кто у нас самый доходный клиент за все время"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "кто нам должен денег на май 2017"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "а какой ндс мы должны примерно заплатить за этот период?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "мы должны комуто денег на сегодня?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "а нам?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "какой у нас самый доходный год"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "а за 2017 мы скок заработали?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "сколько вообще денег мы заработали за все время?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "ты умеешь считать дельту по договорам?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "по чепурнову покажи все доки"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "а по свк"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "а сейчас у нас есть что на складе?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "что нам отгружать чепурнов? какой товар или услугу?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "какие остатки на складе на сегодня"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "остатки на март 2016"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "хвосты покажи по счету 60 на август 2022"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "Есть ли остатки товара, которые закупались очень давно"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "Какие конкретно номенклатуры формируют остаток по складу на май 2020"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue