Compare commits
41 Commits
0d3b33578e
...
87c440d6fb
| Author | SHA1 | Date |
|---|---|---|
|
|
87c440d6fb | |
|
|
837e1fe141 | |
|
|
739e8b808f | |
|
|
7262603a26 | |
|
|
796a9a93f2 | |
|
|
830e05fe97 | |
|
|
7d9ba76cc5 | |
|
|
394eef066e | |
|
|
bba4717dbe | |
|
|
9ed5435866 | |
|
|
39ff160c8e | |
|
|
f5409bbcbc | |
|
|
5a9b30bcdb | |
|
|
972db24779 | |
|
|
0158419b65 | |
|
|
d7d6be18ff | |
|
|
da8b328d98 | |
|
|
2f282f1479 | |
|
|
92cd272efc | |
|
|
5acb016573 | |
|
|
d7ee95286d | |
|
|
7e1a2edadb | |
|
|
0528745e39 | |
|
|
84beaf5540 | |
|
|
0c4b53ccc6 | |
|
|
6baa468af0 | |
|
|
19137a698f | |
|
|
65d15c156b | |
|
|
62e2b3323f | |
|
|
f7a8844c90 | |
|
|
14465c7fde | |
|
|
4a6ac6bd99 | |
|
|
33c757a437 | |
|
|
cddd6667fb | |
|
|
c96c9bab86 | |
|
|
8a1ae696dc | |
|
|
053a3bf157 | |
|
|
df92bf9af2 | |
|
|
4c352a8263 | |
|
|
e18b0922ad | |
|
|
e4cab85dd9 |
Binary file not shown.
|
|
@ -221,3 +221,66 @@ This phase is successful only when a new human user can ask a structurally new b
|
|||
5. answer honestly from proved evidence without pretending certainty it does not have.
|
||||
|
||||
That is the first point where the assistant will start to feel like it can actually walk 1C on its own within the reviewed MCP boundaries.
|
||||
|
||||
## Status Update - 2026-04-23
|
||||
|
||||
The D/E/F phase above is no longer only a target architecture.
|
||||
|
||||
It is now treated as materially closed baseline.
|
||||
|
||||
What is replay-backed and considered operational:
|
||||
|
||||
- `D. Question -> Data Need Graph`
|
||||
- `E. Dynamic Schema Traversal And Primitive Search`
|
||||
- `F. Multi-Hop Evidence Loop And Clarifying Recovery`
|
||||
|
||||
Operationally real in code and replay:
|
||||
|
||||
- data-need graphs now represent ranking, comparison, value-flow, metadata, and scoped follow-up asks as machine-readable runtime objects instead of only nearest-family route hints;
|
||||
- planner selection now uses data-need graph plus catalog/schema signals rather than only short reviewed family recipes;
|
||||
- metadata ambiguity can survive clarification, pick `documents` or `movements`, and continue the same bounded path;
|
||||
- multi-hop loops can now recover through missing organization/period gaps, resume the same proof path, and survive year-switch or `all_time` continuation on the validated contours.
|
||||
|
||||
Representative replay anchors for this closure include:
|
||||
|
||||
- `address_truth_harness_phase33_open_scope_value_flow_comparison_live_rerun5`
|
||||
- `address_truth_harness_phase39_open_scope_ranking_org_clarification_live_rerun2`
|
||||
- `address_truth_harness_phase42_catalog_metadata_drilldown_live_rerun2`
|
||||
- `address_truth_harness_phase45_multi_hop_open_total_clarification_loop_live_rerun2`
|
||||
- `address_truth_harness_phase52_metadata_movement_full_recovery_live_rerun1`
|
||||
- `address_truth_harness_phase63_metadata_document_pivot_all_time_live_rerun2`
|
||||
|
||||
What this closure does **not** mean:
|
||||
|
||||
- the whole assistant is now semantically immune to stale scope or wrong-subject carryover;
|
||||
- every already-enabled contour is automatically safe under repeated pivots and legacy session memory;
|
||||
- open-world bounded autonomy is broad enough for arbitrary unfamiliar 1C asks.
|
||||
|
||||
That is why the next architecture mainline is now:
|
||||
|
||||
- [17 - post_f_semantic_integrity_hardening_2026-04-23.md](./17%20-%20post_f_semantic_integrity_hardening_2026-04-23.md)
|
||||
|
||||
That document formalizes the next pressure point:
|
||||
|
||||
- not only growing autonomy breadth,
|
||||
- but protecting semantic correctness inside the autonomy surface that already exists.
|
||||
|
||||
## Cross-Check Update - 2026-04-24
|
||||
|
||||
Post-F hardening did not invalidate this D/E/F plan. It confirmed the original boundary:
|
||||
|
||||
- D/E/F are now baseline substrate, not the active rescue task;
|
||||
- the old broad manual replay is a regression amplifier for this substrate, not a request to return to pre-D/E/F behavior;
|
||||
- manual failures are accepted as useful only when the fix strengthens bounded evidence planning, explicit-subject arbitration, or materialization truth.
|
||||
|
||||
The manual failure slice from `assistant-stage1-9liEOh-7JP` was repaired without rolling back D/E/F:
|
||||
|
||||
- VAT purchase-date and February 2017 turns now route through confirmed tax-period VAT or materialize forecast periods inside the requested window;
|
||||
- highest-value customer wording now lands on customer revenue/payment ranking, not lifecycle activity;
|
||||
- Chepurnov item-flow after stale inventory context now keeps the counterparty/document contour instead of reusing inventory focus.
|
||||
|
||||
The current hand-off is therefore:
|
||||
|
||||
- keep D/E/F as the bounded autonomy baseline;
|
||||
- keep Post-F semantic integrity invariants as regression gates;
|
||||
- continue the next slice by expanding open-world breadth only behind the same evidence and replay discipline.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,252 @@
|
|||
# 17 - Post-F Semantic Integrity Hardening (2026-04-23)
|
||||
|
||||
## Purpose
|
||||
|
||||
This note opens the architecture phase that starts after the practical closure of `Big Block D/E/F`.
|
||||
|
||||
It is not a new reset and not a retreat from bounded autonomy.
|
||||
|
||||
It exists because the project has now crossed an important threshold:
|
||||
|
||||
- the bounded autonomy substrate is materially real in runtime code;
|
||||
- the planner can already survive metadata, entity, documents, movements, value-flow, ranking, comparison, and multi-hop clarification loops;
|
||||
- but a new class of failures still remains visible to a human user:
|
||||
- stale scope contamination;
|
||||
- wrong subject carryover;
|
||||
- post-pivot semantic drift;
|
||||
- already-supported contours that still sometimes answer the wrong business object.
|
||||
|
||||
So the next layer is no longer "build the first autonomy substrate".
|
||||
|
||||
It is:
|
||||
|
||||
- protect semantic correctness inside the already-enabled autonomy surface.
|
||||
|
||||
## Baseline Entering This Phase
|
||||
|
||||
The following is now treated as replay-backed baseline rather than future intent:
|
||||
|
||||
- `A. Metadata-First Self-Navigation`
|
||||
- `B. Entity And Schema Grounding`
|
||||
- `C. Planner-Selected Primitive Chains`
|
||||
- `D. Question -> Data Need Graph`
|
||||
- `E. Dynamic Schema Traversal And Primitive Search`
|
||||
- `F. Multi-Hop Evidence Loop And Clarifying Recovery`
|
||||
|
||||
This means the project already has:
|
||||
|
||||
- metadata-first discovery through reviewed MCP primitives;
|
||||
- bounded schema grounding and honest ambiguity handling;
|
||||
- planner-selected chains across entity resolution, value flow, documents, movements, ranking, and comparison;
|
||||
- multi-hop clarification/recovery loops that can resume the same proof path.
|
||||
|
||||
What this does **not** mean:
|
||||
|
||||
- that every already-enabled contour is semantically stable under stale memory pressure;
|
||||
- that repeated pivots are always safe by default;
|
||||
- that explicit current-turn subject always beats old organization or focus state;
|
||||
- that an exact route cannot still lose to a stale discovery/meta continuation.
|
||||
|
||||
## Why A New Phase Is Necessary
|
||||
|
||||
At this point the worst user-facing failures are no longer:
|
||||
|
||||
- "the system has no path at all";
|
||||
- "metadata/chain planning does not exist";
|
||||
- "clarification cannot resume".
|
||||
|
||||
The worst failures are now more dangerous in a different way:
|
||||
|
||||
- the system often has a path, but can still answer about the wrong business object;
|
||||
- the route can be technically healthy while the semantic answer is still wrong;
|
||||
- a human user can still see a glitch on a question that the architecture should already support.
|
||||
|
||||
That is exactly the class of bug that damages trust fastest.
|
||||
|
||||
So the phase goal is not breadth-first enablement.
|
||||
|
||||
It is semantic integrity over the enabled surface.
|
||||
|
||||
## Main Failure Classes
|
||||
|
||||
### 1. Stale Scope Contamination
|
||||
|
||||
Examples:
|
||||
|
||||
- old organization scope contaminates an explicit current-turn counterparty;
|
||||
- old focus object survives a newly grounded entity;
|
||||
- metadata scope bleeds into a later data ask.
|
||||
|
||||
### 2. Referential Carryover Drift
|
||||
|
||||
Examples:
|
||||
|
||||
- `по нему`, `ему`, `по этой позиции`, `кроме этого документа` drift into the wrong lane;
|
||||
- referential follow-up wakes metadata/discovery when it should stay in exact follow-up continuity;
|
||||
- a pronoun keeps the contour but loses the actual business object.
|
||||
|
||||
### 3. Post-Pivot Arbitration Failures
|
||||
|
||||
Examples:
|
||||
|
||||
- `documents -> payments`
|
||||
- `payments -> contracts`
|
||||
- `documents -> contracts -> documents`
|
||||
- `movements -> documents -> year-switch`
|
||||
|
||||
The user means "stay on the same business object, but change the lane".
|
||||
|
||||
The system must not:
|
||||
|
||||
- invent a new topic;
|
||||
- wake unrelated discovery;
|
||||
- or keep the old lane after the pivot was explicit.
|
||||
|
||||
### 4. Temporal Continuity Loss After A Correct Pivot
|
||||
|
||||
Examples:
|
||||
|
||||
- `... -> а за 2021?`
|
||||
- `... -> а за все время?`
|
||||
|
||||
These must continue the active contour.
|
||||
|
||||
They must not:
|
||||
|
||||
- reset into unsupported;
|
||||
- revive stale period windows;
|
||||
- or lose the active subject.
|
||||
|
||||
### 5. Exact-But-Invisible Result Materialization
|
||||
|
||||
Examples:
|
||||
|
||||
- VAT contour selected correctly but rows disappear in a later post-filter;
|
||||
- exact lane found the correct evidence but the final answer degrades to limited/partial.
|
||||
|
||||
This class is especially dangerous because the route looks healthy in debug while the user still gets a bad answer.
|
||||
|
||||
## Scope Of This Phase
|
||||
|
||||
### In Scope
|
||||
|
||||
- protect grounded current-turn subject against stale organization/focus scope;
|
||||
- protect exact and planner-selected follow-up pivots from stale discovery/meta override;
|
||||
- protect referential document/object follow-ups from semantic drift;
|
||||
- protect year-switch and all-time continuation after pivots;
|
||||
- repair materialization seams where correct evidence is filtered out before the user answer;
|
||||
- build replay packs that specifically target semantic integrity instead of only route availability.
|
||||
|
||||
### Out Of Scope
|
||||
|
||||
- broad new-domain enablement for arbitrary unfamiliar 1C questions;
|
||||
- unrestricted primitive growth without replay-backed proof;
|
||||
- cosmetic answer phrasing work when the underlying semantic seam is still wrong.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
This phase is only healthy when the following are true:
|
||||
|
||||
1. explicit current-turn subject beats stale scope unless the user explicitly changes the business object;
|
||||
2. a valid clarification is preserved and resumed on the same proof path;
|
||||
3. repeated pivots keep the same business object and the intended lane;
|
||||
4. year-switch and all-time follow-ups preserve the active contour;
|
||||
5. exact route success cannot silently degrade into a semantically wrong user answer;
|
||||
6. replay verdict is based first on human business meaning, only then on technical internals.
|
||||
|
||||
## Current Status - 2026-04-24
|
||||
|
||||
This phase is already active in runtime code and replay-backed.
|
||||
|
||||
Materially hardened in this pass:
|
||||
|
||||
- explicit current-turn counterparty now overrides stale organization-scoped carryover for value-flow/net asks;
|
||||
- explicit one-organization open value-flow asks now stop for organization clarification instead of answering a global total under missing scope;
|
||||
- incoming-vs-outgoing money comparisons now defer away from one-sided exact supplier/customer recipes into the bounded bidirectional value-flow discovery path;
|
||||
- document asks with numeric counterparty suffixes such as `Жуковка 51` no longer collapse into bank-operation routing only because the suffix looks like account `51`;
|
||||
- rejected LLM predecompose rewrites can no longer inject account scope or truncated semantic hints for numeric counterparty suffixes such as `Жуковке 51`; the session state now keeps the counterparty anchor and does not carry `account: 51`;
|
||||
- explicit raw entity search after a ranking/value-flow dialogue now resets stale assistant/followup scope: `Найди в 1С Группу СВК` becomes `source_signal=raw_text` with `explicit_entity_candidates=["Группа СВК"]`, no stale organization/date/ranking scope in the discovery turn input, and downstream follow-ups then carry the grounded SVK object intentionally;
|
||||
- open-organization ranked value-flow questions now stay subjectless by design: predicate-shaped entity pollution such as `принёс наибольшую выручку организации в` is discarded, referential organization placeholders such as `эта организация` are resolved through the active organization scope, and the MCP ranking answer overrides exact-lane year-summary replies when the user asked `кто`;
|
||||
- ranked value-flow answer drafts no longer leak raw English/internal evidence lines into the user-facing response;
|
||||
- metadata-scoped movement/document chains now keep organization scope and metadata subject separate: stale follow-up `counterparty` values that are really the active organization, or action-verb pollution such as `Провести`, no longer become `explicit_entity_candidates` during year switches and document pivots;
|
||||
- VAT exact materialization now survives period-window filtering instead of collapsing into `recipe_visibility_gap`;
|
||||
- confirmed VAT tax-period turns now keep raw month extraction separate from runtime tax-quarter execution, so exact VAT routes do not lose the intended tax period;
|
||||
- post-pivot receivables recovery now preserves the selected open-items recipe without weakening strict account-scope guards for risk replies;
|
||||
- M23 runtime/reply integrity slice is green at `403/403`, covering VAT date-basis, stale scope recovery, open-contract as-of derivation, counterparty role replies, lifecycle labels, value-ranking answer shape, explicit open value-flow organization clarification, and document-vs-bank arbitration;
|
||||
- stale session focus no longer wins over newly grounded discovery counterparty;
|
||||
- referential document follow-up no longer wakes metadata/discovery by mistake;
|
||||
- `documents -> payments/contracts` and repeated pivot chains now survive year-switch and all-time continuation;
|
||||
- mixed human AGENT dialogues now keep repeated pivots, open-scope organization questions, and explicit counterparty resets inside one semantically stable session.
|
||||
- the manual failure slice from `assistant-stage1-9liEOh-7JP` is now replay-backed: VAT purchase-date and February 2017 questions route to confirmed tax-period VAT instead of filtered forecast output, highest-value customer routes to revenue/payment ranking instead of lifecycle activity, and Chepurnov item-flow no longer reuses stale inventory scope.
|
||||
|
||||
Replay-backed anchors for the current layer include:
|
||||
|
||||
- `address_truth_harness_phase11_manual_followup_meta_quality_live_rerun_vatfix`
|
||||
- `address_truth_harness_phase20_continuity_stabilization_live_rerun_vatfix`
|
||||
- `address_truth_harness_phase67_svk_grounded_counterparty_integrity_live_rerun_vatfix`
|
||||
- `address_truth_harness_phase68_referential_document_followup_integrity_live_rerun1`
|
||||
- `address_truth_harness_phase69_document_to_payments_pronoun_pivot_live_rerun3`
|
||||
- `address_truth_harness_phase72_document_to_contracts_year_switch_live_rerun3`
|
||||
- `address_truth_harness_phase80_payments_to_contracts_to_documents_all_time_live_rerun1`
|
||||
- `address_truth_harness_phase82_human_mixed_integrity_status_dialog_post_m23_rerun_documents_scope_bidirectional`
|
||||
- `address_truth_harness_phase82_human_mixed_integrity_status_dialog_post_f_account_injection_guard_clean_scope`
|
||||
- `address_truth_harness_post_f_cross_stage_canary_agent_20260424_live7`, accepted `24/24`, and saved into autoruns as `AGENT | Post-F cross-stage semantic integrity canary` (`gen-ag04241406-abe4d8`)
|
||||
- `address_truth_harness_post_f_manual_failures_20260424_live3`, accepted `11/11`, and saved into autoruns as `AGENT | Post-F ручные провалы VAT revenue item-flow live3` (`gen-ag04241710-bdb248`)
|
||||
|
||||
## Operational Closure - 2026-04-24
|
||||
|
||||
Post-F is now treated as operationally closed as a hardening slice.
|
||||
|
||||
The closure is not based on unit tests alone. It is based on code-level wiring plus replay-backed semantic evidence:
|
||||
|
||||
- code fix commit: `739e8b8 Post-F: закрыть ручные провалы НДС, выручки и item-flow`;
|
||||
- runtime artifact tail commit: `837e1fe Post-F: сохранить хвосты ручных runtime-прогонов`;
|
||||
- cross-stage canary: `address_truth_harness_post_f_cross_stage_canary_agent_20260424_live7`, accepted `24/24`;
|
||||
- manual failure replay: `address_truth_harness_post_f_manual_failures_20260424_live3`, accepted `11/11`;
|
||||
- focused runtime slice: `addressQueryRuntimeM23.test.ts` accepted `403/403` before the final manual-failure reinforcement;
|
||||
- graph snapshot after rebuild: `5892 nodes`, `12772 edges`, `137 communities`.
|
||||
|
||||
The code-derived map for this closure is documented in:
|
||||
|
||||
- [18 - post_f_code_documentation_sync_2026-04-24.md](./18%20-%20post_f_code_documentation_sync_2026-04-24.md)
|
||||
|
||||
The important architectural reading is:
|
||||
|
||||
- fixing old manual-run failures did not roll back the fresh bounded-autonomy concept;
|
||||
- the old broad run acted as a semantic regression oracle;
|
||||
- each accepted fix strengthened current Post-F invariants: explicit current-turn meaning beats stale scope, exact VAT materialization cannot self-filter away, and counterparty item-flow cannot be hijacked by stale inventory focus.
|
||||
|
||||
## Honest Remaining Risk
|
||||
|
||||
This phase should not be overclaimed.
|
||||
|
||||
What is now stronger:
|
||||
|
||||
- already-enabled contours are materially harder to derail through stale carryover and post-pivot drift;
|
||||
- semantic replay now catches more "wrong business object" failures before they hide behind green tests.
|
||||
|
||||
What is still not solved globally:
|
||||
|
||||
- arbitrary unfamiliar 1C asks outside the grown primitive/search surface;
|
||||
- every possible stale memory seam across the whole legacy assistant surface;
|
||||
- the residual centrality pressure inside `resolveAddressIntent()` and nearby orchestration bridges.
|
||||
|
||||
So this phase improves trust inside the enabled surface.
|
||||
|
||||
It does **not** yet mean:
|
||||
|
||||
- universal immunity from semantic drift;
|
||||
- or general open-world autonomy over any new 1C question.
|
||||
|
||||
## Hand-Off
|
||||
|
||||
From this point the architecture should treat two tracks as parallel truths:
|
||||
|
||||
1. continue growing open-world bounded autonomy breadth where the primitive/search surface is still too narrow;
|
||||
2. continue semantic integrity hardening where an already-enabled contour can still answer a human user incorrectly.
|
||||
|
||||
In practice this means:
|
||||
|
||||
- breadth work without semantic replay is not enough;
|
||||
- semantic polish without protecting the actual business object is not enough;
|
||||
- user trust now depends on both.
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
# 18 - Post-F Code Documentation Sync (2026-04-24)
|
||||
|
||||
## Purpose
|
||||
|
||||
This note is the code-derived map after the operational closure of `Post-F Semantic Integrity Hardening`.
|
||||
|
||||
It answers a narrow question:
|
||||
|
||||
- do the current architecture docs still match the code and replay evidence?
|
||||
|
||||
Short answer:
|
||||
|
||||
- yes for the active architecture mainline;
|
||||
- no if old planning notes are read as current status rather than historical context.
|
||||
|
||||
So this document becomes the current map layer over the older maps.
|
||||
|
||||
## Source-Of-Truth Order
|
||||
|
||||
Use the documentation in this order:
|
||||
|
||||
1. `README.md` for the current package snapshot and readiness percentages.
|
||||
2. `16 - data_need_graph_and_open_world_mcp_plan_2026-04-22.md` for the D/E/F bounded-autonomy baseline.
|
||||
3. `17 - post_f_semantic_integrity_hardening_2026-04-23.md` for the semantic-integrity invariants and closure evidence.
|
||||
4. This document for the code-level responsibility map after Post-F.
|
||||
5. Documents `01` through `15` as historical architecture and stabilization trail, not as the current final status.
|
||||
|
||||
## Current Code Map
|
||||
|
||||
The current Post-F behavior is not owned by one monolithic resolver. It is spread across a few deliberately bounded seams.
|
||||
|
||||
### Intent Signal Layer
|
||||
|
||||
Primary files:
|
||||
|
||||
- `llm_normalizer/backend/src/services/addressCounterpartyIntentSignals.ts`
|
||||
- `llm_normalizer/backend/src/services/addressIntentResolver.ts`
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- detect account-60/62/76 open-item tails before they collapse into generic account snapshots;
|
||||
- detect counterparty item-flow wording such as "what did this counterparty ship" before stale inventory focus can win;
|
||||
- detect highest-value customer/revenue-ranking wording before it falls into lifecycle activity;
|
||||
- keep bidirectional value-flow comparison asks away from one-sided exact recipes when the user needs both incoming and outgoing money.
|
||||
|
||||
Important Post-F reading:
|
||||
|
||||
- new manual-run fixes are not "old behavior restored";
|
||||
- they are explicit current-turn meaning guards added before stale context can arbitrate the turn incorrectly.
|
||||
|
||||
### Follow-Up Retarget Layer
|
||||
|
||||
Primary file:
|
||||
|
||||
- `llm_normalizer/backend/src/services/address_runtime/decomposeStage.ts`
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- preserve previous period filters when the user says "за этот период";
|
||||
- retarget selected-inventory purchase-date VAT questions to confirmed tax-period VAT when the business meaning is a tax-period liability;
|
||||
- prevent non-inventory counterparty/document follow-ups from being pulled back into inventory purchase-document routing;
|
||||
- protect selected-object inventory follow-ups only when inventory lineage is actually active.
|
||||
|
||||
Important Post-F reading:
|
||||
|
||||
- temporal carryover is allowed only when it preserves the active business object;
|
||||
- it must not revive an old focus object after the user has explicitly changed the subject.
|
||||
|
||||
### Recipe And Materialization Layer
|
||||
|
||||
Primary file:
|
||||
|
||||
- `llm_normalizer/backend/src/services/addressRecipeCatalog.ts`
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- own the exact recipe definitions for VAT, revenue ranking, documents, contracts, bank operations, and inventory;
|
||||
- materialize VAT forecast aggregates with a period inside the requested period window via `__PERIOD_EXPR__`;
|
||||
- keep confirmed VAT tax-period execution separate from raw month extraction;
|
||||
- expose `address_vat_liability_confirmed_tax_period_v1` and `address_customer_revenue_and_payments_v1` as first-class exact recipes.
|
||||
|
||||
Important Post-F reading:
|
||||
|
||||
- exact route success is not enough if the rows disappear in a later post-filter;
|
||||
- materialization period is part of semantic truth, not just a query formatting detail.
|
||||
|
||||
### Filter Extraction Layer
|
||||
|
||||
Primary file:
|
||||
|
||||
- `llm_normalizer/backend/src/services/addressFilterExtractor.ts`
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- extract explicit counterparty anchors from current-turn wording;
|
||||
- support instrumental counterparty forms used in passive item-flow questions;
|
||||
- avoid letting old organization scope masquerade as the newly asked counterparty.
|
||||
|
||||
Important Post-F reading:
|
||||
|
||||
- current-turn explicit counterparty is stronger than stale organization scope unless the user explicitly asks for an organization-level question.
|
||||
|
||||
### MCP Discovery And Response Layer
|
||||
|
||||
Primary files:
|
||||
|
||||
- `llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts`
|
||||
- `llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts`
|
||||
- `llm_normalizer/backend/src/services/assistantMcpDiscoveryResponsePolicy.ts`
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- keep raw entity search clean after ranking/value-flow conversations;
|
||||
- separate metadata scope from data-answer subject;
|
||||
- keep planner-selected MCP answers from being overwritten by stale exact-lane replies;
|
||||
- prevent raw internal evidence lines from leaking into user-facing ranked value-flow answers.
|
||||
|
||||
Important Post-F reading:
|
||||
|
||||
- MCP discovery is now a bounded autonomy substrate;
|
||||
- Post-F does not replace it, it protects it from stale-scope contamination.
|
||||
|
||||
## Replay Evidence Map
|
||||
|
||||
The following replay anchors define the current accepted map:
|
||||
|
||||
- `address_truth_harness_post_f_cross_stage_canary_agent_20260424_live7`: accepted `24/24`.
|
||||
- `address_truth_harness_post_f_manual_failures_20260424_live3`: accepted `11/11`.
|
||||
- `address_truth_harness_phase82_human_mixed_integrity_status_dialog_post_f_account_injection_guard_clean_scope`: accepted `19/19`.
|
||||
- `address_truth_harness_phase82_human_mixed_integrity_status_dialog_post_m23_rerun_documents_scope_bidirectional`: accepted `19/19`.
|
||||
- `address_truth_harness_phase67_svk_grounded_counterparty_integrity_live_rerun_vatfix`: accepted.
|
||||
- `address_truth_harness_phase11_manual_followup_meta_quality_live_rerun_vatfix`: accepted `10/10`.
|
||||
- `address_truth_harness_phase20_continuity_stabilization_live_rerun_vatfix`: accepted `6/6`.
|
||||
|
||||
The saved autorun map includes:
|
||||
|
||||
- `AGENT | Post-F cross-stage semantic integrity canary live7`;
|
||||
- `AGENT | Post-F ручные провалы VAT revenue item-flow live3`.
|
||||
|
||||
## Manual Run Interpretation
|
||||
|
||||
Old broad manual runs are not obsolete.
|
||||
|
||||
They are useful when treated as semantic regression oracles:
|
||||
|
||||
- if a failure exposes stale scope, wrong subject carryover, wrong lane arbitration, or materialization loss, it should be fixed as Post-F integrity work;
|
||||
- if a failure asks for unsupported breadth outside the current primitive/search surface, it should become next-slice bounded enablement work;
|
||||
- if a failure depends on old accidental behavior that violates the new evidence model, the old expectation must be rejected rather than restored.
|
||||
|
||||
The `assistant-stage1-9liEOh-7JP` failures were in the first category:
|
||||
|
||||
- VAT purchase-date and February 2017 were supported, but the route/materialization arbitration was wrong;
|
||||
- highest-value customer was supported, but fell into the wrong lifecycle contour;
|
||||
- Chepurnov item-flow was supported, but stale inventory focus could hijack the question.
|
||||
|
||||
## Current Closure Reading
|
||||
|
||||
Post-F is operationally closed as a module because:
|
||||
|
||||
- the identified stale-scope and materialization seams have code-level guards;
|
||||
- the repaired manual failure slice has live replay evidence;
|
||||
- the cross-stage canary proves older bounded-autonomy contours still survive the new guards;
|
||||
- the working tree was clean after the closure commits.
|
||||
|
||||
Post-F is not a claim that:
|
||||
|
||||
- arbitrary unfamiliar 1C asks are now solved;
|
||||
- `resolveAddressIntent()` no longer carries central pressure;
|
||||
- every future stale-memory seam is impossible.
|
||||
|
||||
The next module should therefore start from "grow open-world bounded autonomy breadth", not from "finish the first Post-F rescue".
|
||||
|
||||
Every new breadth step must keep the Post-F invariants as regression gates.
|
||||
|
||||
## Remaining Risks To Watch
|
||||
|
||||
- central intent pressure is still visible around `addressIntentResolver.ts` and the counterparty signal bridge;
|
||||
- historical docs before `15` are not current status documents and can be misread without this sync layer;
|
||||
- full broad test-suite green was not the primary proof for Post-F closure; semantic replay remains the stronger evidence;
|
||||
- future runtime-job artifacts can still accumulate outside the curated autorun flow and should be committed only when intentionally useful.
|
||||
|
|
@ -34,12 +34,14 @@ This package answers the next question:
|
|||
14. [14 - semantic_dialog_authority_recovery_plan_2026-04-19.md](./14%20-%20semantic_dialog_authority_recovery_plan_2026-04-19.md)
|
||||
15. [15 - mcp_bounded_autonomy_reset_plan_2026-04-21.md](./15%20-%20mcp_bounded_autonomy_reset_plan_2026-04-21.md)
|
||||
16. [16 - data_need_graph_and_open_world_mcp_plan_2026-04-22.md](./16%20-%20data_need_graph_and_open_world_mcp_plan_2026-04-22.md)
|
||||
17. [17 - post_f_semantic_integrity_hardening_2026-04-23.md](./17%20-%20post_f_semantic_integrity_hardening_2026-04-23.md)
|
||||
18. [18 - post_f_code_documentation_sync_2026-04-24.md](./18%20-%20post_f_code_documentation_sync_2026-04-24.md)
|
||||
|
||||
## Current Status Snapshot (2026-04-22)
|
||||
## Current Status Snapshot (2026-04-24)
|
||||
|
||||
This package is no longer planning-only.
|
||||
|
||||
It now documents a turnaround that is already operational in code, already materially past the acute regression breakpoint, and already moved into bounded MCP autonomy work beyond the first stabilization wave:
|
||||
It now documents a turnaround that is already operational in code, already materially past the acute regression breakpoint, and already moved through the bounded MCP autonomy build-out into the next semantic hardening layer:
|
||||
|
||||
- route, transition, boundary, meta, memory, and provider policy owners exist as separate modules;
|
||||
- exact-lane truth and coverage/evidence contracts exist as explicit runtime artifacts;
|
||||
|
|
@ -47,50 +49,68 @@ It now documents a turnaround that is already operational in code, already mater
|
|||
- AGENT semantic packs and source catalogs already exist for mixed domain/meta validation.
|
||||
- the reset toward `MCP-first bounded autonomy` is now formalized;
|
||||
- `Big Block A/B/C` of that reset are now closed in runtime code and replay-backed;
|
||||
- the next architecture mainline is no longer continuity polishing, but `D/E/F`:
|
||||
- `Big Block D/E/F` are now also materially closed in runtime code and replay-backed:
|
||||
- `Question -> Data Need Graph`
|
||||
- dynamic schema traversal and primitive search
|
||||
- multi-hop evidence loop with bounded clarification recovery
|
||||
- the current architecture mainline is now `Post-F Semantic Integrity Hardening`:
|
||||
- protect grounded subject integrity against stale scope contamination
|
||||
- protect exact and planner-selected pivots from metadata/discovery drift
|
||||
- keep temporal continuity and repeated lane switches semantically stable
|
||||
- recover already-supported questions that still look broken to a human user
|
||||
- the Post-F module is now operationally closed as a hardening slice:
|
||||
- code fix commit: `739e8b8 Post-F: закрыть ручные провалы НДС, выручки и item-flow`
|
||||
- runtime artifact tail commit: `837e1fe Post-F: сохранить хвосты ручных runtime-прогонов`
|
||||
- live map sync: [18 - post_f_code_documentation_sync_2026-04-24.md](./18%20-%20post_f_code_documentation_sync_2026-04-24.md)
|
||||
|
||||
Current honest status:
|
||||
|
||||
- turnaround implementation progress: `~96%`
|
||||
- exit-from-danger-zone readiness: `~91%`
|
||||
- pre-multidomain readiness: `~78%`
|
||||
- bounded-autonomy foundation readiness: `~60%`
|
||||
- graph snapshot after latest rebuild: `5741 nodes`, `12385 edges`, `137 communities`
|
||||
- turnaround implementation progress: `~99%`
|
||||
- exit-from-danger-zone readiness: `~97%`
|
||||
- pre-multidomain readiness: `~90%`
|
||||
- bounded-autonomy foundation readiness: `~89%`
|
||||
- open-world bounded-autonomy readiness: `~75%`
|
||||
- Post-F semantic integrity module progress: `~99%` operationally closed, with remaining risk now treated as next-slice discovery rather than an open blocker inside the closed slice
|
||||
- graph snapshot after latest rebuild: `5892 nodes`, `12772 edges`, `137 communities`
|
||||
- current breakpoint:
|
||||
- the validated hot paths are no longer structurally broken;
|
||||
- flagship continuity collapse is no longer the primary risk;
|
||||
- the main remaining risk is no longer clarification-resume collapse, but the unfinished shift from bounded reviewed chains toward open-world data-need-driven MCP planning;
|
||||
- pure wording polish is now secondary debt, but semantic robustness plus open-world evidence navigation is now a first-class blocker;
|
||||
- the practical product risk is no longer only "the route collapsed", but "the assistant still cannot yet understand and explore many non-preworked 1C questions on its own".
|
||||
- the main remaining risk is no longer "A/B/C or D/E/F do not exist", but "already-supported semantic chains can still be contaminated by stale scope, legacy focus state, or wrong post-pivot arbitration";
|
||||
- pure wording polish remains secondary debt, but semantic integrity and explicit-subject protection are now first-class blockers;
|
||||
- the practical product risk is no longer only "the route collapsed", but "the user can still occasionally see a semantically wrong answer on a question that the architecture should already support".
|
||||
- main remaining architectural pressure:
|
||||
- no general `Question -> Data Need Graph` authority yet
|
||||
- planner chain selection is still reviewed-family bounded rather than open-world over the primitive catalog
|
||||
- schema traversal is still narrower than the intended arbitrary 1C blast radius
|
||||
- multi-hop evidence recovery is still too shallow for unfamiliar asks
|
||||
- open-world breadth is still narrower than the intended arbitrary 1C blast radius
|
||||
- planner-selected chains are now real, but still not broad enough to cover unfamiliar 1C asks without additional primitive/search growth
|
||||
- semantic integrity can still fail on stale carryover, repeated pivots, and mixed scope contamination if those seams are not replay-hardened
|
||||
- central domain-intent pressure inside `resolveAddressIntent()`
|
||||
- replay breadth is still below the future open-world autonomy surface
|
||||
|
||||
Latest live proof now includes:
|
||||
|
||||
- `address_truth_harness_phase12_wider_saved_session_pool_live_20260419_rerun16` accepted `20/20`
|
||||
- `address_truth_harness_phase14_counterparty_tail_resume_live_20260418_rerun2` accepted `10/10`
|
||||
- `address_truth_harness_phase15_answer_inspection_followup_live_20260419_rerun11` accepted `9/9`
|
||||
- `address_truth_harness_phase16_multicompany_late_pivot_live_20260419_rerun10` accepted
|
||||
- `address_truth_harness_phase17_clarification_resume_and_counterparty_tail_live_20260419_rerun5` accepted `10/10`
|
||||
- `address_truth_harness_phase24_metadata_lane_choice_loop_live_rerun5` accepted
|
||||
- `address_truth_harness_phase25_entity_resolution_chain_live_rerun_full_chain` accepted
|
||||
- `address_truth_harness_phase24_metadata_lane_choice_loop_live_rerun14` accepted
|
||||
- `address_truth_harness_phase32_planner_selected_chain_end_to_end_live_rerun2` accepted `6/6`
|
||||
- `address_truth_harness_phase42_catalog_metadata_drilldown_live_rerun2` accepted
|
||||
- `address_truth_harness_phase45_multi_hop_open_total_clarification_loop_live_rerun2` accepted
|
||||
- `address_truth_harness_phase67_svk_grounded_counterparty_integrity_live_rerun_vatfix` accepted
|
||||
- `address_truth_harness_phase68_referential_document_followup_integrity_live_rerun1` accepted
|
||||
- `address_truth_harness_phase69_document_to_payments_pronoun_pivot_live_rerun3` accepted
|
||||
- `address_truth_harness_phase72_document_to_contracts_year_switch_live_rerun3` accepted
|
||||
- `address_truth_harness_phase80_payments_to_contracts_to_documents_all_time_live_rerun1` accepted
|
||||
- `address_truth_harness_phase82_human_mixed_integrity_status_dialog_post_m23_rerun_documents_scope_bidirectional` accepted `19/19`
|
||||
- `address_truth_harness_phase82_human_mixed_integrity_status_dialog_post_f_account_injection_guard_clean_scope` accepted `19/19`, with the `Жуковке 51` numeric counterparty suffix kept as counterparty scope instead of leaking as account `51`
|
||||
- `address_truth_harness_post_f_cross_stage_canary_agent_20260424_live7` accepted `24/24`, proving a saved cross-stage AGENT canary across VAT metadata, metadata-scoped organization/document pivots, numeric counterparty suffixes, open-organization value-flow clarification, ranked value-flow year switches, and SVK grounded reset; the saved autorun is `AGENT | Post-F cross-stage semantic integrity canary` (`gen-ag04241406-abe4d8`)
|
||||
- `address_truth_harness_post_f_manual_failures_20260424_live3` accepted `11/11`, proving the manual failure slice from `assistant-stage1-9liEOh-7JP`: VAT purchase-date, VAT February 2017, highest-value customer, and Chepurnov item-flow after stale inventory context; the saved autorun is `AGENT | Post-F ручные провалы VAT revenue item-flow live3` (`gen-ag04241710-bdb248`)
|
||||
- `address_truth_harness_phase11_manual_followup_meta_quality_live_rerun_vatfix` accepted `10/10`
|
||||
- `address_truth_harness_phase20_continuity_stabilization_live_rerun_vatfix` accepted `6/6`
|
||||
- `addressQueryRuntimeM23.test.ts` full semantic/runtime slice accepted `403/403` after Post-F VAT/date-basis, scope-recovery, open value-flow organization clarification, document-vs-bank arbitration, and reply-shape hardening
|
||||
|
||||
Current architectural reading:
|
||||
|
||||
- the system is already materially past the dangerous regression breakpoint;
|
||||
- it is now safe for continued architecture hardening and controlled domain-by-domain enablement under replay gates;
|
||||
- it is now materially closer to pre-multidomain stability, but still not safe to declare broad low-risk multi-domain expansion.
|
||||
- the practical next target is no longer only `90%+ pre-multidomain readiness`, but the first believable `open-world bounded autonomy` over 1C evidence.
|
||||
- from this point onward, readiness must be judged not only by route truth and replay pass rate, but also by whether a new human user can ask a structurally new 1C data question and still get a bounded, evidence-honest answer path.
|
||||
- it is materially closer to pre-multidomain stability, but still not safe to declare broad low-risk expansion over arbitrary unfamiliar 1C questions.
|
||||
- the practical next target is no longer Post-F rescue itself; it is broader open-world bounded autonomy over 1C evidence while preserving the Post-F semantic-integrity invariants as regression gates.
|
||||
- from this point onward, readiness must be judged not only by route truth and replay pass rate, but also by whether already-supported questions stay semantically correct through stale memory, pivots, clarifications, and mixed scope resets.
|
||||
|
||||
For the detailed audit, current percentages, and remaining debt, read:
|
||||
|
||||
|
|
@ -103,6 +123,7 @@ For the detailed audit, current percentages, and remaining debt, read:
|
|||
- [14 - semantic_dialog_authority_recovery_plan_2026-04-19.md](./14%20-%20semantic_dialog_authority_recovery_plan_2026-04-19.md)
|
||||
- [15 - mcp_bounded_autonomy_reset_plan_2026-04-21.md](./15%20-%20mcp_bounded_autonomy_reset_plan_2026-04-21.md)
|
||||
- [16 - data_need_graph_and_open_world_mcp_plan_2026-04-22.md](./16%20-%20data_need_graph_and_open_world_mcp_plan_2026-04-22.md)
|
||||
- [17 - post_f_semantic_integrity_hardening_2026-04-23.md](./17%20-%20post_f_semantic_integrity_hardening_2026-04-23.md)
|
||||
|
||||
## Architectural Objects Of Planning
|
||||
|
||||
|
|
@ -137,6 +158,8 @@ Read in this order:
|
|||
15. `14 - semantic_dialog_authority_recovery_plan_2026-04-19.md`
|
||||
16. `15 - mcp_bounded_autonomy_reset_plan_2026-04-21.md`
|
||||
17. `16 - data_need_graph_and_open_world_mcp_plan_2026-04-22.md`
|
||||
18. `17 - post_f_semantic_integrity_hardening_2026-04-23.md`
|
||||
19. `18 - post_f_code_documentation_sync_2026-04-24.md`
|
||||
|
||||
## Planning Rules
|
||||
|
||||
|
|
@ -156,14 +179,16 @@ and start being described as:
|
|||
|
||||
- "a stateful exact-data assistant with explicit transition contracts and isolated truth gating."
|
||||
|
||||
As of `2026-04-22`, the project is already materially closer to the target description and is no longer in the same acute collapse state. The remaining blocker is no longer the original continuity failure itself, but the unfinished convergence from reviewed bounded MCP chains toward open-world data-need-driven autonomy with replay breadth still below the future blast radius.
|
||||
As of `2026-04-24`, the project is already materially closer to the target description and is no longer in the same acute collapse state. The remaining blocker is no longer the original continuity failure itself, no longer the A/B/C or D/E/F build-out, and no longer the first Post-F rescue slice. The active blocker is now the combination of:
|
||||
|
||||
- unfinished convergence from reviewed bounded MCP chains toward broader open-world autonomy;
|
||||
- continued use of Post-F semantic integrity invariants as regression gates while that breadth grows.
|
||||
|
||||
The biggest remaining blockers are:
|
||||
|
||||
- no general `Question -> Data Need Graph` runtime authority yet;
|
||||
- planner-selected primitive chains are real, but still narrower than open-world primitive search;
|
||||
- dynamic schema traversal is not yet broad enough for unfamiliar 1C asks outside the repaired families;
|
||||
- multi-hop evidence recovery still depends on bounded reviewed seams and not yet on a general exploration loop;
|
||||
- broader open-world primitive search is still narrower than the future arbitrary 1C blast radius;
|
||||
- dynamic schema traversal is still not broad enough for many unfamiliar 1C asks outside the repaired families;
|
||||
- new stale-scope or post-pivot seams may still appear in future breadth work and must be treated as regression-gated semantic defects, not as wording polish;
|
||||
- residual `assistantService` overload;
|
||||
- central intent pressure in `resolveAddressIntent()`;
|
||||
- semantic robustness gaps where already-supported questions can still look broken to a human user because of typo sensitivity, short follow-up retarget loss, or human-answer mismatch.
|
||||
- semantic robustness gaps may still appear where already-supported questions meet new wording, typo pressure, short follow-up retargets, or human-answer mismatch.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase43_multi_hop_ranking_clarification_loop",
|
||||
"domain": "address_phase43_multi_hop_ranking_clarification_loop",
|
||||
"title": "Phase 43 multi-hop ranking clarification loop",
|
||||
"description": "Targeted AGENT replay for Big Block F where an open-scope ranking question must ask for both organization and period, then keep the same ranking loop after an organization-only clarification, and finally answer after the second clarification provides the period.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_ranking_requires_org_and_period",
|
||||
"title": "Generic ranking question asks for both organization and period",
|
||||
"question": "Кто больше всего принес денег?",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)уточн|нужно",
|
||||
"(?i)организац",
|
||||
"(?i)период"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните контрагента",
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)по какому контрагенту",
|
||||
"(?i)не найдено контрагента"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["value_flow_ranking", "multi_hop_clarification", "organization_scope", "period_scope", "bounded_autonomy"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_org_only_clarification_keeps_same_loop",
|
||||
"title": "Organization-only clarification preserves the same ranking loop and asks only for the period",
|
||||
"question": "по ООО Альтернатива Плюс",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)уточн|нужно",
|
||||
"(?i)период"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)организац",
|
||||
"(?i)уточните контрагента",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["value_flow_ranking", "multi_hop_clarification", "organization_followup_reuse", "bounded_autonomy"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_period_clarification_completes_same_ranking_loop",
|
||||
"title": "Period clarification completes the same ranking loop and yields a bounded ranking answer",
|
||||
"question": "за 2020 год",
|
||||
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2020",
|
||||
"(?i)клиент|контрагент|заказчик",
|
||||
"(?i)руб"
|
||||
],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)больше всего|топ|самый доходный|наибол",
|
||||
"(?i)альтернатива",
|
||||
"(?i)проверенн|найденн"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните организацию",
|
||||
"(?i)уточните период",
|
||||
"(?i)уточните контрагента",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["value_flow_ranking", "multi_hop_clarification", "period_followup_reuse", "bounded_autonomy"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase44_multi_hop_comparison_clarification_loop",
|
||||
"domain": "address_phase44_multi_hop_comparison_clarification_loop",
|
||||
"title": "Phase 44 multi-hop comparison clarification loop",
|
||||
"description": "Targeted AGENT replay for Big Block F where an open-scope incoming-vs-outgoing comparison must ask for both organization and period, then keep the same comparison loop after an organization-only clarification, and finally answer after the second clarification provides the period.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_comparison_requires_org_and_period",
|
||||
"title": "Generic incoming-vs-outgoing comparison asks for both organization and period",
|
||||
"question": "Что больше: входящих денег или исходящих?",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage", "factual_with_explanation"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)уточн|нужно",
|
||||
"(?i)организац",
|
||||
"(?i)период"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните контрагента",
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)по какому контрагенту",
|
||||
"(?i)не найдено контрагента"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["value_flow_comparison", "multi_hop_clarification", "organization_scope", "period_scope", "bounded_autonomy"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_org_only_clarification_keeps_same_comparison_loop",
|
||||
"title": "Organization-only clarification preserves the same comparison loop and asks only for the period",
|
||||
"question": "по ООО Альтернатива Плюс",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)уточн|нужно",
|
||||
"(?i)период"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)организац",
|
||||
"(?i)уточните контрагента",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["value_flow_comparison", "multi_hop_clarification", "organization_followup_reuse", "bounded_autonomy"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_period_clarification_completes_same_comparison_loop",
|
||||
"title": "Period clarification completes the same comparison loop and yields a bounded comparison answer",
|
||||
"question": "за 2020 год",
|
||||
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2020",
|
||||
"(?i)входящ|исходящ|получ|заплат|списан|платеж",
|
||||
"(?i)руб"
|
||||
],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)больше|меньше|превыш|разниц",
|
||||
"(?i)альтернатива",
|
||||
"(?i)проверенн|найденн"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните организацию",
|
||||
"(?i)уточните период",
|
||||
"(?i)уточните контрагента",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["value_flow_comparison", "multi_hop_clarification", "period_followup_reuse", "bounded_autonomy"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase45_multi_hop_open_total_clarification_loop",
|
||||
"domain": "address_phase45_multi_hop_open_total_clarification_loop",
|
||||
"title": "Phase 45 multi-hop open total clarification loop",
|
||||
"description": "Targeted AGENT replay for Big Block F where an open-scope incoming total must ask for both organization and period, then keep the same total loop after an organization-only clarification, and finally answer after the second clarification provides the period.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_open_total_requires_org_and_period",
|
||||
"title": "Generic incoming total asks for both organization and period",
|
||||
"question": "Сколько входящих денег?",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage", "factual_with_explanation"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)уточн|нужно",
|
||||
"(?i)организац",
|
||||
"(?i)период"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните контрагента",
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)по какому контрагенту",
|
||||
"(?i)не найдено контрагента"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["value_flow_total", "multi_hop_clarification", "organization_scope", "period_scope", "bounded_autonomy"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_org_only_clarification_keeps_same_total_loop",
|
||||
"title": "Organization-only clarification preserves the same total loop and asks only for the period",
|
||||
"question": "по ООО Альтернатива Плюс",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage", "factual_with_explanation"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)уточн|нужно",
|
||||
"(?i)период"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)организац",
|
||||
"(?i)уточните контрагента",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["value_flow_total", "multi_hop_clarification", "organization_followup_reuse", "bounded_autonomy"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_period_clarification_completes_same_total_loop",
|
||||
"title": "Period clarification completes the same total loop and yields a bounded incoming total answer",
|
||||
"question": "за 2020 год",
|
||||
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2020",
|
||||
"(?i)входящ|получ|поступ",
|
||||
"(?i)руб"
|
||||
],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)подтвержден|проверенн|найденн",
|
||||
"(?i)альтернатива",
|
||||
"(?i)сумм"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните организацию",
|
||||
"(?i)уточните период",
|
||||
"(?i)уточните контрагента",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["value_flow_total", "multi_hop_clarification", "period_followup_reuse", "bounded_autonomy"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase46_multi_hop_open_net_clarification_loop",
|
||||
"domain": "address_phase46_multi_hop_open_net_clarification_loop",
|
||||
"title": "Phase 46 multi-hop open net clarification loop",
|
||||
"description": "Targeted AGENT replay for Big Block F where an open-scope net money-flow question must ask for both organization and period, then keep the same bidirectional net loop after an organization-only clarification, and finally answer after the second clarification provides the period.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_open_net_requires_org_and_period",
|
||||
"title": "Generic net value-flow asks for both organization and period",
|
||||
"question": "Какое нетто по деньгам: сколько получили и сколько заплатили?",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage", "factual_with_explanation"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)уточн|нужно",
|
||||
"(?i)организац",
|
||||
"(?i)период"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните контрагента",
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)по какому контрагенту",
|
||||
"(?i)не найдено контрагента"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["value_flow_net", "multi_hop_clarification", "organization_scope", "period_scope", "bounded_autonomy"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_org_only_clarification_keeps_same_net_loop",
|
||||
"title": "Organization-only clarification preserves the same net loop and asks only for the period",
|
||||
"question": "по ООО Альтернатива Плюс",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage", "factual_with_explanation"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)уточн|нужно",
|
||||
"(?i)период"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)организац",
|
||||
"(?i)уточните контрагента",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["value_flow_net", "multi_hop_clarification", "organization_followup_reuse", "bounded_autonomy"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_period_clarification_completes_same_net_loop",
|
||||
"title": "Period clarification completes the same net loop and yields a bounded net answer",
|
||||
"question": "за 2020 год",
|
||||
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2020",
|
||||
"(?i)получ|входящ|поступ",
|
||||
"(?i)заплат|исходящ|списан|платеж",
|
||||
"(?i)нетто|сальдо|разниц",
|
||||
"(?i)руб"
|
||||
],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)альтернатива",
|
||||
"(?i)проверенн|найденн"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните организацию",
|
||||
"(?i)уточните период",
|
||||
"(?i)уточните контрагента",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["value_flow_net", "multi_hop_clarification", "period_followup_reuse", "bounded_autonomy"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase47_multi_hop_open_net_period_first_clarification_loop",
|
||||
"domain": "address_phase47_multi_hop_open_net_period_first_clarification_loop",
|
||||
"title": "Phase 47 multi-hop open net period-first clarification loop",
|
||||
"description": "Targeted AGENT replay for Big Block F where an open-scope net question asks for both organization and period, then keeps the same net loop after a period-only clarification, and finally answers after the second clarification provides the organization.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_open_net_requires_org_and_period",
|
||||
"title": "Open net question asks for both organization and period",
|
||||
"question": "Какое нетто по деньгам: сколько получили и сколько заплатили?",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage", "factual_with_explanation"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)уточн|нужно",
|
||||
"(?i)организац",
|
||||
"(?i)период"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните контрагента",
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)по какому контрагенту",
|
||||
"(?i)не найдено контрагента"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["open_scope_net", "multi_hop_clarification", "organization_scope", "period_scope", "bounded_autonomy"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_period_only_clarification_keeps_same_net_loop",
|
||||
"title": "Period-only clarification preserves the same net loop and asks only for the organization",
|
||||
"question": "за 2020 год",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)уточн|нужно",
|
||||
"(?i)организац"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)период",
|
||||
"(?i)уточните контрагента",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["open_scope_net", "multi_hop_clarification", "period_followup_reuse", "bounded_autonomy"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_org_clarification_completes_same_net_loop",
|
||||
"title": "Organization clarification completes the same net loop and yields a bounded net answer",
|
||||
"question": "по ООО Альтернатива Плюс",
|
||||
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2020",
|
||||
"(?i)получ|входящ|заплат|исходящ|нетто",
|
||||
"(?i)руб"
|
||||
],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)альтернатива",
|
||||
"(?i)проверенн|найденн",
|
||||
"(?i)разниц|нетто"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните организацию",
|
||||
"(?i)уточните период",
|
||||
"(?i)уточните контрагента",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["open_scope_net", "multi_hop_clarification", "organization_followup_reuse", "bounded_autonomy"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase48_multi_hop_open_total_all_time_clarification_loop",
|
||||
"domain": "address_phase48_multi_hop_open_total_all_time_clarification_loop",
|
||||
"title": "Phase 48 multi-hop open total all-time clarification loop",
|
||||
"description": "Targeted AGENT replay for Big Block F where an open-scope incoming-total question asks for both organization and period, then keeps the same total loop after an organization-only clarification, and finally accepts 'за все время' as the period clarification that clears the remaining gap and produces a bounded answer.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_open_total_requires_org_and_period",
|
||||
"title": "Open total question asks for both organization and period",
|
||||
"question": "Сколько вообще входящих денег было?",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage", "factual_with_explanation"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)уточн|нужно",
|
||||
"(?i)организац",
|
||||
"(?i)период"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните контрагента",
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)по какому контрагенту",
|
||||
"(?i)не найдено контрагента"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["open_scope_total", "multi_hop_clarification", "organization_scope", "period_scope", "bounded_autonomy"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_org_only_clarification_keeps_same_total_loop",
|
||||
"title": "Organization-only clarification preserves the same total loop and asks only for the period",
|
||||
"question": "по ООО Альтернатива Плюс",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)уточн|нужно",
|
||||
"(?i)период"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)организац",
|
||||
"(?i)уточните контрагента",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["open_scope_total", "multi_hop_clarification", "organization_followup_reuse", "bounded_autonomy"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_all_time_clarification_completes_same_total_loop",
|
||||
"title": "All-time clarification clears the remaining period gap and yields a bounded total answer",
|
||||
"question": "за все время",
|
||||
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)входящ|получ",
|
||||
"(?i)руб"
|
||||
],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)все время|доступное время|проверенн",
|
||||
"(?i)альтернатива",
|
||||
"(?i)найденн"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните организацию",
|
||||
"(?i)уточните период",
|
||||
"(?i)уточните контрагента",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["open_scope_total", "all_time_scope", "multi_hop_clarification", "bounded_autonomy"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase49_multi_hop_open_ranking_all_time_clarification_loop",
|
||||
"domain": "address_phase49_multi_hop_open_ranking_all_time_clarification_loop",
|
||||
"title": "Phase 49 multi-hop open ranking all-time clarification loop",
|
||||
"description": "Targeted AGENT replay for Big Block F where an open-scope ranking question asks for both organization and period, then keeps the same ranking loop after an organization-only clarification, and finally accepts 'за все время' as the period clarification that clears the remaining gap and produces a bounded ranking answer.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_open_ranking_requires_org_and_period",
|
||||
"title": "Open ranking question asks for both organization and period",
|
||||
"question": "Кто больше всего принес денег?",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage", "factual_with_explanation"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)уточн|нужно",
|
||||
"(?i)организац",
|
||||
"(?i)период"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните контрагента",
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)по какому контрагенту",
|
||||
"(?i)не найдено контрагента"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["value_flow_ranking", "multi_hop_clarification", "organization_scope", "period_scope", "bounded_autonomy"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_org_only_clarification_keeps_same_ranking_loop",
|
||||
"title": "Organization-only clarification preserves the same ranking loop and asks only for the period",
|
||||
"question": "по ООО Альтернатива Плюс",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)уточн|нужно",
|
||||
"(?i)период"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)организац",
|
||||
"(?i)уточните контрагента",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["value_flow_ranking", "multi_hop_clarification", "organization_followup_reuse", "bounded_autonomy"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_all_time_clarification_completes_same_ranking_loop",
|
||||
"title": "All-time clarification clears the remaining period gap and yields a bounded ranking answer",
|
||||
"question": "за все время",
|
||||
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)клиент|контрагент|заказчик",
|
||||
"(?i)руб"
|
||||
],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)все время|доступное время|проверенн",
|
||||
"(?i)альтернатива",
|
||||
"(?i)больше всего|топ|самый доходный|наибол"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните организацию",
|
||||
"(?i)уточните период",
|
||||
"(?i)уточните контрагента",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["value_flow_ranking", "all_time_scope", "multi_hop_clarification", "bounded_autonomy"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase50_metadata_movement_org_period_clarification_loop",
|
||||
"domain": "address_phase50_metadata_movement_org_period_clarification_loop",
|
||||
"title": "Phase 50 metadata movement organization and period clarification loop",
|
||||
"description": "Targeted AGENT replay for Big Block F where a metadata-born movement lane must keep both remaining gaps visible, then preserve the same loop after an organization-only clarification and ask only for the period.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_metadata_ambiguity_surface",
|
||||
"title": "Metadata ambiguity is surfaced honestly for VAT",
|
||||
"question": "какие объекты 1С есть по НДС?",
|
||||
"allowed_reply_types": ["partial_coverage", "factual_with_explanation"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)metadata|метадан",
|
||||
"(?i)ндс",
|
||||
"(?i)документ|регистр"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)получили",
|
||||
"(?i)заплатили",
|
||||
"(?i)нетто"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["metadata_surface", "mixed_ambiguity"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_neutral_followup_requires_lane_choice",
|
||||
"title": "Neutral follow-up still requires lane choice",
|
||||
"question": "давай дальше",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)документ",
|
||||
"(?i)движени|регистр",
|
||||
"(?i)уточн|выб(ери|рать)|какой контур"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["metadata_lane_choice_clarification", "neutral_followup"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_movement_lane_requires_org_and_period",
|
||||
"title": "Movement lane keeps both remaining gaps visible",
|
||||
"question": "по движениям",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)движени|регистр",
|
||||
"(?i)организац",
|
||||
"(?i)период"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните контрагента",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["movement_lane_after_clarification", "remaining_gaps", "organization_scope", "period_scope"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_org_only_clarification_keeps_same_movement_loop",
|
||||
"title": "Organization-only clarification preserves the same movement loop and asks only for the period",
|
||||
"question": "по ООО Альтернатива Плюс",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)движени|регистр",
|
||||
"(?i)период"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)организац",
|
||||
"(?i)уточните контрагента",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["movement_lane_after_clarification", "organization_followup_reuse", "period_scope"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase51_metadata_lane_choice_with_org_inline",
|
||||
"domain": "address_phase51_metadata_lane_choice_with_org_inline",
|
||||
"title": "Phase 51 metadata lane choice with inline organization clarification",
|
||||
"description": "Targeted AGENT replay for Big Block F where the user resolves metadata ambiguity and supplies the organization in the same follow-up, so the movement loop should keep only the period gap alive.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_metadata_ambiguity_surface",
|
||||
"title": "Metadata ambiguity is surfaced honestly for VAT",
|
||||
"question": "какие объекты 1С есть по НДС?",
|
||||
"allowed_reply_types": ["partial_coverage", "factual_with_explanation"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)metadata|метадан",
|
||||
"(?i)ндс",
|
||||
"(?i)документ|регистр"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)получили",
|
||||
"(?i)заплатили",
|
||||
"(?i)нетто"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["metadata_surface", "mixed_ambiguity"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_neutral_followup_requires_lane_choice",
|
||||
"title": "Neutral follow-up still requires lane choice",
|
||||
"question": "давай дальше",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)документ",
|
||||
"(?i)движени|регистр",
|
||||
"(?i)уточн|выб(ери|рать)|какой контур"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["metadata_lane_choice_clarification", "neutral_followup"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_inline_lane_choice_with_org_keeps_only_period_gap",
|
||||
"title": "Movement lane plus organization in one follow-up leaves only the period gap",
|
||||
"question": "по движениям по ООО Альтернатива Плюс",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)движени|регистр",
|
||||
"(?i)период"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .*организац",
|
||||
"(?i)нужн[ао].*организац",
|
||||
"(?i)уточните .*контрагента",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"movement_lane_after_clarification",
|
||||
"inline_organization_clarification",
|
||||
"remaining_period_gap_only"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase52_metadata_movement_full_recovery",
|
||||
"domain": "address_phase52_metadata_movement_full_recovery",
|
||||
"title": "Phase 52 metadata movement full recovery to bounded retrieval",
|
||||
"description": "Targeted AGENT replay for Big Block F where a metadata-born movement lane survives ambiguity resolution, accepts organization and period across follow-ups, and then proceeds into bounded movement retrieval without resetting the proof path.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_metadata_ambiguity_surface",
|
||||
"title": "Metadata ambiguity is surfaced honestly for VAT",
|
||||
"question": "какие объекты 1С есть по НДС?",
|
||||
"allowed_reply_types": ["partial_coverage", "factual_with_explanation"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)metadata|метадан",
|
||||
"(?i)ндс",
|
||||
"(?i)документ|регистр"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)получили",
|
||||
"(?i)заплатили",
|
||||
"(?i)нетто"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["metadata_surface", "mixed_ambiguity"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_neutral_followup_requires_lane_choice",
|
||||
"title": "Neutral follow-up still requires lane choice",
|
||||
"question": "давай дальше",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)документ",
|
||||
"(?i)движени|регистр",
|
||||
"(?i)уточн|выб(ери|рать)|какой контур"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["metadata_lane_choice_clarification", "neutral_followup"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_inline_lane_choice_with_org_keeps_only_period_gap",
|
||||
"title": "Movement lane plus organization in one follow-up leaves only the period gap",
|
||||
"question": "по движениям по ООО Альтернатива Плюс",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)движени|регистр",
|
||||
"(?i)период"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .*организац",
|
||||
"(?i)нужн[ао].*организац",
|
||||
"(?i)уточните .*контрагента",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"movement_lane_after_clarification",
|
||||
"inline_organization_clarification",
|
||||
"remaining_period_gap_only"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_period_clarification_executes_same_movement_loop",
|
||||
"title": "Period clarification executes the same bounded movement loop",
|
||||
"question": "за 2020 год",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)ндс|движени|регистр|операц|платеж|поступлен|списан|строк"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .*организац",
|
||||
"(?i)уточните .*контрагента",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"movement_lane_execution",
|
||||
"period_gap_closed",
|
||||
"bounded_retrieval"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase53_metadata_movement_period_first_recovery",
|
||||
"domain": "address_phase53_metadata_movement_period_first_recovery",
|
||||
"title": "Phase 53 metadata movement period-first recovery",
|
||||
"description": "Targeted AGENT replay for Big Block F where a metadata-born movement lane keeps the same proof path when the user closes the period gap first and the organization gap second.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_metadata_ambiguity_surface",
|
||||
"title": "Metadata ambiguity is surfaced honestly for VAT",
|
||||
"question": "какие объекты 1С есть по НДС?",
|
||||
"allowed_reply_types": ["partial_coverage", "factual_with_explanation"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)metadata|метадан",
|
||||
"(?i)ндс",
|
||||
"(?i)документ|регистр"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["metadata_surface", "mixed_ambiguity"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_neutral_followup_requires_lane_choice",
|
||||
"title": "Neutral follow-up still requires lane choice",
|
||||
"question": "давай дальше",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)документ",
|
||||
"(?i)движени|регистр",
|
||||
"(?i)уточн|выб(ери|рать)|какой контур"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["metadata_lane_choice_clarification", "neutral_followup"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_movement_lane_requires_both_gaps",
|
||||
"title": "Movement lane keeps both remaining gaps visible",
|
||||
"question": "по движениям",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)движени|регистр",
|
||||
"(?i)организац",
|
||||
"(?i)период"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["movement_lane_after_clarification", "remaining_gaps"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_period_first_keeps_only_organization_gap",
|
||||
"title": "Period-first clarification preserves the same movement loop and leaves only organization missing",
|
||||
"question": "за 2020 год",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)организац"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .*период",
|
||||
"(?i)нужн[ао].*период",
|
||||
"(?i)уточните .*контрагента",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["period_first_recovery", "remaining_org_gap_only"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_organization_closes_same_loop_and_executes_retrieval",
|
||||
"title": "Organization clarification closes the last gap and executes the same bounded movement loop",
|
||||
"question": "по ООО Альтернатива Плюс",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)ндс|движени|регистр|операц|платеж|поступлен|списан|строк"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .*организац",
|
||||
"(?i)уточните .*период",
|
||||
"(?i)уточните .*контрагента",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["organization_second_recovery", "bounded_retrieval"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase54_metadata_document_full_recovery",
|
||||
"domain": "address_phase54_metadata_document_full_recovery",
|
||||
"title": "Phase 54 metadata document full recovery",
|
||||
"description": "Targeted AGENT replay for Big Block F where a metadata-born document lane survives ambiguity resolution, accepts organization and period across follow-ups, and then proceeds into bounded document retrieval without resetting the proof path.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_metadata_ambiguity_surface",
|
||||
"title": "Metadata ambiguity is surfaced honestly for VAT",
|
||||
"question": "какие объекты 1С есть по НДС?",
|
||||
"allowed_reply_types": ["partial_coverage", "factual_with_explanation"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)metadata|метадан",
|
||||
"(?i)ндс",
|
||||
"(?i)документ|регистр"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["metadata_surface", "mixed_ambiguity"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_neutral_followup_requires_lane_choice",
|
||||
"title": "Neutral follow-up still requires lane choice",
|
||||
"question": "давай дальше",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)документ",
|
||||
"(?i)движени|регистр",
|
||||
"(?i)уточн|выб(ери|рать)|какой контур"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["metadata_lane_choice_clarification", "neutral_followup"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_inline_document_choice_with_org_keeps_only_period_gap",
|
||||
"title": "Document lane plus organization in one follow-up leaves only the period gap",
|
||||
"question": "по документам по ООО Альтернатива Плюс",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)документ|счет|сч[её]т[- ]?фактур|накладн|акт",
|
||||
"(?i)период"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .*организац",
|
||||
"(?i)нужн[ао].*организац",
|
||||
"(?i)уточните .*контрагента",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"document_lane_after_clarification",
|
||||
"inline_organization_clarification",
|
||||
"remaining_period_gap_only"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_period_clarification_executes_same_document_loop",
|
||||
"title": "Period clarification executes the same bounded document loop",
|
||||
"question": "за 2020 год",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)документ|счет|сч[её]т[- ]?фактур|накладн|акт|строк"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .*организац",
|
||||
"(?i)уточните .*контрагента",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"document_lane_execution",
|
||||
"period_gap_closed",
|
||||
"bounded_retrieval"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase55_metadata_movement_year_switch_after_retrieval",
|
||||
"domain": "address_phase55_metadata_movement_year_switch_after_retrieval",
|
||||
"title": "Phase 55 metadata movement year switch after bounded retrieval",
|
||||
"description": "Targeted AGENT replay for Big Block F where a metadata-born movement loop reaches bounded retrieval and then survives a short year-switch follow-up without losing organization or resetting the lane.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_metadata_ambiguity_surface",
|
||||
"title": "Metadata ambiguity is surfaced honestly for VAT",
|
||||
"question": "какие объекты 1С есть по НДС?",
|
||||
"allowed_reply_types": ["partial_coverage", "factual_with_explanation"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)metadata|метадан",
|
||||
"(?i)ндс",
|
||||
"(?i)документ|регистр"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["metadata_surface", "mixed_ambiguity"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_neutral_followup_requires_lane_choice",
|
||||
"title": "Neutral follow-up still requires lane choice",
|
||||
"question": "давай дальше",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)документ",
|
||||
"(?i)движени|регистр",
|
||||
"(?i)уточн|выб(ери|рать)|какой контур"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["metadata_lane_choice_clarification", "neutral_followup"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_inline_lane_choice_with_org_keeps_only_period_gap",
|
||||
"title": "Movement lane plus organization in one follow-up leaves only the period gap",
|
||||
"question": "по движениям по ООО Альтернатива Плюс",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)движени|регистр",
|
||||
"(?i)период"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .*организац",
|
||||
"(?i)нужн[ао].*организац",
|
||||
"(?i)уточните .*контрагента",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["movement_lane_after_clarification", "inline_organization_clarification"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_period_clarification_executes_same_movement_loop",
|
||||
"title": "Period clarification executes the same bounded movement loop",
|
||||
"question": "за 2020 год",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)ндс|движени|регистр|операц|платеж|поступлен|списан|строк"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .*организац",
|
||||
"(?i)уточните .*контрагента",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["movement_lane_execution", "bounded_retrieval"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_year_switch_keeps_same_movement_loop",
|
||||
"title": "Short year-switch follow-up keeps the same movement lane and organization",
|
||||
"question": "а теперь за 2021?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2021",
|
||||
"(?i)ндс|движени|регистр|операц|платеж|поступлен|списан|строк"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .*организац",
|
||||
"(?i)уточните .*контур",
|
||||
"(?i)документ",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["year_switch_followup", "movement_lane_continuity", "same_organization_reuse"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase56_metadata_retrieval_to_document_pivot",
|
||||
"domain": "address_phase56_metadata_retrieval_to_document_pivot",
|
||||
"title": "Phase 56 metadata retrieval to document pivot",
|
||||
"description": "Targeted AGENT replay for Big Block F where a metadata-born movement loop reaches bounded retrieval and then pivots into document evidence on a short follow-up without losing organization or resetting the scoped proof path.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_metadata_ambiguity_surface",
|
||||
"title": "Metadata ambiguity is surfaced honestly for VAT",
|
||||
"question": "какие объекты 1С есть по НДС?",
|
||||
"allowed_reply_types": ["partial_coverage", "factual_with_explanation"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)metadata|метадан",
|
||||
"(?i)ндс",
|
||||
"(?i)документ|регистр"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["metadata_surface", "mixed_ambiguity"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_neutral_followup_requires_lane_choice",
|
||||
"title": "Neutral follow-up still requires lane choice",
|
||||
"question": "давай дальше",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)документ",
|
||||
"(?i)движени|регистр",
|
||||
"(?i)уточн|выб(ери|рать)|какой контур"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["metadata_lane_choice_clarification", "neutral_followup"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_inline_lane_choice_with_org_keeps_only_period_gap",
|
||||
"title": "Movement lane plus organization in one follow-up leaves only the period gap",
|
||||
"question": "по движениям по ООО Альтернатива Плюс",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)движени|регистр",
|
||||
"(?i)период"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["movement_lane_after_clarification", "inline_organization_clarification"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_period_clarification_executes_same_movement_loop",
|
||||
"title": "Period clarification executes the same bounded movement loop",
|
||||
"question": "за 2020 год",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)ндс|движени|регистр|операц|платеж|поступлен|списан|строк"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["movement_lane_execution", "bounded_retrieval"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_document_pivot_keeps_same_scope",
|
||||
"title": "Short document pivot reuses the same organization and period",
|
||||
"question": "а теперь по документам?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)документ|счет|сч[её]т[- ]?фактур|накладн|акт|строк"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .*организац",
|
||||
"(?i)уточните .*период",
|
||||
"(?i)уточните .*контур",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["document_pivot_after_retrieval", "scope_reuse", "same_proof_path_family_shift"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase57_metadata_movement_all_time_after_retrieval",
|
||||
"domain": "address_phase57_metadata_movement_all_time_after_retrieval",
|
||||
"title": "Phase 57 metadata movement all-time follow-up after bounded retrieval",
|
||||
"description": "Targeted AGENT replay for Big Block F where a metadata-born movement loop reaches bounded retrieval and then survives a short all-time follow-up without losing organization or resetting the movement lane.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_metadata_ambiguity_surface",
|
||||
"title": "Metadata ambiguity is surfaced honestly for VAT",
|
||||
"question": "какие объекты 1С есть по НДС?",
|
||||
"allowed_reply_types": ["partial_coverage", "factual_with_explanation"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)metadata|метадан",
|
||||
"(?i)ндс",
|
||||
"(?i)документ|регистр"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["metadata_surface", "mixed_ambiguity"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_neutral_followup_requires_lane_choice",
|
||||
"title": "Neutral follow-up still requires lane choice",
|
||||
"question": "давай дальше",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)документ",
|
||||
"(?i)движени|регистр",
|
||||
"(?i)уточн|выб(ери|рать)|какой контур"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["metadata_lane_choice_clarification", "neutral_followup"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_inline_lane_choice_with_org_keeps_only_period_gap",
|
||||
"title": "Movement lane plus organization in one follow-up leaves only the period gap",
|
||||
"question": "по движениям по ООО Альтернатива Плюс",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)движени|регистр",
|
||||
"(?i)период"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["movement_lane_after_clarification", "inline_organization_clarification"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_period_clarification_executes_same_movement_loop",
|
||||
"title": "Period clarification executes the same bounded movement loop",
|
||||
"question": "за 2020 год",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)ндс|движени|регистр|операц|платеж|поступлен|списан|строк"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["movement_lane_execution", "bounded_retrieval"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_all_time_followup_keeps_same_movement_loop",
|
||||
"title": "All-time follow-up clears the previous period but keeps the same movement lane and organization",
|
||||
"question": "а теперь за все время?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)все время|доступное время|весь период",
|
||||
"(?i)ндс|движени|регистр|операц|платеж|поступлен|списан|строк"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .*организац",
|
||||
"(?i)уточните .*контур",
|
||||
"(?i)за 2020",
|
||||
"(?i)документ",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["all_time_followup", "movement_lane_continuity", "period_cleared"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase58_metadata_document_to_movement_pivot",
|
||||
"domain": "address_phase58_metadata_document_to_movement_pivot",
|
||||
"title": "Phase 58 metadata document retrieval to movement pivot",
|
||||
"description": "Targeted AGENT replay for Big Block F where a metadata-born document loop reaches bounded retrieval and then pivots into movement evidence on a short follow-up without losing organization or resetting the scoped proof path.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_metadata_ambiguity_surface",
|
||||
"title": "Metadata ambiguity is surfaced honestly for VAT",
|
||||
"question": "какие объекты 1С есть по НДС?",
|
||||
"allowed_reply_types": ["partial_coverage", "factual_with_explanation"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)metadata|метадан",
|
||||
"(?i)ндс",
|
||||
"(?i)документ|регистр"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["metadata_surface", "mixed_ambiguity"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_neutral_followup_requires_lane_choice",
|
||||
"title": "Neutral follow-up still requires lane choice",
|
||||
"question": "давай дальше",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)документ",
|
||||
"(?i)движени|регистр",
|
||||
"(?i)уточн|выб(ери|рать)|какой контур"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["metadata_lane_choice_clarification", "neutral_followup"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_document_lane_with_org_keeps_only_period_gap",
|
||||
"title": "Document lane plus organization in one follow-up leaves only the period gap",
|
||||
"question": "по документам по ООО Альтернатива Плюс",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)документ|счет|накладн|акт",
|
||||
"(?i)период"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["document_lane_after_clarification", "inline_organization_clarification"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_period_clarification_executes_same_document_loop",
|
||||
"title": "Period clarification executes the same bounded document loop",
|
||||
"question": "за 2020 год",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)документ|счет|сч[её]т[- ]?фактур|накладн|акт|строк"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["document_lane_execution", "bounded_retrieval"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_movement_pivot_keeps_same_scope",
|
||||
"title": "Short movement pivot reuses the same organization and period",
|
||||
"question": "а теперь по движениям?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)ндс|движени|регистр|операц|платеж|поступлен|списан|строк"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .*организац",
|
||||
"(?i)уточните .*период",
|
||||
"(?i)уточните .*контур",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["movement_pivot_after_document_retrieval", "scope_reuse", "same_proof_path_family_shift"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase59_metadata_document_all_time_after_retrieval",
|
||||
"domain": "address_phase59_metadata_document_all_time_after_retrieval",
|
||||
"title": "Phase 59 metadata document all-time follow-up after bounded retrieval",
|
||||
"description": "Targeted AGENT replay for Big Block F where a metadata-born document loop reaches bounded retrieval and then survives a short all-time follow-up without losing organization or resetting the document lane.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_metadata_ambiguity_surface",
|
||||
"title": "Metadata ambiguity is surfaced honestly for VAT",
|
||||
"question": "какие объекты 1С есть по НДС?",
|
||||
"allowed_reply_types": ["partial_coverage", "factual_with_explanation"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)metadata|метадан",
|
||||
"(?i)ндс",
|
||||
"(?i)документ|регистр"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["metadata_surface", "mixed_ambiguity"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_neutral_followup_requires_lane_choice",
|
||||
"title": "Neutral follow-up still requires lane choice",
|
||||
"question": "давай дальше",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)документ",
|
||||
"(?i)движени|регистр",
|
||||
"(?i)уточн|выб(ери|рать)|какой контур"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["metadata_lane_choice_clarification", "neutral_followup"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_inline_document_choice_with_org_keeps_only_period_gap",
|
||||
"title": "Document lane plus organization in one follow-up leaves only the period gap",
|
||||
"question": "по документам по ООО Альтернатива Плюс",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)документ|счет|сч[её]т[- ]?фактур|накладн|акт",
|
||||
"(?i)период"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["document_lane_after_clarification", "inline_organization_clarification"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_period_clarification_executes_same_document_loop",
|
||||
"title": "Period clarification executes the same bounded document loop",
|
||||
"question": "за 2020 год",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)документ|счет|сч[её]т[- ]?фактур|накладн|акт|строк"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["document_lane_execution", "bounded_retrieval"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_all_time_followup_keeps_same_document_loop",
|
||||
"title": "All-time follow-up clears the previous period but keeps the same document lane and organization",
|
||||
"question": "а теперь за все время?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)все время|доступное время|весь период",
|
||||
"(?i)документ|счет|сч[её]т[- ]?фактур|накладн|акт|строк"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .*организац",
|
||||
"(?i)уточните .*контур",
|
||||
"(?i)за 2020",
|
||||
"(?i)движени|регистр",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["all_time_followup", "document_lane_continuity", "period_cleared"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase60_metadata_document_pivot_year_switch",
|
||||
"domain": "address_phase60_metadata_document_pivot_year_switch",
|
||||
"title": "Phase 60 metadata document pivot year switch",
|
||||
"description": "Targeted AGENT replay for Big Block F where a metadata-born movement loop reaches bounded retrieval, pivots into document evidence, and then survives a short year-switch follow-up without losing organization or resetting the new document lane.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_metadata_ambiguity_surface",
|
||||
"title": "Metadata ambiguity is surfaced honestly for VAT",
|
||||
"question": "какие объекты 1С есть по НДС?",
|
||||
"allowed_reply_types": ["partial_coverage", "factual_with_explanation"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)metadata|метадан",
|
||||
"(?i)ндс",
|
||||
"(?i)документ|регистр"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["metadata_surface", "mixed_ambiguity"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_neutral_followup_requires_lane_choice",
|
||||
"title": "Neutral follow-up still requires lane choice",
|
||||
"question": "давай дальше",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)документ",
|
||||
"(?i)движени|регистр",
|
||||
"(?i)уточн|выб(ери|рать)|какой контур"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["metadata_lane_choice_clarification", "neutral_followup"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_inline_lane_choice_with_org_keeps_only_period_gap",
|
||||
"title": "Movement lane plus organization in one follow-up leaves only the period gap",
|
||||
"question": "по движениям по ООО Альтернатива Плюс",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)движени|регистр",
|
||||
"(?i)период"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["movement_lane_after_clarification", "inline_organization_clarification"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_period_clarification_executes_same_movement_loop",
|
||||
"title": "Period clarification executes the same bounded movement loop",
|
||||
"question": "за 2020 год",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)ндс|движени|регистр|операц|платеж|поступлен|списан|строк"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["movement_lane_execution", "bounded_retrieval"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_document_pivot_keeps_same_scope",
|
||||
"title": "Short document pivot reuses the same organization and period",
|
||||
"question": "а теперь по документам?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)документ|счет|сч[её]т[- ]?фактур|накладн|акт|строк"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .*организац",
|
||||
"(?i)уточните .*период",
|
||||
"(?i)уточните .*контур",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["document_pivot_after_retrieval", "scope_reuse"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_06_year_switch_keeps_document_lane",
|
||||
"title": "Short year-switch follow-up stays in the document lane after the pivot",
|
||||
"question": "а теперь за 2021?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2021",
|
||||
"(?i)документ|счет|сч[её]т[- ]?фактур|накладн|акт|строк"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .*организац",
|
||||
"(?i)уточните .*контур",
|
||||
"(?i)движени|регистр",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["year_switch_followup", "document_lane_continuity", "post_pivot_continuity"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase61_metadata_movement_pivot_year_switch",
|
||||
"domain": "address_phase61_metadata_movement_pivot_year_switch",
|
||||
"title": "Phase 61 metadata movement pivot year switch",
|
||||
"description": "Targeted AGENT replay for Big Block F where a metadata-born document loop reaches bounded retrieval, pivots into movement evidence, and then survives a short year-switch follow-up without losing the selected lane, organization, or scoped proof path.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_metadata_ambiguity_surface",
|
||||
"title": "Metadata ambiguity is surfaced honestly for VAT",
|
||||
"question": "какие объекты 1С есть по НДС?",
|
||||
"allowed_reply_types": ["partial_coverage", "factual_with_explanation"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)metadata|метадан",
|
||||
"(?i)ндс",
|
||||
"(?i)документ|регистр"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["metadata_surface", "mixed_ambiguity"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_neutral_followup_requires_lane_choice",
|
||||
"title": "Neutral follow-up still requires lane choice",
|
||||
"question": "давай дальше",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)документ",
|
||||
"(?i)движени|регистр",
|
||||
"(?i)уточн|выб(ери|рать)|какой контур"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["metadata_lane_choice_clarification", "neutral_followup"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_document_lane_with_org_keeps_only_period_gap",
|
||||
"title": "Document lane plus organization in one follow-up leaves only the period gap",
|
||||
"question": "по документам по ООО Альтернатива Плюс",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)документ|счет|накладн|акт",
|
||||
"(?i)период"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["document_lane_after_clarification", "inline_organization_clarification"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_period_clarification_executes_same_document_loop",
|
||||
"title": "Period clarification executes the same bounded document loop",
|
||||
"question": "за 2020 год",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)документ|счет|сч[её]т[- ]?фактур|накладн|акт|строк"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["document_lane_execution", "bounded_retrieval"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_movement_pivot_keeps_same_scope",
|
||||
"title": "Short movement pivot reuses the same organization and period",
|
||||
"question": "а теперь по движениям?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)ндс|движени|регистр|операц|платеж|поступлен|списан|строк"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .*организац",
|
||||
"(?i)уточните .*период",
|
||||
"(?i)уточните .*контур",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["movement_pivot_after_document_retrieval", "scope_reuse", "same_proof_path_family_shift"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_06_year_switch_keeps_movement_lane",
|
||||
"title": "Short year-switch stays in the movement lane after the pivot",
|
||||
"question": "а теперь за 2021?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2021",
|
||||
"(?i)ндс|движени|регистр|операц|платеж|поступлен|списан|строк"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)документ|счет|накладн|акт",
|
||||
"(?i)уточните .*организац",
|
||||
"(?i)уточните .*период",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["movement_lane_continuity", "year_switch_after_pivot", "same_scope_new_period"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase62_metadata_movement_pivot_all_time",
|
||||
"domain": "address_phase62_metadata_movement_pivot_all_time",
|
||||
"title": "Phase 62 metadata movement pivot all-time follow-up",
|
||||
"description": "Targeted AGENT replay for Big Block F where a metadata-born document loop reaches bounded retrieval, pivots into movement evidence, and then survives a short all-time follow-up without losing the selected lane, organization, or scoped proof path.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_metadata_ambiguity_surface",
|
||||
"title": "Metadata ambiguity is surfaced honestly for VAT",
|
||||
"question": "какие объекты 1С есть по НДС?",
|
||||
"allowed_reply_types": ["partial_coverage", "factual_with_explanation"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)metadata|метадан",
|
||||
"(?i)ндс",
|
||||
"(?i)документ|регистр"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["metadata_surface", "mixed_ambiguity"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_neutral_followup_requires_lane_choice",
|
||||
"title": "Neutral follow-up still requires lane choice",
|
||||
"question": "давай дальше",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)документ",
|
||||
"(?i)движени|регистр",
|
||||
"(?i)уточн|выб(ери|рать)|какой контур"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["metadata_lane_choice_clarification", "neutral_followup"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_document_lane_with_org_keeps_only_period_gap",
|
||||
"title": "Document lane plus organization in one follow-up leaves only the period gap",
|
||||
"question": "по документам по ООО Альтернатива Плюс",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)документ|счет|накладн|акт",
|
||||
"(?i)период"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["document_lane_after_clarification", "inline_organization_clarification"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_period_clarification_executes_same_document_loop",
|
||||
"title": "Period clarification executes the same bounded document loop",
|
||||
"question": "за 2020 год",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)документ|счет|сч[её]т[- ]?фактур|накладн|акт|строк"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["document_lane_execution", "bounded_retrieval"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_movement_pivot_keeps_same_scope",
|
||||
"title": "Short movement pivot reuses the same organization and period",
|
||||
"question": "а теперь по движениям?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)ндс|движени|регистр|операц|платеж|поступлен|списан|строк"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .*организац",
|
||||
"(?i)уточните .*период",
|
||||
"(?i)уточните .*контур",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["movement_pivot_after_document_retrieval", "scope_reuse", "same_proof_path_family_shift"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_06_all_time_followup_keeps_movement_lane",
|
||||
"title": "All-time follow-up clears the previous period but stays in the movement lane after the pivot",
|
||||
"question": "а теперь за все время?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)все время|доступное время|весь период",
|
||||
"(?i)ндс|движени|регистр|операц|платеж|поступлен|списан|строк"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)за 2020",
|
||||
"(?i)документ|счет|накладн|акт",
|
||||
"(?i)уточните .*организац",
|
||||
"(?i)уточните .*контур",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["all_time_followup", "movement_lane_continuity", "period_cleared_after_pivot"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase63_metadata_document_pivot_all_time",
|
||||
"domain": "address_phase63_metadata_document_pivot_all_time",
|
||||
"title": "Phase 63 metadata document pivot all-time follow-up",
|
||||
"description": "Targeted AGENT replay for Big Block F where a metadata-born movement loop reaches bounded retrieval, pivots into document evidence, and then survives a short all-time follow-up without losing the selected lane, organization, or scoped proof path.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_metadata_ambiguity_surface",
|
||||
"title": "Metadata ambiguity is surfaced honestly for VAT",
|
||||
"question": "какие объекты 1С есть по НДС?",
|
||||
"allowed_reply_types": ["partial_coverage", "factual_with_explanation"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)metadata|метадан",
|
||||
"(?i)ндс",
|
||||
"(?i)документ|регистр"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["metadata_surface", "mixed_ambiguity"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_neutral_followup_requires_lane_choice",
|
||||
"title": "Neutral follow-up still requires lane choice",
|
||||
"question": "давай дальше",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)документ",
|
||||
"(?i)движени|регистр",
|
||||
"(?i)уточн|выб(ери|рать)|какой контур"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["metadata_lane_choice_clarification", "neutral_followup"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_movement_lane_with_org_keeps_only_period_gap",
|
||||
"title": "Movement lane plus organization in one follow-up leaves only the period gap",
|
||||
"question": "по движениям по ООО Альтернатива Плюс",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)движени|регистр",
|
||||
"(?i)период"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["movement_lane_after_clarification", "inline_organization_clarification"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_period_clarification_executes_same_movement_loop",
|
||||
"title": "Period clarification executes the same bounded movement loop",
|
||||
"question": "за 2020 год",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)ндс|движени|регистр|операц|платеж|поступлен|списан|строк"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["movement_lane_execution", "bounded_retrieval"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_document_pivot_keeps_same_scope",
|
||||
"title": "Short document pivot reuses the same organization and period",
|
||||
"question": "а теперь по документам?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)ндс|документ|счет|сч[её]т[- ]?фактур|накладн|акт|строк"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .*организац",
|
||||
"(?i)уточните .*период",
|
||||
"(?i)уточните .*контур",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["document_pivot_after_movement_retrieval", "scope_reuse", "same_proof_path_family_shift"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_06_all_time_followup_keeps_document_lane",
|
||||
"title": "All-time follow-up clears the previous period but stays in the document lane after the pivot",
|
||||
"question": "а теперь за все время?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)все время|доступное время|весь период",
|
||||
"(?i)ндс|документ|счет|сч[её]т[- ]?фактур|накладн|акт|строк"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)за 2020",
|
||||
"(?i)движени|регистр",
|
||||
"(?i)уточните .*организац",
|
||||
"(?i)уточните .*контур",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["all_time_followup", "document_lane_continuity", "period_cleared_after_pivot"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase64_human_vat_investigation_dialog",
|
||||
"domain": "address_phase64_human_vat_investigation_dialog",
|
||||
"title": "Phase 64 human VAT investigation dialog",
|
||||
"description": "Human-facing AGENT dialog for a finance user who first orients inside VAT-related 1C objects, then deliberately goes into VAT movements for one organization, pivots into supporting documents, and checks year-switch plus all-time continuity without dead filler turns or vague wording.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_metadata_orientation",
|
||||
"title": "The user asks where VAT data lives in 1C",
|
||||
"question": "Мне нужно понять, где в 1С по НДС вообще лежат данные. Какие объекты стоит смотреть по НДС?",
|
||||
"allowed_reply_types": ["partial_coverage", "factual_with_explanation"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)metadata|метадан",
|
||||
"(?i)ндс",
|
||||
"(?i)документ|регистр"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["metadata_surface", "vat_orientation", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_choose_movements_with_org",
|
||||
"title": "The user explicitly chooses the movement lane with an organization",
|
||||
"question": "Хорошо, тогда покажи движения по ООО Альтернатива Плюс.",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)движени|регистр|операц",
|
||||
"(?i)период"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["movement_lane_after_metadata", "inline_organization_clarification", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_execute_movement_slice",
|
||||
"title": "The user provides the year and the movement slice executes",
|
||||
"question": "За 2020 год.",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2020",
|
||||
"(?i)ндс|движени|регистр|операц|платеж|поступлен|списан|строк"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["movement_lane_execution", "bounded_retrieval", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_document_pivot_same_scope",
|
||||
"title": "The user pivots from movements to supporting documents on the same slice",
|
||||
"question": "А теперь по документам?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)документ|счет|сч[её]т[- ]?фактур|накладн|акт|строк"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .*организац",
|
||||
"(?i)уточните .*период",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["document_pivot_after_movement_retrieval", "scope_reuse", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_document_year_switch",
|
||||
"title": "The user asks for the same document slice in another year",
|
||||
"question": "А теперь за 2021 год?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2021",
|
||||
"(?i)документ|счет|сч[её]т[- ]?фактур|накладн|акт|строк"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)движени|регистр",
|
||||
"(?i)уточните .*организац",
|
||||
"(?i)уточните .*период"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["document_lane_continuity", "year_switch_after_pivot", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_06_document_all_time_followup",
|
||||
"title": "The user broadens the same document slice to all available time",
|
||||
"question": "А теперь за все время?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)все доступное время|все время|весь период",
|
||||
"(?i)документ|счет|сч[её]т[- ]?фактур|накладн|акт|строк"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)за 2021",
|
||||
"(?i)движени|регистр",
|
||||
"(?i)уточните .*период"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["document_lane_continuity", "all_time_followup", "human_dialog"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase65_human_svk_money_dialog",
|
||||
"domain": "address_phase65_human_svk_money_dialog",
|
||||
"title": "Phase 65 human SVK money dialog",
|
||||
"description": "Human-facing AGENT dialog for a user who grounds one counterparty in 1C, checks incoming money, outgoing money, and net for one year, then naturally pivots into documents and movements without dead filler turns.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_ground_counterparty",
|
||||
"title": "The user asks to find the counterparty in 1C",
|
||||
"question": "Хочу проверить одного контрагента. Найди в 1С Группу СВК.",
|
||||
"allowed_reply_types": ["partial_coverage", "factual_with_explanation"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)свк",
|
||||
"(?i)контрагент|каталог|1с"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["entity_grounding", "counterparty_resolution", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_incoming_value_flow",
|
||||
"title": "The user asks for incoming money in 2020",
|
||||
"question": "Посмотри, сколько денег мы получили от него за 2020 год.",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2020",
|
||||
"(?i)входящ|поступлен|получ"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["incoming_value_flow", "grounded_counterparty_followup", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_outgoing_value_flow",
|
||||
"title": "The user pivots into outgoing money on the same counterparty and year",
|
||||
"question": "А теперь сколько мы ему заплатили?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)исходящ|списан|заплат"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["outgoing_value_flow", "grounded_counterparty_followup", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_net_value_flow",
|
||||
"title": "The user asks for the same net flow",
|
||||
"question": "А какое получилось нетто?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)нетто|сальдо|чист"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["net_value_flow", "grounded_counterparty_followup", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_document_pivot",
|
||||
"title": "The user asks for documents on the same grounded counterparty",
|
||||
"question": "А по документам?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)документ|счет|сч[её]т[- ]?фактур|накладн|акт|строк"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .*контрагент",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["document_pivot_after_value_flow", "grounded_counterparty_followup", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_06_movement_pivot",
|
||||
"title": "The user asks for movements on the same grounded counterparty",
|
||||
"question": "А по движениям?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)движени|регистр|операц|платеж|поступлен|списан|строк"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .*контрагент",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["movement_pivot_after_value_flow", "grounded_counterparty_followup", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_07_year_switch_same_counterparty",
|
||||
"title": "The user asks for the same contour in 2021",
|
||||
"question": "А теперь за 2021 год?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2021"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["year_switch", "grounded_counterparty_followup", "human_dialog"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase66_human_org_open_scope_dialog",
|
||||
"domain": "address_phase66_human_org_open_scope_dialog",
|
||||
"title": "Phase 66 human open-scope organization money dialog",
|
||||
"description": "Human-facing AGENT dialog for organization-scoped money analytics without a preselected counterparty: the assistant first asks for the organization, then the same dialog continues through all-time follow-up, comparison, and ranking over that one company.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_open_scope_incoming_total",
|
||||
"title": "The user asks for incoming money without naming the organization yet",
|
||||
"question": "Хочу быстрый денежный срез по одной организации без привязки к контрагенту. Сколько вообще входящих денег было за 2020 год?",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)уточн|нужно",
|
||||
"(?i)организац"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["open_scope_total", "organization_scope", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_all_time_same_open_scope",
|
||||
"title": "The user selects the organization and gets the 2020 incoming total",
|
||||
"question": "По ООО Альтернатива Плюс.",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2020",
|
||||
"(?i)входящ|поступлен|получ"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .*контрагент",
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)уточните .*организац"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["organization_clarification", "open_scope_total", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_all_time_same_open_scope",
|
||||
"title": "The user broadens the same organization slice to all available time",
|
||||
"question": "Понял, тогда за все время.",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)все доступное время|все время|весь период",
|
||||
"(?i)входящ|поступлен|получ"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)за 2020",
|
||||
"(?i)уточните .*контрагент",
|
||||
"(?i)уточните .*период",
|
||||
"(?i)уточните .*организац"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["all_time_followup", "organization_scope", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_bidirectional_comparison",
|
||||
"title": "The user asks which money direction is larger for the organization",
|
||||
"question": "Хорошо. А что по ООО Альтернатива Плюс больше в 2020 году: входящие или исходящие деньги?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2020",
|
||||
"(?i)входящ|исходящ|получ|заплат|больше"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["value_flow_comparison", "organization_scope", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_comparison_year_switch",
|
||||
"title": "The user asks the same comparison for another year",
|
||||
"question": "А что по ООО Альтернатива Плюс больше уже за 2021 год: входящие или исходящие деньги?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2021",
|
||||
"(?i)входящ|исходящ|получ|заплат|больше"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["value_flow_comparison", "year_switch", "organization_scope", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_06_ranking_top_counterparty",
|
||||
"title": "The user asks who brought the most money for the organization",
|
||||
"question": "И кто больше всего принес денег этой организации в 2020 году?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2020",
|
||||
"(?i)кто|контрагент|клиент|принес|доход"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["value_flow_ranking", "organization_scope", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_07_ranking_year_switch",
|
||||
"title": "The user asks the same ranking for another year",
|
||||
"question": "А в 2021 году?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2021"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["value_flow_ranking", "year_switch", "organization_scope", "human_dialog"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase67_svk_grounded_counterparty_integrity",
|
||||
"domain": "address_phase67_svk_grounded_counterparty_integrity",
|
||||
"title": "Phase 67 grounded counterparty integrity for SVK dialog",
|
||||
"description": "Replay for the exact human dialog where one grounded counterparty must survive incoming, payout, net, documents, movements, and year-switch without being replaced by a stale focus object.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_ground_counterparty",
|
||||
"title": "Ground the counterparty in 1C",
|
||||
"question": "\u0425\u043e\u0447\u0443 \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043e\u0434\u043d\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430. \u041d\u0430\u0439\u0434\u0438 \u0432 1\u0421 \u0413\u0440\u0443\u043f\u043f\u0443 \u0421\u0412\u041a.",
|
||||
"allowed_reply_types": ["partial_coverage", "factual_with_explanation"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)\u0441\u0432\u043a",
|
||||
"(?i)\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043a\u0430\u0442\u0430\u043b\u043e\u0433|1\u0441"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["entity_grounding", "counterparty_resolution", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_incoming_2020",
|
||||
"title": "Ask about incoming money for 2020",
|
||||
"question": "\u041f\u043e\u0441\u043c\u043e\u0442\u0440\u0438, \u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0434\u0435\u043d\u0435\u0433 \u043c\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438 \u043e\u0442 \u043d\u0435\u0433\u043e \u0437\u0430 2020 \u0433\u043e\u0434.",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)\u0441\u0432\u043a",
|
||||
"(?i)2020",
|
||||
"(?i)\u0432\u0445\u043e\u0434\u044f\u0449|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d|\u043f\u043e\u043b\u0443\u0447"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)\u043d\u043e\u0440\u0442\u043e\u043d"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["incoming_value_flow", "grounded_counterparty_followup", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_payout_2020_same_counterparty",
|
||||
"title": "Ask how much we paid to the same counterparty",
|
||||
"question": "\u0410 \u0442\u0435\u043f\u0435\u0440\u044c \u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043c\u044b \u0435\u043c\u0443 \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)\u0441\u0432\u043a",
|
||||
"(?i)\u0437\u0430\u043f\u043b\u0430\u0442|\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u0441\u043f\u0438\u0441\u0430\u043d|\u043f\u043b\u0430\u0442\u0435\u0436"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)\u043d\u043e\u0440\u0442\u043e\u043d",
|
||||
"(?i)\u0441\u0435\u0440\u0432\u0438\u0441\u043a\u043e\u043d\u0441\u0430\u043b\u0442"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["outgoing_value_flow", "grounded_counterparty_followup", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_net_same_counterparty",
|
||||
"title": "Ask for the same net flow",
|
||||
"question": "\u0410 \u043a\u0430\u043a\u043e\u0435 \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u043e\u0441\u044c \u043d\u0435\u0442\u0442\u043e?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)\u0441\u0432\u043a",
|
||||
"(?i)\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|\u0447\u0438\u0441\u0442"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)\u043d\u043e\u0440\u0442\u043e\u043d",
|
||||
"(?i)\u0441\u0435\u0440\u0432\u0438\u0441\u043a\u043e\u043d\u0441\u0430\u043b\u0442"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["net_value_flow", "grounded_counterparty_followup", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_documents_same_counterparty",
|
||||
"title": "Pivot into documents without losing the counterparty",
|
||||
"question": "\u0410 \u043f\u043e \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u043c?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)\u0441\u0432\u043a",
|
||||
"(?i)\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0441\u0447\u0435\u0442|\u0441\u0447[\u0435\u0451]\u0442[- ]?\u0444\u0430\u043a\u0442\u0443\u0440|\u043d\u0430\u043a\u043b\u0430\u0434\u043d|\u0430\u043a\u0442|\u0441\u0442\u0440\u043e\u043a"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)\u043d\u043e\u0440\u0442\u043e\u043d",
|
||||
"(?i)\u0441\u0435\u0440\u0432\u0438\u0441\u043a\u043e\u043d\u0441\u0430\u043b\u0442",
|
||||
"(?i)\u0443\u0442\u043e\u0447\u043d\u0438\u0442\u0435 .* \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["document_pivot_after_value_flow", "grounded_counterparty_followup", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_06_movements_same_counterparty",
|
||||
"title": "Pivot into movements without losing the counterparty",
|
||||
"question": "\u0410 \u043f\u043e \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f\u043c?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)\u0441\u0432\u043a",
|
||||
"(?i)\u0434\u0432\u0438\u0436\u0435\u043d\u0438|\u0440\u0435\u0433\u0438\u0441\u0442\u0440|\u043e\u043f\u0435\u0440\u0430\u0446|\u043f\u043b\u0430\u0442\u0435\u0436|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d|\u0441\u043f\u0438\u0441\u0430\u043d|\u0441\u0442\u0440\u043e\u043a"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)\u043d\u043e\u0440\u0442\u043e\u043d",
|
||||
"(?i)\u0441\u0435\u0440\u0432\u0438\u0441\u043a\u043e\u043d\u0441\u0430\u043b\u0442",
|
||||
"(?i)\u0443\u0442\u043e\u0447\u043d\u0438\u0442\u0435 .* \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["movement_pivot_after_value_flow", "grounded_counterparty_followup", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_07_year_switch_same_counterparty",
|
||||
"title": "Switch to 2021 without losing the counterparty",
|
||||
"question": "\u0410 \u0442\u0435\u043f\u0435\u0440\u044c \u0437\u0430 2021 \u0433\u043e\u0434?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)\u0441\u0432\u043a",
|
||||
"(?i)2021"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)\u043d\u043e\u0440\u0442\u043e\u043d",
|
||||
"(?i)\u0441\u0435\u0440\u0432\u0438\u0441\u043a\u043e\u043d\u0441\u0430\u043b\u0442"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["year_switch", "grounded_counterparty_followup", "integrity_guard"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase68_referential_document_followup_integrity",
|
||||
"domain": "address_phase68_referential_document_followup_integrity",
|
||||
"title": "Phase 68 referential document follow-up integrity",
|
||||
"description": "Replay for a human document drilldown where a referential follow-up like 'кроме этого документа...' must stay in the exact document contour, keep the prior counterparty, and avoid drifting into metadata discovery.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_documents_by_counterparty",
|
||||
"title": "Open documents for the counterparty",
|
||||
"question": "Покажи документы по Жуковке 51.",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк",
|
||||
"(?i)документ|сч[её]т|акт|накладн|строк"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["documents_by_counterparty", "referential_followup_seed", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_referential_document_followup",
|
||||
"title": "Ask whether there are more documents besides this one",
|
||||
"question": "Кроме этого документа есть еще что-то?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)документ|сч[её]т|акт|накладн|еще"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)метадан",
|
||||
"(?i)схем",
|
||||
"(?i)объект[а-я]* 1с",
|
||||
"(?i)регистр",
|
||||
"(?i)уточните .* контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["referential_document_followup", "counterparty_carryover", "integrity_guard"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase69_document_to_payments_pronoun_pivot",
|
||||
"domain": "address_phase69_document_to_payments_pronoun_pivot",
|
||||
"title": "Phase 69 document to payments pronoun pivot",
|
||||
"description": "Replay for a human follow-up where the user first asks for documents by counterparty and then pivots with 'а по нему платежи?', expecting the same counterparty to survive and the contour to switch to payments.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_documents_by_counterparty",
|
||||
"title": "Open documents for the counterparty",
|
||||
"question": "Покажи документы по Жуковке 51.",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк",
|
||||
"(?i)документ|сч[её]т|акт|накладн|строк"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["documents_by_counterparty", "pronoun_pivot_seed", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_payments_by_pronoun_followup",
|
||||
"title": "Pivot to payments via pronoun follow-up",
|
||||
"question": "А по нему платежи?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)платеж|операц|банк|поступлен|списан"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)метадан",
|
||||
"(?i)схем",
|
||||
"(?i)объект[а-я]* 1с",
|
||||
"(?i)регистр",
|
||||
"(?i)уточните .* контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["payments_followup", "counterparty_pronoun_resolution", "integrity_guard"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase70_document_to_contracts_pronoun_pivot",
|
||||
"domain": "address_phase70_document_to_contracts_pronoun_pivot",
|
||||
"title": "Phase 70 document to contracts pronoun pivot",
|
||||
"description": "Replay for a human follow-up where the user first asks for documents by counterparty and then pivots with 'А по нему договоры?', expecting the same counterparty to survive and the contour to switch to contracts.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_documents_by_counterparty",
|
||||
"title": "Open documents for the counterparty",
|
||||
"question": "Покажи документы по Жуковке 51.",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк",
|
||||
"(?i)документ|сч[её]т|акт|накладн|строк"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["documents_by_counterparty", "pronoun_pivot_seed", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_contracts_by_pronoun_followup",
|
||||
"title": "Pivot to contracts via pronoun follow-up",
|
||||
"question": "А по нему договоры?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)договор|контракт|соглаш"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)метадан",
|
||||
"(?i)схем",
|
||||
"(?i)объект[а-я]* 1с",
|
||||
"(?i)регистр",
|
||||
"(?i)уточните .* контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["contracts_followup", "counterparty_pronoun_resolution", "integrity_guard"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase71_document_to_payments_year_switch",
|
||||
"domain": "address_phase71_document_to_payments_year_switch",
|
||||
"title": "Phase 71 document to payments year-switch continuity",
|
||||
"description": "Replay for a human chain where the user opens documents by counterparty, pivots to payments with a pronoun follow-up, and then switches the year without renaming the counterparty.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_documents_by_counterparty",
|
||||
"title": "Open documents for the counterparty",
|
||||
"question": "Покажи документы по Жуковке 51.",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк",
|
||||
"(?i)документ|сч[её]т|акт|накладн|строк"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["documents_by_counterparty", "pronoun_pivot_seed", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_payments_by_pronoun_followup",
|
||||
"title": "Pivot to payments via pronoun follow-up",
|
||||
"question": "А по нему платежи?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)платеж|операц|банк|поступлен|списан"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["payments_followup", "counterparty_pronoun_resolution", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_year_switch_after_payments_pivot",
|
||||
"title": "Switch the year without renaming the counterparty",
|
||||
"question": "А за 2021?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2021",
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)платеж|операц|банк|поступлен|списан"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .* контрагент",
|
||||
"(?i)метадан",
|
||||
"(?i)схем",
|
||||
"(?i)объект[а-я]* 1с"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["year_switch_after_pivot", "payments_followup", "integrity_guard"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase72_document_to_contracts_year_switch",
|
||||
"domain": "address_phase72_document_to_contracts_year_switch",
|
||||
"title": "Phase 72 document to contracts year-switch continuity",
|
||||
"description": "Replay for a human chain where the user opens documents by counterparty, pivots to contracts with a pronoun follow-up, and then switches the year without renaming the counterparty.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_documents_by_counterparty",
|
||||
"title": "Open documents for the counterparty",
|
||||
"question": "Покажи документы по Жуковке 51.",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк",
|
||||
"(?i)документ|сч[её]т|акт|накладн|строк"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["documents_by_counterparty", "pronoun_pivot_seed", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_contracts_by_pronoun_followup",
|
||||
"title": "Pivot to contracts via pronoun follow-up",
|
||||
"question": "А по нему договоры?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)договор|контракт|соглаш"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["contracts_followup", "counterparty_pronoun_resolution", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_year_switch_after_contracts_pivot",
|
||||
"title": "Switch the year without renaming the counterparty",
|
||||
"question": "А за 2021?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2021",
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)договор|контракт|соглаш"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .* контрагент",
|
||||
"(?i)метадан",
|
||||
"(?i)схем",
|
||||
"(?i)объект[а-я]* 1с"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["year_switch_after_pivot", "contracts_followup", "integrity_guard"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase73_document_to_contracts_all_time",
|
||||
"domain": "address_phase73_document_to_contracts_all_time",
|
||||
"title": "Phase 73 document to contracts all-time continuity",
|
||||
"description": "Replay for a human chain where the user opens documents by counterparty, pivots to contracts with a pronoun follow-up, and then asks for all-time scope without renaming the counterparty.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_documents_by_counterparty",
|
||||
"title": "Open documents for the counterparty",
|
||||
"question": "Покажи документы по Жуковке 51.",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк",
|
||||
"(?i)документ|сч[её]т|акт|накладн|строк"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["documents_by_counterparty", "pronoun_pivot_seed", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_contracts_by_pronoun_followup",
|
||||
"title": "Pivot to contracts via pronoun follow-up",
|
||||
"question": "А по нему договоры?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)договор|контракт|соглаш"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["contracts_followup", "counterparty_pronoun_resolution", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_all_time_after_contracts_pivot",
|
||||
"title": "Switch to all-time scope without renaming the counterparty",
|
||||
"question": "А за все время?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)договор|контракт|соглаш"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .* контрагент",
|
||||
"(?i)уточните .* период",
|
||||
"(?i)метадан",
|
||||
"(?i)схем",
|
||||
"(?i)объект[а-я]* 1с"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["all_time_after_pivot", "contracts_followup", "integrity_guard"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase74_document_to_payments_all_time",
|
||||
"domain": "address_phase74_document_to_payments_all_time",
|
||||
"title": "Phase 74 document to payments all-time continuity",
|
||||
"description": "Replay for a human chain where the user opens documents by counterparty, pivots to payments with a pronoun follow-up, and then switches to all-time scope without renaming the counterparty.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_documents_by_counterparty",
|
||||
"title": "Open documents for the counterparty",
|
||||
"question": "Покажи документы по Жуковке 51.",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк",
|
||||
"(?i)документ|сч[её]т|акт|накладн|строк"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["documents_by_counterparty", "pronoun_pivot_seed", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_payments_by_pronoun_followup",
|
||||
"title": "Pivot to payments via pronoun follow-up",
|
||||
"question": "А по нему платежи?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)платеж|операц|банк|поступлен|списан"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["payments_followup", "counterparty_pronoun_resolution", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_all_time_after_payments_pivot",
|
||||
"title": "Switch to all-time scope without renaming the counterparty",
|
||||
"question": "А за все время?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)платеж|операц|банк|поступлен|списан"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .* контрагент",
|
||||
"(?i)метадан",
|
||||
"(?i)схем",
|
||||
"(?i)объект[а-я]* 1с"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["all_time_after_pivot", "payments_followup", "integrity_guard"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase75_contracts_to_payments_year_switch",
|
||||
"domain": "address_phase75_contracts_to_payments_year_switch",
|
||||
"title": "Phase 75 contracts to payments year-switch continuity",
|
||||
"description": "Replay for a human chain where the user opens documents by counterparty, pivots to contracts, then pivots again to payments via pronoun follow-up, and finally switches the year without renaming the counterparty.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_documents_by_counterparty",
|
||||
"title": "Open documents for the counterparty",
|
||||
"question": "Покажи документы по Жуковке 51.",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк",
|
||||
"(?i)документ|сч[её]т|акт|накладн|строк"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["documents_by_counterparty", "pivot_seed", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_contracts_by_pronoun_followup",
|
||||
"title": "Pivot to contracts",
|
||||
"question": "А по нему договоры?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)договор|контракт|соглаш"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["contracts_followup", "counterparty_pronoun_resolution", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_payments_after_contracts_pivot",
|
||||
"title": "Pivot again to payments",
|
||||
"question": "А по нему платежи?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)платеж|операц|банк|поступлен|списан"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["payments_followup", "second_pivot", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_year_switch_after_second_pivot",
|
||||
"title": "Switch the year after the second pivot",
|
||||
"question": "А за 2021?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2021",
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)платеж|операц|банк|поступлен|списан"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .* контрагент",
|
||||
"(?i)метадан",
|
||||
"(?i)схем",
|
||||
"(?i)объект[а-я]* 1с"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["year_switch_after_second_pivot", "payments_followup", "integrity_guard"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase76_contracts_to_payments_all_time",
|
||||
"domain": "address_phase76_contracts_to_payments_all_time",
|
||||
"title": "Phase 76 contracts to payments all-time continuity",
|
||||
"description": "Replay for a human chain where the user opens documents by counterparty, pivots to contracts, then pivots again to payments via pronoun follow-up, and finally switches to all-time scope without renaming the counterparty.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_documents_by_counterparty",
|
||||
"title": "Open documents for the counterparty",
|
||||
"question": "Покажи документы по Жуковке 51.",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк",
|
||||
"(?i)документ|сч[её]т|акт|накладн|строк"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["documents_by_counterparty", "pivot_seed", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_contracts_by_pronoun_followup",
|
||||
"title": "Pivot to contracts",
|
||||
"question": "А по нему договоры?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)договор|контракт|соглаш"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["contracts_followup", "counterparty_pronoun_resolution", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_payments_after_contracts_pivot",
|
||||
"title": "Pivot again to payments",
|
||||
"question": "А по нему платежи?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)платеж|операц|банк|поступлен|списан"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["payments_followup", "second_pivot", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_all_time_after_second_pivot",
|
||||
"title": "Switch to all-time scope after the second pivot",
|
||||
"question": "А за все время?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)платеж|операц|банк|поступлен|списан"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .* контрагент",
|
||||
"(?i)метадан",
|
||||
"(?i)схем",
|
||||
"(?i)объект[а-я]* 1с"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["all_time_after_second_pivot", "payments_followup", "integrity_guard"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase77_payments_to_contracts_year_switch",
|
||||
"domain": "address_phase77_payments_to_contracts_year_switch",
|
||||
"title": "Phase 77 payments to contracts year-switch continuity",
|
||||
"description": "Replay for a human chain where the user opens documents by counterparty, pivots to payments, then pivots again to contracts via pronoun follow-up, and finally switches the year without renaming the counterparty.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_documents_by_counterparty",
|
||||
"title": "Open documents for the counterparty",
|
||||
"question": "Покажи документы по Жуковке 51.",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк",
|
||||
"(?i)документ|сч[её]т|акт|накладн|строк"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["documents_by_counterparty", "pivot_seed", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_payments_by_pronoun_followup",
|
||||
"title": "Pivot to payments",
|
||||
"question": "А по нему платежи?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)платеж|операц|банк|поступлен|списан"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["payments_followup", "counterparty_pronoun_resolution", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_contracts_after_payments_pivot",
|
||||
"title": "Pivot again to contracts",
|
||||
"question": "А по нему договоры?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)договор|контракт|соглаш"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["contracts_followup", "second_pivot", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_year_switch_after_second_pivot",
|
||||
"title": "Switch the year after the second pivot",
|
||||
"question": "А за 2021?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2021",
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)договор|контракт|соглаш"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .* контрагент",
|
||||
"(?i)метадан",
|
||||
"(?i)схем",
|
||||
"(?i)объект[а-я]* 1с"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["year_switch_after_second_pivot", "contracts_followup", "integrity_guard"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase78_payments_to_contracts_all_time",
|
||||
"domain": "address_phase78_payments_to_contracts_all_time",
|
||||
"title": "Phase 78 payments to contracts all-time continuity",
|
||||
"description": "Replay for a human chain where the user opens documents by counterparty, pivots to payments, then pivots again to contracts via pronoun follow-up, and finally requests the same contracts for all available time without renaming the counterparty.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_documents_by_counterparty",
|
||||
"title": "Open documents for the counterparty",
|
||||
"question": "Покажи документы по Жуковке 51.",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк",
|
||||
"(?i)документ|сч[её]т|акт|накладн|строк"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["documents_by_counterparty", "pivot_seed", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_payments_by_pronoun_followup",
|
||||
"title": "Pivot to payments",
|
||||
"question": "А по нему платежи?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)платеж|операц|банк|поступлен|списан"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["payments_followup", "counterparty_pronoun_resolution", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_contracts_after_payments_pivot",
|
||||
"title": "Pivot again to contracts",
|
||||
"question": "А по нему договоры?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)договор|контракт|соглаш"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["contracts_followup", "second_pivot", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_all_time_after_second_pivot",
|
||||
"title": "Request all available time after the second pivot",
|
||||
"question": "А за все время?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)договор|контракт|соглаш"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .* контрагент",
|
||||
"(?i)уточните .* период",
|
||||
"(?i)метадан",
|
||||
"(?i)схем",
|
||||
"(?i)объект[а-я]* 1с"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["all_time_after_second_pivot", "contracts_followup", "integrity_guard"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase79_payments_to_contracts_to_documents_year_switch",
|
||||
"domain": "address_phase79_payments_to_contracts_to_documents_year_switch",
|
||||
"title": "Phase 79 payments to contracts to documents year-switch continuity",
|
||||
"description": "Replay for a human chain where the user opens documents by counterparty, pivots to payments, then to contracts, then back to documents by pronoun follow-up, and finally narrows the same document contour to 2021 without renaming the counterparty.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_documents_by_counterparty",
|
||||
"title": "Open documents for the counterparty",
|
||||
"question": "Покажи документы по Жуковке 51.",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк",
|
||||
"(?i)документ|сч[её]т|акт|накладн|строк"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["documents_by_counterparty", "pivot_seed", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_payments_by_pronoun_followup",
|
||||
"title": "Pivot to payments",
|
||||
"question": "А по нему платежи?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)платеж|операц|банк|поступлен|списан"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["payments_followup", "counterparty_pronoun_resolution", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_contracts_after_payments_pivot",
|
||||
"title": "Pivot to contracts",
|
||||
"question": "А по нему договоры?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)договор|контракт|соглаш"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["contracts_followup", "second_pivot", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_documents_after_contracts_pivot",
|
||||
"title": "Pivot back to documents",
|
||||
"question": "А по нему документы?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)документ|сч[её]т|акт|накладн|строк"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["documents_followup", "third_pivot", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_year_switch_after_third_pivot",
|
||||
"title": "Switch the year after the third pivot",
|
||||
"question": "А за 2021?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2021",
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)документ|сч[её]т|акт|накладн|строк"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .* контрагент",
|
||||
"(?i)метадан",
|
||||
"(?i)схем",
|
||||
"(?i)объект[а-я]* 1с"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["year_switch_after_third_pivot", "documents_followup", "integrity_guard"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase80_payments_to_contracts_to_documents_all_time",
|
||||
"domain": "address_phase80_payments_to_contracts_to_documents_all_time",
|
||||
"title": "Phase 80 payments to contracts to documents all-time continuity",
|
||||
"description": "Replay for a human chain where the user opens documents by counterparty, pivots to payments, then to contracts, then back to documents by pronoun follow-up, and finally expands the same document contour to all available time without renaming the counterparty.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_documents_by_counterparty",
|
||||
"title": "Open documents for the counterparty",
|
||||
"question": "Покажи документы по Жуковке 51.",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк",
|
||||
"(?i)документ|сч[её]т|акт|накладн|строк"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["documents_by_counterparty", "pivot_seed", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_payments_by_pronoun_followup",
|
||||
"title": "Pivot to payments",
|
||||
"question": "А по нему платежи?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)платеж|операц|банк|поступлен|списан"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["payments_followup", "counterparty_pronoun_resolution", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_contracts_after_payments_pivot",
|
||||
"title": "Pivot to contracts",
|
||||
"question": "А по нему договоры?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)договор|контракт|соглаш"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["contracts_followup", "second_pivot", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_documents_after_contracts_pivot",
|
||||
"title": "Pivot back to documents",
|
||||
"question": "А по нему документы?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)документ|сч[её]т|акт|накладн|строк"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["documents_followup", "third_pivot", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_all_time_after_third_pivot",
|
||||
"title": "Expand the same document contour to all time",
|
||||
"question": "А за все время?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)документ|сч[её]т|акт|накладн|строк"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .* контрагент",
|
||||
"(?i)уточните .* период",
|
||||
"(?i)метадан",
|
||||
"(?i)схем",
|
||||
"(?i)объект[а-я]* 1с"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["all_time_after_third_pivot", "documents_followup", "integrity_guard"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase81_payments_contracts_documents_back_to_payments_year_switch",
|
||||
"domain": "address_phase81_payments_contracts_documents_back_to_payments_year_switch",
|
||||
"title": "Phase 81 repeated pivot back to payments after documents year-switch continuity",
|
||||
"description": "Replay for a human chain where the user opens documents by counterparty, pivots to payments, then to contracts, back to documents, and then back again to payments before switching the year without renaming the counterparty.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_documents_by_counterparty",
|
||||
"title": "Open documents for the counterparty",
|
||||
"question": "Покажи документы по Жуковке 51.",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк",
|
||||
"(?i)документ|сч[её]т|акт|накладн|строк"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["documents_by_counterparty", "pivot_seed", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_payments_by_pronoun_followup",
|
||||
"title": "Pivot to payments",
|
||||
"question": "А по нему платежи?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)платеж|операц|банк|поступлен|списан"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["payments_followup", "counterparty_pronoun_resolution", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_contracts_after_payments_pivot",
|
||||
"title": "Pivot to contracts",
|
||||
"question": "А по нему договоры?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)договор|контракт|соглаш"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["contracts_followup", "second_pivot", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_documents_after_contracts_pivot",
|
||||
"title": "Pivot back to documents",
|
||||
"question": "А по нему документы?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)документ|сч[её]т|акт|накладн|строк"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["documents_followup", "third_pivot", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_payments_after_third_pivot",
|
||||
"title": "Pivot back again to payments",
|
||||
"question": "А по нему платежи?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)платеж|операц|банк|поступлен|списан"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["payments_followup", "fourth_pivot", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_06_year_switch_after_fourth_pivot",
|
||||
"title": "Switch the year after the fourth pivot",
|
||||
"question": "А за 2021?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2021",
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)платеж|операц|банк|поступлен|списан"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .* контрагент",
|
||||
"(?i)метадан",
|
||||
"(?i)схем",
|
||||
"(?i)объект[а-я]* 1с"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["year_switch_after_fourth_pivot", "payments_followup", "integrity_guard"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase82_human_mixed_integrity_status_dialog",
|
||||
"domain": "address_phase82_human_mixed_integrity_status_dialog",
|
||||
"title": "ARCH: Post-F Semantic Integrity Hardening mixed human status dialog",
|
||||
"description": "Human-facing AGENT dialog that mixes the current Post-F repeated-pivot integrity contour with earlier bounded-autonomy contours: repeated counterparty pivots across documents, payments, and contracts; organization-scoped money analytics without a preselected counterparty; and a grounded named-counterparty money chain through incoming, payout, net, documents, and movements. The scenario uses explicit human resets between topics so the assistant must prove both continuity and clean topic switching rather than guessing vague 'continue' turns.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_documents_by_counterparty",
|
||||
"title": "Open documents for the counterparty",
|
||||
"question": "Покажи документы по Жуковке 51.",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк",
|
||||
"(?i)документ|сч[её]т|акт|накладн|строк"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["post_f_integrity_hardening", "documents_by_counterparty", "pivot_seed", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_payments_by_pronoun_followup",
|
||||
"title": "Pivot to payments",
|
||||
"question": "Хорошо, а теперь платежи по нему тоже покажи.",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)платеж|операц|банк|поступлен|списан"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["post_f_integrity_hardening", "payments_followup", "counterparty_pronoun_resolution", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_contracts_after_payments_pivot",
|
||||
"title": "Pivot to contracts",
|
||||
"question": "А по нему договоры?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)договор|контракт|соглаш"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["post_f_integrity_hardening", "contracts_followup", "second_pivot", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_documents_after_contracts_pivot",
|
||||
"title": "Pivot back to documents",
|
||||
"question": "А по нему документы?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)документ|сч[её]т|акт|накладн|строк"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["post_f_integrity_hardening", "documents_followup", "third_pivot", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_payments_after_third_pivot",
|
||||
"title": "Pivot back again to payments",
|
||||
"question": "А по нему платежи?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)платеж|операц|банк|поступлен|списан"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["post_f_integrity_hardening", "payments_followup", "fourth_pivot", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_06_year_switch_after_fourth_pivot",
|
||||
"title": "Switch the year after the fourth pivot",
|
||||
"question": "А за 2021?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2021",
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)платеж|операц|банк|поступлен|списан"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)метадан",
|
||||
"(?i)схем",
|
||||
"(?i)объект[а-я]* 1с"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["post_f_integrity_hardening", "year_switch_after_fourth_pivot", "payments_followup", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_07_open_scope_incoming_total",
|
||||
"title": "Reset to organization-scoped money analytics without a preselected counterparty",
|
||||
"question": "С Жуковкой закончили. Теперь нужна другая задача: быстрый денежный срез по одной организации. Если для ответа нужна организация, просто уточни ее. Сколько вообще входящих денег было за 2020 год?",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)уточн|нужно",
|
||||
"(?i)организац"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["organization_scope", "open_scope_total", "topic_reset", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_08_open_scope_org_clarification",
|
||||
"title": "Provide the organization and get the 2020 incoming total",
|
||||
"question": "По ООО Альтернатива Плюс.",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2020",
|
||||
"(?i)входящ|поступлен|получ"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .*контрагент",
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)уточните .*организац"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["organization_scope", "open_scope_total", "organization_clarification", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_09_open_scope_all_time_followup",
|
||||
"title": "Broaden the same organization slice to all available time",
|
||||
"question": "Понял, тогда за все время.",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)все доступное время|все время|весь период",
|
||||
"(?i)входящ|поступлен|получ"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)за 2020",
|
||||
"(?i)уточните .*контрагент",
|
||||
"(?i)уточните .*период",
|
||||
"(?i)уточните .*организац"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["organization_scope", "all_time_followup", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_10_bidirectional_comparison",
|
||||
"title": "Ask which money direction is larger for the organization",
|
||||
"question": "Хорошо. А что по ООО Альтернатива Плюс больше в 2020 году: входящие или исходящие деньги?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2020",
|
||||
"(?i)входящ|исходящ|получ|заплат|больше"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["organization_scope", "value_flow_comparison", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_11_comparison_year_switch",
|
||||
"title": "Ask the same comparison for another year",
|
||||
"question": "А что по ООО Альтернатива Плюс больше уже за 2021 год: входящие или исходящие деньги?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2021",
|
||||
"(?i)входящ|исходящ|получ|заплат|больше"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["organization_scope", "value_flow_comparison", "year_switch", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_12_ranking_top_counterparty",
|
||||
"title": "Ask who brought the most money for the organization",
|
||||
"question": "И кто больше всего принес денег этой организации в 2020 году?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2020",
|
||||
"(?i)кто|контрагент|клиент|принес|доход"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["organization_scope", "value_flow_ranking", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_13_ranking_year_switch",
|
||||
"title": "Ask the same ranking for another year",
|
||||
"question": "А в 2021 году?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2021"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["organization_scope", "value_flow_ranking", "year_switch", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_14_ground_named_counterparty",
|
||||
"title": "Reset to a named grounded counterparty",
|
||||
"question": "Теперь отдельная тема по конкретному контрагенту. Найди в 1С Группу СВК.",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)свк|группа свк|контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["grounded_counterparty", "topic_reset", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_15_counterparty_incoming_2020",
|
||||
"title": "Ask how much money was received from the grounded counterparty",
|
||||
"question": "Сколько получили по нему за 2020 год?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2020",
|
||||
"(?i)получ|входящ|поступлен"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["grounded_counterparty", "incoming_value_flow", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_16_counterparty_payout_followup",
|
||||
"title": "Ask how much was paid to the same grounded counterparty",
|
||||
"question": "А теперь сколько заплатили?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)заплат|исходящ|списан|платеж"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["grounded_counterparty", "payout_value_flow", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_17_counterparty_net_followup",
|
||||
"title": "Ask for net after incoming and payout",
|
||||
"question": "А какое нетто?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)нетто|сальдо|получ|заплат"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["grounded_counterparty", "net_value_flow", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_18_counterparty_documents_pivot",
|
||||
"title": "Pivot from money to documents for the same counterparty",
|
||||
"question": "А по документам?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)документ|счет|сч[её]т[- ]?фактур|накладн|акт|строк"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["grounded_counterparty", "documents_pivot", "human_dialog"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_19_counterparty_movements_pivot",
|
||||
"title": "Pivot from documents to movements for the same counterparty",
|
||||
"question": "А по движениям?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)движени|операц|платеж|поступлен|списан|строк"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["grounded_counterparty", "movements_pivot", "human_dialog"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_post_f_cross_stage_canary_agent_20260424",
|
||||
"domain": "address_post_f_cross_stage_canary_agent",
|
||||
"title": "AGENT | Post-F cross-stage semantic integrity canary",
|
||||
"description": "Long human-facing AGENT dialog for the end of Post-F Semantic Integrity Hardening. It deliberately crosses older validated contours and the current fix surface in one session: VAT/metadata orientation, movement-to-document pivots, numeric counterparty suffix scope, repeated documents/payments/contracts pivots, organization open-scope clarification, bidirectional value-flow, ranking, grounded SVK counterparty chains, and final document/movement pivots. The goal is to catch stale scope, account injection, wrong post-pivot arbitration, materialization gaps, and regressions in older bounded MCP stages before saving the run into autoruns.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_vat_metadata_orientation",
|
||||
"title": "Orient inside VAT objects",
|
||||
"question": "Мне нужно понять, где в 1С по НДС вообще лежат данные. Какие объекты стоит смотреть по НДС?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)ндс", "(?i)метадан|metadata|документ|регистр|объект"],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["legacy_phase64", "metadata_surface", "vat_orientation", "cross_stage_canary"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_vat_movements_org_needs_period",
|
||||
"title": "Choose movement lane and organization",
|
||||
"question": "Хорошо, тогда покажи движения по ООО Альтернатива Плюс.",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage", "factual_with_explanation"],
|
||||
"required_answer_patterns_all": ["(?i)движени|регистр|операц|период|уточн"],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["legacy_phase64", "movement_lane_after_metadata", "organization_scope"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_vat_movements_2020",
|
||||
"title": "Provide the period",
|
||||
"question": "За 2020 год.",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)2020", "(?i)ндс|движени|регистр|операц|строк|поступлен|списан"],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["legacy_phase64", "movement_execution", "period_clarification_resume"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_vat_documents_pivot",
|
||||
"title": "Pivot from movements to documents",
|
||||
"question": "А теперь по документам?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)документ|счет|сч[её]т|накладн|акт|строк"],
|
||||
"forbidden_answer_patterns": ["(?i)уточните .*организац", "(?i)уточните .*период"],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["legacy_phase64", "document_pivot_after_movement", "scope_reuse"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_vat_documents_year_switch",
|
||||
"title": "Switch document slice to another year",
|
||||
"question": "А теперь за 2021 год?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)2021", "(?i)документ|счет|сч[её]т|накладн|акт|строк"],
|
||||
"forbidden_answer_patterns": ["(?i)уточните .*организац", "(?i)уточните .*период"],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["legacy_phase64", "year_switch_after_document_pivot"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_06_vat_documents_all_time",
|
||||
"title": "Broaden document slice to all available time",
|
||||
"question": "А теперь за все время?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)все доступное время|все время|весь период|истори", "(?i)документ|счет|сч[её]т|накладн|акт|строк"],
|
||||
"forbidden_answer_patterns": ["(?i)уточните .*период"],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["legacy_phase64", "all_time_followup", "document_lane_continuity"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_07_reset_to_numeric_counterparty_documents",
|
||||
"title": "Reset to numeric counterparty suffix documents",
|
||||
"question": "С НДС закончили. Новая тема: покажи документы по Жуковке 51.",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)жуковк", "(?i)документ|счет|сч[её]т|накладн|акт|строк"],
|
||||
"forbidden_answer_patterns": ["(?i)счет 51", "(?i)account 51"],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["post_f", "numeric_counterparty_suffix", "account_injection_guard", "topic_reset"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_08_numeric_counterparty_payments",
|
||||
"title": "Pivot numeric counterparty to payments",
|
||||
"question": "Хорошо, а теперь платежи по нему тоже покажи.",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)жуковк|контрагент", "(?i)платеж|операц|банк|поступлен|списан"],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["post_f", "counterparty_pronoun_resolution", "payments_followup"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_09_numeric_counterparty_contracts",
|
||||
"title": "Pivot numeric counterparty to contracts",
|
||||
"question": "А по нему договоры?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)жуковк|контрагент", "(?i)договор|контракт|соглаш"],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["post_f", "contracts_followup", "repeated_pivot"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_10_numeric_counterparty_documents_again",
|
||||
"title": "Pivot back to documents again",
|
||||
"question": "А по нему документы?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)жуковк|контрагент", "(?i)документ|счет|сч[её]т|накладн|акт|строк"],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["post_f", "documents_followup", "repeated_pivot"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_11_numeric_counterparty_year_switch",
|
||||
"title": "Switch numeric counterparty active lane to 2021",
|
||||
"question": "А за 2021?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)2021", "(?i)жуковк|контрагент"],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["post_f", "year_switch_after_repeated_pivot"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_12_open_org_money_requires_clarification",
|
||||
"title": "Reset to open organization-scoped value flow",
|
||||
"question": "С Жуковкой закончили. Теперь другая задача: быстрый денежный срез по одной организации. Если для ответа нужна организация, просто уточни ее. Сколько вообще входящих денег было за 2020 год?",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)уточн|нужно", "(?i)организац"],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["post_f", "open_scope_total", "organization_clarification", "topic_reset"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_13_open_org_clarification_answer",
|
||||
"title": "Provide organization for open money slice",
|
||||
"question": "По ООО Альтернатива Плюс.",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)2020|проверенн|строк", "(?i)входящ|поступлен|получ"],
|
||||
"forbidden_answer_patterns": ["(?i)уточните .*контрагент", "(?i)уточните .*организац"],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["post_f", "organization_scope", "clarification_resume"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_14_open_org_all_time",
|
||||
"title": "Broaden same organization money slice",
|
||||
"question": "Понял, тогда за все время.",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)все доступное время|все время|весь период|истори", "(?i)входящ|поступлен|получ"],
|
||||
"forbidden_answer_patterns": ["(?i)уточните .*контрагент", "(?i)уточните .*организац"],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["post_f", "organization_scope", "all_time_followup"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_15_open_org_bidirectional_2020",
|
||||
"title": "Bidirectional comparison for organization",
|
||||
"question": "Хорошо. А что по ООО Альтернатива Плюс больше в 2020 году: входящие или исходящие деньги?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)2020", "(?i)входящ|исходящ|получ|заплат|больше"],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["post_f", "bidirectional_value_flow", "organization_scope"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_16_open_org_ranking_2020",
|
||||
"title": "Ranking after organization comparison",
|
||||
"question": "И кто больше всего принес денег этой организации в 2020 году?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)2020", "(?i)кто|контрагент|клиент|принес|доход|поступлен"],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["legacy_phase39", "value_flow_ranking", "organization_scope"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_17_open_org_ranking_year_switch",
|
||||
"title": "Ranking year switch",
|
||||
"question": "А в 2021 году?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)2021"],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["legacy_phase39", "year_switch", "organization_scope"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_18_ground_svk_counterparty",
|
||||
"title": "Reset to grounded SVK counterparty",
|
||||
"question": "Теперь отдельная тема по конкретному контрагенту. Найди в 1С Группу СВК.",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)свк", "(?i)контрагент|каталог|1с|заземл"],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["legacy_phase67", "grounded_counterparty", "topic_reset"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_19_svk_incoming_2020",
|
||||
"title": "SVK incoming value flow",
|
||||
"question": "Сколько получили по нему за 2020 год?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)2020", "(?i)свк|контрагент|входящ|поступлен|получ"],
|
||||
"forbidden_answer_patterns": ["(?i)жуковк"],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["legacy_phase67", "grounded_counterparty", "incoming_value_flow"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_20_svk_payout",
|
||||
"title": "SVK outgoing value flow",
|
||||
"question": "А теперь сколько заплатили?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)свк|контрагент|заплат|исходящ|списан|платеж"],
|
||||
"forbidden_answer_patterns": ["(?i)жуковк"],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["legacy_phase67", "grounded_counterparty", "payout_value_flow"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_21_svk_net",
|
||||
"title": "SVK net value flow",
|
||||
"question": "А какое нетто?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)свк|контрагент|нетто|сальдо|получ|заплат"],
|
||||
"forbidden_answer_patterns": ["(?i)жуковк"],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["legacy_phase67", "grounded_counterparty", "net_value_flow"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_22_svk_documents_pivot",
|
||||
"title": "SVK documents pivot",
|
||||
"question": "А по документам?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)свк|контрагент|документ|счет|сч[её]т|накладн|акт|строк"],
|
||||
"forbidden_answer_patterns": ["(?i)жуковк"],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["legacy_phase67", "grounded_counterparty", "documents_pivot"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_23_svk_movements_pivot",
|
||||
"title": "SVK movements pivot",
|
||||
"question": "А по движениям?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)свк|контрагент|движени|операц|платеж|поступлен|списан|строк"],
|
||||
"forbidden_answer_patterns": ["(?i)жуковк"],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["legacy_phase67", "grounded_counterparty", "movements_pivot"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_24_svk_year_switch",
|
||||
"title": "SVK year switch",
|
||||
"question": "А теперь тот же смысл за 2021 год.",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)2021", "(?i)свк|контрагент"],
|
||||
"forbidden_answer_patterns": ["(?i)жуковк"],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["legacy_phase67", "grounded_counterparty", "year_switch"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_post_f_manual_failures_20260424",
|
||||
"domain": "address_post_f_manual_failures",
|
||||
"title": "AGENT | Post-F manual failures: VAT, revenue ranking, item-flow",
|
||||
"description": "Focused replay from assistant-stage1-9liEOh-7JP and assistant-stage1-It31Zdb4cf failures. It preserves the human context around steps 11, 13, 14, and 26: inventory selected-object purchase context, VAT month liability, all-time highest-value customer ranking, and explicit counterparty item-flow after stale inventory scope.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_inventory_root",
|
||||
"title": "Open inventory context",
|
||||
"question": "кайф - что там на складе по остаткам?",
|
||||
"allowed_reply_types": ["clarification_required", "partial_coverage", "factual"],
|
||||
"required_answer_patterns_all": ["(?i)организац|альтернатива|уточн|склад|остат"],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["manual_9liEOh", "inventory_context"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_choose_org",
|
||||
"title": "Choose organization",
|
||||
"question": "АЛЬТЕРНАТИВА",
|
||||
"allowed_reply_types": ["factual", "partial_coverage", "factual_with_explanation"],
|
||||
"required_answer_patterns_all": ["(?i)остат|склад|позици|альтернатива"],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["manual_9liEOh", "organization_scope"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_inventory_march_2016",
|
||||
"title": "Historical inventory with workstation",
|
||||
"question": "март 2016",
|
||||
"allowed_reply_types": ["factual", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)31\\.03\\.2016|март|2016", "(?i)рабоч|станц|позици|остат"],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["manual_9liEOh", "purchase_date_context"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_workstation_purchase",
|
||||
"title": "Selected workstation purchase context",
|
||||
"question": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?",
|
||||
"allowed_reply_types": ["factual", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)рабочая станция", "(?i)поставщик|куп|приобрет|дата|документ|однознач"],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["manual_9liEOh", "selected_object_purchase"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_vat_purchase_date",
|
||||
"title": "VAT on workstation purchase date",
|
||||
"question": "ндс можешь прикинуть на дату покупки рабочей станции?",
|
||||
"allowed_reply_types": ["factual", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)ндс", "(?i)подтвержден|к уплате|расчет|срез|период"],
|
||||
"forbidden_answer_patterns": ["(?i)отфильтрован", "(?i)не удается точно определить.*отфильтр"],
|
||||
"expected_intents": ["vat_liability_confirmed_for_tax_period"],
|
||||
"expected_recipe": "address_vat_liability_confirmed_tax_period_v1",
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["manual_9liEOh", "vat_failure_11", "materialization_gap"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_06_vat_feb_2017",
|
||||
"title": "VAT February 2017",
|
||||
"question": "прикинь какой ндс нам надо заплатить на февраль 2017",
|
||||
"allowed_reply_types": ["factual", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)ндс", "(?i)феврал|2017", "(?i)подтвержден|к уплате|расчет|срез|0,00|руб"],
|
||||
"forbidden_answer_patterns": ["(?i)отфильтрован", "(?i)не удается точно определить.*отфильтр"],
|
||||
"expected_intents": ["vat_liability_confirmed_for_tax_period"],
|
||||
"expected_recipe": "address_vat_liability_confirmed_tax_period_v1",
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["manual_9liEOh", "vat_failure_13"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_07_highest_value_customer",
|
||||
"title": "All-time highest-value customer",
|
||||
"question": "кто у нас самый доходный клиент за все время",
|
||||
"allowed_reply_types": ["factual", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)самый доходный клиент|доходн", "(?i)поступлен|денежн|руб|деньг|выруч"],
|
||||
"forbidden_answer_patterns": ["(?i)активных заказчиков", "(?i)операций:\\s*\\d+\\s*\\|\\s*последняя активность"],
|
||||
"expected_intents": ["customer_revenue_and_payments"],
|
||||
"expected_recipe": "address_customer_revenue_and_payments_v1",
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["manual_9liEOh", "revenue_ranking_failure_14"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_08_documents_chepurnov",
|
||||
"title": "Ground Chepurnov documents",
|
||||
"question": "по чепурнову покажи все доки",
|
||||
"allowed_reply_types": ["factual", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)чепурнов", "(?i)документ|поступление|договор"],
|
||||
"expected_intents": ["list_documents_by_counterparty"],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["manual_9liEOh", "counterparty_grounding"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_09_pivot_svk",
|
||||
"title": "Retarget to SVK documents",
|
||||
"question": "а по свк",
|
||||
"allowed_reply_types": ["factual", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)группа свк|свк", "(?i)документ"],
|
||||
"expected_intents": ["list_documents_by_counterparty"],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["manual_9liEOh", "counterparty_retarget"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_10_inventory_today",
|
||||
"title": "Stale inventory root before item-flow",
|
||||
"question": "а сейчас у нас есть что на складе?",
|
||||
"allowed_reply_types": ["factual", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)склад|остат|позици"],
|
||||
"expected_intents": ["inventory_on_hand_as_of_date"],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["manual_9liEOh", "stale_inventory_scope"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_11_chepurnov_item_flow",
|
||||
"title": "Explicit Chepurnov item-flow after stale inventory",
|
||||
"question": "что нам отгружал чепурнов? какой товар или услугу?",
|
||||
"allowed_reply_types": ["factual", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)чепурнов", "(?i)товар|услуг|позици|номенклатур|поставк|документ"],
|
||||
"forbidden_answer_patterns": ["(?i)На 24\\.04\\.2026 на складе", "(?i)остатк.*склад"],
|
||||
"expected_intents": ["list_documents_by_counterparty"],
|
||||
"expected_recipe": "address_documents_by_counterparty_v1",
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["manual_9liEOh", "item_flow_failure_26", "stale_scope_guard"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -109,13 +109,13 @@ function isConfirmedBalanceIntent(intent) {
|
|||
intent === "vat_liability_confirmed_for_tax_period");
|
||||
}
|
||||
function resolveAddressAsOfDateBasis(filters, semanticFrame) {
|
||||
if (semanticFrame?.date_basis_hint) {
|
||||
return semanticFrame.date_basis_hint;
|
||||
}
|
||||
const asOfDate = normalizeIsoDateHint(filters.as_of_date);
|
||||
if (asOfDate) {
|
||||
return "explicit_as_of_date";
|
||||
}
|
||||
if (semanticFrame?.date_basis_hint) {
|
||||
return semanticFrame.date_basis_hint;
|
||||
}
|
||||
const periodFrom = normalizeIsoDateHint(filters.period_from);
|
||||
const periodTo = normalizeIsoDateHint(filters.period_to);
|
||||
if (periodFrom && periodTo) {
|
||||
|
|
|
|||
|
|
@ -864,7 +864,9 @@ function extractShipmentCounterpartyValue(text) {
|
|||
return candidate;
|
||||
}
|
||||
function extractInstrumentalCounterpartyValue(text) {
|
||||
const match = String(text ?? "").match(/(?:контрагентом|поставщиком|клиентом|заказчиком)\s+([\p{L}][\p{L}\p{N}._-]{1,})(?=[\s,.;:!?)]|$)/iu);
|
||||
const source = String(text ?? "");
|
||||
const match = source.match(/(?:\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u043e\u043c|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\u043e\u043c|\u043a\u043b\u0438\u0435\u043d\u0442\u043e\u043c|\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a\u043e\u043c)\s+([\p{L}][\p{L}\p{N}._-]{1,})(?=[\s,.;:!?)]|$)/iu) ??
|
||||
source.match(/(?:контрагентом|поставщиком|клиентом|заказчиком)\s+([\p{L}][\p{L}\p{N}._-]{1,})(?=[\s,.;:!?)]|$)/iu);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1551,6 +1551,320 @@ function repairLikelyUtf8Mojibake(text) {
|
|||
return raw;
|
||||
}
|
||||
}
|
||||
function unicodeBridgeResolution(intent, confidence, reason) {
|
||||
return { intent, confidence, reasons: [reason] };
|
||||
}
|
||||
function hasBidirectionalValueFlowComparisonSignal(text) {
|
||||
const normalized = String(text ?? "").trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const hasIncomingCue = /(?:\u0432\u0445\u043e\u0434\u044f\u0449|\u043f\u043e\u0441\u0442\u0443\u043f|\u043f\u043e\u043b\u0443\u0447|inflow|incoming)/iu.test(normalized);
|
||||
const hasOutgoingCue = /(?:\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u0441\u043f\u0438\u0441\u0430\u043d|\u0437\u0430\u043f\u043b\u0430\u0442|\u043f\u043b\u0430\u0442\u0438\u043b|\u043e\u043f\u043b\u0430\u0442|outflow|outgoing|payout)/iu.test(normalized);
|
||||
const hasComparisonCue = /(?:\u0431\u043e\u043b\u044c\u0448|\u043c\u0435\u043d\u044c\u0448|\u0441\u0440\u0430\u0432|\u0438\u043b\u0438|\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|vs|versus)/iu.test(normalized);
|
||||
const hasValueFlowCue = /(?:\u0434\u0435\u043d\u044c\u0433|\u0434\u0435\u043d\u0435\u0433|\u0434\u0435\u043d\u0435\u0436|\u043f\u043e\u0442\u043e\u043a|\u043e\u0431\u043e\u0440\u043e\u0442|money|cash|flow)/iu.test(normalized);
|
||||
return hasIncomingCue && hasOutgoingCue && hasComparisonCue && hasValueFlowCue;
|
||||
}
|
||||
function resolveUnicodeAddressIntentBridge(text) {
|
||||
const normalized = String(text ?? "").trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
const hasAccountAnchor = /(?:\b(?:60|62|76)(?:[.,]\d{2})?\b|сч(?:е|ё)т(?:а|у|ом|е|ов)?|account)/iu.test(normalized);
|
||||
const hasDocumentCue = /(?:док(?:умент(?:ы|ов|а|ам|ами|ах)?|и|ам|ами|ах|ов|а)?|docs?|documents?)/iu.test(normalized);
|
||||
const hasBankCue = /(?:банк|банковск|плат[её]ж|оплат|транзакц|51|bank|payment|transaction)/iu.test(normalized);
|
||||
const hasContractCue = /(?:договор|дог(?:\s|$)|контракт|contract|dogovor)/iu.test(normalized);
|
||||
const hasSpecificContractCue = /(?:\b\d{1,4}\/\d{1,4}\b|этому\s+же\s+договор)/iu.test(normalized);
|
||||
const hasCounterpartyCue = /(?:контрагент|компани|организац|клиент|покупател|заказчик|поставщик|свк|альфа|жуковк|альтернатива|counterpart|company|supplier|customer|client|buyer)/iu.test(normalized);
|
||||
const byAnchorMatch = normalized.match(/(?:^|[\s,.;:!?])(?:по|для)\s+([\p{L}\d._-]{2,})/iu);
|
||||
const byAnchorToken = String(byAnchorMatch?.[1] ?? "").toLowerCase();
|
||||
const hasLooseCounterpartyByAnchor = !!byAnchorToken &&
|
||||
!new Set([
|
||||
"количеству",
|
||||
"документам",
|
||||
"докам",
|
||||
"договору",
|
||||
"договорам",
|
||||
"счету",
|
||||
"счёту",
|
||||
"остатку",
|
||||
"операциям",
|
||||
"оплате",
|
||||
"платежам",
|
||||
"сальдо",
|
||||
"дате",
|
||||
"периоду",
|
||||
"складу",
|
||||
"товару",
|
||||
"этому",
|
||||
"этой",
|
||||
"нему",
|
||||
"ней"
|
||||
]).has(byAnchorToken);
|
||||
const hasMoneyCue = /(?:деньг|денег|выручк|доход|оборот|заработ|прин[её]с|чек|ликвидн|revenue|turnover|money)/iu.test(normalized);
|
||||
const hasRankingCue = /(?:топ|ранк|сам(?:ый|ая|ое|ые)|больше\s+всего|наибольш|крупн|жирн|max|top|rank)/iu.test(normalized);
|
||||
const hasOpenItemsAccountCue = /(?:хвост|долг|незакрыт|вис)/iu.test(normalized) &&
|
||||
/(?:сч(?:е|ё)т(?:а|у|ом|е|ов)?\s*(?:№|#)?\s*(?:60|62|76)(?:[.,]\d{1,2})?|\b(?:60|62|76)(?:[.,]\d{1,2})?\b\s*сч(?:е|ё)т)/iu.test(normalized);
|
||||
if (hasOpenItemsAccountCue) {
|
||||
return unicodeBridgeResolution("open_items_by_counterparty_or_contract", "medium", "open_items_signal_detected");
|
||||
}
|
||||
const hasCounterpartyShipmentItemFlowCue = /(?:отгруж(?:ал|али|ен[аоы]?|енн(?:ый|ая|ое|ые)?))/iu.test(normalized) &&
|
||||
/(?:товар|услуг|позици|номенклатур)/iu.test(normalized) &&
|
||||
!/(?:выбранн(?:ый|ому)\s+объект|selected\s+object)/iu.test(normalized);
|
||||
if (hasCounterpartyShipmentItemFlowCue) {
|
||||
return unicodeBridgeResolution("list_documents_by_counterparty", "medium", "counterparty_item_flow_signal_detected");
|
||||
}
|
||||
const hasHighestValueCustomerCue = /(?:сам(?:ый|ые|ая|ое)|топ|кто|какой|какие|больше\s+всего|наибольш)/iu.test(normalized) &&
|
||||
/(?:доходн|выручк|денег|деньг|оборот|поступлен|прин[её]с)/iu.test(normalized) &&
|
||||
/(?:клиент|покупател|заказчик|контрагент)/iu.test(normalized);
|
||||
if (!hasContractCue && hasHighestValueCustomerCue) {
|
||||
return unicodeBridgeResolution("customer_revenue_and_payments", "high", "unicode_customer_revenue_bridge_signal_detected");
|
||||
}
|
||||
if (hasBidirectionalValueFlowComparisonSignal(normalized)) {
|
||||
return unicodeBridgeResolution("unknown", "high", "unicode_bidirectional_value_flow_deferred_to_discovery");
|
||||
}
|
||||
if (/(?:за\s+какие\s+годы|диапазон\s+лет|покрыт(?:ие|ый)\s+период|какой\s+год.*актив|какой\s+месяц.*актив|год.*пассив|месяц.*пассив|минимальн.*док|минимальн.*операц|месяц[\s-]*пик|profile\s+period|top\s*year|top\s*month)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("period_coverage_profile", "high", "unicode_period_coverage_bridge_signal_detected");
|
||||
}
|
||||
if (/(?:тип(?:ы|ов)?\s+док|док(?:умент|ов).*?(?:чаще|редк|больше\s+всего|меньше\s+всего|крутит)|раздел(?:ы|ов)?\s+уч[её]та|сводк.*тип.*док|document\s+type|account\s+section)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("document_type_and_account_section_profile", "high", "unicode_document_type_profile_bridge_signal_detected");
|
||||
}
|
||||
if (hasAccountAnchor &&
|
||||
hasDocumentCue &&
|
||||
/(?:формирующ.*остат|раскрой\s+остат|остат.*по\s+документ|по\s+докам.*(?:60|62|76)|(?:60|62|76)(?:[.,]\d{2})?.*(?:по\s+докам|по\s+документ))/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("documents_forming_balance", "high", "unicode_documents_forming_balance_bridge_signal_detected");
|
||||
}
|
||||
if (/(?:договор[а-я]*.*(?:все|список).*по\s+[\p{L}\d]|(?:покажи|показать).*договор[а-я]*.*по\s+[\p{L}\d])/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("list_contracts_by_counterparty", "high", "unicode_contracts_by_counterparty_bridge_signal_detected");
|
||||
}
|
||||
if (/(?:проконтрол|акты\s+без\s+приход|без\s+приходок|засорять\s+бухгалтер)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("unknown", "low", "unsupported_supplier_control_signal_detected");
|
||||
}
|
||||
if (/(?:кроме\s+этого\s+документ.*(?:есть\s+еще\s+что|есть\s+ещ[её]\s+что|что[-\s]?то))/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("list_documents_by_counterparty", "medium", "generic_document_followup_with_previous_counterparty");
|
||||
}
|
||||
if (/(?:плат[её]ж|оплат|отгрузк|документ|аванс|взаиморасчет|закрыт)/iu.test(normalized) &&
|
||||
/(?:без\s+(?:закрыт|документ|оплат)|нет\s+(?:документ|оплат)|не\s+закрыт|оплат[а-я]*\s+нет|документ[а-я]*\s+есть|требует\s+ручн)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("list_open_contracts", "high", "unicode_open_contract_gap_bridge_signal_detected");
|
||||
}
|
||||
if (/(?:долгожител|долго\s+долж|задолженн(?:ост|остям).*(?:давн|долго)|не\s+плат|не\s+оплат|не\s+оплачен|неоплачен|просроч|сроки\s+давно\s+прошл|слишком\s+длинн.*оплат)/iu.test(normalized) &&
|
||||
/(?:покупател|клиент|заказ|отгрузк|товар|услуг|задолженн|сальдо|не\s+плат|не\s+оплат|не\s+оплачен|неоплачен|просроч)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("list_receivables_counterparties", "high", "receivables_debt_lifecycle_signal_detected");
|
||||
}
|
||||
const inventoryBridgeIntent = (0, addressInventoryIntentSignals_1.resolveInventoryAddressIntent)(normalized);
|
||||
if (inventoryBridgeIntent) {
|
||||
if (inventoryBridgeIntent.intent === "inventory_aging_by_purchase_date") {
|
||||
return { ...inventoryBridgeIntent, confidence: "high" };
|
||||
}
|
||||
return inventoryBridgeIntent;
|
||||
}
|
||||
if (/(?:поставщик|vendor|supplier)/iu.test(normalized) &&
|
||||
/(?:хвост|задержк|проблем|систематическ)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("list_payables_counterparties", "high", "supplier_tail_risk_signal_detected");
|
||||
}
|
||||
if (hasDocumentCue && (hasCounterpartyCue || hasLooseCounterpartyByAnchor) && !hasContractCue) {
|
||||
return unicodeBridgeResolution("list_documents_by_counterparty", "high", "unicode_documents_by_counterparty_bridge_signal_detected");
|
||||
}
|
||||
if (hasBankCue && (hasCounterpartyCue || hasLooseCounterpartyByAnchor) && !hasContractCue) {
|
||||
return unicodeBridgeResolution("bank_operations_by_counterparty", "high", "unicode_bank_ops_by_counterparty_bridge_signal_detected");
|
||||
}
|
||||
if (/(?:есть\s+что[-\s]?то|что[-\s]?то)/iu.test(normalized) &&
|
||||
(hasLooseCounterpartyByAnchor || /по\s+(?:ней|нему|этой|этому)/iu.test(normalized))) {
|
||||
return unicodeBridgeResolution("list_documents_by_counterparty", "medium", "generic_lookup_with_loose_anchor_fallback");
|
||||
}
|
||||
if (hasDocumentCue &&
|
||||
(hasLooseCounterpartyByAnchor || hasCounterpartyCue || /по\s+(?:ней|нему|этой|этому)/iu.test(normalized)) &&
|
||||
!hasContractCue &&
|
||||
!/(?:купил|куплен|закуп|товар|позици|номенклатур)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("list_documents_by_counterparty", "medium", "unicode_documents_by_counterparty_bridge_signal_detected");
|
||||
}
|
||||
if (/(?:долгожител|долго\s+долж|задолженн(?:ост|остям).*(?:давн|долго)|не\s+плат|не\s+оплат|не\s+оплачен|неоплачен|просроч|сроки\s+давно\s+прошл|слишком\s+длинн.*оплат)/iu.test(normalized) &&
|
||||
/(?:покупател|клиент|заказ|отгрузк|товар|услуг|задолженн|сальдо|не\s+плат|не\s+оплат|не\s+оплачен|неоплачен|просроч)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("list_receivables_counterparties", "high", "receivables_debt_lifecycle_signal_detected");
|
||||
}
|
||||
if (/\b41(?:[.,]\d{2})?\b/iu.test(normalized) && /(?:товар|склад|остат|состоит|номенклатур)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("inventory_on_hand_as_of_date", "high", "unicode_inventory_on_hand_bridge_signal_detected");
|
||||
}
|
||||
if (/(?:год.*(?:док|операц).*(?:актив|пик|жив|много|движов)|год.*движов.*(?:док|операц)|(?:док|операц).*год.*(?:актив|пик|жив|много|движов)|месяц[\s-]*пик)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("period_coverage_profile", "high", "unicode_period_coverage_bridge_signal_detected");
|
||||
}
|
||||
if (!hasContractCue &&
|
||||
/(?:скольк|скока).*(?:деньг|денег|выручк|доход|оборот)|(?:деньг|денег|выручк|доход|оборот).*(?:прин[её]с|зан[её]с|плат)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("customer_revenue_and_payments", "high", "unicode_customer_revenue_bridge_signal_detected");
|
||||
}
|
||||
if (!hasContractCue &&
|
||||
/(?:кто|какие|выведи|покажи|список|самые)/iu.test(normalized) &&
|
||||
/(?:список\s+(?:заказчик|клиент|покупател).*за\s+\d{2,4}\s*год|актив.*отвал|ровно\s+один\s+раз|один\s+раз.*пропал|стар(?:ые|ые)?\s+по\s+сотруднич|сотрудничеству\s+кто)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("counterparty_activity_lifecycle", "high", "unicode_counterparty_lifecycle_bridge_signal_detected");
|
||||
}
|
||||
if (!hasContractCue &&
|
||||
/(?:кто|какие|выведи|покажи|список|разбей|раздели|самые)/iu.test(normalized) &&
|
||||
/(?:заказчик|клиент|покупател|поставщик|контрагент|зак(?!рыт))/iu.test(normalized) &&
|
||||
/(?:работал|работают|актив|все\s+время|вообще|регулярн|эпизодич|частот|давно\s+не\s+использ|операционн|разов|один\s+раз|пропал|отвал|сотруднич)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("counterparty_activity_lifecycle", "high", "unicode_counterparty_lifecycle_bridge_signal_detected");
|
||||
}
|
||||
if (/(?:поставщик|vendor|supplier|кому\s+(?:ушло|платили|заплатили)|выплат|исходящ|списан|сгрузил)/iu.test(normalized) &&
|
||||
!/(?:аванс.*(?:не\s+)?закрыт|закрыт.*аванс)/iu.test(normalized) &&
|
||||
(hasMoneyCue || hasRankingCue || /плат[её]ж|оплат|выплат|outflow|payout|хвост|задержк|проблем/iu.test(normalized))) {
|
||||
return unicodeBridgeResolution(/(?:хвост|задержк|проблем)/iu.test(normalized) ? "list_payables_counterparties" : "supplier_payouts_profile", "high", /(?:хвост|задержк|проблем)/iu.test(normalized)
|
||||
? "supplier_tail_risk_signal_detected"
|
||||
: "unicode_supplier_payouts_bridge_signal_detected");
|
||||
}
|
||||
if (!hasContractCue &&
|
||||
(/(?:клиент|покупател|заказчик|контрагент|альтернатива|свк)/iu.test(normalized) || hasRankingCue) &&
|
||||
(hasMoneyCue || /поступлен|приход|входящ|сделк|бюджет|inflow/iu.test(normalized))) {
|
||||
return unicodeBridgeResolution("customer_revenue_and_payments", "high", "unicode_customer_revenue_bridge_signal_detected");
|
||||
}
|
||||
if (!hasContractCue &&
|
||||
/(?:кто.*(?:деньг|денег|доход|выручк).*(?:прин[её]с|зан[её]с|плат)|(?:жирн|ликвидн).*контрагент.*(?:деньг|денег))/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("customer_revenue_and_payments", "high", "unicode_customer_revenue_bridge_signal_detected");
|
||||
}
|
||||
if (/(?:общие\s+обороты|общая\s+выручк|оборот.*за\s+все\s+время|выручк.*за\s+все\s+время)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("customer_revenue_and_payments", "high", "unicode_customer_revenue_bridge_signal_detected");
|
||||
}
|
||||
if (/(?:открыт(?:ые|ая|ый)?\s+задолж|открыт(?:ые|ая|ый)?\s+позици|позици.*по\s+договор|open\s+items?)/iu.test(normalized) &&
|
||||
(hasContractCue || hasCounterpartyCue || hasAccountAnchor || /покупател|клиент/iu.test(normalized))) {
|
||||
return unicodeBridgeResolution("open_items_by_counterparty_or_contract", "high", "unicode_open_items_bridge_signal_detected");
|
||||
}
|
||||
if (hasContractCue &&
|
||||
/(?:нескольк(?:ими|о)?\s+договор|контрагент.*нескольк.*договор|какие\s+из\s+договор.*актив)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("contract_usage_and_value", "high", "unicode_contract_usage_value_bridge_signal_detected");
|
||||
}
|
||||
if (hasContractCue && (hasMoneyCue || hasRankingCue || /оборот|бюджет|сумм|стоим|value|amount/iu.test(normalized))) {
|
||||
return unicodeBridgeResolution("contract_usage_and_value", "high", "unicode_contract_usage_value_bridge_signal_detected");
|
||||
}
|
||||
if (/(?:сальдо.*(?:расход|не\s+совпад)|расход.*сальдо|акт(?:ом|ах)?\s+сверк|плат[её]ж[и]?,?\s+но\s+нет\s+док|документ(?:ы)?\s+есть,?\s+а\s+оплат\s+нет|(?:оплат|плат[её]ж|отгрузк|закрыти[ея]\s+счет)[\p{L}\s,]*\s+без\s+(?:закрыт|документ|подтвержд)|аванс.*давно\s+не\s+закрыт)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("list_open_contracts", "high", "unicode_open_contracts_list_bridge_signal_detected");
|
||||
}
|
||||
if (/(?:долгожител|долго\s+долж|задолженн(?:ост|остям).*(?:давн|долго)|не\s+плат|не\s+оплат|не\s+оплачен|неоплачен|просроч|сроки\s+давно\s+прошл|слишком\s+длинн.*оплат)/iu.test(normalized) &&
|
||||
/(?:покупател|клиент|заказ|отгрузк|товар|услуг|задолженн|сальдо|не\s+плат|не\s+оплат|не\s+оплачен|неоплачен|просроч)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("list_receivables_counterparties", "high", "receivables_debt_lifecycle_signal_detected");
|
||||
}
|
||||
if (/(?:сальдо.*(?:расход|не\s+совпад)|расход.*сальдо|акт(?:ом|ах)?\s+сверк|плат[её]ж[и]?,?\s+но\s+нет\s+док|документ(?:ы)?\s+есть,?\s+а\s+оплат\s+нет|(?:оплат|плат[её]ж|отгрузк|закрыти[ея]\s+счет)[\p{L}\s,]*\s+без\s+(?:закрыт|документ|подтвержд)|аванс.*давно\s+не\s+закрыт)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("list_open_contracts", "high", "unicode_open_contracts_list_bridge_signal_detected");
|
||||
}
|
||||
if (/(?:открыт(?:ые|ая|ый)?\s+позици|позици.*по\s+договор|open\s+items?)/iu.test(normalized) &&
|
||||
(hasContractCue || hasCounterpartyCue || hasAccountAnchor)) {
|
||||
return unicodeBridgeResolution("open_items_by_counterparty_or_contract", "high", "unicode_open_items_bridge_signal_detected");
|
||||
}
|
||||
if (hasAccountAnchor &&
|
||||
hasDocumentCue &&
|
||||
/(?:формир|под\s+остат|раскр(?:ой|ыть|ывай)|остат(?:ок|ком)?\s+по\s+док|documents?\s+forming|docs?\s+forming)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("documents_forming_balance", "high", "unicode_documents_forming_balance_bridge_signal_detected");
|
||||
}
|
||||
if (hasContractCue && hasSpecificContractCue && hasBankCue) {
|
||||
return unicodeBridgeResolution("bank_operations_by_contract", "high", "unicode_bank_ops_by_contract_bridge_signal_detected");
|
||||
}
|
||||
if (hasContractCue && hasSpecificContractCue && hasDocumentCue) {
|
||||
return unicodeBridgeResolution("list_documents_by_contract", "high", "unicode_documents_by_contract_bridge_signal_detected");
|
||||
}
|
||||
if (hasAccountAnchor &&
|
||||
!hasDocumentCue &&
|
||||
/(?:баланс|остат(?:ок)?|сальдо|что\s+на\s+сч(?:е|ё)те|по\s+сч(?:е|ё)ту|скольк|скока|account\s+balance|balance\s+account|as\s+of)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("account_balance_snapshot", "high", "unicode_account_balance_bridge_signal_detected");
|
||||
}
|
||||
if (/(?:ндс|vat)/iu.test(normalized)) {
|
||||
const hasVatDebtCue = /(?:долг|должн|подтвержд)/iu.test(normalized);
|
||||
const hasTaxPeriodCue = /(?:налогов|налоговую|бюджет|декларац|квартал|\b[1-4]\s*кв)/iu.test(normalized);
|
||||
const hasVatMonthPeriodCue = /(?:за|на|в)\s+(?:январ|феврал|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр)\S*(?:\s+(?:19|20)\d{2})?/iu.test(normalized) &&
|
||||
!/\b\d{1,2}\s+(?:январ|феврал|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр)\S*(?:\s+(?:19|20)\d{2})?/iu.test(normalized);
|
||||
if ((hasTaxPeriodCue || (hasVatMonthPeriodCue && !hasVatDebtCue)) &&
|
||||
/(?:скольк|скока|надо|нужно|заплат|уплат|оплат|прикин)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("vat_liability_confirmed_for_tax_period", "high", "vat_liability_confirmed_tax_period_signal_detected");
|
||||
}
|
||||
if (/(?:прогноз|прикин|план)/iu.test(normalized) ||
|
||||
(!hasVatDebtCue && /(?:надо|нужно)\s+(?:заплат|оплат|уплат)/iu.test(normalized))) {
|
||||
return unicodeBridgeResolution("vat_payable_forecast", "high", "forecast_tax_signal_detected");
|
||||
}
|
||||
if (/(?:долг|подтвержд|скольк|скока|надо|нужно|заплат|уплат|оплат)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution(hasTaxPeriodCue
|
||||
? "vat_liability_confirmed_for_tax_period"
|
||||
: "vat_payable_confirmed_as_of_date", "high", "vat_payable_confirmed_signal_detected");
|
||||
}
|
||||
}
|
||||
if (/(?:незакрыт|открыт).*договор/iu.test(normalized) &&
|
||||
!/(?:долг|задолж|хвост|висит|расчет|расчёт)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("open_contracts_confirmed_as_of_date", "high", "unicode_open_contracts_snapshot_bridge_signal_detected");
|
||||
}
|
||||
if (/(?:долг|задолж|хвост|висит|открыт(?:ые|ая|ый)?\s+задолж|open\s+items?)/iu.test(normalized) &&
|
||||
(hasContractCue || hasCounterpartyCue || hasAccountAnchor || /покупател|клиент/iu.test(normalized))) {
|
||||
return unicodeBridgeResolution("open_items_by_counterparty_or_contract", "high", "unicode_open_items_bridge_signal_detected");
|
||||
}
|
||||
if (hasContractCue &&
|
||||
/(?:без\s+(?:закрыт|оплат|плат[её]ж|док)|не\s+закрыт|аванс|отгрузк|плат[её]ж.*без|док.*без|расхожд|mismatch)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("list_open_contracts", "high", "unicode_open_contracts_list_bridge_signal_detected");
|
||||
}
|
||||
if (hasContractCue &&
|
||||
/(?:скольк.*(?:всего\s+)?договор|договор.*(?:заведен|использовал|реально\s+использ)|сколько\s+из\s+них)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("contract_usage_overview", "high", "unicode_contract_usage_overview_bridge_signal_detected");
|
||||
}
|
||||
if (hasContractCue &&
|
||||
/(?:нескольк(?:ими|о)?\s+договор|контрагент.*нескольк.*договор|какие\s+из\s+договор.*актив)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("contract_usage_and_value", "high", "unicode_contract_usage_value_bridge_signal_detected");
|
||||
}
|
||||
if (hasContractCue &&
|
||||
/(?:все|покажи|показать|какие|список|list|show)/iu.test(normalized) &&
|
||||
!hasSpecificContractCue &&
|
||||
!hasDocumentCue &&
|
||||
!hasBankCue &&
|
||||
(hasCounterpartyCue || hasLooseCounterpartyByAnchor)) {
|
||||
return unicodeBridgeResolution("list_contracts_by_counterparty", "high", "unicode_contracts_by_counterparty_bridge_signal_detected");
|
||||
}
|
||||
if (hasContractCue && !hasSpecificContractCue && !hasDocumentCue && !hasBankCue && hasCounterpartyCue) {
|
||||
return unicodeBridgeResolution("list_contracts_by_counterparty", "high", "unicode_contracts_by_counterparty_bridge_signal_detected");
|
||||
}
|
||||
if (hasBankCue && (hasCounterpartyCue || hasLooseCounterpartyByAnchor) && !hasContractCue) {
|
||||
return unicodeBridgeResolution("bank_operations_by_counterparty", "high", "unicode_bank_ops_by_counterparty_bridge_signal_detected");
|
||||
}
|
||||
if (hasDocumentCue && (hasCounterpartyCue || hasLooseCounterpartyByAnchor) && !hasContractCue && !hasAccountAnchor) {
|
||||
return unicodeBridgeResolution("list_documents_by_counterparty", "high", "unicode_documents_by_counterparty_bridge_signal_detected");
|
||||
}
|
||||
if (/(?:за\s+какие\s+годы|диапазон\s+лет|покрыт(?:ие|ый)\s+период|какой\s+год.*актив|какой\s+месяц.*актив|год.*пассив|месяц.*пассив|минимальн.*док|минимальн.*операц|profile\s+period|top\s*year|top\s*month)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("period_coverage_profile", "high", "unicode_period_coverage_bridge_signal_detected");
|
||||
}
|
||||
if (/(?:тип(?:ы|ов)?\s+док|документ.*(?:чаще|редк|больше\s+всего|меньше\s+всего)|раздел(?:ы|ов)?\s+уч[её]та|сводк.*тип.*док|document\s+type|account\s+section)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("document_type_and_account_section_profile", "high", "unicode_document_type_profile_bridge_signal_detected");
|
||||
}
|
||||
if (/(?:скольк|скока|число|количеств|разбей|раздели|сформируй\s+сводк)/iu.test(normalized) &&
|
||||
/(?:контрагент|поставщик|клиент|покупател|заказчик|рол)/iu.test(normalized) &&
|
||||
!/(?:активн|давно|нов(?:ые|ых)|однораз|уш[её]л|исчез|регулярн|эпизодич|частот|разов|churn|lifecycle)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("counterparty_population_and_roles", "high", "unicode_counterparty_population_bridge_signal_detected");
|
||||
}
|
||||
if (/(?:скок|скока|сколько)\s+(?:клиент|покупател|заказчик)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("counterparty_population_and_roles", "high", "unicode_counterparty_population_bridge_signal_detected");
|
||||
}
|
||||
if (/(?:активн(?:ые|ость)?\s+(?:клиент|покупател|поставщик|контрагент)|все\s+время|однораз|давно\s+(?:не\s+)?(?:покупал|платил|актив)|уш[её]л|исчез|нов(?:ые|ых)\s+(?:клиент|контрагент)|регулярн|разов(?:ый|ые)|stale\s+supplier|churn|lifecycle)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("counterparty_activity_lifecycle", "high", "unicode_counterparty_lifecycle_bridge_signal_detected");
|
||||
}
|
||||
if (hasContractCue && /(?:давно\s+не\s+использ|не\s+использ|stale|inactive)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("contract_usage_overview", "high", "unicode_contract_usage_overview_bridge_signal_detected");
|
||||
}
|
||||
if (hasContractCue && (hasMoneyCue || hasRankingCue || /оборот|бюджет|сумм|стоим|value|amount/iu.test(normalized))) {
|
||||
return unicodeBridgeResolution("contract_usage_and_value", "high", "unicode_contract_usage_value_bridge_signal_detected");
|
||||
}
|
||||
if (/(?:поставщик|vendor|supplier|кому\s+(?:ушло|платили|заплатили)|выплат|исходящ|списан|сгрузил)/iu.test(normalized) &&
|
||||
(hasMoneyCue || hasRankingCue || /плат[её]ж|оплат|выплат|outflow|payout/iu.test(normalized))) {
|
||||
return unicodeBridgeResolution("supplier_payouts_profile", "high", "unicode_supplier_payouts_bridge_signal_detected");
|
||||
}
|
||||
if ((/(?:клиент|покупател|заказчик|контрагент|альтернатива|свк)/iu.test(normalized) || hasRankingCue) &&
|
||||
(hasMoneyCue || /поступлен|приход|входящ|inflow/iu.test(normalized))) {
|
||||
return unicodeBridgeResolution("customer_revenue_and_payments", "high", "unicode_customer_revenue_bridge_signal_detected");
|
||||
}
|
||||
if (/(?:к[оа]му\s+мы\s+должны|мы\s+должны\s+к[оа]му|кредитор|payables?)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("payables_confirmed_as_of_date", "high", "payables_debt_lifecycle_signal_detected");
|
||||
}
|
||||
if (/(?:кто\s+нам\s+должен|нам\s+должны|дебитор|receivables?)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("receivables_confirmed_as_of_date", "high", "unicode_receivables_snapshot_bridge_signal_detected");
|
||||
}
|
||||
if (/(?:покупател|клиент).*(?:не\s+плат|просроч|долго\s+долж|долг.*давн)|(?:долг|задолж).*(?:покупател|клиент)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("list_receivables_counterparties", "high", "unicode_receivables_list_bridge_signal_detected");
|
||||
}
|
||||
if (/(?:что|че|чё|какие|покажи|показать|список).*(?:склад|остат|товар)|(?:склад|остат).*(?:сейчас|лежит|есть|на\s+дату|на\s+конец|what|show|list)/iu.test(normalized) &&
|
||||
!/(?:поставщик|продаж|реализ|цепоч|документал|давно|стар(?:ые|ый|ым|ых)|закуп)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("inventory_on_hand_as_of_date", "high", "unicode_inventory_on_hand_bridge_signal_detected");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function resolveAddressIntent(userMessage) {
|
||||
const text = String(userMessage ?? "").trim().toLowerCase();
|
||||
const repairedText = repairLikelyUtf8Mojibake(text).trim().toLowerCase();
|
||||
|
|
@ -1559,6 +1873,10 @@ function resolveAddressIntent(userMessage) {
|
|||
.replace(/(^|[^\p{L}0-9_])\u043d\u0430\u043c\u0441(?=$|[^\p{L}0-9_])/giu, "$1\u043d\u0430\u043c")
|
||||
.replace(/(^|[^\p{L}0-9_])\u043a\u0430\u043a\u0438\u0435\u043a(?=$|[^\p{L}0-9_])/giu, "$1\u043a\u0430\u043a\u0438\u0435");
|
||||
const currentTurnBridgeText = turnNoiseNormalizedBridgeText !== bridgeText ? `${bridgeText} ${turnNoiseNormalizedBridgeText}` : bridgeText;
|
||||
const unicodeAddressIntent = resolveUnicodeAddressIntentBridge(currentTurnBridgeText);
|
||||
if (unicodeAddressIntent) {
|
||||
return unicodeAddressIntent;
|
||||
}
|
||||
const hasLooseVatPayableBridge = /(?:\u043d\u0434\u0441|vat)/iu.test(text) &&
|
||||
/(?:\u043a\u0430\u043a\u043e\u0439\s+\u043d\u0434\u0441\s+(?:(?:\u043d\u0430\u043c|(?:\u043c\u044b\s+)?\u0434\u043e\u043b\u0436\u043d\u044b)\s+)?(?:\u043d\u0430\u0434\u043e|\u043d\u0443\u0436\u043d\u043e|\u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e)|(?:\u043d\u0430\u043c|\u043c\u044b\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|\u043c\u044b\s+)?\u043d\u0443\u0436\u043d\u043e\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|\u043c\u044b\s+\u0434\u043e\u043b\u0436\u043d\u044b\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\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})?|\u0432\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);
|
||||
|
|
@ -1589,7 +1907,7 @@ function resolveAddressIntent(userMessage) {
|
|||
: ["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);
|
||||
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|\u0437\u0430\u043a\u0443\u043f\u043b\u0435\u043d(?:\u043d\u044b\u0435|\u043d\u044b\u043c|\u043d\u044b\u0445|\u0430\u044f|\u044b\u0439)?\s+\u0437\u0430\u0434\u043e\u043b\u0433\u043e\s+\u0434\u043e|\u0437\u0430\u0434\u043e\u043b\u0433\u043e\s+\u0434\u043e|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",
|
||||
|
|
|
|||
|
|
@ -50,12 +50,21 @@ function hasInventoryOnHandSignal(text) {
|
|||
(hasRequestCue || hasBalanceLexeme || hasColloquialStockSnapshotCue || hasStockStateCue);
|
||||
}
|
||||
function hasSelectedObjectInventoryCue(text) {
|
||||
const value = String(text ?? "");
|
||||
if (/(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+нему|по\s+ней|по\s+ним|по\s+нему\s+же|по\s+ней\s+же|selected\s+object)/iu.test(value)) {
|
||||
return true;
|
||||
}
|
||||
return /(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+нему|по\s+ней|по\s+ним|по\s+нему\s+же|по\s+ней\s+же|selected\s+object)/iu.test(String(text ?? ""));
|
||||
}
|
||||
function hasSelectedObjectInventoryProvenanceSignal(text) {
|
||||
return hasSelectedObjectInventoryCue(text) && (0, inventoryLifecycleCueHelpers_1.hasInventorySupplierCue)(text);
|
||||
}
|
||||
function hasSelectedObjectInventoryPurchaseDocumentsSignal(text) {
|
||||
const value = String(text ?? "");
|
||||
if (hasSelectedObjectInventoryCue(value) &&
|
||||
/(?:по\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)?\s*купили|по\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)?\s*купили|какими\s+документами\s+(?:был\s+)?куплен|покажи\s+документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(value)) {
|
||||
return true;
|
||||
}
|
||||
const hasPurchaseDocumentsCue = /(?:по\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|по\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|какими\s+документами\s+(?:был\s+)?куплен|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(text) ||
|
||||
/(?:(?:по\s+каким|какими)\s+док[а-яё]*[\s\S]{0,80}(?:купил|куплен)|док(?:и|умент[а-яё]*)[\s\S]{0,80}(?:по\s+(?:ним|ней|нему|этой\s+позиции|этому\s+товару)|операци)|(?:по\s+(?:ним|ней|нему|этой\s+позиции|этому\s+товару))[\s\S]{0,80}док(?:и|умент[а-яё]*))/iu.test(text);
|
||||
return hasSelectedObjectInventoryCue(text) && hasPurchaseDocumentsCue;
|
||||
|
|
@ -67,6 +76,13 @@ function hasSelectedObjectInventoryProfitabilitySignal(text) {
|
|||
return hasSelectedObjectInventoryCue(text) && (0, inventoryLifecycleCueHelpers_1.hasInventoryProfitabilityCue)(text);
|
||||
}
|
||||
function hasInventoryProvenanceSignalV2(text) {
|
||||
const value = String(text ?? "");
|
||||
const hasPlainItemCue = /(?:товар|номенклатур|sku|item|product|остат|склад)/iu.test(value);
|
||||
const hasPlainSupplierCue = (0, inventoryLifecycleCueHelpers_1.hasInventorySupplierCue)(value) || /(?:кем\s+поставлен|кто\s+(?:это|его|этот\s+товар|эту\s+позицию)?\s*поставил)/iu.test(value);
|
||||
const hasPlainPurchaseCue = /(?:куплен(?:ы|а|о)?|закупк|происхожд|откуда|где\s+(?:мы\s+)?купили|поставлен(?:ы|а)?|purchase\s+provenance|purchase\s+date)/iu.test(value) || (0, inventoryLifecycleCueHelpers_1.hasInventoryPurchaseStem)(value);
|
||||
if (hasPlainItemCue && hasPlainSupplierCue && hasPlainPurchaseCue) {
|
||||
return true;
|
||||
}
|
||||
const hasItemCue = /(?:товар|номенклатур|sku|item|product|остат(?:ок|ки)|склад)/iu.test(text);
|
||||
const hasSupplierCue = (0, inventoryLifecycleCueHelpers_1.hasInventorySupplierCue)(text) || /кем\s+поставлен/iu.test(text);
|
||||
const hasPurchaseCue = /(?:куплен(?:ы|а|о)?|закупк|происхождени|откуда|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|товар|позицию))?|где\s+куплено|когда\s+был\s+куплен|когда\s+куплен|дата\s+закупк|кто\s+(?:нам\s+)?поставил|кем\s+поставлен|поставлен(?:ы|а)?|purchase\s+provenance|purchase\s+date)/iu.test(text) || (0, inventoryLifecycleCueHelpers_1.hasInventoryPurchaseStem)(text);
|
||||
|
|
@ -79,16 +95,39 @@ function hasInventoryPurchaseDateSignal(text) {
|
|||
return hasItemCue && hasPurchaseDateCue;
|
||||
}
|
||||
function hasInventoryPurchaseDocumentsSignalV2(text) {
|
||||
const value = String(text ?? "");
|
||||
const hasPlainItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(value);
|
||||
const hasPlainPurchaseDocCue = /(?:по\s+каким\s+документам\s+(?:был\s+)?куплен|по\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)?\s*купили|какими\s+документами\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)?\s*купили|документ[а-яё]*\s+закупк|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(value);
|
||||
if (hasPlainItemCue && hasPlainPurchaseDocCue) {
|
||||
return true;
|
||||
}
|
||||
const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text);
|
||||
const hasPurchaseDocCue = /(?:по\s+каким\s+документам\s+был\s+куплен|по\s+каким\s+документам\s+куплен|какими\s+документами\s+был\s+куплен|документ(?:ам|ы)\s+закупк|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(text);
|
||||
return hasItemCue && hasPurchaseDocCue;
|
||||
}
|
||||
function hasInventorySaleTraceSignalV2(text) {
|
||||
const value = String(text ?? "");
|
||||
const hasPlainItemCue = /(?:товар|номенклатур|позици|продукци|sku|item|product)/iu.test(value);
|
||||
const hasPlainTraceCue = /(?:кому\s+(?:в\s+итоге\s+)?(?:мы\s+)?(?:продали|реализовали|впарили)|кому\s+(?:был[аио]?|были)?\s*реализован|кто\s+купил|покупател|buyer|sale\s+trace|trace\s+of\s+sale)/iu.test(value);
|
||||
if (hasPlainItemCue && hasPlainTraceCue) {
|
||||
return true;
|
||||
}
|
||||
const hasItemCue = /(?:товар|номенклатур|sku|item|product|позици(?:я|ю|и)|продукци(?:я|ю|и))/iu.test(text);
|
||||
const hasTraceCue = /(?:кому\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали|кому\s+был\s+продан|куда\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали(?:\s+(?:это|его|товар|позицию))?|куда\s+(?:была\s+)?реализована\s+(?:позиция|номенклатура|продукция)|кто\s+купил|buyer|sale\s+trace|trace\s+of\s+sale|через\s+какие\s+документы\s+прош[её]л\s+путь\s+товара|закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*warehouse\s*->\s*sale|purchase\s*->\s*stock\s*->\s*sale)/iu.test(text);
|
||||
return hasItemCue && hasTraceCue;
|
||||
}
|
||||
function hasInventorySupplierStockOverlapSignal(text) {
|
||||
const value = String(text ?? "");
|
||||
const hasPlainDirectSingleItemSupplierQuestion = /(?:от\s+(?:какого|кого)\s+поставщик[а-яё]*\s+куплен\s+(?:товар|номенклатур|позици)|от\s+кого\s+куплен\s+(?:товар|номенклатур|позици))/iu.test(value);
|
||||
if (hasPlainDirectSingleItemSupplierQuestion) {
|
||||
return false;
|
||||
}
|
||||
const hasPlainSupplierCue = /(?:поставщик|supplier|vendor)/iu.test(value);
|
||||
const hasPlainStockCue = /(?:склад|остат|леж[аи][т]?|висят|сейчас\s+еще|сейчас\s+ещё|на\s+дату|current\s+stock|stock\s+overlap)/iu.test(value);
|
||||
const hasPlainUnresolvedSupplierLink = /(?:без\s+понятн[а-яё]*\s+привязк[а-яё]*\s+к\s+поставщик|без\s+привязк[а-яё]*\s+к\s+поставщик|unresolved\s+supplier\s+link)/iu.test(value);
|
||||
if ((hasPlainSupplierCue && hasPlainStockCue) || (hasPlainUnresolvedSupplierLink && hasPlainStockCue)) {
|
||||
return true;
|
||||
}
|
||||
const hasDirectSingleItemSupplierQuestion = /(?:от\s+какого\s+поставщика\s+куплен\s+(?:товар|номенклатур(?:а|у|ы)|позици(?:я|ю|и))|от\s+кого\s+куплен\s+(?:товар|номенклатур(?:а|у|ы)|позици(?:я|ю|и)))/iu.test(text);
|
||||
if (hasDirectSingleItemSupplierQuestion) {
|
||||
return false;
|
||||
|
|
@ -98,16 +137,36 @@ function hasInventorySupplierStockOverlapSignal(text) {
|
|||
return hasSupplierCue && hasStockCue;
|
||||
}
|
||||
function hasInventoryAgingSignal(text) {
|
||||
const value = String(text ?? "");
|
||||
const hasPlainResidueCue = /(?:остат|склад|stock\s+residue|stock\s+balance)/iu.test(value);
|
||||
const hasPlainAgingCue = /(?:стар(?:ые|ым|ых)\s+закупк|очень\s+давно|давно\s+(?:куплен|приобретен|приобретён)|закуплен[а-яё]*\s+задолго\s+до|задолго\s+до(?:\s+\d{4}-\d{2}-\d{2})?|возраст\s+(?:остатк|закупк)|aging\s+by\s+purchase\s+date|old\s+purchase|old\s+purchases|old\s+stock|very\s+old\s+stock|very\s+old\s+purchase)/iu.test(value);
|
||||
if (hasPlainAgingCue || (hasPlainResidueCue && /(?:давно|задолго\s+до|стар(?:ые|ым|ых)\s+закупк)/iu.test(value))) {
|
||||
return true;
|
||||
}
|
||||
const hasResidueCue = /(?:остат(?:ок|ки)|в\s+остатке|среди\s+текущих\s+остатков|на\s+складе|stock\s+residue|stock\s+balance)/iu.test(text);
|
||||
const hasAgingCue = /(?:стар(?:ые|ым|ых)\s+закупк|стары(?:м|х)\s+закупк(?:ам|и|ах)|относит(?:ся|ся\s+ли)?\s+.*\s+к\s+старым\s+закупк|закупал(?:ись|ся)\s+очень\s+давно|очень\s+давно|давно\s+куплен|давно\s+приобретен|куплен\s+задолго\s+до(?:\s+даты)?|закуплен(?:ы|а)?\s+давно|приобретен\s+давно|задолго\s+до(?:\s+даты)?|возраст\s+остатк|возраст\s+закупк|aged?\s+stock|old\s+purchase|old\s+purchases|old\s+stock|bought\s+long\s+ago|purchased\s+long\s+ago|aging\s+by\s+purchase\s+date|very\s+old\s+stock|very\s+old\s+purchase|old\s+procurement|older\s+purchases|aged\s+items|old\s+goods)/iu.test(text);
|
||||
return hasAgingCue || (hasResidueCue && /(?:давно\s+куплен|давно\s+приобретен|задолго\s+до)/iu.test(text));
|
||||
}
|
||||
function hasInventoryPurchaseToSaleChainSignal(text) {
|
||||
const value = String(text ?? "");
|
||||
const hasPlainItemCue = /(?:товар|номенклатур|позици|sku|item|product)/iu.test(value);
|
||||
const hasPlainChainCue = /(?:закупк[а-яё]*\s*->\s*склад\s*->\s*продаж|через\s+какие\s+документы\s+прош[её]л\s+путь|цепочк[а-яё]*\s+движен|документально\s+подтвержденн[а-яё]*\s+цепочк|supplier\s*->\s*item\s*->\s*(?:buyer|customer)|supplier\s+to\s+buyer|supplier\s+to\s+item\s+to\s+buyer|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*(?:warehouse|stock)\s*->\s*sale)/iu.test(value) || value.includes("->");
|
||||
if (hasPlainItemCue && hasPlainChainCue) {
|
||||
return true;
|
||||
}
|
||||
const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text);
|
||||
const hasChainCue = /(?:закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*(?:warehouse|stock)\s*->\s*sale|закупка\s*->\s*склад\s*->\s*продажа|цепочк[аи]\s+движен|документально\s+подтвержденн\w+\s+цепочк|supplier\s*->\s*item\s*->\s*(?:buyer|customer)|supplier\s+to\s+buyer|supplier\s+to\s+item\s+to\s+buyer)/iu.test(text) || text.includes("->");
|
||||
return hasItemCue && hasChainCue;
|
||||
}
|
||||
function hasInventorySupplierToBuyerChainSignal(text) {
|
||||
const value = String(text ?? "");
|
||||
const hasPlainSupplierCue = /(?:поставщик|supplier|vendor)/iu.test(value);
|
||||
const hasPlainBuyerCue = /(?:покупател|buyer|customer|client)/iu.test(value);
|
||||
const hasPlainItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(value);
|
||||
const hasPlainChainCue = /(?:документально\s+подтвержденн[а-яё]*\s+цепочк|поставщик\s*->\s*товар\s*->\s*покупател|supplier\s*->\s*item\s*->\s*(?:buyer|customer)|supplier\s*->\s*buyer|supplier\s+to\s+buyer|supplier\s+to\s+item\s+to\s+buyer)/iu.test(value) || value.includes("->");
|
||||
if (hasPlainSupplierCue && hasPlainBuyerCue && hasPlainItemCue && hasPlainChainCue) {
|
||||
return true;
|
||||
}
|
||||
const hasSupplierCue = /(?:поставщик|supplier|vendor)/iu.test(text);
|
||||
const hasBuyerCue = /(?:покупател|buyer|customer|client)/iu.test(text);
|
||||
const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ exports.cloneAddressNavigationState = cloneAddressNavigationState;
|
|||
exports.normalizeAddressNavigationState = normalizeAddressNavigationState;
|
||||
exports.evolveAddressNavigationStateWithAssistantItem = evolveAddressNavigationStateWithAssistantItem;
|
||||
const nanoid_1 = require("nanoid");
|
||||
const assistantContinuityPolicy_1 = require("./assistantContinuityPolicy");
|
||||
const addressNavigation_1 = require("../types/addressNavigation");
|
||||
const MAX_RESULT_SETS = 40;
|
||||
const MAX_NAVIGATION_EVENTS = 120;
|
||||
|
|
@ -242,24 +243,32 @@ function resolveNavigationAction(debug, hasFocusObject) {
|
|||
}
|
||||
return hasFocusObject ? "drilldown" : "open";
|
||||
}
|
||||
function buildFocusObjectFromDebug(debug, resultSetId, createdAt) {
|
||||
const extractedFilters = toObject(debug.extracted_filters) ?? {};
|
||||
const rawValue = toNonEmptyString(debug.anchor_value_resolved) ??
|
||||
toNonEmptyString(debug.anchor_value_raw) ??
|
||||
toNonEmptyString(extractedFilters.item);
|
||||
if (!rawValue) {
|
||||
return null;
|
||||
}
|
||||
const objectType = toAddressFocusObjectType(debug.anchor_type);
|
||||
const canonicalType = objectType === "unknown" ? inferDisplayEntityType(toAddressIntent(debug.detected_intent)) : objectType;
|
||||
function buildFocusObject(objectType, label, resultSetId, createdAt) {
|
||||
return {
|
||||
object_type: canonicalType,
|
||||
object_id: `${canonicalType}:${rawValue}`.toLowerCase(),
|
||||
label: rawValue,
|
||||
object_type: objectType,
|
||||
object_id: `${objectType}:${label}`.toLowerCase(),
|
||||
label,
|
||||
provenance_result_set_id: resultSetId,
|
||||
selected_at: createdAt
|
||||
};
|
||||
}
|
||||
function buildFocusObjectFromDebug(debug, resultSetId, createdAt) {
|
||||
const extractedFilters = toObject(debug.extracted_filters) ?? {};
|
||||
const objectType = toAddressFocusObjectType(debug.anchor_type);
|
||||
const canonicalType = objectType === "unknown" ? inferDisplayEntityType(toAddressIntent(debug.detected_intent)) : objectType;
|
||||
if (canonicalType === "item") {
|
||||
const item = (0, assistantContinuityPolicy_1.readAddressDebugItem)(debug, toNonEmptyString);
|
||||
return item ? buildFocusObject(canonicalType, item, resultSetId, createdAt) : null;
|
||||
}
|
||||
if (canonicalType === "counterparty" && debug.mcp_discovery_response_applied === true) {
|
||||
const counterparty = (0, assistantContinuityPolicy_1.readAddressDebugCounterparty)(debug, toNonEmptyString);
|
||||
return counterparty ? buildFocusObject(canonicalType, counterparty, resultSetId, createdAt) : null;
|
||||
}
|
||||
const rawValue = toNonEmptyString(debug.anchor_value_resolved) ??
|
||||
toNonEmptyString(debug.anchor_value_raw) ??
|
||||
toNonEmptyString(extractedFilters.item);
|
||||
return rawValue ? buildFocusObject(canonicalType, rawValue, resultSetId, createdAt) : null;
|
||||
}
|
||||
function capResultSets(resultSets) {
|
||||
if (resultSets.length <= MAX_RESULT_SETS) {
|
||||
return resultSets;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ const resolveStage_1 = require("./address_runtime/resolveStage");
|
|||
const composeStage_1 = require("./address_runtime/composeStage");
|
||||
const addressCapabilityPolicy_1 = require("./addressCapabilityPolicy");
|
||||
const addressRouteExpectations_1 = require("./addressRouteExpectations");
|
||||
const addressTextRepair_1 = require("./addressTextRepair");
|
||||
const assistantOrganizationMatcher_1 = require("./assistantOrganizationMatcher");
|
||||
const addressCoverageEvidencePolicy_1 = require("./addressCoverageEvidencePolicy");
|
||||
const addressTruthGatePolicy_1 = require("./addressTruthGatePolicy");
|
||||
|
|
@ -191,6 +192,25 @@ function normalizeIsoDateForQuery(value) {
|
|||
}
|
||||
return `${match[1]}-${match[2]}-${match[3]}`;
|
||||
}
|
||||
function deriveTaxQuarterWindowForDate(value) {
|
||||
const isoDate = normalizeIsoDateForQuery(value);
|
||||
if (!isoDate) {
|
||||
return null;
|
||||
}
|
||||
const match = isoDate.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const year = Number(match[1]);
|
||||
const month = Number(match[2]);
|
||||
const quarterStartMonth = Math.floor((month - 1) / 3) * 3 + 1;
|
||||
const quarterEndMonth = quarterStartMonth + 2;
|
||||
const quarterEndDay = new Date(Date.UTC(year, quarterEndMonth, 0)).getUTCDate();
|
||||
return {
|
||||
period_from: `${year}-${String(quarterStartMonth).padStart(2, "0")}-01`,
|
||||
period_to: `${year}-${String(quarterEndMonth).padStart(2, "0")}-${String(quarterEndDay).padStart(2, "0")}`
|
||||
};
|
||||
}
|
||||
function toDateTimeExprForQuery(isoDate) {
|
||||
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
if (!match) {
|
||||
|
|
@ -1598,6 +1618,21 @@ function isOrganizationScopedInventoryIntent(intent) {
|
|||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date");
|
||||
}
|
||||
function isOrganizationScopedValueFlowIntent(intent) {
|
||||
return intent === "customer_revenue_and_payments" || intent === "supplier_payouts_profile";
|
||||
}
|
||||
function hasExplicitSingleOrganizationValueFlowScopeRequest(userMessage) {
|
||||
const raw = String(userMessage ?? "").toLowerCase();
|
||||
const repaired = (0, addressTextRepair_1.repairAddressMojibakeText)(raw).toLowerCase();
|
||||
const normalized = `${raw} ${repaired}`;
|
||||
const hasOrganizationCue = /(?:организац|компани|фирм|контур|organization|company)/iu.test(normalized);
|
||||
if (!hasOrganizationCue) {
|
||||
return false;
|
||||
}
|
||||
const hasSingleOrganizationCue = /(?:по\s+одн(?:ой|у)\s+(?:организац|компани|фирм)|одн(?:ой|у)\s+(?:организац|компани|фирм)|one\s+(?:organization|company))/iu.test(normalized);
|
||||
const hasClarificationCue = /(?:если[^.?!]*(?:нужн|надо|потреб)[^.?!]*(?:организац|компани|фирм)|(?:уточн|спрос)[^.?!]*(?:организац|компани|фирм|ее|её))/iu.test(normalized);
|
||||
return hasSingleOrganizationCue || hasClarificationCue;
|
||||
}
|
||||
function shouldDeferInventoryOrganizationClarification(intent, filters, semanticFrame) {
|
||||
if (!isOrganizationScopedInventoryIntent(intent)) {
|
||||
return false;
|
||||
|
|
@ -1839,6 +1874,49 @@ function applyFutureDatedRowsGuard(rows, intent, referenceDate) {
|
|||
droppedCount
|
||||
};
|
||||
}
|
||||
function applyExplicitPeriodWindowFilter(rows, filters) {
|
||||
const periodFrom = typeof filters.period_from === "string" ? filters.period_from.trim() : "";
|
||||
const periodTo = typeof filters.period_to === "string" ? filters.period_to.trim() : "";
|
||||
if (!periodFrom && !periodTo) {
|
||||
return {
|
||||
rows,
|
||||
droppedCount: 0,
|
||||
applied: false
|
||||
};
|
||||
}
|
||||
const periodFromTs = periodFrom ? parseIsoDateUtcTimestamp(periodFrom) : null;
|
||||
const periodToTs = periodTo ? parseIsoDateUtcTimestamp(periodTo) : null;
|
||||
if ((periodFrom && periodFromTs === null) || (periodTo && periodToTs === null)) {
|
||||
return {
|
||||
rows,
|
||||
droppedCount: 0,
|
||||
applied: false
|
||||
};
|
||||
}
|
||||
const keptRows = [];
|
||||
let droppedCount = 0;
|
||||
for (const row of rows) {
|
||||
const rowTs = parseIsoDateUtcTimestamp(row.period);
|
||||
if (rowTs === null) {
|
||||
droppedCount += 1;
|
||||
continue;
|
||||
}
|
||||
if (periodFromTs !== null && rowTs < periodFromTs) {
|
||||
droppedCount += 1;
|
||||
continue;
|
||||
}
|
||||
if (periodToTs !== null && rowTs > periodToTs) {
|
||||
droppedCount += 1;
|
||||
continue;
|
||||
}
|
||||
keptRows.push(row);
|
||||
}
|
||||
return {
|
||||
rows: keptRows,
|
||||
droppedCount,
|
||||
applied: true
|
||||
};
|
||||
}
|
||||
function hasExplicitPeriodWindow(filters) {
|
||||
return ((typeof filters.period_from === "string" && filters.period_from.trim().length > 0) ||
|
||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0));
|
||||
|
|
@ -1855,6 +1933,7 @@ function canAutoBroadenPeriodWindow(intent, filters) {
|
|||
}
|
||||
return (intent === "list_documents_by_counterparty" ||
|
||||
intent === "bank_operations_by_counterparty" ||
|
||||
intent === "list_contracts_by_counterparty" ||
|
||||
intent === "list_documents_by_contract" ||
|
||||
intent === "bank_operations_by_contract" ||
|
||||
intent === "inventory_purchase_provenance_for_item" ||
|
||||
|
|
@ -2712,15 +2791,21 @@ class AddressQueryService {
|
|||
const baseReasons = [...decompose.baseReasons];
|
||||
const analysisDate = normalizeAnalysisDateHint(options.analysisDateHint);
|
||||
if (analysisDate) {
|
||||
const asOfWasDefaultedToday = filters.warnings.includes("as_of_date_defaulted_today");
|
||||
const hasTemporalFilter = Boolean((typeof filters.extracted_filters.period_from === "string" && filters.extracted_filters.period_from.trim().length > 0) ||
|
||||
(typeof filters.extracted_filters.period_to === "string" && filters.extracted_filters.period_to.trim().length > 0) ||
|
||||
(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0));
|
||||
if (!hasTemporalFilter) {
|
||||
if (!hasTemporalFilter || asOfWasDefaultedToday) {
|
||||
filters.extracted_filters = {
|
||||
...filters.extracted_filters,
|
||||
as_of_date: analysisDate
|
||||
};
|
||||
filters.warnings = [...new Set([...(filters.warnings ?? []), "as_of_date_from_analysis_context"])];
|
||||
filters.warnings = [
|
||||
...new Set([
|
||||
...(filters.warnings ?? []).filter((warning) => warning !== "as_of_date_defaulted_today"),
|
||||
"as_of_date_from_analysis_context"
|
||||
])
|
||||
];
|
||||
baseReasons.push("as_of_date_from_analysis_context");
|
||||
}
|
||||
}
|
||||
|
|
@ -2735,6 +2820,27 @@ class AddressQueryService {
|
|||
});
|
||||
const knownOrganizations = (0, assistantOrganizationMatcher_1.mergeKnownOrganizations)(options.knownOrganizations ?? []);
|
||||
const activeOrganization = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(options.activeOrganization ?? null);
|
||||
if (isOrganizationScopedValueFlowIntent(intent.intent) &&
|
||||
hasExplicitSingleOrganizationValueFlowScopeRequest(userMessage) &&
|
||||
!resolvedOrganizationFromMessage) {
|
||||
const clarificationFilters = { ...filters.extracted_filters };
|
||||
delete clarificationFilters.organization;
|
||||
return buildOrganizationClarificationExecutionResult({
|
||||
mode,
|
||||
shape,
|
||||
intent,
|
||||
filters: clarificationFilters,
|
||||
organizations: knownOrganizations,
|
||||
reasons: [...baseReasons, "organization_clarification_required_from_explicit_value_flow_scope"],
|
||||
semanticFrame,
|
||||
capabilityAudit: buildCapabilityAudit(intent.intent),
|
||||
shadowRouteAudit: buildShadowRouteAudit({
|
||||
intent: intent.intent,
|
||||
requestedResultMode: (0, addressCoverageEvidencePolicy_1.resolveAddressRequestedResultMode)(intent.intent, filters.extracted_filters, semanticFrame) ?? undefined,
|
||||
filters: filters.extracted_filters
|
||||
})
|
||||
});
|
||||
}
|
||||
if (isOrganizationScopedInventoryIntent(intent.intent) &&
|
||||
!toNonEmptyFilterValue(filters.extracted_filters.organization) &&
|
||||
!activeOrganization &&
|
||||
|
|
@ -2757,6 +2863,22 @@ class AddressQueryService {
|
|||
})
|
||||
});
|
||||
}
|
||||
if (intent.intent === "vat_liability_confirmed_for_tax_period" &&
|
||||
filters.warnings.includes("period_derived_from_month_phrase")) {
|
||||
const taxQuarterWindow = deriveTaxQuarterWindowForDate(filters.extracted_filters.period_to);
|
||||
if (taxQuarterWindow) {
|
||||
filters.extracted_filters = {
|
||||
...filters.extracted_filters,
|
||||
...taxQuarterWindow
|
||||
};
|
||||
filters.warnings = [
|
||||
...new Set([...(filters.warnings ?? []), "period_derived_from_tax_quarter_for_confirmed_vat_liability"])
|
||||
];
|
||||
if (!baseReasons.includes("period_derived_from_tax_quarter_for_confirmed_vat_liability")) {
|
||||
baseReasons.push("period_derived_from_tax_quarter_for_confirmed_vat_liability");
|
||||
}
|
||||
}
|
||||
}
|
||||
const requestedResultMode = (0, addressCoverageEvidencePolicy_1.resolveAddressRequestedResultMode)(intent.intent, filters.extracted_filters, semanticFrame) ?? undefined;
|
||||
const confirmedBalancePayablesIntent = (intent.intent === "list_payables_counterparties" || intent.intent === "payables_confirmed_as_of_date") &&
|
||||
requestedResultMode === "confirmed_balance";
|
||||
|
|
@ -2793,6 +2915,10 @@ class AddressQueryService {
|
|||
}
|
||||
if (payablesConfirmedExecution?.asOfDerived &&
|
||||
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)) {
|
||||
filters.extracted_filters = {
|
||||
...filters.extracted_filters,
|
||||
as_of_date: payablesConfirmedExecution.asOfDerived
|
||||
};
|
||||
if (!filters.warnings.includes("as_of_date_derived_for_confirmed_payables")) {
|
||||
filters.warnings.push("as_of_date_derived_for_confirmed_payables");
|
||||
}
|
||||
|
|
@ -2802,6 +2928,10 @@ class AddressQueryService {
|
|||
}
|
||||
if (receivablesConfirmedExecution?.asOfDerived &&
|
||||
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)) {
|
||||
filters.extracted_filters = {
|
||||
...filters.extracted_filters,
|
||||
as_of_date: receivablesConfirmedExecution.asOfDerived
|
||||
};
|
||||
if (!filters.warnings.includes("as_of_date_derived_for_confirmed_receivables")) {
|
||||
filters.warnings.push("as_of_date_derived_for_confirmed_receivables");
|
||||
}
|
||||
|
|
@ -2811,6 +2941,10 @@ class AddressQueryService {
|
|||
}
|
||||
if (vatPayableConfirmedExecution?.asOfDerived &&
|
||||
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)) {
|
||||
filters.extracted_filters = {
|
||||
...filters.extracted_filters,
|
||||
as_of_date: vatPayableConfirmedExecution.asOfDerived
|
||||
};
|
||||
if (!filters.warnings.includes("as_of_date_derived_for_confirmed_vat_payable")) {
|
||||
filters.warnings.push("as_of_date_derived_for_confirmed_vat_payable");
|
||||
}
|
||||
|
|
@ -2820,6 +2954,10 @@ class AddressQueryService {
|
|||
}
|
||||
if (inventoryConfirmedExecution?.asOfDerived &&
|
||||
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)) {
|
||||
filters.extracted_filters = {
|
||||
...filters.extracted_filters,
|
||||
as_of_date: inventoryConfirmedExecution.asOfDerived
|
||||
};
|
||||
if (!filters.warnings.includes("as_of_date_derived_for_inventory_on_hand")) {
|
||||
filters.warnings.push("as_of_date_derived_for_inventory_on_hand");
|
||||
}
|
||||
|
|
@ -2951,8 +3089,8 @@ class AddressQueryService {
|
|||
});
|
||||
const futureGuardReferenceDate = resolveFutureGuardReferenceDate(analysisDate, executionFilters);
|
||||
const debtLifecycleReceivablesScenario = intent.intent === "list_receivables_counterparties" &&
|
||||
Array.isArray(intent.reasons) &&
|
||||
intent.reasons.includes("receivables_debt_lifecycle_signal_detected");
|
||||
((Array.isArray(intent.reasons) && intent.reasons.includes("receivables_debt_lifecycle_signal_detected")) ||
|
||||
/(?:долгожител|задолженн(?:ост|остям).*(?:давн|долго)|срок[а-я\s]+жизн[а-я\s]+задолженн)/iu.test(String(userMessage ?? "")));
|
||||
const debtLifecyclePayablesScenario = intent.intent === "list_payables_counterparties" &&
|
||||
Array.isArray(intent.reasons) &&
|
||||
(intent.reasons.includes("payables_debt_lifecycle_signal_detected") ||
|
||||
|
|
@ -3090,10 +3228,6 @@ class AddressQueryService {
|
|||
if (shouldAttemptCounterpartyCatalogResolution(intent.intent, filters.extracted_filters)) {
|
||||
const catalogResolution = await resolveCounterpartyViaCatalog(rawCounterpartyAnchor);
|
||||
if (catalogResolution.resolvedValue) {
|
||||
filters.extracted_filters = {
|
||||
...filters.extracted_filters,
|
||||
counterparty: catalogResolution.resolvedValue
|
||||
};
|
||||
executionFilters = {
|
||||
...executionFilters,
|
||||
counterparty: catalogResolution.resolvedValue
|
||||
|
|
@ -3327,7 +3461,8 @@ class AddressQueryService {
|
|||
});
|
||||
let anchorFilter = applyAddressFilters(normalizedRows, filtersForMatching);
|
||||
let filterByAnchors = anchorFilter.rows;
|
||||
let filteredRowsBeforeFutureGuard = applyIntentSpecificFilter(intent.intent, filterByAnchors);
|
||||
let explicitPeriodWindowFilter = applyExplicitPeriodWindowFilter(filterByAnchors, executionFilters);
|
||||
let filteredRowsBeforeFutureGuard = applyIntentSpecificFilter(intent.intent, explicitPeriodWindowFilter.rows);
|
||||
let filteredRowsFutureGuard = applyFutureDatedRowsGuard(filteredRowsBeforeFutureGuard, intent.intent, futureGuardReferenceDate);
|
||||
let filteredRows = filteredRowsFutureGuard.rows;
|
||||
let organizationWarehouseRecoveryApplied = false;
|
||||
|
|
@ -3369,7 +3504,8 @@ class AddressQueryService {
|
|||
}
|
||||
anchorFilter = applyAddressFilters(normalizedRows, filtersForMatching);
|
||||
filterByAnchors = anchorFilter.rows;
|
||||
filteredRowsBeforeFutureGuard = applyIntentSpecificFilter(intent.intent, filterByAnchors);
|
||||
explicitPeriodWindowFilter = applyExplicitPeriodWindowFilter(filterByAnchors, executionFilters);
|
||||
filteredRowsBeforeFutureGuard = applyIntentSpecificFilter(intent.intent, explicitPeriodWindowFilter.rows);
|
||||
filteredRowsFutureGuard = applyFutureDatedRowsGuard(filteredRowsBeforeFutureGuard, intent.intent, futureGuardReferenceDate);
|
||||
filteredRows = filteredRowsFutureGuard.rows;
|
||||
organizationWarehouseRecoveryApplied = filteredRows.length > 0;
|
||||
|
|
@ -3382,6 +3518,19 @@ class AddressQueryService {
|
|||
baseReasons.push("future_rows_excluded_from_response");
|
||||
}
|
||||
}
|
||||
if (explicitPeriodWindowFilter.applied) {
|
||||
if (!baseReasons.includes("explicit_period_window_post_filter_applied")) {
|
||||
baseReasons.push("explicit_period_window_post_filter_applied");
|
||||
}
|
||||
if (explicitPeriodWindowFilter.droppedCount > 0) {
|
||||
if (!filters.warnings.includes("rows_outside_explicit_period_window_excluded")) {
|
||||
filters.warnings.push("rows_outside_explicit_period_window_excluded");
|
||||
}
|
||||
if (!baseReasons.includes("rows_outside_explicit_period_window_excluded")) {
|
||||
baseReasons.push("rows_outside_explicit_period_window_excluded");
|
||||
}
|
||||
}
|
||||
}
|
||||
const rowDiagnostics = deriveRowStageDiagnostics(mcp.raw_rows, normalizedRows.length, normalizedRows.length);
|
||||
const stageStatus = deriveMcpStageStatus({
|
||||
rawRowsReceived: mcp.raw_rows.length,
|
||||
|
|
@ -3726,9 +3875,9 @@ class AddressQueryService {
|
|||
...executionFilters,
|
||||
limit: ADDRESS_ANCHOR_RECOVERY_LIMIT
|
||||
};
|
||||
const expandedSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)(intent.intent, expandedLimitFilters);
|
||||
const expandedSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)(recipeIntent, expandedLimitFilters);
|
||||
if (expandedSelection.selected_recipe && expandedSelection.missing_required_filters.length === 0) {
|
||||
const expandedPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(expandedSelection.selected_recipe, expandedLimitFilters);
|
||||
const expandedPlan = enforceStrictAccountScopeForIntent((0, addressRecipeCatalog_1.buildAddressRecipePlan)(expandedSelection.selected_recipe, expandedLimitFilters), intent.intent);
|
||||
if (expandedPlan.limit > currentLimit) {
|
||||
const expandedMcp = await (0, addressMcpClient_1.executeAddressMcpQuery)({
|
||||
query: expandedPlan.query,
|
||||
|
|
@ -3758,7 +3907,8 @@ class AddressQueryService {
|
|||
});
|
||||
const expandedAnchorFilter = applyAddressFilters(expandedNormalizedRows, expandedFiltersForMatching);
|
||||
const expandedRowsByAnchor = expandedAnchorFilter.rows;
|
||||
const expandedFilteredRowsBeforeFutureGuard = applyIntentSpecificFilter(intent.intent, expandedRowsByAnchor);
|
||||
const expandedExplicitPeriodWindowFilter = applyExplicitPeriodWindowFilter(expandedRowsByAnchor, expandedLimitFilters);
|
||||
const expandedFilteredRowsBeforeFutureGuard = applyIntentSpecificFilter(intent.intent, expandedExplicitPeriodWindowFilter.rows);
|
||||
const expandedFutureGuard = applyFutureDatedRowsGuard(expandedFilteredRowsBeforeFutureGuard, intent.intent, resolveFutureGuardReferenceDate(analysisDate, expandedLimitFilters));
|
||||
const expandedFilteredRows = expandedFutureGuard.rows;
|
||||
if (expandedFutureGuard.droppedCount > 0) {
|
||||
|
|
@ -3827,9 +3977,9 @@ class AddressQueryService {
|
|||
? Math.max(1, Math.trunc(autoBroadenedFilters.limit))
|
||||
: 0);
|
||||
}
|
||||
const broadenedSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)(intent.intent, autoBroadenedFilters);
|
||||
const broadenedSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)(recipeIntent, autoBroadenedFilters);
|
||||
if (broadenedSelection.selected_recipe && broadenedSelection.missing_required_filters.length === 0) {
|
||||
const broadenedPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(broadenedSelection.selected_recipe, autoBroadenedFilters);
|
||||
const broadenedPlan = enforceStrictAccountScopeForIntent((0, addressRecipeCatalog_1.buildAddressRecipePlan)(broadenedSelection.selected_recipe, autoBroadenedFilters), intent.intent);
|
||||
const broadenedMcp = await (0, addressMcpClient_1.executeAddressMcpQuery)({
|
||||
query: broadenedPlan.query,
|
||||
limit: broadenedPlan.limit
|
||||
|
|
@ -3950,9 +4100,9 @@ class AddressQueryService {
|
|||
sort: invertSort(filters.extracted_filters.sort),
|
||||
limit: Math.max(currentLimit, ADDRESS_ANCHOR_RECOVERY_LIMIT)
|
||||
};
|
||||
const historicalSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)(intent.intent, historicalFilters);
|
||||
const historicalSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)(recipeIntent, historicalFilters);
|
||||
if (historicalSelection.selected_recipe && historicalSelection.missing_required_filters.length === 0) {
|
||||
const historicalPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(historicalSelection.selected_recipe, historicalFilters);
|
||||
const historicalPlan = enforceStrictAccountScopeForIntent((0, addressRecipeCatalog_1.buildAddressRecipePlan)(historicalSelection.selected_recipe, historicalFilters), intent.intent);
|
||||
const historicalMcp = await (0, addressMcpClient_1.executeAddressMcpQuery)({
|
||||
query: historicalPlan.query,
|
||||
limit: historicalPlan.limit
|
||||
|
|
@ -3981,7 +4131,8 @@ class AddressQueryService {
|
|||
});
|
||||
const historicalAnchorFilter = applyAddressFilters(historicalNormalizedRows, historicalFiltersForMatching);
|
||||
const historicalRowsByAnchor = historicalAnchorFilter.rows;
|
||||
const historicalFilteredRowsBeforeFutureGuard = applyIntentSpecificFilter(intent.intent, historicalRowsByAnchor);
|
||||
const historicalExplicitPeriodWindowFilter = applyExplicitPeriodWindowFilter(historicalRowsByAnchor, historicalFilters);
|
||||
const historicalFilteredRowsBeforeFutureGuard = applyIntentSpecificFilter(intent.intent, historicalExplicitPeriodWindowFilter.rows);
|
||||
const historicalFutureGuard = applyFutureDatedRowsGuard(historicalFilteredRowsBeforeFutureGuard, intent.intent, resolveFutureGuardReferenceDate(analysisDate, historicalFilters));
|
||||
const historicalFilteredRows = historicalFutureGuard.rows;
|
||||
if (historicalFutureGuard.droppedCount > 0) {
|
||||
|
|
|
|||
|
|
@ -586,7 +586,7 @@ const CONTRACTS_BY_COUNTERPARTY_QUERY_TEMPLATE = `
|
|||
`;
|
||||
const VAT_PAYABLE_FORECAST_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ
|
||||
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
|
||||
__PERIOD_EXPR__ КАК Период,
|
||||
"VAT_68_CREDIT" КАК Регистратор,
|
||||
"68" КАК СчетДт,
|
||||
"" КАК СчетКт,
|
||||
|
|
@ -600,7 +600,7 @@ const VAT_PAYABLE_FORECAST_QUERY_TEMPLATE = `
|
|||
__WHERE_CLAUSE__
|
||||
ОБЪЕДИНИТЬ ВСЕ
|
||||
ВЫБРАТЬ
|
||||
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
|
||||
__PERIOD_EXPR__ КАК Период,
|
||||
"VAT_68_DEBIT" КАК Регистратор,
|
||||
"68" КАК СчетДт,
|
||||
"" КАК СчетКт,
|
||||
|
|
@ -614,7 +614,7 @@ __WHERE_CLAUSE__
|
|||
__WHERE_CLAUSE__
|
||||
ОБЪЕДИНИТЬ ВСЕ
|
||||
ВЫБРАТЬ
|
||||
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
|
||||
__PERIOD_EXPR__ КАК Период,
|
||||
"VAT_19_DEBIT" КАК Регистратор,
|
||||
"19" КАК СчетДт,
|
||||
"" КАК СчетКт,
|
||||
|
|
@ -628,7 +628,7 @@ __WHERE_CLAUSE__
|
|||
__WHERE_CLAUSE__
|
||||
ОБЪЕДИНИТЬ ВСЕ
|
||||
ВЫБРАТЬ
|
||||
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
|
||||
__PERIOD_EXPR__ КАК Период,
|
||||
"VAT_19_CREDIT" КАК Регистратор,
|
||||
"19" КАК СчетДт,
|
||||
"" КАК СчетКт,
|
||||
|
|
@ -645,7 +645,7 @@ __WHERE_CLAUSE__
|
|||
`;
|
||||
const VAT_LIABILITY_CONFIRMED_TAX_PERIOD_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ
|
||||
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
|
||||
__PERIOD_TO_EXPR__ КАК Период,
|
||||
"VAT_BOOK_SALES" КАК Регистратор,
|
||||
"68.02" КАК СчетДт,
|
||||
"" КАК СчетКт,
|
||||
|
|
@ -655,7 +655,7 @@ const VAT_LIABILITY_CONFIRMED_TAX_PERIOD_QUERY_TEMPLATE = `
|
|||
__WHERE_CLAUSE__
|
||||
ОБЪЕДИНИТЬ ВСЕ
|
||||
ВЫБРАТЬ
|
||||
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
|
||||
__PERIOD_TO_EXPR__ КАК Период,
|
||||
"VAT_BOOK_PURCHASES" КАК Регистратор,
|
||||
"19" КАК СчетДт,
|
||||
"" КАК СчетКт,
|
||||
|
|
@ -871,7 +871,7 @@ const BASE_RECIPES = [
|
|||
intent: "list_contracts_by_counterparty",
|
||||
purpose: "List contracts by counterparty from contract catalog",
|
||||
required_filters: ["counterparty"],
|
||||
optional_filters: ["limit", "sort"],
|
||||
optional_filters: ["period_from", "period_to", "as_of_date", "organization", "limit", "sort"],
|
||||
default_limit: 300,
|
||||
account_scope_mode: "preferred",
|
||||
query_template: "contracts_by_counterparty_profile"
|
||||
|
|
@ -1388,14 +1388,38 @@ function buildAddressRecipePlan(recipe, filters) {
|
|||
.replaceAll("__WHERE_OUT_VALUE__", buildContractValueWhereClause(filters, "БанкСписание.Дата", "БанкСписание.ДоговорКонтрагента"))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort))
|
||||
: recipe.query_template === "vat_payable_forecast_profile"
|
||||
? VAT_PAYABLE_FORECAST_QUERY_TEMPLATE
|
||||
.replaceAll("__WHERE_CLAUSE__", buildManagementWhereClause(filters, "Движения.Период"))
|
||||
.replaceAll("__VAT68_KT_MATCH__", buildAccountPrefixPredicate("Движения.СчетКт", config_1.VAT_PAYABLE_68_PREFIXES))
|
||||
.replaceAll("__VAT68_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", config_1.VAT_PAYABLE_68_PREFIXES))
|
||||
.replaceAll("__VAT19_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", config_1.VAT_PAYABLE_19_PREFIXES))
|
||||
.replaceAll("__VAT19_KT_MATCH__", buildAccountPrefixPredicate("Движения.СчетКт", config_1.VAT_PAYABLE_19_PREFIXES))
|
||||
? (() => {
|
||||
const periodExpr = (typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_to, true)
|
||||
: null) ??
|
||||
(typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||
? toDateTimeExpr(filters.as_of_date, true)
|
||||
: null) ??
|
||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_from, true)
|
||||
: null) ??
|
||||
"ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0)";
|
||||
return VAT_PAYABLE_FORECAST_QUERY_TEMPLATE
|
||||
.replaceAll("__WHERE_CLAUSE__", buildManagementWhereClause(filters, "Движения.Период"))
|
||||
.replaceAll("__PERIOD_EXPR__", periodExpr)
|
||||
.replaceAll("__VAT68_KT_MATCH__", buildAccountPrefixPredicate("Движения.СчетКт", config_1.VAT_PAYABLE_68_PREFIXES))
|
||||
.replaceAll("__VAT68_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", config_1.VAT_PAYABLE_68_PREFIXES))
|
||||
.replaceAll("__VAT19_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", config_1.VAT_PAYABLE_19_PREFIXES))
|
||||
.replaceAll("__VAT19_KT_MATCH__", buildAccountPrefixPredicate("Движения.СчетКт", config_1.VAT_PAYABLE_19_PREFIXES));
|
||||
})()
|
||||
: recipe.query_template === "vat_liability_confirmed_tax_period_profile"
|
||||
? VAT_LIABILITY_CONFIRMED_TAX_PERIOD_QUERY_TEMPLATE.replaceAll("__WHERE_CLAUSE__", buildManagementWhereClause(filters, "Движения.Период"))
|
||||
? (() => {
|
||||
const periodToExpr = (typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_to, true)
|
||||
: null) ??
|
||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_from, true)
|
||||
: null) ??
|
||||
"ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0)";
|
||||
return VAT_LIABILITY_CONFIRMED_TAX_PERIOD_QUERY_TEMPLATE
|
||||
.replaceAll("__WHERE_CLAUSE__", buildManagementWhereClause(filters, "Движения.Период"))
|
||||
.replaceAll("__PERIOD_TO_EXPR__", periodToExpr);
|
||||
})()
|
||||
: recipe.query_template === "vat_payable_confirmed_as_of_balance_profile"
|
||||
? (() => {
|
||||
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||
|
|
@ -1435,7 +1459,7 @@ function buildAddressRecipePlan(recipe, filters) {
|
|||
: recipe.query_template === "inventory_purchase_provenance_profile"
|
||||
? buildInventoryPurchaseDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_purchase_documents_profile"
|
||||
? buildInventoryPurchaseDocumentQuery(filters, resolvedLimit)
|
||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||
: recipe.query_template === "inventory_supplier_stock_overlap_profile"
|
||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||
: recipe.query_template === "inventory_sale_trace_profile"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.repairAddressMojibakeText = repairAddressMojibakeText;
|
||||
exports.normalizeRussianComparableText = normalizeRussianComparableText;
|
||||
const iconv_lite_1 = __importDefault(require("iconv-lite"));
|
||||
function compactWhitespace(value) {
|
||||
return value.replace(/\s+/g, " ").trim();
|
||||
}
|
||||
function textMojibakeScore(value) {
|
||||
const source = String(value ?? "");
|
||||
const cyrillic = (source.match(/[\u0400-\u04ff]/g) ?? []).length;
|
||||
const latin = (source.match(/[A-Za-z]/g) ?? []).length;
|
||||
const replacement = (source.match(/[<5B>]/g) ?? []).length;
|
||||
const pairMarkers = (source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length;
|
||||
const doubleEncodedMarkers = (source.match(/(?:Р“[Р-џ]|Р’[Р-џ]|Ã.|Â.)/gu) ?? []).length;
|
||||
return cyrillic + latin - replacement * 3 - pairMarkers * 2 - doubleEncodedMarkers * 2;
|
||||
}
|
||||
function looksLikeAddressMojibake(value) {
|
||||
const source = String(value ?? "");
|
||||
if (!source.trim()) {
|
||||
return false;
|
||||
}
|
||||
if (/[<5B>]/.test(source)) {
|
||||
return true;
|
||||
}
|
||||
if ((source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length >= 2) {
|
||||
return true;
|
||||
}
|
||||
if ((source.match(/(?:Р“[Р-џ]|Р’[Р-џ]|Ã.|Â.)/gu) ?? []).length >= 2) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function repairAddressMojibakeText(value) {
|
||||
const source = String(value ?? "");
|
||||
if (!looksLikeAddressMojibake(source)) {
|
||||
return source;
|
||||
}
|
||||
let candidate = source;
|
||||
for (let pass = 0; pass < 3; pass += 1) {
|
||||
let improved = false;
|
||||
try {
|
||||
const fromWin1251 = iconv_lite_1.default.encode(candidate, "win1251").toString("utf8");
|
||||
if (textMojibakeScore(fromWin1251) > textMojibakeScore(candidate)) {
|
||||
candidate = fromWin1251;
|
||||
improved = true;
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// Ignore decode failures and keep the current candidate.
|
||||
}
|
||||
try {
|
||||
const fromLatin1 = Buffer.from(candidate, "latin1").toString("utf8");
|
||||
if (textMojibakeScore(fromLatin1) > textMojibakeScore(candidate)) {
|
||||
candidate = fromLatin1;
|
||||
improved = true;
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// Ignore decode failures and keep the current candidate.
|
||||
}
|
||||
if (!improved) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
function normalizeRussianComparableText(value) {
|
||||
return compactWhitespace(repairAddressMojibakeText(String(value ?? "")).toLowerCase()).replace(/ё/g, "е");
|
||||
}
|
||||
|
|
@ -2775,6 +2775,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
|||
});
|
||||
const lines = [
|
||||
`Коротко: на ${formatDateRu(asOfDate)} подтверждено открытых договоров с коммерческим остатком ${openContractNetBalanceDirectionLabel(commercialNetTotal)} ${formatMoneyRub(Math.abs(commercialNetTotal))}.`,
|
||||
"Результат: подтвержденный срез договоров с открытыми взаиморасчетами на дату.",
|
||||
`Брутто по коммерческим компонентам: ${formatMoneyRub(commercialGrossTotal)}.`,
|
||||
`Отдельно вынесены специальные финансовые позиции: ${formatNumberWithDots(specialProfiles.length)} на ${formatMoneyRub(specialTotal)}.`,
|
||||
`Спорные или некачественно нормализованные позиции: ${formatNumberWithDots(dirtyProfiles.length)} на ${formatMoneyRub(dirtyTotal)}.`,
|
||||
|
|
@ -2948,7 +2949,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
|||
}, { supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 });
|
||||
const lines = [
|
||||
`Коротко: подтвержденный долг к оплате на ${formatDateRu(payablesAsOfDate)} — ${formatMoneyRub(totalOutstandingAmount)}.`,
|
||||
"Это подтвержденный срез обязательств к оплате, а не эвристический shortlist."
|
||||
"Это подтвержденный срез обязательств к оплате по точному остатку."
|
||||
];
|
||||
lines.push("");
|
||||
lines.push("Что учтено");
|
||||
|
|
|
|||
|
|
@ -48,9 +48,9 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
|||
const includeTotal = focus === "full_profile" || focus === "total_only";
|
||||
const includeRoles = focus === "full_profile" || focus === "roles_only";
|
||||
const directLead = focus === "suppliers_only"
|
||||
? `Контрагентов только в роли поставщика: ${supplierOnly}.`
|
||||
? `Поставщиков (только supplier-роль): ${supplierOnly}.`
|
||||
: focus === "customers_only"
|
||||
? `Контрагентов только в роли заказчика: ${customerOnly}.`
|
||||
? `Заказчиков (только customer-роль): ${customerOnly}.`
|
||||
: focus === "mixed_only"
|
||||
? `Контрагентов со смешанной ролью: ${mixedActive}.`
|
||||
: includeTotal && totalCounterparties > 0
|
||||
|
|
@ -74,10 +74,10 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
|||
}
|
||||
if (includeRoles) {
|
||||
if (resolvedActive > 0 || activeCounterparties > 0) {
|
||||
lines.push("Распределение ролей по активности:");
|
||||
lines.push(`1. Только заказчики: ${customerOnly}.`);
|
||||
lines.push(`2. Только поставщики: ${supplierOnly}.`);
|
||||
lines.push(`3. И заказчики, и поставщики: ${mixedActive}.`);
|
||||
lines.push("Роли контрагентов по активности:");
|
||||
lines.push(`Заказчики (только customer-роль): ${customerOnly}.`);
|
||||
lines.push(`Поставщики (только supplier-роль): ${supplierOnly}.`);
|
||||
lines.push(`Смешанные (и покупатель, и поставщик): ${mixedActive}.`);
|
||||
lines.push(`4. Всего активных контрагентов: ${activeCounterparties}.`);
|
||||
if (otherCounterparties !== null) {
|
||||
lines.push(`5. Прочие или неактивные в выбранном окне: ${otherCounterparties}.`);
|
||||
|
|
@ -88,10 +88,10 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
|||
}
|
||||
}
|
||||
if (focus === "suppliers_only") {
|
||||
lines.push(`Контрагентов только в роли поставщика: ${supplierOnly}.`);
|
||||
lines.push(`Поставщиков (только supplier-роль): ${supplierOnly}.`);
|
||||
}
|
||||
if (focus === "customers_only") {
|
||||
lines.push(`Контрагентов только в роли заказчика: ${customerOnly}.`);
|
||||
lines.push(`Заказчиков (только customer-роль): ${customerOnly}.`);
|
||||
}
|
||||
if (focus === "mixed_only") {
|
||||
lines.push(`Контрагентов со смешанной ролью: ${mixedActive}.`);
|
||||
|
|
@ -319,6 +319,10 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
|||
]
|
||||
: [
|
||||
`Коротко: активных заказчиков ${scopeLabel} — ${counterparties.length}.`,
|
||||
`Собран профиль активности заказчиков ${scopeLabel}.`,
|
||||
requestedYear
|
||||
? `Активные заказчики в ${requestedYear} году: ${counterparties.length}.`
|
||||
: `Активные заказчики ${scopeLabel}: ${counterparties.length}.`,
|
||||
`Оценка собрана по подтвержденным платежным документам: ${rows.length} строк в выборке.`
|
||||
];
|
||||
if (counterparties.length === 0) {
|
||||
|
|
@ -550,7 +554,7 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
|||
? `Топ-${visible.length} поставщиков по максимальной разовой выплате:`
|
||||
: `Топ-${visible.length} заказчиков по максимальной сумме одной входящей операции:`;
|
||||
lines.unshift(heading);
|
||||
lines.push(...visible.map((item, index) => `${index + 1}. ${item.name} | максимальная разовая сумма: ${deps.formatMoneyRub(item.maxSingle)} | сумма: ${deps.formatMoneyRub(item.total)} | операций: ${item.ops}`));
|
||||
lines.push(...visible.map((item, index) => `${index + 1}. ${item.name} | max single: ${item.maxSingle} | максимальная разовая сумма: ${deps.formatMoneyRub(item.maxSingle)} | сумма: ${deps.formatMoneyRub(item.total)} | операций: ${item.ops}`));
|
||||
return (0, replyContracts_1.buildFactualListReply)(lines);
|
||||
}
|
||||
if (focus === "top_by_avg_check_min_ops") {
|
||||
|
|
@ -571,7 +575,7 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
|||
const visible = rankedDealsTop.slice(0, limit);
|
||||
const heading = isSupplier
|
||||
? `Топ-${visible.length} самых крупных разовых выплат поставщикам:`
|
||||
: `Топ-${visible.length} самых крупных разовых поступлений:`;
|
||||
: `Топ-${visible.length} самых крупных разовых сделок по поступлениям:`;
|
||||
lines.unshift(heading);
|
||||
lines.push(...visible.map((item, index) => `${index + 1}. ${formatOptionalDate(item.period, deps.formatDateRu)} | ${item.counterparty} | ${item.registrator} | ${deps.formatMoneyRub(item.amount)}`));
|
||||
return (0, replyContracts_1.buildFactualListReply)(lines);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ const addressQueryShapeClassifier_1 = require("../addressQueryShapeClassifier");
|
|||
const addressIntentResolver_1 = require("../addressIntentResolver");
|
||||
const addressFilterExtractor_1 = require("../addressFilterExtractor");
|
||||
const inventoryLifecycleCueHelpers_1 = require("../inventoryLifecycleCueHelpers");
|
||||
const addressTextRepair_1 = require("../addressTextRepair");
|
||||
const assistantOrganizationMatcher_1 = require("../assistantOrganizationMatcher");
|
||||
const semanticHintOverlay_1 = require("./semanticHintOverlay");
|
||||
function hasExplicitPeriodWindow(filters) {
|
||||
|
|
@ -28,6 +29,11 @@ function toNonEmptyString(value) {
|
|||
const normalized = String(value).trim();
|
||||
return normalized.length > 0 ? normalized : null;
|
||||
}
|
||||
function textWithRepairedVariant(text) {
|
||||
const source = String(text ?? "");
|
||||
const repaired = (0, addressTextRepair_1.repairAddressMojibakeText)(source);
|
||||
return repaired && repaired !== source ? `${source} ${repaired}` : source;
|
||||
}
|
||||
function hasAllTimeHint(text) {
|
||||
const normalized = String(text ?? "");
|
||||
return /(?:за\s+вс[её]\s+время|за\s+весь\s+период|за\s+весь\s+срок|за\s+всю\s+истори(?:ю|и)|за\s+любой\s+период|за\s+любой\s+срок|for\s+all\s+time|all\s+time|for\s+entire\s+period|entire\s+period|for\s+any\s+period|any\s+period|for\s+full\s+history|full\s+history)/iu.test(normalized);
|
||||
|
|
@ -45,13 +51,13 @@ function hasExplicitPeriodLiteral(text) {
|
|||
return /(?:^|[^\d*×xх])((?:19|20)\d{2}(?:[./-](?:0?[1-9]|1[0-2]))?)(?=$|[^\d*×xх])/iu.test(String(text ?? ""));
|
||||
}
|
||||
function hasExplicitCurrentDateHint(text) {
|
||||
return /(?:на\s+текущ(?:ую|ая|ий|ее|ей|ем|его)\s+дат(?:у|а|е|ой|ою)|на\s+сегодняшн(?:юю|ий|ей|ем|его)\s+дат(?:у|а|е|ой|ою)|на\s+сегодня|сегодня|на\s+текущ(?:ий|ую)\s+момент|today|as\s+of\s+today|current\s+date|as\s+of\s+current\s+date)/iu.test(String(text ?? ""));
|
||||
return /(?:на\s+текущ(?:ую|ая|ий|ее|ей|ем|его)\s+дат(?:у|а|е|ой|ою)|на\s+сегодняшн(?:юю|ий|ей|ем|его)\s+дат(?:у|а|е|ой|ою)|на\s+сегодня|сегодня|на\s+текущ(?:ий|ую)\s+момент|today|as\s+of\s+today|current\s+date|as\s+of\s+current\s+date)/iu.test(textWithRepairedVariant(String(text ?? "")));
|
||||
}
|
||||
function hasOpenItemsHint(text) {
|
||||
return /(?:open\s+items|unclosed\s+items|хвост|висят|незакрыт|не\s+закрыт|открыт|долг|задолж|позиц)/iu.test(String(text ?? ""));
|
||||
}
|
||||
function hasVatCue(text) {
|
||||
return /(?:^|[\s,.;:!?()\-])(?:ндс|vat)(?=$|[\s,.;:!?()\-])/iu.test(String(text ?? ""));
|
||||
return /(?:^|[\s,.;:!?()\-])(?:ндс[а-яё]*|vat)(?=$|[\s,.;:!?()\-])/iu.test(textWithRepairedVariant(String(text ?? "")));
|
||||
}
|
||||
function hasVatForecastCue(text) {
|
||||
return /(?:прогноз|forecast|прикин|оцен|план)/iu.test(String(text ?? ""));
|
||||
|
|
@ -1059,6 +1065,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
|||
previousHasPeriod &&
|
||||
hasFollowupSignal &&
|
||||
!hasExplicitPeriodInMessage &&
|
||||
!hasExplicitCurrentDateInMessage &&
|
||||
!inventoryLifecycleHistoryIntent &&
|
||||
!vatRelativeMonthFollowup &&
|
||||
!shouldSuppressGenericPeriodCarryover) {
|
||||
|
|
@ -1143,6 +1150,8 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
|||
return detectedIntent;
|
||||
}
|
||||
const previousFilters = followupContext.previous_filters ?? {};
|
||||
const previousPeriodFrom = toNonEmptyString(previousFilters.period_from);
|
||||
const previousPeriodTo = toNonEmptyString(previousFilters.period_to);
|
||||
const previousContract = toNonEmptyString(previousFilters.contract);
|
||||
const previousCounterparty = toNonEmptyString(previousFilters.counterparty);
|
||||
const previousContractFromAnchor = followupContext.previous_anchor_type === "contract" ? toNonEmptyString(followupContext.previous_anchor_value) : null;
|
||||
|
|
@ -1151,19 +1160,42 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
|||
const hasPreviousCounterparty = Boolean(previousCounterparty ?? previousCounterpartyFromAnchor);
|
||||
const hasAnyPartyAnchor = hasPreviousContract || hasPreviousCounterparty;
|
||||
const isVatFollowup = hasVatCue(normalizedMessage);
|
||||
const samePeriodVatFollowup = isVatFollowup &&
|
||||
hasSamePeriodHint(normalizedMessage) &&
|
||||
Boolean(previousPeriodFrom || previousPeriodTo);
|
||||
const previousIsInventoryFamily = isInventoryIntent(sourceIntent ?? undefined);
|
||||
const inventorySelectedObjectFollowup = hasSelectedObjectInventorySignal(normalizedMessage) || (previousIsInventoryFamily && hasFollowupSignal);
|
||||
const rootIsInventoryFamily = isInventoryIntent(followupContext.root_intent ?? undefined);
|
||||
const inventoryLineageActive = previousIsInventoryFamily ||
|
||||
rootIsInventoryFamily ||
|
||||
followupContext.previous_anchor_type === "item" ||
|
||||
followupContext.root_anchor_type === "item" ||
|
||||
followupContext.current_frame_kind === "inventory_root" ||
|
||||
followupContext.current_frame_kind === "inventory_drilldown";
|
||||
const inventorySelectedObjectFollowup = inventoryLineageActive &&
|
||||
(hasSelectedObjectInventorySignal(normalizedMessage) || (previousIsInventoryFamily && hasFollowupSignal));
|
||||
const hasExplicitInventoryItemReference = /(?:товар|номенклатур|позици|склад|остат|sku|item|product|товар|номенклатур|позици|склад|остат)/iu.test(normalizedMessage) || hasSelectedObjectInlineSnapshotMetadata(normalizedMessage);
|
||||
const inventoryPurchaseDateVatBridge = inventorySelectedObjectFollowup && hasInventoryPurchaseDateVatBridgeCue(normalizedMessage);
|
||||
if (inventoryPurchaseDateVatBridge &&
|
||||
(detectedIntent.intent === "unknown" ||
|
||||
detectedIntent.intent === sourceIntent ||
|
||||
detectedIntent.intent === "vat_payable_confirmed_as_of_date")) {
|
||||
detectedIntent.intent === "vat_payable_confirmed_as_of_date" ||
|
||||
detectedIntent.intent === "vat_payable_forecast")) {
|
||||
return {
|
||||
intent: "vat_liability_confirmed_for_tax_period",
|
||||
confidence: "low",
|
||||
reasons: [...detectedIntent.reasons, "intent_adjusted_to_inventory_purchase_date_vat_bridge"]
|
||||
};
|
||||
}
|
||||
if (samePeriodVatFollowup &&
|
||||
(detectedIntent.intent === "vat_payable_confirmed_as_of_date" ||
|
||||
detectedIntent.intent === "vat_payable_forecast" ||
|
||||
detectedIntent.intent === "unknown")) {
|
||||
return {
|
||||
intent: "vat_liability_confirmed_for_tax_period",
|
||||
confidence: "low",
|
||||
reasons: [...detectedIntent.reasons, "intent_adjusted_to_vat_same_period_followup"]
|
||||
};
|
||||
}
|
||||
if (detectedIntent.intent === "unknown" && isVatFollowup) {
|
||||
const vatIntent = hasVatTaxPaymentCue(normalizedMessage)
|
||||
? "vat_liability_confirmed_for_tax_period"
|
||||
|
|
@ -1176,6 +1208,16 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
|||
reasons: [...detectedIntent.reasons, "intent_adjusted_to_vat_followup_context"]
|
||||
};
|
||||
}
|
||||
if (!inventoryLineageActive &&
|
||||
hasAnyPartyAnchor &&
|
||||
!hasExplicitInventoryItemReference &&
|
||||
detectedIntent.intent === "inventory_purchase_documents_for_item") {
|
||||
return {
|
||||
intent: hasPreviousContract && !hasPreviousCounterparty ? "list_documents_by_contract" : "list_documents_by_counterparty",
|
||||
confidence: "low",
|
||||
reasons: [...detectedIntent.reasons, "intent_adjusted_from_non_inventory_followup_context"]
|
||||
};
|
||||
}
|
||||
const allowOpenItemsFollowupFallback = detectedIntent.intent === "unknown" && !isVatFollowup;
|
||||
if (allowOpenItemsFollowupFallback &&
|
||||
!inventorySelectedObjectFollowup &&
|
||||
|
|
@ -1399,6 +1441,13 @@ function runAddressDecomposeStage(userMessage, followupContext, llmSemanticHints
|
|||
warnings: [...new Set([...extractedFilters.warnings, ...followupMerged.reasons])],
|
||||
semantic_frame: extractedFilters.semantic_frame
|
||||
};
|
||||
if ((intent.intent === "list_open_contracts" || intent.intent === "open_contracts_confirmed_as_of_date") &&
|
||||
typeof filters.extracted_filters.as_of_date === "string" &&
|
||||
typeof filters.extracted_filters.period_to === "string" &&
|
||||
filters.extracted_filters.as_of_date === filters.extracted_filters.period_to &&
|
||||
!filters.warnings.includes("as_of_date_derived_from_period_for_open_contracts")) {
|
||||
filters.warnings.push("as_of_date_derived_from_period_for_open_contracts");
|
||||
}
|
||||
const followupContextApplied = Boolean(effectiveFollowupContext) &&
|
||||
(mode.reasons.includes("address_mode_from_followup_context") ||
|
||||
intent.reasons.includes("intent_from_followup_context") ||
|
||||
|
|
@ -1409,6 +1458,7 @@ function runAddressDecomposeStage(userMessage, followupContext, llmSemanticHints
|
|||
...shape.reasons,
|
||||
...intent.reasons,
|
||||
...followupMerged.reasons,
|
||||
...filters.warnings.filter((reason) => reason === "as_of_date_derived_from_period_for_open_contracts"),
|
||||
...(followupContextApplied ? ["address_followup_context_applied"] : [])
|
||||
];
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -77,12 +77,17 @@ function shouldPreferRawFollowupMessage(userMessage, addressInputMessage, carryo
|
|||
const previousIntent = toNonEmptyString(followupContext?.previous_intent);
|
||||
const rootIntent = toNonEmptyString(followupContext?.root_intent);
|
||||
const previousAnchorType = toNonEmptyString(followupContext?.previous_anchor_type);
|
||||
const hasReferentialDocumentExclusionFollowupCue = /(?:\u043a\u0440\u043e\u043c\u0435|\u043f\u043e\u043c\u0438\u043c\u043e)\s+(?:\u044d\u0442\u043e\u0433\u043e|\u044d\u0442\u043e\u0439|\u044d\u0442\u043e\u0442|\u044d\u0442\u0443|\u044d\u0442\u0438\u0445)(?:\s+(?:\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430|\u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0430|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430))?/iu.test(rawMessage);
|
||||
const hasInventoryItemCarryover = previousAnchorType === "item" && isInventorySelectedObjectOrRootIntent(previousIntent);
|
||||
const hasInventoryFrameCarryover = isInventorySelectedObjectOrRootIntent(previousIntent) ||
|
||||
isInventorySelectedObjectOrRootIntent(rootIntent);
|
||||
const hasDocumentCarryover = previousIntent === "list_documents_by_counterparty" || previousIntent === "list_documents_by_contract";
|
||||
if (mode === "unsupported" && intent === "unknown") {
|
||||
return true;
|
||||
}
|
||||
if (hasDocumentCarryover && hasReferentialDocumentExclusionFollowupCue) {
|
||||
return true;
|
||||
}
|
||||
if (hasSameDateFollowupSignal(rawMessage) && hasExplicitCurrentDateSignal(canonicalMessage)) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,15 @@ exports.readAssistantMcpDiscoveryEntityAmbiguityCandidates = readAssistantMcpDis
|
|||
exports.readAssistantMcpDiscoveryEntityCandidates = readAssistantMcpDiscoveryEntityCandidates;
|
||||
exports.readAssistantMcpDiscoveryPilotScope = readAssistantMcpDiscoveryPilotScope;
|
||||
exports.readAssistantMcpDiscoveryRankingNeed = readAssistantMcpDiscoveryRankingNeed;
|
||||
exports.readAssistantMcpDiscoveryLoopStatus = readAssistantMcpDiscoveryLoopStatus;
|
||||
exports.readAssistantMcpDiscoveryLoopSelectedChainId = readAssistantMcpDiscoveryLoopSelectedChainId;
|
||||
exports.readAssistantMcpDiscoveryLoopPendingAxes = readAssistantMcpDiscoveryLoopPendingAxes;
|
||||
exports.readAssistantMcpDiscoveryLoopProvidedAxes = readAssistantMcpDiscoveryLoopProvidedAxes;
|
||||
exports.readAssistantMcpDiscoveryLoopAskedDomainFamily = readAssistantMcpDiscoveryLoopAskedDomainFamily;
|
||||
exports.readAssistantMcpDiscoveryLoopAskedActionFamily = readAssistantMcpDiscoveryLoopAskedActionFamily;
|
||||
exports.readAssistantMcpDiscoveryLoopUnsupportedFamily = readAssistantMcpDiscoveryLoopUnsupportedFamily;
|
||||
exports.readAssistantMcpDiscoveryLoopMetadataScopeHint = readAssistantMcpDiscoveryLoopMetadataScopeHint;
|
||||
exports.readAssistantMcpDiscoveryLoopSubjectResolutionOptional = readAssistantMcpDiscoveryLoopSubjectResolutionOptional;
|
||||
exports.readAssistantMcpDiscoveryMetadataRouteFamily = readAssistantMcpDiscoveryMetadataRouteFamily;
|
||||
exports.readAssistantMcpDiscoveryMetadataRouteFamilySelectionBasis = readAssistantMcpDiscoveryMetadataRouteFamilySelectionBasis;
|
||||
exports.readAssistantMcpDiscoveryMetadataSelectedEntitySet = readAssistantMcpDiscoveryMetadataSelectedEntitySet;
|
||||
|
|
@ -42,6 +51,7 @@ exports.resolveAssistantContinuitySnapshot = resolveAssistantContinuitySnapshot;
|
|||
exports.resolveAssistantOrganizationAuthority = resolveAssistantOrganizationAuthority;
|
||||
exports.resolveOrganizationClarificationContinuation = resolveOrganizationClarificationContinuation;
|
||||
const assistantOrganizationMatcher_1 = require("./assistantOrganizationMatcher");
|
||||
const addressTextRepair_1 = require("./addressTextRepair");
|
||||
function fallbackToNonEmptyString(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
|
|
@ -98,6 +108,9 @@ function readAssistantMcpDiscoveryActionFamily(debug, toNonEmptyString = fallbac
|
|||
function readAssistantMcpDiscoveryBridge(debug) {
|
||||
return toRecordObject(readAssistantMcpDiscoveryEntry(debug)?.bridge);
|
||||
}
|
||||
function readAssistantMcpDiscoveryLoopState(debug) {
|
||||
return toRecordObject(readAssistantMcpDiscoveryBridge(debug)?.loop_state);
|
||||
}
|
||||
function readAssistantMcpDiscoveryDerivedMetadataSurface(debug) {
|
||||
const bridge = readAssistantMcpDiscoveryBridge(debug);
|
||||
const pilot = toRecordObject(bridge?.pilot);
|
||||
|
|
@ -152,7 +165,47 @@ function readAssistantMcpDiscoveryPilotScope(debug, toNonEmptyString = fallbackT
|
|||
return toNonEmptyString(pilot?.pilot_scope);
|
||||
}
|
||||
function readAssistantMcpDiscoveryRankingNeed(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
return toNonEmptyString(readAssistantMcpDiscoveryDataNeedGraph(debug)?.ranking_need);
|
||||
return (toNonEmptyString(readAssistantMcpDiscoveryLoopState(debug)?.ranking_need) ??
|
||||
toNonEmptyString(readAssistantMcpDiscoveryDataNeedGraph(debug)?.ranking_need));
|
||||
}
|
||||
function readAssistantMcpDiscoveryLoopStatus(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
return toNonEmptyString(readAssistantMcpDiscoveryLoopState(debug)?.loop_status);
|
||||
}
|
||||
function readAssistantMcpDiscoveryLoopSelectedChainId(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
return toNonEmptyString(readAssistantMcpDiscoveryLoopState(debug)?.selected_chain_id);
|
||||
}
|
||||
function readAssistantMcpDiscoveryLoopPendingAxes(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
const values = readAssistantMcpDiscoveryLoopState(debug)?.pending_axes;
|
||||
if (!Array.isArray(values)) {
|
||||
return [];
|
||||
}
|
||||
return values.map((item) => toNonEmptyString(item)).filter((item) => Boolean(item));
|
||||
}
|
||||
function readAssistantMcpDiscoveryLoopProvidedAxes(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
const values = readAssistantMcpDiscoveryLoopState(debug)?.provided_axes;
|
||||
if (!Array.isArray(values)) {
|
||||
return [];
|
||||
}
|
||||
return values.map((item) => toNonEmptyString(item)).filter((item) => Boolean(item));
|
||||
}
|
||||
function readAssistantMcpDiscoveryLoopAskedDomainFamily(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
return toNonEmptyString(readAssistantMcpDiscoveryLoopState(debug)?.asked_domain_family);
|
||||
}
|
||||
function readAssistantMcpDiscoveryLoopAskedActionFamily(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
return toNonEmptyString(readAssistantMcpDiscoveryLoopState(debug)?.asked_action_family);
|
||||
}
|
||||
function readAssistantMcpDiscoveryLoopUnsupportedFamily(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
return toNonEmptyString(readAssistantMcpDiscoveryLoopState(debug)?.unsupported_but_understood_family);
|
||||
}
|
||||
function readAssistantMcpDiscoveryLoopMetadataScopeHint(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
return (toNonEmptyString(readAssistantMcpDiscoveryLoopState(debug)?.metadata_scope_hint) ??
|
||||
toNonEmptyString(readAssistantMcpDiscoveryTurnMeaning(debug)?.metadata_scope_hint) ??
|
||||
toNonEmptyString(readAssistantMcpDiscoveryDataNeedGraph(debug)?.metadata_scope_hint));
|
||||
}
|
||||
function readAssistantMcpDiscoveryLoopSubjectResolutionOptional(debug) {
|
||||
return (readAssistantMcpDiscoveryLoopState(debug)?.subject_resolution_optional === true ||
|
||||
readAssistantMcpDiscoveryTurnMeaning(debug)?.subject_resolution_optional === true ||
|
||||
readAssistantMcpDiscoveryDataNeedGraph(debug)?.subject_resolution_optional === true);
|
||||
}
|
||||
function readAssistantMcpDiscoveryMetadataRouteFamily(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
return toNonEmptyString(readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.downstream_route_family);
|
||||
|
|
@ -313,22 +366,74 @@ function readAddressDebugItem(debug, toNonEmptyString = fallbackToNonEmptyString
|
|||
? toNonEmptyString(debug?.anchor_value_resolved) ?? toNonEmptyString(debug?.anchor_value_raw)
|
||||
: null));
|
||||
}
|
||||
function readAddressDebugCounterparty(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
const extractedFilters = readAddressDebugFilters(debug);
|
||||
if (toNonEmptyString(extractedFilters?.counterparty)) {
|
||||
return toNonEmptyString(extractedFilters?.counterparty);
|
||||
function isReferentialCounterpartyPlaceholder(value) {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
if (String(debug?.anchor_type ?? "") === "counterparty") {
|
||||
return toNonEmptyString(debug?.anchor_value_resolved) ?? toNonEmptyString(debug?.anchor_value_raw);
|
||||
return new Set([
|
||||
"он",
|
||||
"она",
|
||||
"оно",
|
||||
"они",
|
||||
"ему",
|
||||
"ней",
|
||||
"нему",
|
||||
"ним",
|
||||
"ними",
|
||||
"его",
|
||||
"ее",
|
||||
"их",
|
||||
"этому",
|
||||
"этой",
|
||||
"этом",
|
||||
"этим",
|
||||
"эта",
|
||||
"этот",
|
||||
"эти"
|
||||
]).has((0, addressTextRepair_1.normalizeRussianComparableText)(value));
|
||||
}
|
||||
function normalizeCounterpartyCandidate(value, toNonEmptyString) {
|
||||
const text = toNonEmptyString(value);
|
||||
if (!text || isReferentialCounterpartyPlaceholder(text)) {
|
||||
return null;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
function sameCounterpartyCandidate(left, right) {
|
||||
return Boolean(left &&
|
||||
right &&
|
||||
(0, addressTextRepair_1.normalizeRussianComparableText)(left) === (0, addressTextRepair_1.normalizeRussianComparableText)(right));
|
||||
}
|
||||
function readGroundedDiscoveryCounterparty(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
const discoveryPilotScope = readAssistantMcpDiscoveryPilotScope(debug, toNonEmptyString);
|
||||
const suppressDiscoveryEntityCarryover = discoveryPilotScope === "metadata_inspection_v1" ||
|
||||
readAssistantMcpDiscoveryLoopSubjectResolutionOptional(debug);
|
||||
if (suppressDiscoveryEntityCarryover) {
|
||||
return null;
|
||||
}
|
||||
const discoveryEntities = collectAssistantMcpDiscoveryEntityCandidates(debug, toNonEmptyString);
|
||||
for (const entity of discoveryEntities) {
|
||||
const text = toNonEmptyString(entity);
|
||||
if (text) {
|
||||
return text;
|
||||
const normalized = normalizeCounterpartyCandidate(entity, toNonEmptyString);
|
||||
if (normalized) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return normalizeCounterpartyCandidate(readAssistantMcpDiscoveryLoopMetadataScopeHint(debug, toNonEmptyString), toNonEmptyString);
|
||||
}
|
||||
function readAddressDebugCounterparty(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
const extractedFilters = readAddressDebugFilters(debug);
|
||||
const extractedCounterparty = normalizeCounterpartyCandidate(extractedFilters?.counterparty, toNonEmptyString);
|
||||
const anchorCounterparty = String(debug?.anchor_type ?? "") === "counterparty"
|
||||
? normalizeCounterpartyCandidate(toNonEmptyString(debug?.anchor_value_resolved) ?? toNonEmptyString(debug?.anchor_value_raw), toNonEmptyString)
|
||||
: null;
|
||||
const groundedDiscoveryCounterparty = readGroundedDiscoveryCounterparty(debug, toNonEmptyString);
|
||||
if (hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString) && groundedDiscoveryCounterparty) {
|
||||
if (!extractedCounterparty || !sameCounterpartyCandidate(extractedCounterparty, groundedDiscoveryCounterparty)) {
|
||||
return groundedDiscoveryCounterparty;
|
||||
}
|
||||
return extractedCounterparty;
|
||||
}
|
||||
return extractedCounterparty ?? anchorCounterparty ?? groundedDiscoveryCounterparty;
|
||||
}
|
||||
function readAddressDebugIntent(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
const detectedIntent = toNonEmptyString(debug?.detected_intent);
|
||||
|
|
@ -373,8 +478,16 @@ function readAddressDebugTemporalScope(debug, toNonEmptyString = fallbackToNonEm
|
|||
}
|
||||
function resolveAddressDebugAnchorContext(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
const explicitAnchorType = toNonEmptyString(debug?.anchor_type);
|
||||
const explicitAnchorValue = toNonEmptyString(debug?.anchor_value_resolved) ?? toNonEmptyString(debug?.anchor_value_raw);
|
||||
if (explicitAnchorType || explicitAnchorValue) {
|
||||
const explicitAnchorValueRaw = toNonEmptyString(debug?.anchor_value_resolved) ?? toNonEmptyString(debug?.anchor_value_raw);
|
||||
const explicitAnchorValue = explicitAnchorType === "counterparty"
|
||||
? normalizeCounterpartyCandidate(explicitAnchorValueRaw, toNonEmptyString)
|
||||
: explicitAnchorValueRaw;
|
||||
const groundedDiscoveryCounterparty = readGroundedDiscoveryCounterparty(debug, toNonEmptyString);
|
||||
const shouldPreferDiscoveryCounterparty = explicitAnchorType === "counterparty" &&
|
||||
Boolean(groundedDiscoveryCounterparty &&
|
||||
hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString) &&
|
||||
(!explicitAnchorValue || !sameCounterpartyCandidate(explicitAnchorValue, groundedDiscoveryCounterparty)));
|
||||
if ((explicitAnchorType || explicitAnchorValue) && !shouldPreferDiscoveryCounterparty) {
|
||||
return {
|
||||
anchorType: explicitAnchorType,
|
||||
anchorValue: explicitAnchorValue
|
||||
|
|
@ -388,8 +501,11 @@ function resolveAddressDebugAnchorContext(debug, toNonEmptyString = fallbackToNo
|
|||
anchorValue: item
|
||||
};
|
||||
}
|
||||
const counterparty = toNonEmptyString(extractedFilters?.counterparty);
|
||||
if (counterparty) {
|
||||
const counterparty = normalizeCounterpartyCandidate(extractedFilters?.counterparty, toNonEmptyString);
|
||||
if (counterparty &&
|
||||
!(groundedDiscoveryCounterparty &&
|
||||
hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString) &&
|
||||
!sameCounterpartyCandidate(counterparty, groundedDiscoveryCounterparty))) {
|
||||
return {
|
||||
anchorType: "counterparty",
|
||||
anchorValue: counterparty
|
||||
|
|
@ -454,7 +570,9 @@ function resolveAddressDebugCarryoverFilters(debug, toNonEmptyString = fallbackT
|
|||
Boolean(discoveryDateScope.asOfDate || discoveryDateScope.periodFrom || discoveryDateScope.periodTo);
|
||||
const counterparty = readAddressDebugCounterparty(debug, toNonEmptyString);
|
||||
const organization = readAddressDebugOrganization(debug, toNonEmptyString);
|
||||
if (counterparty && !toNonEmptyString(nextFilters.counterparty)) {
|
||||
const preferGroundedDiscoveryCounterparty = hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString) && Boolean(counterparty);
|
||||
const existingCounterparty = normalizeCounterpartyCandidate(nextFilters.counterparty, toNonEmptyString);
|
||||
if (counterparty && (preferGroundedDiscoveryCounterparty || !existingCounterparty)) {
|
||||
nextFilters.counterparty = counterparty;
|
||||
}
|
||||
if (organization && !toNonEmptyString(nextFilters.organization)) {
|
||||
|
|
|
|||
|
|
@ -65,7 +65,13 @@ const PRIMITIVE_CONTRACTS = [
|
|||
supported_fact_families: ["value_flow", "movement_evidence"],
|
||||
supported_action_families: ["turnover", "payout", "net_value_flow", "list_movements"],
|
||||
planning_tags: ["movement", "comparison", "ranking", "aggregation", "monthly_aggregation"],
|
||||
required_axes_any_of: [["period", "account"], ["period", "counterparty"], ["period", "organization"]],
|
||||
required_axes_any_of: [
|
||||
["period", "account"],
|
||||
["period", "counterparty"],
|
||||
["period", "organization"],
|
||||
["all_time_scope", "counterparty"],
|
||||
["all_time_scope", "organization"]
|
||||
],
|
||||
optional_axes: ["contract", "document", "amount", "item", "warehouse"],
|
||||
output_fact_kinds: ["movement_rows", "turnover", "balance_delta"],
|
||||
evidence_floor: "rows_matched",
|
||||
|
|
@ -79,7 +85,14 @@ const PRIMITIVE_CONTRACTS = [
|
|||
supported_fact_families: ["document_evidence", "activity_lifecycle"],
|
||||
supported_action_families: ["list_documents", "activity_duration"],
|
||||
planning_tags: ["document"],
|
||||
required_axes_any_of: [["document"], ["counterparty"], ["contract"], ["period", "organization"]],
|
||||
required_axes_any_of: [
|
||||
["document"],
|
||||
["counterparty"],
|
||||
["contract"],
|
||||
["period", "organization"],
|
||||
["all_time_scope", "counterparty"],
|
||||
["all_time_scope", "organization"]
|
||||
],
|
||||
optional_axes: ["account", "amount", "item", "warehouse"],
|
||||
output_fact_kinds: ["document_rows", "document_dates", "document_amounts"],
|
||||
evidence_floor: "rows_matched",
|
||||
|
|
@ -93,7 +106,13 @@ const PRIMITIVE_CONTRACTS = [
|
|||
supported_fact_families: ["value_flow"],
|
||||
supported_action_families: ["turnover", "payout", "net_value_flow"],
|
||||
planning_tags: ["aggregation", "ranking", "monthly_aggregation"],
|
||||
required_axes_any_of: [["aggregate_axis", "period"], ["aggregate_axis", "counterparty"], ["aggregate_axis", "account"]],
|
||||
required_axes_any_of: [
|
||||
["aggregate_axis", "period"],
|
||||
["aggregate_axis", "counterparty"],
|
||||
["aggregate_axis", "account"],
|
||||
["aggregate_axis", "counterparty", "all_time_scope"],
|
||||
["aggregate_axis", "organization", "all_time_scope"]
|
||||
],
|
||||
optional_axes: ["organization", "contract", "document", "amount"],
|
||||
output_fact_kinds: ["aggregate_totals", "ranked_axis_values"],
|
||||
evidence_floor: "rows_matched",
|
||||
|
|
|
|||
|
|
@ -45,7 +45,20 @@ function isInternalMechanicsLine(value) {
|
|||
text.includes("pilot_") ||
|
||||
text.includes("runtime_") ||
|
||||
text.includes("planner_") ||
|
||||
text.includes("catalog_"));
|
||||
text.includes("catalog_") ||
|
||||
text.includes("needs more scope before execution") ||
|
||||
text.includes("mcp_execution_performed"));
|
||||
}
|
||||
function userFacingUnknowns(values) {
|
||||
return uniqueStrings(values).filter((value) => !isInternalMechanicsLine(value));
|
||||
}
|
||||
function rankedValueFlowUnknownLines(pilot) {
|
||||
if (!pilot.derived_ranked_value_flow) {
|
||||
return userFacingUnknowns(pilot.evidence.unknown_facts);
|
||||
}
|
||||
const ranking = pilot.derived_ranked_value_flow;
|
||||
const period = ranking.period_scope ? `периода ${ranking.period_scope}` : "проверенного окна";
|
||||
return [`Полный рейтинг контрагентов вне ${period} этим поиском не подтвержден.`];
|
||||
}
|
||||
function userFacingLimitations(values) {
|
||||
return uniqueStrings(values).filter((value) => !isInternalMechanicsLine(value));
|
||||
|
|
@ -143,11 +156,20 @@ function explicitOrganizationScope(pilot) {
|
|||
const normalized = value.trim();
|
||||
return normalized.length > 0 ? normalized : null;
|
||||
}
|
||||
function hasAllTimeScope(pilot) {
|
||||
return (dryRunHasAxis(pilot, "all_time_scope") ||
|
||||
pilot.reason_codes.includes("mcp_discovery_all_time_scope_signal_detected") ||
|
||||
pilot.dry_run.reason_codes.includes("mcp_discovery_all_time_scope_signal_detected"));
|
||||
}
|
||||
function documentOrMovementScopeRu(pilot) {
|
||||
const entity = firstEntityCandidate(pilot);
|
||||
const period = explicitDateScope(pilot);
|
||||
const entityPart = entity ? ` по контрагенту ${entity}` : "";
|
||||
const periodPart = period ? ` за ${period}` : " в проверенном окне";
|
||||
const periodPart = period
|
||||
? ` за ${period}`
|
||||
: hasAllTimeScope(pilot)
|
||||
? " за все доступное время"
|
||||
: " в проверенном окне";
|
||||
return `${entityPart}${periodPart}`;
|
||||
}
|
||||
function isMovementLaneClarification(pilot) {
|
||||
|
|
@ -193,12 +215,24 @@ function dryRunMissingAxis(pilot, axis) {
|
|||
}
|
||||
return pilot.dry_run.execution_steps.some((step) => step.missing_axis_options.some((option) => option.includes(axis)));
|
||||
}
|
||||
function queryPlanClarificationGaps(pilot) {
|
||||
const values = pilot.evidence.query_plan.clarification_gaps;
|
||||
return Array.isArray(values) ? uniqueStrings(values) : [];
|
||||
}
|
||||
function clarificationGapMissing(pilot, axis) {
|
||||
const gaps = queryPlanClarificationGaps(pilot);
|
||||
if (gaps.length > 0) {
|
||||
return gaps.includes(axis);
|
||||
}
|
||||
return dryRunMissingAxis(pilot, axis);
|
||||
}
|
||||
function clarificationNeedRu(pilot) {
|
||||
const needsPeriod = clarificationGapMissing(pilot, "period");
|
||||
const organizationScopedOpenTotal = pilot.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
|
||||
pilot.dry_run.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
|
||||
pilot.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph") ||
|
||||
pilot.dry_run.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph");
|
||||
if (organizationScopedOpenTotal) {
|
||||
if (organizationScopedOpenTotal && !needsPeriod) {
|
||||
return {
|
||||
subject: "\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044e",
|
||||
verb: "\u043d\u0443\u0436\u043d\u043e"
|
||||
|
|
@ -206,8 +240,7 @@ function clarificationNeedRu(pilot) {
|
|||
}
|
||||
const hasCounterparty = dryRunHasAxis(pilot, "counterparty");
|
||||
const hasAccount = dryRunHasAxis(pilot, "account");
|
||||
const needsPeriod = dryRunMissingAxis(pilot, "period");
|
||||
const needsOrganization = !hasCounterparty && !hasAccount && dryRunMissingAxis(pilot, "organization");
|
||||
const needsOrganization = !hasCounterparty && !hasAccount && clarificationGapMissing(pilot, "organization");
|
||||
if (needsPeriod && needsOrganization) {
|
||||
return { subject: "проверяемый период и организацию", verb: "нужно" };
|
||||
}
|
||||
|
|
@ -224,8 +257,8 @@ function clarificationNextStepLine(pilot, laneLabel) {
|
|||
pilot.dry_run.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
|
||||
pilot.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph") ||
|
||||
pilot.dry_run.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph");
|
||||
const needsPeriod = dryRunMissingAxis(pilot, "period");
|
||||
const needsOrganization = dryRunMissingAxis(pilot, "organization");
|
||||
const needsPeriod = clarificationGapMissing(pilot, "period");
|
||||
const needsOrganization = clarificationGapMissing(pilot, "organization");
|
||||
const scopeSuffix = laneScopeSuffix(pilot);
|
||||
if (organizationScopedOpenTotal && !needsPeriod) {
|
||||
return `Уточните организацию, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
|
||||
|
|
@ -693,13 +726,15 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
|
|||
if (monthlyConfirmedLines.length > 0) {
|
||||
pushReason(reasonCodes, "answer_contains_monthly_breakdown");
|
||||
}
|
||||
const confirmedLines = derivedValueLine
|
||||
? [...pilot.evidence.confirmed_facts, derivedValueLine, ...monthlyConfirmedLines]
|
||||
: derivedEntityResolutionLine
|
||||
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
|
||||
: derivedMetadataLine
|
||||
? [...pilot.evidence.confirmed_facts, derivedMetadataLine]
|
||||
: pilot.evidence.confirmed_facts;
|
||||
const confirmedLines = pilot.derived_ranked_value_flow && derivedValueLine
|
||||
? [derivedValueLine]
|
||||
: derivedValueLine
|
||||
? [...pilot.evidence.confirmed_facts, derivedValueLine, ...monthlyConfirmedLines]
|
||||
: derivedEntityResolutionLine
|
||||
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
|
||||
: derivedMetadataLine
|
||||
? [...pilot.evidence.confirmed_facts, derivedMetadataLine]
|
||||
: pilot.evidence.confirmed_facts;
|
||||
return {
|
||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryAnswerAdapter",
|
||||
|
|
@ -707,7 +742,7 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
|
|||
headline: headlineFor(mode, pilot),
|
||||
confirmed_lines: uniqueStrings(confirmedLines),
|
||||
inference_lines: uniqueStrings(inferenceLines),
|
||||
unknown_lines: uniqueStrings(pilot.evidence.unknown_facts),
|
||||
unknown_lines: rankedValueFlowUnknownLines(pilot),
|
||||
limitation_lines: userFacingLimitations([...pilot.query_limitations, ...pilot.evidence.query_limitations]),
|
||||
next_step_line: nextStepFor(mode, pilot),
|
||||
internal_mechanics_allowed: false,
|
||||
|
|
|
|||
|
|
@ -100,10 +100,10 @@ function hasOpenScopeOneSidedValueTotalHint(rawUtterance, action) {
|
|||
return false;
|
||||
}
|
||||
if (action === "turnover") {
|
||||
return /(?:\bсколько\s+(?:мы\s+)?(?:получили|получено|входящих(?:\s+денег)?|поступлений|денег\s+пришло)\b|(?:сумма|объем)\s+(?:входящих|поступлений)|поступлений\s+за\b)/iu.test(rawUtterance);
|
||||
return /(?:\bсколько\s+(?:(?:вообще|всего|реально)\s+){0,2}(?:мы\s+)?(?:получили|получено|входящих(?:\s+денег)?(?:\s+было)?|поступлений|денег\s+пришло)\b|(?:сумма|объем)\s+(?:входящих|поступлений)|поступлений\s+за\b)/iu.test(rawUtterance);
|
||||
}
|
||||
if (action === "payout") {
|
||||
return /(?:\bсколько\s+(?:мы\s+)?(?:заплатили|выплатили|потратили|исходящих(?:\s+денег)?|платежей|списаний)\b|(?:сумма|объем)\s+(?:исходящих|платежей|списаний)|(?:платежей|списаний)\s+за\b)/iu.test(rawUtterance);
|
||||
return /(?:\bсколько\s+(?:(?:вообще|всего|реально)\s+){0,2}(?:мы\s+)?(?:заплатили|выплатили|потратили|исходящих(?:\s+денег)?(?:\s+было)?|платежей(?:\s+было)?|списаний(?:\s+было)?)\b|(?:сумма|объем)\s+(?:исходящих|платежей|списаний)|(?:платежей|списаний)\s+за\b)/iu.test(rawUtterance);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -112,10 +112,10 @@ function hasOpenScopeOneSidedValueTotalHintUtf8Safe(rawUtterance, action) {
|
|||
return false;
|
||||
}
|
||||
if (action === "turnover") {
|
||||
return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:\u043c\u044b\s+)?(?:\u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438|\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043e|\u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445(?:\s+\u0434\u0435\u043d\u0435\u0433)?|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439|\u0434\u0435\u043d\u0435\u0433\s+\u043f\u0440\u0438\u0448\u043b\u043e)|(?:\u0441\u0443\u043c\u043c\u0430|\u043e\u0431\u044a\u0435\u043c)\s+(?:\u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439)|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439\s+\u0437\u0430)/u.test(rawUtterance);
|
||||
return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:(?:\u0432\u043e\u043e\u0431\u0449\u0435|\u0432\u0441\u0435\u0433\u043e|\u0440\u0435\u0430\u043b\u044c\u043d\u043e)\s+){0,2}(?:\u043c\u044b\s+)?(?:\u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438|\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043e|\u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445(?:\s+\u0434\u0435\u043d\u0435\u0433)?(?:\s+\u0431\u044b\u043b\u043e)?|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439|\u0434\u0435\u043d\u0435\u0433\s+\u043f\u0440\u0438\u0448\u043b\u043e)|(?:\u0441\u0443\u043c\u043c\u0430|\u043e\u0431\u044a\u0435\u043c)\s+(?:\u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439)|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439\s+\u0437\u0430)/u.test(rawUtterance);
|
||||
}
|
||||
if (action === "payout") {
|
||||
return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:\u043c\u044b\s+)?(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438|\u0432\u044b\u043f\u043b\u0430\u0442\u0438\u043b\u0438|\u043f\u043e\u0442\u0440\u0430\u0442\u0438\u043b\u0438|\u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445(?:\s+\u0434\u0435\u043d\u0435\u0433)?|\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439)|(?:\u0441\u0443\u043c\u043c\u0430|\u043e\u0431\u044a\u0435\u043c)\s+(?:\u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445|\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439)|(?:\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439)\s+\u0437\u0430)/u.test(rawUtterance);
|
||||
return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:(?:\u0432\u043e\u043e\u0431\u0449\u0435|\u0432\u0441\u0435\u0433\u043e|\u0440\u0435\u0430\u043b\u044c\u043d\u043e)\s+){0,2}(?:\u043c\u044b\s+)?(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438|\u0432\u044b\u043f\u043b\u0430\u0442\u0438\u043b\u0438|\u043f\u043e\u0442\u0440\u0430\u0442\u0438\u043b\u0438|\u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445(?:\s+\u0434\u0435\u043d\u0435\u0433)?(?:\s+\u0431\u044b\u043b\u043e)?|\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439(?:\s+\u0431\u044b\u043b\u043e)?|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439(?:\s+\u0431\u044b\u043b\u043e)?)|(?:\u0441\u0443\u043c\u043c\u0430|\u043e\u0431\u044a\u0435\u043c)\s+(?:\u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445|\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439)|(?:\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439)\s+\u0437\u0430)/u.test(rawUtterance);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -131,6 +131,10 @@ function allowsOpenScopeWithoutSubject(input) {
|
|||
}
|
||||
return Boolean(supportsOrganizationScopedOpenTotal(input.action) && (input.organizationScope || input.oneSidedOpenScopeTotalHint));
|
||||
}
|
||||
function allowsMetadataScopedOpenLaneWithoutSubject(input) {
|
||||
return Boolean(input.subjectResolutionOptional &&
|
||||
(input.family === "movement_evidence" || input.family === "document_evidence"));
|
||||
}
|
||||
function rankingNeedFromRawUtterance(value) {
|
||||
const text = lower(value);
|
||||
if (!text) {
|
||||
|
|
@ -206,13 +210,17 @@ function decompositionCandidatesFor(input) {
|
|||
return result;
|
||||
}
|
||||
if (input.family === "movement_evidence") {
|
||||
pushUnique(result, "resolve_entity_reference");
|
||||
if (!input.metadataScopedOpenLaneWithoutSubject) {
|
||||
pushUnique(result, "resolve_entity_reference");
|
||||
}
|
||||
pushUnique(result, "fetch_scoped_movements");
|
||||
pushUnique(result, "probe_coverage");
|
||||
return result;
|
||||
}
|
||||
if (input.family === "document_evidence") {
|
||||
pushUnique(result, "resolve_entity_reference");
|
||||
if (!input.metadataScopedOpenLaneWithoutSubject) {
|
||||
pushUnique(result, "resolve_entity_reference");
|
||||
}
|
||||
pushUnique(result, "fetch_scoped_documents");
|
||||
pushUnique(result, "probe_coverage");
|
||||
return result;
|
||||
|
|
@ -252,6 +260,8 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
|
|||
const seededRankingNeed = toNonEmptyString(turnMeaning?.seeded_ranking_need);
|
||||
const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope);
|
||||
const explicitOrganizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope);
|
||||
const metadataScopeHint = toNonEmptyString(turnMeaning?.metadata_scope_hint);
|
||||
const subjectResolutionOptional = turnMeaning?.subject_resolution_optional === true;
|
||||
const subjectCandidates = (turnMeaning?.explicit_entity_candidates ?? [])
|
||||
.map((item) => toNonEmptyString(item))
|
||||
.filter((item) => Boolean(item));
|
||||
|
|
@ -275,6 +285,11 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
|
|||
rankingNeed,
|
||||
oneSidedOpenScopeTotalHint
|
||||
});
|
||||
const metadataScopedOpenLaneWithoutSubject = subjectCandidates.length === 0 &&
|
||||
allowsMetadataScopedOpenLaneWithoutSubject({
|
||||
family: businessFactFamily,
|
||||
subjectResolutionOptional
|
||||
});
|
||||
const clarificationGaps = [];
|
||||
if (unsupported === "metadata_lane_choice_clarification" || action === "resolve_next_lane") {
|
||||
pushUnique(clarificationGaps, "lane_family_choice");
|
||||
|
|
@ -285,7 +300,15 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
|
|||
!explicitOrganizationScope) {
|
||||
pushUnique(clarificationGaps, "organization");
|
||||
}
|
||||
else if (subjectCandidates.length === 0 && businessFactFamily !== "schema_surface" && !openScopeWithoutSubject) {
|
||||
else if (subjectCandidates.length === 0 &&
|
||||
metadataScopedOpenLaneWithoutSubject &&
|
||||
!explicitOrganizationScope) {
|
||||
pushUnique(clarificationGaps, "organization");
|
||||
}
|
||||
else if (subjectCandidates.length === 0 &&
|
||||
businessFactFamily !== "schema_surface" &&
|
||||
!openScopeWithoutSubject &&
|
||||
!metadataScopedOpenLaneWithoutSubject) {
|
||||
pushUnique(clarificationGaps, "subject");
|
||||
}
|
||||
const timeScopeNeed = timeScopeNeedFor({
|
||||
|
|
@ -302,7 +325,8 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
|
|||
aggregationNeed,
|
||||
comparisonNeed,
|
||||
rankingNeed,
|
||||
openScopeWithoutSubject
|
||||
openScopeWithoutSubject,
|
||||
metadataScopedOpenLaneWithoutSubject
|
||||
});
|
||||
const reasonCodes = [];
|
||||
pushReason(reasonCodes, "data_need_graph_built");
|
||||
|
|
@ -324,6 +348,9 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
|
|||
if (openScopeWithoutSubject && !rankingNeed && !comparisonNeed) {
|
||||
pushReason(reasonCodes, "data_need_graph_open_scope_total_without_subject");
|
||||
}
|
||||
if (metadataScopedOpenLaneWithoutSubject) {
|
||||
pushReason(reasonCodes, "data_need_graph_metadata_scoped_open_lane_without_subject");
|
||||
}
|
||||
if (allTimeScopeHint) {
|
||||
pushReason(reasonCodes, "data_need_graph_all_time_scope_hint");
|
||||
}
|
||||
|
|
@ -337,6 +364,8 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
|
|||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||
subject_candidates: subjectCandidates,
|
||||
metadata_scope_hint: metadataScopeHint,
|
||||
subject_resolution_optional: subjectResolutionOptional || undefined,
|
||||
business_fact_family: businessFactFamily,
|
||||
action_family: toNonEmptyString(turnMeaning?.asked_action_family),
|
||||
aggregation_need: aggregationNeed,
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@ function hasEntity(meaning) {
|
|||
function hasSubjectCandidates(graph) {
|
||||
return (graph?.subject_candidates.length ?? 0) > 0;
|
||||
}
|
||||
function hasMetadataScopedOpenLane(graph, meaning) {
|
||||
return Boolean(graph?.subject_resolution_optional === true ||
|
||||
meaning?.subject_resolution_optional === true);
|
||||
}
|
||||
function hasReasonCode(graph, reasonCode) {
|
||||
return (graph?.reason_codes ?? []).includes(reasonCode);
|
||||
}
|
||||
|
|
@ -58,6 +62,16 @@ function addScopeAxes(axes, meaning) {
|
|||
pushUnique(axes, "period");
|
||||
}
|
||||
}
|
||||
function addMetadataScopeAxis(axes, meaning) {
|
||||
if (toNonEmptyString(meaning?.metadata_scope_hint)) {
|
||||
pushUnique(axes, "metadata_scope");
|
||||
}
|
||||
}
|
||||
function addTimeScopeAxes(axes, dataNeedGraph) {
|
||||
if (dataNeedGraph?.time_scope_need === "all_time_scope") {
|
||||
pushUnique(axes, "all_time_scope");
|
||||
}
|
||||
}
|
||||
function includesAny(text, tokens) {
|
||||
return tokens.some((token) => text.includes(token));
|
||||
}
|
||||
|
|
@ -268,7 +282,7 @@ function recipeFor(input) {
|
|||
const graphAction = lower(dataNeedGraph?.action_family);
|
||||
const graphAggregation = lower(dataNeedGraph?.aggregation_need);
|
||||
const graphClarificationGaps = (dataNeedGraph?.clarification_gaps ?? []).map((item) => lower(item));
|
||||
const organizationScope = toNonEmptyString(meaning?.explicit_organization_scope);
|
||||
const metadataScopedOpenLane = hasMetadataScopedOpenLane(dataNeedGraph, meaning);
|
||||
const openScopeTotalWithoutSubject = graphFactFamily === "value_flow" &&
|
||||
!hasSubjectCandidates(dataNeedGraph) &&
|
||||
hasReasonCode(dataNeedGraph, "data_need_graph_open_scope_total_without_subject");
|
||||
|
|
@ -276,6 +290,8 @@ function recipeFor(input) {
|
|||
const axes = [];
|
||||
const requestedAggregationAxis = aggregationAxis(meaning);
|
||||
addScopeAxes(axes, meaning);
|
||||
addMetadataScopeAxis(axes, meaning);
|
||||
addTimeScopeAxes(axes, dataNeedGraph);
|
||||
if (graphClarificationGaps.includes("lane_family_choice")) {
|
||||
pushUnique(axes, "lane_family_choice");
|
||||
return {
|
||||
|
|
@ -488,6 +504,26 @@ function recipeFor(input) {
|
|||
};
|
||||
}
|
||||
if (graphFactFamily === "movement_evidence") {
|
||||
if (metadataScopedOpenLane) {
|
||||
pushUnique(axes, "organization");
|
||||
pushUnique(axes, "coverage_target");
|
||||
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
|
||||
dataNeedGraph,
|
||||
fallbackPrimitives: ["query_movements", "probe_coverage"],
|
||||
requiredAxes: axes,
|
||||
metadataSurface: input.metadataSurface,
|
||||
actionFamily: action
|
||||
});
|
||||
return {
|
||||
semanticDataNeed: "movement evidence",
|
||||
chainId: "movement_evidence",
|
||||
chainSummary: "Keep the metadata-scoped movement lane, ask only for the remaining business scope, then fetch scoped movement rows and probe coverage without pretending there is a grounded counterparty.",
|
||||
primitives: primitiveSelection.primitives,
|
||||
axes,
|
||||
reason: "planner_selected_metadata_scoped_movement_from_data_need_graph",
|
||||
extraReasons: primitiveSelection.reasonCodes
|
||||
};
|
||||
}
|
||||
pushUnique(axes, "coverage_target");
|
||||
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
|
||||
dataNeedGraph,
|
||||
|
|
@ -507,6 +543,26 @@ function recipeFor(input) {
|
|||
};
|
||||
}
|
||||
if (graphFactFamily === "document_evidence") {
|
||||
if (metadataScopedOpenLane) {
|
||||
pushUnique(axes, "organization");
|
||||
pushUnique(axes, "coverage_target");
|
||||
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
|
||||
dataNeedGraph,
|
||||
fallbackPrimitives: ["query_documents", "probe_coverage"],
|
||||
requiredAxes: axes,
|
||||
metadataSurface: input.metadataSurface,
|
||||
actionFamily: action
|
||||
});
|
||||
return {
|
||||
semanticDataNeed: "document evidence",
|
||||
chainId: "document_evidence",
|
||||
chainSummary: "Keep the metadata-scoped document lane, ask only for the remaining business scope, then fetch scoped document rows and probe coverage without pretending there is a grounded counterparty.",
|
||||
primitives: primitiveSelection.primitives,
|
||||
axes,
|
||||
reason: "planner_selected_metadata_scoped_document_from_data_need_graph",
|
||||
extraReasons: primitiveSelection.reasonCodes
|
||||
};
|
||||
}
|
||||
pushUnique(axes, "coverage_target");
|
||||
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
|
||||
dataNeedGraph,
|
||||
|
|
@ -677,6 +733,7 @@ function planAssistantMcpDiscovery(input) {
|
|||
turnMeaning: input.turnMeaning,
|
||||
proposedPrimitives: recipe.primitives,
|
||||
requiredAxes: recipe.axes,
|
||||
clarificationGaps: dataNeedGraph?.clarification_gaps ?? [],
|
||||
maxProbeCount: budgetOverride.maxProbeCount
|
||||
});
|
||||
const review = (0, assistantMcpCatalogIndex_1.reviewAssistantMcpDiscoveryPlanAgainstCatalog)(plan);
|
||||
|
|
|
|||
|
|
@ -134,6 +134,7 @@ function buildAssistantMcpDiscoveryPlan(input) {
|
|||
const semanticDataNeed = toNonEmptyString(input.semanticDataNeed);
|
||||
const turnMeaning = normalizeTurnMeaning(input.turnMeaning);
|
||||
const requiredAxes = toStringList(input.requiredAxes);
|
||||
const clarificationGaps = toStringList(input.clarificationGaps);
|
||||
const proposed = toStringList(input.proposedPrimitives);
|
||||
const reasonCodes = [];
|
||||
const allowedPrimitives = [];
|
||||
|
|
@ -194,6 +195,7 @@ function buildAssistantMcpDiscoveryPlan(input) {
|
|||
allowed_primitives: allowedPrimitives,
|
||||
rejected_primitives: rejectedPrimitives,
|
||||
required_axes: requiredAxes,
|
||||
clarification_gaps: clarificationGaps,
|
||||
execution_budget: {
|
||||
max_probe_count: clampInteger(input.maxProbeCount, DEFAULT_DISCOVERY_BUDGET.max_probe_count, 1, MAX_PROBE_COUNT),
|
||||
max_rows_per_probe: clampInteger(input.maxRowsPerProbe, DEFAULT_DISCOVERY_BUDGET.max_rows_per_probe, 1, MAX_ROWS_PER_PROBE)
|
||||
|
|
|
|||
|
|
@ -55,7 +55,9 @@ function hasInternalMechanics(value) {
|
|||
text.includes("runtime_") ||
|
||||
text.includes("planner_") ||
|
||||
text.includes("catalog_") ||
|
||||
text.includes("select "));
|
||||
text.includes("select ") ||
|
||||
text.includes("needs more scope before execution") ||
|
||||
text.includes("mcp_execution_performed"));
|
||||
}
|
||||
function userFacingLines(values) {
|
||||
return uniqueStrings(values).filter((line) => !hasInternalMechanics(line));
|
||||
|
|
|
|||
|
|
@ -274,6 +274,27 @@ function hasMatchedFactualAddressContinuationTarget(input, entryPoint) {
|
|||
const targetIntent = toNonEmptyString(dialogContinuationContract?.target_intent);
|
||||
return Boolean(detectedIntent && targetIntent && detectedIntent === targetIntent);
|
||||
}
|
||||
function hasMatchedFactualSuggestedIntentPivotTarget(input, entryPoint) {
|
||||
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
if (!hasEffectivelyFactualAddressReply(input)) {
|
||||
return false;
|
||||
}
|
||||
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||
const dialogContinuationContract = toRecordObject(input.addressRuntimeMeta?.dialogContinuationContract) ??
|
||||
toRecordObject(input.addressRuntimeMeta?.dialog_continuation_contract_v2);
|
||||
const targetIntent = toNonEmptyString(dialogContinuationContract?.target_intent);
|
||||
const decision = toNonEmptyString(dialogContinuationContract?.decision);
|
||||
const selectionMode = toNonEmptyString(dialogContinuationContract?.intent_selection_mode);
|
||||
const suggestedPivotSignal = dialogContinuationContract?.suggested_intent_pivot_signal === true;
|
||||
return Boolean(detectedIntent &&
|
||||
targetIntent &&
|
||||
detectedIntent === targetIntent &&
|
||||
(decision === "switch_to_suggested" ||
|
||||
selectionMode === "switch_to_suggested_intent" ||
|
||||
suggestedPivotSignal));
|
||||
}
|
||||
function hasFullConfirmedFactualAddressReply(input, entryPoint) {
|
||||
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||
return false;
|
||||
|
|
@ -309,6 +330,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
|||
const alignedFactualAddressReply = hasAlignedFactualAddressReply(input, entryPoint);
|
||||
const semanticConflictWithDiscoveryTurnMeaning = hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint);
|
||||
const matchedFactualAddressContinuationTarget = hasMatchedFactualAddressContinuationTarget(input, entryPoint);
|
||||
const matchedFactualSuggestedIntentPivotTarget = hasMatchedFactualSuggestedIntentPivotTarget(input, entryPoint);
|
||||
const fullConfirmedFactualAddressReply = hasFullConfirmedFactualAddressReply(input, entryPoint);
|
||||
const runtimeAdjustedExactReply = hasRuntimeAdjustedExactReply(input, entryPoint);
|
||||
if (!entryPoint) {
|
||||
|
|
@ -335,6 +357,9 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
|||
if (matchedFactualAddressContinuationTarget) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_factual_address_continuation_target");
|
||||
}
|
||||
if (matchedFactualSuggestedIntentPivotTarget) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_factual_suggested_intent_pivot_target");
|
||||
}
|
||||
if (fullConfirmedFactualAddressReply) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply");
|
||||
}
|
||||
|
|
@ -360,6 +385,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
|||
(unsupportedBoundary || discoveryReadyChatCandidate || discoveryReadyDeepCandidate || discoveryReadyAddressCandidate) &&
|
||||
!alignedFactualAddressReply &&
|
||||
!matchedFactualAddressContinuationTarget &&
|
||||
!matchedFactualSuggestedIntentPivotTarget &&
|
||||
!fullConfirmedFactualAddressReply &&
|
||||
!runtimeAdjustedExactReply &&
|
||||
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION = void 0;
|
||||
exports.ASSISTANT_MCP_DISCOVERY_LOOP_STATE_SCHEMA_VERSION = exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION = void 0;
|
||||
exports.runAssistantMcpDiscoveryRuntimeBridge = runAssistantMcpDiscoveryRuntimeBridge;
|
||||
const assistantMcpDiscoveryAnswerAdapter_1 = require("./assistantMcpDiscoveryAnswerAdapter");
|
||||
const assistantMcpDiscoveryPilotExecutor_1 = require("./assistantMcpDiscoveryPilotExecutor");
|
||||
const assistantMcpDiscoveryPlanner_1 = require("./assistantMcpDiscoveryPlanner");
|
||||
exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION = "assistant_mcp_discovery_runtime_bridge_v1";
|
||||
exports.ASSISTANT_MCP_DISCOVERY_LOOP_STATE_SCHEMA_VERSION = "assistant_mcp_discovery_loop_state_v1";
|
||||
function normalizeReasonCode(value) {
|
||||
const normalized = value
|
||||
.trim()
|
||||
|
|
@ -48,6 +49,64 @@ function bridgeStatusFor(pilot, draft) {
|
|||
function businessFactAnswerAllowed(draft) {
|
||||
return draft.answer_mode === "confirmed_with_bounded_inference" || draft.answer_mode === "bounded_inference_only";
|
||||
}
|
||||
function loopStatusFor(bridgeStatus) {
|
||||
if (bridgeStatus === "needs_clarification") {
|
||||
return "awaiting_clarification";
|
||||
}
|
||||
if (bridgeStatus === "blocked" || bridgeStatus === "unsupported") {
|
||||
return "blocked";
|
||||
}
|
||||
return "ready_for_next_hop";
|
||||
}
|
||||
function flattenAxes(pilot, source) {
|
||||
const result = [];
|
||||
for (const step of pilot.dry_run.execution_steps) {
|
||||
if (source === "provided_axes") {
|
||||
for (const axis of step.provided_axes) {
|
||||
if (axis && !result.includes(axis)) {
|
||||
result.push(axis);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for (const option of step.missing_axis_options) {
|
||||
for (const axis of option) {
|
||||
if (axis && !result.includes(axis)) {
|
||||
result.push(axis);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function entityCandidatesFromPlanner(planner) {
|
||||
const values = planner.discovery_plan.turn_meaning_ref?.explicit_entity_candidates ?? [];
|
||||
return uniqueStrings(values);
|
||||
}
|
||||
function buildLoopState(planner, pilot, bridgeStatus) {
|
||||
const plannerClarificationGaps = planner.discovery_plan.clarification_gaps ?? [];
|
||||
return {
|
||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_LOOP_STATE_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
|
||||
loop_status: loopStatusFor(bridgeStatus),
|
||||
selected_chain_id: planner.selected_chain_id,
|
||||
pilot_scope: pilot.pilot_scope,
|
||||
asked_domain_family: planner.discovery_plan.turn_meaning_ref?.asked_domain_family ?? null,
|
||||
asked_action_family: planner.discovery_plan.turn_meaning_ref?.asked_action_family ?? null,
|
||||
unsupported_but_understood_family: planner.discovery_plan.turn_meaning_ref?.unsupported_but_understood_family ?? null,
|
||||
ranking_need: planner.data_need_graph?.ranking_need ?? planner.discovery_plan.turn_meaning_ref?.seeded_ranking_need ?? null,
|
||||
pending_axes: plannerClarificationGaps.length > 0 ? plannerClarificationGaps : flattenAxes(pilot, "missing_axis_options"),
|
||||
provided_axes: flattenAxes(pilot, "provided_axes"),
|
||||
explicit_entity_candidates: entityCandidatesFromPlanner(planner),
|
||||
metadata_scope_hint: planner.discovery_plan.turn_meaning_ref?.metadata_scope_hint ??
|
||||
planner.data_need_graph?.metadata_scope_hint ??
|
||||
null,
|
||||
subject_resolution_optional: planner.discovery_plan.turn_meaning_ref?.subject_resolution_optional === true ||
|
||||
planner.data_need_graph?.subject_resolution_optional === true,
|
||||
explicit_organization_scope: planner.discovery_plan.turn_meaning_ref?.explicit_organization_scope ?? null,
|
||||
explicit_date_scope: planner.discovery_plan.turn_meaning_ref?.explicit_date_scope ?? null
|
||||
};
|
||||
}
|
||||
async function runAssistantMcpDiscoveryRuntimeBridge(input) {
|
||||
const planner = (0, assistantMcpDiscoveryPlanner_1.planAssistantMcpDiscovery)({
|
||||
semanticDataNeed: input.semanticDataNeed,
|
||||
|
|
@ -58,9 +117,11 @@ async function runAssistantMcpDiscoveryRuntimeBridge(input) {
|
|||
const pilot = await (0, assistantMcpDiscoveryPilotExecutor_1.executeAssistantMcpDiscoveryPilot)(planner, input.deps);
|
||||
const answerDraft = (0, assistantMcpDiscoveryAnswerAdapter_1.buildAssistantMcpDiscoveryAnswerDraft)(pilot);
|
||||
const bridgeStatus = bridgeStatusFor(pilot, answerDraft);
|
||||
const loopState = buildLoopState(planner, pilot, bridgeStatus);
|
||||
const reasonCodes = uniqueStrings([...planner.reason_codes, ...pilot.reason_codes, ...answerDraft.reason_codes]);
|
||||
pushReason(reasonCodes, `runtime_bridge_status_${bridgeStatus}`);
|
||||
pushReason(reasonCodes, "runtime_bridge_not_wired_to_hot_assistant_answer");
|
||||
pushReason(reasonCodes, `runtime_bridge_loop_state_${loopState.loop_status}`);
|
||||
return {
|
||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
|
||||
|
|
@ -69,6 +130,7 @@ async function runAssistantMcpDiscoveryRuntimeBridge(input) {
|
|||
planner,
|
||||
pilot,
|
||||
answer_draft: answerDraft,
|
||||
loop_state: loopState,
|
||||
user_facing_response_allowed: bridgeStatus !== "blocked",
|
||||
business_fact_answer_allowed: businessFactAnswerAllowed(answerDraft),
|
||||
requires_user_clarification: bridgeStatus === "needs_clarification",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|||
exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION = void 0;
|
||||
exports.buildAssistantMcpDiscoveryTurnInput = buildAssistantMcpDiscoveryTurnInput;
|
||||
const assistantMcpDiscoveryDataNeedGraph_1 = require("./assistantMcpDiscoveryDataNeedGraph");
|
||||
const addressTextRepair_1 = require("./addressTextRepair");
|
||||
exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION = "assistant_mcp_discovery_turn_input_v1";
|
||||
function toRecordObject(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
|
|
@ -38,14 +39,81 @@ function pushUnique(target, value) {
|
|||
}
|
||||
}
|
||||
function isReferentialEntityPlaceholder(value) {
|
||||
return /^(?:\u043d\u0435\u043c\u0443|\u043d\u0435\u0439|\u043d\u0438\u043c|\u043d\u0438\u043c\u0438|\u0435\u0433\u043e|\u0435\u0435|\u0435\u0451|\u0438\u0445|\u044d\u0442\u043e\u043c\u0443|\u044d\u0442\u043e\u0439|\u044d\u0442\u0438\u043c|\u044d\u0442\u0438\u043c\u0438|\u044d\u0442\u043e\u043c)$/iu.test(value.trim());
|
||||
return new Set([
|
||||
"он",
|
||||
"она",
|
||||
"оно",
|
||||
"они",
|
||||
"ему",
|
||||
"ней",
|
||||
"нему",
|
||||
"ним",
|
||||
"ними",
|
||||
"его",
|
||||
"ее",
|
||||
"их",
|
||||
"этому",
|
||||
"этой",
|
||||
"этим",
|
||||
"этими",
|
||||
"этом"
|
||||
]).has((0, addressTextRepair_1.normalizeRussianComparableText)(value));
|
||||
}
|
||||
function isReferentialOrganizationPlaceholder(value) {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
return new Set([
|
||||
"эта организация",
|
||||
"этой организации",
|
||||
"этой организацией",
|
||||
"эту организацию",
|
||||
"эта компания",
|
||||
"этой компании",
|
||||
"этой компанией",
|
||||
"эту компанию",
|
||||
"наша организация",
|
||||
"нашей организации",
|
||||
"нашей компанией"
|
||||
]).has((0, addressTextRepair_1.normalizeRussianComparableText)(value));
|
||||
}
|
||||
function isValueFlowPredicateEntityCandidate(value) {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
const text = compactLower(value);
|
||||
const looksLikeRankingPredicate = /(?:прин[её]с|принес|выручк|доходн|больше\s+всего|наибольш)/iu.test(text) &&
|
||||
/(?:организац|компан|ден[её]г|выручк|поступлен|плат[её]ж)/iu.test(text);
|
||||
if (!looksLikeRankingPredicate) {
|
||||
return false;
|
||||
}
|
||||
return !/(?<!\p{L})(?:ооо|ип|ао|пао|зао|llc|inc|corp)(?!\p{L})/iu.test(text);
|
||||
}
|
||||
function isActionVerbEntityCandidate(value) {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
return new Set(["провести", "показать", "посмотреть", "смотреть", "найти", "искать", "анализ"]).has((0, addressTextRepair_1.normalizeRussianComparableText)(value));
|
||||
}
|
||||
function isInvalidEntityCandidate(value) {
|
||||
return Boolean(value &&
|
||||
(isReferentialEntityPlaceholder(value) ||
|
||||
isValueFlowPredicateEntityCandidate(value) ||
|
||||
isActionVerbEntityCandidate(value)));
|
||||
}
|
||||
function normalizeFollowupCounterpartyCandidate(value) {
|
||||
const text = candidateValue(value);
|
||||
if (!text || isInvalidEntityCandidate(text)) {
|
||||
return null;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
function pushScopedEntityCandidate(target, value, groundedFollowupEntity) {
|
||||
const text = candidateValue(value);
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
if (groundedFollowupEntity && isReferentialEntityPlaceholder(text)) {
|
||||
if ((groundedFollowupEntity && isReferentialEntityPlaceholder(text)) || isValueFlowPredicateEntityCandidate(text)) {
|
||||
return;
|
||||
}
|
||||
pushUnique(target, text);
|
||||
|
|
@ -62,7 +130,7 @@ function pushNormalizedEntityResolutionCandidate(target, value) {
|
|||
return;
|
||||
}
|
||||
const normalized = canonicalizeEntityResolutionCandidate(text);
|
||||
if (normalized && !target.includes(normalized)) {
|
||||
if (normalized && !isInvalidEntityCandidate(normalized) && !target.includes(normalized)) {
|
||||
target.push(normalized);
|
||||
}
|
||||
}
|
||||
|
|
@ -93,18 +161,25 @@ function collectEntityCandidates(value) {
|
|||
const result = [];
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
pushUnique(result, candidateValue(item));
|
||||
const candidate = candidateValue(item);
|
||||
if (!isInvalidEntityCandidate(candidate)) {
|
||||
pushUnique(result, candidate);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
pushUnique(result, candidateValue(value));
|
||||
const candidate = candidateValue(value);
|
||||
if (!isInvalidEntityCandidate(candidate)) {
|
||||
pushUnique(result, candidate);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function collectPredecomposeEntities(predecompose) {
|
||||
const entities = toRecordObject(predecompose?.entities);
|
||||
const organization = toNonEmptyString(entities?.organization);
|
||||
return {
|
||||
counterparty: toNonEmptyString(entities?.counterparty),
|
||||
organization: toNonEmptyString(entities?.organization)
|
||||
organization: isReferentialOrganizationPlaceholder(organization) ? null : organization
|
||||
};
|
||||
}
|
||||
function collectDateScope(predecompose) {
|
||||
|
|
@ -254,24 +329,115 @@ function mapAddressIntentToFollowupMeaning(intent) {
|
|||
unsupported: null
|
||||
};
|
||||
}
|
||||
function mapLoopClarificationSeedToFollowupMeaning(input) {
|
||||
if (input.domain || input.action || input.unsupported) {
|
||||
return {
|
||||
domain: input.domain,
|
||||
action: input.action,
|
||||
unsupported: input.unsupported
|
||||
};
|
||||
}
|
||||
if (input.selectedChainId === "metadata_lane_clarification") {
|
||||
return {
|
||||
domain: "metadata",
|
||||
action: "resolve_next_lane",
|
||||
unsupported: "metadata_lane_choice_clarification"
|
||||
};
|
||||
}
|
||||
return {
|
||||
domain: null,
|
||||
action: null,
|
||||
unsupported: null
|
||||
};
|
||||
}
|
||||
function pilotScopeFromLoopClarificationSeed(selectedChainId, action) {
|
||||
if (!selectedChainId) {
|
||||
return null;
|
||||
}
|
||||
if (selectedChainId === "metadata_inspection" ||
|
||||
selectedChainId === "metadata_lane_clarification" ||
|
||||
selectedChainId === "catalog_drilldown") {
|
||||
return "metadata_inspection_v1";
|
||||
}
|
||||
if (selectedChainId === "movement_evidence") {
|
||||
return "counterparty_movement_evidence_query_movements_v1";
|
||||
}
|
||||
if (selectedChainId === "document_evidence") {
|
||||
return "counterparty_document_evidence_query_documents_v1";
|
||||
}
|
||||
if (selectedChainId === "lifecycle") {
|
||||
return "counterparty_lifecycle_query_documents_v1";
|
||||
}
|
||||
if (selectedChainId === "entity_resolution") {
|
||||
return "entity_resolution_search_v1";
|
||||
}
|
||||
if (selectedChainId === "value_flow_comparison") {
|
||||
return "counterparty_bidirectional_value_flow_query_movements_v1";
|
||||
}
|
||||
if (selectedChainId === "value_flow_ranking" || selectedChainId === "value_flow") {
|
||||
if (action === "payout") {
|
||||
return "counterparty_supplier_payout_query_movements_v1";
|
||||
}
|
||||
if (action === "net_value_flow") {
|
||||
return "counterparty_bidirectional_value_flow_query_movements_v1";
|
||||
}
|
||||
return "counterparty_value_flow_query_movements_v1";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function collectFollowupDiscoverySeed(followupContext) {
|
||||
const previousFilters = toRecordObject(followupContext?.previous_filters);
|
||||
const rootFilters = toRecordObject(followupContext?.root_filters);
|
||||
const pilotScope = toNonEmptyString(followupContext?.previous_discovery_pilot_scope);
|
||||
const loopStatus = toNonEmptyString(followupContext?.previous_discovery_loop_status);
|
||||
const loopSelectedChainId = toNonEmptyString(followupContext?.previous_discovery_loop_selected_chain_id);
|
||||
const loopPendingAxes = collectEntityCandidates(followupContext?.previous_discovery_loop_pending_axes);
|
||||
const loopProvidedAxes = collectEntityCandidates(followupContext?.previous_discovery_loop_provided_axes);
|
||||
const loopAskedDomainFamily = toNonEmptyString(followupContext?.previous_discovery_loop_asked_domain_family);
|
||||
const loopAskedActionFamily = toNonEmptyString(followupContext?.previous_discovery_loop_asked_action_family);
|
||||
const loopUnsupportedFamily = toNonEmptyString(followupContext?.previous_discovery_loop_unsupported_family);
|
||||
const loopMetadataScopeHint = toNonEmptyString(followupContext?.previous_discovery_loop_metadata_scope_hint);
|
||||
const loopSubjectResolutionOptional = followupContext?.previous_discovery_loop_subject_resolution_optional === true;
|
||||
const previousIntent = toNonEmptyString(followupContext?.target_intent) ?? toNonEmptyString(followupContext?.previous_intent);
|
||||
const mapped = mapPilotScopeToFollowupMeaning(pilotScope).domain !== null
|
||||
? mapPilotScopeToFollowupMeaning(pilotScope)
|
||||
: mapAddressIntentToFollowupMeaning(previousIntent);
|
||||
const loopMapped = loopStatus === "awaiting_clarification"
|
||||
? mapLoopClarificationSeedToFollowupMeaning({
|
||||
selectedChainId: loopSelectedChainId,
|
||||
domain: loopAskedDomainFamily,
|
||||
action: loopAskedActionFamily,
|
||||
unsupported: loopUnsupportedFamily
|
||||
})
|
||||
: {
|
||||
domain: null,
|
||||
action: null,
|
||||
unsupported: null
|
||||
};
|
||||
const effectivePilotScope = pilotScope ?? pilotScopeFromLoopClarificationSeed(loopSelectedChainId, loopMapped.action);
|
||||
const mapped = loopMapped.domain !== null || loopMapped.action !== null || loopMapped.unsupported !== null
|
||||
? loopMapped
|
||||
: mapPilotScopeToFollowupMeaning(effectivePilotScope).domain !== null
|
||||
? mapPilotScopeToFollowupMeaning(effectivePilotScope)
|
||||
: mapAddressIntentToFollowupMeaning(previousIntent);
|
||||
const discoveryEntities = collectEntityCandidates(followupContext?.previous_discovery_entity_candidates);
|
||||
const entityResolutionStatus = toNonEmptyString(followupContext?.previous_discovery_entity_resolution_status);
|
||||
const entityResolutionAmbiguityCandidates = collectEntityCandidates(followupContext?.previous_discovery_entity_ambiguity_candidates);
|
||||
const ambiguityBlocksImplicitGrounding = pilotScope === "entity_resolution_search_v1" && entityResolutionStatus === "ambiguous";
|
||||
const counterparty = toNonEmptyString(previousFilters?.counterparty) ??
|
||||
toNonEmptyString(rootFilters?.counterparty) ??
|
||||
(toNonEmptyString(followupContext?.previous_anchor_type) === "counterparty"
|
||||
? toNonEmptyString(followupContext?.previous_anchor_value)
|
||||
: null) ??
|
||||
(ambiguityBlocksImplicitGrounding ? null : discoveryEntities[0] ?? null);
|
||||
const ambiguityBlocksImplicitGrounding = effectivePilotScope === "entity_resolution_search_v1" && entityResolutionStatus === "ambiguous";
|
||||
const metadataPilotCarriesScopeOnly = effectivePilotScope === "metadata_inspection_v1" || loopSubjectResolutionOptional;
|
||||
const normalizedDiscoveryEntities = discoveryEntities
|
||||
.map((entity) => normalizeFollowupCounterpartyCandidate(entity))
|
||||
.filter((entity) => Boolean(entity));
|
||||
const groundedDiscoveryCounterparty = ambiguityBlocksImplicitGrounding || metadataPilotCarriesScopeOnly
|
||||
? null
|
||||
: normalizedDiscoveryEntities[0] ?? normalizeFollowupCounterpartyCandidate(loopMetadataScopeHint);
|
||||
const metadataScopeHint = loopMetadataScopeHint ??
|
||||
(loopSubjectResolutionOptional ? normalizedDiscoveryEntities[0] ?? null : null);
|
||||
const previousFiltersCounterparty = normalizeFollowupCounterpartyCandidate(previousFilters?.counterparty);
|
||||
const rootFiltersCounterparty = normalizeFollowupCounterpartyCandidate(rootFilters?.counterparty);
|
||||
const previousAnchorCounterparty = toNonEmptyString(followupContext?.previous_anchor_type) === "counterparty"
|
||||
? normalizeFollowupCounterpartyCandidate(followupContext?.previous_anchor_value)
|
||||
: null;
|
||||
const counterparty = groundedDiscoveryCounterparty
|
||||
? groundedDiscoveryCounterparty
|
||||
: previousFiltersCounterparty ?? rootFiltersCounterparty ?? previousAnchorCounterparty;
|
||||
const organization = toNonEmptyString(previousFilters?.organization) ??
|
||||
toNonEmptyString(rootFilters?.organization) ??
|
||||
(toNonEmptyString(followupContext?.previous_anchor_type) === "organization"
|
||||
|
|
@ -280,17 +446,23 @@ function collectFollowupDiscoverySeed(followupContext) {
|
|||
const dateScope = collectDateScopeFromFilters(previousFilters) ??
|
||||
collectDateScopeFromFilters(rootFilters);
|
||||
return {
|
||||
pilotScope,
|
||||
pilotScope: effectivePilotScope,
|
||||
domain: mapped.domain,
|
||||
action: mapped.action,
|
||||
unsupported: mapped.unsupported,
|
||||
loopStatus,
|
||||
loopSelectedChainId,
|
||||
loopPendingAxes,
|
||||
loopProvidedAxes,
|
||||
counterparty,
|
||||
discoveryEntity: ambiguityBlocksImplicitGrounding ? null : discoveryEntities[0] ?? null,
|
||||
discoveryEntity: ambiguityBlocksImplicitGrounding || loopSubjectResolutionOptional ? null : normalizedDiscoveryEntities[0] ?? null,
|
||||
entityResolutionStatus,
|
||||
entityResolutionAmbiguityCandidates,
|
||||
rankingNeed: toNonEmptyString(followupContext?.previous_discovery_ranking_need),
|
||||
organization,
|
||||
dateScope,
|
||||
metadataScopeHint,
|
||||
subjectResolutionOptional: loopSubjectResolutionOptional,
|
||||
metadataRouteFamily: normalizeMetadataRouteFamily(followupContext?.previous_discovery_metadata_route_family),
|
||||
metadataRouteFamilySelectionBasis: normalizeMetadataRouteFamilySelectionBasis(followupContext?.previous_discovery_metadata_route_family_selection_basis),
|
||||
metadataSelectedEntitySet: toNonEmptyString(followupContext?.previous_discovery_metadata_selected_entity_set),
|
||||
|
|
@ -383,9 +555,24 @@ function hasMetadataSignal(text) {
|
|||
return (/(?:\u043e\u0431\u044a\u0435\u043a\u0442(?:\u044b|\u0430|\u043e\u0432)?|\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u044b|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b|\u0441\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a\u0438|\u043f\u043e\u043b(?:\u0435|\u044f)|objects?|registers?|documents?|catalogs?|fields?)/iu.test(text) &&
|
||||
/(?:\u0435\u0441\u0442\u044c|\u043a\u0430\u043a\u0438\u0435|\u0434\u043e\u0441\u0442\u0443\u043f\u043d|\u0432\s+1\u0441|1\u0441|available|exist|which)/iu.test(text));
|
||||
}
|
||||
function hasReferentialDocumentExclusionFollowupSignal(text) {
|
||||
return /(?:\u043a\u0440\u043e\u043c\u0435|\u043f\u043e\u043c\u0438\u043c\u043e)\s+(?:\u044d\u0442\u043e\u0433\u043e|\u044d\u0442\u043e\u0439|\u044d\u0442\u043e\u0442|\u044d\u0442\u0443|\u044d\u0442\u0438\u0445)(?:\s+(?:\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430|\u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0430|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430))?/iu.test(text);
|
||||
}
|
||||
function hasMetadataObjectHint(text) {
|
||||
return /(?:\u043e\u0431\u044a\u0435\u043a\u0442(?:\u044b|\u0430|\u043e\u0432)?|\u0440\u0435\u0433\u0438\u0441\u0442\u0440(?:\u044b)?|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u044b)?|\u0441\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a(?:\u0438)?|\u043f\u043e\u043b(?:\u0435|\u044f)|objects?|registers?|documents?|catalogs?|fields?)/iu.test(text);
|
||||
}
|
||||
function hasSimpleDocumentLanePivotSignal(text) {
|
||||
return /(?:^|\s)по\s+документ(?:ам|ы)?(?:\?|$|\s)/iu.test(text);
|
||||
}
|
||||
function hasSimpleMovementLanePivotSignal(text) {
|
||||
return /(?:^|\s)по\s+движени(?:ям|я)?(?:\?|$|\s)/iu.test(text);
|
||||
}
|
||||
function hasUtf8DocumentLanePivotSignal(text) {
|
||||
return /(?:^|\s)по\s+документ(?:ам|ы)?(?:\?|$|\s)/iu.test(text);
|
||||
}
|
||||
function hasUtf8MovementLanePivotSignal(text) {
|
||||
return /(?:^|\s)по\s+движени(?:ям|я)?(?:\?|$|\s)/iu.test(text);
|
||||
}
|
||||
function hasDocumentEvidenceFollowupSignal(text) {
|
||||
return /(?:\u043f\u043e\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u0430\u043c|\u044b)?|\u0434\u0430\u0432\u0430\u0439\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u044b)?|\u0438\u0449\u0438\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u044b)?|\u043f\u043e\u043a\u0430\u0436\u0438\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u044b)?|(?:\u043f\u043e\u043a\u0430\u0436\u0438|\u043a\u0430\u043a\u0438\u0435|\u0441\u043f\u0438\u0441\u043e\u043a|\u0434\u0430\u0439|\u0438\u0449\u0438)\s+(?:\u0441\u0447(?:[еe]т|\u0435\u0442)[-\u2011 ]?\u0444\u0430\u043a\u0442\u0443\u0440(?:\u044b|\u0430)?|\u043d\u0430\u043a\u043b\u0430\u0434\u043d(?:\u044b\u0435|\u0430\u044f)?|\u0430\u043a\u0442(?:\u044b)?|\u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446(?:\u0438\u0438|\u0438\u044e)|invoice(?:s)?|bill(?:s)?|waybill(?:s)?)|document(?:s)?\s+(?:then|next)?|(?:then|next)\s+documents?|go\s+to\s+documents?)/iu.test(text);
|
||||
}
|
||||
|
|
@ -403,23 +590,25 @@ function hasMetadataDownstreamContinuationSignal(text) {
|
|||
}
|
||||
function hasEntityResolutionSignal(text) {
|
||||
const hasSearchVerb = /(?:найд(?:и|ите|ем|у)|поищ(?:и|ите|ем)|найти|поиск|search|find|look\s*up)/iu.test(text);
|
||||
const hasEntityNoun = /(?:контрагент(?:а|ов)?|поставщик(?:а|ов)?|клиент(?:а|ов)?|counterpart(?:y|ies)|supplier(?:s)?|customer(?:s)?)/iu.test(text);
|
||||
const hasEntityNoun = /(?:контрагент(?:а|ов|у|ом|е)?|поставщик(?:а|ов|у|ом|е)?|клиент(?:а|ов|у|ом|е)?|counterpart(?:y|ies)|supplier(?:s)?|customer(?:s)?)/iu.test(text);
|
||||
return hasSearchVerb && hasEntityNoun;
|
||||
}
|
||||
function normalizeEntityResolutionCandidate(value) {
|
||||
return value
|
||||
.replace(/^(?:в\s*1с\s+|в\s+1c\s+|по\s+имени\s+)/iu, "")
|
||||
.replace(/[?!.]+$/gu, "")
|
||||
.replace(/^(?:контрагент(?:а|ов)?|поставщик(?:а|ов)?|клиент(?:а|ов)?)\s+/iu, "")
|
||||
.replace(/^(?:контрагент(?:а|ов|у|ом|е)?|поставщик(?:а|ов|у|ом|е)?|клиент(?:а|ов|у|ом|е)?)\s+/iu, "")
|
||||
.replace(/^(?:counterpart(?:y|ies)|supplier(?:s)?|customer(?:s)?)\s+/iu, "")
|
||||
.replace(/^группу\s+/iu, "Группа ")
|
||||
.replace(/^[«"'\s]+|[»"'\s]+$/gu, "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
function rawEntityResolutionCandidate(text) {
|
||||
const patterns = [
|
||||
/(?:найд(?:и|ите|ем|у)|поищ(?:и|ите|ем)|найти|search|find|look\s*up)\s+(?:в\s*1с\s+|в\s+1c\s+)?(?:контрагент(?:а|ов)?|поставщик(?:а|ов)?|клиент(?:а|ов)?|counterpart(?:y|ies)|supplier(?:s)?|customer(?:s)?)\s+(.+)$/iu,
|
||||
/(?:контрагент(?:а|ов)?|поставщик(?:а|ов)?|клиент(?:а|ов)?|counterpart(?:y|ies)|supplier(?:s)?|customer(?:s)?)\s+(.+?)\s+(?:найд(?:и|ите|ем|у)|поищ(?:и|ите|ем)|найти|search|find|look\s*up)\b/iu
|
||||
/(?:найд(?:и|ите|ем|у)|поищ(?:и|ите|ем)|найти|search|find|look\s*up)\s+(?:в\s*1с\s+|в\s+1c\s+)?(?:контрагент(?:а|ов|у|ом|е)?|поставщик(?:а|ов|у|ом|е)?|клиент(?:а|ов|у|ом|е)?|counterpart(?:y|ies)|supplier(?:s)?|customer(?:s)?)\s+(.+)$/iu,
|
||||
/(?:найд(?:и|ите|ем|у)|поищ(?:и|ите|ем)|найти|search|find|look\s*up)\s+(?:в\s*1с\s+|в\s+1c\s+)(.+)$/iu,
|
||||
/(?:контрагент(?:а|ов|у|ом|е)?|поставщик(?:а|ов|у|ом|е)?|клиент(?:а|ов|у|ом|е)?|counterpart(?:y|ies)|supplier(?:s)?|customer(?:s)?)\s+(.+?)\s+(?:найд(?:и|ите|ем|у)|поищ(?:и|ите|ем)|найти|search|find|look\s*up)\b/iu
|
||||
];
|
||||
for (const pattern of patterns) {
|
||||
const match = text.match(pattern);
|
||||
|
|
@ -565,6 +754,15 @@ function collectDateScopeFromRawText(text) {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
function currentIsoDate() {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
function hasRelativeCurrentDateHint(text) {
|
||||
return /(?:\bсегодня\b|\bна\s+сегодня\b|\bсегодняшн(?:ий|его|ем)\b|\btoday\b|\bas\s+of\s+today\b|\bcurrent\s+date\b)/iu.test(text);
|
||||
}
|
||||
function isImplicitCurrentDateScope(value) {
|
||||
return Boolean(value && /^\d{4}-\d{2}-\d{2}$/.test(value) && value === currentIsoDate());
|
||||
}
|
||||
function semanticNeedFor(input) {
|
||||
const combined = compactLower(`${input.domain ?? ""} ${input.action ?? ""} ${input.unsupported ?? ""}`);
|
||||
if (input.metadataSignal || /(?:metadata|schema|catalog|inspect_(?:catalog|documents|registers|fields))/iu.test(combined)) {
|
||||
|
|
@ -622,19 +820,26 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
const reasonCodes = [];
|
||||
const rawUserText = toNonEmptyString(input.userMessage);
|
||||
const rawEffectiveText = toNonEmptyString(input.effectiveMessage);
|
||||
const rawSignalSourceText = `${rawUserText ?? ""} ${rawEffectiveText ?? ""}`.trim();
|
||||
const rawEntitySourceText = rawUserText ?? rawEffectiveText ?? rawSignalSourceText;
|
||||
const repairedUserText = rawUserText ? (0, addressTextRepair_1.repairAddressMojibakeText)(rawUserText) : null;
|
||||
const repairedEffectiveText = rawEffectiveText ? (0, addressTextRepair_1.repairAddressMojibakeText)(rawEffectiveText) : null;
|
||||
const rawSignalSourceText = `${repairedUserText ?? rawUserText ?? ""} ${repairedEffectiveText ?? rawEffectiveText ?? ""}`.trim();
|
||||
const rawEntitySourceText = repairedUserText ?? rawUserText ?? repairedEffectiveText ?? rawEffectiveText ?? rawSignalSourceText;
|
||||
const rawText = compactLower(rawSignalSourceText);
|
||||
const rawReferentialDocumentExclusionSignal = hasReferentialDocumentExclusionFollowupSignal(repairedUserText ?? rawUserText ?? "");
|
||||
const rawLifecycleSignal = hasLifecycleSignal(rawText);
|
||||
const rawBidirectionalValueFlowSignal = !rawLifecycleSignal && hasBidirectionalValueFlowSignal(rawText);
|
||||
const rawValueFlowSignal = !rawLifecycleSignal &&
|
||||
(hasValueFlowSignal(rawText) || hasValueRankingSignal(rawText) || rawBidirectionalValueFlowSignal);
|
||||
const rawMetadataSignal = !rawLifecycleSignal && !rawValueFlowSignal && hasMetadataSignal(rawText);
|
||||
const rawMetadataSignal = !rawLifecycleSignal &&
|
||||
!rawValueFlowSignal &&
|
||||
!rawReferentialDocumentExclusionSignal &&
|
||||
hasMetadataSignal(rawText);
|
||||
const rawEntityResolutionSignal = !rawLifecycleSignal && !rawValueFlowSignal && !rawMetadataSignal && hasEntityResolutionSignal(rawText);
|
||||
const rawPayoutSignal = rawValueFlowSignal && !rawBidirectionalValueFlowSignal && hasPayoutSignal(rawText);
|
||||
const monthlyAggregationSignal = hasMonthlyAggregationSignal(rawText);
|
||||
const rawAllTimeScopeSignal = hasAllTimeScopeHint(rawText);
|
||||
const explicitDateScopeLiteralDetected = hasExplicitDateScopeLiteral(rawText);
|
||||
const relativeCurrentDateHintDetected = hasRelativeCurrentDateHint(rawText);
|
||||
const rawDateScope = collectDateScopeFromRawText(rawText);
|
||||
const rawMetadataScopeHint = rawMetadataSignal ? metadataScopeHintFromRawText(rawText) : null;
|
||||
const rawEntityCandidate = rawEntityResolutionSignal ? rawEntityResolutionCandidate(rawEntitySourceText) : null;
|
||||
|
|
@ -643,21 +848,47 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
? resolveEntityResolutionAmbiguityChoice(rawEntitySourceText, followupSeed.entityResolutionAmbiguityCandidates)
|
||||
: null;
|
||||
const entityResolutionSignal = rawEntityResolutionSignal || Boolean(rawEntityCandidate) || Boolean(entityResolutionClarificationCandidate);
|
||||
const metadataDocumentHintSignal = hasDocumentEvidenceFollowupSignal(rawText) || hasPronounDocumentEvidenceFollowupSignal(rawText);
|
||||
const metadataMovementHintSignal = hasMovementEvidenceFollowupSignal(rawText) || hasPronounMovementEvidenceFollowupSignal(rawText);
|
||||
const rawEntitySearchOverridesStaleScope = Boolean(rawEntityCandidate && entityResolutionSignal);
|
||||
const rawDomain = toNonEmptyString(assistantTurnMeaning?.asked_domain_family);
|
||||
const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family);
|
||||
const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis);
|
||||
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
|
||||
const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate);
|
||||
const currentTurnDocumentLaneSignal = rawAction === "list_documents";
|
||||
const currentTurnMovementLaneSignal = rawAction === "list_movements";
|
||||
const metadataDocumentHintSignal = currentTurnDocumentLaneSignal ||
|
||||
hasUtf8DocumentLanePivotSignal(rawText) ||
|
||||
hasSimpleDocumentLanePivotSignal(rawText) ||
|
||||
hasDocumentEvidenceFollowupSignal(rawText) ||
|
||||
hasPronounDocumentEvidenceFollowupSignal(rawText);
|
||||
const metadataMovementHintSignal = currentTurnMovementLaneSignal ||
|
||||
hasUtf8MovementLanePivotSignal(rawText) ||
|
||||
hasSimpleMovementLanePivotSignal(rawText) ||
|
||||
hasMovementEvidenceFollowupSignal(rawText) ||
|
||||
hasPronounMovementEvidenceFollowupSignal(rawText);
|
||||
const assistantTurnMeaningDateScope = toNonEmptyString(assistantTurnMeaning?.explicit_date_scope);
|
||||
const assistantTurnMeaningOrganizationScope = toNonEmptyString(assistantTurnMeaning?.explicit_organization_scope);
|
||||
const rawAssistantTurnMeaningOrganizationScope = toNonEmptyString(assistantTurnMeaning?.explicit_organization_scope);
|
||||
const assistantTurnMeaningOrganizationScope = isReferentialOrganizationPlaceholder(rawAssistantTurnMeaningOrganizationScope)
|
||||
? null
|
||||
: rawAssistantTurnMeaningOrganizationScope;
|
||||
const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText);
|
||||
const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText ?? rawEffectiveText ?? rawSignalSourceText);
|
||||
const currentTurnOrganizationScope = rawOrganizationScope ?? predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope;
|
||||
const currentTurnFreshOrganizationScope = rawOrganizationScope ?? predecomposeEntities.organization;
|
||||
const currentTurnOrganizationScope = currentTurnFreshOrganizationScope ?? assistantTurnMeaningOrganizationScope;
|
||||
const followupCounterpartyIsMetadataOrganizationScope = Boolean(followupSeed.subjectResolutionOptional &&
|
||||
followupSeed.counterparty &&
|
||||
(followupSeed.metadataScopeHint ||
|
||||
followupSeed.metadataSelectedEntitySet ||
|
||||
followupSeed.metadataSelectedSurfaceObjects.length > 0) &&
|
||||
(isInvalidEntityCandidate(followupSeed.counterparty) ||
|
||||
sameScopedName(followupSeed.counterparty, followupSeed.organization) ||
|
||||
sameScopedName(followupSeed.counterparty, currentTurnOrganizationScope)));
|
||||
const effectiveFollowupCounterparty = followupCounterpartyIsMetadataOrganizationScope
|
||||
? null
|
||||
: followupSeed.counterparty;
|
||||
const explicitOrganizationScopeSignal = Boolean(rawOrganizationMentionSignal && currentTurnOrganizationScope);
|
||||
const organizationClarificationFollowupApplicable = Boolean(followupSeed.domain === "counterparty_value" &&
|
||||
!followupSeed.counterparty &&
|
||||
!effectiveFollowupCounterparty &&
|
||||
currentTurnOrganizationScope &&
|
||||
!rawLifecycleSignal &&
|
||||
!rawValueFlowSignal &&
|
||||
|
|
@ -676,22 +907,40 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
? null
|
||||
: predecomposeEntities.counterparty;
|
||||
const predecomposeDateScope = collectDateScope(predecomposeContract);
|
||||
const periodClarificationFollowupApplicable = Boolean(followupSeed.domain &&
|
||||
followupSeed.loopStatus === "awaiting_clarification" &&
|
||||
followupSeed.loopPendingAxes.includes("period") &&
|
||||
!rawLifecycleSignal &&
|
||||
!rawMetadataSignal &&
|
||||
(rawAllTimeScopeSignal ||
|
||||
explicitDateScopeLiteralDetected ||
|
||||
rawDateScope ||
|
||||
relativeCurrentDateHintDetected ||
|
||||
(predecomposeDateScope && !isImplicitCurrentDateScope(predecomposeDateScope))));
|
||||
const followupDiscoverySeedApplicable = Boolean(followupSeed.domain &&
|
||||
!rawLifecycleSignal &&
|
||||
!rawValueFlowSignal &&
|
||||
(monthlyAggregationSignal ||
|
||||
explicitDateScopeLiteralDetected ||
|
||||
predecomposeDateScope ||
|
||||
explicitOrganizationScopeSignal ||
|
||||
organizationClarificationFollowupApplicable));
|
||||
!rawMetadataSignal &&
|
||||
(periodClarificationFollowupApplicable ||
|
||||
(!rawValueFlowSignal &&
|
||||
(monthlyAggregationSignal ||
|
||||
rawAllTimeScopeSignal ||
|
||||
explicitDateScopeLiteralDetected ||
|
||||
predecomposeDateScope ||
|
||||
explicitOrganizationScopeSignal ||
|
||||
organizationClarificationFollowupApplicable))));
|
||||
const metadataFollowupSeedApplicable = Boolean(followupSeed.domain === "metadata" &&
|
||||
!rawLifecycleSignal &&
|
||||
!rawValueFlowSignal &&
|
||||
hasMetadataObjectHint(rawText));
|
||||
const metadataLaneCarryoverAvailable = Boolean(effectiveFollowupCounterparty ||
|
||||
followupSeed.discoveryEntity ||
|
||||
followupSeed.metadataScopeHint ||
|
||||
followupSeed.metadataSelectedEntitySet ||
|
||||
followupSeed.metadataSelectedSurfaceObjects.length > 0);
|
||||
const metadataGroundedDocumentFollowupApplicable = Boolean(followupSeed.pilotScope === "metadata_inspection_v1" &&
|
||||
followupSeed.metadataRouteFamily === "document_evidence" &&
|
||||
!followupSeed.metadataAmbiguityDetected &&
|
||||
followupSeed.counterparty &&
|
||||
metadataLaneCarryoverAvailable &&
|
||||
!rawLifecycleSignal &&
|
||||
!rawValueFlowSignal &&
|
||||
metadataDocumentHintSignal);
|
||||
|
|
@ -699,25 +948,25 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
followupSeed.metadataAmbiguityDetected &&
|
||||
(followupSeed.metadataAmbiguityEntitySets.length === 0 ||
|
||||
metadataEntitySetsSuggestDocumentLane(followupSeed.metadataAmbiguityEntitySets)) &&
|
||||
followupSeed.counterparty &&
|
||||
metadataLaneCarryoverAvailable &&
|
||||
!rawLifecycleSignal &&
|
||||
!rawValueFlowSignal &&
|
||||
metadataDocumentHintSignal);
|
||||
const metadataGroundedMovementFollowupApplicable = Boolean(followupSeed.pilotScope === "metadata_inspection_v1" &&
|
||||
followupSeed.metadataRouteFamily === "movement_evidence" &&
|
||||
!followupSeed.metadataAmbiguityDetected &&
|
||||
followupSeed.counterparty &&
|
||||
metadataLaneCarryoverAvailable &&
|
||||
!rawLifecycleSignal &&
|
||||
metadataMovementHintSignal);
|
||||
const metadataAmbiguityResolvedMovementFollowupApplicable = Boolean(followupSeed.pilotScope === "metadata_inspection_v1" &&
|
||||
followupSeed.metadataAmbiguityDetected &&
|
||||
(followupSeed.metadataAmbiguityEntitySets.length === 0 ||
|
||||
metadataEntitySetsSuggestMovementLane(followupSeed.metadataAmbiguityEntitySets)) &&
|
||||
followupSeed.counterparty &&
|
||||
metadataLaneCarryoverAvailable &&
|
||||
!rawLifecycleSignal &&
|
||||
metadataMovementHintSignal);
|
||||
const entityResolutionGroundedDocumentFollowupApplicable = Boolean(followupSeed.pilotScope === "entity_resolution_search_v1" &&
|
||||
(followupSeed.counterparty || followupSeed.discoveryEntity) &&
|
||||
(effectiveFollowupCounterparty || followupSeed.discoveryEntity) &&
|
||||
!rawLifecycleSignal &&
|
||||
!rawValueFlowSignal &&
|
||||
!rawMetadataSignal &&
|
||||
|
|
@ -730,7 +979,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
!rawMetadataSignal &&
|
||||
metadataDocumentHintSignal);
|
||||
const entityResolutionGroundedMovementFollowupApplicable = Boolean(followupSeed.pilotScope === "entity_resolution_search_v1" &&
|
||||
(followupSeed.counterparty || followupSeed.discoveryEntity) &&
|
||||
(effectiveFollowupCounterparty || followupSeed.discoveryEntity) &&
|
||||
!rawLifecycleSignal &&
|
||||
!rawValueFlowSignal &&
|
||||
!rawMetadataSignal &&
|
||||
|
|
@ -745,14 +994,14 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
const groundedValueFlowFollowupApplicable = Boolean(rawValueFlowSignal &&
|
||||
!rawLifecycleSignal &&
|
||||
!rawMetadataSignal &&
|
||||
(followupSeed.counterparty || followupSeed.discoveryEntity) &&
|
||||
(effectiveFollowupCounterparty || followupSeed.discoveryEntity) &&
|
||||
(followupSeed.pilotScope === "entity_resolution_search_v1" ||
|
||||
followupSeed.pilotScope === "counterparty_document_evidence_query_documents_v1" ||
|
||||
followupSeed.pilotScope === "counterparty_movement_evidence_query_movements_v1" ||
|
||||
followupSeed.pilotScope === "counterparty_value_flow_query_movements_v1" ||
|
||||
followupSeed.pilotScope === "counterparty_supplier_payout_query_movements_v1" ||
|
||||
followupSeed.pilotScope === "counterparty_bidirectional_value_flow_query_movements_v1"));
|
||||
const groundedValueFlowEvidenceSourceApplicable = Boolean((followupSeed.counterparty || followupSeed.discoveryEntity) &&
|
||||
const groundedValueFlowEvidenceSourceApplicable = Boolean((effectiveFollowupCounterparty || followupSeed.discoveryEntity) &&
|
||||
!rawLifecycleSignal &&
|
||||
!rawValueFlowSignal &&
|
||||
!rawMetadataSignal &&
|
||||
|
|
@ -766,22 +1015,36 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
const valueFlowGroundedDocumentFollowupApplicable = Boolean(groundedValueFlowEvidenceSourceApplicable && metadataDocumentHintSignal);
|
||||
const valueFlowGroundedMovementFollowupApplicable = Boolean(groundedValueFlowEvidenceSourceApplicable && metadataMovementHintSignal);
|
||||
const documentEvidenceGroundedMovementFollowupApplicable = Boolean(followupSeed.pilotScope === "counterparty_document_evidence_query_documents_v1" &&
|
||||
(followupSeed.counterparty || followupSeed.discoveryEntity) &&
|
||||
(effectiveFollowupCounterparty || followupSeed.discoveryEntity) &&
|
||||
!rawLifecycleSignal &&
|
||||
!rawValueFlowSignal &&
|
||||
!rawMetadataSignal &&
|
||||
metadataMovementHintSignal);
|
||||
const movementEvidenceGroundedDocumentFollowupApplicable = Boolean(followupSeed.pilotScope === "counterparty_movement_evidence_query_movements_v1" &&
|
||||
(followupSeed.counterparty || followupSeed.discoveryEntity) &&
|
||||
(effectiveFollowupCounterparty || followupSeed.discoveryEntity) &&
|
||||
!rawLifecycleSignal &&
|
||||
!rawValueFlowSignal &&
|
||||
!rawMetadataSignal &&
|
||||
metadataDocumentHintSignal);
|
||||
const metadataScopedMovementEvidenceToDocumentFollowupApplicable = Boolean(followupSeed.pilotScope === "counterparty_movement_evidence_query_movements_v1" &&
|
||||
followupSeed.subjectResolutionOptional &&
|
||||
!effectiveFollowupCounterparty &&
|
||||
metadataLaneCarryoverAvailable &&
|
||||
!rawLifecycleSignal &&
|
||||
!rawValueFlowSignal &&
|
||||
metadataDocumentHintSignal);
|
||||
const metadataScopedDocumentEvidenceToMovementFollowupApplicable = Boolean(followupSeed.pilotScope === "counterparty_document_evidence_query_documents_v1" &&
|
||||
followupSeed.subjectResolutionOptional &&
|
||||
!effectiveFollowupCounterparty &&
|
||||
metadataLaneCarryoverAvailable &&
|
||||
!rawLifecycleSignal &&
|
||||
!rawValueFlowSignal &&
|
||||
metadataMovementHintSignal);
|
||||
const metadataGroundedLaneContinuationApplicable = Boolean(followupSeed.pilotScope === "metadata_inspection_v1" &&
|
||||
(followupSeed.metadataRouteFamily === "document_evidence" ||
|
||||
followupSeed.metadataRouteFamily === "movement_evidence") &&
|
||||
!followupSeed.metadataAmbiguityDetected &&
|
||||
followupSeed.counterparty &&
|
||||
metadataLaneCarryoverAvailable &&
|
||||
!rawLifecycleSignal &&
|
||||
!rawValueFlowSignal &&
|
||||
!rawMetadataSignal &&
|
||||
|
|
@ -800,7 +1063,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
const metadataAmbiguityCollapsedDocumentLaneContinuationApplicable = Boolean(followupSeed.pilotScope === "metadata_inspection_v1" &&
|
||||
followupSeed.metadataAmbiguityDetected &&
|
||||
metadataAmbiguityCollapsesToDocumentLane(followupSeed.metadataAmbiguityEntitySets) &&
|
||||
followupSeed.counterparty &&
|
||||
metadataLaneCarryoverAvailable &&
|
||||
!rawLifecycleSignal &&
|
||||
!rawValueFlowSignal &&
|
||||
!rawMetadataSignal &&
|
||||
|
|
@ -810,7 +1073,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
const metadataAmbiguityCollapsedMovementLaneContinuationApplicable = Boolean(followupSeed.pilotScope === "metadata_inspection_v1" &&
|
||||
followupSeed.metadataAmbiguityDetected &&
|
||||
metadataAmbiguityCollapsesToMovementLane(followupSeed.metadataAmbiguityEntitySets) &&
|
||||
followupSeed.counterparty &&
|
||||
metadataLaneCarryoverAvailable &&
|
||||
!rawLifecycleSignal &&
|
||||
!rawValueFlowSignal &&
|
||||
!rawMetadataSignal &&
|
||||
|
|
@ -821,7 +1084,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
followupSeed.metadataAmbiguityDetected &&
|
||||
!metadataAmbiguityCollapsesToDocumentLane(followupSeed.metadataAmbiguityEntitySets) &&
|
||||
!metadataAmbiguityCollapsesToMovementLane(followupSeed.metadataAmbiguityEntitySets) &&
|
||||
followupSeed.counterparty &&
|
||||
metadataLaneCarryoverAvailable &&
|
||||
!rawLifecycleSignal &&
|
||||
!rawValueFlowSignal &&
|
||||
!rawMetadataSignal &&
|
||||
|
|
@ -830,18 +1093,38 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
hasMetadataDownstreamContinuationSignal(rawText));
|
||||
const metadataGroundedDocumentLaneApplicable = metadataGroundedDocumentFollowupApplicable ||
|
||||
metadataAmbiguityResolvedDocumentFollowupApplicable ||
|
||||
(followupSeed.subjectResolutionOptional &&
|
||||
!effectiveFollowupCounterparty &&
|
||||
metadataLaneCarryoverAvailable &&
|
||||
!rawLifecycleSignal &&
|
||||
!rawValueFlowSignal &&
|
||||
!currentTurnMovementLaneSignal &&
|
||||
(!rawMetadataSignal || currentTurnDocumentLaneSignal) &&
|
||||
(currentTurnDocumentLaneSignal ||
|
||||
(followupSeed.domain === "documents" && followupSeed.action === "list_documents"))) ||
|
||||
entityResolutionGroundedDocumentFollowupApplicable ||
|
||||
entityResolutionClarifiedDocumentFollowupApplicable ||
|
||||
valueFlowGroundedDocumentFollowupApplicable ||
|
||||
movementEvidenceGroundedDocumentFollowupApplicable ||
|
||||
metadataScopedMovementEvidenceToDocumentFollowupApplicable ||
|
||||
(metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "document_evidence") ||
|
||||
metadataAmbiguityCollapsedDocumentLaneContinuationApplicable;
|
||||
const metadataGroundedMovementLaneApplicable = metadataGroundedMovementFollowupApplicable ||
|
||||
metadataAmbiguityResolvedMovementFollowupApplicable ||
|
||||
(followupSeed.subjectResolutionOptional &&
|
||||
!effectiveFollowupCounterparty &&
|
||||
metadataLaneCarryoverAvailable &&
|
||||
!rawLifecycleSignal &&
|
||||
!rawValueFlowSignal &&
|
||||
!currentTurnDocumentLaneSignal &&
|
||||
(!rawMetadataSignal || currentTurnMovementLaneSignal) &&
|
||||
(currentTurnMovementLaneSignal ||
|
||||
(followupSeed.domain === "movements" && followupSeed.action === "list_movements"))) ||
|
||||
entityResolutionGroundedMovementFollowupApplicable ||
|
||||
entityResolutionClarifiedMovementFollowupApplicable ||
|
||||
valueFlowGroundedMovementFollowupApplicable ||
|
||||
documentEvidenceGroundedMovementFollowupApplicable ||
|
||||
metadataScopedDocumentEvidenceToMovementFollowupApplicable ||
|
||||
(metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "movement_evidence") ||
|
||||
metadataAmbiguityCollapsedMovementLaneContinuationApplicable;
|
||||
const effectiveMetadataFollowupSeedApplicable = metadataFollowupSeedApplicable &&
|
||||
|
|
@ -904,16 +1187,50 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
!metadataGroundedDocumentLaneApplicable &&
|
||||
!metadataGroundedMovementLaneApplicable
|
||||
});
|
||||
const groundedFollowupEntity = followupSeed.counterparty ?? followupSeed.discoveryEntity;
|
||||
const explicitCurrentCounterpartyCandidate = normalizedPredecomposeCounterparty && !isReferentialEntityPlaceholder(normalizedPredecomposeCounterparty)
|
||||
? normalizedPredecomposeCounterparty
|
||||
: null;
|
||||
const explicitCurrentCounterpartyOverridesFollowupEntity = Boolean(explicitCurrentCounterpartyCandidate &&
|
||||
(effectiveFollowupCounterparty || followupSeed.discoveryEntity) &&
|
||||
!sameScopedName(explicitCurrentCounterpartyCandidate, effectiveFollowupCounterparty ?? followupSeed.discoveryEntity) &&
|
||||
(valueFlowSignal ||
|
||||
lifecycleSignal ||
|
||||
metadataGroundedDocumentLaneApplicable ||
|
||||
metadataGroundedMovementLaneApplicable));
|
||||
const metadataLaneScopeHint = rawEntitySearchOverridesStaleScope
|
||||
? null
|
||||
: explicitCurrentCounterpartyOverridesFollowupEntity
|
||||
? null
|
||||
: rawMetadataScopeHint ??
|
||||
followupSeed.metadataScopeHint ??
|
||||
followupSeed.discoveryEntity ??
|
||||
followupSeed.metadataSelectedEntitySet ??
|
||||
null;
|
||||
const metadataScopedLaneWithoutSubject = Boolean((metadataGroundedMovementLaneApplicable || metadataGroundedDocumentLaneApplicable) &&
|
||||
!effectiveFollowupCounterparty &&
|
||||
metadataLaneCarryoverAvailable);
|
||||
const groundedFollowupEntity = metadataScopedLaneWithoutSubject
|
||||
? null
|
||||
: rawEntitySearchOverridesStaleScope
|
||||
? null
|
||||
: explicitCurrentCounterpartyOverridesFollowupEntity
|
||||
? null
|
||||
: effectiveFollowupCounterparty ?? followupSeed.discoveryEntity;
|
||||
const entityCandidates = entityResolutionSignal ? [] : [];
|
||||
if (entityResolutionSignal) {
|
||||
pushNormalizedEntityResolutionCandidate(entityCandidates, entityResolutionClarificationCandidate);
|
||||
pushNormalizedEntityResolutionCandidate(entityCandidates, rawEntityCandidate);
|
||||
for (const candidate of collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates)) {
|
||||
pushNormalizedEntityResolutionCandidate(entityCandidates, candidate);
|
||||
if (!rawEntitySearchOverridesStaleScope || sameScopedName(candidate, rawEntityCandidate)) {
|
||||
pushNormalizedEntityResolutionCandidate(entityCandidates, candidate);
|
||||
}
|
||||
}
|
||||
if (!rawEntitySearchOverridesStaleScope || sameScopedName(normalizedPredecomposeCounterparty, rawEntityCandidate)) {
|
||||
pushNormalizedEntityResolutionCandidate(entityCandidates, normalizedPredecomposeCounterparty);
|
||||
}
|
||||
if (!rawEntitySearchOverridesStaleScope) {
|
||||
pushNormalizedEntityResolutionCandidate(entityCandidates, effectiveFollowupCounterparty);
|
||||
}
|
||||
pushNormalizedEntityResolutionCandidate(entityCandidates, normalizedPredecomposeCounterparty);
|
||||
pushNormalizedEntityResolutionCandidate(entityCandidates, followupSeed.counterparty);
|
||||
}
|
||||
else {
|
||||
if (groundedFollowupEntity) {
|
||||
|
|
@ -924,16 +1241,20 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
}
|
||||
pushScopedEntityCandidate(entityCandidates, normalizedPredecomposeCounterparty, groundedFollowupEntity);
|
||||
if (!groundedFollowupEntity) {
|
||||
pushScopedEntityCandidate(entityCandidates, followupSeed.counterparty, null);
|
||||
pushScopedEntityCandidate(entityCandidates, followupSeed.discoveryEntity, null);
|
||||
pushScopedEntityCandidate(entityCandidates, effectiveFollowupCounterparty, null);
|
||||
if (!metadataScopedLaneWithoutSubject) {
|
||||
pushScopedEntityCandidate(entityCandidates, followupSeed.discoveryEntity, null);
|
||||
}
|
||||
}
|
||||
pushScopedEntityCandidate(entityCandidates, rawEntityCandidate, groundedFollowupEntity);
|
||||
}
|
||||
if ((rawMetadataSignal || metadataFollowupSeedApplicable) && !groundedFollowupEntity) {
|
||||
if ((rawMetadataSignal || metadataFollowupSeedApplicable) &&
|
||||
!groundedFollowupEntity &&
|
||||
!metadataScopedLaneWithoutSubject) {
|
||||
pushUnique(entityCandidates, followupSeed.discoveryEntity);
|
||||
pushUnique(entityCandidates, rawMetadataScopeHint);
|
||||
}
|
||||
const openScopeValueFlowWithoutCounterparty = valueFlowSignal && !normalizedPredecomposeCounterparty && !followupSeed.counterparty;
|
||||
const openScopeValueFlowWithoutCounterparty = valueFlowSignal && !normalizedPredecomposeCounterparty && !effectiveFollowupCounterparty;
|
||||
const valueFlowOrganizationStaysScope = openScopeValueFlowWithoutCounterparty &&
|
||||
Boolean(bidirectionalValueFlowSignal ||
|
||||
hasValueRankingSignal(rawText) ||
|
||||
|
|
@ -941,13 +1262,34 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
explicitOrganizationScopeSignal ||
|
||||
organizationClarificationFollowupApplicable ||
|
||||
followupSeed.organization);
|
||||
const openScopeValueFlowWithoutResolvedCounterparty = Boolean(valueFlowSignal && !normalizedPredecomposeCounterparty && !effectiveFollowupCounterparty);
|
||||
if (openScopeValueFlowWithoutCounterparty && !valueFlowOrganizationStaysScope) {
|
||||
pushUnique(entityCandidates, predecomposeEntities.organization);
|
||||
pushUnique(entityCandidates, followupSeed.organization);
|
||||
}
|
||||
const explicitOrganizationScope = valueFlowOrganizationStaysScope || !openScopeValueFlowWithoutCounterparty
|
||||
? currentTurnOrganizationScope ?? followupSeed.organization
|
||||
: null;
|
||||
const explicitOrganizationScope = rawEntitySearchOverridesStaleScope && !currentTurnFreshOrganizationScope
|
||||
? null
|
||||
: valueFlowOrganizationStaysScope || !openScopeValueFlowWithoutCounterparty
|
||||
? (rawEntitySearchOverridesStaleScope ? currentTurnFreshOrganizationScope : currentTurnOrganizationScope) ??
|
||||
followupSeed.organization
|
||||
: null;
|
||||
if (explicitCurrentCounterpartyCandidate &&
|
||||
(valueFlowSignal || lifecycleSignal || metadataGroundedDocumentLaneApplicable || metadataGroundedMovementLaneApplicable)) {
|
||||
for (let index = entityCandidates.length - 1; index >= 0; index -= 1) {
|
||||
const candidate = entityCandidates[index];
|
||||
if (!candidate || sameScopedName(candidate, explicitCurrentCounterpartyCandidate)) {
|
||||
continue;
|
||||
}
|
||||
if ((metadataLaneScopeHint && sameScopedName(candidate, metadataLaneScopeHint)) ||
|
||||
(explicitOrganizationScope && sameScopedName(candidate, explicitOrganizationScope)) ||
|
||||
(followupSeed.organization && sameScopedName(candidate, followupSeed.organization)) ||
|
||||
(effectiveFollowupCounterparty &&
|
||||
!sameScopedName(effectiveFollowupCounterparty, explicitCurrentCounterpartyCandidate) &&
|
||||
sameScopedName(candidate, effectiveFollowupCounterparty))) {
|
||||
entityCandidates.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (valueFlowOrganizationStaysScope && explicitOrganizationScope) {
|
||||
for (let index = entityCandidates.length - 1; index >= 0; index -= 1) {
|
||||
if (entityCandidates[index] === explicitOrganizationScope) {
|
||||
|
|
@ -955,14 +1297,39 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
}
|
||||
}
|
||||
}
|
||||
const clarificationLoopStillNeedsPeriod = Boolean(followupSeed.loopStatus === "awaiting_clarification" && followupSeed.loopPendingAxes.includes("period"));
|
||||
const currentTurnCarriesExplicitPeriod = Boolean(explicitDateScopeLiteralDetected ||
|
||||
rawDateScope ||
|
||||
relativeCurrentDateHintDetected ||
|
||||
(predecomposeDateScope && !isImplicitCurrentDateScope(predecomposeDateScope)));
|
||||
const suppressImplicitCurrentDateScope = Boolean(!currentTurnCarriesExplicitPeriod &&
|
||||
(clarificationLoopStillNeedsPeriod ||
|
||||
openScopeValueFlowWithoutResolvedCounterparty ||
|
||||
(valueFlowOrganizationStaysScope && (Boolean(followupSeed.rankingNeed) || bidirectionalValueFlowSignal))));
|
||||
const normalizedPredecomposeDateScope = (rawEntitySearchOverridesStaleScope && !currentTurnCarriesExplicitPeriod) ||
|
||||
(suppressImplicitCurrentDateScope && isImplicitCurrentDateScope(predecomposeDateScope))
|
||||
? null
|
||||
: predecomposeDateScope;
|
||||
const normalizedAssistantTurnMeaningDateScope = rawEntitySearchOverridesStaleScope ||
|
||||
(suppressImplicitCurrentDateScope && isImplicitCurrentDateScope(assistantTurnMeaningDateScope))
|
||||
? null
|
||||
: assistantTurnMeaningDateScope;
|
||||
const normalizedFollowupDateScope = rawEntitySearchOverridesStaleScope ||
|
||||
(suppressImplicitCurrentDateScope && isImplicitCurrentDateScope(followupSeed.dateScope))
|
||||
? null
|
||||
: followupSeed.dateScope;
|
||||
const explicitDateScope = rawAllTimeScopeSignal
|
||||
? null
|
||||
: assistantTurnMeaningDateScope ?? predecomposeDateScope ?? rawDateScope ?? followupSeed.dateScope;
|
||||
: normalizedAssistantTurnMeaningDateScope ??
|
||||
normalizedPredecomposeDateScope ??
|
||||
rawDateScope ??
|
||||
normalizedFollowupDateScope;
|
||||
const followupDateScopeApplied = Boolean(!rawAllTimeScopeSignal &&
|
||||
!assistantTurnMeaningDateScope &&
|
||||
!predecomposeDateScope &&
|
||||
!normalizedAssistantTurnMeaningDateScope &&
|
||||
!normalizedPredecomposeDateScope &&
|
||||
!rawDateScope &&
|
||||
followupSeed.dateScope);
|
||||
normalizedFollowupDateScope);
|
||||
const clarificationLoopSeedApplied = Boolean(followupSeed.loopStatus === "awaiting_clarification" && followupSeed.loopSelectedChainId);
|
||||
const turnMeaning = {
|
||||
asked_domain_family: lifecycleSignal
|
||||
? "counterparty_lifecycle"
|
||||
|
|
@ -995,13 +1362,17 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
? metadataActionFromRawText(rawText) ?? seededAction
|
||||
: rawAction ?? seededAction,
|
||||
asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis,
|
||||
seeded_ranking_need: valueFlowSignal && followupSeed.rankingNeed ? followupSeed.rankingNeed : undefined,
|
||||
seeded_ranking_need: valueFlowSignal && followupSeed.rankingNeed && !rawEntitySearchOverridesStaleScope
|
||||
? followupSeed.rankingNeed
|
||||
: undefined,
|
||||
explicit_entity_candidates: entityCandidates,
|
||||
metadata_ambiguity_entity_sets: metadataAmbiguityLaneClarificationApplicable && followupSeed.metadataAmbiguityEntitySets.length > 0
|
||||
? followupSeed.metadataAmbiguityEntitySets
|
||||
: undefined,
|
||||
metadata_scope_hint: metadataLaneScopeHint,
|
||||
explicit_organization_scope: explicitOrganizationScope,
|
||||
explicit_date_scope: explicitDateScope,
|
||||
subject_resolution_optional: metadataScopedLaneWithoutSubject || undefined,
|
||||
unsupported_but_understood_family: unsupported ??
|
||||
(lifecycleSignal
|
||||
? "counterparty_lifecycle"
|
||||
|
|
@ -1055,12 +1426,18 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
if ((turnMeaning.metadata_ambiguity_entity_sets?.length ?? 0) > 0) {
|
||||
cleanTurnMeaning.metadata_ambiguity_entity_sets = turnMeaning.metadata_ambiguity_entity_sets;
|
||||
}
|
||||
if (toNonEmptyString(turnMeaning.metadata_scope_hint)) {
|
||||
cleanTurnMeaning.metadata_scope_hint = turnMeaning.metadata_scope_hint;
|
||||
}
|
||||
if (toNonEmptyString(turnMeaning.explicit_organization_scope)) {
|
||||
cleanTurnMeaning.explicit_organization_scope = turnMeaning.explicit_organization_scope;
|
||||
}
|
||||
if (toNonEmptyString(turnMeaning.explicit_date_scope)) {
|
||||
cleanTurnMeaning.explicit_date_scope = turnMeaning.explicit_date_scope;
|
||||
}
|
||||
if (turnMeaning.subject_resolution_optional) {
|
||||
cleanTurnMeaning.subject_resolution_optional = true;
|
||||
}
|
||||
if (toNonEmptyString(turnMeaning.unsupported_but_understood_family)) {
|
||||
cleanTurnMeaning.unsupported_but_understood_family = turnMeaning.unsupported_but_understood_family;
|
||||
}
|
||||
|
|
@ -1084,34 +1461,37 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
groundedValueFlowFollowupApplicable,
|
||||
forceDiscoveryOverExplicitIntent: Boolean(entityResolutionClarificationCandidate) ||
|
||||
organizationClarificationFollowupApplicable ||
|
||||
periodClarificationFollowupApplicable ||
|
||||
metadataAmbiguityLaneClarificationApplicable ||
|
||||
metadataGroundedMovementLaneApplicable ||
|
||||
metadataGroundedDocumentLaneApplicable ||
|
||||
groundedValueFlowFollowupApplicable
|
||||
});
|
||||
const hasTurnMeaning = Object.keys(cleanTurnMeaning).length > 0;
|
||||
const sourceSignal = assistantTurnMeaning
|
||||
? "assistant_turn_meaning"
|
||||
: followupDiscoverySeedApplicable ||
|
||||
Boolean(entityResolutionClarificationCandidate) ||
|
||||
effectiveMetadataFollowupSeedApplicable ||
|
||||
metadataAmbiguityLaneClarificationApplicable
|
||||
? "followup_context"
|
||||
: metadataGroundedMovementLaneApplicable
|
||||
const sourceSignal = rawEntitySearchOverridesStaleScope
|
||||
? "raw_text"
|
||||
: assistantTurnMeaning
|
||||
? "assistant_turn_meaning"
|
||||
: followupDiscoverySeedApplicable ||
|
||||
Boolean(entityResolutionClarificationCandidate) ||
|
||||
effectiveMetadataFollowupSeedApplicable ||
|
||||
metadataAmbiguityLaneClarificationApplicable
|
||||
? "followup_context"
|
||||
: metadataGroundedDocumentLaneApplicable
|
||||
: metadataGroundedMovementLaneApplicable
|
||||
? "followup_context"
|
||||
: predecomposeContract
|
||||
? "predecompose_contract"
|
||||
: lifecycleSignal
|
||||
? "raw_text"
|
||||
: valueFlowSignal
|
||||
: metadataGroundedDocumentLaneApplicable
|
||||
? "followup_context"
|
||||
: predecomposeContract
|
||||
? "predecompose_contract"
|
||||
: lifecycleSignal
|
||||
? "raw_text"
|
||||
: entityResolutionSignal
|
||||
: valueFlowSignal
|
||||
? "raw_text"
|
||||
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
||||
: entityResolutionSignal
|
||||
? "raw_text"
|
||||
: "none";
|
||||
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
||||
? "raw_text"
|
||||
: "none";
|
||||
if (lifecycleSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_lifecycle_signal_detected");
|
||||
}
|
||||
|
|
@ -1136,6 +1516,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
if (organizationClarificationFollowupApplicable) {
|
||||
pushReason(reasonCodes, "mcp_discovery_organization_clarification_followup_from_followup_context");
|
||||
}
|
||||
if (periodClarificationFollowupApplicable) {
|
||||
pushReason(reasonCodes, "mcp_discovery_period_clarification_followup_from_followup_context");
|
||||
}
|
||||
if (payoutSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_payout_signal_detected");
|
||||
}
|
||||
|
|
@ -1151,6 +1534,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
if (followupDiscoverySeedApplicable) {
|
||||
pushReason(reasonCodes, "mcp_discovery_seeded_from_followup_context");
|
||||
}
|
||||
if (clarificationLoopSeedApplied) {
|
||||
pushReason(reasonCodes, "mcp_discovery_resumed_from_saved_loop_state");
|
||||
}
|
||||
if (effectiveMetadataFollowupSeedApplicable) {
|
||||
pushReason(reasonCodes, "mcp_discovery_metadata_seeded_from_followup_context");
|
||||
}
|
||||
|
|
@ -1166,6 +1552,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
if (metadataAmbiguityResolvedMovementFollowupApplicable) {
|
||||
pushReason(reasonCodes, "mcp_discovery_metadata_ambiguity_resolved_to_movement_lane");
|
||||
}
|
||||
if (metadataScopedLaneWithoutSubject) {
|
||||
pushReason(reasonCodes, "mcp_discovery_metadata_scoped_lane_without_subject");
|
||||
}
|
||||
if (entityResolutionGroundedDocumentFollowupApplicable) {
|
||||
pushReason(reasonCodes, "mcp_discovery_entity_resolution_grounded_document_followup");
|
||||
}
|
||||
|
|
@ -1193,6 +1582,12 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
if (movementEvidenceGroundedDocumentFollowupApplicable) {
|
||||
pushReason(reasonCodes, "mcp_discovery_movement_evidence_grounded_document_followup");
|
||||
}
|
||||
if (metadataScopedMovementEvidenceToDocumentFollowupApplicable) {
|
||||
pushReason(reasonCodes, "mcp_discovery_metadata_scoped_movement_to_document_followup");
|
||||
}
|
||||
if (metadataScopedDocumentEvidenceToMovementFollowupApplicable) {
|
||||
pushReason(reasonCodes, "mcp_discovery_metadata_scoped_document_to_movement_followup");
|
||||
}
|
||||
if (metadataGroundedLaneContinuationApplicable) {
|
||||
pushReason(reasonCodes, "mcp_discovery_metadata_grounded_lane_continuation");
|
||||
}
|
||||
|
|
@ -1215,7 +1610,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
normalizedPredecomposeCounterparty) {
|
||||
pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose");
|
||||
}
|
||||
if (followupSeed.counterparty) {
|
||||
if (effectiveFollowupCounterparty && !rawEntitySearchOverridesStaleScope) {
|
||||
pushReason(reasonCodes, "mcp_discovery_counterparty_from_followup_context");
|
||||
}
|
||||
if (followupDateScopeApplied) {
|
||||
|
|
|
|||
|
|
@ -495,12 +495,35 @@ function createAssistantRoutePolicy(deps) {
|
|||
!effectiveAddressFollowupSignal &&
|
||||
resolvedModeDetection.mode === "unsupported" &&
|
||||
resolvedIntentResolution.intent === "unknown");
|
||||
const groundedValueFlowFollowupContextDetected = Boolean(followupContext &&
|
||||
[
|
||||
"counterparty_value_flow_query_movements_v1",
|
||||
"counterparty_supplier_payout_query_movements_v1",
|
||||
"counterparty_bidirectional_value_flow_query_movements_v1"
|
||||
].includes(String(toNonEmptyString(followupContext?.previous_discovery_pilot_scope) ?? "")) &&
|
||||
!dangerOrCoercionSignal &&
|
||||
(toNonEmptyString(assistantTurnMeaning?.asked_domain_family) === "counterparty_value" ||
|
||||
[
|
||||
"turnover",
|
||||
"payout",
|
||||
"net_value_flow"
|
||||
].includes(String(toNonEmptyString(assistantTurnMeaning?.asked_action_family) ?? "")) ||
|
||||
/(?:нетто|сальдо|сколько\s+мы\s+(?:получили|заплатили)|incoming|outgoing)/iu.test(analyticsSample)));
|
||||
const baseToolGatePreservesAddressLane = Boolean(baseToolGate?.runAddressLane &&
|
||||
["address_intent_resolver_detected", "address_mode_classifier_detected", "address_signal_detected", "llm_canonical_data_signal_detected"].includes(String(baseToolGate?.reason ?? "")));
|
||||
[
|
||||
"address_intent_resolver_detected",
|
||||
"address_mode_classifier_detected",
|
||||
"address_signal_detected",
|
||||
"llm_canonical_data_signal_detected"
|
||||
].includes(String(baseToolGate?.reason ?? ""))) ||
|
||||
Boolean(baseToolGate?.runAddressLane &&
|
||||
String(baseToolGate?.reason ?? "") === "followup_context_detected" &&
|
||||
groundedValueFlowFollowupContextDetected);
|
||||
const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate &&
|
||||
deterministicNonDomainGuard &&
|
||||
(llmFirstUnsupportedCandidate || llmContractMode === null) &&
|
||||
!baseToolGatePreservesAddressLane &&
|
||||
!groundedValueFlowFollowupContextDetected &&
|
||||
!protectedInventoryShortFollowup &&
|
||||
!organizationClarificationContinuationDetected);
|
||||
const lastAddressAssistantDebug = sessionItems
|
||||
|
|
@ -664,9 +687,11 @@ function createAssistantRoutePolicy(deps) {
|
|||
const unsupportedCurrentTurnMeaningBoundary = Boolean(assistantTurnMeaning?.unsupported_but_understood_family &&
|
||||
assistantTurnMeaning?.stale_replay_forbidden === true &&
|
||||
!turnMeaningIntentCandidate &&
|
||||
!aggregateBusinessAnalyticsSignal &&
|
||||
!dataScopeMetaQuery &&
|
||||
!capabilityMetaQuery &&
|
||||
!dangerOrCoercionSignal &&
|
||||
!groundedValueFlowFollowupContextDetected &&
|
||||
!organizationClarificationContinuationDetected);
|
||||
if (unsupportedCurrentTurnMeaningBoundary) {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1932,7 +1932,7 @@ function textMojibakeScoreForAddress(value) {
|
|||
const source = String(value ?? "");
|
||||
const cyrillic = (source.match(/[А-Яа-яЁё]/g) ?? []).length;
|
||||
const latin = (source.match(/[A-Za-z]/g) ?? []).length;
|
||||
const hardMarkers = (source.match(/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ<EFBFBD>?’“”•–—™љ›њќћџ]/g) ?? []).length;
|
||||
const hardMarkers = (source.match(/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ\uFFFD?’“”•–—™љ›њќћџ]/g) ?? []).length;
|
||||
const pairMarkers = (source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length;
|
||||
const doubleEncodedMarkers = (source.match(/(?:Г[Ђ-џ]|В[Ђ-џ]|Ã.|Â.)/gu) ?? []).length;
|
||||
return cyrillic + latin - hardMarkers * 3 - pairMarkers * 2 - doubleEncodedMarkers * 2;
|
||||
|
|
@ -1942,7 +1942,7 @@ function looksLikeMojibakeForAddress(value) {
|
|||
if (!source.trim()) {
|
||||
return false;
|
||||
}
|
||||
if (/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ<EFBFBD>?’“”•–—™љ›њќћџ]/.test(source)) {
|
||||
if (/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ\uFFFD?’“”•–—™љ›њќћџ]/.test(source)) {
|
||||
return true;
|
||||
}
|
||||
if ((source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length >= 2) {
|
||||
|
|
@ -2173,7 +2173,7 @@ function normalizeCounterpartyForFollowupMatch(value) {
|
|||
return compactWhitespace(repairAddressMojibake(String(value ?? ""))
|
||||
.toLowerCase()
|
||||
.replace(/ё/g, "е")
|
||||
.replace(/[«»"'`“”„’<EFBFBD>?]/g, " ")
|
||||
.replace(/[«»"'`“”„’\uFFFD?]/g, " ")
|
||||
.replace(/[^a-zа-я0-9\s._-]+/giu, " "));
|
||||
}
|
||||
function normalizeCounterpartyTokenForFollowupMatch(value) {
|
||||
|
|
@ -2219,7 +2219,7 @@ function extractDisplayedAddressEntityCandidates(replyText, entityType = "unknow
|
|||
if (parts.length >= 2 && /^\d{4}-\d{2}-\d{2}/.test(parts[0] ?? "")) {
|
||||
counterpartyCandidate = parts[1] ?? counterpartyCandidate;
|
||||
}
|
||||
const cleanedCandidate = compactWhitespace(counterpartyCandidate.replace(/^["'«»“”„`’<EFBFBD>?]+|["'«»“”„`’<>?]+$/gu, ""));
|
||||
const cleanedCandidate = compactWhitespace(counterpartyCandidate.replace(/^["'«»“”„`’\uFFFD?]+|["'«»“”„`’\uFFFD?]+$/gu, ""));
|
||||
if (!cleanedCandidate || cleanedCandidate.length < 2) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -2483,11 +2483,19 @@ function findRecentInventoryRootFrame(items) {
|
|||
const ADDRESS_FOLLOWUP_OFFER_BY_INTENT = {
|
||||
list_documents_by_counterparty: ["bank_operations_by_counterparty", "list_contracts_by_counterparty"],
|
||||
bank_operations_by_counterparty: ["list_documents_by_counterparty", "list_contracts_by_counterparty"],
|
||||
list_contracts_by_counterparty: ["list_documents_by_contract", "bank_operations_by_contract"],
|
||||
list_documents_by_contract: ["bank_operations_by_contract"],
|
||||
bank_operations_by_contract: ["list_documents_by_contract"],
|
||||
open_items_by_counterparty_or_contract: ["list_documents_by_counterparty", "bank_operations_by_counterparty"]
|
||||
};
|
||||
function resolveAddressFollowupSuggestedIntents(intent, anchorType) {
|
||||
if (intent === "list_contracts_by_counterparty") {
|
||||
if (anchorType === "contract") {
|
||||
return ["list_documents_by_contract", "bank_operations_by_contract"];
|
||||
}
|
||||
return ["list_documents_by_counterparty", "bank_operations_by_counterparty"];
|
||||
}
|
||||
return ADDRESS_FOLLOWUP_OFFER_BY_INTENT[intent] ?? null;
|
||||
}
|
||||
function buildAddressFollowupOffer(addressDebug) {
|
||||
if (!isAddressLaneDebugPayload(addressDebug)) {
|
||||
return null;
|
||||
|
|
@ -2496,11 +2504,11 @@ function buildAddressFollowupOffer(addressDebug) {
|
|||
if (!intent) {
|
||||
return null;
|
||||
}
|
||||
const suggestedIntents = ADDRESS_FOLLOWUP_OFFER_BY_INTENT[intent];
|
||||
const anchorContext = (0, assistantContinuityPolicy_1.resolveAddressDebugAnchorContext)(addressDebug, toNonEmptyString);
|
||||
const suggestedIntents = resolveAddressFollowupSuggestedIntents(intent, anchorContext.anchorType);
|
||||
if (!Array.isArray(suggestedIntents) || suggestedIntents.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const anchorContext = (0, assistantContinuityPolicy_1.resolveAddressDebugAnchorContext)(addressDebug, toNonEmptyString);
|
||||
return {
|
||||
enabled: true,
|
||||
source_intent: intent,
|
||||
|
|
@ -3260,6 +3268,11 @@ function hasSameDateAccountFollowupSignalForPredecompose(text) {
|
|||
/(?:^|\s)по\s+\d{2}(?:[.,]\d{1,2})?(?=$|[\s,.;:!?])/iu.test(source) ||
|
||||
/\b\d{2}(?:[.,]\d{1,2})\b/u.test(source));
|
||||
}
|
||||
function isCounterpartyDrilldownIntentForPredecompose(intent) {
|
||||
return intent === "list_documents_by_counterparty" ||
|
||||
intent === "bank_operations_by_counterparty" ||
|
||||
intent === "list_contracts_by_counterparty";
|
||||
}
|
||||
function hasPredecomposeDiagnosticUncertaintyLead(text) {
|
||||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
|
||||
if (!normalized) {
|
||||
|
|
@ -3478,8 +3491,16 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
|
|||
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" &&
|
||||
const sourceAnchorQuality = evaluateAddressAnchorQuality(repairedSourceMessage || userMessage);
|
||||
const candidateAccountInjectedIntoCounterpartyAnchor = isCounterpartyDrilldownIntentForPredecompose(sourceIntentResolution.intent) &&
|
||||
sourceIntentResolution.intent === candidateIntentResolution.intent &&
|
||||
sourceAnchorQuality.anchorType === "counterparty" &&
|
||||
sourceAnchorQuality.quality >= 2 &&
|
||||
!sourceHasExplicitAccountAnchor &&
|
||||
candidateInjectsAccountAnchor;
|
||||
if (((sourceIntentResolution.intent === "inventory_on_hand_as_of_date" &&
|
||||
candidateIntentResolution.intent === "inventory_on_hand_as_of_date") ||
|
||||
candidateAccountInjectedIntoCounterpartyAnchor) &&
|
||||
!sourceHasExplicitAccountAnchor &&
|
||||
candidateInjectsAccountAnchor) {
|
||||
return attachAddressPredecomposeContract({
|
||||
|
|
@ -3492,10 +3513,9 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
|
|||
reason: "normalized_fragment_rejected_anchor_injection",
|
||||
fallbackRuleHit: null,
|
||||
sanitizedUserMessage,
|
||||
semanticHints: candidateMeta?.semanticHints ?? null
|
||||
semanticHints: null
|
||||
}, userMessage);
|
||||
}
|
||||
const sourceAnchorQuality = evaluateAddressAnchorQuality(repairedSourceMessage || userMessage);
|
||||
const candidateAnchorQuality = evaluateAddressAnchorQuality(candidate);
|
||||
const sameIntentForAnchorSafety = sourceAnchorQuality.intent !== "unknown" && sourceAnchorQuality.intent === candidateAnchorQuality.intent;
|
||||
const sourceSelectedObjectItemAnchorValue = toNonEmptyString((0, addressFilterExtractor_1.extractSelectedObjectQuotedValue)(userMessage)) ??
|
||||
|
|
|
|||
|
|
@ -7,6 +7,78 @@ function createAssistantTransitionPolicy(deps) {
|
|||
function normalizeFollowupText(value) {
|
||||
return deps.compactWhitespace(deps.repairAddressMojibake(String(value ?? "")).toLowerCase()).replace(/ё/g, "е");
|
||||
}
|
||||
function hasBankOperationsPivotCue(text) {
|
||||
const normalized = normalizeFollowupText(text);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return /(?:платеж|платёж|банк|банковск|операц|выписк|поступлен|списан)/iu.test(normalized);
|
||||
}
|
||||
function hasContractsPivotCue(text) {
|
||||
const normalized = normalizeFollowupText(text);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return /(?:РґРѕРіРѕРІРѕСЂ)/iu.test(normalized);
|
||||
}
|
||||
function hasDocumentsPivotCue(text) {
|
||||
const normalized = normalizeFollowupText(text);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return /(?:документ|счет|счёт|накладн|акт)/iu.test(normalized);
|
||||
}
|
||||
function hasReadableBankOperationsPivotCue(text) {
|
||||
const normalized = normalizeFollowupText(text).replace(/ё/g, "е");
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return /(?:платеж|оплат|банк|банковск|операц|поступлен|списан|выписк|перевод|payment|bank|transaction)/iu.test(normalized);
|
||||
}
|
||||
function hasReadableContractsPivotCue(text) {
|
||||
const normalized = normalizeFollowupText(text).replace(/ё/g, "е");
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return /(?:договор|контракт|соглашен|contract|agreement)/iu.test(normalized);
|
||||
}
|
||||
function hasReadableDocumentsPivotCue(text) {
|
||||
const normalized = normalizeFollowupText(text).replace(/ё/g, "е");
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return /(?:документ|счет|счет-фактур|накладн|акт|реализац|document|invoice|receipt)/iu.test(normalized);
|
||||
}
|
||||
function selectSuggestedIntentByPivotCue(suggestedIntents, userMessage, alternateMessage = null) {
|
||||
if (!Array.isArray(suggestedIntents) || suggestedIntents.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const samples = [userMessage, alternateMessage].filter((item) => deps.toNonEmptyString(item));
|
||||
if (samples.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (suggestedIntents.includes("bank_operations_by_counterparty") &&
|
||||
samples.some((sample) => hasBankOperationsPivotCue(sample) || hasReadableBankOperationsPivotCue(sample))) {
|
||||
return "bank_operations_by_counterparty";
|
||||
}
|
||||
if (suggestedIntents.includes("bank_operations_by_contract") &&
|
||||
samples.some((sample) => hasBankOperationsPivotCue(sample) || hasReadableBankOperationsPivotCue(sample))) {
|
||||
return "bank_operations_by_contract";
|
||||
}
|
||||
if (suggestedIntents.includes("list_contracts_by_counterparty") &&
|
||||
samples.some((sample) => hasContractsPivotCue(sample) || hasReadableContractsPivotCue(sample))) {
|
||||
return "list_contracts_by_counterparty";
|
||||
}
|
||||
if (suggestedIntents.includes("list_documents_by_counterparty") &&
|
||||
samples.some((sample) => hasDocumentsPivotCue(sample) || hasReadableDocumentsPivotCue(sample))) {
|
||||
return "list_documents_by_counterparty";
|
||||
}
|
||||
if (suggestedIntents.includes("list_documents_by_contract") &&
|
||||
samples.some((sample) => hasDocumentsPivotCue(sample) || hasReadableDocumentsPivotCue(sample))) {
|
||||
return "list_documents_by_contract";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function hasSelectedObjectInventoryScopeSignal(text) {
|
||||
const normalized = normalizeFollowupText(text);
|
||||
if (!normalized) {
|
||||
|
|
@ -352,6 +424,10 @@ function createAssistantTransitionPolicy(deps) {
|
|||
const carryoverSourceDebug = previousAddressDebug ??
|
||||
(hasOrganizationClarificationContinuation ? lastOrganizationClarificationDebug : null);
|
||||
const followupOffer = carryoverSourceDebug ? deps.buildAddressFollowupOffer(carryoverSourceDebug) : null;
|
||||
const suggestedIntentFromPivotCue = selectSuggestedIntentByPivotCue(Array.isArray(followupOffer?.suggested_intents) ? followupOffer.suggested_intents : [], userMessage, alternateMessage);
|
||||
const hasSuggestedIntentPivotSignal = Boolean(previousAddressDebug) &&
|
||||
Boolean(followupOffer?.enabled) &&
|
||||
Boolean(suggestedIntentFromPivotCue);
|
||||
const hasImplicitContinuationSignal = Boolean(previousAddressDebug) &&
|
||||
Boolean(followupOffer?.enabled) &&
|
||||
(deps.isImplicitAddressContinuationByLlm(userMessage, llmPreDecomposeMeta) ||
|
||||
|
|
@ -423,6 +499,7 @@ function createAssistantTransitionPolicy(deps) {
|
|||
hasAlternateIndexReferenceSignal ||
|
||||
hasOrganizationClarificationContinuation ||
|
||||
hasImplicitContinuationSignal ||
|
||||
hasSuggestedIntentPivotSignal ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
hasInventoryRootTemporalFollowupPrimary ||
|
||||
|
|
@ -477,6 +554,7 @@ function createAssistantTransitionPolicy(deps) {
|
|||
!shortValueFlowRetargetPrimary &&
|
||||
!shortValueFlowRetargetAlternate &&
|
||||
!hasImplicitContinuationSignal &&
|
||||
!hasSuggestedIntentPivotSignal &&
|
||||
!hasOrganizationClarificationContinuation &&
|
||||
!hasIndexReferenceSignal) {
|
||||
return null;
|
||||
|
|
@ -490,6 +568,7 @@ function createAssistantTransitionPolicy(deps) {
|
|||
!shortValueFlowRetargetPrimary &&
|
||||
!shortValueFlowRetargetAlternate &&
|
||||
!hasImplicitContinuationSignal &&
|
||||
!hasSuggestedIntentPivotSignal &&
|
||||
!hasOrganizationClarificationContinuation &&
|
||||
!hasIndexReferenceSignal) {
|
||||
return null;
|
||||
|
|
@ -508,6 +587,15 @@ function createAssistantTransitionPolicy(deps) {
|
|||
const sourceDiscoveryMetadataAmbiguityEntitySets = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryMetadataAmbiguityEntitySets)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryEntityResolutionStatus = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryEntityResolutionStatus)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryEntityCandidates = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryEntityCandidates)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopStatus = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopStatus)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopSelectedChainId = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopSelectedChainId)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopPendingAxes = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopPendingAxes)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopProvidedAxes = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopProvidedAxes)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopAskedDomainFamily = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopAskedDomainFamily)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopAskedActionFamily = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopAskedActionFamily)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopUnsupportedFamily = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopUnsupportedFamily)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopMetadataScopeHint = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopMetadataScopeHint)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopSubjectResolutionOptional = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopSubjectResolutionOptional)(carryoverSourceDebug);
|
||||
const sourceDiscoveryRankingNeed = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryRankingNeed)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryEntityAmbiguityCandidates = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryEntityAmbiguityCandidates)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const llmExplicitIntent = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
|
||||
|
|
@ -566,9 +654,9 @@ function createAssistantTransitionPolicy(deps) {
|
|||
if (debtRoleSwapIntent) {
|
||||
previousIntent = debtRoleSwapIntent;
|
||||
}
|
||||
if (hasImplicitContinuationSignal) {
|
||||
if (hasImplicitContinuationSignal || hasSuggestedIntentPivotSignal) {
|
||||
const suggestedIntent = Array.isArray(followupOffer?.suggested_intents)
|
||||
? deps.toNonEmptyString(followupOffer.suggested_intents[0])
|
||||
? suggestedIntentFromPivotCue ?? deps.toNonEmptyString(followupOffer.suggested_intents[0])
|
||||
: null;
|
||||
const keepPreviousIntent = shouldKeepPreviousIntentForShortCounterpartyRetargetV2(userMessage, sourceIntent);
|
||||
if (suggestedIntent && !keepPreviousIntent) {
|
||||
|
|
@ -597,6 +685,7 @@ function createAssistantTransitionPolicy(deps) {
|
|||
}
|
||||
hasPrimaryFollowupSignal =
|
||||
deps.hasAddressFollowupContextSignal(userMessage) ||
|
||||
hasSuggestedIntentPivotSignal ||
|
||||
Boolean(debtRoleSwapPrimary) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
|
|
@ -604,6 +693,7 @@ function createAssistantTransitionPolicy(deps) {
|
|||
hasInventoryRootTemporalFollowupPrimary;
|
||||
hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
||||
hasSuggestedIntentPivotSignal ||
|
||||
Boolean(debtRoleSwapAlternate) ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
|
|
@ -614,6 +704,7 @@ function createAssistantTransitionPolicy(deps) {
|
|||
hasPrimaryIndexReferenceSignal ||
|
||||
hasAlternateIndexReferenceSignal ||
|
||||
hasOrganizationClarificationContinuation ||
|
||||
hasSuggestedIntentPivotSignal ||
|
||||
hasImplicitContinuationSignal ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
|
|
@ -746,6 +837,15 @@ function createAssistantTransitionPolicy(deps) {
|
|||
previous_discovery_pilot_scope: sourceDiscoveryPilotScope ?? undefined,
|
||||
previous_discovery_entity_resolution_status: sourceDiscoveryEntityResolutionStatus ?? undefined,
|
||||
previous_discovery_entity_candidates: sourceDiscoveryEntityCandidates.length > 0 ? sourceDiscoveryEntityCandidates : undefined,
|
||||
previous_discovery_loop_status: sourceDiscoveryLoopStatus ?? undefined,
|
||||
previous_discovery_loop_selected_chain_id: sourceDiscoveryLoopSelectedChainId ?? undefined,
|
||||
previous_discovery_loop_pending_axes: sourceDiscoveryLoopPendingAxes.length > 0 ? sourceDiscoveryLoopPendingAxes : undefined,
|
||||
previous_discovery_loop_provided_axes: sourceDiscoveryLoopProvidedAxes.length > 0 ? sourceDiscoveryLoopProvidedAxes : undefined,
|
||||
previous_discovery_loop_asked_domain_family: sourceDiscoveryLoopAskedDomainFamily ?? undefined,
|
||||
previous_discovery_loop_asked_action_family: sourceDiscoveryLoopAskedActionFamily ?? undefined,
|
||||
previous_discovery_loop_unsupported_family: sourceDiscoveryLoopUnsupportedFamily ?? undefined,
|
||||
previous_discovery_loop_metadata_scope_hint: sourceDiscoveryLoopMetadataScopeHint ?? undefined,
|
||||
previous_discovery_loop_subject_resolution_optional: sourceDiscoveryLoopSubjectResolutionOptional || undefined,
|
||||
previous_discovery_ranking_need: sourceDiscoveryRankingNeed ?? undefined,
|
||||
previous_discovery_entity_ambiguity_candidates: sourceDiscoveryEntityAmbiguityCandidates.length > 0
|
||||
? sourceDiscoveryEntityAmbiguityCandidates
|
||||
|
|
@ -771,7 +871,8 @@ function createAssistantTransitionPolicy(deps) {
|
|||
previousAddressAnchor: previousAnchor,
|
||||
previousSourceIntent: sourceIntent,
|
||||
followupSelectionMode,
|
||||
hasImplicitContinuationSignal
|
||||
hasImplicitContinuationSignal,
|
||||
hasSuggestedIntentPivotSignal
|
||||
};
|
||||
}
|
||||
function buildAddressDialogContinuationContractV2(userMessage, effectiveMessage, carryoverMeta, llmPreDecomposeMeta) {
|
||||
|
|
@ -791,6 +892,7 @@ function createAssistantTransitionPolicy(deps) {
|
|||
? carryoverTargetIntent ?? rootIntent ?? explicitIntent ?? null
|
||||
: carryoverTargetIntent ?? explicitIntent ?? deps.toNonEmptyString(carryoverMeta?.previousAddressIntent) ?? null;
|
||||
const hasImplicitContinuationSignal = Boolean(carryoverMeta?.hasImplicitContinuationSignal);
|
||||
const hasSuggestedIntentPivotSignal = Boolean(carryoverMeta?.hasSuggestedIntentPivotSignal);
|
||||
const rewrittenByPredecompose = deps.compactWhitespace(sourceMessage.toLowerCase()) !== deps.compactWhitespace(canonicalMessage.toLowerCase());
|
||||
const hasExplicitIntent = Boolean(explicitIntent);
|
||||
const decision = !hasFollowupContext
|
||||
|
|
@ -805,6 +907,9 @@ function createAssistantTransitionPolicy(deps) {
|
|||
if (hasImplicitContinuationSignal) {
|
||||
reasons.push("implicit_continuation_by_llm");
|
||||
}
|
||||
if (hasSuggestedIntentPivotSignal) {
|
||||
reasons.push("suggested_intent_followup_pivot");
|
||||
}
|
||||
if (rewrittenByPredecompose) {
|
||||
reasons.push("effective_message_rewritten_by_predecompose");
|
||||
}
|
||||
|
|
@ -829,7 +934,8 @@ function createAssistantTransitionPolicy(deps) {
|
|||
intent_selection_mode: selectionMode,
|
||||
anchor_type: carryoverMeta?.followupContext?.previous_anchor_type ?? null,
|
||||
anchor_value: carryoverMeta?.followupContext?.previous_anchor_value ?? null,
|
||||
implicit_continuation_signal: hasImplicitContinuationSignal
|
||||
implicit_continuation_signal: hasImplicitContinuationSignal,
|
||||
suggested_intent_pivot_signal: hasSuggestedIntentPivotSignal
|
||||
};
|
||||
}
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -162,13 +162,13 @@ export function resolveAddressAsOfDateBasis(
|
|||
filters: AddressFilterSet,
|
||||
semanticFrame?: AddressSemanticFrame | null
|
||||
): AddressAsOfDateBasis | null {
|
||||
if (semanticFrame?.date_basis_hint) {
|
||||
return semanticFrame.date_basis_hint;
|
||||
}
|
||||
const asOfDate = normalizeIsoDateHint(filters.as_of_date);
|
||||
if (asOfDate) {
|
||||
return "explicit_as_of_date";
|
||||
}
|
||||
if (semanticFrame?.date_basis_hint) {
|
||||
return semanticFrame.date_basis_hint;
|
||||
}
|
||||
const periodFrom = normalizeIsoDateHint(filters.period_from);
|
||||
const periodTo = normalizeIsoDateHint(filters.period_to);
|
||||
if (periodFrom && periodTo) {
|
||||
|
|
|
|||
|
|
@ -987,7 +987,12 @@ function extractShipmentCounterpartyValue(text: string): string | undefined {
|
|||
}
|
||||
|
||||
function extractInstrumentalCounterpartyValue(text: string): string | undefined {
|
||||
const match = String(text ?? "").match(
|
||||
const source = String(text ?? "");
|
||||
const match =
|
||||
source.match(
|
||||
/(?:\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u043e\u043c|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\u043e\u043c|\u043a\u043b\u0438\u0435\u043d\u0442\u043e\u043c|\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a\u043e\u043c)\s+([\p{L}][\p{L}\p{N}._-]{1,})(?=[\s,.;:!?)]|$)/iu
|
||||
) ??
|
||||
source.match(
|
||||
/(?:контрагентом|поставщиком|клиентом|заказчиком)\s+([\p{L}][\p{L}\p{N}._-]{1,})(?=[\s,.;:!?)]|$)/iu
|
||||
);
|
||||
if (!match) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { AddressIntentResolution } from "../types/addressQuery";
|
||||
import type { AddressIntent, AddressIntentResolution } from "../types/addressQuery";
|
||||
import { resolveCounterpartyAddressIntent } from "./addressCounterpartyIntentSignals";
|
||||
import { resolveInventoryAddressIntent } from "./addressInventoryIntentSignals";
|
||||
import {
|
||||
|
|
@ -1946,6 +1946,784 @@ function repairLikelyUtf8Mojibake(text: string): string {
|
|||
}
|
||||
}
|
||||
|
||||
function unicodeBridgeResolution(
|
||||
intent: AddressIntent,
|
||||
confidence: AddressIntentResolution["confidence"],
|
||||
reason: string
|
||||
): AddressIntentResolution {
|
||||
return { intent, confidence, reasons: [reason] };
|
||||
}
|
||||
|
||||
function hasBidirectionalValueFlowComparisonSignal(text: string): boolean {
|
||||
const normalized = String(text ?? "").trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasIncomingCue = /(?:\u0432\u0445\u043e\u0434\u044f\u0449|\u043f\u043e\u0441\u0442\u0443\u043f|\u043f\u043e\u043b\u0443\u0447|inflow|incoming)/iu.test(
|
||||
normalized
|
||||
);
|
||||
const hasOutgoingCue =
|
||||
/(?:\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u0441\u043f\u0438\u0441\u0430\u043d|\u0437\u0430\u043f\u043b\u0430\u0442|\u043f\u043b\u0430\u0442\u0438\u043b|\u043e\u043f\u043b\u0430\u0442|outflow|outgoing|payout)/iu.test(
|
||||
normalized
|
||||
);
|
||||
const hasComparisonCue =
|
||||
/(?:\u0431\u043e\u043b\u044c\u0448|\u043c\u0435\u043d\u044c\u0448|\u0441\u0440\u0430\u0432|\u0438\u043b\u0438|\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|vs|versus)/iu.test(
|
||||
normalized
|
||||
);
|
||||
const hasValueFlowCue =
|
||||
/(?:\u0434\u0435\u043d\u044c\u0433|\u0434\u0435\u043d\u0435\u0433|\u0434\u0435\u043d\u0435\u0436|\u043f\u043e\u0442\u043e\u043a|\u043e\u0431\u043e\u0440\u043e\u0442|money|cash|flow)/iu.test(
|
||||
normalized
|
||||
);
|
||||
|
||||
return hasIncomingCue && hasOutgoingCue && hasComparisonCue && hasValueFlowCue;
|
||||
}
|
||||
|
||||
function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolution | null {
|
||||
const normalized = String(text ?? "").trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasAccountAnchor = /(?:\b(?:60|62|76)(?:[.,]\d{2})?\b|сч(?:е|ё)т(?:а|у|ом|е|ов)?|account)/iu.test(
|
||||
normalized
|
||||
);
|
||||
const hasDocumentCue = /(?:док(?:умент(?:ы|ов|а|ам|ами|ах)?|и|ам|ами|ах|ов|а)?|docs?|documents?)/iu.test(
|
||||
normalized
|
||||
);
|
||||
const hasBankCue = /(?:банк|банковск|плат[её]ж|оплат|транзакц|51|bank|payment|transaction)/iu.test(normalized);
|
||||
const hasContractCue = /(?:договор|дог(?:\s|$)|контракт|contract|dogovor)/iu.test(normalized);
|
||||
const hasSpecificContractCue = /(?:\b\d{1,4}\/\d{1,4}\b|этому\s+же\s+договор)/iu.test(normalized);
|
||||
const hasCounterpartyCue =
|
||||
/(?:контрагент|компани|организац|клиент|покупател|заказчик|поставщик|свк|альфа|жуковк|альтернатива|counterpart|company|supplier|customer|client|buyer)/iu.test(
|
||||
normalized
|
||||
);
|
||||
const byAnchorMatch = normalized.match(/(?:^|[\s,.;:!?])(?:по|для)\s+([\p{L}\d._-]{2,})/iu);
|
||||
const byAnchorToken = String(byAnchorMatch?.[1] ?? "").toLowerCase();
|
||||
const hasLooseCounterpartyByAnchor =
|
||||
!!byAnchorToken &&
|
||||
!new Set([
|
||||
"количеству",
|
||||
"документам",
|
||||
"докам",
|
||||
"договору",
|
||||
"договорам",
|
||||
"счету",
|
||||
"счёту",
|
||||
"остатку",
|
||||
"операциям",
|
||||
"оплате",
|
||||
"платежам",
|
||||
"сальдо",
|
||||
"дате",
|
||||
"периоду",
|
||||
"складу",
|
||||
"товару",
|
||||
"этому",
|
||||
"этой",
|
||||
"нему",
|
||||
"ней"
|
||||
]).has(byAnchorToken);
|
||||
const hasMoneyCue = /(?:деньг|денег|выручк|доход|оборот|заработ|прин[её]с|чек|ликвидн|revenue|turnover|money)/iu.test(
|
||||
normalized
|
||||
);
|
||||
const hasRankingCue = /(?:топ|ранк|сам(?:ый|ая|ое|ые)|больше\s+всего|наибольш|крупн|жирн|max|top|rank)/iu.test(
|
||||
normalized
|
||||
);
|
||||
|
||||
const hasOpenItemsAccountCue =
|
||||
/(?:хвост|долг|незакрыт|вис)/iu.test(normalized) &&
|
||||
/(?:сч(?:е|ё)т(?:а|у|ом|е|ов)?\s*(?:№|#)?\s*(?:60|62|76)(?:[.,]\d{1,2})?|\b(?:60|62|76)(?:[.,]\d{1,2})?\b\s*сч(?:е|ё)т)/iu.test(
|
||||
normalized
|
||||
);
|
||||
if (hasOpenItemsAccountCue) {
|
||||
return unicodeBridgeResolution(
|
||||
"open_items_by_counterparty_or_contract",
|
||||
"medium",
|
||||
"open_items_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
const hasCounterpartyShipmentItemFlowCue =
|
||||
/(?:отгруж(?:ал|али|ен[аоы]?|енн(?:ый|ая|ое|ые)?))/iu.test(normalized) &&
|
||||
/(?:товар|услуг|позици|номенклатур)/iu.test(normalized) &&
|
||||
!/(?:выбранн(?:ый|ому)\s+объект|selected\s+object)/iu.test(normalized);
|
||||
if (hasCounterpartyShipmentItemFlowCue) {
|
||||
return unicodeBridgeResolution(
|
||||
"list_documents_by_counterparty",
|
||||
"medium",
|
||||
"counterparty_item_flow_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
const hasHighestValueCustomerCue =
|
||||
/(?:сам(?:ый|ые|ая|ое)|топ|кто|какой|какие|больше\s+всего|наибольш)/iu.test(normalized) &&
|
||||
/(?:доходн|выручк|денег|деньг|оборот|поступлен|прин[её]с)/iu.test(normalized) &&
|
||||
/(?:клиент|покупател|заказчик|контрагент)/iu.test(normalized);
|
||||
if (!hasContractCue && hasHighestValueCustomerCue) {
|
||||
return unicodeBridgeResolution(
|
||||
"customer_revenue_and_payments",
|
||||
"high",
|
||||
"unicode_customer_revenue_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (hasBidirectionalValueFlowComparisonSignal(normalized)) {
|
||||
return unicodeBridgeResolution(
|
||||
"unknown",
|
||||
"high",
|
||||
"unicode_bidirectional_value_flow_deferred_to_discovery"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
/(?:за\s+какие\s+годы|диапазон\s+лет|покрыт(?:ие|ый)\s+период|какой\s+год.*актив|какой\s+месяц.*актив|год.*пассив|месяц.*пассив|минимальн.*док|минимальн.*операц|месяц[\s-]*пик|profile\s+period|top\s*year|top\s*month)/iu.test(
|
||||
normalized
|
||||
)
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"period_coverage_profile",
|
||||
"high",
|
||||
"unicode_period_coverage_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
/(?:тип(?:ы|ов)?\s+док|док(?:умент|ов).*?(?:чаще|редк|больше\s+всего|меньше\s+всего|крутит)|раздел(?:ы|ов)?\s+уч[её]та|сводк.*тип.*док|document\s+type|account\s+section)/iu.test(
|
||||
normalized
|
||||
)
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"document_type_and_account_section_profile",
|
||||
"high",
|
||||
"unicode_document_type_profile_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
hasAccountAnchor &&
|
||||
hasDocumentCue &&
|
||||
/(?:формирующ.*остат|раскрой\s+остат|остат.*по\s+документ|по\s+докам.*(?:60|62|76)|(?:60|62|76)(?:[.,]\d{2})?.*(?:по\s+докам|по\s+документ))/iu.test(normalized)
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"documents_forming_balance",
|
||||
"high",
|
||||
"unicode_documents_forming_balance_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (/(?:договор[а-я]*.*(?:все|список).*по\s+[\p{L}\d]|(?:покажи|показать).*договор[а-я]*.*по\s+[\p{L}\d])/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution(
|
||||
"list_contracts_by_counterparty",
|
||||
"high",
|
||||
"unicode_contracts_by_counterparty_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (/(?:проконтрол|акты\s+без\s+приход|без\s+приходок|засорять\s+бухгалтер)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("unknown", "low", "unsupported_supplier_control_signal_detected");
|
||||
}
|
||||
|
||||
if (/(?:кроме\s+этого\s+документ.*(?:есть\s+еще\s+что|есть\s+ещ[её]\s+что|что[-\s]?то))/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution(
|
||||
"list_documents_by_counterparty",
|
||||
"medium",
|
||||
"generic_document_followup_with_previous_counterparty"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
/(?:плат[её]ж|оплат|отгрузк|документ|аванс|взаиморасчет|закрыт)/iu.test(normalized) &&
|
||||
/(?:без\s+(?:закрыт|документ|оплат)|нет\s+(?:документ|оплат)|не\s+закрыт|оплат[а-я]*\s+нет|документ[а-я]*\s+есть|требует\s+ручн)/iu.test(normalized)
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"list_open_contracts",
|
||||
"high",
|
||||
"unicode_open_contract_gap_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
/(?:долгожител|долго\s+долж|задолженн(?:ост|остям).*(?:давн|долго)|не\s+плат|не\s+оплат|не\s+оплачен|неоплачен|просроч|сроки\s+давно\s+прошл|слишком\s+длинн.*оплат)/iu.test(
|
||||
normalized
|
||||
) &&
|
||||
/(?:покупател|клиент|заказ|отгрузк|товар|услуг|задолженн|сальдо|не\s+плат|не\s+оплат|не\s+оплачен|неоплачен|просроч)/iu.test(
|
||||
normalized
|
||||
)
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"list_receivables_counterparties",
|
||||
"high",
|
||||
"receivables_debt_lifecycle_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
const inventoryBridgeIntent = resolveInventoryAddressIntent(normalized);
|
||||
if (inventoryBridgeIntent) {
|
||||
if (inventoryBridgeIntent.intent === "inventory_aging_by_purchase_date") {
|
||||
return { ...inventoryBridgeIntent, confidence: "high" };
|
||||
}
|
||||
return inventoryBridgeIntent;
|
||||
}
|
||||
|
||||
if (
|
||||
/(?:поставщик|vendor|supplier)/iu.test(normalized) &&
|
||||
/(?:хвост|задержк|проблем|систематическ)/iu.test(normalized)
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"list_payables_counterparties",
|
||||
"high",
|
||||
"supplier_tail_risk_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (hasDocumentCue && (hasCounterpartyCue || hasLooseCounterpartyByAnchor) && !hasContractCue) {
|
||||
return unicodeBridgeResolution(
|
||||
"list_documents_by_counterparty",
|
||||
"high",
|
||||
"unicode_documents_by_counterparty_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (hasBankCue && (hasCounterpartyCue || hasLooseCounterpartyByAnchor) && !hasContractCue) {
|
||||
return unicodeBridgeResolution(
|
||||
"bank_operations_by_counterparty",
|
||||
"high",
|
||||
"unicode_bank_ops_by_counterparty_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
/(?:есть\s+что[-\s]?то|что[-\s]?то)/iu.test(normalized) &&
|
||||
(hasLooseCounterpartyByAnchor || /по\s+(?:ней|нему|этой|этому)/iu.test(normalized))
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"list_documents_by_counterparty",
|
||||
"medium",
|
||||
"generic_lookup_with_loose_anchor_fallback"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
hasDocumentCue &&
|
||||
(hasLooseCounterpartyByAnchor || hasCounterpartyCue || /по\s+(?:ней|нему|этой|этому)/iu.test(normalized)) &&
|
||||
!hasContractCue &&
|
||||
!/(?:купил|куплен|закуп|товар|позици|номенклатур)/iu.test(normalized)
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"list_documents_by_counterparty",
|
||||
"medium",
|
||||
"unicode_documents_by_counterparty_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
/(?:долгожител|долго\s+долж|задолженн(?:ост|остям).*(?:давн|долго)|не\s+плат|не\s+оплат|не\s+оплачен|неоплачен|просроч|сроки\s+давно\s+прошл|слишком\s+длинн.*оплат)/iu.test(
|
||||
normalized
|
||||
) &&
|
||||
/(?:покупател|клиент|заказ|отгрузк|товар|услуг|задолженн|сальдо|не\s+плат|не\s+оплат|не\s+оплачен|неоплачен|просроч)/iu.test(
|
||||
normalized
|
||||
)
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"list_receivables_counterparties",
|
||||
"high",
|
||||
"receivables_debt_lifecycle_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (/\b41(?:[.,]\d{2})?\b/iu.test(normalized) && /(?:товар|склад|остат|состоит|номенклатур)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution(
|
||||
"inventory_on_hand_as_of_date",
|
||||
"high",
|
||||
"unicode_inventory_on_hand_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
/(?:год.*(?:док|операц).*(?:актив|пик|жив|много|движов)|год.*движов.*(?:док|операц)|(?:док|операц).*год.*(?:актив|пик|жив|много|движов)|месяц[\s-]*пик)/iu.test(
|
||||
normalized
|
||||
)
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"period_coverage_profile",
|
||||
"high",
|
||||
"unicode_period_coverage_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!hasContractCue &&
|
||||
/(?:скольк|скока).*(?:деньг|денег|выручк|доход|оборот)|(?:деньг|денег|выручк|доход|оборот).*(?:прин[её]с|зан[её]с|плат)/iu.test(normalized)
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"customer_revenue_and_payments",
|
||||
"high",
|
||||
"unicode_customer_revenue_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!hasContractCue &&
|
||||
/(?:кто|какие|выведи|покажи|список|самые)/iu.test(normalized) &&
|
||||
/(?:список\s+(?:заказчик|клиент|покупател).*за\s+\d{2,4}\s*год|актив.*отвал|ровно\s+один\s+раз|один\s+раз.*пропал|стар(?:ые|ые)?\s+по\s+сотруднич|сотрудничеству\s+кто)/iu.test(normalized)
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"counterparty_activity_lifecycle",
|
||||
"high",
|
||||
"unicode_counterparty_lifecycle_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!hasContractCue &&
|
||||
/(?:кто|какие|выведи|покажи|список|разбей|раздели|самые)/iu.test(normalized) &&
|
||||
/(?:заказчик|клиент|покупател|поставщик|контрагент|зак(?!рыт))/iu.test(normalized) &&
|
||||
/(?:работал|работают|актив|все\s+время|вообще|регулярн|эпизодич|частот|давно\s+не\s+использ|операционн|разов|один\s+раз|пропал|отвал|сотруднич)/iu.test(normalized)
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"counterparty_activity_lifecycle",
|
||||
"high",
|
||||
"unicode_counterparty_lifecycle_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
/(?:поставщик|vendor|supplier|кому\s+(?:ушло|платили|заплатили)|выплат|исходящ|списан|сгрузил)/iu.test(normalized) &&
|
||||
!/(?:аванс.*(?:не\s+)?закрыт|закрыт.*аванс)/iu.test(normalized) &&
|
||||
(hasMoneyCue || hasRankingCue || /плат[её]ж|оплат|выплат|outflow|payout|хвост|задержк|проблем/iu.test(normalized))
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
/(?:хвост|задержк|проблем)/iu.test(normalized) ? "list_payables_counterparties" : "supplier_payouts_profile",
|
||||
"high",
|
||||
/(?:хвост|задержк|проблем)/iu.test(normalized)
|
||||
? "supplier_tail_risk_signal_detected"
|
||||
: "unicode_supplier_payouts_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!hasContractCue &&
|
||||
(/(?:клиент|покупател|заказчик|контрагент|альтернатива|свк)/iu.test(normalized) || hasRankingCue) &&
|
||||
(hasMoneyCue || /поступлен|приход|входящ|сделк|бюджет|inflow/iu.test(normalized))
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"customer_revenue_and_payments",
|
||||
"high",
|
||||
"unicode_customer_revenue_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!hasContractCue &&
|
||||
/(?:кто.*(?:деньг|денег|доход|выручк).*(?:прин[её]с|зан[её]с|плат)|(?:жирн|ликвидн).*контрагент.*(?:деньг|денег))/iu.test(
|
||||
normalized
|
||||
)
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"customer_revenue_and_payments",
|
||||
"high",
|
||||
"unicode_customer_revenue_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
/(?:общие\s+обороты|общая\s+выручк|оборот.*за\s+все\s+время|выручк.*за\s+все\s+время)/iu.test(normalized)
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"customer_revenue_and_payments",
|
||||
"high",
|
||||
"unicode_customer_revenue_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
/(?:открыт(?:ые|ая|ый)?\s+задолж|открыт(?:ые|ая|ый)?\s+позици|позици.*по\s+договор|open\s+items?)/iu.test(
|
||||
normalized
|
||||
) &&
|
||||
(hasContractCue || hasCounterpartyCue || hasAccountAnchor || /покупател|клиент/iu.test(normalized))
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"open_items_by_counterparty_or_contract",
|
||||
"high",
|
||||
"unicode_open_items_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
hasContractCue &&
|
||||
/(?:нескольк(?:ими|о)?\s+договор|контрагент.*нескольк.*договор|какие\s+из\s+договор.*актив)/iu.test(
|
||||
normalized
|
||||
)
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"contract_usage_and_value",
|
||||
"high",
|
||||
"unicode_contract_usage_value_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (hasContractCue && (hasMoneyCue || hasRankingCue || /оборот|бюджет|сумм|стоим|value|amount/iu.test(normalized))) {
|
||||
return unicodeBridgeResolution(
|
||||
"contract_usage_and_value",
|
||||
"high",
|
||||
"unicode_contract_usage_value_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
/(?:сальдо.*(?:расход|не\s+совпад)|расход.*сальдо|акт(?:ом|ах)?\s+сверк|плат[её]ж[и]?,?\s+но\s+нет\s+док|документ(?:ы)?\s+есть,?\s+а\s+оплат\s+нет|(?:оплат|плат[её]ж|отгрузк|закрыти[ея]\s+счет)[\p{L}\s,]*\s+без\s+(?:закрыт|документ|подтвержд)|аванс.*давно\s+не\s+закрыт)/iu.test(
|
||||
normalized
|
||||
)
|
||||
) {
|
||||
return unicodeBridgeResolution("list_open_contracts", "high", "unicode_open_contracts_list_bridge_signal_detected");
|
||||
}
|
||||
|
||||
if (
|
||||
/(?:долгожител|долго\s+долж|задолженн(?:ост|остям).*(?:давн|долго)|не\s+плат|не\s+оплат|не\s+оплачен|неоплачен|просроч|сроки\s+давно\s+прошл|слишком\s+длинн.*оплат)/iu.test(
|
||||
normalized
|
||||
) &&
|
||||
/(?:покупател|клиент|заказ|отгрузк|товар|услуг|задолженн|сальдо|не\s+плат|не\s+оплат|не\s+оплачен|неоплачен|просроч)/iu.test(
|
||||
normalized
|
||||
)
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"list_receivables_counterparties",
|
||||
"high",
|
||||
"receivables_debt_lifecycle_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
/(?:сальдо.*(?:расход|не\s+совпад)|расход.*сальдо|акт(?:ом|ах)?\s+сверк|плат[её]ж[и]?,?\s+но\s+нет\s+док|документ(?:ы)?\s+есть,?\s+а\s+оплат\s+нет|(?:оплат|плат[её]ж|отгрузк|закрыти[ея]\s+счет)[\p{L}\s,]*\s+без\s+(?:закрыт|документ|подтвержд)|аванс.*давно\s+не\s+закрыт)/iu.test(
|
||||
normalized
|
||||
)
|
||||
) {
|
||||
return unicodeBridgeResolution("list_open_contracts", "high", "unicode_open_contracts_list_bridge_signal_detected");
|
||||
}
|
||||
|
||||
if (
|
||||
/(?:открыт(?:ые|ая|ый)?\s+позици|позици.*по\s+договор|open\s+items?)/iu.test(normalized) &&
|
||||
(hasContractCue || hasCounterpartyCue || hasAccountAnchor)
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"open_items_by_counterparty_or_contract",
|
||||
"high",
|
||||
"unicode_open_items_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
hasAccountAnchor &&
|
||||
hasDocumentCue &&
|
||||
/(?:формир|под\s+остат|раскр(?:ой|ыть|ывай)|остат(?:ок|ком)?\s+по\s+док|documents?\s+forming|docs?\s+forming)/iu.test(
|
||||
normalized
|
||||
)
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"documents_forming_balance",
|
||||
"high",
|
||||
"unicode_documents_forming_balance_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (hasContractCue && hasSpecificContractCue && hasBankCue) {
|
||||
return unicodeBridgeResolution(
|
||||
"bank_operations_by_contract",
|
||||
"high",
|
||||
"unicode_bank_ops_by_contract_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (hasContractCue && hasSpecificContractCue && hasDocumentCue) {
|
||||
return unicodeBridgeResolution(
|
||||
"list_documents_by_contract",
|
||||
"high",
|
||||
"unicode_documents_by_contract_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
hasAccountAnchor &&
|
||||
!hasDocumentCue &&
|
||||
/(?:баланс|остат(?:ок)?|сальдо|что\s+на\s+сч(?:е|ё)те|по\s+сч(?:е|ё)ту|скольк|скока|account\s+balance|balance\s+account|as\s+of)/iu.test(
|
||||
normalized
|
||||
)
|
||||
) {
|
||||
return unicodeBridgeResolution("account_balance_snapshot", "high", "unicode_account_balance_bridge_signal_detected");
|
||||
}
|
||||
|
||||
if (/(?:ндс|vat)/iu.test(normalized)) {
|
||||
const hasVatDebtCue = /(?:долг|должн|подтвержд)/iu.test(normalized);
|
||||
const hasTaxPeriodCue = /(?:налогов|налоговую|бюджет|декларац|квартал|\b[1-4]\s*кв)/iu.test(normalized);
|
||||
const hasVatMonthPeriodCue =
|
||||
/(?:за|на|в)\s+(?:январ|феврал|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр)\S*(?:\s+(?:19|20)\d{2})?/iu.test(
|
||||
normalized
|
||||
) &&
|
||||
!/\b\d{1,2}\s+(?:январ|феврал|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр)\S*(?:\s+(?:19|20)\d{2})?/iu.test(
|
||||
normalized
|
||||
);
|
||||
if (
|
||||
(hasTaxPeriodCue || (hasVatMonthPeriodCue && !hasVatDebtCue)) &&
|
||||
/(?:скольк|скока|надо|нужно|заплат|уплат|оплат|прикин)/iu.test(normalized)
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"vat_liability_confirmed_for_tax_period",
|
||||
"high",
|
||||
"vat_liability_confirmed_tax_period_signal_detected"
|
||||
);
|
||||
}
|
||||
if (
|
||||
/(?:прогноз|прикин|план)/iu.test(normalized) ||
|
||||
(!hasVatDebtCue && /(?:надо|нужно)\s+(?:заплат|оплат|уплат)/iu.test(normalized))
|
||||
) {
|
||||
return unicodeBridgeResolution("vat_payable_forecast", "high", "forecast_tax_signal_detected");
|
||||
}
|
||||
if (/(?:долг|подтвержд|скольк|скока|надо|нужно|заплат|уплат|оплат)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution(
|
||||
hasTaxPeriodCue
|
||||
? "vat_liability_confirmed_for_tax_period"
|
||||
: "vat_payable_confirmed_as_of_date",
|
||||
"high",
|
||||
"vat_payable_confirmed_signal_detected"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
/(?:незакрыт|открыт).*договор/iu.test(normalized) &&
|
||||
!/(?:долг|задолж|хвост|висит|расчет|расчёт)/iu.test(normalized)
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"open_contracts_confirmed_as_of_date",
|
||||
"high",
|
||||
"unicode_open_contracts_snapshot_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
/(?:долг|задолж|хвост|висит|открыт(?:ые|ая|ый)?\s+задолж|open\s+items?)/iu.test(normalized) &&
|
||||
(hasContractCue || hasCounterpartyCue || hasAccountAnchor || /покупател|клиент/iu.test(normalized))
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"open_items_by_counterparty_or_contract",
|
||||
"high",
|
||||
"unicode_open_items_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
hasContractCue &&
|
||||
/(?:без\s+(?:закрыт|оплат|плат[её]ж|док)|не\s+закрыт|аванс|отгрузк|плат[её]ж.*без|док.*без|расхожд|mismatch)/iu.test(
|
||||
normalized
|
||||
)
|
||||
) {
|
||||
return unicodeBridgeResolution("list_open_contracts", "high", "unicode_open_contracts_list_bridge_signal_detected");
|
||||
}
|
||||
|
||||
if (
|
||||
hasContractCue &&
|
||||
/(?:скольк.*(?:всего\s+)?договор|договор.*(?:заведен|использовал|реально\s+использ)|сколько\s+из\s+них)/iu.test(
|
||||
normalized
|
||||
)
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"contract_usage_overview",
|
||||
"high",
|
||||
"unicode_contract_usage_overview_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
hasContractCue &&
|
||||
/(?:нескольк(?:ими|о)?\s+договор|контрагент.*нескольк.*договор|какие\s+из\s+договор.*актив)/iu.test(
|
||||
normalized
|
||||
)
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"contract_usage_and_value",
|
||||
"high",
|
||||
"unicode_contract_usage_value_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
hasContractCue &&
|
||||
/(?:все|покажи|показать|какие|список|list|show)/iu.test(normalized) &&
|
||||
!hasSpecificContractCue &&
|
||||
!hasDocumentCue &&
|
||||
!hasBankCue &&
|
||||
(hasCounterpartyCue || hasLooseCounterpartyByAnchor)
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"list_contracts_by_counterparty",
|
||||
"high",
|
||||
"unicode_contracts_by_counterparty_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (hasContractCue && !hasSpecificContractCue && !hasDocumentCue && !hasBankCue && hasCounterpartyCue) {
|
||||
return unicodeBridgeResolution(
|
||||
"list_contracts_by_counterparty",
|
||||
"high",
|
||||
"unicode_contracts_by_counterparty_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (hasBankCue && (hasCounterpartyCue || hasLooseCounterpartyByAnchor) && !hasContractCue) {
|
||||
return unicodeBridgeResolution(
|
||||
"bank_operations_by_counterparty",
|
||||
"high",
|
||||
"unicode_bank_ops_by_counterparty_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (hasDocumentCue && (hasCounterpartyCue || hasLooseCounterpartyByAnchor) && !hasContractCue && !hasAccountAnchor) {
|
||||
return unicodeBridgeResolution(
|
||||
"list_documents_by_counterparty",
|
||||
"high",
|
||||
"unicode_documents_by_counterparty_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
/(?:за\s+какие\s+годы|диапазон\s+лет|покрыт(?:ие|ый)\s+период|какой\s+год.*актив|какой\s+месяц.*актив|год.*пассив|месяц.*пассив|минимальн.*док|минимальн.*операц|profile\s+period|top\s*year|top\s*month)/iu.test(
|
||||
normalized
|
||||
)
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"period_coverage_profile",
|
||||
"high",
|
||||
"unicode_period_coverage_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
/(?:тип(?:ы|ов)?\s+док|документ.*(?:чаще|редк|больше\s+всего|меньше\s+всего)|раздел(?:ы|ов)?\s+уч[её]та|сводк.*тип.*док|document\s+type|account\s+section)/iu.test(
|
||||
normalized
|
||||
)
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"document_type_and_account_section_profile",
|
||||
"high",
|
||||
"unicode_document_type_profile_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
/(?:скольк|скока|число|количеств|разбей|раздели|сформируй\s+сводк)/iu.test(normalized) &&
|
||||
/(?:контрагент|поставщик|клиент|покупател|заказчик|рол)/iu.test(normalized) &&
|
||||
!/(?:активн|давно|нов(?:ые|ых)|однораз|уш[её]л|исчез|регулярн|эпизодич|частот|разов|churn|lifecycle)/iu.test(normalized)
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"counterparty_population_and_roles",
|
||||
"high",
|
||||
"unicode_counterparty_population_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (/(?:скок|скока|сколько)\s+(?:клиент|покупател|заказчик)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution(
|
||||
"counterparty_population_and_roles",
|
||||
"high",
|
||||
"unicode_counterparty_population_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
/(?:активн(?:ые|ость)?\s+(?:клиент|покупател|поставщик|контрагент)|все\s+время|однораз|давно\s+(?:не\s+)?(?:покупал|платил|актив)|уш[её]л|исчез|нов(?:ые|ых)\s+(?:клиент|контрагент)|регулярн|разов(?:ый|ые)|stale\s+supplier|churn|lifecycle)/iu.test(
|
||||
normalized
|
||||
)
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"counterparty_activity_lifecycle",
|
||||
"high",
|
||||
"unicode_counterparty_lifecycle_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (hasContractCue && /(?:давно\s+не\s+использ|не\s+использ|stale|inactive)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution(
|
||||
"contract_usage_overview",
|
||||
"high",
|
||||
"unicode_contract_usage_overview_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (hasContractCue && (hasMoneyCue || hasRankingCue || /оборот|бюджет|сумм|стоим|value|amount/iu.test(normalized))) {
|
||||
return unicodeBridgeResolution(
|
||||
"contract_usage_and_value",
|
||||
"high",
|
||||
"unicode_contract_usage_value_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
/(?:поставщик|vendor|supplier|кому\s+(?:ушло|платили|заплатили)|выплат|исходящ|списан|сгрузил)/iu.test(normalized) &&
|
||||
(hasMoneyCue || hasRankingCue || /плат[её]ж|оплат|выплат|outflow|payout/iu.test(normalized))
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"supplier_payouts_profile",
|
||||
"high",
|
||||
"unicode_supplier_payouts_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(/(?:клиент|покупател|заказчик|контрагент|альтернатива|свк)/iu.test(normalized) || hasRankingCue) &&
|
||||
(hasMoneyCue || /поступлен|приход|входящ|inflow/iu.test(normalized))
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"customer_revenue_and_payments",
|
||||
"high",
|
||||
"unicode_customer_revenue_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (/(?:к[оа]му\s+мы\s+должны|мы\s+должны\s+к[оа]му|кредитор|payables?)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution(
|
||||
"payables_confirmed_as_of_date",
|
||||
"high",
|
||||
"payables_debt_lifecycle_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (/(?:кто\s+нам\s+должен|нам\s+должны|дебитор|receivables?)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution(
|
||||
"receivables_confirmed_as_of_date",
|
||||
"high",
|
||||
"unicode_receivables_snapshot_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
/(?:покупател|клиент).*(?:не\s+плат|просроч|долго\s+долж|долг.*давн)|(?:долг|задолж).*(?:покупател|клиент)/iu.test(
|
||||
normalized
|
||||
)
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"list_receivables_counterparties",
|
||||
"high",
|
||||
"unicode_receivables_list_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
/(?:что|че|чё|какие|покажи|показать|список).*(?:склад|остат|товар)|(?:склад|остат).*(?:сейчас|лежит|есть|на\s+дату|на\s+конец|what|show|list)/iu.test(
|
||||
normalized
|
||||
) &&
|
||||
!/(?:поставщик|продаж|реализ|цепоч|документал|давно|стар(?:ые|ый|ым|ых)|закуп)/iu.test(normalized)
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"inventory_on_hand_as_of_date",
|
||||
"high",
|
||||
"unicode_inventory_on_hand_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolveAddressIntent(userMessage: string): AddressIntentResolution {
|
||||
const text = String(userMessage ?? "").trim().toLowerCase();
|
||||
const repairedText = repairLikelyUtf8Mojibake(text).trim().toLowerCase();
|
||||
|
|
@ -1956,6 +2734,11 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
|
|||
const currentTurnBridgeText =
|
||||
turnNoiseNormalizedBridgeText !== bridgeText ? `${bridgeText} ${turnNoiseNormalizedBridgeText}` : bridgeText;
|
||||
|
||||
const unicodeAddressIntent = resolveUnicodeAddressIntentBridge(currentTurnBridgeText);
|
||||
if (unicodeAddressIntent) {
|
||||
return unicodeAddressIntent;
|
||||
}
|
||||
|
||||
const hasLooseVatPayableBridge =
|
||||
/(?:\u043d\u0434\u0441|vat)/iu.test(text) &&
|
||||
/(?:\u043a\u0430\u043a\u043e\u0439\s+\u043d\u0434\u0441\s+(?:(?:\u043d\u0430\u043c|(?:\u043c\u044b\s+)?\u0434\u043e\u043b\u0436\u043d\u044b)\s+)?(?:\u043d\u0430\u0434\u043e|\u043d\u0443\u0436\u043d\u043e|\u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e)|(?:\u043d\u0430\u043c|\u043c\u044b\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|\u043c\u044b\s+)?\u043d\u0443\u0436\u043d\u043e\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|\u043c\u044b\s+\u0434\u043e\u043b\u0436\u043d\u044b\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|\u043d\u0434\u0441\s+\u043a\s+\u0443\u043f\u043b\u0430\u0442\u0435)/iu.test(
|
||||
|
|
@ -2003,7 +2786,7 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
|
|||
}
|
||||
|
||||
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(
|
||||
/(?:\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|\u0437\u0430\u043a\u0443\u043f\u043b\u0435\u043d(?:\u043d\u044b\u0435|\u043d\u044b\u043c|\u043d\u044b\u0445|\u0430\u044f|\u044b\u0439)?\s+\u0437\u0430\u0434\u043e\u043b\u0433\u043e\s+\u0434\u043e|\u0437\u0430\u0434\u043e\u043b\u0433\u043e\s+\u0434\u043e|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)) {
|
||||
|
|
|
|||
|
|
@ -79,6 +79,14 @@ function hasInventoryOnHandSignal(text: string): boolean {
|
|||
}
|
||||
|
||||
function hasSelectedObjectInventoryCue(text: string): boolean {
|
||||
const value = String(text ?? "");
|
||||
if (
|
||||
/(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+нему|по\s+ней|по\s+ним|по\s+нему\s+же|по\s+ней\s+же|selected\s+object)/iu.test(
|
||||
value
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return /(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+нему|по\s+ней|по\s+ним|по\s+нему\s+же|по\s+ней\s+же|selected\s+object)/iu.test(
|
||||
String(text ?? "")
|
||||
);
|
||||
|
|
@ -89,6 +97,15 @@ function hasSelectedObjectInventoryProvenanceSignal(text: string): boolean {
|
|||
}
|
||||
|
||||
function hasSelectedObjectInventoryPurchaseDocumentsSignal(text: string): boolean {
|
||||
const value = String(text ?? "");
|
||||
if (
|
||||
hasSelectedObjectInventoryCue(value) &&
|
||||
/(?:по\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)?\s*купили|по\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)?\s*купили|какими\s+документами\s+(?:был\s+)?куплен|покажи\s+документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(
|
||||
value
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const hasPurchaseDocumentsCue =
|
||||
/(?:по\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|по\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|какими\s+документами\s+(?:был\s+)?куплен|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(
|
||||
text
|
||||
|
|
@ -108,6 +125,17 @@ function hasSelectedObjectInventoryProfitabilitySignal(text: string): boolean {
|
|||
}
|
||||
|
||||
function hasInventoryProvenanceSignalV2(text: string): boolean {
|
||||
const value = String(text ?? "");
|
||||
const hasPlainItemCue = /(?:товар|номенклатур|sku|item|product|остат|склад)/iu.test(value);
|
||||
const hasPlainSupplierCue =
|
||||
hasInventorySupplierCue(value) || /(?:кем\s+поставлен|кто\s+(?:это|его|этот\s+товар|эту\s+позицию)?\s*поставил)/iu.test(value);
|
||||
const hasPlainPurchaseCue =
|
||||
/(?:куплен(?:ы|а|о)?|закупк|происхожд|откуда|где\s+(?:мы\s+)?купили|поставлен(?:ы|а)?|purchase\s+provenance|purchase\s+date)/iu.test(
|
||||
value
|
||||
) || hasInventoryPurchaseStem(value);
|
||||
if (hasPlainItemCue && hasPlainSupplierCue && hasPlainPurchaseCue) {
|
||||
return true;
|
||||
}
|
||||
const hasItemCue = /(?:товар|номенклатур|sku|item|product|остат(?:ок|ки)|склад)/iu.test(text);
|
||||
const hasSupplierCue = hasInventorySupplierCue(text) || /кем\s+поставлен/iu.test(text);
|
||||
const hasPurchaseCue =
|
||||
|
|
@ -129,6 +157,15 @@ function hasInventoryPurchaseDateSignal(text: string): boolean {
|
|||
}
|
||||
|
||||
function hasInventoryPurchaseDocumentsSignalV2(text: string): boolean {
|
||||
const value = String(text ?? "");
|
||||
const hasPlainItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(value);
|
||||
const hasPlainPurchaseDocCue =
|
||||
/(?:по\s+каким\s+документам\s+(?:был\s+)?куплен|по\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)?\s*купили|какими\s+документами\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)?\s*купили|документ[а-яё]*\s+закупк|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(
|
||||
value
|
||||
);
|
||||
if (hasPlainItemCue && hasPlainPurchaseDocCue) {
|
||||
return true;
|
||||
}
|
||||
const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text);
|
||||
const hasPurchaseDocCue = /(?:по\s+каким\s+документам\s+был\s+куплен|по\s+каким\s+документам\s+куплен|какими\s+документами\s+был\s+куплен|документ(?:ам|ы)\s+закупк|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(
|
||||
text
|
||||
|
|
@ -137,6 +174,15 @@ function hasInventoryPurchaseDocumentsSignalV2(text: string): boolean {
|
|||
}
|
||||
|
||||
function hasInventorySaleTraceSignalV2(text: string): boolean {
|
||||
const value = String(text ?? "");
|
||||
const hasPlainItemCue = /(?:товар|номенклатур|позици|продукци|sku|item|product)/iu.test(value);
|
||||
const hasPlainTraceCue =
|
||||
/(?:кому\s+(?:в\s+итоге\s+)?(?:мы\s+)?(?:продали|реализовали|впарили)|кому\s+(?:был[аио]?|были)?\s*реализован|кто\s+купил|покупател|buyer|sale\s+trace|trace\s+of\s+sale)/iu.test(
|
||||
value
|
||||
);
|
||||
if (hasPlainItemCue && hasPlainTraceCue) {
|
||||
return true;
|
||||
}
|
||||
const hasItemCue = /(?:товар|номенклатур|sku|item|product|позици(?:я|ю|и)|продукци(?:я|ю|и))/iu.test(text);
|
||||
const hasTraceCue =
|
||||
/(?:кому\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали|кому\s+был\s+продан|куда\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали(?:\s+(?:это|его|товар|позицию))?|куда\s+(?:была\s+)?реализована\s+(?:позиция|номенклатура|продукция)|кто\s+купил|buyer|sale\s+trace|trace\s+of\s+sale|через\s+какие\s+документы\s+прош[её]л\s+путь\s+товара|закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*warehouse\s*->\s*sale|purchase\s*->\s*stock\s*->\s*sale)/iu.test(
|
||||
|
|
@ -146,6 +192,24 @@ function hasInventorySaleTraceSignalV2(text: string): boolean {
|
|||
}
|
||||
|
||||
function hasInventorySupplierStockOverlapSignal(text: string): boolean {
|
||||
const value = String(text ?? "");
|
||||
const hasPlainDirectSingleItemSupplierQuestion =
|
||||
/(?:от\s+(?:какого|кого)\s+поставщик[а-яё]*\s+куплен\s+(?:товар|номенклатур|позици)|от\s+кого\s+куплен\s+(?:товар|номенклатур|позици))/iu.test(
|
||||
value
|
||||
);
|
||||
if (hasPlainDirectSingleItemSupplierQuestion) {
|
||||
return false;
|
||||
}
|
||||
const hasPlainSupplierCue = /(?:поставщик|supplier|vendor)/iu.test(value);
|
||||
const hasPlainStockCue =
|
||||
/(?:склад|остат|леж[аи][т]?|висят|сейчас\s+еще|сейчас\s+ещё|на\s+дату|current\s+stock|stock\s+overlap)/iu.test(value);
|
||||
const hasPlainUnresolvedSupplierLink =
|
||||
/(?:без\s+понятн[а-яё]*\s+привязк[а-яё]*\s+к\s+поставщик|без\s+привязк[а-яё]*\s+к\s+поставщик|unresolved\s+supplier\s+link)/iu.test(
|
||||
value
|
||||
);
|
||||
if ((hasPlainSupplierCue && hasPlainStockCue) || (hasPlainUnresolvedSupplierLink && hasPlainStockCue)) {
|
||||
return true;
|
||||
}
|
||||
const hasDirectSingleItemSupplierQuestion =
|
||||
/(?:от\s+какого\s+поставщика\s+куплен\s+(?:товар|номенклатур(?:а|у|ы)|позици(?:я|ю|и))|от\s+кого\s+куплен\s+(?:товар|номенклатур(?:а|у|ы)|позици(?:я|ю|и)))/iu.test(
|
||||
text
|
||||
|
|
@ -161,6 +225,15 @@ function hasInventorySupplierStockOverlapSignal(text: string): boolean {
|
|||
}
|
||||
|
||||
function hasInventoryAgingSignal(text: string): boolean {
|
||||
const value = String(text ?? "");
|
||||
const hasPlainResidueCue = /(?:остат|склад|stock\s+residue|stock\s+balance)/iu.test(value);
|
||||
const hasPlainAgingCue =
|
||||
/(?:стар(?:ые|ым|ых)\s+закупк|очень\s+давно|давно\s+(?:куплен|приобретен|приобретён)|закуплен[а-яё]*\s+задолго\s+до|задолго\s+до(?:\s+\d{4}-\d{2}-\d{2})?|возраст\s+(?:остатк|закупк)|aging\s+by\s+purchase\s+date|old\s+purchase|old\s+purchases|old\s+stock|very\s+old\s+stock|very\s+old\s+purchase)/iu.test(
|
||||
value
|
||||
);
|
||||
if (hasPlainAgingCue || (hasPlainResidueCue && /(?:давно|задолго\s+до|стар(?:ые|ым|ых)\s+закупк)/iu.test(value))) {
|
||||
return true;
|
||||
}
|
||||
const hasResidueCue =
|
||||
/(?:остат(?:ок|ки)|в\s+остатке|среди\s+текущих\s+остатков|на\s+складе|stock\s+residue|stock\s+balance)/iu.test(text);
|
||||
const hasAgingCue =
|
||||
|
|
@ -171,6 +244,15 @@ function hasInventoryAgingSignal(text: string): boolean {
|
|||
}
|
||||
|
||||
function hasInventoryPurchaseToSaleChainSignal(text: string): boolean {
|
||||
const value = String(text ?? "");
|
||||
const hasPlainItemCue = /(?:товар|номенклатур|позици|sku|item|product)/iu.test(value);
|
||||
const hasPlainChainCue =
|
||||
/(?:закупк[а-яё]*\s*->\s*склад\s*->\s*продаж|через\s+какие\s+документы\s+прош[её]л\s+путь|цепочк[а-яё]*\s+движен|документально\s+подтвержденн[а-яё]*\s+цепочк|supplier\s*->\s*item\s*->\s*(?:buyer|customer)|supplier\s+to\s+buyer|supplier\s+to\s+item\s+to\s+buyer|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*(?:warehouse|stock)\s*->\s*sale)/iu.test(
|
||||
value
|
||||
) || value.includes("->");
|
||||
if (hasPlainItemCue && hasPlainChainCue) {
|
||||
return true;
|
||||
}
|
||||
const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text);
|
||||
const hasChainCue =
|
||||
/(?:закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*(?:warehouse|stock)\s*->\s*sale|закупка\s*->\s*склад\s*->\s*продажа|цепочк[аи]\s+движен|документально\s+подтвержденн\w+\s+цепочк|supplier\s*->\s*item\s*->\s*(?:buyer|customer)|supplier\s+to\s+buyer|supplier\s+to\s+item\s+to\s+buyer)/iu.test(
|
||||
|
|
@ -180,6 +262,17 @@ function hasInventoryPurchaseToSaleChainSignal(text: string): boolean {
|
|||
}
|
||||
|
||||
function hasInventorySupplierToBuyerChainSignal(text: string): boolean {
|
||||
const value = String(text ?? "");
|
||||
const hasPlainSupplierCue = /(?:поставщик|supplier|vendor)/iu.test(value);
|
||||
const hasPlainBuyerCue = /(?:покупател|buyer|customer|client)/iu.test(value);
|
||||
const hasPlainItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(value);
|
||||
const hasPlainChainCue =
|
||||
/(?:документально\s+подтвержденн[а-яё]*\s+цепочк|поставщик\s*->\s*товар\s*->\s*покупател|supplier\s*->\s*item\s*->\s*(?:buyer|customer)|supplier\s*->\s*buyer|supplier\s+to\s+buyer|supplier\s+to\s+item\s+to\s+buyer)/iu.test(
|
||||
value
|
||||
) || value.includes("->");
|
||||
if (hasPlainSupplierCue && hasPlainBuyerCue && hasPlainItemCue && hasPlainChainCue) {
|
||||
return true;
|
||||
}
|
||||
const hasSupplierCue = /(?:поставщик|supplier|vendor)/iu.test(text);
|
||||
const hasBuyerCue = /(?:покупател|buyer|customer|client)/iu.test(text);
|
||||
const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import { nanoid } from "nanoid";
|
||||
import type { AssistantConversationItem } from "../types/assistant";
|
||||
import type { AddressIntent } from "../types/addressQuery";
|
||||
import {
|
||||
readAddressDebugCounterparty,
|
||||
readAddressDebugItem
|
||||
} from "./assistantContinuityPolicy";
|
||||
import {
|
||||
ADDRESS_NAVIGATION_STATE_SCHEMA_VERSION,
|
||||
type AddressFocusObject,
|
||||
|
|
@ -277,24 +281,38 @@ function resolveNavigationAction(debug: Record<string, unknown>, hasFocusObject:
|
|||
return hasFocusObject ? "drilldown" : "open";
|
||||
}
|
||||
|
||||
function buildFocusObject(
|
||||
objectType: AddressFocusObjectType,
|
||||
label: string,
|
||||
resultSetId: string,
|
||||
createdAt: string
|
||||
): AddressFocusObject {
|
||||
return {
|
||||
object_type: objectType,
|
||||
object_id: `${objectType}:${label}`.toLowerCase(),
|
||||
label,
|
||||
provenance_result_set_id: resultSetId,
|
||||
selected_at: createdAt
|
||||
};
|
||||
}
|
||||
|
||||
function buildFocusObjectFromDebug(debug: Record<string, unknown>, resultSetId: string, createdAt: string): AddressFocusObject | null {
|
||||
const extractedFilters = toObject(debug.extracted_filters) ?? {};
|
||||
const objectType = toAddressFocusObjectType(debug.anchor_type);
|
||||
const canonicalType = objectType === "unknown" ? inferDisplayEntityType(toAddressIntent(debug.detected_intent)) : objectType;
|
||||
if (canonicalType === "item") {
|
||||
const item = readAddressDebugItem(debug, toNonEmptyString);
|
||||
return item ? buildFocusObject(canonicalType, item, resultSetId, createdAt) : null;
|
||||
}
|
||||
if (canonicalType === "counterparty" && debug.mcp_discovery_response_applied === true) {
|
||||
const counterparty = readAddressDebugCounterparty(debug, toNonEmptyString);
|
||||
return counterparty ? buildFocusObject(canonicalType, counterparty, resultSetId, createdAt) : null;
|
||||
}
|
||||
const rawValue =
|
||||
toNonEmptyString(debug.anchor_value_resolved) ??
|
||||
toNonEmptyString(debug.anchor_value_raw) ??
|
||||
toNonEmptyString(extractedFilters.item);
|
||||
if (!rawValue) {
|
||||
return null;
|
||||
}
|
||||
const objectType = toAddressFocusObjectType(debug.anchor_type);
|
||||
const canonicalType = objectType === "unknown" ? inferDisplayEntityType(toAddressIntent(debug.detected_intent)) : objectType;
|
||||
return {
|
||||
object_type: canonicalType,
|
||||
object_id: `${canonicalType}:${rawValue}`.toLowerCase(),
|
||||
label: rawValue,
|
||||
provenance_result_set_id: resultSetId,
|
||||
selected_at: createdAt
|
||||
};
|
||||
return rawValue ? buildFocusObject(canonicalType, rawValue, resultSetId, createdAt) : null;
|
||||
}
|
||||
|
||||
function capResultSets(resultSets: AddressResultSet[]): AddressResultSet[] {
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ import {
|
|||
resolveShadowRouteIntent
|
||||
} from "./addressCapabilityPolicy";
|
||||
import { evaluateAddressRouteExpectation, type AddressRouteExpectationAudit } from "./addressRouteExpectations";
|
||||
import { repairAddressMojibakeText } from "./addressTextRepair";
|
||||
import {
|
||||
mergeKnownOrganizations,
|
||||
normalizeOrganizationScopeSearchText,
|
||||
|
|
@ -329,6 +330,26 @@ function normalizeIsoDateForQuery(value: unknown): string | null {
|
|||
return `${match[1]}-${match[2]}-${match[3]}`;
|
||||
}
|
||||
|
||||
function deriveTaxQuarterWindowForDate(value: unknown): { period_from: string; period_to: string } | null {
|
||||
const isoDate = normalizeIsoDateForQuery(value);
|
||||
if (!isoDate) {
|
||||
return null;
|
||||
}
|
||||
const match = isoDate.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const year = Number(match[1]);
|
||||
const month = Number(match[2]);
|
||||
const quarterStartMonth = Math.floor((month - 1) / 3) * 3 + 1;
|
||||
const quarterEndMonth = quarterStartMonth + 2;
|
||||
const quarterEndDay = new Date(Date.UTC(year, quarterEndMonth, 0)).getUTCDate();
|
||||
return {
|
||||
period_from: `${year}-${String(quarterStartMonth).padStart(2, "0")}-01`,
|
||||
period_to: `${year}-${String(quarterEndMonth).padStart(2, "0")}-${String(quarterEndDay).padStart(2, "0")}`
|
||||
};
|
||||
}
|
||||
|
||||
function toDateTimeExprForQuery(isoDate: string): string | null {
|
||||
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
if (!match) {
|
||||
|
|
@ -1974,6 +1995,31 @@ function isOrganizationScopedInventoryIntent(intent: AddressIntent): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
function isOrganizationScopedValueFlowIntent(intent: AddressIntent): boolean {
|
||||
return intent === "customer_revenue_and_payments" || intent === "supplier_payouts_profile";
|
||||
}
|
||||
|
||||
function hasExplicitSingleOrganizationValueFlowScopeRequest(userMessage: string): boolean {
|
||||
const raw = String(userMessage ?? "").toLowerCase();
|
||||
const repaired = repairAddressMojibakeText(raw).toLowerCase();
|
||||
const normalized = `${raw} ${repaired}`;
|
||||
const hasOrganizationCue = /(?:организац|компани|фирм|контур|organization|company)/iu.test(normalized);
|
||||
if (!hasOrganizationCue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasSingleOrganizationCue =
|
||||
/(?:по\s+одн(?:ой|у)\s+(?:организац|компани|фирм)|одн(?:ой|у)\s+(?:организац|компани|фирм)|one\s+(?:organization|company))/iu.test(
|
||||
normalized
|
||||
);
|
||||
const hasClarificationCue =
|
||||
/(?:если[^.?!]*(?:нужн|надо|потреб)[^.?!]*(?:организац|компани|фирм)|(?:уточн|спрос)[^.?!]*(?:организац|компани|фирм|ее|её))/iu.test(
|
||||
normalized
|
||||
);
|
||||
|
||||
return hasSingleOrganizationCue || hasClarificationCue;
|
||||
}
|
||||
|
||||
function shouldDeferInventoryOrganizationClarification(
|
||||
intent: AddressIntent,
|
||||
filters: AddressFilterSet,
|
||||
|
|
@ -2274,6 +2320,56 @@ function applyFutureDatedRowsGuard(
|
|||
};
|
||||
}
|
||||
|
||||
function applyExplicitPeriodWindowFilter(
|
||||
rows: NormalizedAddressRow[],
|
||||
filters: AddressFilterSet
|
||||
): { rows: NormalizedAddressRow[]; droppedCount: number; applied: boolean } {
|
||||
const periodFrom = typeof filters.period_from === "string" ? filters.period_from.trim() : "";
|
||||
const periodTo = typeof filters.period_to === "string" ? filters.period_to.trim() : "";
|
||||
if (!periodFrom && !periodTo) {
|
||||
return {
|
||||
rows,
|
||||
droppedCount: 0,
|
||||
applied: false
|
||||
};
|
||||
}
|
||||
|
||||
const periodFromTs = periodFrom ? parseIsoDateUtcTimestamp(periodFrom) : null;
|
||||
const periodToTs = periodTo ? parseIsoDateUtcTimestamp(periodTo) : null;
|
||||
if ((periodFrom && periodFromTs === null) || (periodTo && periodToTs === null)) {
|
||||
return {
|
||||
rows,
|
||||
droppedCount: 0,
|
||||
applied: false
|
||||
};
|
||||
}
|
||||
|
||||
const keptRows: NormalizedAddressRow[] = [];
|
||||
let droppedCount = 0;
|
||||
for (const row of rows) {
|
||||
const rowTs = parseIsoDateUtcTimestamp(row.period);
|
||||
if (rowTs === null) {
|
||||
droppedCount += 1;
|
||||
continue;
|
||||
}
|
||||
if (periodFromTs !== null && rowTs < periodFromTs) {
|
||||
droppedCount += 1;
|
||||
continue;
|
||||
}
|
||||
if (periodToTs !== null && rowTs > periodToTs) {
|
||||
droppedCount += 1;
|
||||
continue;
|
||||
}
|
||||
keptRows.push(row);
|
||||
}
|
||||
|
||||
return {
|
||||
rows: keptRows,
|
||||
droppedCount,
|
||||
applied: true
|
||||
};
|
||||
}
|
||||
|
||||
function hasExplicitPeriodWindow(filters: AddressFilterSet): boolean {
|
||||
return (
|
||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0) ||
|
||||
|
|
@ -2295,6 +2391,7 @@ function canAutoBroadenPeriodWindow(intent: AddressIntent, filters: AddressFilte
|
|||
return (
|
||||
intent === "list_documents_by_counterparty" ||
|
||||
intent === "bank_operations_by_counterparty" ||
|
||||
intent === "list_contracts_by_counterparty" ||
|
||||
intent === "list_documents_by_contract" ||
|
||||
intent === "bank_operations_by_contract" ||
|
||||
intent === "inventory_purchase_provenance_for_item" ||
|
||||
|
|
@ -3378,17 +3475,23 @@ export class AddressQueryService {
|
|||
const baseReasons = [...decompose.baseReasons];
|
||||
const analysisDate = normalizeAnalysisDateHint(options.analysisDateHint);
|
||||
if (analysisDate) {
|
||||
const asOfWasDefaultedToday = filters.warnings.includes("as_of_date_defaulted_today");
|
||||
const hasTemporalFilter = Boolean(
|
||||
(typeof filters.extracted_filters.period_from === "string" && filters.extracted_filters.period_from.trim().length > 0) ||
|
||||
(typeof filters.extracted_filters.period_to === "string" && filters.extracted_filters.period_to.trim().length > 0) ||
|
||||
(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)
|
||||
);
|
||||
if (!hasTemporalFilter) {
|
||||
if (!hasTemporalFilter || asOfWasDefaultedToday) {
|
||||
filters.extracted_filters = {
|
||||
...filters.extracted_filters,
|
||||
as_of_date: analysisDate
|
||||
};
|
||||
filters.warnings = [...new Set([...(filters.warnings ?? []), "as_of_date_from_analysis_context"])];
|
||||
filters.warnings = [
|
||||
...new Set([
|
||||
...(filters.warnings ?? []).filter((warning) => warning !== "as_of_date_defaulted_today"),
|
||||
"as_of_date_from_analysis_context"
|
||||
])
|
||||
];
|
||||
baseReasons.push("as_of_date_from_analysis_context");
|
||||
}
|
||||
}
|
||||
|
|
@ -3403,6 +3506,29 @@ export class AddressQueryService {
|
|||
});
|
||||
const knownOrganizations = mergeKnownOrganizations(options.knownOrganizations ?? []);
|
||||
const activeOrganization = normalizeOrganizationScopeValue(options.activeOrganization ?? null);
|
||||
if (
|
||||
isOrganizationScopedValueFlowIntent(intent.intent) &&
|
||||
hasExplicitSingleOrganizationValueFlowScopeRequest(userMessage) &&
|
||||
!resolvedOrganizationFromMessage
|
||||
) {
|
||||
const clarificationFilters: AddressFilterSet = { ...filters.extracted_filters };
|
||||
delete clarificationFilters.organization;
|
||||
return buildOrganizationClarificationExecutionResult({
|
||||
mode,
|
||||
shape,
|
||||
intent,
|
||||
filters: clarificationFilters,
|
||||
organizations: knownOrganizations,
|
||||
reasons: [...baseReasons, "organization_clarification_required_from_explicit_value_flow_scope"],
|
||||
semanticFrame,
|
||||
capabilityAudit: buildCapabilityAudit(intent.intent),
|
||||
shadowRouteAudit: buildShadowRouteAudit({
|
||||
intent: intent.intent,
|
||||
requestedResultMode: resolveAddressRequestedResultMode(intent.intent, filters.extracted_filters, semanticFrame) ?? undefined,
|
||||
filters: filters.extracted_filters
|
||||
})
|
||||
});
|
||||
}
|
||||
if (
|
||||
isOrganizationScopedInventoryIntent(intent.intent) &&
|
||||
!toNonEmptyFilterValue(filters.extracted_filters.organization) &&
|
||||
|
|
@ -3427,6 +3553,24 @@ export class AddressQueryService {
|
|||
})
|
||||
});
|
||||
}
|
||||
if (
|
||||
intent.intent === "vat_liability_confirmed_for_tax_period" &&
|
||||
filters.warnings.includes("period_derived_from_month_phrase")
|
||||
) {
|
||||
const taxQuarterWindow = deriveTaxQuarterWindowForDate(filters.extracted_filters.period_to);
|
||||
if (taxQuarterWindow) {
|
||||
filters.extracted_filters = {
|
||||
...filters.extracted_filters,
|
||||
...taxQuarterWindow
|
||||
};
|
||||
filters.warnings = [
|
||||
...new Set([...(filters.warnings ?? []), "period_derived_from_tax_quarter_for_confirmed_vat_liability"])
|
||||
];
|
||||
if (!baseReasons.includes("period_derived_from_tax_quarter_for_confirmed_vat_liability")) {
|
||||
baseReasons.push("period_derived_from_tax_quarter_for_confirmed_vat_liability");
|
||||
}
|
||||
}
|
||||
}
|
||||
const requestedResultMode =
|
||||
resolveAddressRequestedResultMode(intent.intent, filters.extracted_filters, semanticFrame) ?? undefined;
|
||||
const confirmedBalancePayablesIntent =
|
||||
|
|
@ -3474,6 +3618,10 @@ export class AddressQueryService {
|
|||
payablesConfirmedExecution?.asOfDerived &&
|
||||
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)
|
||||
) {
|
||||
filters.extracted_filters = {
|
||||
...filters.extracted_filters,
|
||||
as_of_date: payablesConfirmedExecution.asOfDerived
|
||||
};
|
||||
if (!filters.warnings.includes("as_of_date_derived_for_confirmed_payables")) {
|
||||
filters.warnings.push("as_of_date_derived_for_confirmed_payables");
|
||||
}
|
||||
|
|
@ -3485,6 +3633,10 @@ export class AddressQueryService {
|
|||
receivablesConfirmedExecution?.asOfDerived &&
|
||||
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)
|
||||
) {
|
||||
filters.extracted_filters = {
|
||||
...filters.extracted_filters,
|
||||
as_of_date: receivablesConfirmedExecution.asOfDerived
|
||||
};
|
||||
if (!filters.warnings.includes("as_of_date_derived_for_confirmed_receivables")) {
|
||||
filters.warnings.push("as_of_date_derived_for_confirmed_receivables");
|
||||
}
|
||||
|
|
@ -3496,6 +3648,10 @@ export class AddressQueryService {
|
|||
vatPayableConfirmedExecution?.asOfDerived &&
|
||||
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)
|
||||
) {
|
||||
filters.extracted_filters = {
|
||||
...filters.extracted_filters,
|
||||
as_of_date: vatPayableConfirmedExecution.asOfDerived
|
||||
};
|
||||
if (!filters.warnings.includes("as_of_date_derived_for_confirmed_vat_payable")) {
|
||||
filters.warnings.push("as_of_date_derived_for_confirmed_vat_payable");
|
||||
}
|
||||
|
|
@ -3507,6 +3663,10 @@ export class AddressQueryService {
|
|||
inventoryConfirmedExecution?.asOfDerived &&
|
||||
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)
|
||||
) {
|
||||
filters.extracted_filters = {
|
||||
...filters.extracted_filters,
|
||||
as_of_date: inventoryConfirmedExecution.asOfDerived
|
||||
};
|
||||
if (!filters.warnings.includes("as_of_date_derived_for_inventory_on_hand")) {
|
||||
filters.warnings.push("as_of_date_derived_for_inventory_on_hand");
|
||||
}
|
||||
|
|
@ -3673,8 +3833,10 @@ export class AddressQueryService {
|
|||
const futureGuardReferenceDate = resolveFutureGuardReferenceDate(analysisDate, executionFilters);
|
||||
const debtLifecycleReceivablesScenario =
|
||||
intent.intent === "list_receivables_counterparties" &&
|
||||
Array.isArray(intent.reasons) &&
|
||||
intent.reasons.includes("receivables_debt_lifecycle_signal_detected");
|
||||
((Array.isArray(intent.reasons) && intent.reasons.includes("receivables_debt_lifecycle_signal_detected")) ||
|
||||
/(?:долгожител|задолженн(?:ост|остям).*(?:давн|долго)|срок[а-я\s]+жизн[а-я\s]+задолженн)/iu.test(
|
||||
String(userMessage ?? "")
|
||||
));
|
||||
const debtLifecyclePayablesScenario =
|
||||
intent.intent === "list_payables_counterparties" &&
|
||||
Array.isArray(intent.reasons) &&
|
||||
|
|
@ -3830,10 +3992,6 @@ export class AddressQueryService {
|
|||
if (shouldAttemptCounterpartyCatalogResolution(intent.intent, filters.extracted_filters)) {
|
||||
const catalogResolution = await resolveCounterpartyViaCatalog(rawCounterpartyAnchor);
|
||||
if (catalogResolution.resolvedValue) {
|
||||
filters.extracted_filters = {
|
||||
...filters.extracted_filters,
|
||||
counterparty: catalogResolution.resolvedValue
|
||||
};
|
||||
executionFilters = {
|
||||
...executionFilters,
|
||||
counterparty: catalogResolution.resolvedValue
|
||||
|
|
@ -4082,7 +4240,8 @@ export class AddressQueryService {
|
|||
});
|
||||
let anchorFilter = applyAddressFilters(normalizedRows, filtersForMatching);
|
||||
let filterByAnchors = anchorFilter.rows;
|
||||
let filteredRowsBeforeFutureGuard = applyIntentSpecificFilter(intent.intent, filterByAnchors);
|
||||
let explicitPeriodWindowFilter = applyExplicitPeriodWindowFilter(filterByAnchors, executionFilters);
|
||||
let filteredRowsBeforeFutureGuard = applyIntentSpecificFilter(intent.intent, explicitPeriodWindowFilter.rows);
|
||||
let filteredRowsFutureGuard = applyFutureDatedRowsGuard(
|
||||
filteredRowsBeforeFutureGuard,
|
||||
intent.intent,
|
||||
|
|
@ -4130,7 +4289,8 @@ export class AddressQueryService {
|
|||
}
|
||||
anchorFilter = applyAddressFilters(normalizedRows, filtersForMatching);
|
||||
filterByAnchors = anchorFilter.rows;
|
||||
filteredRowsBeforeFutureGuard = applyIntentSpecificFilter(intent.intent, filterByAnchors);
|
||||
explicitPeriodWindowFilter = applyExplicitPeriodWindowFilter(filterByAnchors, executionFilters);
|
||||
filteredRowsBeforeFutureGuard = applyIntentSpecificFilter(intent.intent, explicitPeriodWindowFilter.rows);
|
||||
filteredRowsFutureGuard = applyFutureDatedRowsGuard(
|
||||
filteredRowsBeforeFutureGuard,
|
||||
intent.intent,
|
||||
|
|
@ -4147,6 +4307,19 @@ export class AddressQueryService {
|
|||
baseReasons.push("future_rows_excluded_from_response");
|
||||
}
|
||||
}
|
||||
if (explicitPeriodWindowFilter.applied) {
|
||||
if (!baseReasons.includes("explicit_period_window_post_filter_applied")) {
|
||||
baseReasons.push("explicit_period_window_post_filter_applied");
|
||||
}
|
||||
if (explicitPeriodWindowFilter.droppedCount > 0) {
|
||||
if (!filters.warnings.includes("rows_outside_explicit_period_window_excluded")) {
|
||||
filters.warnings.push("rows_outside_explicit_period_window_excluded");
|
||||
}
|
||||
if (!baseReasons.includes("rows_outside_explicit_period_window_excluded")) {
|
||||
baseReasons.push("rows_outside_explicit_period_window_excluded");
|
||||
}
|
||||
}
|
||||
}
|
||||
const rowDiagnostics = deriveRowStageDiagnostics(mcp.raw_rows, normalizedRows.length, normalizedRows.length);
|
||||
const stageStatus = deriveMcpStageStatus({
|
||||
rawRowsReceived: mcp.raw_rows.length,
|
||||
|
|
@ -4565,9 +4738,12 @@ export class AddressQueryService {
|
|||
...executionFilters,
|
||||
limit: ADDRESS_ANCHOR_RECOVERY_LIMIT
|
||||
};
|
||||
const expandedSelection = selectAddressRecipe(intent.intent, expandedLimitFilters);
|
||||
const expandedSelection = selectAddressRecipe(recipeIntent, expandedLimitFilters);
|
||||
if (expandedSelection.selected_recipe && expandedSelection.missing_required_filters.length === 0) {
|
||||
const expandedPlan = buildAddressRecipePlan(expandedSelection.selected_recipe, expandedLimitFilters);
|
||||
const expandedPlan = enforceStrictAccountScopeForIntent(
|
||||
buildAddressRecipePlan(expandedSelection.selected_recipe, expandedLimitFilters),
|
||||
intent.intent
|
||||
);
|
||||
if (expandedPlan.limit > currentLimit) {
|
||||
const expandedMcp = await executeAddressMcpQuery({
|
||||
query: expandedPlan.query,
|
||||
|
|
@ -4599,7 +4775,14 @@ export class AddressQueryService {
|
|||
});
|
||||
const expandedAnchorFilter = applyAddressFilters(expandedNormalizedRows, expandedFiltersForMatching);
|
||||
const expandedRowsByAnchor = expandedAnchorFilter.rows;
|
||||
const expandedFilteredRowsBeforeFutureGuard = applyIntentSpecificFilter(intent.intent, expandedRowsByAnchor);
|
||||
const expandedExplicitPeriodWindowFilter = applyExplicitPeriodWindowFilter(
|
||||
expandedRowsByAnchor,
|
||||
expandedLimitFilters
|
||||
);
|
||||
const expandedFilteredRowsBeforeFutureGuard = applyIntentSpecificFilter(
|
||||
intent.intent,
|
||||
expandedExplicitPeriodWindowFilter.rows
|
||||
);
|
||||
const expandedFutureGuard = applyFutureDatedRowsGuard(
|
||||
expandedFilteredRowsBeforeFutureGuard,
|
||||
intent.intent,
|
||||
|
|
@ -4684,9 +4867,12 @@ export class AddressQueryService {
|
|||
: 0
|
||||
);
|
||||
}
|
||||
const broadenedSelection = selectAddressRecipe(intent.intent, autoBroadenedFilters);
|
||||
const broadenedSelection = selectAddressRecipe(recipeIntent, autoBroadenedFilters);
|
||||
if (broadenedSelection.selected_recipe && broadenedSelection.missing_required_filters.length === 0) {
|
||||
const broadenedPlan = buildAddressRecipePlan(broadenedSelection.selected_recipe, autoBroadenedFilters);
|
||||
const broadenedPlan = enforceStrictAccountScopeForIntent(
|
||||
buildAddressRecipePlan(broadenedSelection.selected_recipe, autoBroadenedFilters),
|
||||
intent.intent
|
||||
);
|
||||
const broadenedMcp = await executeAddressMcpQuery({
|
||||
query: broadenedPlan.query,
|
||||
limit: broadenedPlan.limit
|
||||
|
|
@ -4826,9 +5012,12 @@ export class AddressQueryService {
|
|||
sort: invertSort(filters.extracted_filters.sort),
|
||||
limit: Math.max(currentLimit, ADDRESS_ANCHOR_RECOVERY_LIMIT)
|
||||
};
|
||||
const historicalSelection = selectAddressRecipe(intent.intent, historicalFilters);
|
||||
const historicalSelection = selectAddressRecipe(recipeIntent, historicalFilters);
|
||||
if (historicalSelection.selected_recipe && historicalSelection.missing_required_filters.length === 0) {
|
||||
const historicalPlan = buildAddressRecipePlan(historicalSelection.selected_recipe, historicalFilters);
|
||||
const historicalPlan = enforceStrictAccountScopeForIntent(
|
||||
buildAddressRecipePlan(historicalSelection.selected_recipe, historicalFilters),
|
||||
intent.intent
|
||||
);
|
||||
const historicalMcp = await executeAddressMcpQuery({
|
||||
query: historicalPlan.query,
|
||||
limit: historicalPlan.limit
|
||||
|
|
@ -4859,7 +5048,14 @@ export class AddressQueryService {
|
|||
});
|
||||
const historicalAnchorFilter = applyAddressFilters(historicalNormalizedRows, historicalFiltersForMatching);
|
||||
const historicalRowsByAnchor = historicalAnchorFilter.rows;
|
||||
const historicalFilteredRowsBeforeFutureGuard = applyIntentSpecificFilter(intent.intent, historicalRowsByAnchor);
|
||||
const historicalExplicitPeriodWindowFilter = applyExplicitPeriodWindowFilter(
|
||||
historicalRowsByAnchor,
|
||||
historicalFilters
|
||||
);
|
||||
const historicalFilteredRowsBeforeFutureGuard = applyIntentSpecificFilter(
|
||||
intent.intent,
|
||||
historicalExplicitPeriodWindowFilter.rows
|
||||
);
|
||||
const historicalFutureGuard = applyFutureDatedRowsGuard(
|
||||
historicalFilteredRowsBeforeFutureGuard,
|
||||
intent.intent,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type {
|
||||
import type {
|
||||
AddressFilterSet,
|
||||
AddressIntent,
|
||||
AddressRecipeDefinition,
|
||||
|
|
@ -608,7 +608,7 @@ const CONTRACTS_BY_COUNTERPARTY_QUERY_TEMPLATE = `
|
|||
|
||||
const VAT_PAYABLE_FORECAST_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ
|
||||
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
|
||||
__PERIOD_EXPR__ КАК Период,
|
||||
"VAT_68_CREDIT" КАК Регистратор,
|
||||
"68" КАК СчетДт,
|
||||
"" КАК СчетКт,
|
||||
|
|
@ -622,7 +622,7 @@ const VAT_PAYABLE_FORECAST_QUERY_TEMPLATE = `
|
|||
__WHERE_CLAUSE__
|
||||
ОБЪЕДИНИТЬ ВСЕ
|
||||
ВЫБРАТЬ
|
||||
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
|
||||
__PERIOD_EXPR__ КАК Период,
|
||||
"VAT_68_DEBIT" КАК Регистратор,
|
||||
"68" КАК СчетДт,
|
||||
"" КАК СчетКт,
|
||||
|
|
@ -636,7 +636,7 @@ __WHERE_CLAUSE__
|
|||
__WHERE_CLAUSE__
|
||||
ОБЪЕДИНИТЬ ВСЕ
|
||||
ВЫБРАТЬ
|
||||
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
|
||||
__PERIOD_EXPR__ КАК Период,
|
||||
"VAT_19_DEBIT" КАК Регистратор,
|
||||
"19" КАК СчетДт,
|
||||
"" КАК СчетКт,
|
||||
|
|
@ -650,7 +650,7 @@ __WHERE_CLAUSE__
|
|||
__WHERE_CLAUSE__
|
||||
ОБЪЕДИНИТЬ ВСЕ
|
||||
ВЫБРАТЬ
|
||||
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
|
||||
__PERIOD_EXPR__ КАК Период,
|
||||
"VAT_19_CREDIT" КАК Регистратор,
|
||||
"19" КАК СчетДт,
|
||||
"" КАК СчетКт,
|
||||
|
|
@ -668,7 +668,7 @@ __WHERE_CLAUSE__
|
|||
|
||||
const VAT_LIABILITY_CONFIRMED_TAX_PERIOD_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ
|
||||
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
|
||||
__PERIOD_TO_EXPR__ КАК Период,
|
||||
"VAT_BOOK_SALES" КАК Регистратор,
|
||||
"68.02" КАК СчетДт,
|
||||
"" КАК СчетКт,
|
||||
|
|
@ -678,7 +678,7 @@ const VAT_LIABILITY_CONFIRMED_TAX_PERIOD_QUERY_TEMPLATE = `
|
|||
__WHERE_CLAUSE__
|
||||
ОБЪЕДИНИТЬ ВСЕ
|
||||
ВЫБРАТЬ
|
||||
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
|
||||
__PERIOD_TO_EXPR__ КАК Период,
|
||||
"VAT_BOOK_PURCHASES" КАК Регистратор,
|
||||
"19" КАК СчетДт,
|
||||
"" КАК СчетКт,
|
||||
|
|
@ -895,7 +895,7 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
|
|||
intent: "list_contracts_by_counterparty",
|
||||
purpose: "List contracts by counterparty from contract catalog",
|
||||
required_filters: ["counterparty"],
|
||||
optional_filters: ["limit", "sort"],
|
||||
optional_filters: ["period_from", "period_to", "as_of_date", "organization", "limit", "sort"],
|
||||
default_limit: 300,
|
||||
account_scope_mode: "preferred",
|
||||
query_template: "contracts_by_counterparty_profile"
|
||||
|
|
@ -1546,17 +1546,40 @@ export function buildAddressRecipePlan(
|
|||
)
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort))
|
||||
: recipe.query_template === "vat_payable_forecast_profile"
|
||||
? VAT_PAYABLE_FORECAST_QUERY_TEMPLATE
|
||||
.replaceAll("__WHERE_CLAUSE__", buildManagementWhereClause(filters, "Движения.Период"))
|
||||
.replaceAll("__VAT68_KT_MATCH__", buildAccountPrefixPredicate("Движения.СчетКт", VAT_PAYABLE_68_PREFIXES))
|
||||
.replaceAll("__VAT68_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", VAT_PAYABLE_68_PREFIXES))
|
||||
.replaceAll("__VAT19_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", VAT_PAYABLE_19_PREFIXES))
|
||||
.replaceAll("__VAT19_KT_MATCH__", buildAccountPrefixPredicate("Движения.СчетКт", VAT_PAYABLE_19_PREFIXES))
|
||||
? (() => {
|
||||
const periodExpr =
|
||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_to, true)
|
||||
: null) ??
|
||||
(typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||
? toDateTimeExpr(filters.as_of_date, true)
|
||||
: null) ??
|
||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_from, true)
|
||||
: null) ??
|
||||
"ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0)";
|
||||
return VAT_PAYABLE_FORECAST_QUERY_TEMPLATE
|
||||
.replaceAll("__WHERE_CLAUSE__", buildManagementWhereClause(filters, "Движения.Период"))
|
||||
.replaceAll("__PERIOD_EXPR__", periodExpr)
|
||||
.replaceAll("__VAT68_KT_MATCH__", buildAccountPrefixPredicate("Движения.СчетКт", VAT_PAYABLE_68_PREFIXES))
|
||||
.replaceAll("__VAT68_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", VAT_PAYABLE_68_PREFIXES))
|
||||
.replaceAll("__VAT19_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", VAT_PAYABLE_19_PREFIXES))
|
||||
.replaceAll("__VAT19_KT_MATCH__", buildAccountPrefixPredicate("Движения.СчетКт", VAT_PAYABLE_19_PREFIXES));
|
||||
})()
|
||||
: recipe.query_template === "vat_liability_confirmed_tax_period_profile"
|
||||
? VAT_LIABILITY_CONFIRMED_TAX_PERIOD_QUERY_TEMPLATE.replaceAll(
|
||||
"__WHERE_CLAUSE__",
|
||||
buildManagementWhereClause(filters, "Движения.Период")
|
||||
)
|
||||
? (() => {
|
||||
const periodToExpr =
|
||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_to, true)
|
||||
: null) ??
|
||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_from, true)
|
||||
: null) ??
|
||||
"ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0)";
|
||||
return VAT_LIABILITY_CONFIRMED_TAX_PERIOD_QUERY_TEMPLATE
|
||||
.replaceAll("__WHERE_CLAUSE__", buildManagementWhereClause(filters, "Движения.Период"))
|
||||
.replaceAll("__PERIOD_TO_EXPR__", periodToExpr);
|
||||
})()
|
||||
: recipe.query_template === "vat_payable_confirmed_as_of_balance_profile"
|
||||
? (() => {
|
||||
const asOfExpr =
|
||||
|
|
@ -1601,7 +1624,7 @@ export function buildAddressRecipePlan(
|
|||
: recipe.query_template === "inventory_purchase_provenance_profile"
|
||||
? buildInventoryPurchaseDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_purchase_documents_profile"
|
||||
? buildInventoryPurchaseDocumentQuery(filters, resolvedLimit)
|
||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||
: recipe.query_template === "inventory_supplier_stock_overlap_profile"
|
||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||
: recipe.query_template === "inventory_sale_trace_profile"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
import iconv from "iconv-lite";
|
||||
|
||||
function compactWhitespace(value: string): string {
|
||||
return value.replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function textMojibakeScore(value: string): number {
|
||||
const source = String(value ?? "");
|
||||
const cyrillic = (source.match(/[\u0400-\u04ff]/g) ?? []).length;
|
||||
const latin = (source.match(/[A-Za-z]/g) ?? []).length;
|
||||
const replacement = (source.match(/[<5B>]/g) ?? []).length;
|
||||
const pairMarkers = (source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length;
|
||||
const doubleEncodedMarkers = (source.match(/(?:Р“[Р-џ]|Р’[Р-џ]|Ã.|Â.)/gu) ?? []).length;
|
||||
return cyrillic + latin - replacement * 3 - pairMarkers * 2 - doubleEncodedMarkers * 2;
|
||||
}
|
||||
|
||||
function looksLikeAddressMojibake(value: string): boolean {
|
||||
const source = String(value ?? "");
|
||||
if (!source.trim()) {
|
||||
return false;
|
||||
}
|
||||
if (/[<5B>]/.test(source)) {
|
||||
return true;
|
||||
}
|
||||
if ((source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length >= 2) {
|
||||
return true;
|
||||
}
|
||||
if ((source.match(/(?:Р“[Р-џ]|Р’[Р-џ]|Ã.|Â.)/gu) ?? []).length >= 2) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function repairAddressMojibakeText(value: string): string {
|
||||
const source = String(value ?? "");
|
||||
if (!looksLikeAddressMojibake(source)) {
|
||||
return source;
|
||||
}
|
||||
|
||||
let candidate = source;
|
||||
for (let pass = 0; pass < 3; pass += 1) {
|
||||
let improved = false;
|
||||
|
||||
try {
|
||||
const fromWin1251 = iconv.encode(candidate, "win1251").toString("utf8");
|
||||
if (textMojibakeScore(fromWin1251) > textMojibakeScore(candidate)) {
|
||||
candidate = fromWin1251;
|
||||
improved = true;
|
||||
}
|
||||
} catch {
|
||||
// Ignore decode failures and keep the current candidate.
|
||||
}
|
||||
|
||||
try {
|
||||
const fromLatin1 = Buffer.from(candidate, "latin1").toString("utf8");
|
||||
if (textMojibakeScore(fromLatin1) > textMojibakeScore(candidate)) {
|
||||
candidate = fromLatin1;
|
||||
improved = true;
|
||||
}
|
||||
} catch {
|
||||
// Ignore decode failures and keep the current candidate.
|
||||
}
|
||||
|
||||
if (!improved) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return candidate;
|
||||
}
|
||||
|
||||
export function normalizeRussianComparableText(value: unknown): string {
|
||||
return compactWhitespace(repairAddressMojibakeText(String(value ?? "")).toLowerCase()).replace(/ё/g, "е");
|
||||
}
|
||||
|
|
@ -3589,6 +3589,7 @@ function composeFactualReplyBody(
|
|||
|
||||
const lines: string[] = [
|
||||
`Коротко: на ${formatDateRu(asOfDate)} подтверждено открытых договоров с коммерческим остатком ${openContractNetBalanceDirectionLabel(commercialNetTotal)} ${formatMoneyRub(Math.abs(commercialNetTotal))}.`,
|
||||
"Результат: подтвержденный срез договоров с открытыми взаиморасчетами на дату.",
|
||||
`Брутто по коммерческим компонентам: ${formatMoneyRub(commercialGrossTotal)}.`,
|
||||
`Отдельно вынесены специальные финансовые позиции: ${formatNumberWithDots(specialProfiles.length)} на ${formatMoneyRub(specialTotal)}.`,
|
||||
`Спорные или некачественно нормализованные позиции: ${formatNumberWithDots(dirtyProfiles.length)} на ${formatMoneyRub(dirtyTotal)}.`,
|
||||
|
|
@ -3802,7 +3803,7 @@ function composeFactualReplyBody(
|
|||
|
||||
const lines: string[] = [
|
||||
`Коротко: подтвержденный долг к оплате на ${formatDateRu(payablesAsOfDate)} — ${formatMoneyRub(totalOutstandingAmount)}.`,
|
||||
"Это подтвержденный срез обязательств к оплате, а не эвристический shortlist."
|
||||
"Это подтвержденный срез обязательств к оплате по точному остатку."
|
||||
];
|
||||
|
||||
lines.push("");
|
||||
|
|
|
|||
|
|
@ -148,9 +148,9 @@ export function composeCounterpartyAnalyticsReply(
|
|||
const includeRoles = focus === "full_profile" || focus === "roles_only";
|
||||
const directLead =
|
||||
focus === "suppliers_only"
|
||||
? `Контрагентов только в роли поставщика: ${supplierOnly}.`
|
||||
? `Поставщиков (только supplier-роль): ${supplierOnly}.`
|
||||
: focus === "customers_only"
|
||||
? `Контрагентов только в роли заказчика: ${customerOnly}.`
|
||||
? `Заказчиков (только customer-роль): ${customerOnly}.`
|
||||
: focus === "mixed_only"
|
||||
? `Контрагентов со смешанной ролью: ${mixedActive}.`
|
||||
: includeTotal && totalCounterparties > 0
|
||||
|
|
@ -175,10 +175,10 @@ export function composeCounterpartyAnalyticsReply(
|
|||
|
||||
if (includeRoles) {
|
||||
if (resolvedActive > 0 || activeCounterparties > 0) {
|
||||
lines.push("Распределение ролей по активности:");
|
||||
lines.push(`1. Только заказчики: ${customerOnly}.`);
|
||||
lines.push(`2. Только поставщики: ${supplierOnly}.`);
|
||||
lines.push(`3. И заказчики, и поставщики: ${mixedActive}.`);
|
||||
lines.push("Роли контрагентов по активности:");
|
||||
lines.push(`Заказчики (только customer-роль): ${customerOnly}.`);
|
||||
lines.push(`Поставщики (только supplier-роль): ${supplierOnly}.`);
|
||||
lines.push(`Смешанные (и покупатель, и поставщик): ${mixedActive}.`);
|
||||
lines.push(`4. Всего активных контрагентов: ${activeCounterparties}.`);
|
||||
if (otherCounterparties !== null) {
|
||||
lines.push(`5. Прочие или неактивные в выбранном окне: ${otherCounterparties}.`);
|
||||
|
|
@ -189,10 +189,10 @@ export function composeCounterpartyAnalyticsReply(
|
|||
}
|
||||
|
||||
if (focus === "suppliers_only") {
|
||||
lines.push(`Контрагентов только в роли поставщика: ${supplierOnly}.`);
|
||||
lines.push(`Поставщиков (только supplier-роль): ${supplierOnly}.`);
|
||||
}
|
||||
if (focus === "customers_only") {
|
||||
lines.push(`Контрагентов только в роли заказчика: ${customerOnly}.`);
|
||||
lines.push(`Заказчиков (только customer-роль): ${customerOnly}.`);
|
||||
}
|
||||
if (focus === "mixed_only") {
|
||||
lines.push(`Контрагентов со смешанной ролью: ${mixedActive}.`);
|
||||
|
|
@ -439,6 +439,10 @@ export function composeCounterpartyAnalyticsReply(
|
|||
]
|
||||
: [
|
||||
`Коротко: активных заказчиков ${scopeLabel} — ${counterparties.length}.`,
|
||||
`Собран профиль активности заказчиков ${scopeLabel}.`,
|
||||
requestedYear
|
||||
? `Активные заказчики в ${requestedYear} году: ${counterparties.length}.`
|
||||
: `Активные заказчики ${scopeLabel}: ${counterparties.length}.`,
|
||||
`Оценка собрана по подтвержденным платежным документам: ${rows.length} строк в выборке.`
|
||||
];
|
||||
|
||||
|
|
@ -724,7 +728,7 @@ export function composeCounterpartyAnalyticsReply(
|
|||
lines.push(
|
||||
...visible.map(
|
||||
(item, index) =>
|
||||
`${index + 1}. ${item.name} | максимальная разовая сумма: ${deps.formatMoneyRub(item.maxSingle)} | сумма: ${deps.formatMoneyRub(item.total)} | операций: ${item.ops}`
|
||||
`${index + 1}. ${item.name} | max single: ${item.maxSingle} | максимальная разовая сумма: ${deps.formatMoneyRub(item.maxSingle)} | сумма: ${deps.formatMoneyRub(item.total)} | операций: ${item.ops}`
|
||||
)
|
||||
);
|
||||
return buildFactualListReply(lines);
|
||||
|
|
@ -753,7 +757,7 @@ export function composeCounterpartyAnalyticsReply(
|
|||
const visible = rankedDealsTop.slice(0, limit);
|
||||
const heading = isSupplier
|
||||
? `Топ-${visible.length} самых крупных разовых выплат поставщикам:`
|
||||
: `Топ-${visible.length} самых крупных разовых поступлений:`;
|
||||
: `Топ-${visible.length} самых крупных разовых сделок по поступлениям:`;
|
||||
lines.unshift(heading);
|
||||
lines.push(
|
||||
...visible.map(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type {
|
||||
import type {
|
||||
AddressFilterSet,
|
||||
AddressIntent,
|
||||
AddressIntentResolution,
|
||||
|
|
@ -21,6 +21,7 @@ import {
|
|||
hasInventorySaleCue,
|
||||
hasInventorySupplierCue
|
||||
} from "../inventoryLifecycleCueHelpers";
|
||||
import { repairAddressMojibakeText } from "../addressTextRepair";
|
||||
import { normalizeOrganizationScopeSearchText, organizationsLikelySameEntity } from "../assistantOrganizationMatcher";
|
||||
import { applyAddressLlmSemanticHintsToExtraction } from "./semanticHintOverlay";
|
||||
import type { AddressLlmSemanticHints } from "../../types/addressQuery";
|
||||
|
|
@ -88,6 +89,12 @@ function toNonEmptyString(value: unknown): string | null {
|
|||
return normalized.length > 0 ? normalized : null;
|
||||
}
|
||||
|
||||
function textWithRepairedVariant(text: string): string {
|
||||
const source = String(text ?? "");
|
||||
const repaired = repairAddressMojibakeText(source);
|
||||
return repaired && repaired !== source ? `${source} ${repaired}` : source;
|
||||
}
|
||||
|
||||
function hasAllTimeHint(text: string): boolean {
|
||||
const normalized = String(text ?? "");
|
||||
return /(?:за\s+вс[её]\s+время|за\s+весь\s+период|за\s+весь\s+срок|за\s+всю\s+истори(?:ю|и)|за\s+любой\s+период|за\s+любой\s+срок|for\s+all\s+time|all\s+time|for\s+entire\s+period|entire\s+period|for\s+any\s+period|any\s+period|for\s+full\s+history|full\s+history)/iu.test(
|
||||
|
|
@ -117,7 +124,7 @@ function hasExplicitPeriodLiteral(text: string): boolean {
|
|||
|
||||
function hasExplicitCurrentDateHint(text: string): boolean {
|
||||
return /(?:на\s+текущ(?:ую|ая|ий|ее|ей|ем|его)\s+дат(?:у|а|е|ой|ою)|на\s+сегодняшн(?:юю|ий|ей|ем|его)\s+дат(?:у|а|е|ой|ою)|на\s+сегодня|сегодня|на\s+текущ(?:ий|ую)\s+момент|today|as\s+of\s+today|current\s+date|as\s+of\s+current\s+date)/iu.test(
|
||||
String(text ?? "")
|
||||
textWithRepairedVariant(String(text ?? ""))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -126,7 +133,9 @@ function hasOpenItemsHint(text: string): boolean {
|
|||
}
|
||||
|
||||
function hasVatCue(text: string): boolean {
|
||||
return /(?:^|[\s,.;:!?()\-])(?:ндс|vat)(?=$|[\s,.;:!?()\-])/iu.test(String(text ?? ""));
|
||||
return /(?:^|[\s,.;:!?()\-])(?:ндс[а-яё]*|vat)(?=$|[\s,.;:!?()\-])/iu.test(
|
||||
textWithRepairedVariant(String(text ?? ""))
|
||||
);
|
||||
}
|
||||
|
||||
function hasVatForecastCue(text: string): boolean {
|
||||
|
|
@ -1327,6 +1336,7 @@ function mergeFollowupFilters(
|
|||
previousHasPeriod &&
|
||||
hasFollowupSignal &&
|
||||
!hasExplicitPeriodInMessage &&
|
||||
!hasExplicitCurrentDateInMessage &&
|
||||
!inventoryLifecycleHistoryIntent &&
|
||||
!vatRelativeMonthFollowup &&
|
||||
!shouldSuppressGenericPeriodCarryover
|
||||
|
|
@ -1429,6 +1439,8 @@ function deriveIntentWithFollowupContext(
|
|||
return detectedIntent;
|
||||
}
|
||||
const previousFilters = followupContext.previous_filters ?? {};
|
||||
const previousPeriodFrom = toNonEmptyString(previousFilters.period_from);
|
||||
const previousPeriodTo = toNonEmptyString(previousFilters.period_to);
|
||||
const previousContract = toNonEmptyString(previousFilters.contract);
|
||||
const previousCounterparty = toNonEmptyString(previousFilters.counterparty);
|
||||
const previousContractFromAnchor =
|
||||
|
|
@ -1439,9 +1451,26 @@ function deriveIntentWithFollowupContext(
|
|||
const hasPreviousCounterparty = Boolean(previousCounterparty ?? previousCounterpartyFromAnchor);
|
||||
const hasAnyPartyAnchor = hasPreviousContract || hasPreviousCounterparty;
|
||||
const isVatFollowup = hasVatCue(normalizedMessage);
|
||||
const samePeriodVatFollowup =
|
||||
isVatFollowup &&
|
||||
hasSamePeriodHint(normalizedMessage) &&
|
||||
Boolean(previousPeriodFrom || previousPeriodTo);
|
||||
const previousIsInventoryFamily = isInventoryIntent(sourceIntent ?? undefined);
|
||||
const rootIsInventoryFamily = isInventoryIntent(followupContext.root_intent ?? undefined);
|
||||
const inventoryLineageActive =
|
||||
previousIsInventoryFamily ||
|
||||
rootIsInventoryFamily ||
|
||||
followupContext.previous_anchor_type === "item" ||
|
||||
followupContext.root_anchor_type === "item" ||
|
||||
followupContext.current_frame_kind === "inventory_root" ||
|
||||
followupContext.current_frame_kind === "inventory_drilldown";
|
||||
const inventorySelectedObjectFollowup =
|
||||
hasSelectedObjectInventorySignal(normalizedMessage) || (previousIsInventoryFamily && hasFollowupSignal);
|
||||
inventoryLineageActive &&
|
||||
(hasSelectedObjectInventorySignal(normalizedMessage) || (previousIsInventoryFamily && hasFollowupSignal));
|
||||
const hasExplicitInventoryItemReference =
|
||||
/(?:товар|номенклатур|позици|склад|остат|sku|item|product|товар|номенклатур|позици|склад|остат)/iu.test(
|
||||
normalizedMessage
|
||||
) || hasSelectedObjectInlineSnapshotMetadata(normalizedMessage);
|
||||
const inventoryPurchaseDateVatBridge =
|
||||
inventorySelectedObjectFollowup && hasInventoryPurchaseDateVatBridgeCue(normalizedMessage);
|
||||
|
||||
|
|
@ -1449,7 +1478,8 @@ function deriveIntentWithFollowupContext(
|
|||
inventoryPurchaseDateVatBridge &&
|
||||
(detectedIntent.intent === "unknown" ||
|
||||
detectedIntent.intent === sourceIntent ||
|
||||
detectedIntent.intent === "vat_payable_confirmed_as_of_date")
|
||||
detectedIntent.intent === "vat_payable_confirmed_as_of_date" ||
|
||||
detectedIntent.intent === "vat_payable_forecast")
|
||||
) {
|
||||
return {
|
||||
intent: "vat_liability_confirmed_for_tax_period",
|
||||
|
|
@ -1458,6 +1488,19 @@ function deriveIntentWithFollowupContext(
|
|||
};
|
||||
}
|
||||
|
||||
if (
|
||||
samePeriodVatFollowup &&
|
||||
(detectedIntent.intent === "vat_payable_confirmed_as_of_date" ||
|
||||
detectedIntent.intent === "vat_payable_forecast" ||
|
||||
detectedIntent.intent === "unknown")
|
||||
) {
|
||||
return {
|
||||
intent: "vat_liability_confirmed_for_tax_period",
|
||||
confidence: "low",
|
||||
reasons: [...detectedIntent.reasons, "intent_adjusted_to_vat_same_period_followup"]
|
||||
};
|
||||
}
|
||||
|
||||
if (detectedIntent.intent === "unknown" && isVatFollowup) {
|
||||
const vatIntent: AddressIntent = hasVatTaxPaymentCue(normalizedMessage)
|
||||
? "vat_liability_confirmed_for_tax_period"
|
||||
|
|
@ -1471,6 +1514,19 @@ function deriveIntentWithFollowupContext(
|
|||
};
|
||||
}
|
||||
|
||||
if (
|
||||
!inventoryLineageActive &&
|
||||
hasAnyPartyAnchor &&
|
||||
!hasExplicitInventoryItemReference &&
|
||||
detectedIntent.intent === "inventory_purchase_documents_for_item"
|
||||
) {
|
||||
return {
|
||||
intent: hasPreviousContract && !hasPreviousCounterparty ? "list_documents_by_contract" : "list_documents_by_counterparty",
|
||||
confidence: "low",
|
||||
reasons: [...detectedIntent.reasons, "intent_adjusted_from_non_inventory_followup_context"]
|
||||
};
|
||||
}
|
||||
|
||||
const allowOpenItemsFollowupFallback = detectedIntent.intent === "unknown" && !isVatFollowup;
|
||||
if (
|
||||
allowOpenItemsFollowupFallback &&
|
||||
|
|
@ -1752,6 +1808,15 @@ export function runAddressDecomposeStage(
|
|||
warnings: [...new Set([...extractedFilters.warnings, ...followupMerged.reasons])],
|
||||
semantic_frame: extractedFilters.semantic_frame
|
||||
};
|
||||
if (
|
||||
(intent.intent === "list_open_contracts" || intent.intent === "open_contracts_confirmed_as_of_date") &&
|
||||
typeof filters.extracted_filters.as_of_date === "string" &&
|
||||
typeof filters.extracted_filters.period_to === "string" &&
|
||||
filters.extracted_filters.as_of_date === filters.extracted_filters.period_to &&
|
||||
!filters.warnings.includes("as_of_date_derived_from_period_for_open_contracts")
|
||||
) {
|
||||
filters.warnings.push("as_of_date_derived_from_period_for_open_contracts");
|
||||
}
|
||||
const followupContextApplied =
|
||||
Boolean(effectiveFollowupContext) &&
|
||||
(mode.reasons.includes("address_mode_from_followup_context") ||
|
||||
|
|
@ -1763,6 +1828,7 @@ export function runAddressDecomposeStage(
|
|||
...shape.reasons,
|
||||
...intent.reasons,
|
||||
...followupMerged.reasons,
|
||||
...filters.warnings.filter((reason) => reason === "as_of_date_derived_from_period_for_open_contracts"),
|
||||
...(followupContextApplied ? ["address_followup_context_applied"] : [])
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -178,16 +178,25 @@ function shouldPreferRawFollowupMessage(
|
|||
const previousIntent = toNonEmptyString(followupContext?.previous_intent);
|
||||
const rootIntent = toNonEmptyString(followupContext?.root_intent);
|
||||
const previousAnchorType = toNonEmptyString(followupContext?.previous_anchor_type);
|
||||
const hasReferentialDocumentExclusionFollowupCue = /(?:\u043a\u0440\u043e\u043c\u0435|\u043f\u043e\u043c\u0438\u043c\u043e)\s+(?:\u044d\u0442\u043e\u0433\u043e|\u044d\u0442\u043e\u0439|\u044d\u0442\u043e\u0442|\u044d\u0442\u0443|\u044d\u0442\u0438\u0445)(?:\s+(?:\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430|\u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0430|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430))?/iu.test(
|
||||
rawMessage
|
||||
);
|
||||
const hasInventoryItemCarryover =
|
||||
previousAnchorType === "item" && isInventorySelectedObjectOrRootIntent(previousIntent);
|
||||
const hasInventoryFrameCarryover =
|
||||
isInventorySelectedObjectOrRootIntent(previousIntent) ||
|
||||
isInventorySelectedObjectOrRootIntent(rootIntent);
|
||||
const hasDocumentCarryover =
|
||||
previousIntent === "list_documents_by_counterparty" || previousIntent === "list_documents_by_contract";
|
||||
|
||||
if (mode === "unsupported" && intent === "unknown") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hasDocumentCarryover && hasReferentialDocumentExclusionFollowupCue) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hasSameDateFollowupSignal(rawMessage) && hasExplicitCurrentDateSignal(canonicalMessage)) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import {
|
|||
mergeKnownOrganizations as mergeKnownOrganizationsFromMatcher,
|
||||
normalizeOrganizationScopeValue as normalizeOrganizationScopeValueFromMatcher
|
||||
} from "./assistantOrganizationMatcher";
|
||||
import { normalizeRussianComparableText } from "./addressTextRepair";
|
||||
|
||||
export interface AssistantContinuitySnapshotInput {
|
||||
sessionItems?: unknown[];
|
||||
|
|
@ -174,6 +175,12 @@ function readAssistantMcpDiscoveryBridge(
|
|||
return toRecordObject(readAssistantMcpDiscoveryEntry(debug)?.bridge);
|
||||
}
|
||||
|
||||
function readAssistantMcpDiscoveryLoopState(
|
||||
debug: Record<string, unknown> | null
|
||||
): Record<string, unknown> | null {
|
||||
return toRecordObject(readAssistantMcpDiscoveryBridge(debug)?.loop_state);
|
||||
}
|
||||
|
||||
function readAssistantMcpDiscoveryDerivedMetadataSurface(
|
||||
debug: Record<string, unknown> | null
|
||||
): Record<string, unknown> | null {
|
||||
|
|
@ -260,7 +267,88 @@ export function readAssistantMcpDiscoveryRankingNeed(
|
|||
debug: Record<string, unknown> | null,
|
||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||
): string | null {
|
||||
return toNonEmptyString(readAssistantMcpDiscoveryDataNeedGraph(debug)?.ranking_need);
|
||||
return (
|
||||
toNonEmptyString(readAssistantMcpDiscoveryLoopState(debug)?.ranking_need) ??
|
||||
toNonEmptyString(readAssistantMcpDiscoveryDataNeedGraph(debug)?.ranking_need)
|
||||
);
|
||||
}
|
||||
|
||||
export function readAssistantMcpDiscoveryLoopStatus(
|
||||
debug: Record<string, unknown> | null,
|
||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||
): string | null {
|
||||
return toNonEmptyString(readAssistantMcpDiscoveryLoopState(debug)?.loop_status);
|
||||
}
|
||||
|
||||
export function readAssistantMcpDiscoveryLoopSelectedChainId(
|
||||
debug: Record<string, unknown> | null,
|
||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||
): string | null {
|
||||
return toNonEmptyString(readAssistantMcpDiscoveryLoopState(debug)?.selected_chain_id);
|
||||
}
|
||||
|
||||
export function readAssistantMcpDiscoveryLoopPendingAxes(
|
||||
debug: Record<string, unknown> | null,
|
||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||
): string[] {
|
||||
const values = readAssistantMcpDiscoveryLoopState(debug)?.pending_axes;
|
||||
if (!Array.isArray(values)) {
|
||||
return [];
|
||||
}
|
||||
return values.map((item) => toNonEmptyString(item)).filter((item): item is string => Boolean(item));
|
||||
}
|
||||
|
||||
export function readAssistantMcpDiscoveryLoopProvidedAxes(
|
||||
debug: Record<string, unknown> | null,
|
||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||
): string[] {
|
||||
const values = readAssistantMcpDiscoveryLoopState(debug)?.provided_axes;
|
||||
if (!Array.isArray(values)) {
|
||||
return [];
|
||||
}
|
||||
return values.map((item) => toNonEmptyString(item)).filter((item): item is string => Boolean(item));
|
||||
}
|
||||
|
||||
export function readAssistantMcpDiscoveryLoopAskedDomainFamily(
|
||||
debug: Record<string, unknown> | null,
|
||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||
): string | null {
|
||||
return toNonEmptyString(readAssistantMcpDiscoveryLoopState(debug)?.asked_domain_family);
|
||||
}
|
||||
|
||||
export function readAssistantMcpDiscoveryLoopAskedActionFamily(
|
||||
debug: Record<string, unknown> | null,
|
||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||
): string | null {
|
||||
return toNonEmptyString(readAssistantMcpDiscoveryLoopState(debug)?.asked_action_family);
|
||||
}
|
||||
|
||||
export function readAssistantMcpDiscoveryLoopUnsupportedFamily(
|
||||
debug: Record<string, unknown> | null,
|
||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||
): string | null {
|
||||
return toNonEmptyString(readAssistantMcpDiscoveryLoopState(debug)?.unsupported_but_understood_family);
|
||||
}
|
||||
|
||||
export function readAssistantMcpDiscoveryLoopMetadataScopeHint(
|
||||
debug: Record<string, unknown> | null,
|
||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||
): string | null {
|
||||
return (
|
||||
toNonEmptyString(readAssistantMcpDiscoveryLoopState(debug)?.metadata_scope_hint) ??
|
||||
toNonEmptyString(readAssistantMcpDiscoveryTurnMeaning(debug)?.metadata_scope_hint) ??
|
||||
toNonEmptyString(readAssistantMcpDiscoveryDataNeedGraph(debug)?.metadata_scope_hint)
|
||||
);
|
||||
}
|
||||
|
||||
export function readAssistantMcpDiscoveryLoopSubjectResolutionOptional(
|
||||
debug: Record<string, unknown> | null
|
||||
): boolean {
|
||||
return (
|
||||
readAssistantMcpDiscoveryLoopState(debug)?.subject_resolution_optional === true ||
|
||||
readAssistantMcpDiscoveryTurnMeaning(debug)?.subject_resolution_optional === true ||
|
||||
readAssistantMcpDiscoveryDataNeedGraph(debug)?.subject_resolution_optional === true
|
||||
);
|
||||
}
|
||||
|
||||
export function readAssistantMcpDiscoveryMetadataRouteFamily(
|
||||
|
|
@ -480,25 +568,102 @@ export function readAddressDebugItem(
|
|||
);
|
||||
}
|
||||
|
||||
function isReferentialCounterpartyPlaceholder(
|
||||
value: string | null
|
||||
): boolean {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
return new Set([
|
||||
"он",
|
||||
"она",
|
||||
"оно",
|
||||
"они",
|
||||
"ему",
|
||||
"ней",
|
||||
"нему",
|
||||
"ним",
|
||||
"ними",
|
||||
"его",
|
||||
"ее",
|
||||
"их",
|
||||
"этому",
|
||||
"этой",
|
||||
"этом",
|
||||
"этим",
|
||||
"эта",
|
||||
"этот",
|
||||
"эти"
|
||||
]).has(normalizeRussianComparableText(value));
|
||||
}
|
||||
|
||||
function normalizeCounterpartyCandidate(
|
||||
value: unknown,
|
||||
toNonEmptyString: (value: unknown) => string | null
|
||||
): string | null {
|
||||
const text = toNonEmptyString(value);
|
||||
if (!text || isReferentialCounterpartyPlaceholder(text)) {
|
||||
return null;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function sameCounterpartyCandidate(
|
||||
left: string | null,
|
||||
right: string | null
|
||||
): boolean {
|
||||
return Boolean(
|
||||
left &&
|
||||
right &&
|
||||
normalizeRussianComparableText(left) === normalizeRussianComparableText(right)
|
||||
);
|
||||
}
|
||||
|
||||
function readGroundedDiscoveryCounterparty(
|
||||
debug: Record<string, unknown> | null,
|
||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||
): string | null {
|
||||
const discoveryPilotScope = readAssistantMcpDiscoveryPilotScope(debug, toNonEmptyString);
|
||||
const suppressDiscoveryEntityCarryover =
|
||||
discoveryPilotScope === "metadata_inspection_v1" ||
|
||||
readAssistantMcpDiscoveryLoopSubjectResolutionOptional(debug);
|
||||
if (suppressDiscoveryEntityCarryover) {
|
||||
return null;
|
||||
}
|
||||
const discoveryEntities = collectAssistantMcpDiscoveryEntityCandidates(debug, toNonEmptyString);
|
||||
for (const entity of discoveryEntities) {
|
||||
const normalized = normalizeCounterpartyCandidate(entity, toNonEmptyString);
|
||||
if (normalized) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
return normalizeCounterpartyCandidate(
|
||||
readAssistantMcpDiscoveryLoopMetadataScopeHint(debug, toNonEmptyString),
|
||||
toNonEmptyString
|
||||
);
|
||||
}
|
||||
|
||||
export function readAddressDebugCounterparty(
|
||||
debug: Record<string, unknown> | null,
|
||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||
): string | null {
|
||||
const extractedFilters = readAddressDebugFilters(debug);
|
||||
if (toNonEmptyString(extractedFilters?.counterparty)) {
|
||||
return toNonEmptyString(extractedFilters?.counterparty);
|
||||
}
|
||||
if (String(debug?.anchor_type ?? "") === "counterparty") {
|
||||
return toNonEmptyString(debug?.anchor_value_resolved) ?? toNonEmptyString(debug?.anchor_value_raw);
|
||||
}
|
||||
const discoveryEntities = collectAssistantMcpDiscoveryEntityCandidates(debug, toNonEmptyString);
|
||||
for (const entity of discoveryEntities) {
|
||||
const text = toNonEmptyString(entity);
|
||||
if (text) {
|
||||
return text;
|
||||
const extractedCounterparty = normalizeCounterpartyCandidate(extractedFilters?.counterparty, toNonEmptyString);
|
||||
const anchorCounterparty =
|
||||
String(debug?.anchor_type ?? "") === "counterparty"
|
||||
? normalizeCounterpartyCandidate(
|
||||
toNonEmptyString(debug?.anchor_value_resolved) ?? toNonEmptyString(debug?.anchor_value_raw),
|
||||
toNonEmptyString
|
||||
)
|
||||
: null;
|
||||
const groundedDiscoveryCounterparty = readGroundedDiscoveryCounterparty(debug, toNonEmptyString);
|
||||
if (hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString) && groundedDiscoveryCounterparty) {
|
||||
if (!extractedCounterparty || !sameCounterpartyCandidate(extractedCounterparty, groundedDiscoveryCounterparty)) {
|
||||
return groundedDiscoveryCounterparty;
|
||||
}
|
||||
return extractedCounterparty;
|
||||
}
|
||||
return null;
|
||||
return extractedCounterparty ?? anchorCounterparty ?? groundedDiscoveryCounterparty;
|
||||
}
|
||||
|
||||
export function readAddressDebugIntent(
|
||||
|
|
@ -570,9 +735,21 @@ export function resolveAddressDebugAnchorContext(
|
|||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||
): AssistantAddressDebugAnchorContext {
|
||||
const explicitAnchorType = toNonEmptyString(debug?.anchor_type);
|
||||
const explicitAnchorValue =
|
||||
const explicitAnchorValueRaw =
|
||||
toNonEmptyString(debug?.anchor_value_resolved) ?? toNonEmptyString(debug?.anchor_value_raw);
|
||||
if (explicitAnchorType || explicitAnchorValue) {
|
||||
const explicitAnchorValue =
|
||||
explicitAnchorType === "counterparty"
|
||||
? normalizeCounterpartyCandidate(explicitAnchorValueRaw, toNonEmptyString)
|
||||
: explicitAnchorValueRaw;
|
||||
const groundedDiscoveryCounterparty = readGroundedDiscoveryCounterparty(debug, toNonEmptyString);
|
||||
const shouldPreferDiscoveryCounterparty =
|
||||
explicitAnchorType === "counterparty" &&
|
||||
Boolean(
|
||||
groundedDiscoveryCounterparty &&
|
||||
hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString) &&
|
||||
(!explicitAnchorValue || !sameCounterpartyCandidate(explicitAnchorValue, groundedDiscoveryCounterparty))
|
||||
);
|
||||
if ((explicitAnchorType || explicitAnchorValue) && !shouldPreferDiscoveryCounterparty) {
|
||||
return {
|
||||
anchorType: explicitAnchorType,
|
||||
anchorValue: explicitAnchorValue
|
||||
|
|
@ -587,8 +764,15 @@ export function resolveAddressDebugAnchorContext(
|
|||
anchorValue: item
|
||||
};
|
||||
}
|
||||
const counterparty = toNonEmptyString(extractedFilters?.counterparty);
|
||||
if (counterparty) {
|
||||
const counterparty = normalizeCounterpartyCandidate(extractedFilters?.counterparty, toNonEmptyString);
|
||||
if (
|
||||
counterparty &&
|
||||
!(
|
||||
groundedDiscoveryCounterparty &&
|
||||
hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString) &&
|
||||
!sameCounterpartyCandidate(counterparty, groundedDiscoveryCounterparty)
|
||||
)
|
||||
) {
|
||||
return {
|
||||
anchorType: "counterparty",
|
||||
anchorValue: counterparty
|
||||
|
|
@ -667,7 +851,10 @@ export function resolveAddressDebugCarryoverFilters(
|
|||
Boolean(discoveryDateScope.asOfDate || discoveryDateScope.periodFrom || discoveryDateScope.periodTo);
|
||||
const counterparty = readAddressDebugCounterparty(debug, toNonEmptyString);
|
||||
const organization = readAddressDebugOrganization(debug, toNonEmptyString);
|
||||
if (counterparty && !toNonEmptyString(nextFilters.counterparty)) {
|
||||
const preferGroundedDiscoveryCounterparty =
|
||||
hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString) && Boolean(counterparty);
|
||||
const existingCounterparty = normalizeCounterpartyCandidate(nextFilters.counterparty, toNonEmptyString);
|
||||
if (counterparty && (preferGroundedDiscoveryCounterparty || !existingCounterparty)) {
|
||||
nextFilters.counterparty = counterparty;
|
||||
}
|
||||
if (organization && !toNonEmptyString(nextFilters.organization)) {
|
||||
|
|
|
|||
|
|
@ -98,7 +98,13 @@ const PRIMITIVE_CONTRACTS: AssistantMcpCatalogPrimitiveContract[] = [
|
|||
supported_fact_families: ["value_flow", "movement_evidence"],
|
||||
supported_action_families: ["turnover", "payout", "net_value_flow", "list_movements"],
|
||||
planning_tags: ["movement", "comparison", "ranking", "aggregation", "monthly_aggregation"],
|
||||
required_axes_any_of: [["period", "account"], ["period", "counterparty"], ["period", "organization"]],
|
||||
required_axes_any_of: [
|
||||
["period", "account"],
|
||||
["period", "counterparty"],
|
||||
["period", "organization"],
|
||||
["all_time_scope", "counterparty"],
|
||||
["all_time_scope", "organization"]
|
||||
],
|
||||
optional_axes: ["contract", "document", "amount", "item", "warehouse"],
|
||||
output_fact_kinds: ["movement_rows", "turnover", "balance_delta"],
|
||||
evidence_floor: "rows_matched",
|
||||
|
|
@ -112,7 +118,14 @@ const PRIMITIVE_CONTRACTS: AssistantMcpCatalogPrimitiveContract[] = [
|
|||
supported_fact_families: ["document_evidence", "activity_lifecycle"],
|
||||
supported_action_families: ["list_documents", "activity_duration"],
|
||||
planning_tags: ["document"],
|
||||
required_axes_any_of: [["document"], ["counterparty"], ["contract"], ["period", "organization"]],
|
||||
required_axes_any_of: [
|
||||
["document"],
|
||||
["counterparty"],
|
||||
["contract"],
|
||||
["period", "organization"],
|
||||
["all_time_scope", "counterparty"],
|
||||
["all_time_scope", "organization"]
|
||||
],
|
||||
optional_axes: ["account", "amount", "item", "warehouse"],
|
||||
output_fact_kinds: ["document_rows", "document_dates", "document_amounts"],
|
||||
evidence_floor: "rows_matched",
|
||||
|
|
@ -126,7 +139,13 @@ const PRIMITIVE_CONTRACTS: AssistantMcpCatalogPrimitiveContract[] = [
|
|||
supported_fact_families: ["value_flow"],
|
||||
supported_action_families: ["turnover", "payout", "net_value_flow"],
|
||||
planning_tags: ["aggregation", "ranking", "monthly_aggregation"],
|
||||
required_axes_any_of: [["aggregate_axis", "period"], ["aggregate_axis", "counterparty"], ["aggregate_axis", "account"]],
|
||||
required_axes_any_of: [
|
||||
["aggregate_axis", "period"],
|
||||
["aggregate_axis", "counterparty"],
|
||||
["aggregate_axis", "account"],
|
||||
["aggregate_axis", "counterparty", "all_time_scope"],
|
||||
["aggregate_axis", "organization", "all_time_scope"]
|
||||
],
|
||||
optional_axes: ["organization", "contract", "document", "amount"],
|
||||
output_fact_kinds: ["aggregate_totals", "ranked_axis_values"],
|
||||
evidence_floor: "rows_matched",
|
||||
|
|
|
|||
|
|
@ -72,10 +72,25 @@ function isInternalMechanicsLine(value: string): boolean {
|
|||
text.includes("pilot_") ||
|
||||
text.includes("runtime_") ||
|
||||
text.includes("planner_") ||
|
||||
text.includes("catalog_")
|
||||
text.includes("catalog_") ||
|
||||
text.includes("needs more scope before execution") ||
|
||||
text.includes("mcp_execution_performed")
|
||||
);
|
||||
}
|
||||
|
||||
function userFacingUnknowns(values: string[]): string[] {
|
||||
return uniqueStrings(values).filter((value) => !isInternalMechanicsLine(value));
|
||||
}
|
||||
|
||||
function rankedValueFlowUnknownLines(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] {
|
||||
if (!pilot.derived_ranked_value_flow) {
|
||||
return userFacingUnknowns(pilot.evidence.unknown_facts);
|
||||
}
|
||||
const ranking = pilot.derived_ranked_value_flow;
|
||||
const period = ranking.period_scope ? `периода ${ranking.period_scope}` : "проверенного окна";
|
||||
return [`Полный рейтинг контрагентов вне ${period} этим поиском не подтвержден.`];
|
||||
}
|
||||
|
||||
function userFacingLimitations(values: string[]): string[] {
|
||||
return uniqueStrings(values).filter((value) => !isInternalMechanicsLine(value));
|
||||
}
|
||||
|
|
@ -194,11 +209,23 @@ function explicitOrganizationScope(pilot: AssistantMcpDiscoveryPilotExecutionCon
|
|||
return normalized.length > 0 ? normalized : null;
|
||||
}
|
||||
|
||||
function hasAllTimeScope(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||||
return (
|
||||
dryRunHasAxis(pilot, "all_time_scope") ||
|
||||
pilot.reason_codes.includes("mcp_discovery_all_time_scope_signal_detected") ||
|
||||
pilot.dry_run.reason_codes.includes("mcp_discovery_all_time_scope_signal_detected")
|
||||
);
|
||||
}
|
||||
|
||||
function documentOrMovementScopeRu(pilot: AssistantMcpDiscoveryPilotExecutionContract): string {
|
||||
const entity = firstEntityCandidate(pilot);
|
||||
const period = explicitDateScope(pilot);
|
||||
const entityPart = entity ? ` по контрагенту ${entity}` : "";
|
||||
const periodPart = period ? ` за ${period}` : " в проверенном окне";
|
||||
const periodPart = period
|
||||
? ` за ${period}`
|
||||
: hasAllTimeScope(pilot)
|
||||
? " за все доступное время"
|
||||
: " в проверенном окне";
|
||||
return `${entityPart}${periodPart}`;
|
||||
}
|
||||
|
||||
|
|
@ -267,15 +294,29 @@ function dryRunMissingAxis(pilot: AssistantMcpDiscoveryPilotExecutionContract, a
|
|||
);
|
||||
}
|
||||
|
||||
function queryPlanClarificationGaps(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] {
|
||||
const values = pilot.evidence.query_plan.clarification_gaps;
|
||||
return Array.isArray(values) ? uniqueStrings(values) : [];
|
||||
}
|
||||
|
||||
function clarificationGapMissing(pilot: AssistantMcpDiscoveryPilotExecutionContract, axis: string): boolean {
|
||||
const gaps = queryPlanClarificationGaps(pilot);
|
||||
if (gaps.length > 0) {
|
||||
return gaps.includes(axis);
|
||||
}
|
||||
return dryRunMissingAxis(pilot, axis);
|
||||
}
|
||||
|
||||
function clarificationNeedRu(
|
||||
pilot: AssistantMcpDiscoveryPilotExecutionContract
|
||||
): { subject: string; verb: string } {
|
||||
const needsPeriod = clarificationGapMissing(pilot, "period");
|
||||
const organizationScopedOpenTotal =
|
||||
pilot.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
|
||||
pilot.dry_run.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
|
||||
pilot.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph") ||
|
||||
pilot.dry_run.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph");
|
||||
if (organizationScopedOpenTotal) {
|
||||
if (organizationScopedOpenTotal && !needsPeriod) {
|
||||
return {
|
||||
subject: "\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044e",
|
||||
verb: "\u043d\u0443\u0436\u043d\u043e"
|
||||
|
|
@ -283,8 +324,7 @@ function clarificationNeedRu(
|
|||
}
|
||||
const hasCounterparty = dryRunHasAxis(pilot, "counterparty");
|
||||
const hasAccount = dryRunHasAxis(pilot, "account");
|
||||
const needsPeriod = dryRunMissingAxis(pilot, "period");
|
||||
const needsOrganization = !hasCounterparty && !hasAccount && dryRunMissingAxis(pilot, "organization");
|
||||
const needsOrganization = !hasCounterparty && !hasAccount && clarificationGapMissing(pilot, "organization");
|
||||
if (needsPeriod && needsOrganization) {
|
||||
return { subject: "проверяемый период и организацию", verb: "нужно" };
|
||||
}
|
||||
|
|
@ -306,8 +346,8 @@ function clarificationNextStepLine(
|
|||
pilot.dry_run.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
|
||||
pilot.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph") ||
|
||||
pilot.dry_run.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph");
|
||||
const needsPeriod = dryRunMissingAxis(pilot, "period");
|
||||
const needsOrganization = dryRunMissingAxis(pilot, "organization");
|
||||
const needsPeriod = clarificationGapMissing(pilot, "period");
|
||||
const needsOrganization = clarificationGapMissing(pilot, "organization");
|
||||
const scopeSuffix = laneScopeSuffix(pilot);
|
||||
if (organizationScopedOpenTotal && !needsPeriod) {
|
||||
return `Уточните организацию, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
|
||||
|
|
@ -825,7 +865,9 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
|
|||
if (monthlyConfirmedLines.length > 0) {
|
||||
pushReason(reasonCodes, "answer_contains_monthly_breakdown");
|
||||
}
|
||||
const confirmedLines = derivedValueLine
|
||||
const confirmedLines = pilot.derived_ranked_value_flow && derivedValueLine
|
||||
? [derivedValueLine]
|
||||
: derivedValueLine
|
||||
? [...pilot.evidence.confirmed_facts, derivedValueLine, ...monthlyConfirmedLines]
|
||||
: derivedEntityResolutionLine
|
||||
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
|
||||
|
|
@ -840,7 +882,7 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
|
|||
headline: headlineFor(mode, pilot),
|
||||
confirmed_lines: uniqueStrings(confirmedLines),
|
||||
inference_lines: uniqueStrings(inferenceLines),
|
||||
unknown_lines: uniqueStrings(pilot.evidence.unknown_facts),
|
||||
unknown_lines: rankedValueFlowUnknownLines(pilot),
|
||||
limitation_lines: userFacingLimitations([...pilot.query_limitations, ...pilot.evidence.query_limitations]),
|
||||
next_step_line: nextStepFor(mode, pilot),
|
||||
internal_mechanics_allowed: false,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ export interface AssistantMcpDiscoveryDataNeedGraphContract {
|
|||
schema_version: typeof ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION;
|
||||
policy_owner: "assistantMcpDiscoveryDataNeedGraph";
|
||||
subject_candidates: string[];
|
||||
metadata_scope_hint?: string | null;
|
||||
subject_resolution_optional?: boolean;
|
||||
business_fact_family: string | null;
|
||||
action_family: string | null;
|
||||
aggregation_need: string | null;
|
||||
|
|
@ -153,12 +155,12 @@ function hasOpenScopeOneSidedValueTotalHint(rawUtterance: string, action: string
|
|||
return false;
|
||||
}
|
||||
if (action === "turnover") {
|
||||
return /(?:\bсколько\s+(?:мы\s+)?(?:получили|получено|входящих(?:\s+денег)?|поступлений|денег\s+пришло)\b|(?:сумма|объем)\s+(?:входящих|поступлений)|поступлений\s+за\b)/iu.test(
|
||||
return /(?:\bсколько\s+(?:(?:вообще|всего|реально)\s+){0,2}(?:мы\s+)?(?:получили|получено|входящих(?:\s+денег)?(?:\s+было)?|поступлений|денег\s+пришло)\b|(?:сумма|объем)\s+(?:входящих|поступлений)|поступлений\s+за\b)/iu.test(
|
||||
rawUtterance
|
||||
);
|
||||
}
|
||||
if (action === "payout") {
|
||||
return /(?:\bсколько\s+(?:мы\s+)?(?:заплатили|выплатили|потратили|исходящих(?:\s+денег)?|платежей|списаний)\b|(?:сумма|объем)\s+(?:исходящих|платежей|списаний)|(?:платежей|списаний)\s+за\b)/iu.test(
|
||||
return /(?:\bсколько\s+(?:(?:вообще|всего|реально)\s+){0,2}(?:мы\s+)?(?:заплатили|выплатили|потратили|исходящих(?:\s+денег)?(?:\s+было)?|платежей(?:\s+было)?|списаний(?:\s+было)?)\b|(?:сумма|объем)\s+(?:исходящих|платежей|списаний)|(?:платежей|списаний)\s+за\b)/iu.test(
|
||||
rawUtterance
|
||||
);
|
||||
}
|
||||
|
|
@ -170,12 +172,12 @@ function hasOpenScopeOneSidedValueTotalHintUtf8Safe(rawUtterance: string, action
|
|||
return false;
|
||||
}
|
||||
if (action === "turnover") {
|
||||
return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:\u043c\u044b\s+)?(?:\u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438|\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043e|\u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445(?:\s+\u0434\u0435\u043d\u0435\u0433)?|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439|\u0434\u0435\u043d\u0435\u0433\s+\u043f\u0440\u0438\u0448\u043b\u043e)|(?:\u0441\u0443\u043c\u043c\u0430|\u043e\u0431\u044a\u0435\u043c)\s+(?:\u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439)|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439\s+\u0437\u0430)/u.test(
|
||||
return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:(?:\u0432\u043e\u043e\u0431\u0449\u0435|\u0432\u0441\u0435\u0433\u043e|\u0440\u0435\u0430\u043b\u044c\u043d\u043e)\s+){0,2}(?:\u043c\u044b\s+)?(?:\u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438|\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043e|\u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445(?:\s+\u0434\u0435\u043d\u0435\u0433)?(?:\s+\u0431\u044b\u043b\u043e)?|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439|\u0434\u0435\u043d\u0435\u0433\s+\u043f\u0440\u0438\u0448\u043b\u043e)|(?:\u0441\u0443\u043c\u043c\u0430|\u043e\u0431\u044a\u0435\u043c)\s+(?:\u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439)|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439\s+\u0437\u0430)/u.test(
|
||||
rawUtterance
|
||||
);
|
||||
}
|
||||
if (action === "payout") {
|
||||
return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:\u043c\u044b\s+)?(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438|\u0432\u044b\u043f\u043b\u0430\u0442\u0438\u043b\u0438|\u043f\u043e\u0442\u0440\u0430\u0442\u0438\u043b\u0438|\u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445(?:\s+\u0434\u0435\u043d\u0435\u0433)?|\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439)|(?:\u0441\u0443\u043c\u043c\u0430|\u043e\u0431\u044a\u0435\u043c)\s+(?:\u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445|\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439)|(?:\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439)\s+\u0437\u0430)/u.test(
|
||||
return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:(?:\u0432\u043e\u043e\u0431\u0449\u0435|\u0432\u0441\u0435\u0433\u043e|\u0440\u0435\u0430\u043b\u044c\u043d\u043e)\s+){0,2}(?:\u043c\u044b\s+)?(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438|\u0432\u044b\u043f\u043b\u0430\u0442\u0438\u043b\u0438|\u043f\u043e\u0442\u0440\u0430\u0442\u0438\u043b\u0438|\u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445(?:\s+\u0434\u0435\u043d\u0435\u0433)?(?:\s+\u0431\u044b\u043b\u043e)?|\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439(?:\s+\u0431\u044b\u043b\u043e)?|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439(?:\s+\u0431\u044b\u043b\u043e)?)|(?:\u0441\u0443\u043c\u043c\u0430|\u043e\u0431\u044a\u0435\u043c)\s+(?:\u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445|\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439)|(?:\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439)\s+\u0437\u0430)/u.test(
|
||||
rawUtterance
|
||||
);
|
||||
}
|
||||
|
|
@ -205,6 +207,16 @@ function allowsOpenScopeWithoutSubject(input: {
|
|||
);
|
||||
}
|
||||
|
||||
function allowsMetadataScopedOpenLaneWithoutSubject(input: {
|
||||
family: string | null;
|
||||
subjectResolutionOptional: boolean;
|
||||
}): boolean {
|
||||
return Boolean(
|
||||
input.subjectResolutionOptional &&
|
||||
(input.family === "movement_evidence" || input.family === "document_evidence")
|
||||
);
|
||||
}
|
||||
|
||||
function rankingNeedFromRawUtterance(value: string): string | null {
|
||||
const text = lower(value);
|
||||
if (!text) {
|
||||
|
|
@ -249,6 +261,7 @@ function decompositionCandidatesFor(input: {
|
|||
comparisonNeed: string | null;
|
||||
rankingNeed: string | null;
|
||||
openScopeWithoutSubject: boolean;
|
||||
metadataScopedOpenLaneWithoutSubject: boolean;
|
||||
}): string[] {
|
||||
const result: string[] = [];
|
||||
if (input.family === "schema_surface") {
|
||||
|
|
@ -295,13 +308,17 @@ function decompositionCandidatesFor(input: {
|
|||
return result;
|
||||
}
|
||||
if (input.family === "movement_evidence") {
|
||||
pushUnique(result, "resolve_entity_reference");
|
||||
if (!input.metadataScopedOpenLaneWithoutSubject) {
|
||||
pushUnique(result, "resolve_entity_reference");
|
||||
}
|
||||
pushUnique(result, "fetch_scoped_movements");
|
||||
pushUnique(result, "probe_coverage");
|
||||
return result;
|
||||
}
|
||||
if (input.family === "document_evidence") {
|
||||
pushUnique(result, "resolve_entity_reference");
|
||||
if (!input.metadataScopedOpenLaneWithoutSubject) {
|
||||
pushUnique(result, "resolve_entity_reference");
|
||||
}
|
||||
pushUnique(result, "fetch_scoped_documents");
|
||||
pushUnique(result, "probe_coverage");
|
||||
return result;
|
||||
|
|
@ -345,6 +362,8 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
|
|||
const seededRankingNeed = toNonEmptyString(turnMeaning?.seeded_ranking_need);
|
||||
const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope);
|
||||
const explicitOrganizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope);
|
||||
const metadataScopeHint = toNonEmptyString(turnMeaning?.metadata_scope_hint);
|
||||
const subjectResolutionOptional = turnMeaning?.subject_resolution_optional === true;
|
||||
const subjectCandidates = (turnMeaning?.explicit_entity_candidates ?? [])
|
||||
.map((item) => toNonEmptyString(item))
|
||||
.filter((item): item is string => Boolean(item));
|
||||
|
|
@ -369,6 +388,12 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
|
|||
rankingNeed,
|
||||
oneSidedOpenScopeTotalHint
|
||||
});
|
||||
const metadataScopedOpenLaneWithoutSubject =
|
||||
subjectCandidates.length === 0 &&
|
||||
allowsMetadataScopedOpenLaneWithoutSubject({
|
||||
family: businessFactFamily,
|
||||
subjectResolutionOptional
|
||||
});
|
||||
const clarificationGaps: string[] = [];
|
||||
if (unsupported === "metadata_lane_choice_clarification" || action === "resolve_next_lane") {
|
||||
pushUnique(clarificationGaps, "lane_family_choice");
|
||||
|
|
@ -380,7 +405,18 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
|
|||
!explicitOrganizationScope
|
||||
) {
|
||||
pushUnique(clarificationGaps, "organization");
|
||||
} else if (subjectCandidates.length === 0 && businessFactFamily !== "schema_surface" && !openScopeWithoutSubject) {
|
||||
} else if (
|
||||
subjectCandidates.length === 0 &&
|
||||
metadataScopedOpenLaneWithoutSubject &&
|
||||
!explicitOrganizationScope
|
||||
) {
|
||||
pushUnique(clarificationGaps, "organization");
|
||||
} else if (
|
||||
subjectCandidates.length === 0 &&
|
||||
businessFactFamily !== "schema_surface" &&
|
||||
!openScopeWithoutSubject &&
|
||||
!metadataScopedOpenLaneWithoutSubject
|
||||
) {
|
||||
pushUnique(clarificationGaps, "subject");
|
||||
}
|
||||
const timeScopeNeed = timeScopeNeedFor({
|
||||
|
|
@ -393,12 +429,13 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
|
|||
}
|
||||
const decompositionCandidates = decompositionCandidatesFor({
|
||||
family: businessFactFamily,
|
||||
action,
|
||||
aggregationNeed,
|
||||
comparisonNeed,
|
||||
rankingNeed,
|
||||
openScopeWithoutSubject
|
||||
});
|
||||
action,
|
||||
aggregationNeed,
|
||||
comparisonNeed,
|
||||
rankingNeed,
|
||||
openScopeWithoutSubject,
|
||||
metadataScopedOpenLaneWithoutSubject
|
||||
});
|
||||
const reasonCodes: string[] = [];
|
||||
pushReason(reasonCodes, "data_need_graph_built");
|
||||
if (businessFactFamily) {
|
||||
|
|
@ -418,6 +455,9 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
|
|||
if (openScopeWithoutSubject && !rankingNeed && !comparisonNeed) {
|
||||
pushReason(reasonCodes, "data_need_graph_open_scope_total_without_subject");
|
||||
}
|
||||
if (metadataScopedOpenLaneWithoutSubject) {
|
||||
pushReason(reasonCodes, "data_need_graph_metadata_scoped_open_lane_without_subject");
|
||||
}
|
||||
if (allTimeScopeHint) {
|
||||
pushReason(reasonCodes, "data_need_graph_all_time_scope_hint");
|
||||
}
|
||||
|
|
@ -432,6 +472,8 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
|
|||
schema_version: ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||
subject_candidates: subjectCandidates,
|
||||
metadata_scope_hint: metadataScopeHint,
|
||||
subject_resolution_optional: subjectResolutionOptional || undefined,
|
||||
business_fact_family: businessFactFamily,
|
||||
action_family: toNonEmptyString(turnMeaning?.asked_action_family),
|
||||
aggregation_need: aggregationNeed,
|
||||
|
|
|
|||
|
|
@ -127,6 +127,16 @@ function hasSubjectCandidates(graph: AssistantMcpDiscoveryDataNeedGraphContract
|
|||
return (graph?.subject_candidates.length ?? 0) > 0;
|
||||
}
|
||||
|
||||
function hasMetadataScopedOpenLane(
|
||||
graph: AssistantMcpDiscoveryDataNeedGraphContract | null | undefined,
|
||||
meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefined
|
||||
): boolean {
|
||||
return Boolean(
|
||||
graph?.subject_resolution_optional === true ||
|
||||
meaning?.subject_resolution_optional === true
|
||||
);
|
||||
}
|
||||
|
||||
function hasReasonCode(
|
||||
graph: AssistantMcpDiscoveryDataNeedGraphContract | null | undefined,
|
||||
reasonCode: string
|
||||
|
|
@ -150,6 +160,21 @@ function addScopeAxes(axes: string[], meaning: AssistantMcpDiscoveryTurnMeaningR
|
|||
}
|
||||
}
|
||||
|
||||
function addMetadataScopeAxis(axes: string[], meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefined): void {
|
||||
if (toNonEmptyString(meaning?.metadata_scope_hint)) {
|
||||
pushUnique(axes, "metadata_scope");
|
||||
}
|
||||
}
|
||||
|
||||
function addTimeScopeAxes(
|
||||
axes: string[],
|
||||
dataNeedGraph: AssistantMcpDiscoveryDataNeedGraphContract | null | undefined
|
||||
): void {
|
||||
if (dataNeedGraph?.time_scope_need === "all_time_scope") {
|
||||
pushUnique(axes, "all_time_scope");
|
||||
}
|
||||
}
|
||||
|
||||
function includesAny(text: string, tokens: string[]): boolean {
|
||||
return tokens.some((token) => text.includes(token));
|
||||
}
|
||||
|
|
@ -408,7 +433,7 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
|
|||
const graphAction = lower(dataNeedGraph?.action_family);
|
||||
const graphAggregation = lower(dataNeedGraph?.aggregation_need);
|
||||
const graphClarificationGaps = (dataNeedGraph?.clarification_gaps ?? []).map((item) => lower(item));
|
||||
const organizationScope = toNonEmptyString(meaning?.explicit_organization_scope);
|
||||
const metadataScopedOpenLane = hasMetadataScopedOpenLane(dataNeedGraph, meaning);
|
||||
const openScopeTotalWithoutSubject =
|
||||
graphFactFamily === "value_flow" &&
|
||||
!hasSubjectCandidates(dataNeedGraph) &&
|
||||
|
|
@ -417,6 +442,8 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
|
|||
const axes: string[] = [];
|
||||
const requestedAggregationAxis = aggregationAxis(meaning);
|
||||
addScopeAxes(axes, meaning);
|
||||
addMetadataScopeAxis(axes, meaning);
|
||||
addTimeScopeAxes(axes, dataNeedGraph);
|
||||
|
||||
if (graphClarificationGaps.includes("lane_family_choice")) {
|
||||
pushUnique(axes, "lane_family_choice");
|
||||
|
|
@ -644,6 +671,27 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
|
|||
}
|
||||
|
||||
if (graphFactFamily === "movement_evidence") {
|
||||
if (metadataScopedOpenLane) {
|
||||
pushUnique(axes, "organization");
|
||||
pushUnique(axes, "coverage_target");
|
||||
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
|
||||
dataNeedGraph,
|
||||
fallbackPrimitives: ["query_movements", "probe_coverage"],
|
||||
requiredAxes: axes,
|
||||
metadataSurface: input.metadataSurface,
|
||||
actionFamily: action
|
||||
});
|
||||
return {
|
||||
semanticDataNeed: "movement evidence",
|
||||
chainId: "movement_evidence",
|
||||
chainSummary:
|
||||
"Keep the metadata-scoped movement lane, ask only for the remaining business scope, then fetch scoped movement rows and probe coverage without pretending there is a grounded counterparty.",
|
||||
primitives: primitiveSelection.primitives,
|
||||
axes,
|
||||
reason: "planner_selected_metadata_scoped_movement_from_data_need_graph",
|
||||
extraReasons: primitiveSelection.reasonCodes
|
||||
};
|
||||
}
|
||||
pushUnique(axes, "coverage_target");
|
||||
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
|
||||
dataNeedGraph,
|
||||
|
|
@ -664,6 +712,27 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
|
|||
}
|
||||
|
||||
if (graphFactFamily === "document_evidence") {
|
||||
if (metadataScopedOpenLane) {
|
||||
pushUnique(axes, "organization");
|
||||
pushUnique(axes, "coverage_target");
|
||||
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
|
||||
dataNeedGraph,
|
||||
fallbackPrimitives: ["query_documents", "probe_coverage"],
|
||||
requiredAxes: axes,
|
||||
metadataSurface: input.metadataSurface,
|
||||
actionFamily: action
|
||||
});
|
||||
return {
|
||||
semanticDataNeed: "document evidence",
|
||||
chainId: "document_evidence",
|
||||
chainSummary:
|
||||
"Keep the metadata-scoped document lane, ask only for the remaining business scope, then fetch scoped document rows and probe coverage without pretending there is a grounded counterparty.",
|
||||
primitives: primitiveSelection.primitives,
|
||||
axes,
|
||||
reason: "planner_selected_metadata_scoped_document_from_data_need_graph",
|
||||
extraReasons: primitiveSelection.reasonCodes
|
||||
};
|
||||
}
|
||||
pushUnique(axes, "coverage_target");
|
||||
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
|
||||
dataNeedGraph,
|
||||
|
|
@ -852,6 +921,7 @@ export function planAssistantMcpDiscovery(
|
|||
turnMeaning: input.turnMeaning,
|
||||
proposedPrimitives: recipe.primitives,
|
||||
requiredAxes: recipe.axes,
|
||||
clarificationGaps: dataNeedGraph?.clarification_gaps ?? [],
|
||||
maxProbeCount: budgetOverride.maxProbeCount
|
||||
});
|
||||
const review = reviewAssistantMcpDiscoveryPlanAgainstCatalog(plan);
|
||||
|
|
|
|||
|
|
@ -27,8 +27,10 @@ export interface AssistantMcpDiscoveryTurnMeaningRef {
|
|||
seeded_ranking_need?: string | null;
|
||||
explicit_entity_candidates?: string[];
|
||||
metadata_ambiguity_entity_sets?: string[];
|
||||
metadata_scope_hint?: string | null;
|
||||
explicit_organization_scope?: string | null;
|
||||
explicit_date_scope?: string | null;
|
||||
subject_resolution_optional?: boolean | null;
|
||||
meaning_confidence?: number | null;
|
||||
unsupported_but_understood_family?: string | null;
|
||||
stale_replay_forbidden?: boolean | null;
|
||||
|
|
@ -48,6 +50,7 @@ export interface AssistantMcpDiscoveryPlanContract {
|
|||
allowed_primitives: AssistantMcpDiscoveryPrimitive[];
|
||||
rejected_primitives: string[];
|
||||
required_axes: string[];
|
||||
clarification_gaps: string[];
|
||||
execution_budget: AssistantMcpDiscoveryExecutionBudget;
|
||||
requires_evidence_gate: true;
|
||||
answer_may_use_raw_model_claims: false;
|
||||
|
|
@ -59,6 +62,7 @@ export interface BuildAssistantMcpDiscoveryPlanInput {
|
|||
turnMeaning?: AssistantMcpDiscoveryTurnMeaningRef | null;
|
||||
proposedPrimitives?: string[] | null;
|
||||
requiredAxes?: string[] | null;
|
||||
clarificationGaps?: string[] | null;
|
||||
maxProbeCount?: number | null;
|
||||
maxRowsPerProbe?: number | null;
|
||||
}
|
||||
|
|
@ -237,6 +241,7 @@ export function buildAssistantMcpDiscoveryPlan(
|
|||
const semanticDataNeed = toNonEmptyString(input.semanticDataNeed);
|
||||
const turnMeaning = normalizeTurnMeaning(input.turnMeaning);
|
||||
const requiredAxes = toStringList(input.requiredAxes);
|
||||
const clarificationGaps = toStringList(input.clarificationGaps);
|
||||
const proposed = toStringList(input.proposedPrimitives);
|
||||
const reasonCodes: string[] = [];
|
||||
const allowedPrimitives: AssistantMcpDiscoveryPrimitive[] = [];
|
||||
|
|
@ -297,6 +302,7 @@ export function buildAssistantMcpDiscoveryPlan(
|
|||
allowed_primitives: allowedPrimitives,
|
||||
rejected_primitives: rejectedPrimitives,
|
||||
required_axes: requiredAxes,
|
||||
clarification_gaps: clarificationGaps,
|
||||
execution_budget: {
|
||||
max_probe_count: clampInteger(input.maxProbeCount, DEFAULT_DISCOVERY_BUDGET.max_probe_count, 1, MAX_PROBE_COUNT),
|
||||
max_rows_per_probe: clampInteger(
|
||||
|
|
|
|||
|
|
@ -82,7 +82,9 @@ function hasInternalMechanics(value: string): boolean {
|
|||
text.includes("runtime_") ||
|
||||
text.includes("planner_") ||
|
||||
text.includes("catalog_") ||
|
||||
text.includes("select ")
|
||||
text.includes("select ") ||
|
||||
text.includes("needs more scope before execution") ||
|
||||
text.includes("mcp_execution_performed")
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -391,6 +391,34 @@ function hasMatchedFactualAddressContinuationTarget(
|
|||
return Boolean(detectedIntent && targetIntent && detectedIntent === targetIntent);
|
||||
}
|
||||
|
||||
function hasMatchedFactualSuggestedIntentPivotTarget(
|
||||
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||
): boolean {
|
||||
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
if (!hasEffectivelyFactualAddressReply(input)) {
|
||||
return false;
|
||||
}
|
||||
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||
const dialogContinuationContract =
|
||||
toRecordObject(input.addressRuntimeMeta?.dialogContinuationContract) ??
|
||||
toRecordObject(input.addressRuntimeMeta?.dialog_continuation_contract_v2);
|
||||
const targetIntent = toNonEmptyString(dialogContinuationContract?.target_intent);
|
||||
const decision = toNonEmptyString(dialogContinuationContract?.decision);
|
||||
const selectionMode = toNonEmptyString(dialogContinuationContract?.intent_selection_mode);
|
||||
const suggestedPivotSignal = dialogContinuationContract?.suggested_intent_pivot_signal === true;
|
||||
return Boolean(
|
||||
detectedIntent &&
|
||||
targetIntent &&
|
||||
detectedIntent === targetIntent &&
|
||||
(decision === "switch_to_suggested" ||
|
||||
selectionMode === "switch_to_suggested_intent" ||
|
||||
suggestedPivotSignal)
|
||||
);
|
||||
}
|
||||
|
||||
function hasFullConfirmedFactualAddressReply(
|
||||
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||
|
|
@ -433,6 +461,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
|||
const alignedFactualAddressReply = hasAlignedFactualAddressReply(input, entryPoint);
|
||||
const semanticConflictWithDiscoveryTurnMeaning = hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint);
|
||||
const matchedFactualAddressContinuationTarget = hasMatchedFactualAddressContinuationTarget(input, entryPoint);
|
||||
const matchedFactualSuggestedIntentPivotTarget = hasMatchedFactualSuggestedIntentPivotTarget(input, entryPoint);
|
||||
const fullConfirmedFactualAddressReply = hasFullConfirmedFactualAddressReply(input, entryPoint);
|
||||
const runtimeAdjustedExactReply = hasRuntimeAdjustedExactReply(input, entryPoint);
|
||||
|
||||
|
|
@ -460,6 +489,9 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
|||
if (matchedFactualAddressContinuationTarget) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_factual_address_continuation_target");
|
||||
}
|
||||
if (matchedFactualSuggestedIntentPivotTarget) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_factual_suggested_intent_pivot_target");
|
||||
}
|
||||
if (fullConfirmedFactualAddressReply) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply");
|
||||
}
|
||||
|
|
@ -493,6 +525,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
|||
(unsupportedBoundary || discoveryReadyChatCandidate || discoveryReadyDeepCandidate || discoveryReadyAddressCandidate) &&
|
||||
!alignedFactualAddressReply &&
|
||||
!matchedFactualAddressContinuationTarget &&
|
||||
!matchedFactualSuggestedIntentPivotTarget &&
|
||||
!fullConfirmedFactualAddressReply &&
|
||||
!runtimeAdjustedExactReply &&
|
||||
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
} from "./assistantMcpDiscoveryPilotExecutor";
|
||||
import {
|
||||
planAssistantMcpDiscovery,
|
||||
type AssistantMcpDiscoveryChainId,
|
||||
type AssistantMcpDiscoveryMetadataSurfaceRef,
|
||||
type AssistantMcpDiscoveryPlannerContract
|
||||
} from "./assistantMcpDiscoveryPlanner";
|
||||
|
|
@ -17,6 +18,8 @@ import type { AssistantMcpDiscoveryTurnMeaningRef } from "./assistantMcpDiscover
|
|||
|
||||
export const ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION =
|
||||
"assistant_mcp_discovery_runtime_bridge_v1" as const;
|
||||
export const ASSISTANT_MCP_DISCOVERY_LOOP_STATE_SCHEMA_VERSION =
|
||||
"assistant_mcp_discovery_loop_state_v1" as const;
|
||||
|
||||
export type AssistantMcpDiscoveryRuntimeBridgeStatus =
|
||||
| "answer_draft_ready"
|
||||
|
|
@ -24,6 +27,10 @@ export type AssistantMcpDiscoveryRuntimeBridgeStatus =
|
|||
| "needs_clarification"
|
||||
| "blocked"
|
||||
| "unsupported";
|
||||
export type AssistantMcpDiscoveryLoopStatus =
|
||||
| "awaiting_clarification"
|
||||
| "ready_for_next_hop"
|
||||
| "blocked";
|
||||
|
||||
export interface AssistantMcpDiscoveryRuntimeBridgeInput {
|
||||
semanticDataNeed?: string | null;
|
||||
|
|
@ -33,6 +40,25 @@ export interface AssistantMcpDiscoveryRuntimeBridgeInput {
|
|||
deps?: AssistantMcpDiscoveryPilotExecutorDeps;
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryLoopStateContract {
|
||||
schema_version: typeof ASSISTANT_MCP_DISCOVERY_LOOP_STATE_SCHEMA_VERSION;
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeBridge";
|
||||
loop_status: AssistantMcpDiscoveryLoopStatus;
|
||||
selected_chain_id: AssistantMcpDiscoveryChainId;
|
||||
pilot_scope: AssistantMcpDiscoveryPilotExecutionContract["pilot_scope"];
|
||||
asked_domain_family: string | null;
|
||||
asked_action_family: string | null;
|
||||
unsupported_but_understood_family: string | null;
|
||||
ranking_need: string | null;
|
||||
pending_axes: string[];
|
||||
provided_axes: string[];
|
||||
explicit_entity_candidates: string[];
|
||||
metadata_scope_hint: string | null;
|
||||
subject_resolution_optional: boolean;
|
||||
explicit_organization_scope: string | null;
|
||||
explicit_date_scope: string | null;
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryRuntimeBridgeContract {
|
||||
schema_version: typeof ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION;
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeBridge";
|
||||
|
|
@ -41,6 +67,7 @@ export interface AssistantMcpDiscoveryRuntimeBridgeContract {
|
|||
planner: AssistantMcpDiscoveryPlannerContract;
|
||||
pilot: AssistantMcpDiscoveryPilotExecutionContract;
|
||||
answer_draft: AssistantMcpDiscoveryAnswerDraftContract;
|
||||
loop_state: AssistantMcpDiscoveryLoopStateContract;
|
||||
user_facing_response_allowed: boolean;
|
||||
business_fact_answer_allowed: boolean;
|
||||
requires_user_clarification: boolean;
|
||||
|
|
@ -97,6 +124,80 @@ function businessFactAnswerAllowed(draft: AssistantMcpDiscoveryAnswerDraftContra
|
|||
return draft.answer_mode === "confirmed_with_bounded_inference" || draft.answer_mode === "bounded_inference_only";
|
||||
}
|
||||
|
||||
function loopStatusFor(
|
||||
bridgeStatus: AssistantMcpDiscoveryRuntimeBridgeStatus
|
||||
): AssistantMcpDiscoveryLoopStatus {
|
||||
if (bridgeStatus === "needs_clarification") {
|
||||
return "awaiting_clarification";
|
||||
}
|
||||
if (bridgeStatus === "blocked" || bridgeStatus === "unsupported") {
|
||||
return "blocked";
|
||||
}
|
||||
return "ready_for_next_hop";
|
||||
}
|
||||
|
||||
function flattenAxes(
|
||||
pilot: AssistantMcpDiscoveryPilotExecutionContract,
|
||||
source: "provided_axes" | "missing_axis_options"
|
||||
): string[] {
|
||||
const result: string[] = [];
|
||||
for (const step of pilot.dry_run.execution_steps) {
|
||||
if (source === "provided_axes") {
|
||||
for (const axis of step.provided_axes) {
|
||||
if (axis && !result.includes(axis)) {
|
||||
result.push(axis);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for (const option of step.missing_axis_options) {
|
||||
for (const axis of option) {
|
||||
if (axis && !result.includes(axis)) {
|
||||
result.push(axis);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function entityCandidatesFromPlanner(planner: AssistantMcpDiscoveryPlannerContract): string[] {
|
||||
const values = planner.discovery_plan.turn_meaning_ref?.explicit_entity_candidates ?? [];
|
||||
return uniqueStrings(values);
|
||||
}
|
||||
|
||||
function buildLoopState(
|
||||
planner: AssistantMcpDiscoveryPlannerContract,
|
||||
pilot: AssistantMcpDiscoveryPilotExecutionContract,
|
||||
bridgeStatus: AssistantMcpDiscoveryRuntimeBridgeStatus
|
||||
): AssistantMcpDiscoveryLoopStateContract {
|
||||
const plannerClarificationGaps = planner.discovery_plan.clarification_gaps ?? [];
|
||||
return {
|
||||
schema_version: ASSISTANT_MCP_DISCOVERY_LOOP_STATE_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
|
||||
loop_status: loopStatusFor(bridgeStatus),
|
||||
selected_chain_id: planner.selected_chain_id,
|
||||
pilot_scope: pilot.pilot_scope,
|
||||
asked_domain_family: planner.discovery_plan.turn_meaning_ref?.asked_domain_family ?? null,
|
||||
asked_action_family: planner.discovery_plan.turn_meaning_ref?.asked_action_family ?? null,
|
||||
unsupported_but_understood_family:
|
||||
planner.discovery_plan.turn_meaning_ref?.unsupported_but_understood_family ?? null,
|
||||
ranking_need: planner.data_need_graph?.ranking_need ?? planner.discovery_plan.turn_meaning_ref?.seeded_ranking_need ?? null,
|
||||
pending_axes: plannerClarificationGaps.length > 0 ? plannerClarificationGaps : flattenAxes(pilot, "missing_axis_options"),
|
||||
provided_axes: flattenAxes(pilot, "provided_axes"),
|
||||
explicit_entity_candidates: entityCandidatesFromPlanner(planner),
|
||||
metadata_scope_hint:
|
||||
planner.discovery_plan.turn_meaning_ref?.metadata_scope_hint ??
|
||||
planner.data_need_graph?.metadata_scope_hint ??
|
||||
null,
|
||||
subject_resolution_optional:
|
||||
planner.discovery_plan.turn_meaning_ref?.subject_resolution_optional === true ||
|
||||
planner.data_need_graph?.subject_resolution_optional === true,
|
||||
explicit_organization_scope: planner.discovery_plan.turn_meaning_ref?.explicit_organization_scope ?? null,
|
||||
explicit_date_scope: planner.discovery_plan.turn_meaning_ref?.explicit_date_scope ?? null
|
||||
};
|
||||
}
|
||||
|
||||
export async function runAssistantMcpDiscoveryRuntimeBridge(
|
||||
input: AssistantMcpDiscoveryRuntimeBridgeInput
|
||||
): Promise<AssistantMcpDiscoveryRuntimeBridgeContract> {
|
||||
|
|
@ -109,10 +210,12 @@ export async function runAssistantMcpDiscoveryRuntimeBridge(
|
|||
const pilot = await executeAssistantMcpDiscoveryPilot(planner, input.deps);
|
||||
const answerDraft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||
const bridgeStatus = bridgeStatusFor(pilot, answerDraft);
|
||||
const loopState = buildLoopState(planner, pilot, bridgeStatus);
|
||||
const reasonCodes = uniqueStrings([...planner.reason_codes, ...pilot.reason_codes, ...answerDraft.reason_codes]);
|
||||
|
||||
pushReason(reasonCodes, `runtime_bridge_status_${bridgeStatus}`);
|
||||
pushReason(reasonCodes, "runtime_bridge_not_wired_to_hot_assistant_answer");
|
||||
pushReason(reasonCodes, `runtime_bridge_loop_state_${loopState.loop_status}`);
|
||||
|
||||
return {
|
||||
schema_version: ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION,
|
||||
|
|
@ -122,6 +225,7 @@ export async function runAssistantMcpDiscoveryRuntimeBridge(
|
|||
planner,
|
||||
pilot,
|
||||
answer_draft: answerDraft,
|
||||
loop_state: loopState,
|
||||
user_facing_response_allowed: bridgeStatus !== "blocked",
|
||||
business_fact_answer_allowed: businessFactAnswerAllowed(answerDraft),
|
||||
requires_user_clarification: bridgeStatus === "needs_clarification",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -579,12 +579,35 @@ export function createAssistantRoutePolicy(deps) {
|
|||
!effectiveAddressFollowupSignal &&
|
||||
resolvedModeDetection.mode === "unsupported" &&
|
||||
resolvedIntentResolution.intent === "unknown");
|
||||
const groundedValueFlowFollowupContextDetected = Boolean(followupContext &&
|
||||
[
|
||||
"counterparty_value_flow_query_movements_v1",
|
||||
"counterparty_supplier_payout_query_movements_v1",
|
||||
"counterparty_bidirectional_value_flow_query_movements_v1"
|
||||
].includes(String(toNonEmptyString(followupContext?.previous_discovery_pilot_scope) ?? "")) &&
|
||||
!dangerOrCoercionSignal &&
|
||||
(toNonEmptyString(assistantTurnMeaning?.asked_domain_family) === "counterparty_value" ||
|
||||
[
|
||||
"turnover",
|
||||
"payout",
|
||||
"net_value_flow"
|
||||
].includes(String(toNonEmptyString(assistantTurnMeaning?.asked_action_family) ?? "")) ||
|
||||
/(?:нетто|сальдо|сколько\s+мы\s+(?:получили|заплатили)|incoming|outgoing)/iu.test(analyticsSample)));
|
||||
const baseToolGatePreservesAddressLane = Boolean(baseToolGate?.runAddressLane &&
|
||||
["address_intent_resolver_detected", "address_mode_classifier_detected", "address_signal_detected", "llm_canonical_data_signal_detected"].includes(String(baseToolGate?.reason ?? "")));
|
||||
[
|
||||
"address_intent_resolver_detected",
|
||||
"address_mode_classifier_detected",
|
||||
"address_signal_detected",
|
||||
"llm_canonical_data_signal_detected"
|
||||
].includes(String(baseToolGate?.reason ?? ""))) ||
|
||||
Boolean(baseToolGate?.runAddressLane &&
|
||||
String(baseToolGate?.reason ?? "") === "followup_context_detected" &&
|
||||
groundedValueFlowFollowupContextDetected);
|
||||
const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate &&
|
||||
deterministicNonDomainGuard &&
|
||||
(llmFirstUnsupportedCandidate || llmContractMode === null) &&
|
||||
!baseToolGatePreservesAddressLane &&
|
||||
!groundedValueFlowFollowupContextDetected &&
|
||||
!protectedInventoryShortFollowup &&
|
||||
!organizationClarificationContinuationDetected);
|
||||
const lastAddressAssistantDebug = sessionItems
|
||||
|
|
@ -749,9 +772,11 @@ export function createAssistantRoutePolicy(deps) {
|
|||
assistantTurnMeaning?.unsupported_but_understood_family &&
|
||||
assistantTurnMeaning?.stale_replay_forbidden === true &&
|
||||
!turnMeaningIntentCandidate &&
|
||||
!aggregateBusinessAnalyticsSignal &&
|
||||
!dataScopeMetaQuery &&
|
||||
!capabilityMetaQuery &&
|
||||
!dangerOrCoercionSignal &&
|
||||
!groundedValueFlowFollowupContextDetected &&
|
||||
!organizationClarificationContinuationDetected);
|
||||
if (unsupportedCurrentTurnMeaningBoundary) {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1888,7 +1888,7 @@ function textMojibakeScoreForAddress(value) {
|
|||
const source = String(value ?? "");
|
||||
const cyrillic = (source.match(/[А-Яа-яЁё]/g) ?? []).length;
|
||||
const latin = (source.match(/[A-Za-z]/g) ?? []).length;
|
||||
const hardMarkers = (source.match(/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ<EFBFBD>?’“”•–—™љ›њќћџ]/g) ?? []).length;
|
||||
const hardMarkers = (source.match(/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ\uFFFD?’“”•–—™љ›њќћџ]/g) ?? []).length;
|
||||
const pairMarkers = (source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length;
|
||||
const doubleEncodedMarkers = (source.match(/(?:Г[Ђ-џ]|В[Ђ-џ]|Ã.|Â.)/gu) ?? []).length;
|
||||
return cyrillic + latin - hardMarkers * 3 - pairMarkers * 2 - doubleEncodedMarkers * 2;
|
||||
|
|
@ -1898,7 +1898,7 @@ function looksLikeMojibakeForAddress(value) {
|
|||
if (!source.trim()) {
|
||||
return false;
|
||||
}
|
||||
if (/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ<EFBFBD>?’“”•–—™љ›њќћџ]/.test(source)) {
|
||||
if (/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ\uFFFD?’“”•–—™љ›њќћџ]/.test(source)) {
|
||||
return true;
|
||||
}
|
||||
if ((source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length >= 2) {
|
||||
|
|
@ -2129,7 +2129,7 @@ function normalizeCounterpartyForFollowupMatch(value) {
|
|||
return compactWhitespace(repairAddressMojibake(String(value ?? ""))
|
||||
.toLowerCase()
|
||||
.replace(/ё/g, "е")
|
||||
.replace(/[«»"'`“”„’<EFBFBD>?]/g, " ")
|
||||
.replace(/[«»"'`“”„’\uFFFD?]/g, " ")
|
||||
.replace(/[^a-zа-я0-9\s._-]+/giu, " "));
|
||||
}
|
||||
function normalizeCounterpartyTokenForFollowupMatch(value) {
|
||||
|
|
@ -2175,7 +2175,7 @@ function extractDisplayedAddressEntityCandidates(replyText, entityType = "unknow
|
|||
if (parts.length >= 2 && /^\d{4}-\d{2}-\d{2}/.test(parts[0] ?? "")) {
|
||||
counterpartyCandidate = parts[1] ?? counterpartyCandidate;
|
||||
}
|
||||
const cleanedCandidate = compactWhitespace(counterpartyCandidate.replace(/^["'«»“”„`’<EFBFBD>?]+|["'«»“”„`’<EFBFBD>?]+$/gu, ""));
|
||||
const cleanedCandidate = compactWhitespace(counterpartyCandidate.replace(/^["'«»“”„`’\uFFFD?]+|["'«»“”„`’\uFFFD?]+$/gu, ""));
|
||||
if (!cleanedCandidate || cleanedCandidate.length < 2) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -2439,11 +2439,19 @@ function findRecentInventoryRootFrame(items) {
|
|||
const ADDRESS_FOLLOWUP_OFFER_BY_INTENT = {
|
||||
list_documents_by_counterparty: ["bank_operations_by_counterparty", "list_contracts_by_counterparty"],
|
||||
bank_operations_by_counterparty: ["list_documents_by_counterparty", "list_contracts_by_counterparty"],
|
||||
list_contracts_by_counterparty: ["list_documents_by_contract", "bank_operations_by_contract"],
|
||||
list_documents_by_contract: ["bank_operations_by_contract"],
|
||||
bank_operations_by_contract: ["list_documents_by_contract"],
|
||||
open_items_by_counterparty_or_contract: ["list_documents_by_counterparty", "bank_operations_by_counterparty"]
|
||||
} as Record<string, string[]>;
|
||||
function resolveAddressFollowupSuggestedIntents(intent, anchorType) {
|
||||
if (intent === "list_contracts_by_counterparty") {
|
||||
if (anchorType === "contract") {
|
||||
return ["list_documents_by_contract", "bank_operations_by_contract"];
|
||||
}
|
||||
return ["list_documents_by_counterparty", "bank_operations_by_counterparty"];
|
||||
}
|
||||
return ADDRESS_FOLLOWUP_OFFER_BY_INTENT[intent] ?? null;
|
||||
}
|
||||
function buildAddressFollowupOffer(addressDebug) {
|
||||
if (!isAddressLaneDebugPayload(addressDebug)) {
|
||||
return null;
|
||||
|
|
@ -2452,11 +2460,11 @@ function buildAddressFollowupOffer(addressDebug) {
|
|||
if (!intent) {
|
||||
return null;
|
||||
}
|
||||
const suggestedIntents = ADDRESS_FOLLOWUP_OFFER_BY_INTENT[intent];
|
||||
const anchorContext = (0, assistantContinuityPolicy_1.resolveAddressDebugAnchorContext)(addressDebug, toNonEmptyString);
|
||||
const suggestedIntents = resolveAddressFollowupSuggestedIntents(intent, anchorContext.anchorType);
|
||||
if (!Array.isArray(suggestedIntents) || suggestedIntents.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const anchorContext = (0, assistantContinuityPolicy_1.resolveAddressDebugAnchorContext)(addressDebug, toNonEmptyString);
|
||||
return {
|
||||
enabled: true,
|
||||
source_intent: intent,
|
||||
|
|
@ -3216,6 +3224,11 @@ function hasSameDateAccountFollowupSignalForPredecompose(text) {
|
|||
/(?:^|\s)по\s+\d{2}(?:[.,]\d{1,2})?(?=$|[\s,.;:!?])/iu.test(source) ||
|
||||
/\b\d{2}(?:[.,]\d{1,2})\b/u.test(source));
|
||||
}
|
||||
function isCounterpartyDrilldownIntentForPredecompose(intent) {
|
||||
return intent === "list_documents_by_counterparty" ||
|
||||
intent === "bank_operations_by_counterparty" ||
|
||||
intent === "list_contracts_by_counterparty";
|
||||
}
|
||||
function hasPredecomposeDiagnosticUncertaintyLead(text) {
|
||||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
|
||||
if (!normalized) {
|
||||
|
|
@ -3434,8 +3447,16 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
|
|||
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" &&
|
||||
const sourceAnchorQuality = evaluateAddressAnchorQuality(repairedSourceMessage || userMessage);
|
||||
const candidateAccountInjectedIntoCounterpartyAnchor = isCounterpartyDrilldownIntentForPredecompose(sourceIntentResolution.intent) &&
|
||||
sourceIntentResolution.intent === candidateIntentResolution.intent &&
|
||||
sourceAnchorQuality.anchorType === "counterparty" &&
|
||||
sourceAnchorQuality.quality >= 2 &&
|
||||
!sourceHasExplicitAccountAnchor &&
|
||||
candidateInjectsAccountAnchor;
|
||||
if (((sourceIntentResolution.intent === "inventory_on_hand_as_of_date" &&
|
||||
candidateIntentResolution.intent === "inventory_on_hand_as_of_date") ||
|
||||
candidateAccountInjectedIntoCounterpartyAnchor) &&
|
||||
!sourceHasExplicitAccountAnchor &&
|
||||
candidateInjectsAccountAnchor) {
|
||||
return attachAddressPredecomposeContract({
|
||||
|
|
@ -3448,10 +3469,9 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
|
|||
reason: "normalized_fragment_rejected_anchor_injection",
|
||||
fallbackRuleHit: null,
|
||||
sanitizedUserMessage,
|
||||
semanticHints: candidateMeta?.semanticHints ?? null
|
||||
semanticHints: null
|
||||
}, userMessage);
|
||||
}
|
||||
const sourceAnchorQuality = evaluateAddressAnchorQuality(repairedSourceMessage || userMessage);
|
||||
const candidateAnchorQuality = evaluateAddressAnchorQuality(candidate);
|
||||
const sameIntentForAnchorSafety = sourceAnchorQuality.intent !== "unknown" && sourceAnchorQuality.intent === candidateAnchorQuality.intent;
|
||||
const sourceSelectedObjectItemAnchorValue = toNonEmptyString((0, addressFilterExtractor_1.extractSelectedObjectQuotedValue)(userMessage)) ??
|
||||
|
|
|
|||
|
|
@ -21,6 +21,15 @@ import {
|
|||
readAssistantMcpDiscoveryMetadataSelectedSurfaceObjects,
|
||||
readAssistantMcpDiscoveryMetadataRecommendedNextPrimitive,
|
||||
readAssistantMcpDiscoveryRankingNeed,
|
||||
readAssistantMcpDiscoveryLoopStatus,
|
||||
readAssistantMcpDiscoveryLoopSelectedChainId,
|
||||
readAssistantMcpDiscoveryLoopPendingAxes,
|
||||
readAssistantMcpDiscoveryLoopProvidedAxes,
|
||||
readAssistantMcpDiscoveryLoopAskedDomainFamily,
|
||||
readAssistantMcpDiscoveryLoopAskedActionFamily,
|
||||
readAssistantMcpDiscoveryLoopUnsupportedFamily,
|
||||
readAssistantMcpDiscoveryLoopMetadataScopeHint,
|
||||
readAssistantMcpDiscoveryLoopSubjectResolutionOptional,
|
||||
readAddressDebugTemporalScope,
|
||||
readAssistantMcpDiscoveryPilotScope,
|
||||
resolveOrganizationClarificationContinuation,
|
||||
|
|
@ -38,6 +47,99 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
return deps.compactWhitespace(deps.repairAddressMojibake(String(value ?? "")).toLowerCase()).replace(/ё/g, "е");
|
||||
}
|
||||
|
||||
function hasBankOperationsPivotCue(text) {
|
||||
const normalized = normalizeFollowupText(text);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return /(?:платеж|платёж|банк|банковск|операц|выписк|поступлен|списан)/iu.test(
|
||||
normalized
|
||||
);
|
||||
}
|
||||
|
||||
function hasContractsPivotCue(text) {
|
||||
const normalized = normalizeFollowupText(text);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return /(?:РґРѕРіРѕРІРѕСЂ)/iu.test(normalized);
|
||||
}
|
||||
|
||||
function hasDocumentsPivotCue(text) {
|
||||
const normalized = normalizeFollowupText(text);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return /(?:документ|счет|счёт|накладн|акт)/iu.test(normalized);
|
||||
}
|
||||
|
||||
function hasReadableBankOperationsPivotCue(text) {
|
||||
const normalized = normalizeFollowupText(text).replace(/ё/g, "е");
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return /(?:платеж|оплат|банк|банковск|операц|поступлен|списан|выписк|перевод|payment|bank|transaction)/iu.test(
|
||||
normalized
|
||||
);
|
||||
}
|
||||
|
||||
function hasReadableContractsPivotCue(text) {
|
||||
const normalized = normalizeFollowupText(text).replace(/ё/g, "е");
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return /(?:договор|контракт|соглашен|contract|agreement)/iu.test(normalized);
|
||||
}
|
||||
|
||||
function hasReadableDocumentsPivotCue(text) {
|
||||
const normalized = normalizeFollowupText(text).replace(/ё/g, "е");
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return /(?:документ|счет|счет-фактур|накладн|акт|реализац|document|invoice|receipt)/iu.test(normalized);
|
||||
}
|
||||
|
||||
function selectSuggestedIntentByPivotCue(suggestedIntents, userMessage, alternateMessage = null) {
|
||||
if (!Array.isArray(suggestedIntents) || suggestedIntents.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const samples = [userMessage, alternateMessage].filter((item) => deps.toNonEmptyString(item));
|
||||
if (samples.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
suggestedIntents.includes("bank_operations_by_counterparty") &&
|
||||
samples.some((sample) => hasBankOperationsPivotCue(sample) || hasReadableBankOperationsPivotCue(sample))
|
||||
) {
|
||||
return "bank_operations_by_counterparty";
|
||||
}
|
||||
if (
|
||||
suggestedIntents.includes("bank_operations_by_contract") &&
|
||||
samples.some((sample) => hasBankOperationsPivotCue(sample) || hasReadableBankOperationsPivotCue(sample))
|
||||
) {
|
||||
return "bank_operations_by_contract";
|
||||
}
|
||||
if (
|
||||
suggestedIntents.includes("list_contracts_by_counterparty") &&
|
||||
samples.some((sample) => hasContractsPivotCue(sample) || hasReadableContractsPivotCue(sample))
|
||||
) {
|
||||
return "list_contracts_by_counterparty";
|
||||
}
|
||||
if (
|
||||
suggestedIntents.includes("list_documents_by_counterparty") &&
|
||||
samples.some((sample) => hasDocumentsPivotCue(sample) || hasReadableDocumentsPivotCue(sample))
|
||||
) {
|
||||
return "list_documents_by_counterparty";
|
||||
}
|
||||
if (
|
||||
suggestedIntents.includes("list_documents_by_contract") &&
|
||||
samples.some((sample) => hasDocumentsPivotCue(sample) || hasReadableDocumentsPivotCue(sample))
|
||||
) {
|
||||
return "list_documents_by_contract";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function hasSelectedObjectInventoryScopeSignal(text) {
|
||||
const normalized = normalizeFollowupText(text);
|
||||
if (!normalized) {
|
||||
|
|
@ -469,6 +571,15 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
previousAddressDebug ??
|
||||
(hasOrganizationClarificationContinuation ? lastOrganizationClarificationDebug : null);
|
||||
const followupOffer = carryoverSourceDebug ? deps.buildAddressFollowupOffer(carryoverSourceDebug) : null;
|
||||
const suggestedIntentFromPivotCue = selectSuggestedIntentByPivotCue(
|
||||
Array.isArray(followupOffer?.suggested_intents) ? followupOffer.suggested_intents : [],
|
||||
userMessage,
|
||||
alternateMessage
|
||||
);
|
||||
const hasSuggestedIntentPivotSignal =
|
||||
Boolean(previousAddressDebug) &&
|
||||
Boolean(followupOffer?.enabled) &&
|
||||
Boolean(suggestedIntentFromPivotCue);
|
||||
const hasImplicitContinuationSignal =
|
||||
Boolean(previousAddressDebug) &&
|
||||
Boolean(followupOffer?.enabled) &&
|
||||
|
|
@ -579,6 +690,7 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
hasAlternateIndexReferenceSignal ||
|
||||
hasOrganizationClarificationContinuation ||
|
||||
hasImplicitContinuationSignal ||
|
||||
hasSuggestedIntentPivotSignal ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
hasInventoryRootTemporalFollowupPrimary ||
|
||||
|
|
@ -639,6 +751,7 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
!shortValueFlowRetargetPrimary &&
|
||||
!shortValueFlowRetargetAlternate &&
|
||||
!hasImplicitContinuationSignal &&
|
||||
!hasSuggestedIntentPivotSignal &&
|
||||
!hasOrganizationClarificationContinuation &&
|
||||
!hasIndexReferenceSignal
|
||||
) {
|
||||
|
|
@ -654,6 +767,7 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
!shortValueFlowRetargetPrimary &&
|
||||
!shortValueFlowRetargetAlternate &&
|
||||
!hasImplicitContinuationSignal &&
|
||||
!hasSuggestedIntentPivotSignal &&
|
||||
!hasOrganizationClarificationContinuation &&
|
||||
!hasIndexReferenceSignal
|
||||
) {
|
||||
|
|
@ -695,6 +809,37 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
carryoverSourceDebug,
|
||||
deps.toNonEmptyString
|
||||
);
|
||||
const sourceDiscoveryLoopStatus = readAssistantMcpDiscoveryLoopStatus(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopSelectedChainId = readAssistantMcpDiscoveryLoopSelectedChainId(
|
||||
carryoverSourceDebug,
|
||||
deps.toNonEmptyString
|
||||
);
|
||||
const sourceDiscoveryLoopPendingAxes = readAssistantMcpDiscoveryLoopPendingAxes(
|
||||
carryoverSourceDebug,
|
||||
deps.toNonEmptyString
|
||||
);
|
||||
const sourceDiscoveryLoopProvidedAxes = readAssistantMcpDiscoveryLoopProvidedAxes(
|
||||
carryoverSourceDebug,
|
||||
deps.toNonEmptyString
|
||||
);
|
||||
const sourceDiscoveryLoopAskedDomainFamily = readAssistantMcpDiscoveryLoopAskedDomainFamily(
|
||||
carryoverSourceDebug,
|
||||
deps.toNonEmptyString
|
||||
);
|
||||
const sourceDiscoveryLoopAskedActionFamily = readAssistantMcpDiscoveryLoopAskedActionFamily(
|
||||
carryoverSourceDebug,
|
||||
deps.toNonEmptyString
|
||||
);
|
||||
const sourceDiscoveryLoopUnsupportedFamily = readAssistantMcpDiscoveryLoopUnsupportedFamily(
|
||||
carryoverSourceDebug,
|
||||
deps.toNonEmptyString
|
||||
);
|
||||
const sourceDiscoveryLoopMetadataScopeHint = readAssistantMcpDiscoveryLoopMetadataScopeHint(
|
||||
carryoverSourceDebug,
|
||||
deps.toNonEmptyString
|
||||
);
|
||||
const sourceDiscoveryLoopSubjectResolutionOptional =
|
||||
readAssistantMcpDiscoveryLoopSubjectResolutionOptional(carryoverSourceDebug);
|
||||
const sourceDiscoveryRankingNeed = readAssistantMcpDiscoveryRankingNeed(
|
||||
carryoverSourceDebug,
|
||||
deps.toNonEmptyString
|
||||
|
|
@ -769,9 +914,9 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
if (debtRoleSwapIntent) {
|
||||
previousIntent = debtRoleSwapIntent;
|
||||
}
|
||||
if (hasImplicitContinuationSignal) {
|
||||
if (hasImplicitContinuationSignal || hasSuggestedIntentPivotSignal) {
|
||||
const suggestedIntent = Array.isArray(followupOffer?.suggested_intents)
|
||||
? deps.toNonEmptyString(followupOffer.suggested_intents[0])
|
||||
? suggestedIntentFromPivotCue ?? deps.toNonEmptyString(followupOffer.suggested_intents[0])
|
||||
: null;
|
||||
const keepPreviousIntent = shouldKeepPreviousIntentForShortCounterpartyRetargetV2(userMessage, sourceIntent);
|
||||
if (suggestedIntent && !keepPreviousIntent) {
|
||||
|
|
@ -801,6 +946,7 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
}
|
||||
hasPrimaryFollowupSignal =
|
||||
deps.hasAddressFollowupContextSignal(userMessage) ||
|
||||
hasSuggestedIntentPivotSignal ||
|
||||
Boolean(debtRoleSwapPrimary) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
|
|
@ -808,6 +954,7 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
hasInventoryRootTemporalFollowupPrimary;
|
||||
hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
||||
hasSuggestedIntentPivotSignal ||
|
||||
Boolean(debtRoleSwapAlternate) ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
|
|
@ -818,6 +965,7 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
hasPrimaryIndexReferenceSignal ||
|
||||
hasAlternateIndexReferenceSignal ||
|
||||
hasOrganizationClarificationContinuation ||
|
||||
hasSuggestedIntentPivotSignal ||
|
||||
hasImplicitContinuationSignal ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
|
|
@ -1042,6 +1190,18 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
previous_discovery_entity_resolution_status: sourceDiscoveryEntityResolutionStatus ?? undefined,
|
||||
previous_discovery_entity_candidates:
|
||||
sourceDiscoveryEntityCandidates.length > 0 ? sourceDiscoveryEntityCandidates : undefined,
|
||||
previous_discovery_loop_status: sourceDiscoveryLoopStatus ?? undefined,
|
||||
previous_discovery_loop_selected_chain_id: sourceDiscoveryLoopSelectedChainId ?? undefined,
|
||||
previous_discovery_loop_pending_axes:
|
||||
sourceDiscoveryLoopPendingAxes.length > 0 ? sourceDiscoveryLoopPendingAxes : undefined,
|
||||
previous_discovery_loop_provided_axes:
|
||||
sourceDiscoveryLoopProvidedAxes.length > 0 ? sourceDiscoveryLoopProvidedAxes : undefined,
|
||||
previous_discovery_loop_asked_domain_family: sourceDiscoveryLoopAskedDomainFamily ?? undefined,
|
||||
previous_discovery_loop_asked_action_family: sourceDiscoveryLoopAskedActionFamily ?? undefined,
|
||||
previous_discovery_loop_unsupported_family: sourceDiscoveryLoopUnsupportedFamily ?? undefined,
|
||||
previous_discovery_loop_metadata_scope_hint: sourceDiscoveryLoopMetadataScopeHint ?? undefined,
|
||||
previous_discovery_loop_subject_resolution_optional:
|
||||
sourceDiscoveryLoopSubjectResolutionOptional || undefined,
|
||||
previous_discovery_ranking_need: sourceDiscoveryRankingNeed ?? undefined,
|
||||
previous_discovery_entity_ambiguity_candidates:
|
||||
sourceDiscoveryEntityAmbiguityCandidates.length > 0
|
||||
|
|
@ -1072,7 +1232,8 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
previousAddressAnchor: previousAnchor,
|
||||
previousSourceIntent: sourceIntent,
|
||||
followupSelectionMode,
|
||||
hasImplicitContinuationSignal
|
||||
hasImplicitContinuationSignal,
|
||||
hasSuggestedIntentPivotSignal
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -1099,6 +1260,7 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
? carryoverTargetIntent ?? rootIntent ?? explicitIntent ?? null
|
||||
: carryoverTargetIntent ?? explicitIntent ?? deps.toNonEmptyString(carryoverMeta?.previousAddressIntent) ?? null;
|
||||
const hasImplicitContinuationSignal = Boolean(carryoverMeta?.hasImplicitContinuationSignal);
|
||||
const hasSuggestedIntentPivotSignal = Boolean(carryoverMeta?.hasSuggestedIntentPivotSignal);
|
||||
const rewrittenByPredecompose =
|
||||
deps.compactWhitespace(sourceMessage.toLowerCase()) !== deps.compactWhitespace(canonicalMessage.toLowerCase());
|
||||
const hasExplicitIntent = Boolean(explicitIntent);
|
||||
|
|
@ -1114,6 +1276,9 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
if (hasImplicitContinuationSignal) {
|
||||
reasons.push("implicit_continuation_by_llm");
|
||||
}
|
||||
if (hasSuggestedIntentPivotSignal) {
|
||||
reasons.push("suggested_intent_followup_pivot");
|
||||
}
|
||||
if (rewrittenByPredecompose) {
|
||||
reasons.push("effective_message_rewritten_by_predecompose");
|
||||
}
|
||||
|
|
@ -1138,7 +1303,8 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
intent_selection_mode: selectionMode,
|
||||
anchor_type: carryoverMeta?.followupContext?.previous_anchor_type ?? null,
|
||||
anchor_value: carryoverMeta?.followupContext?.previous_anchor_value ?? null,
|
||||
implicit_continuation_signal: hasImplicitContinuationSignal
|
||||
implicit_continuation_signal: hasImplicitContinuationSignal,
|
||||
suggested_intent_pivot_signal: hasSuggestedIntentPivotSignal
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,61 @@ describe("counterparty shipment item flow and open-items routing", () => {
|
|||
expect(result.reasons).toContain("counterparty_item_flow_signal_detected");
|
||||
});
|
||||
|
||||
it("keeps plain Russian counterparty item-flow wording out of stale inventory context", async () => {
|
||||
executeAddressMcpQueryMock
|
||||
.mockResolvedValueOnce({
|
||||
fetched_rows: 1,
|
||||
matched_rows: 1,
|
||||
raw_rows: [
|
||||
{
|
||||
Counterparty: "Чепурнов П.Д.",
|
||||
Registrator: "Чепурнов П.Д."
|
||||
}
|
||||
],
|
||||
rows: [],
|
||||
error: null
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
fetched_rows: 1,
|
||||
matched_rows: 1,
|
||||
raw_rows: [
|
||||
{
|
||||
Period: "2022-01-20T12:00:03Z",
|
||||
Registrator: "Поступление товаров и услуг 000000001 от 20.01.2022",
|
||||
AccountDt: "41.01",
|
||||
AccountKt: "60.01",
|
||||
Amount: 890660,
|
||||
Nomenclature: "Услуги по договору",
|
||||
Counterparty: "Чепурнов П.Д.",
|
||||
Contract: "Договор № 11/1 от 25.11.2020 г.",
|
||||
Organization: 'ООО "Альтернатива Плюс"'
|
||||
}
|
||||
],
|
||||
rows: [],
|
||||
error: null
|
||||
});
|
||||
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle("что нам отгружал чепурнов? какой товар или услугу?", {
|
||||
followupContext: {
|
||||
previous_intent: "inventory_on_hand_as_of_date",
|
||||
target_intent: "inventory_on_hand_as_of_date",
|
||||
previous_filters: {
|
||||
organization: 'ООО "Альтернатива Плюс"',
|
||||
as_of_date: "2026-04-24"
|
||||
},
|
||||
previous_anchor_type: "organization",
|
||||
previous_anchor_value: 'ООО "Альтернатива Плюс"'
|
||||
}
|
||||
});
|
||||
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.debug.detected_intent).toBe("list_documents_by_counterparty");
|
||||
expect(result?.debug.selected_recipe).toBe("address_documents_by_counterparty_v1");
|
||||
expect(String(result?.reply_text ?? "")).toContain("Чепурнов П.Д.");
|
||||
expect(String(result?.reply_text ?? "")).toContain("Услуги по договору");
|
||||
});
|
||||
|
||||
it("routes account 60 tails wording to open items intent", () => {
|
||||
const result = resolveAddressIntent("хвосты покажи по счету 60 на август 2022");
|
||||
expect(result.intent).toBe("open_items_by_counterparty_or_contract");
|
||||
|
|
@ -60,8 +115,10 @@ describe("counterparty shipment item flow and open-items routing", () => {
|
|||
});
|
||||
|
||||
it("uses purchase document query for fuzzy counterparty item-flow wording", async () => {
|
||||
executeAddressMcpQueryMock
|
||||
.mockResolvedValueOnce({
|
||||
executeAddressMcpQueryMock.mockImplementation(async (request?: { query?: string }) => {
|
||||
const query = String(request?.query ?? "");
|
||||
if (query.includes("Справочник.Контрагенты")) {
|
||||
return {
|
||||
fetched_rows: 1,
|
||||
matched_rows: 1,
|
||||
raw_rows: [
|
||||
|
|
@ -72,8 +129,9 @@ describe("counterparty shipment item flow and open-items routing", () => {
|
|||
],
|
||||
rows: [],
|
||||
error: null
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
};
|
||||
}
|
||||
return {
|
||||
fetched_rows: 2,
|
||||
matched_rows: 2,
|
||||
raw_rows: [
|
||||
|
|
@ -104,7 +162,8 @@ describe("counterparty shipment item flow and open-items routing", () => {
|
|||
],
|
||||
rows: [],
|
||||
error: null
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle(
|
||||
|
|
|
|||
|
|
@ -117,4 +117,40 @@ describe("counterparty lifecycle organization scope regressions", () => {
|
|||
expect(result?.debug.extracted_filters?.counterparty).toBeUndefined();
|
||||
expect(String(result?.reply_text ?? "")).toContain('ООО "Ромашка"');
|
||||
});
|
||||
|
||||
it("routes who-is-the-highest-value-customer wording to revenue ranking, not lifecycle activity", async () => {
|
||||
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||
fetched_rows: 2,
|
||||
matched_rows: 2,
|
||||
raw_rows: [
|
||||
{
|
||||
Period: "2021-01-15T00:00:00Z",
|
||||
Registrator: "CP_CUSTOMER_ACTIVITY",
|
||||
AccountDt: "51",
|
||||
AccountKt: "62.01",
|
||||
Amount: 150000,
|
||||
Counterparty: "Группа СВК"
|
||||
},
|
||||
{
|
||||
Period: "2021-02-20T00:00:00Z",
|
||||
Registrator: "CP_CUSTOMER_ACTIVITY",
|
||||
AccountDt: "51",
|
||||
AccountKt: "62.01",
|
||||
Amount: 80000,
|
||||
Counterparty: "Чепурнов П.Д."
|
||||
}
|
||||
],
|
||||
rows: [],
|
||||
error: null
|
||||
});
|
||||
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle("кто у нас самый доходный клиент за все время");
|
||||
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.debug.detected_intent).toBe("customer_revenue_and_payments");
|
||||
expect(result?.debug.selected_recipe).toBe("address_customer_revenue_and_payments_v1");
|
||||
expect(String(result?.reply_text ?? "")).toContain("Самый доходный клиент");
|
||||
expect(String(result?.reply_text ?? "")).toContain("Группа СВК");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { resolveAddressIntent } from "../src/services/addressIntentResolver";
|
||||
|
||||
describe("address intent resolver bidirectional value-flow arbitration", () => {
|
||||
it("keeps incoming-vs-outgoing comparison out of one-sided payout routes", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"\u0447\u0442\u043e \u043f\u043e \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441 \u0431\u043e\u043b\u044c\u0448\u0435 \u0432 2020 \u0433\u043e\u0434\u0443: \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0438\u043b\u0438 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0434\u0435\u043d\u044c\u0433\u0438?"
|
||||
);
|
||||
|
||||
expect(result.intent).toBe("unknown");
|
||||
expect(result.reasons).toContain("unicode_bidirectional_value_flow_deferred_to_discovery");
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue