ARCH: ввести data-need graph и довести open-scope comparison до live replay
This commit is contained in:
parent
dca49ef4e1
commit
f2bd2dfdb1
|
|
@ -199,3 +199,50 @@ The next move is larger:
|
||||||
- make the assistant able to look into 1C through bounded MCP discovery,
|
- make the assistant able to look into 1C through bounded MCP discovery,
|
||||||
- choose its path through reviewed primitives,
|
- choose its path through reviewed primitives,
|
||||||
- and answer from proved evidence instead of memorized route scripts.
|
- and answer from proved evidence instead of memorized route scripts.
|
||||||
|
|
||||||
|
## Status Update - 2026-04-22
|
||||||
|
|
||||||
|
The reset above is no longer only directional.
|
||||||
|
|
||||||
|
Its first three large blocks are now considered closed:
|
||||||
|
|
||||||
|
- `Big Block A. Metadata-First Self-Navigation`
|
||||||
|
- `Big Block B. Entity And Schema Grounding`
|
||||||
|
- `Big Block C. Planner-Selected Primitive Chains`
|
||||||
|
|
||||||
|
What is now materially real in code and replay-backed:
|
||||||
|
|
||||||
|
- metadata wording can start a live `inspect_1c_metadata` discovery path instead of collapsing into a hand-written route;
|
||||||
|
- metadata ambiguity can be held as a bounded clarification and resumed into `documents` or `movements`;
|
||||||
|
- grounded entity resolution can continue into `incoming`, `payout`, `net`, `documents`, and `movements` without repeating the entity name on every turn;
|
||||||
|
- planner-selected chains now survive year-switch, lane-switch, and direct follow-up pivots under the guarded evidence gate.
|
||||||
|
|
||||||
|
Current replay anchors for this closure:
|
||||||
|
|
||||||
|
- `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_phase32_planner_selected_chain_end_to_end_live_rerun2` accepted `6/6`.
|
||||||
|
|
||||||
|
What is **not** complete yet:
|
||||||
|
|
||||||
|
- a general open-world understanding layer for structurally new 1C questions;
|
||||||
|
- planner selection from a broader primitive search space without pre-reviewed family scaffolding;
|
||||||
|
- a multi-hop evidence loop that can keep exploring, pause for clarification, and resume on unfamiliar contours.
|
||||||
|
|
||||||
|
That means the reset succeeded at building the bounded autonomy foundation.
|
||||||
|
|
||||||
|
It does **not** yet mean:
|
||||||
|
|
||||||
|
- unrestricted navigation through arbitrary 1C schemas;
|
||||||
|
- no-more-domain-thinking;
|
||||||
|
- "ask anything about 1C and the system will figure it out".
|
||||||
|
|
||||||
|
The next mainline now moves to:
|
||||||
|
|
||||||
|
- [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)
|
||||||
|
|
||||||
|
That document formalizes the next three blocks:
|
||||||
|
|
||||||
|
- `D. Question -> Data Need Graph`
|
||||||
|
- `E. Dynamic Schema Traversal And Primitive Search`
|
||||||
|
- `F. Multi-Hop Evidence Loop`
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,223 @@
|
||||||
|
# 16 - Data Need Graph And Open-World MCP Plan (2026-04-22)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This note opens the next architecture phase after the completion of bounded-autonomy foundation blocks `A/B/C`.
|
||||||
|
|
||||||
|
It is not a restart.
|
||||||
|
|
||||||
|
It is the formal hand-off from:
|
||||||
|
|
||||||
|
- metadata-first self-navigation;
|
||||||
|
- entity/schema grounding;
|
||||||
|
- planner-selected primitive chains;
|
||||||
|
|
||||||
|
to the next problem:
|
||||||
|
|
||||||
|
- how the assistant starts understanding structurally new 1C questions without waiting for one more domain-specific route.
|
||||||
|
|
||||||
|
## Baseline Entering This Phase
|
||||||
|
|
||||||
|
The following is now treated as implemented baseline rather than future intent:
|
||||||
|
|
||||||
|
- live metadata inspection through reviewed MCP primitives;
|
||||||
|
- bounded schema grounding with honest ambiguity handling;
|
||||||
|
- planner-selected reviewed chains across entity resolution, value flow, documents, and movements;
|
||||||
|
- continuity that can preserve grounded entity and period context across the validated chain families.
|
||||||
|
|
||||||
|
Replay-backed anchors for that baseline include:
|
||||||
|
|
||||||
|
- `address_truth_harness_phase24_metadata_lane_choice_loop_live_rerun5`;
|
||||||
|
- `address_truth_harness_phase25_entity_resolution_chain_live_rerun_full_chain`;
|
||||||
|
- `address_truth_harness_phase32_planner_selected_chain_end_to_end_live_rerun2`.
|
||||||
|
|
||||||
|
This is enough to say:
|
||||||
|
|
||||||
|
- the assistant is no longer only a deterministic route bundle;
|
||||||
|
- the system already has a bounded MCP discovery substrate.
|
||||||
|
|
||||||
|
This is **not** enough to say:
|
||||||
|
|
||||||
|
- the assistant already understands arbitrary 1C questions;
|
||||||
|
- the planner already walks open-world 1C structure on its own;
|
||||||
|
- the system can already derive the correct probe sequence for unfamiliar asks without more architecture.
|
||||||
|
|
||||||
|
## Main Remaining Gap
|
||||||
|
|
||||||
|
The current system still understands many turns through a reviewed family scaffold.
|
||||||
|
|
||||||
|
That scaffold is much healthier than old route hardcoding, but it is still a scaffold.
|
||||||
|
|
||||||
|
The next large gap is:
|
||||||
|
|
||||||
|
- new questions must stop depending on whether their wording already resembles one of the preworked families;
|
||||||
|
- the runtime must first understand the user's data need as a machine-readable object;
|
||||||
|
- then search for a safe path through the MCP primitive catalog and observed 1C surface;
|
||||||
|
- then keep iterating until the answer is either proved, honestly bounded, or blocked for a concrete reason.
|
||||||
|
|
||||||
|
In short:
|
||||||
|
|
||||||
|
- move from `reviewed chain families`
|
||||||
|
- toward `open-world bounded evidence planning`.
|
||||||
|
|
||||||
|
## Big Block D. Question -> Data Need Graph
|
||||||
|
|
||||||
|
### Goal
|
||||||
|
|
||||||
|
Create one runtime object that represents the user's business data need independently from any one preworked domain route.
|
||||||
|
|
||||||
|
### Why This Block Comes First
|
||||||
|
|
||||||
|
Without this layer the planner still starts too low:
|
||||||
|
|
||||||
|
- it knows primitive families;
|
||||||
|
- it knows reviewed chains;
|
||||||
|
- but it does not yet have one explicit object saying what kind of business fact the user is asking to prove.
|
||||||
|
|
||||||
|
That means unfamiliar wording still risks falling back into:
|
||||||
|
|
||||||
|
- a nearby known family;
|
||||||
|
- a shallow unsupported verdict;
|
||||||
|
- or a hand-added route patch.
|
||||||
|
|
||||||
|
### Target Object
|
||||||
|
|
||||||
|
`assistant_data_need_graph_v1`
|
||||||
|
|
||||||
|
### Minimum Axes
|
||||||
|
|
||||||
|
- `subject_candidates`
|
||||||
|
- `business_fact_family`
|
||||||
|
- `action_family`
|
||||||
|
- `aggregation_need`
|
||||||
|
- `time_scope_need`
|
||||||
|
- `comparison_need`
|
||||||
|
- `ranking_need`
|
||||||
|
- `proof_expectation`
|
||||||
|
- `clarification_gaps`
|
||||||
|
- `decomposition_candidates`
|
||||||
|
- `forbidden_overclaim_flags`
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
|
||||||
|
- normalize user wording into a business-meaning graph before primitive selection;
|
||||||
|
- distinguish direct fact asks from ranking, comparison, trend, list, drilldown, and explanation asks;
|
||||||
|
- carry explicit versus inherited axes separately;
|
||||||
|
- represent "understood but not yet grounded" without forcing a wrong domain route;
|
||||||
|
- give the planner one shared contract that survives wording variation.
|
||||||
|
|
||||||
|
### Acceptance
|
||||||
|
|
||||||
|
- structurally similar questions with different wording build equivalent data-need graphs;
|
||||||
|
- unfamiliar but intelligible questions stop collapsing into nearest-route guessing;
|
||||||
|
- the planner consumes the graph rather than ad hoc route-only hints;
|
||||||
|
- unsupported cases become "understood but missing evidence path", not "question not understood".
|
||||||
|
|
||||||
|
## Big Block E. Dynamic Schema Traversal And Primitive Search
|
||||||
|
|
||||||
|
### Goal
|
||||||
|
|
||||||
|
Teach the planner to search a broader reviewed primitive space against the observed 1C surface instead of depending on a fixed set of precomposed chain families.
|
||||||
|
|
||||||
|
### Why This Block Matters
|
||||||
|
|
||||||
|
Today the system can chain reviewed primitives well inside validated families.
|
||||||
|
|
||||||
|
What it still cannot do broadly enough is:
|
||||||
|
|
||||||
|
- look at the current data-need graph;
|
||||||
|
- inspect the schema surface;
|
||||||
|
- score plausible objects and primitives;
|
||||||
|
- assemble the next safe probe path dynamically.
|
||||||
|
|
||||||
|
That is the step from `good bounded recipes` to `bounded open-world navigation`.
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
|
||||||
|
- introduce machine-readable primitive capability descriptors and prerequisites;
|
||||||
|
- rank candidate schema objects against the data-need graph;
|
||||||
|
- score document/register/catalog surfaces dynamically;
|
||||||
|
- let the planner build candidate chains from the catalog instead of picking from a short reviewed list only;
|
||||||
|
- preserve ambiguity when the schema does not yet justify a single path;
|
||||||
|
- keep traversal bounded by budget, proof rules, and reviewed primitive availability.
|
||||||
|
|
||||||
|
### Acceptance
|
||||||
|
|
||||||
|
- the planner can assemble a safe next chain without a dedicated per-family recipe;
|
||||||
|
- new questions can move from metadata into plausible schema traversal without route-per-wording work;
|
||||||
|
- ambiguity is retained honestly when multiple schema paths compete;
|
||||||
|
- no chain may bypass the evidence gate or invent unsupported primitives.
|
||||||
|
|
||||||
|
## Big Block F. Multi-Hop Evidence Loop And Clarifying Recovery
|
||||||
|
|
||||||
|
### Goal
|
||||||
|
|
||||||
|
Turn one-shot chain execution into a bounded evidence loop that can probe, detect missing axes, ask for clarification, resume, and stop honestly.
|
||||||
|
|
||||||
|
### Why This Block Is Separate
|
||||||
|
|
||||||
|
Even with a strong data-need graph and better primitive search, the assistant will still fail on unfamiliar asks if it cannot keep iterating safely.
|
||||||
|
|
||||||
|
Open-world bounded autonomy is not one magic route choice.
|
||||||
|
|
||||||
|
It is:
|
||||||
|
|
||||||
|
- execute;
|
||||||
|
- inspect what is still missing;
|
||||||
|
- clarify if needed;
|
||||||
|
- resume the same proof path;
|
||||||
|
- answer only with the evidence that survived the loop.
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
|
||||||
|
- multi-step probe execution with explicit stop conditions;
|
||||||
|
- gap detection for missing organization, period, subject, or surface choice;
|
||||||
|
- clarification turns that preserve the active data-need graph and current probe state;
|
||||||
|
- resume logic that continues the same evidence loop rather than restarting from scratch;
|
||||||
|
- final answer shaping that separates:
|
||||||
|
- proved;
|
||||||
|
- inferred from evidence;
|
||||||
|
- still unknown.
|
||||||
|
|
||||||
|
### Acceptance
|
||||||
|
|
||||||
|
- a new question can survive more than one evidence hop without losing its meaning;
|
||||||
|
- clarification resumes the same proof path instead of resetting into local heuristics;
|
||||||
|
- the assistant can explain what is proved, what is inferred, and what remains unproved;
|
||||||
|
- unfamiliar asks no longer fail just because the first probe was incomplete.
|
||||||
|
|
||||||
|
## Execution Order
|
||||||
|
|
||||||
|
The next module should now be executed in this order:
|
||||||
|
|
||||||
|
1. `D. Question -> Data Need Graph`
|
||||||
|
2. `E. Dynamic Schema Traversal And Primitive Search`
|
||||||
|
3. `F. Multi-Hop Evidence Loop And Clarifying Recovery`
|
||||||
|
|
||||||
|
The order matters.
|
||||||
|
|
||||||
|
Doing `E` without `D` would create smarter probing without a stable representation of the user ask.
|
||||||
|
|
||||||
|
Doing `F` before `D/E` would create a more complex loop still attached to narrow reviewed families.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
This phase should **not** be implemented as:
|
||||||
|
|
||||||
|
- another wave of domain-by-domain route stitching;
|
||||||
|
- a hidden prompt wrapper that pretends to be agentic;
|
||||||
|
- unrestricted free exploration of 1C without reviewed primitive boundaries;
|
||||||
|
- answer-time improvisation that bypasses the evidence gate;
|
||||||
|
- a stealth rewrite of the whole orchestration stack.
|
||||||
|
|
||||||
|
## Success Condition
|
||||||
|
|
||||||
|
This phase is successful only when a new human user can ask a structurally new but intelligible 1C data question and the assistant can:
|
||||||
|
|
||||||
|
1. understand the data need in machine-readable form;
|
||||||
|
2. search for a safe path through reviewed MCP primitives and observed schema surface;
|
||||||
|
3. iterate through bounded evidence steps;
|
||||||
|
4. ask a bounded clarification when needed;
|
||||||
|
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.
|
||||||
|
|
@ -32,38 +32,46 @@ This package answers the next question:
|
||||||
12. [12 - manual_run_system_analysis_3NilqwT1G2_2026-04-18.md](./12%20-%20manual_run_system_analysis_3NilqwT1G2_2026-04-18.md)
|
12. [12 - manual_run_system_analysis_3NilqwT1G2_2026-04-18.md](./12%20-%20manual_run_system_analysis_3NilqwT1G2_2026-04-18.md)
|
||||||
13. [13 - pre_multidomain_readiness_audit_2026-04-18.md](./13%20-%20pre_multidomain_readiness_audit_2026-04-18.md)
|
13. [13 - pre_multidomain_readiness_audit_2026-04-18.md](./13%20-%20pre_multidomain_readiness_audit_2026-04-18.md)
|
||||||
14. [14 - semantic_dialog_authority_recovery_plan_2026-04-19.md](./14%20-%20semantic_dialog_authority_recovery_plan_2026-04-19.md)
|
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)
|
||||||
|
|
||||||
## Current Status Snapshot (2026-04-19)
|
## Current Status Snapshot (2026-04-22)
|
||||||
|
|
||||||
This package is no longer planning-only.
|
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, but still not ready for wide multi-domain expansion:
|
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:
|
||||||
|
|
||||||
- route, transition, boundary, meta, memory, and provider policy owners exist as separate modules;
|
- 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;
|
- exact-lane truth and coverage/evidence contracts exist as explicit runtime artifacts;
|
||||||
- scenario acceptance writes machine-readable `scenario_acceptance_matrix.json` and `pack_state.json`;
|
- scenario acceptance writes machine-readable `scenario_acceptance_matrix.json` and `pack_state.json`;
|
||||||
- AGENT semantic packs and source catalogs already exist for mixed domain/meta validation.
|
- 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`:
|
||||||
|
- `Question -> Data Need Graph`
|
||||||
|
- dynamic schema traversal and primitive search
|
||||||
|
- multi-hop evidence loop with bounded clarification recovery
|
||||||
|
|
||||||
Current honest status:
|
Current honest status:
|
||||||
|
|
||||||
- turnaround implementation progress: `~96%`
|
- turnaround implementation progress: `~96%`
|
||||||
- exit-from-danger-zone readiness: `~91%`
|
- exit-from-danger-zone readiness: `~91%`
|
||||||
- pre-multidomain readiness: `~78%`
|
- pre-multidomain readiness: `~78%`
|
||||||
- graph snapshot after latest rebuild: `5372 nodes`, `11525 edges`, `135 communities`
|
- bounded-autonomy foundation readiness: `~60%`
|
||||||
|
- graph snapshot after latest rebuild: `5741 nodes`, `12385 edges`, `137 communities`
|
||||||
- current breakpoint:
|
- current breakpoint:
|
||||||
- the validated hot paths are no longer structurally broken;
|
- the validated hot paths are no longer structurally broken;
|
||||||
- flagship continuity collapse is no longer the primary risk;
|
- flagship continuity collapse is no longer the primary risk;
|
||||||
- the main remaining risk is no longer clarification-resume collapse, but the unfinished final convergence toward one true runtime authority plus replay breadth still below the intended multi-domain blast radius;
|
- 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 of user-facing answers is now a first-class blocker;
|
- 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 "the route collapsed", but "a new user can still feel that the assistant is glitchy, misses intent, or answers the wrong thing on short live wording".
|
- 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".
|
||||||
- main remaining architectural pressure:
|
- main remaining architectural pressure:
|
||||||
- no single fully authoritative continuity contract consumed by every hot runtime owner
|
- no general `Question -> Data Need Graph` authority yet
|
||||||
- residual coordinator/legacy pressure inside `assistantService.ts`
|
- 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
|
||||||
- central domain-intent pressure inside `resolveAddressIntent()`
|
- central domain-intent pressure inside `resolveAddressIntent()`
|
||||||
- replay breadth still narrower than the intended multi-domain rollout surface beyond the flagship and late-switch families
|
- replay breadth is still below the future open-world autonomy surface
|
||||||
- remaining answer-semantics pressure inside `composeStage.ts` / `answerComposer.ts`
|
|
||||||
- insufficient semantic robustness on live user wording, especially short follow-up retarget, typo tolerance, and intent-faithful human answers
|
|
||||||
- no guarded MCP semantic discovery lane yet for understood long-tail 1C questions that should not require one-off route hardcoding
|
|
||||||
|
|
||||||
Latest live proof now includes:
|
Latest live proof now includes:
|
||||||
|
|
||||||
|
|
@ -72,14 +80,17 @@ Latest live proof now includes:
|
||||||
- `address_truth_harness_phase15_answer_inspection_followup_live_20260419_rerun11` accepted `9/9`
|
- `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_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_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_phase32_planner_selected_chain_end_to_end_live_rerun2` accepted `6/6`
|
||||||
|
|
||||||
Current architectural reading:
|
Current architectural reading:
|
||||||
|
|
||||||
- the system is already materially past the dangerous regression breakpoint;
|
- 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 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.
|
- 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 now `90%+ pre-multidomain readiness`, and the remaining gap should be treated as five large architecture iterations rather than as cosmetic cleanup.
|
- 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 would feel that the assistant understands the intent and responds meaningfully in live wording.
|
- 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.
|
||||||
|
|
||||||
For the detailed audit, current percentages, and remaining debt, read:
|
For the detailed audit, current percentages, and remaining debt, read:
|
||||||
|
|
||||||
|
|
@ -90,6 +101,8 @@ For the detailed audit, current percentages, and remaining debt, read:
|
||||||
- [12 - manual_run_system_analysis_3NilqwT1G2_2026-04-18.md](./12%20-%20manual_run_system_analysis_3NilqwT1G2_2026-04-18.md)
|
- [12 - manual_run_system_analysis_3NilqwT1G2_2026-04-18.md](./12%20-%20manual_run_system_analysis_3NilqwT1G2_2026-04-18.md)
|
||||||
- [13 - pre_multidomain_readiness_audit_2026-04-18.md](./13%20-%20pre_multidomain_readiness_audit_2026-04-18.md)
|
- [13 - pre_multidomain_readiness_audit_2026-04-18.md](./13%20-%20pre_multidomain_readiness_audit_2026-04-18.md)
|
||||||
- [14 - semantic_dialog_authority_recovery_plan_2026-04-19.md](./14%20-%20semantic_dialog_authority_recovery_plan_2026-04-19.md)
|
- [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)
|
||||||
|
|
||||||
## Architectural Objects Of Planning
|
## Architectural Objects Of Planning
|
||||||
|
|
||||||
|
|
@ -122,6 +135,8 @@ Read in this order:
|
||||||
13. `12 - manual_run_system_analysis_3NilqwT1G2_2026-04-18.md`
|
13. `12 - manual_run_system_analysis_3NilqwT1G2_2026-04-18.md`
|
||||||
14. `13 - pre_multidomain_readiness_audit_2026-04-18.md`
|
14. `13 - pre_multidomain_readiness_audit_2026-04-18.md`
|
||||||
15. `14 - semantic_dialog_authority_recovery_plan_2026-04-19.md`
|
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`
|
||||||
|
|
||||||
## Planning Rules
|
## Planning Rules
|
||||||
|
|
||||||
|
|
@ -141,15 +156,14 @@ and start being described as:
|
||||||
|
|
||||||
- "a stateful exact-data assistant with explicit transition contracts and isolated truth gating."
|
- "a stateful exact-data assistant with explicit transition contracts and isolated truth gating."
|
||||||
|
|
||||||
As of `2026-04-19`, 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 toward one runtime authority plus still-insufficient replay breadth for low-risk multi-domain expansion.
|
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.
|
||||||
|
|
||||||
The biggest remaining blockers are:
|
The biggest remaining blockers are:
|
||||||
|
|
||||||
- split continuity ownership across route / transition / recap / coordinator glue;
|
- no general `Question -> Data Need Graph` runtime authority yet;
|
||||||
- saved-session acceptance still too narrow compared with the intended domain-expansion blast radius outside the repaired flagship + late-pivot families;
|
- planner-selected primitive chains are real, but still narrower than open-world primitive search;
|
||||||
- clarification precedence is much better than before, but still not yet proven widely enough outside the repaired replay family;
|
- 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;
|
||||||
- residual `assistantService` overload;
|
- residual `assistantService` overload;
|
||||||
- central intent pressure in `resolveAddressIntent()`;
|
- central intent pressure in `resolveAddressIntent()`;
|
||||||
- remaining answer-semantics pressure in `composeStage.ts` and `answerComposer.ts`.
|
|
||||||
- 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 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.
|
||||||
- missing MCP semantic data-discovery layer where Qwen3 can help plan controlled 1C evidence search without bypassing runtime truth gates.
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"schema_version": "domain_truth_harness_spec_v1",
|
||||||
|
"scenario_id": "address_truth_harness_phase33_open_scope_value_flow_comparison",
|
||||||
|
"domain": "address_phase33_open_scope_value_flow_comparison",
|
||||||
|
"title": "Phase 33 open-scope value-flow comparison replay",
|
||||||
|
"description": "Targeted AGENT replay for Big Block D where an open-scope incoming-vs-outgoing money question must be understood as a bounded comparison need, not as a missing-counterparty fact ask.",
|
||||||
|
"bindings": {},
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step_id": "step_01_compare_incoming_vs_outgoing_for_org",
|
||||||
|
"title": "Raw organization-scoped comparison wording produces a bounded incoming-vs-outgoing answer without inventing a counterparty",
|
||||||
|
"question": "что больше: входящие или исходящие деньги за 2020 год по ООО Альтернатива Плюс?",
|
||||||
|
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"(?i)2020",
|
||||||
|
"(?i)входящ|получили|поступ",
|
||||||
|
"(?i)исходящ|заплатили|списан|платеж",
|
||||||
|
"(?i)руб"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_any": [
|
||||||
|
"(?i)нетто|сальдо",
|
||||||
|
"(?i)больше|превыш",
|
||||||
|
"(?i)альтернатива"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"(?i)уточните контрагента",
|
||||||
|
"(?i)не найден контрагент",
|
||||||
|
"(?i)по какому контрагенту",
|
||||||
|
"(?i)не найдено контрагента"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": ["value_flow_comparison", "open_scope", "organization_scoped", "bounded_autonomy"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -141,6 +141,16 @@ function isMovementLaneClarification(pilot) {
|
||||||
askedActionFamily(pilot) === "list_movements" ||
|
askedActionFamily(pilot) === "list_movements" ||
|
||||||
unsupportedFamily(pilot) === "movement_evidence");
|
unsupportedFamily(pilot) === "movement_evidence");
|
||||||
}
|
}
|
||||||
|
function isRankedValueFlowClarification(pilot) {
|
||||||
|
return (pilot.reason_codes.includes("planner_selected_top_ranked_value_flow_from_data_need_graph") ||
|
||||||
|
pilot.reason_codes.includes("planner_selected_bottom_ranked_value_flow_from_data_need_graph") ||
|
||||||
|
pilot.dry_run.reason_codes.includes("planner_selected_top_ranked_value_flow_from_data_need_graph") ||
|
||||||
|
pilot.dry_run.reason_codes.includes("planner_selected_bottom_ranked_value_flow_from_data_need_graph"));
|
||||||
|
}
|
||||||
|
function isBidirectionalValueFlowComparisonClarification(pilot) {
|
||||||
|
return (pilot.reason_codes.includes("planner_selected_bidirectional_value_flow_comparison_from_data_need_graph") ||
|
||||||
|
pilot.dry_run.reason_codes.includes("planner_selected_bidirectional_value_flow_comparison_from_data_need_graph"));
|
||||||
|
}
|
||||||
function isDocumentLaneClarification(pilot) {
|
function isDocumentLaneClarification(pilot) {
|
||||||
return (isDocumentPilot(pilot) ||
|
return (isDocumentPilot(pilot) ||
|
||||||
pilot.reason_codes.includes("planner_selected_document_recipe") ||
|
pilot.reason_codes.includes("planner_selected_document_recipe") ||
|
||||||
|
|
@ -152,12 +162,20 @@ function laneScopeSuffix(pilot) {
|
||||||
const entity = firstEntityCandidate(pilot);
|
const entity = firstEntityCandidate(pilot);
|
||||||
return entity ? ` по "${entity}"` : "";
|
return entity ? ` по "${entity}"` : "";
|
||||||
}
|
}
|
||||||
|
function dryRunHasAxis(pilot, axis) {
|
||||||
|
return pilot.dry_run.execution_steps.some((step) => step.provided_axes.includes(axis));
|
||||||
|
}
|
||||||
function dryRunMissingAxis(pilot, axis) {
|
function dryRunMissingAxis(pilot, axis) {
|
||||||
|
if (dryRunHasAxis(pilot, axis)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return pilot.dry_run.execution_steps.some((step) => step.missing_axis_options.some((option) => option.includes(axis)));
|
return pilot.dry_run.execution_steps.some((step) => step.missing_axis_options.some((option) => option.includes(axis)));
|
||||||
}
|
}
|
||||||
function clarificationNeedRu(pilot) {
|
function clarificationNeedRu(pilot) {
|
||||||
|
const hasCounterparty = dryRunHasAxis(pilot, "counterparty");
|
||||||
|
const hasAccount = dryRunHasAxis(pilot, "account");
|
||||||
const needsPeriod = dryRunMissingAxis(pilot, "period");
|
const needsPeriod = dryRunMissingAxis(pilot, "period");
|
||||||
const needsOrganization = dryRunMissingAxis(pilot, "organization");
|
const needsOrganization = !hasCounterparty && !hasAccount && dryRunMissingAxis(pilot, "organization");
|
||||||
if (needsPeriod && needsOrganization) {
|
if (needsPeriod && needsOrganization) {
|
||||||
return { subject: "проверяемый период и организацию", verb: "нужно" };
|
return { subject: "проверяемый период и организацию", verb: "нужно" };
|
||||||
}
|
}
|
||||||
|
|
@ -210,6 +228,9 @@ function headlineFor(mode, pilot) {
|
||||||
pilot.derived_entity_resolution?.resolution_status === "not_found") {
|
pilot.derived_entity_resolution?.resolution_status === "not_found") {
|
||||||
return "По текущему каталожному поиску 1С точный контрагент пока не подтвержден.";
|
return "По текущему каталожному поиску 1С точный контрагент пока не подтвержден.";
|
||||||
}
|
}
|
||||||
|
if (pilot.derived_ranked_value_flow && mode === "confirmed_with_bounded_inference") {
|
||||||
|
return "По данным 1С можно построить ограниченный ranking по контрагентам на подтвержденных строках денежных движений.";
|
||||||
|
}
|
||||||
if (isMovementPilot(pilot) && mode === "confirmed_with_bounded_inference") {
|
if (isMovementPilot(pilot) && mode === "confirmed_with_bounded_inference") {
|
||||||
return `По движениям${documentOrMovementScopeRu(pilot)} в 1С найдены подтвержденные строки; ответ ограничен проверенным окном и найденными строками.`;
|
return `По движениям${documentOrMovementScopeRu(pilot)} в 1С найдены подтвержденные строки; ответ ограничен проверенным окном и найденными строками.`;
|
||||||
}
|
}
|
||||||
|
|
@ -269,6 +290,14 @@ function headlineFor(mode, pilot) {
|
||||||
const need = clarificationNeedRu(pilot);
|
const need = clarificationNeedRu(pilot);
|
||||||
return `Могу идти дальше по документам${laneScopeSuffix(pilot)}, но для запуска поиска в 1С ${need.verb} ${need.subject}.`;
|
return `Могу идти дальше по документам${laneScopeSuffix(pilot)}, но для запуска поиска в 1С ${need.verb} ${need.subject}.`;
|
||||||
}
|
}
|
||||||
|
if (mode === "needs_clarification" && isBidirectionalValueFlowComparisonClarification(pilot)) {
|
||||||
|
const need = clarificationNeedRu(pilot);
|
||||||
|
return `Могу сравнить входящий и исходящий денежный поток, но для bounded поиска в 1С ${need.verb} ${need.subject}.`;
|
||||||
|
}
|
||||||
|
if (mode === "needs_clarification" && isRankedValueFlowClarification(pilot)) {
|
||||||
|
const need = clarificationNeedRu(pilot);
|
||||||
|
return `Могу посчитать ranking по денежному потоку между контрагентами, но для bounded поиска в 1С ${need.verb} ${need.subject}.`;
|
||||||
|
}
|
||||||
if (mode === "needs_clarification") {
|
if (mode === "needs_clarification") {
|
||||||
return "Нужно уточнить контекст перед поиском в 1С.";
|
return "Нужно уточнить контекст перед поиском в 1С.";
|
||||||
}
|
}
|
||||||
|
|
@ -302,6 +331,12 @@ function nextStepFor(mode, pilot) {
|
||||||
if (mode === "needs_clarification" && isDocumentLaneClarification(pilot)) {
|
if (mode === "needs_clarification" && isDocumentLaneClarification(pilot)) {
|
||||||
return clarificationNextStepLine(pilot, "документам");
|
return clarificationNextStepLine(pilot, "документам");
|
||||||
}
|
}
|
||||||
|
if (mode === "needs_clarification" && isBidirectionalValueFlowComparisonClarification(pilot)) {
|
||||||
|
return clarificationNextStepLine(pilot, "сравнению входящих и исходящих денежных потоков");
|
||||||
|
}
|
||||||
|
if (mode === "needs_clarification" && isRankedValueFlowClarification(pilot)) {
|
||||||
|
return clarificationNextStepLine(pilot, "ranking-поиску между контрагентами");
|
||||||
|
}
|
||||||
if (mode === "needs_clarification") {
|
if (mode === "needs_clarification") {
|
||||||
return "Уточните контрагента, период или организацию, и я смогу выполнить проверку по 1С.";
|
return "Уточните контрагента, период или организацию, и я смогу выполнить проверку по 1С.";
|
||||||
}
|
}
|
||||||
|
|
@ -336,6 +371,10 @@ function buildMustNotClaim(pilot) {
|
||||||
claims.push("Do not claim full all-time turnover unless the checked period and coverage prove it.");
|
claims.push("Do not claim full all-time turnover unless the checked period and coverage prove it.");
|
||||||
claims.push("Do not present a derived sum as a legal/accounting final total outside the checked 1C rows.");
|
claims.push("Do not present a derived sum as a legal/accounting final total outside the checked 1C rows.");
|
||||||
}
|
}
|
||||||
|
if (pilot.derived_ranked_value_flow) {
|
||||||
|
claims.push("Do not present a bounded ranking as a complete all-time ranking outside the checked period and organization.");
|
||||||
|
claims.push("Do not imply the top-ranked counterparty is globally final when probe-limit or scope boundaries still exist.");
|
||||||
|
}
|
||||||
if (isDocumentPilot(pilot)) {
|
if (isDocumentPilot(pilot)) {
|
||||||
claims.push("Do not claim full document history outside the checked period.");
|
claims.push("Do not claim full document history outside the checked period.");
|
||||||
claims.push("Do not present the confirmed document rows as a complete document universe.");
|
claims.push("Do not present the confirmed document rows as a complete document universe.");
|
||||||
|
|
@ -463,6 +502,40 @@ function derivedEntityResolutionInferenceLine(pilot) {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
function derivedRankedValueFlowInferenceLine(pilot) {
|
||||||
|
const ranking = pilot.derived_ranked_value_flow;
|
||||||
|
if (!ranking) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : "";
|
||||||
|
const period = ranking.period_scope ? ` за период ${ranking.period_scope}` : " в проверенном окне";
|
||||||
|
return `Ranking по контрагентам${organization}${period} рассчитан только по подтвержденным строкам 1С и не доказывает полный исторический срез вне проверенного окна.`;
|
||||||
|
}
|
||||||
|
function derivedRankedValueFlowConfirmedLine(pilot) {
|
||||||
|
const ranking = pilot.derived_ranked_value_flow;
|
||||||
|
if (!ranking || ranking.ranked_values.length <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const leader = ranking.ranked_values[0];
|
||||||
|
const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : "";
|
||||||
|
const period = ranking.period_scope ? ` за период ${ranking.period_scope}` : " в проверенном окне";
|
||||||
|
const directionLead = ranking.ranking_need === "bottom_asc"
|
||||||
|
? ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||||
|
? "Меньше всего заплатили контрагенту"
|
||||||
|
: "Меньше всего денег принёс контрагент"
|
||||||
|
: ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||||
|
? "Больше всего заплатили контрагенту"
|
||||||
|
: "Больше всего денег принёс контрагент";
|
||||||
|
const tail = ranking.ranked_values
|
||||||
|
.slice(1, 3)
|
||||||
|
.map((bucket) => `${bucket.axis_value} — ${bucket.total_amount_human_ru}`)
|
||||||
|
.join("; ");
|
||||||
|
const trail = tail ? ` Следом: ${tail}.` : "";
|
||||||
|
const limitCaveat = ranking.coverage_limited_by_probe_limit
|
||||||
|
? " Лимит строк проверки достигнут; ranking может быть неполным."
|
||||||
|
: "";
|
||||||
|
return `${directionLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${trail}${limitCaveat}`;
|
||||||
|
}
|
||||||
function derivedValueFlowConfirmedLine(pilot) {
|
function derivedValueFlowConfirmedLine(pilot) {
|
||||||
const flow = pilot.derived_value_flow;
|
const flow = pilot.derived_value_flow;
|
||||||
if (!flow) {
|
if (!flow) {
|
||||||
|
|
@ -553,13 +626,16 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
|
||||||
}
|
}
|
||||||
const derivedInferenceLine = derivedActivityInferenceLine(pilot) ??
|
const derivedInferenceLine = derivedActivityInferenceLine(pilot) ??
|
||||||
derivedMetadataInferenceLine(pilot) ??
|
derivedMetadataInferenceLine(pilot) ??
|
||||||
|
derivedRankedValueFlowInferenceLine(pilot) ??
|
||||||
derivedEntityResolutionInferenceLine(pilot);
|
derivedEntityResolutionInferenceLine(pilot);
|
||||||
const inferenceLines = derivedInferenceLine
|
const inferenceLines = derivedInferenceLine
|
||||||
? [derivedInferenceLine]
|
? [derivedInferenceLine]
|
||||||
: pilot.evidence.inferred_facts;
|
: pilot.evidence.inferred_facts;
|
||||||
const derivedMetadataLine = derivedMetadataConfirmedLine(pilot);
|
const derivedMetadataLine = derivedMetadataConfirmedLine(pilot);
|
||||||
const derivedEntityResolutionLine = derivedEntityResolutionConfirmedLine(pilot);
|
const derivedEntityResolutionLine = derivedEntityResolutionConfirmedLine(pilot);
|
||||||
const derivedValueLine = derivedBidirectionalValueFlowConfirmedLine(pilot) ?? derivedValueFlowConfirmedLine(pilot);
|
const derivedValueLine = derivedBidirectionalValueFlowConfirmedLine(pilot) ??
|
||||||
|
derivedRankedValueFlowConfirmedLine(pilot) ??
|
||||||
|
derivedValueFlowConfirmedLine(pilot);
|
||||||
const monthlyConfirmedLines = derivedBidirectionalValueFlowMonthlyLines(pilot).length > 0
|
const monthlyConfirmedLines = derivedBidirectionalValueFlowMonthlyLines(pilot).length > 0
|
||||||
? derivedBidirectionalValueFlowMonthlyLines(pilot)
|
? derivedBidirectionalValueFlowMonthlyLines(pilot)
|
||||||
: derivedValueFlowMonthlyLines(pilot);
|
: derivedValueFlowMonthlyLines(pilot);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,286 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION = void 0;
|
||||||
|
exports.buildAssistantMcpDiscoveryDataNeedGraph = buildAssistantMcpDiscoveryDataNeedGraph;
|
||||||
|
exports.ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION = "assistant_data_need_graph_v1";
|
||||||
|
function toNonEmptyString(value) {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const text = String(value).trim();
|
||||||
|
return text.length > 0 ? text : null;
|
||||||
|
}
|
||||||
|
function lower(value) {
|
||||||
|
return String(value ?? "").trim().toLowerCase();
|
||||||
|
}
|
||||||
|
function normalizeReasonCode(value) {
|
||||||
|
const normalized = value
|
||||||
|
.trim()
|
||||||
|
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
|
||||||
|
.replace(/^_+|_+$/g, "")
|
||||||
|
.toLowerCase();
|
||||||
|
return normalized.length > 0 ? normalized.slice(0, 120) : null;
|
||||||
|
}
|
||||||
|
function pushReason(target, value) {
|
||||||
|
const normalized = normalizeReasonCode(value);
|
||||||
|
if (normalized && !target.includes(normalized)) {
|
||||||
|
target.push(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function pushUnique(target, value) {
|
||||||
|
const text = toNonEmptyString(value);
|
||||||
|
if (text && !target.includes(text)) {
|
||||||
|
target.push(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function businessFactFamilyFor(input) {
|
||||||
|
const combined = `${input.semanticDataNeed} ${input.domain} ${input.action} ${input.unsupported}`.trim();
|
||||||
|
if (combined.includes("metadata lane clarification")) {
|
||||||
|
return "schema_surface";
|
||||||
|
}
|
||||||
|
if (combined.includes("metadata")) {
|
||||||
|
return "schema_surface";
|
||||||
|
}
|
||||||
|
if (combined.includes("entity discovery") || combined.includes("entity_resolution")) {
|
||||||
|
return "entity_grounding";
|
||||||
|
}
|
||||||
|
if (combined.includes("lifecycle") || combined.includes("activity")) {
|
||||||
|
return "activity_lifecycle";
|
||||||
|
}
|
||||||
|
if (combined.includes("movement")) {
|
||||||
|
return "movement_evidence";
|
||||||
|
}
|
||||||
|
if (combined.includes("document")) {
|
||||||
|
return "document_evidence";
|
||||||
|
}
|
||||||
|
if (combined.includes("value-flow") || combined.includes("turnover") || combined.includes("payout") || combined.includes("net")) {
|
||||||
|
return "value_flow";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function aggregationNeedFor(axis) {
|
||||||
|
if (!axis) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (axis === "month") {
|
||||||
|
return "by_month";
|
||||||
|
}
|
||||||
|
return `by_${axis}`;
|
||||||
|
}
|
||||||
|
function timeScopeNeedFor(input) {
|
||||||
|
if (input.explicitDateScope) {
|
||||||
|
return "explicit_period";
|
||||||
|
}
|
||||||
|
if (input.family === "value_flow" || input.family === "movement_evidence" || input.family === "document_evidence") {
|
||||||
|
return "period_required";
|
||||||
|
}
|
||||||
|
if (input.family === "activity_lifecycle") {
|
||||||
|
return "open_activity_window";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function comparisonNeedFor(action) {
|
||||||
|
if (action === "net_value_flow") {
|
||||||
|
return "incoming_vs_outgoing";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function allowsOpenScopeWithoutSubject(input) {
|
||||||
|
if (input.family !== "value_flow") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Boolean(input.rankingNeed || input.comparisonNeed === "incoming_vs_outgoing");
|
||||||
|
}
|
||||||
|
function rankingNeedFromRawUtterance(value) {
|
||||||
|
const text = lower(value);
|
||||||
|
if (!text) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (/(?:\btop[-\s]?\d+\b|\btop\b|топ[-\s]?\d+|топ\b|сам(?:ый|ая|ое|ые)\b|больше\s+всего|наибол[её]е|highest|largest|most)/iu.test(text)) {
|
||||||
|
return "top_desc";
|
||||||
|
}
|
||||||
|
if (/(?:меньше\s+всего|наимен[ьш]е|lowest|smallest|least)/iu.test(text)) {
|
||||||
|
return "bottom_asc";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function proofExpectationFor(input) {
|
||||||
|
if (input.clarificationGaps.length > 0) {
|
||||||
|
return "clarification_required";
|
||||||
|
}
|
||||||
|
if (input.family === "schema_surface") {
|
||||||
|
return "schema_surface";
|
||||||
|
}
|
||||||
|
if (input.family === "entity_grounding") {
|
||||||
|
return "entity_grounding";
|
||||||
|
}
|
||||||
|
if (input.family === "activity_lifecycle") {
|
||||||
|
return "bounded_inference";
|
||||||
|
}
|
||||||
|
return "coverage_checked_fact";
|
||||||
|
}
|
||||||
|
function decompositionCandidatesFor(input) {
|
||||||
|
const result = [];
|
||||||
|
if (input.family === "schema_surface") {
|
||||||
|
pushUnique(result, "inspect_metadata_surface");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (input.family === "entity_grounding") {
|
||||||
|
pushUnique(result, "search_business_entity");
|
||||||
|
pushUnique(result, "resolve_entity_reference");
|
||||||
|
pushUnique(result, "probe_coverage");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (input.family === "value_flow") {
|
||||||
|
if (input.rankingNeed && input.openScopeWithoutSubject) {
|
||||||
|
pushUnique(result, "collect_scoped_movements");
|
||||||
|
pushUnique(result, "aggregate_ranked_axis_values");
|
||||||
|
pushUnique(result, "probe_coverage");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (input.comparisonNeed === "incoming_vs_outgoing" && input.openScopeWithoutSubject) {
|
||||||
|
pushUnique(result, "collect_incoming_movements");
|
||||||
|
pushUnique(result, "collect_outgoing_movements");
|
||||||
|
if (input.aggregationNeed === "by_month") {
|
||||||
|
pushUnique(result, "aggregate_by_month");
|
||||||
|
}
|
||||||
|
pushUnique(result, "probe_coverage");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
pushUnique(result, "resolve_entity_reference");
|
||||||
|
if (input.action === "net_value_flow") {
|
||||||
|
pushUnique(result, "collect_incoming_movements");
|
||||||
|
pushUnique(result, "collect_outgoing_movements");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
pushUnique(result, "collect_scoped_movements");
|
||||||
|
}
|
||||||
|
pushUnique(result, input.aggregationNeed === "by_month" ? "aggregate_by_month" : "aggregate_checked_amounts");
|
||||||
|
pushUnique(result, "probe_coverage");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (input.family === "movement_evidence") {
|
||||||
|
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");
|
||||||
|
pushUnique(result, "fetch_scoped_documents");
|
||||||
|
pushUnique(result, "probe_coverage");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (input.family === "activity_lifecycle") {
|
||||||
|
pushUnique(result, "resolve_entity_reference");
|
||||||
|
pushUnique(result, "fetch_supporting_documents");
|
||||||
|
pushUnique(result, "probe_coverage");
|
||||||
|
pushUnique(result, "explain_evidence_basis");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
function forbiddenOverclaimFlagsFor(family) {
|
||||||
|
const result = ["no_raw_model_claims"];
|
||||||
|
if (family === "schema_surface") {
|
||||||
|
pushUnique(result, "no_fake_schema_surface");
|
||||||
|
}
|
||||||
|
if (family === "entity_grounding") {
|
||||||
|
pushUnique(result, "no_unresolved_entity_claim");
|
||||||
|
}
|
||||||
|
if (family === "activity_lifecycle") {
|
||||||
|
pushUnique(result, "no_legal_age_claim_without_evidence");
|
||||||
|
}
|
||||||
|
if (family === "value_flow" || family === "movement_evidence" || family === "document_evidence") {
|
||||||
|
pushUnique(result, "no_unchecked_fact_totals");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
function buildAssistantMcpDiscoveryDataNeedGraph(input) {
|
||||||
|
const semanticDataNeed = lower(input.semanticDataNeed);
|
||||||
|
const turnMeaning = input.turnMeaning ?? null;
|
||||||
|
const domain = lower(turnMeaning?.asked_domain_family);
|
||||||
|
const action = lower(turnMeaning?.asked_action_family);
|
||||||
|
const unsupported = lower(turnMeaning?.unsupported_but_understood_family);
|
||||||
|
const rawUtterance = lower(input.rawUtterance);
|
||||||
|
const aggregationAxis = lower(turnMeaning?.asked_aggregation_axis);
|
||||||
|
const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope);
|
||||||
|
const subjectCandidates = (turnMeaning?.explicit_entity_candidates ?? [])
|
||||||
|
.map((item) => toNonEmptyString(item))
|
||||||
|
.filter((item) => Boolean(item));
|
||||||
|
const businessFactFamily = businessFactFamilyFor({
|
||||||
|
semanticDataNeed,
|
||||||
|
domain,
|
||||||
|
action,
|
||||||
|
unsupported
|
||||||
|
});
|
||||||
|
const aggregationNeed = aggregationNeedFor(aggregationAxis);
|
||||||
|
const comparisonNeed = comparisonNeedFor(action);
|
||||||
|
const rankingNeed = rankingNeedFromRawUtterance(rawUtterance);
|
||||||
|
const openScopeWithoutSubject = subjectCandidates.length === 0 &&
|
||||||
|
allowsOpenScopeWithoutSubject({
|
||||||
|
family: businessFactFamily,
|
||||||
|
comparisonNeed,
|
||||||
|
rankingNeed
|
||||||
|
});
|
||||||
|
const clarificationGaps = [];
|
||||||
|
if (unsupported === "metadata_lane_choice_clarification" || action === "resolve_next_lane") {
|
||||||
|
pushUnique(clarificationGaps, "lane_family_choice");
|
||||||
|
}
|
||||||
|
if (subjectCandidates.length === 0 && businessFactFamily !== "schema_surface" && !openScopeWithoutSubject) {
|
||||||
|
pushUnique(clarificationGaps, "subject");
|
||||||
|
}
|
||||||
|
const timeScopeNeed = timeScopeNeedFor({
|
||||||
|
family: businessFactFamily,
|
||||||
|
explicitDateScope
|
||||||
|
});
|
||||||
|
if (timeScopeNeed === "period_required" && !explicitDateScope) {
|
||||||
|
pushUnique(clarificationGaps, "period");
|
||||||
|
}
|
||||||
|
const decompositionCandidates = decompositionCandidatesFor({
|
||||||
|
family: businessFactFamily,
|
||||||
|
action,
|
||||||
|
aggregationNeed,
|
||||||
|
comparisonNeed,
|
||||||
|
rankingNeed,
|
||||||
|
openScopeWithoutSubject
|
||||||
|
});
|
||||||
|
const reasonCodes = [];
|
||||||
|
pushReason(reasonCodes, "data_need_graph_built");
|
||||||
|
if (businessFactFamily) {
|
||||||
|
pushReason(reasonCodes, `data_need_graph_family_${businessFactFamily}`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
pushReason(reasonCodes, "data_need_graph_family_unknown");
|
||||||
|
}
|
||||||
|
if (aggregationNeed) {
|
||||||
|
pushReason(reasonCodes, `data_need_graph_aggregation_${aggregationNeed}`);
|
||||||
|
}
|
||||||
|
if (rankingNeed) {
|
||||||
|
pushReason(reasonCodes, `data_need_graph_ranking_${rankingNeed}`);
|
||||||
|
}
|
||||||
|
if (comparisonNeed) {
|
||||||
|
pushReason(reasonCodes, `data_need_graph_comparison_${comparisonNeed}`);
|
||||||
|
}
|
||||||
|
if (clarificationGaps.length > 0) {
|
||||||
|
pushReason(reasonCodes, "data_need_graph_has_clarification_gaps");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
schema_version: exports.ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION,
|
||||||
|
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||||
|
subject_candidates: subjectCandidates,
|
||||||
|
business_fact_family: businessFactFamily,
|
||||||
|
action_family: toNonEmptyString(turnMeaning?.asked_action_family),
|
||||||
|
aggregation_need: aggregationNeed,
|
||||||
|
time_scope_need: timeScopeNeed,
|
||||||
|
comparison_need: comparisonNeed,
|
||||||
|
ranking_need: rankingNeed,
|
||||||
|
proof_expectation: proofExpectationFor({
|
||||||
|
family: businessFactFamily,
|
||||||
|
clarificationGaps
|
||||||
|
}),
|
||||||
|
clarification_gaps: clarificationGaps,
|
||||||
|
decomposition_candidates: decompositionCandidates,
|
||||||
|
forbidden_overclaim_flags: forbiddenOverclaimFlagsFor(businessFactFamily),
|
||||||
|
reason_codes: reasonCodes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -133,6 +133,16 @@ function buildValueFlowFilters(planner) {
|
||||||
sort: "period_asc"
|
sort: "period_asc"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function organizationScopeForPlanner(planner) {
|
||||||
|
return toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_organization_scope);
|
||||||
|
}
|
||||||
|
function rankingNeedForPlanner(planner) {
|
||||||
|
const rankingNeed = toNonEmptyString(planner.data_need_graph?.ranking_need)?.toLowerCase();
|
||||||
|
if (rankingNeed === "top_desc" || rankingNeed === "bottom_asc") {
|
||||||
|
return rankingNeed;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
function normalizeEntityResolutionText(value) {
|
function normalizeEntityResolutionText(value) {
|
||||||
return String(value ?? "")
|
return String(value ?? "")
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
|
|
@ -313,7 +323,9 @@ function isMovementEvidencePilotEligible(planner) {
|
||||||
combined.includes("list_movements")));
|
combined.includes("list_movements")));
|
||||||
}
|
}
|
||||||
function isValueFlowPilotEligible(planner) {
|
function isValueFlowPilotEligible(planner) {
|
||||||
if (planner.selected_chain_id === "value_flow") {
|
if (planner.selected_chain_id === "value_flow" ||
|
||||||
|
planner.selected_chain_id === "value_flow_ranking" ||
|
||||||
|
planner.selected_chain_id === "value_flow_comparison") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const meaning = planner.discovery_plan.turn_meaning_ref;
|
const meaning = planner.discovery_plan.turn_meaning_ref;
|
||||||
|
|
@ -1040,6 +1052,16 @@ function rowAmountValue(row) {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
function rowCounterpartyValue(row) {
|
||||||
|
const candidates = [row["Контрагент"], row["Counterparty"], row["counterparty"], row["Наименование"], row["name"]];
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const text = toNonEmptyString(candidate);
|
||||||
|
if (text) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
function monthBucketFromIsoDate(isoDate) {
|
function monthBucketFromIsoDate(isoDate) {
|
||||||
const match = isoDate?.match(/^(\d{4})-(\d{2})-\d{2}$/);
|
const match = isoDate?.match(/^(\d{4})-(\d{2})-\d{2}$/);
|
||||||
return match ? `${match[1]}-${match[2]}` : null;
|
return match ? `${match[1]}-${match[2]}` : null;
|
||||||
|
|
@ -1213,6 +1235,62 @@ function deriveValueFlow(result, counterparty, periodScope, direction, aggregati
|
||||||
inference_basis: "sum_of_confirmed_1c_value_flow_rows"
|
inference_basis: "sum_of_confirmed_1c_value_flow_rows"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function deriveRankedValueFlow(result, input) {
|
||||||
|
if (!result || result.error || result.matched_rows <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const buckets = new Map();
|
||||||
|
let rowsWithAmount = 0;
|
||||||
|
for (const row of result.rows) {
|
||||||
|
const axisValue = rowCounterpartyValue(row);
|
||||||
|
const amount = rowAmountValue(row);
|
||||||
|
if (!axisValue || amount === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
rowsWithAmount += 1;
|
||||||
|
const current = buckets.get(axisValue) ?? { rows_with_amount: 0, total_amount: 0 };
|
||||||
|
current.rows_with_amount += 1;
|
||||||
|
current.total_amount += amount;
|
||||||
|
buckets.set(axisValue, current);
|
||||||
|
}
|
||||||
|
if (rowsWithAmount <= 0 || buckets.size <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const rankedValues = Array.from(buckets.entries())
|
||||||
|
.map(([axisValue, bucket]) => ({
|
||||||
|
axis_value: axisValue,
|
||||||
|
rows_with_amount: bucket.rows_with_amount,
|
||||||
|
total_amount: bucket.total_amount,
|
||||||
|
total_amount_human_ru: formatAmountHumanRu(bucket.total_amount)
|
||||||
|
}))
|
||||||
|
.sort((left, right) => {
|
||||||
|
const amountDelta = right.total_amount - left.total_amount;
|
||||||
|
if (input.rankingNeed === "bottom_asc") {
|
||||||
|
if (amountDelta !== 0) {
|
||||||
|
return -amountDelta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (amountDelta !== 0) {
|
||||||
|
return amountDelta;
|
||||||
|
}
|
||||||
|
return left.axis_value.localeCompare(right.axis_value, "ru");
|
||||||
|
})
|
||||||
|
.slice(0, 5);
|
||||||
|
return {
|
||||||
|
value_flow_direction: input.direction,
|
||||||
|
ranking_need: input.rankingNeed,
|
||||||
|
ranking_axis: "counterparty",
|
||||||
|
organization_scope: input.organizationScope,
|
||||||
|
period_scope: input.periodScope,
|
||||||
|
rows_matched: result.matched_rows,
|
||||||
|
rows_with_amount: rowsWithAmount,
|
||||||
|
ranked_values: rankedValues,
|
||||||
|
coverage_limited_by_probe_limit: result.coverage_limited_by_probe_limit,
|
||||||
|
coverage_recovered_by_period_chunking: result.coverage_recovered_by_period_chunking,
|
||||||
|
period_chunking_granularity: result.period_chunking_granularity,
|
||||||
|
inference_basis: "ranked_counterparty_totals_from_confirmed_1c_value_flow_rows"
|
||||||
|
};
|
||||||
|
}
|
||||||
function deriveValueFlowSideSummary(result) {
|
function deriveValueFlowSideSummary(result) {
|
||||||
if (!result || result.error || result.matched_rows <= 0) {
|
if (!result || result.error || result.matched_rows <= 0) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -1345,6 +1423,16 @@ function buildValueFlowConfirmedFacts(result, counterparty, direction) {
|
||||||
: "1C value-flow rows were found for the requested counterparty scope"
|
: "1C value-flow rows were found for the requested counterparty scope"
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
function buildRankedValueFlowConfirmedFacts(derived) {
|
||||||
|
if (!derived || derived.ranked_values.length <= 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const leader = derived.ranked_values[0];
|
||||||
|
const directionLabel = derived.value_flow_direction === "outgoing_supplier_payout" ? "supplier-payout" : "incoming value-flow";
|
||||||
|
return [
|
||||||
|
`1C ${directionLabel} rows were ranked by counterparty for the checked scope; leader=${leader.axis_value}, rows_with_amount=${leader.rows_with_amount}`
|
||||||
|
];
|
||||||
|
}
|
||||||
function buildBidirectionalValueFlowConfirmedFacts(derived) {
|
function buildBidirectionalValueFlowConfirmedFacts(derived) {
|
||||||
if (!derived) {
|
if (!derived) {
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -1411,6 +1499,16 @@ function buildValueFlowInferredFacts(derived) {
|
||||||
}
|
}
|
||||||
return facts;
|
return facts;
|
||||||
}
|
}
|
||||||
|
function buildRankedValueFlowInferredFacts(derived) {
|
||||||
|
if (!derived) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const facts = ["Counterparty ranking was calculated from confirmed 1C movement rows grouped by counterparty"];
|
||||||
|
if (derived.coverage_recovered_by_period_chunking && derived.period_chunking_granularity === "month") {
|
||||||
|
facts.push("Requested period coverage for counterparty ranking was recovered through monthly 1C probes after a broad probe hit the row limit");
|
||||||
|
}
|
||||||
|
return facts;
|
||||||
|
}
|
||||||
function buildBidirectionalValueFlowInferredFacts(derived) {
|
function buildBidirectionalValueFlowInferredFacts(derived) {
|
||||||
if (!derived) {
|
if (!derived) {
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -1453,6 +1551,16 @@ function buildValueFlowUnknownFacts(periodScope, direction, derived) {
|
||||||
: "Full all-time turnover is not proven without an explicit checked period");
|
: "Full all-time turnover is not proven without an explicit checked period");
|
||||||
return unknownFacts;
|
return unknownFacts;
|
||||||
}
|
}
|
||||||
|
function buildRankedValueFlowUnknownFacts(periodScope, derived) {
|
||||||
|
const unknownFacts = [];
|
||||||
|
if (derived?.coverage_limited_by_probe_limit) {
|
||||||
|
unknownFacts.push("Complete requested-period ranking coverage is not proven because the MCP discovery probe row limit was reached");
|
||||||
|
}
|
||||||
|
unknownFacts.push(periodScope
|
||||||
|
? "Full ranking outside the checked period is not proven by this MCP discovery pilot"
|
||||||
|
: "Full all-time counterparty ranking is not proven without an explicit checked period");
|
||||||
|
return unknownFacts;
|
||||||
|
}
|
||||||
function buildBidirectionalValueFlowUnknownFacts(periodScope, derived) {
|
function buildBidirectionalValueFlowUnknownFacts(periodScope, derived) {
|
||||||
const unknownFacts = [];
|
const unknownFacts = [];
|
||||||
if (derived?.coverage_limited_by_probe_limit) {
|
if (derived?.coverage_limited_by_probe_limit) {
|
||||||
|
|
@ -1479,6 +1587,8 @@ function pilotScopeForPlanner(planner) {
|
||||||
return "metadata_inspection_v1";
|
return "metadata_inspection_v1";
|
||||||
case "movement_evidence":
|
case "movement_evidence":
|
||||||
return "counterparty_movement_evidence_query_movements_v1";
|
return "counterparty_movement_evidence_query_movements_v1";
|
||||||
|
case "value_flow_comparison":
|
||||||
|
case "value_flow_ranking":
|
||||||
case "value_flow":
|
case "value_flow":
|
||||||
return valueFlowPilotProfile(planner).scope;
|
return valueFlowPilotProfile(planner).scope;
|
||||||
case "document_evidence":
|
case "document_evidence":
|
||||||
|
|
@ -1595,7 +1705,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||||
}
|
}
|
||||||
const counterparty = firstEntityCandidate(planner);
|
const counterparty = firstEntityCandidate(planner);
|
||||||
const dateScope = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_date_scope);
|
const dateScope = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_date_scope);
|
||||||
|
const organizationScope = organizationScopeForPlanner(planner);
|
||||||
const aggregationAxis = aggregationAxisForPlanner(planner);
|
const aggregationAxis = aggregationAxisForPlanner(planner);
|
||||||
|
const rankingNeed = rankingNeedForPlanner(planner);
|
||||||
if (metadataPilotEligible) {
|
if (metadataPilotEligible) {
|
||||||
let metadataResult = null;
|
let metadataResult = null;
|
||||||
const metadataScope = metadataScopeForPlanner(planner);
|
const metadataScope = metadataScopeForPlanner(planner);
|
||||||
|
|
@ -2151,6 +2263,48 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const sourceRowsSummary = queryResult ? summarizeValueFlowRows(queryResult) : null;
|
const sourceRowsSummary = queryResult ? summarizeValueFlowRows(queryResult) : null;
|
||||||
|
if (planner.selected_chain_id === "value_flow_ranking" && rankingNeed) {
|
||||||
|
const derivedRankedValueFlow = deriveRankedValueFlow(queryResult, {
|
||||||
|
organizationScope,
|
||||||
|
periodScope: dateScope,
|
||||||
|
direction: valueFlowProfile.direction,
|
||||||
|
rankingNeed
|
||||||
|
});
|
||||||
|
if (derivedRankedValueFlow) {
|
||||||
|
pushReason(reasonCodes, "pilot_derived_ranked_value_flow_from_confirmed_rows");
|
||||||
|
}
|
||||||
|
const evidence = (0, assistantMcpDiscoveryPolicy_1.resolveAssistantMcpDiscoveryEvidence)({
|
||||||
|
plan: planner.discovery_plan,
|
||||||
|
probeResults,
|
||||||
|
confirmedFacts: buildRankedValueFlowConfirmedFacts(derivedRankedValueFlow),
|
||||||
|
inferredFacts: buildRankedValueFlowInferredFacts(derivedRankedValueFlow),
|
||||||
|
unknownFacts: buildRankedValueFlowUnknownFacts(dateScope, derivedRankedValueFlow),
|
||||||
|
sourceRowsSummary,
|
||||||
|
queryLimitations,
|
||||||
|
recommendedNextProbe: "explain_evidence_basis"
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
|
||||||
|
policy_owner: "assistantMcpDiscoveryPilotExecutor",
|
||||||
|
pilot_status: "executed",
|
||||||
|
pilot_scope: valueFlowProfile.scope,
|
||||||
|
dry_run: dryRun,
|
||||||
|
mcp_execution_performed: executedPrimitives.length > 0,
|
||||||
|
executed_primitives: executedPrimitives,
|
||||||
|
skipped_primitives: skippedPrimitives,
|
||||||
|
probe_results: probeResults,
|
||||||
|
evidence,
|
||||||
|
source_rows_summary: sourceRowsSummary,
|
||||||
|
derived_metadata_surface: null,
|
||||||
|
derived_entity_resolution: null,
|
||||||
|
derived_activity_period: null,
|
||||||
|
derived_ranked_value_flow: derivedRankedValueFlow,
|
||||||
|
derived_value_flow: null,
|
||||||
|
derived_bidirectional_value_flow: null,
|
||||||
|
query_limitations: queryLimitations,
|
||||||
|
reason_codes: reasonCodes
|
||||||
|
};
|
||||||
|
}
|
||||||
const derivedValueFlow = deriveValueFlow(queryResult, counterparty, dateScope, valueFlowProfile.direction, aggregationAxis);
|
const derivedValueFlow = deriveValueFlow(queryResult, counterparty, dateScope, valueFlowProfile.direction, aggregationAxis);
|
||||||
if (derivedValueFlow) {
|
if (derivedValueFlow) {
|
||||||
pushReason(reasonCodes, "pilot_derived_value_flow_from_confirmed_rows");
|
pushReason(reasonCodes, "pilot_derived_value_flow_from_confirmed_rows");
|
||||||
|
|
@ -2183,6 +2337,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||||
derived_metadata_surface: null,
|
derived_metadata_surface: null,
|
||||||
derived_entity_resolution: null,
|
derived_entity_resolution: null,
|
||||||
derived_activity_period: null,
|
derived_activity_period: null,
|
||||||
|
derived_ranked_value_flow: null,
|
||||||
derived_value_flow: derivedValueFlow,
|
derived_value_flow: derivedValueFlow,
|
||||||
derived_bidirectional_value_flow: null,
|
derived_bidirectional_value_flow: null,
|
||||||
query_limitations: queryLimitations,
|
query_limitations: queryLimitations,
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,9 @@ function pushUnique(target, value) {
|
||||||
function hasEntity(meaning) {
|
function hasEntity(meaning) {
|
||||||
return (meaning?.explicit_entity_candidates?.length ?? 0) > 0;
|
return (meaning?.explicit_entity_candidates?.length ?? 0) > 0;
|
||||||
}
|
}
|
||||||
|
function hasSubjectCandidates(graph) {
|
||||||
|
return (graph?.subject_candidates.length ?? 0) > 0;
|
||||||
|
}
|
||||||
function aggregationAxis(meaning) {
|
function aggregationAxis(meaning) {
|
||||||
return toNonEmptyString(meaning?.asked_aggregation_axis)?.toLowerCase() ?? null;
|
return toNonEmptyString(meaning?.asked_aggregation_axis)?.toLowerCase() ?? null;
|
||||||
}
|
}
|
||||||
|
|
@ -75,13 +78,137 @@ function budgetOverrideFor(input, recipe) {
|
||||||
}
|
}
|
||||||
function recipeFor(input) {
|
function recipeFor(input) {
|
||||||
const meaning = input.turnMeaning ?? null;
|
const meaning = input.turnMeaning ?? null;
|
||||||
|
const dataNeedGraph = input.dataNeedGraph ?? null;
|
||||||
const domain = lower(meaning?.asked_domain_family);
|
const domain = lower(meaning?.asked_domain_family);
|
||||||
const action = lower(meaning?.asked_action_family);
|
const action = lower(meaning?.asked_action_family);
|
||||||
const unsupported = lower(meaning?.unsupported_but_understood_family);
|
const unsupported = lower(meaning?.unsupported_but_understood_family);
|
||||||
|
const graphFactFamily = lower(dataNeedGraph?.business_fact_family);
|
||||||
|
const graphAction = lower(dataNeedGraph?.action_family);
|
||||||
|
const graphAggregation = lower(dataNeedGraph?.aggregation_need);
|
||||||
|
const graphClarificationGaps = (dataNeedGraph?.clarification_gaps ?? []).map((item) => lower(item));
|
||||||
const combined = `${domain} ${action} ${unsupported}`.trim();
|
const combined = `${domain} ${action} ${unsupported}`.trim();
|
||||||
const axes = [];
|
const axes = [];
|
||||||
const requestedAggregationAxis = aggregationAxis(meaning);
|
const requestedAggregationAxis = aggregationAxis(meaning);
|
||||||
addScopeAxes(axes, meaning);
|
addScopeAxes(axes, meaning);
|
||||||
|
if (graphClarificationGaps.includes("lane_family_choice")) {
|
||||||
|
pushUnique(axes, "lane_family_choice");
|
||||||
|
return {
|
||||||
|
semanticDataNeed: "metadata lane clarification",
|
||||||
|
chainId: "metadata_lane_clarification",
|
||||||
|
chainSummary: "Preserve the ambiguous metadata surface and ask the user to choose the next data lane before running MCP probes.",
|
||||||
|
primitives: [],
|
||||||
|
axes,
|
||||||
|
reason: "planner_selected_metadata_lane_clarification_from_data_need_graph"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (graphFactFamily === "value_flow") {
|
||||||
|
if (dataNeedGraph?.comparison_need === "incoming_vs_outgoing" && !hasSubjectCandidates(dataNeedGraph)) {
|
||||||
|
pushUnique(axes, "amount");
|
||||||
|
pushUnique(axes, "coverage_target");
|
||||||
|
if (requestedAggregationAxis === "month" || graphAggregation === "by_month") {
|
||||||
|
pushUnique(axes, "calendar_month");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
semanticDataNeed: "bidirectional value-flow comparison evidence",
|
||||||
|
chainId: "value_flow_comparison",
|
||||||
|
chainSummary: "Query incoming and outgoing movements for the checked period and organization, compare the checked sides, and probe coverage before answering a bounded comparison.",
|
||||||
|
primitives: ["query_movements", "probe_coverage"],
|
||||||
|
axes,
|
||||||
|
reason: "planner_selected_bidirectional_value_flow_comparison_from_data_need_graph"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (dataNeedGraph?.ranking_need && !hasSubjectCandidates(dataNeedGraph)) {
|
||||||
|
pushUnique(axes, "aggregate_axis");
|
||||||
|
pushUnique(axes, "amount");
|
||||||
|
pushUnique(axes, "coverage_target");
|
||||||
|
return {
|
||||||
|
semanticDataNeed: "ranked value-flow evidence",
|
||||||
|
chainId: "value_flow_ranking",
|
||||||
|
chainSummary: "Query scoped movements for the checked period and organization, aggregate checked amounts by counterparty, then probe coverage before answering a bounded ranking.",
|
||||||
|
primitives: ["query_movements", "aggregate_by_axis", "probe_coverage"],
|
||||||
|
axes,
|
||||||
|
reason: dataNeedGraph.ranking_need === "bottom_asc"
|
||||||
|
? "planner_selected_bottom_ranked_value_flow_from_data_need_graph"
|
||||||
|
: "planner_selected_top_ranked_value_flow_from_data_need_graph"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
pushUnique(axes, "aggregate_axis");
|
||||||
|
pushUnique(axes, "amount");
|
||||||
|
pushUnique(axes, "coverage_target");
|
||||||
|
if (requestedAggregationAxis === "month" || graphAggregation === "by_month") {
|
||||||
|
pushUnique(axes, "calendar_month");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
semanticDataNeed: "counterparty value-flow evidence",
|
||||||
|
chainId: "value_flow",
|
||||||
|
chainSummary: "Resolve the business entity, query scoped movements, aggregate checked amounts, then probe coverage before answering.",
|
||||||
|
primitives: ["resolve_entity_reference", "query_movements", "aggregate_by_axis", "probe_coverage"],
|
||||||
|
axes,
|
||||||
|
reason: requestedAggregationAxis === "month" || graphAggregation === "by_month"
|
||||||
|
? "planner_selected_monthly_value_flow_from_data_need_graph"
|
||||||
|
: "planner_selected_value_flow_from_data_need_graph"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (graphFactFamily === "activity_lifecycle") {
|
||||||
|
pushUnique(axes, "document_date");
|
||||||
|
pushUnique(axes, "coverage_target");
|
||||||
|
pushUnique(axes, "evidence_basis");
|
||||||
|
return {
|
||||||
|
semanticDataNeed: "counterparty lifecycle evidence",
|
||||||
|
chainId: "lifecycle",
|
||||||
|
chainSummary: "Resolve the business entity, query supporting documents, probe coverage, then explain the evidence basis for the inferred activity window.",
|
||||||
|
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage", "explain_evidence_basis"],
|
||||||
|
axes,
|
||||||
|
reason: "planner_selected_lifecycle_from_data_need_graph"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (graphFactFamily === "schema_surface") {
|
||||||
|
pushUnique(axes, "metadata_scope");
|
||||||
|
return {
|
||||||
|
semanticDataNeed: "1C metadata evidence",
|
||||||
|
chainId: "metadata_inspection",
|
||||||
|
chainSummary: "Inspect the 1C metadata surface first, then ground the next safe lane from confirmed schema evidence.",
|
||||||
|
primitives: ["inspect_1c_metadata"],
|
||||||
|
axes,
|
||||||
|
reason: "planner_selected_metadata_from_data_need_graph"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (graphFactFamily === "movement_evidence") {
|
||||||
|
pushUnique(axes, "coverage_target");
|
||||||
|
return {
|
||||||
|
semanticDataNeed: "movement evidence",
|
||||||
|
chainId: "movement_evidence",
|
||||||
|
chainSummary: "Resolve the business entity, fetch scoped movement rows, and probe coverage without pretending to have a full movement universe.",
|
||||||
|
primitives: ["resolve_entity_reference", "query_movements", "probe_coverage"],
|
||||||
|
axes,
|
||||||
|
reason: "planner_selected_movement_from_data_need_graph"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (graphFactFamily === "document_evidence") {
|
||||||
|
pushUnique(axes, "coverage_target");
|
||||||
|
return {
|
||||||
|
semanticDataNeed: "document evidence",
|
||||||
|
chainId: "document_evidence",
|
||||||
|
chainSummary: "Resolve the business entity, fetch scoped document rows, and probe coverage before stating the checked document evidence.",
|
||||||
|
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage"],
|
||||||
|
axes,
|
||||||
|
reason: "planner_selected_document_from_data_need_graph"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (graphFactFamily === "entity_grounding" || (!graphFactFamily && (dataNeedGraph?.subject_candidates.length ?? 0) > 0)) {
|
||||||
|
pushUnique(axes, "business_entity");
|
||||||
|
pushUnique(axes, "coverage_target");
|
||||||
|
return {
|
||||||
|
semanticDataNeed: "entity discovery evidence",
|
||||||
|
chainId: "entity_resolution",
|
||||||
|
chainSummary: "Search candidate business entities, resolve the most relevant 1C reference, and prove whether the entity grounding is stable enough for the next probe.",
|
||||||
|
primitives: ["search_business_entity", "resolve_entity_reference", "probe_coverage"],
|
||||||
|
axes,
|
||||||
|
reason: graphAction === "search_business_entity"
|
||||||
|
? "planner_selected_entity_resolution_from_data_need_graph"
|
||||||
|
: "planner_selected_entity_resolution_recipe"
|
||||||
|
};
|
||||||
|
}
|
||||||
if (includesAny(combined, ["metadata_lane_choice_clarification", "resolve_next_lane"])) {
|
if (includesAny(combined, ["metadata_lane_choice_clarification", "resolve_next_lane"])) {
|
||||||
pushUnique(axes, "lane_family_choice");
|
pushUnique(axes, "lane_family_choice");
|
||||||
return {
|
return {
|
||||||
|
|
@ -191,8 +318,12 @@ function planAssistantMcpDiscovery(input) {
|
||||||
const recipe = recipeFor(input);
|
const recipe = recipeFor(input);
|
||||||
const budgetOverride = budgetOverrideFor(input, recipe);
|
const budgetOverride = budgetOverrideFor(input, recipe);
|
||||||
const semanticDataNeed = toNonEmptyString(input.semanticDataNeed) ?? recipe.semanticDataNeed;
|
const semanticDataNeed = toNonEmptyString(input.semanticDataNeed) ?? recipe.semanticDataNeed;
|
||||||
|
const dataNeedGraph = input.dataNeedGraph ?? null;
|
||||||
const reasonCodes = [];
|
const reasonCodes = [];
|
||||||
pushReason(reasonCodes, recipe.reason);
|
pushReason(reasonCodes, recipe.reason);
|
||||||
|
if (dataNeedGraph) {
|
||||||
|
pushReason(reasonCodes, "planner_consumed_data_need_graph_v1");
|
||||||
|
}
|
||||||
if (budgetOverride.maxProbeCount) {
|
if (budgetOverride.maxProbeCount) {
|
||||||
pushReason(reasonCodes, "planner_enabled_chunked_coverage_probe_budget");
|
pushReason(reasonCodes, "planner_enabled_chunked_coverage_probe_budget");
|
||||||
}
|
}
|
||||||
|
|
@ -219,6 +350,7 @@ function planAssistantMcpDiscovery(input) {
|
||||||
policy_owner: "assistantMcpDiscoveryPlanner",
|
policy_owner: "assistantMcpDiscoveryPlanner",
|
||||||
planner_status: plannerStatus,
|
planner_status: plannerStatus,
|
||||||
semantic_data_need: semanticDataNeed,
|
semantic_data_need: semanticDataNeed,
|
||||||
|
data_need_graph: dataNeedGraph,
|
||||||
selected_chain_id: recipe.chainId,
|
selected_chain_id: recipe.chainId,
|
||||||
selected_chain_summary: recipe.chainSummary,
|
selected_chain_summary: recipe.chainSummary,
|
||||||
proposed_primitives: recipe.primitives,
|
proposed_primitives: recipe.primitives,
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,28 @@ function userFacingLines(values) {
|
||||||
return uniqueStrings(values).filter((line) => !hasInternalMechanics(line));
|
return uniqueStrings(values).filter((line) => !hasInternalMechanics(line));
|
||||||
}
|
}
|
||||||
function localizeLine(value) {
|
function localizeLine(value) {
|
||||||
|
if (/^1C activity rows were found for the requested counterparty scope$/i.test(value)) {
|
||||||
|
return "В 1С найдены строки активности в запрошенном срезе.";
|
||||||
|
}
|
||||||
|
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
|
||||||
|
return "В 1С найдены строки входящих денежных поступлений в запрошенном срезе.";
|
||||||
|
}
|
||||||
|
if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
|
||||||
|
return "В 1С найдены строки исходящих платежей и списаний в запрошенном срезе.";
|
||||||
|
}
|
||||||
|
const openScopeBidirectionalMatch = value.match(/^1C bidirectional value-flow rows were checked for the requested counterparty scope: incoming=(found|not_found), outgoing=(found|not_found)$/i);
|
||||||
|
if (openScopeBidirectionalMatch) {
|
||||||
|
const incoming = openScopeBidirectionalMatch[1] === "found"
|
||||||
|
? "входящие строки найдены"
|
||||||
|
: "входящие строки не найдены";
|
||||||
|
const outgoing = openScopeBidirectionalMatch[2] === "found"
|
||||||
|
? "исходящие строки найдены"
|
||||||
|
: "исходящие строки не найдены";
|
||||||
|
return `В 1С проверены входящие и исходящие денежные строки в запрошенном срезе: ${incoming}, ${outgoing}.`;
|
||||||
|
}
|
||||||
|
if (/^Requested period hit the MCP row limit, but the approved monthly recovery probe budget is smaller than the required subperiod count$/i.test(value)) {
|
||||||
|
return "Запрошенный период уперся в лимит строк MCP; доступного бюджета помесячных дозапросов не хватило, чтобы покрыть все подпериоды.";
|
||||||
|
}
|
||||||
const counterpartyMatch = value.match(/^1C activity rows were found for counterparty\s+(.+)$/i);
|
const counterpartyMatch = value.match(/^1C activity rows were found for counterparty\s+(.+)$/i);
|
||||||
if (counterpartyMatch) {
|
if (counterpartyMatch) {
|
||||||
return `В 1С найдены строки активности по контрагенту ${counterpartyMatch[1]}.`;
|
return `В 1С найдены строки активности по контрагенту ${counterpartyMatch[1]}.`;
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ function businessFactAnswerAllowed(draft) {
|
||||||
async function runAssistantMcpDiscoveryRuntimeBridge(input) {
|
async function runAssistantMcpDiscoveryRuntimeBridge(input) {
|
||||||
const planner = (0, assistantMcpDiscoveryPlanner_1.planAssistantMcpDiscovery)({
|
const planner = (0, assistantMcpDiscoveryPlanner_1.planAssistantMcpDiscovery)({
|
||||||
semanticDataNeed: input.semanticDataNeed,
|
semanticDataNeed: input.semanticDataNeed,
|
||||||
|
dataNeedGraph: input.dataNeedGraph,
|
||||||
turnMeaning: input.turnMeaning
|
turnMeaning: input.turnMeaning
|
||||||
});
|
});
|
||||||
const pilot = await (0, assistantMcpDiscoveryPilotExecutor_1.executeAssistantMcpDiscoveryPilot)(planner, input.deps);
|
const pilot = await (0, assistantMcpDiscoveryPilotExecutor_1.executeAssistantMcpDiscoveryPilot)(planner, input.deps);
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ async function runAssistantMcpDiscoveryRuntimeEntryPoint(input) {
|
||||||
}
|
}
|
||||||
const bridge = await (0, assistantMcpDiscoveryRuntimeBridge_1.runAssistantMcpDiscoveryRuntimeBridge)({
|
const bridge = await (0, assistantMcpDiscoveryRuntimeBridge_1.runAssistantMcpDiscoveryRuntimeBridge)({
|
||||||
semanticDataNeed: turnInput.semantic_data_need,
|
semanticDataNeed: turnInput.semantic_data_need,
|
||||||
|
dataNeedGraph: turnInput.data_need_graph,
|
||||||
turnMeaning: turnInput.turn_meaning_ref,
|
turnMeaning: turnInput.turn_meaning_ref,
|
||||||
deps: input.deps
|
deps: input.deps
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION = void 0;
|
exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION = void 0;
|
||||||
exports.buildAssistantMcpDiscoveryTurnInput = buildAssistantMcpDiscoveryTurnInput;
|
exports.buildAssistantMcpDiscoveryTurnInput = buildAssistantMcpDiscoveryTurnInput;
|
||||||
|
const assistantMcpDiscoveryDataNeedGraph_1 = require("./assistantMcpDiscoveryDataNeedGraph");
|
||||||
exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION = "assistant_mcp_discovery_turn_input_v1";
|
exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION = "assistant_mcp_discovery_turn_input_v1";
|
||||||
function toRecordObject(value) {
|
function toRecordObject(value) {
|
||||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||||
|
|
@ -71,6 +72,9 @@ function compactLower(value) {
|
||||||
.replace(/\s+/g, " ")
|
.replace(/\s+/g, " ")
|
||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
function sameScopedName(left, right) {
|
||||||
|
return Boolean(left && right && compactLower(left) === compactLower(right));
|
||||||
|
}
|
||||||
function candidateValue(value) {
|
function candidateValue(value) {
|
||||||
const direct = toNonEmptyString(value);
|
const direct = toNonEmptyString(value);
|
||||||
if (direct && direct !== "[object Object]") {
|
if (direct && direct !== "[object Object]") {
|
||||||
|
|
@ -298,6 +302,9 @@ function hasPayoutSignal(text) {
|
||||||
function hasBidirectionalValueFlowSignal(text) {
|
function hasBidirectionalValueFlowSignal(text) {
|
||||||
return /(?:нетто|сальдо|баланс\s+(?:плат|денег|денеж)|взаиморасч[её]т|получил[иа]?.*(?:за)?платил|(?:за)?платил[иа]?.*получил|входящ.*исходящ|исходящ.*входящ|дебет.*кредит|кредит.*дебет|net\s+(?:flow|cash|payment)|cash\s+net|incoming\s+and\s+outgoing|received\s+and\s+paid|paid\s+and\s+received)/iu.test(text);
|
return /(?:нетто|сальдо|баланс\s+(?:плат|денег|денеж)|взаиморасч[её]т|получил[иа]?.*(?:за)?платил|(?:за)?платил[иа]?.*получил|входящ.*исходящ|исходящ.*входящ|дебет.*кредит|кредит.*дебет|net\s+(?:flow|cash|payment)|cash\s+net|incoming\s+and\s+outgoing|received\s+and\s+paid|paid\s+and\s+received)/iu.test(text);
|
||||||
}
|
}
|
||||||
|
function hasValueRankingSignal(text) {
|
||||||
|
return /(?:кто\s+больше\s+всего.*ден[её]г|больше\s+всего.*ден[её]г|прин[её]с.*ден[её]г|сам(?:ый|ая|ое|ые).*(?:доходн|прибыльн)|most.*money|highest\s+(?:revenue|payment))/iu.test(text);
|
||||||
|
}
|
||||||
function hasMonthlyAggregationSignal(text) {
|
function hasMonthlyAggregationSignal(text) {
|
||||||
return /(?:\u043f\u043e\s+\u043c\u0435\u0441\u044f\u0446\u0430\u043c|\u043f\u043e\u043c\u0435\u0441\u044f\u0447\u043d\u043e|\u0435\u0436\u0435\u043c\u0435\u0441\u044f\u0447\u043d\u043e|month\s+by\s+month|by\s+month|monthly)/iu.test(text);
|
return /(?:\u043f\u043e\s+\u043c\u0435\u0441\u044f\u0446\u0430\u043c|\u043f\u043e\u043c\u0435\u0441\u044f\u0447\u043d\u043e|\u0435\u0436\u0435\u043c\u0435\u0441\u044f\u0447\u043d\u043e|month\s+by\s+month|by\s+month|monthly)/iu.test(text);
|
||||||
}
|
}
|
||||||
|
|
@ -551,7 +558,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
const rawText = compactLower(rawSignalSourceText);
|
const rawText = compactLower(rawSignalSourceText);
|
||||||
const rawLifecycleSignal = hasLifecycleSignal(rawText);
|
const rawLifecycleSignal = hasLifecycleSignal(rawText);
|
||||||
const rawBidirectionalValueFlowSignal = !rawLifecycleSignal && hasBidirectionalValueFlowSignal(rawText);
|
const rawBidirectionalValueFlowSignal = !rawLifecycleSignal && hasBidirectionalValueFlowSignal(rawText);
|
||||||
const rawValueFlowSignal = !rawLifecycleSignal && (hasValueFlowSignal(rawText) || rawBidirectionalValueFlowSignal);
|
const rawValueFlowSignal = !rawLifecycleSignal &&
|
||||||
|
(hasValueFlowSignal(rawText) || hasValueRankingSignal(rawText) || rawBidirectionalValueFlowSignal);
|
||||||
const rawMetadataSignal = !rawLifecycleSignal && !rawValueFlowSignal && hasMetadataSignal(rawText);
|
const rawMetadataSignal = !rawLifecycleSignal && !rawValueFlowSignal && hasMetadataSignal(rawText);
|
||||||
const rawEntityResolutionSignal = !rawLifecycleSignal && !rawValueFlowSignal && !rawMetadataSignal && hasEntityResolutionSignal(rawText);
|
const rawEntityResolutionSignal = !rawLifecycleSignal && !rawValueFlowSignal && !rawMetadataSignal && hasEntityResolutionSignal(rawText);
|
||||||
const rawPayoutSignal = rawValueFlowSignal && !rawBidirectionalValueFlowSignal && hasPayoutSignal(rawText);
|
const rawPayoutSignal = rawValueFlowSignal && !rawBidirectionalValueFlowSignal && hasPayoutSignal(rawText);
|
||||||
|
|
@ -574,6 +582,13 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate);
|
const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate);
|
||||||
const assistantTurnMeaningDateScope = toNonEmptyString(assistantTurnMeaning?.explicit_date_scope);
|
const assistantTurnMeaningDateScope = toNonEmptyString(assistantTurnMeaning?.explicit_date_scope);
|
||||||
const assistantTurnMeaningOrganizationScope = toNonEmptyString(assistantTurnMeaning?.explicit_organization_scope);
|
const assistantTurnMeaningOrganizationScope = toNonEmptyString(assistantTurnMeaning?.explicit_organization_scope);
|
||||||
|
const predecomposeOrganizationMirrorsCounterparty = sameScopedName(predecomposeEntities.counterparty, predecomposeEntities.organization);
|
||||||
|
const organizationMirrorsPredecomposeCounterparty = Boolean((rawBidirectionalValueFlowSignal || hasValueRankingSignal(rawText)) &&
|
||||||
|
(sameScopedName(predecomposeEntities.counterparty, assistantTurnMeaningOrganizationScope) ||
|
||||||
|
predecomposeOrganizationMirrorsCounterparty));
|
||||||
|
const normalizedPredecomposeCounterparty = organizationMirrorsPredecomposeCounterparty
|
||||||
|
? null
|
||||||
|
: predecomposeEntities.counterparty;
|
||||||
const predecomposeDateScope = collectDateScope(predecomposeContract);
|
const predecomposeDateScope = collectDateScope(predecomposeContract);
|
||||||
const followupDiscoverySeedApplicable = Boolean(followupSeed.domain &&
|
const followupDiscoverySeedApplicable = Boolean(followupSeed.domain &&
|
||||||
!rawLifecycleSignal &&
|
!rawLifecycleSignal &&
|
||||||
|
|
@ -791,7 +806,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
for (const candidate of collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates)) {
|
for (const candidate of collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates)) {
|
||||||
pushNormalizedEntityResolutionCandidate(entityCandidates, candidate);
|
pushNormalizedEntityResolutionCandidate(entityCandidates, candidate);
|
||||||
}
|
}
|
||||||
pushNormalizedEntityResolutionCandidate(entityCandidates, predecomposeEntities.counterparty);
|
pushNormalizedEntityResolutionCandidate(entityCandidates, normalizedPredecomposeCounterparty);
|
||||||
pushNormalizedEntityResolutionCandidate(entityCandidates, followupSeed.counterparty);
|
pushNormalizedEntityResolutionCandidate(entityCandidates, followupSeed.counterparty);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
@ -801,7 +816,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
for (const candidate of collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates)) {
|
for (const candidate of collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates)) {
|
||||||
pushScopedEntityCandidate(entityCandidates, candidate, groundedFollowupEntity);
|
pushScopedEntityCandidate(entityCandidates, candidate, groundedFollowupEntity);
|
||||||
}
|
}
|
||||||
pushScopedEntityCandidate(entityCandidates, predecomposeEntities.counterparty, groundedFollowupEntity);
|
pushScopedEntityCandidate(entityCandidates, normalizedPredecomposeCounterparty, groundedFollowupEntity);
|
||||||
if (!groundedFollowupEntity) {
|
if (!groundedFollowupEntity) {
|
||||||
pushScopedEntityCandidate(entityCandidates, followupSeed.counterparty, null);
|
pushScopedEntityCandidate(entityCandidates, followupSeed.counterparty, null);
|
||||||
pushScopedEntityCandidate(entityCandidates, followupSeed.discoveryEntity, null);
|
pushScopedEntityCandidate(entityCandidates, followupSeed.discoveryEntity, null);
|
||||||
|
|
@ -812,13 +827,23 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
pushUnique(entityCandidates, followupSeed.discoveryEntity);
|
pushUnique(entityCandidates, followupSeed.discoveryEntity);
|
||||||
pushUnique(entityCandidates, rawMetadataScopeHint);
|
pushUnique(entityCandidates, rawMetadataScopeHint);
|
||||||
}
|
}
|
||||||
if (valueFlowSignal && !predecomposeEntities.counterparty && !followupSeed.counterparty) {
|
const openScopeValueFlowWithoutCounterparty = valueFlowSignal && !normalizedPredecomposeCounterparty && !followupSeed.counterparty;
|
||||||
|
const valueFlowOrganizationStaysScope = openScopeValueFlowWithoutCounterparty &&
|
||||||
|
(bidirectionalValueFlowSignal || hasValueRankingSignal(rawText));
|
||||||
|
if (openScopeValueFlowWithoutCounterparty && !valueFlowOrganizationStaysScope) {
|
||||||
pushUnique(entityCandidates, predecomposeEntities.organization);
|
pushUnique(entityCandidates, predecomposeEntities.organization);
|
||||||
pushUnique(entityCandidates, followupSeed.organization);
|
pushUnique(entityCandidates, followupSeed.organization);
|
||||||
}
|
}
|
||||||
const explicitOrganizationScope = valueFlowSignal && !predecomposeEntities.counterparty && !followupSeed.counterparty
|
const explicitOrganizationScope = valueFlowOrganizationStaysScope || !openScopeValueFlowWithoutCounterparty
|
||||||
? null
|
? predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope ?? followupSeed.organization
|
||||||
: predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope ?? followupSeed.organization;
|
: null;
|
||||||
|
if (valueFlowOrganizationStaysScope && explicitOrganizationScope) {
|
||||||
|
for (let index = entityCandidates.length - 1; index >= 0; index -= 1) {
|
||||||
|
if (entityCandidates[index] === explicitOrganizationScope) {
|
||||||
|
entityCandidates.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const explicitDateScope = assistantTurnMeaningDateScope ?? predecomposeDateScope ?? rawDateScope ?? followupSeed.dateScope;
|
const explicitDateScope = assistantTurnMeaningDateScope ?? predecomposeDateScope ?? rawDateScope ?? followupSeed.dateScope;
|
||||||
const turnMeaning = {
|
const turnMeaning = {
|
||||||
asked_domain_family: lifecycleSignal
|
asked_domain_family: lifecycleSignal
|
||||||
|
|
@ -1054,7 +1079,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
if (unsupported) {
|
if (unsupported) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
|
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
|
||||||
}
|
}
|
||||||
if (predecomposeEntities.counterparty) {
|
if (!(valueFlowOrganizationStaysScope && normalizedPredecomposeCounterparty === explicitOrganizationScope) &&
|
||||||
|
normalizedPredecomposeCounterparty) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose");
|
pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose");
|
||||||
}
|
}
|
||||||
if (followupSeed.counterparty) {
|
if (followupSeed.counterparty) {
|
||||||
|
|
@ -1072,12 +1098,23 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
if (runDiscovery && !hasTurnMeaning) {
|
if (runDiscovery && !hasTurnMeaning) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_turn_meaning_missing");
|
pushReason(reasonCodes, "mcp_discovery_turn_meaning_missing");
|
||||||
}
|
}
|
||||||
|
const dataNeedGraph = runDiscovery && hasTurnMeaning
|
||||||
|
? (0, assistantMcpDiscoveryDataNeedGraph_1.buildAssistantMcpDiscoveryDataNeedGraph)({
|
||||||
|
semanticDataNeed,
|
||||||
|
rawUtterance: rawSignalSourceText,
|
||||||
|
turnMeaning: cleanTurnMeaning
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
if (dataNeedGraph) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_data_need_graph_built");
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION,
|
schema_version: exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION,
|
||||||
policy_owner: "assistantMcpDiscoveryTurnInputAdapter",
|
policy_owner: "assistantMcpDiscoveryTurnInputAdapter",
|
||||||
adapter_status: !runDiscovery ? "not_applicable" : hasTurnMeaning ? "ready" : "needs_more_context",
|
adapter_status: !runDiscovery ? "not_applicable" : hasTurnMeaning ? "ready" : "needs_more_context",
|
||||||
should_run_discovery: runDiscovery,
|
should_run_discovery: runDiscovery,
|
||||||
semantic_data_need: runDiscovery ? semanticDataNeed : null,
|
semantic_data_need: runDiscovery ? semanticDataNeed : null,
|
||||||
|
data_need_graph: dataNeedGraph,
|
||||||
turn_meaning_ref: runDiscovery && hasTurnMeaning ? cleanTurnMeaning : null,
|
turn_meaning_ref: runDiscovery && hasTurnMeaning ? cleanTurnMeaning : null,
|
||||||
source_signal: sourceSignal,
|
source_signal: sourceSignal,
|
||||||
reason_codes: reasonCodes
|
reason_codes: reasonCodes
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,24 @@ function isMovementLaneClarification(pilot: AssistantMcpDiscoveryPilotExecutionC
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isRankedValueFlowClarification(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||||||
|
return (
|
||||||
|
pilot.reason_codes.includes("planner_selected_top_ranked_value_flow_from_data_need_graph") ||
|
||||||
|
pilot.reason_codes.includes("planner_selected_bottom_ranked_value_flow_from_data_need_graph") ||
|
||||||
|
pilot.dry_run.reason_codes.includes("planner_selected_top_ranked_value_flow_from_data_need_graph") ||
|
||||||
|
pilot.dry_run.reason_codes.includes("planner_selected_bottom_ranked_value_flow_from_data_need_graph")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBidirectionalValueFlowComparisonClarification(
|
||||||
|
pilot: AssistantMcpDiscoveryPilotExecutionContract
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
pilot.reason_codes.includes("planner_selected_bidirectional_value_flow_comparison_from_data_need_graph") ||
|
||||||
|
pilot.dry_run.reason_codes.includes("planner_selected_bidirectional_value_flow_comparison_from_data_need_graph")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function isDocumentLaneClarification(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
function isDocumentLaneClarification(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||||||
return (
|
return (
|
||||||
isDocumentPilot(pilot) ||
|
isDocumentPilot(pilot) ||
|
||||||
|
|
@ -207,7 +225,14 @@ function laneScopeSuffix(pilot: AssistantMcpDiscoveryPilotExecutionContract): st
|
||||||
return entity ? ` по "${entity}"` : "";
|
return entity ? ` по "${entity}"` : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function dryRunHasAxis(pilot: AssistantMcpDiscoveryPilotExecutionContract, axis: string): boolean {
|
||||||
|
return pilot.dry_run.execution_steps.some((step) => step.provided_axes.includes(axis));
|
||||||
|
}
|
||||||
|
|
||||||
function dryRunMissingAxis(pilot: AssistantMcpDiscoveryPilotExecutionContract, axis: string): boolean {
|
function dryRunMissingAxis(pilot: AssistantMcpDiscoveryPilotExecutionContract, axis: string): boolean {
|
||||||
|
if (dryRunHasAxis(pilot, axis)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return pilot.dry_run.execution_steps.some((step) =>
|
return pilot.dry_run.execution_steps.some((step) =>
|
||||||
step.missing_axis_options.some((option) => option.includes(axis))
|
step.missing_axis_options.some((option) => option.includes(axis))
|
||||||
);
|
);
|
||||||
|
|
@ -216,8 +241,10 @@ function dryRunMissingAxis(pilot: AssistantMcpDiscoveryPilotExecutionContract, a
|
||||||
function clarificationNeedRu(
|
function clarificationNeedRu(
|
||||||
pilot: AssistantMcpDiscoveryPilotExecutionContract
|
pilot: AssistantMcpDiscoveryPilotExecutionContract
|
||||||
): { subject: string; verb: string } {
|
): { subject: string; verb: string } {
|
||||||
|
const hasCounterparty = dryRunHasAxis(pilot, "counterparty");
|
||||||
|
const hasAccount = dryRunHasAxis(pilot, "account");
|
||||||
const needsPeriod = dryRunMissingAxis(pilot, "period");
|
const needsPeriod = dryRunMissingAxis(pilot, "period");
|
||||||
const needsOrganization = dryRunMissingAxis(pilot, "organization");
|
const needsOrganization = !hasCounterparty && !hasAccount && dryRunMissingAxis(pilot, "organization");
|
||||||
if (needsPeriod && needsOrganization) {
|
if (needsPeriod && needsOrganization) {
|
||||||
return { subject: "проверяемый период и организацию", verb: "нужно" };
|
return { subject: "проверяемый период и организацию", verb: "нужно" };
|
||||||
}
|
}
|
||||||
|
|
@ -281,6 +308,9 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
||||||
) {
|
) {
|
||||||
return "По текущему каталожному поиску 1С точный контрагент пока не подтвержден.";
|
return "По текущему каталожному поиску 1С точный контрагент пока не подтвержден.";
|
||||||
}
|
}
|
||||||
|
if (pilot.derived_ranked_value_flow && mode === "confirmed_with_bounded_inference") {
|
||||||
|
return "По данным 1С можно построить ограниченный ranking по контрагентам на подтвержденных строках денежных движений.";
|
||||||
|
}
|
||||||
if (isMovementPilot(pilot) && mode === "confirmed_with_bounded_inference") {
|
if (isMovementPilot(pilot) && mode === "confirmed_with_bounded_inference") {
|
||||||
return `По движениям${documentOrMovementScopeRu(pilot)} в 1С найдены подтвержденные строки; ответ ограничен проверенным окном и найденными строками.`;
|
return `По движениям${documentOrMovementScopeRu(pilot)} в 1С найдены подтвержденные строки; ответ ограничен проверенным окном и найденными строками.`;
|
||||||
}
|
}
|
||||||
|
|
@ -340,6 +370,14 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
||||||
const need = clarificationNeedRu(pilot);
|
const need = clarificationNeedRu(pilot);
|
||||||
return `Могу идти дальше по документам${laneScopeSuffix(pilot)}, но для запуска поиска в 1С ${need.verb} ${need.subject}.`;
|
return `Могу идти дальше по документам${laneScopeSuffix(pilot)}, но для запуска поиска в 1С ${need.verb} ${need.subject}.`;
|
||||||
}
|
}
|
||||||
|
if (mode === "needs_clarification" && isBidirectionalValueFlowComparisonClarification(pilot)) {
|
||||||
|
const need = clarificationNeedRu(pilot);
|
||||||
|
return `Могу сравнить входящий и исходящий денежный поток, но для bounded поиска в 1С ${need.verb} ${need.subject}.`;
|
||||||
|
}
|
||||||
|
if (mode === "needs_clarification" && isRankedValueFlowClarification(pilot)) {
|
||||||
|
const need = clarificationNeedRu(pilot);
|
||||||
|
return `Могу посчитать ranking по денежному потоку между контрагентами, но для bounded поиска в 1С ${need.verb} ${need.subject}.`;
|
||||||
|
}
|
||||||
if (mode === "needs_clarification") {
|
if (mode === "needs_clarification") {
|
||||||
return "Нужно уточнить контекст перед поиском в 1С.";
|
return "Нужно уточнить контекст перед поиском в 1С.";
|
||||||
}
|
}
|
||||||
|
|
@ -378,6 +416,12 @@ function nextStepFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
||||||
if (mode === "needs_clarification" && isDocumentLaneClarification(pilot)) {
|
if (mode === "needs_clarification" && isDocumentLaneClarification(pilot)) {
|
||||||
return clarificationNextStepLine(pilot, "документам");
|
return clarificationNextStepLine(pilot, "документам");
|
||||||
}
|
}
|
||||||
|
if (mode === "needs_clarification" && isBidirectionalValueFlowComparisonClarification(pilot)) {
|
||||||
|
return clarificationNextStepLine(pilot, "сравнению входящих и исходящих денежных потоков");
|
||||||
|
}
|
||||||
|
if (mode === "needs_clarification" && isRankedValueFlowClarification(pilot)) {
|
||||||
|
return clarificationNextStepLine(pilot, "ranking-поиску между контрагентами");
|
||||||
|
}
|
||||||
if (mode === "needs_clarification") {
|
if (mode === "needs_clarification") {
|
||||||
return "Уточните контрагента, период или организацию, и я смогу выполнить проверку по 1С.";
|
return "Уточните контрагента, период или организацию, и я смогу выполнить проверку по 1С.";
|
||||||
}
|
}
|
||||||
|
|
@ -413,6 +457,10 @@ function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract):
|
||||||
claims.push("Do not claim full all-time turnover unless the checked period and coverage prove it.");
|
claims.push("Do not claim full all-time turnover unless the checked period and coverage prove it.");
|
||||||
claims.push("Do not present a derived sum as a legal/accounting final total outside the checked 1C rows.");
|
claims.push("Do not present a derived sum as a legal/accounting final total outside the checked 1C rows.");
|
||||||
}
|
}
|
||||||
|
if (pilot.derived_ranked_value_flow) {
|
||||||
|
claims.push("Do not present a bounded ranking as a complete all-time ranking outside the checked period and organization.");
|
||||||
|
claims.push("Do not imply the top-ranked counterparty is globally final when probe-limit or scope boundaries still exist.");
|
||||||
|
}
|
||||||
if (isDocumentPilot(pilot)) {
|
if (isDocumentPilot(pilot)) {
|
||||||
claims.push("Do not claim full document history outside the checked period.");
|
claims.push("Do not claim full document history outside the checked period.");
|
||||||
claims.push("Do not present the confirmed document rows as a complete document universe.");
|
claims.push("Do not present the confirmed document rows as a complete document universe.");
|
||||||
|
|
@ -556,6 +604,43 @@ function derivedEntityResolutionInferenceLine(pilot: AssistantMcpDiscoveryPilotE
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function derivedRankedValueFlowInferenceLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||||||
|
const ranking = pilot.derived_ranked_value_flow;
|
||||||
|
if (!ranking) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : "";
|
||||||
|
const period = ranking.period_scope ? ` за период ${ranking.period_scope}` : " в проверенном окне";
|
||||||
|
return `Ranking по контрагентам${organization}${period} рассчитан только по подтвержденным строкам 1С и не доказывает полный исторический срез вне проверенного окна.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function derivedRankedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||||||
|
const ranking = pilot.derived_ranked_value_flow;
|
||||||
|
if (!ranking || ranking.ranked_values.length <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const leader = ranking.ranked_values[0];
|
||||||
|
const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : "";
|
||||||
|
const period = ranking.period_scope ? ` за период ${ranking.period_scope}` : " в проверенном окне";
|
||||||
|
const directionLead =
|
||||||
|
ranking.ranking_need === "bottom_asc"
|
||||||
|
? ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||||
|
? "Меньше всего заплатили контрагенту"
|
||||||
|
: "Меньше всего денег принёс контрагент"
|
||||||
|
: ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||||
|
? "Больше всего заплатили контрагенту"
|
||||||
|
: "Больше всего денег принёс контрагент";
|
||||||
|
const tail = ranking.ranked_values
|
||||||
|
.slice(1, 3)
|
||||||
|
.map((bucket) => `${bucket.axis_value} — ${bucket.total_amount_human_ru}`)
|
||||||
|
.join("; ");
|
||||||
|
const trail = tail ? ` Следом: ${tail}.` : "";
|
||||||
|
const limitCaveat = ranking.coverage_limited_by_probe_limit
|
||||||
|
? " Лимит строк проверки достигнут; ranking может быть неполным."
|
||||||
|
: "";
|
||||||
|
return `${directionLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${trail}${limitCaveat}`;
|
||||||
|
}
|
||||||
|
|
||||||
function derivedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
function derivedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||||||
const flow = pilot.derived_value_flow;
|
const flow = pilot.derived_value_flow;
|
||||||
if (!flow) {
|
if (!flow) {
|
||||||
|
|
@ -662,13 +747,17 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
|
||||||
const derivedInferenceLine =
|
const derivedInferenceLine =
|
||||||
derivedActivityInferenceLine(pilot) ??
|
derivedActivityInferenceLine(pilot) ??
|
||||||
derivedMetadataInferenceLine(pilot) ??
|
derivedMetadataInferenceLine(pilot) ??
|
||||||
|
derivedRankedValueFlowInferenceLine(pilot) ??
|
||||||
derivedEntityResolutionInferenceLine(pilot);
|
derivedEntityResolutionInferenceLine(pilot);
|
||||||
const inferenceLines = derivedInferenceLine
|
const inferenceLines = derivedInferenceLine
|
||||||
? [derivedInferenceLine]
|
? [derivedInferenceLine]
|
||||||
: pilot.evidence.inferred_facts;
|
: pilot.evidence.inferred_facts;
|
||||||
const derivedMetadataLine = derivedMetadataConfirmedLine(pilot);
|
const derivedMetadataLine = derivedMetadataConfirmedLine(pilot);
|
||||||
const derivedEntityResolutionLine = derivedEntityResolutionConfirmedLine(pilot);
|
const derivedEntityResolutionLine = derivedEntityResolutionConfirmedLine(pilot);
|
||||||
const derivedValueLine = derivedBidirectionalValueFlowConfirmedLine(pilot) ?? derivedValueFlowConfirmedLine(pilot);
|
const derivedValueLine =
|
||||||
|
derivedBidirectionalValueFlowConfirmedLine(pilot) ??
|
||||||
|
derivedRankedValueFlowConfirmedLine(pilot) ??
|
||||||
|
derivedValueFlowConfirmedLine(pilot);
|
||||||
const monthlyConfirmedLines =
|
const monthlyConfirmedLines =
|
||||||
derivedBidirectionalValueFlowMonthlyLines(pilot).length > 0
|
derivedBidirectionalValueFlowMonthlyLines(pilot).length > 0
|
||||||
? derivedBidirectionalValueFlowMonthlyLines(pilot)
|
? derivedBidirectionalValueFlowMonthlyLines(pilot)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,358 @@
|
||||||
|
import type { AssistantMcpDiscoveryTurnMeaningRef } from "./assistantMcpDiscoveryPolicy";
|
||||||
|
|
||||||
|
export const ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION =
|
||||||
|
"assistant_data_need_graph_v1" as const;
|
||||||
|
|
||||||
|
export type AssistantMcpDiscoveryDataNeedProofExpectation =
|
||||||
|
| "schema_surface"
|
||||||
|
| "entity_grounding"
|
||||||
|
| "coverage_checked_fact"
|
||||||
|
| "bounded_inference"
|
||||||
|
| "clarification_required";
|
||||||
|
|
||||||
|
export interface AssistantMcpDiscoveryDataNeedGraphContract {
|
||||||
|
schema_version: typeof ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION;
|
||||||
|
policy_owner: "assistantMcpDiscoveryDataNeedGraph";
|
||||||
|
subject_candidates: string[];
|
||||||
|
business_fact_family: string | null;
|
||||||
|
action_family: string | null;
|
||||||
|
aggregation_need: string | null;
|
||||||
|
time_scope_need: string | null;
|
||||||
|
comparison_need: string | null;
|
||||||
|
ranking_need: string | null;
|
||||||
|
proof_expectation: AssistantMcpDiscoveryDataNeedProofExpectation;
|
||||||
|
clarification_gaps: string[];
|
||||||
|
decomposition_candidates: string[];
|
||||||
|
forbidden_overclaim_flags: string[];
|
||||||
|
reason_codes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BuildAssistantMcpDiscoveryDataNeedGraphInput {
|
||||||
|
semanticDataNeed?: string | null;
|
||||||
|
rawUtterance?: string | null;
|
||||||
|
turnMeaning?: AssistantMcpDiscoveryTurnMeaningRef | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toNonEmptyString(value: unknown): string | null {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const text = String(value).trim();
|
||||||
|
return text.length > 0 ? text : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function lower(value: unknown): string {
|
||||||
|
return String(value ?? "").trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeReasonCode(value: string): string | null {
|
||||||
|
const normalized = value
|
||||||
|
.trim()
|
||||||
|
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
|
||||||
|
.replace(/^_+|_+$/g, "")
|
||||||
|
.toLowerCase();
|
||||||
|
return normalized.length > 0 ? normalized.slice(0, 120) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushReason(target: string[], value: string): void {
|
||||||
|
const normalized = normalizeReasonCode(value);
|
||||||
|
if (normalized && !target.includes(normalized)) {
|
||||||
|
target.push(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushUnique(target: string[], value: string | null | undefined): void {
|
||||||
|
const text = toNonEmptyString(value);
|
||||||
|
if (text && !target.includes(text)) {
|
||||||
|
target.push(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function businessFactFamilyFor(input: {
|
||||||
|
semanticDataNeed: string;
|
||||||
|
domain: string;
|
||||||
|
action: string;
|
||||||
|
unsupported: string;
|
||||||
|
}): string | null {
|
||||||
|
const combined = `${input.semanticDataNeed} ${input.domain} ${input.action} ${input.unsupported}`.trim();
|
||||||
|
if (combined.includes("metadata lane clarification")) {
|
||||||
|
return "schema_surface";
|
||||||
|
}
|
||||||
|
if (combined.includes("metadata")) {
|
||||||
|
return "schema_surface";
|
||||||
|
}
|
||||||
|
if (combined.includes("entity discovery") || combined.includes("entity_resolution")) {
|
||||||
|
return "entity_grounding";
|
||||||
|
}
|
||||||
|
if (combined.includes("lifecycle") || combined.includes("activity")) {
|
||||||
|
return "activity_lifecycle";
|
||||||
|
}
|
||||||
|
if (combined.includes("movement")) {
|
||||||
|
return "movement_evidence";
|
||||||
|
}
|
||||||
|
if (combined.includes("document")) {
|
||||||
|
return "document_evidence";
|
||||||
|
}
|
||||||
|
if (combined.includes("value-flow") || combined.includes("turnover") || combined.includes("payout") || combined.includes("net")) {
|
||||||
|
return "value_flow";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function aggregationNeedFor(axis: string): string | null {
|
||||||
|
if (!axis) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (axis === "month") {
|
||||||
|
return "by_month";
|
||||||
|
}
|
||||||
|
return `by_${axis}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function timeScopeNeedFor(input: {
|
||||||
|
family: string | null;
|
||||||
|
explicitDateScope: string | null;
|
||||||
|
}): string | null {
|
||||||
|
if (input.explicitDateScope) {
|
||||||
|
return "explicit_period";
|
||||||
|
}
|
||||||
|
if (input.family === "value_flow" || input.family === "movement_evidence" || input.family === "document_evidence") {
|
||||||
|
return "period_required";
|
||||||
|
}
|
||||||
|
if (input.family === "activity_lifecycle") {
|
||||||
|
return "open_activity_window";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function comparisonNeedFor(action: string): string | null {
|
||||||
|
if (action === "net_value_flow") {
|
||||||
|
return "incoming_vs_outgoing";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function allowsOpenScopeWithoutSubject(input: {
|
||||||
|
family: string | null;
|
||||||
|
comparisonNeed: string | null;
|
||||||
|
rankingNeed: string | null;
|
||||||
|
}): boolean {
|
||||||
|
if (input.family !== "value_flow") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Boolean(input.rankingNeed || input.comparisonNeed === "incoming_vs_outgoing");
|
||||||
|
}
|
||||||
|
|
||||||
|
function rankingNeedFromRawUtterance(value: string): string | null {
|
||||||
|
const text = lower(value);
|
||||||
|
if (!text) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
/(?:\btop[-\s]?\d+\b|\btop\b|топ[-\s]?\d+|топ\b|сам(?:ый|ая|ое|ые)\b|больше\s+всего|наибол[её]е|highest|largest|most)/iu.test(
|
||||||
|
text
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return "top_desc";
|
||||||
|
}
|
||||||
|
if (/(?:меньше\s+всего|наимен[ьш]е|lowest|smallest|least)/iu.test(text)) {
|
||||||
|
return "bottom_asc";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function proofExpectationFor(input: {
|
||||||
|
family: string | null;
|
||||||
|
clarificationGaps: string[];
|
||||||
|
}): AssistantMcpDiscoveryDataNeedProofExpectation {
|
||||||
|
if (input.clarificationGaps.length > 0) {
|
||||||
|
return "clarification_required";
|
||||||
|
}
|
||||||
|
if (input.family === "schema_surface") {
|
||||||
|
return "schema_surface";
|
||||||
|
}
|
||||||
|
if (input.family === "entity_grounding") {
|
||||||
|
return "entity_grounding";
|
||||||
|
}
|
||||||
|
if (input.family === "activity_lifecycle") {
|
||||||
|
return "bounded_inference";
|
||||||
|
}
|
||||||
|
return "coverage_checked_fact";
|
||||||
|
}
|
||||||
|
|
||||||
|
function decompositionCandidatesFor(input: {
|
||||||
|
family: string | null;
|
||||||
|
action: string;
|
||||||
|
aggregationNeed: string | null;
|
||||||
|
comparisonNeed: string | null;
|
||||||
|
rankingNeed: string | null;
|
||||||
|
openScopeWithoutSubject: boolean;
|
||||||
|
}): string[] {
|
||||||
|
const result: string[] = [];
|
||||||
|
if (input.family === "schema_surface") {
|
||||||
|
pushUnique(result, "inspect_metadata_surface");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (input.family === "entity_grounding") {
|
||||||
|
pushUnique(result, "search_business_entity");
|
||||||
|
pushUnique(result, "resolve_entity_reference");
|
||||||
|
pushUnique(result, "probe_coverage");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (input.family === "value_flow") {
|
||||||
|
if (input.rankingNeed && input.openScopeWithoutSubject) {
|
||||||
|
pushUnique(result, "collect_scoped_movements");
|
||||||
|
pushUnique(result, "aggregate_ranked_axis_values");
|
||||||
|
pushUnique(result, "probe_coverage");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (input.comparisonNeed === "incoming_vs_outgoing" && input.openScopeWithoutSubject) {
|
||||||
|
pushUnique(result, "collect_incoming_movements");
|
||||||
|
pushUnique(result, "collect_outgoing_movements");
|
||||||
|
if (input.aggregationNeed === "by_month") {
|
||||||
|
pushUnique(result, "aggregate_by_month");
|
||||||
|
}
|
||||||
|
pushUnique(result, "probe_coverage");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
pushUnique(result, "resolve_entity_reference");
|
||||||
|
if (input.action === "net_value_flow") {
|
||||||
|
pushUnique(result, "collect_incoming_movements");
|
||||||
|
pushUnique(result, "collect_outgoing_movements");
|
||||||
|
} else {
|
||||||
|
pushUnique(result, "collect_scoped_movements");
|
||||||
|
}
|
||||||
|
pushUnique(result, input.aggregationNeed === "by_month" ? "aggregate_by_month" : "aggregate_checked_amounts");
|
||||||
|
pushUnique(result, "probe_coverage");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (input.family === "movement_evidence") {
|
||||||
|
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");
|
||||||
|
pushUnique(result, "fetch_scoped_documents");
|
||||||
|
pushUnique(result, "probe_coverage");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (input.family === "activity_lifecycle") {
|
||||||
|
pushUnique(result, "resolve_entity_reference");
|
||||||
|
pushUnique(result, "fetch_supporting_documents");
|
||||||
|
pushUnique(result, "probe_coverage");
|
||||||
|
pushUnique(result, "explain_evidence_basis");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function forbiddenOverclaimFlagsFor(family: string | null): string[] {
|
||||||
|
const result: string[] = ["no_raw_model_claims"];
|
||||||
|
if (family === "schema_surface") {
|
||||||
|
pushUnique(result, "no_fake_schema_surface");
|
||||||
|
}
|
||||||
|
if (family === "entity_grounding") {
|
||||||
|
pushUnique(result, "no_unresolved_entity_claim");
|
||||||
|
}
|
||||||
|
if (family === "activity_lifecycle") {
|
||||||
|
pushUnique(result, "no_legal_age_claim_without_evidence");
|
||||||
|
}
|
||||||
|
if (family === "value_flow" || family === "movement_evidence" || family === "document_evidence") {
|
||||||
|
pushUnique(result, "no_unchecked_fact_totals");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAssistantMcpDiscoveryDataNeedGraph(
|
||||||
|
input: BuildAssistantMcpDiscoveryDataNeedGraphInput
|
||||||
|
): AssistantMcpDiscoveryDataNeedGraphContract {
|
||||||
|
const semanticDataNeed = lower(input.semanticDataNeed);
|
||||||
|
const turnMeaning = input.turnMeaning ?? null;
|
||||||
|
const domain = lower(turnMeaning?.asked_domain_family);
|
||||||
|
const action = lower(turnMeaning?.asked_action_family);
|
||||||
|
const unsupported = lower(turnMeaning?.unsupported_but_understood_family);
|
||||||
|
const rawUtterance = lower(input.rawUtterance);
|
||||||
|
const aggregationAxis = lower(turnMeaning?.asked_aggregation_axis);
|
||||||
|
const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope);
|
||||||
|
const subjectCandidates = (turnMeaning?.explicit_entity_candidates ?? [])
|
||||||
|
.map((item) => toNonEmptyString(item))
|
||||||
|
.filter((item): item is string => Boolean(item));
|
||||||
|
const businessFactFamily = businessFactFamilyFor({
|
||||||
|
semanticDataNeed,
|
||||||
|
domain,
|
||||||
|
action,
|
||||||
|
unsupported
|
||||||
|
});
|
||||||
|
const aggregationNeed = aggregationNeedFor(aggregationAxis);
|
||||||
|
const comparisonNeed = comparisonNeedFor(action);
|
||||||
|
const rankingNeed = rankingNeedFromRawUtterance(rawUtterance);
|
||||||
|
const openScopeWithoutSubject =
|
||||||
|
subjectCandidates.length === 0 &&
|
||||||
|
allowsOpenScopeWithoutSubject({
|
||||||
|
family: businessFactFamily,
|
||||||
|
comparisonNeed,
|
||||||
|
rankingNeed
|
||||||
|
});
|
||||||
|
const clarificationGaps: string[] = [];
|
||||||
|
if (unsupported === "metadata_lane_choice_clarification" || action === "resolve_next_lane") {
|
||||||
|
pushUnique(clarificationGaps, "lane_family_choice");
|
||||||
|
}
|
||||||
|
if (subjectCandidates.length === 0 && businessFactFamily !== "schema_surface" && !openScopeWithoutSubject) {
|
||||||
|
pushUnique(clarificationGaps, "subject");
|
||||||
|
}
|
||||||
|
const timeScopeNeed = timeScopeNeedFor({
|
||||||
|
family: businessFactFamily,
|
||||||
|
explicitDateScope
|
||||||
|
});
|
||||||
|
if (timeScopeNeed === "period_required" && !explicitDateScope) {
|
||||||
|
pushUnique(clarificationGaps, "period");
|
||||||
|
}
|
||||||
|
const decompositionCandidates = decompositionCandidatesFor({
|
||||||
|
family: businessFactFamily,
|
||||||
|
action,
|
||||||
|
aggregationNeed,
|
||||||
|
comparisonNeed,
|
||||||
|
rankingNeed,
|
||||||
|
openScopeWithoutSubject
|
||||||
|
});
|
||||||
|
const reasonCodes: string[] = [];
|
||||||
|
pushReason(reasonCodes, "data_need_graph_built");
|
||||||
|
if (businessFactFamily) {
|
||||||
|
pushReason(reasonCodes, `data_need_graph_family_${businessFactFamily}`);
|
||||||
|
} else {
|
||||||
|
pushReason(reasonCodes, "data_need_graph_family_unknown");
|
||||||
|
}
|
||||||
|
if (aggregationNeed) {
|
||||||
|
pushReason(reasonCodes, `data_need_graph_aggregation_${aggregationNeed}`);
|
||||||
|
}
|
||||||
|
if (rankingNeed) {
|
||||||
|
pushReason(reasonCodes, `data_need_graph_ranking_${rankingNeed}`);
|
||||||
|
}
|
||||||
|
if (comparisonNeed) {
|
||||||
|
pushReason(reasonCodes, `data_need_graph_comparison_${comparisonNeed}`);
|
||||||
|
}
|
||||||
|
if (clarificationGaps.length > 0) {
|
||||||
|
pushReason(reasonCodes, "data_need_graph_has_clarification_gaps");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
schema_version: ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION,
|
||||||
|
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||||
|
subject_candidates: subjectCandidates,
|
||||||
|
business_fact_family: businessFactFamily,
|
||||||
|
action_family: toNonEmptyString(turnMeaning?.asked_action_family),
|
||||||
|
aggregation_need: aggregationNeed,
|
||||||
|
time_scope_need: timeScopeNeed,
|
||||||
|
comparison_need: comparisonNeed,
|
||||||
|
ranking_need: rankingNeed,
|
||||||
|
proof_expectation: proofExpectationFor({
|
||||||
|
family: businessFactFamily,
|
||||||
|
clarificationGaps
|
||||||
|
}),
|
||||||
|
clarification_gaps: clarificationGaps,
|
||||||
|
decomposition_candidates: decompositionCandidates,
|
||||||
|
forbidden_overclaim_flags: forbiddenOverclaimFlagsFor(businessFactFamily),
|
||||||
|
reason_codes: reasonCodes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -78,6 +78,28 @@ export interface AssistantMcpDiscoveryDerivedValueFlow {
|
||||||
inference_basis: "sum_of_confirmed_1c_value_flow_rows";
|
inference_basis: "sum_of_confirmed_1c_value_flow_rows";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AssistantMcpDiscoveryRankedValueFlowBucket {
|
||||||
|
axis_value: string;
|
||||||
|
rows_with_amount: number;
|
||||||
|
total_amount: number;
|
||||||
|
total_amount_human_ru: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantMcpDiscoveryDerivedRankedValueFlow {
|
||||||
|
value_flow_direction: "incoming_customer_revenue" | "outgoing_supplier_payout";
|
||||||
|
ranking_need: "top_desc" | "bottom_asc";
|
||||||
|
ranking_axis: "counterparty";
|
||||||
|
organization_scope: string | null;
|
||||||
|
period_scope: string | null;
|
||||||
|
rows_matched: number;
|
||||||
|
rows_with_amount: number;
|
||||||
|
ranked_values: AssistantMcpDiscoveryRankedValueFlowBucket[];
|
||||||
|
coverage_limited_by_probe_limit: boolean;
|
||||||
|
coverage_recovered_by_period_chunking: boolean;
|
||||||
|
period_chunking_granularity: AssistantMcpDiscoveryAggregationAxis | null;
|
||||||
|
inference_basis: "ranked_counterparty_totals_from_confirmed_1c_value_flow_rows";
|
||||||
|
}
|
||||||
|
|
||||||
export interface AssistantMcpDiscoveryValueFlowSideSummary {
|
export interface AssistantMcpDiscoveryValueFlowSideSummary {
|
||||||
rows_matched: number;
|
rows_matched: number;
|
||||||
rows_with_amount: number;
|
rows_with_amount: number;
|
||||||
|
|
@ -187,6 +209,7 @@ export interface AssistantMcpDiscoveryPilotExecutionContract {
|
||||||
derived_metadata_surface: AssistantMcpDiscoveryDerivedMetadataSurface | null;
|
derived_metadata_surface: AssistantMcpDiscoveryDerivedMetadataSurface | null;
|
||||||
derived_entity_resolution: AssistantMcpDiscoveryDerivedEntityResolution | null;
|
derived_entity_resolution: AssistantMcpDiscoveryDerivedEntityResolution | null;
|
||||||
derived_activity_period: AssistantMcpDiscoveryDerivedActivityPeriod | null;
|
derived_activity_period: AssistantMcpDiscoveryDerivedActivityPeriod | null;
|
||||||
|
derived_ranked_value_flow?: AssistantMcpDiscoveryDerivedRankedValueFlow | null;
|
||||||
derived_value_flow: AssistantMcpDiscoveryDerivedValueFlow | null;
|
derived_value_flow: AssistantMcpDiscoveryDerivedValueFlow | null;
|
||||||
derived_bidirectional_value_flow: AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null;
|
derived_bidirectional_value_flow: AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null;
|
||||||
query_limitations: string[];
|
query_limitations: string[];
|
||||||
|
|
@ -334,6 +357,20 @@ function buildValueFlowFilters(planner: AssistantMcpDiscoveryPlannerContract): A
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function organizationScopeForPlanner(planner: AssistantMcpDiscoveryPlannerContract): string | null {
|
||||||
|
return toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_organization_scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rankingNeedForPlanner(
|
||||||
|
planner: AssistantMcpDiscoveryPlannerContract
|
||||||
|
): AssistantMcpDiscoveryDerivedRankedValueFlow["ranking_need"] | null {
|
||||||
|
const rankingNeed = toNonEmptyString(planner.data_need_graph?.ranking_need)?.toLowerCase();
|
||||||
|
if (rankingNeed === "top_desc" || rankingNeed === "bottom_asc") {
|
||||||
|
return rankingNeed;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeEntityResolutionText(value: string | null): string {
|
function normalizeEntityResolutionText(value: string | null): string {
|
||||||
return String(value ?? "")
|
return String(value ?? "")
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
|
|
@ -544,7 +581,11 @@ function isMovementEvidencePilotEligible(planner: AssistantMcpDiscoveryPlannerCo
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValueFlowPilotEligible(planner: AssistantMcpDiscoveryPlannerContract): boolean {
|
function isValueFlowPilotEligible(planner: AssistantMcpDiscoveryPlannerContract): boolean {
|
||||||
if (planner.selected_chain_id === "value_flow") {
|
if (
|
||||||
|
planner.selected_chain_id === "value_flow" ||
|
||||||
|
planner.selected_chain_id === "value_flow_ranking" ||
|
||||||
|
planner.selected_chain_id === "value_flow_comparison"
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const meaning = planner.discovery_plan.turn_meaning_ref;
|
const meaning = planner.discovery_plan.turn_meaning_ref;
|
||||||
|
|
@ -1429,6 +1470,17 @@ function rowAmountValue(row: Record<string, unknown>): number | null {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rowCounterpartyValue(row: Record<string, unknown>): string | null {
|
||||||
|
const candidates = [row["Контрагент"], row["Counterparty"], row["counterparty"], row["Наименование"], row["name"]];
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const text = toNonEmptyString(candidate);
|
||||||
|
if (text) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function monthBucketFromIsoDate(isoDate: string | null): string | null {
|
function monthBucketFromIsoDate(isoDate: string | null): string | null {
|
||||||
const match = isoDate?.match(/^(\d{4})-(\d{2})-\d{2}$/);
|
const match = isoDate?.match(/^(\d{4})-(\d{2})-\d{2}$/);
|
||||||
return match ? `${match[1]}-${match[2]}` : null;
|
return match ? `${match[1]}-${match[2]}` : null;
|
||||||
|
|
@ -1629,6 +1681,74 @@ function deriveValueFlow(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function deriveRankedValueFlow(
|
||||||
|
result: AssistantMcpDiscoveryCoverageAwareQueryResult | null,
|
||||||
|
input: {
|
||||||
|
organizationScope: string | null;
|
||||||
|
periodScope: string | null;
|
||||||
|
direction: AssistantMcpDiscoveryDerivedRankedValueFlow["value_flow_direction"];
|
||||||
|
rankingNeed: AssistantMcpDiscoveryDerivedRankedValueFlow["ranking_need"];
|
||||||
|
}
|
||||||
|
): AssistantMcpDiscoveryDerivedRankedValueFlow | null {
|
||||||
|
if (!result || result.error || result.matched_rows <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buckets = new Map<string, { rows_with_amount: number; total_amount: number }>();
|
||||||
|
let rowsWithAmount = 0;
|
||||||
|
for (const row of result.rows) {
|
||||||
|
const axisValue = rowCounterpartyValue(row);
|
||||||
|
const amount = rowAmountValue(row);
|
||||||
|
if (!axisValue || amount === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
rowsWithAmount += 1;
|
||||||
|
const current = buckets.get(axisValue) ?? { rows_with_amount: 0, total_amount: 0 };
|
||||||
|
current.rows_with_amount += 1;
|
||||||
|
current.total_amount += amount;
|
||||||
|
buckets.set(axisValue, current);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rowsWithAmount <= 0 || buckets.size <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rankedValues = Array.from(buckets.entries())
|
||||||
|
.map(([axisValue, bucket]) => ({
|
||||||
|
axis_value: axisValue,
|
||||||
|
rows_with_amount: bucket.rows_with_amount,
|
||||||
|
total_amount: bucket.total_amount,
|
||||||
|
total_amount_human_ru: formatAmountHumanRu(bucket.total_amount)
|
||||||
|
}))
|
||||||
|
.sort((left, right) => {
|
||||||
|
const amountDelta = right.total_amount - left.total_amount;
|
||||||
|
if (input.rankingNeed === "bottom_asc") {
|
||||||
|
if (amountDelta !== 0) {
|
||||||
|
return -amountDelta;
|
||||||
|
}
|
||||||
|
} else if (amountDelta !== 0) {
|
||||||
|
return amountDelta;
|
||||||
|
}
|
||||||
|
return left.axis_value.localeCompare(right.axis_value, "ru");
|
||||||
|
})
|
||||||
|
.slice(0, 5);
|
||||||
|
|
||||||
|
return {
|
||||||
|
value_flow_direction: input.direction,
|
||||||
|
ranking_need: input.rankingNeed,
|
||||||
|
ranking_axis: "counterparty",
|
||||||
|
organization_scope: input.organizationScope,
|
||||||
|
period_scope: input.periodScope,
|
||||||
|
rows_matched: result.matched_rows,
|
||||||
|
rows_with_amount: rowsWithAmount,
|
||||||
|
ranked_values: rankedValues,
|
||||||
|
coverage_limited_by_probe_limit: result.coverage_limited_by_probe_limit,
|
||||||
|
coverage_recovered_by_period_chunking: result.coverage_recovered_by_period_chunking,
|
||||||
|
period_chunking_granularity: result.period_chunking_granularity,
|
||||||
|
inference_basis: "ranked_counterparty_totals_from_confirmed_1c_value_flow_rows"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function deriveValueFlowSideSummary(
|
function deriveValueFlowSideSummary(
|
||||||
result: AssistantMcpDiscoveryCoverageAwareQueryResult | null
|
result: AssistantMcpDiscoveryCoverageAwareQueryResult | null
|
||||||
): AssistantMcpDiscoveryValueFlowSideSummary {
|
): AssistantMcpDiscoveryValueFlowSideSummary {
|
||||||
|
|
@ -1798,6 +1918,18 @@ function buildValueFlowConfirmedFacts(
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildRankedValueFlowConfirmedFacts(derived: AssistantMcpDiscoveryDerivedRankedValueFlow | null): string[] {
|
||||||
|
if (!derived || derived.ranked_values.length <= 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const leader = derived.ranked_values[0];
|
||||||
|
const directionLabel =
|
||||||
|
derived.value_flow_direction === "outgoing_supplier_payout" ? "supplier-payout" : "incoming value-flow";
|
||||||
|
return [
|
||||||
|
`1C ${directionLabel} rows were ranked by counterparty for the checked scope; leader=${leader.axis_value}, rows_with_amount=${leader.rows_with_amount}`
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function buildBidirectionalValueFlowConfirmedFacts(
|
function buildBidirectionalValueFlowConfirmedFacts(
|
||||||
derived: AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null
|
derived: AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null
|
||||||
): string[] {
|
): string[] {
|
||||||
|
|
@ -1880,6 +2012,19 @@ function buildValueFlowInferredFacts(derived: AssistantMcpDiscoveryDerivedValueF
|
||||||
return facts;
|
return facts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildRankedValueFlowInferredFacts(derived: AssistantMcpDiscoveryDerivedRankedValueFlow | null): string[] {
|
||||||
|
if (!derived) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const facts = ["Counterparty ranking was calculated from confirmed 1C movement rows grouped by counterparty"];
|
||||||
|
if (derived.coverage_recovered_by_period_chunking && derived.period_chunking_granularity === "month") {
|
||||||
|
facts.push(
|
||||||
|
"Requested period coverage for counterparty ranking was recovered through monthly 1C probes after a broad probe hit the row limit"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return facts;
|
||||||
|
}
|
||||||
|
|
||||||
function buildBidirectionalValueFlowInferredFacts(
|
function buildBidirectionalValueFlowInferredFacts(
|
||||||
derived: AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null
|
derived: AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null
|
||||||
): string[] {
|
): string[] {
|
||||||
|
|
@ -1939,6 +2084,22 @@ function buildValueFlowUnknownFacts(
|
||||||
return unknownFacts;
|
return unknownFacts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildRankedValueFlowUnknownFacts(
|
||||||
|
periodScope: string | null,
|
||||||
|
derived: AssistantMcpDiscoveryDerivedRankedValueFlow | null
|
||||||
|
): string[] {
|
||||||
|
const unknownFacts: string[] = [];
|
||||||
|
if (derived?.coverage_limited_by_probe_limit) {
|
||||||
|
unknownFacts.push("Complete requested-period ranking coverage is not proven because the MCP discovery probe row limit was reached");
|
||||||
|
}
|
||||||
|
unknownFacts.push(
|
||||||
|
periodScope
|
||||||
|
? "Full ranking outside the checked period is not proven by this MCP discovery pilot"
|
||||||
|
: "Full all-time counterparty ranking is not proven without an explicit checked period"
|
||||||
|
);
|
||||||
|
return unknownFacts;
|
||||||
|
}
|
||||||
|
|
||||||
function buildBidirectionalValueFlowUnknownFacts(
|
function buildBidirectionalValueFlowUnknownFacts(
|
||||||
periodScope: string | null,
|
periodScope: string | null,
|
||||||
derived: AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null
|
derived: AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null
|
||||||
|
|
@ -1979,6 +2140,8 @@ function pilotScopeForPlanner(planner: AssistantMcpDiscoveryPlannerContract): As
|
||||||
return "metadata_inspection_v1";
|
return "metadata_inspection_v1";
|
||||||
case "movement_evidence":
|
case "movement_evidence":
|
||||||
return "counterparty_movement_evidence_query_movements_v1";
|
return "counterparty_movement_evidence_query_movements_v1";
|
||||||
|
case "value_flow_comparison":
|
||||||
|
case "value_flow_ranking":
|
||||||
case "value_flow":
|
case "value_flow":
|
||||||
return valueFlowPilotProfile(planner).scope;
|
return valueFlowPilotProfile(planner).scope;
|
||||||
case "document_evidence":
|
case "document_evidence":
|
||||||
|
|
@ -2107,7 +2270,9 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||||
|
|
||||||
const counterparty = firstEntityCandidate(planner);
|
const counterparty = firstEntityCandidate(planner);
|
||||||
const dateScope = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_date_scope);
|
const dateScope = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_date_scope);
|
||||||
|
const organizationScope = organizationScopeForPlanner(planner);
|
||||||
const aggregationAxis = aggregationAxisForPlanner(planner);
|
const aggregationAxis = aggregationAxisForPlanner(planner);
|
||||||
|
const rankingNeed = rankingNeedForPlanner(planner);
|
||||||
|
|
||||||
if (metadataPilotEligible) {
|
if (metadataPilotEligible) {
|
||||||
let metadataResult: AddressMcpMetadataRowsResult | null = null;
|
let metadataResult: AddressMcpMetadataRowsResult | null = null;
|
||||||
|
|
@ -2694,6 +2859,50 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceRowsSummary = queryResult ? summarizeValueFlowRows(queryResult) : null;
|
const sourceRowsSummary = queryResult ? summarizeValueFlowRows(queryResult) : null;
|
||||||
|
if (planner.selected_chain_id === "value_flow_ranking" && rankingNeed) {
|
||||||
|
const derivedRankedValueFlow = deriveRankedValueFlow(queryResult, {
|
||||||
|
organizationScope,
|
||||||
|
periodScope: dateScope,
|
||||||
|
direction: valueFlowProfile.direction,
|
||||||
|
rankingNeed
|
||||||
|
});
|
||||||
|
if (derivedRankedValueFlow) {
|
||||||
|
pushReason(reasonCodes, "pilot_derived_ranked_value_flow_from_confirmed_rows");
|
||||||
|
}
|
||||||
|
const evidence = resolveAssistantMcpDiscoveryEvidence({
|
||||||
|
plan: planner.discovery_plan,
|
||||||
|
probeResults,
|
||||||
|
confirmedFacts: buildRankedValueFlowConfirmedFacts(derivedRankedValueFlow),
|
||||||
|
inferredFacts: buildRankedValueFlowInferredFacts(derivedRankedValueFlow),
|
||||||
|
unknownFacts: buildRankedValueFlowUnknownFacts(dateScope, derivedRankedValueFlow),
|
||||||
|
sourceRowsSummary,
|
||||||
|
queryLimitations,
|
||||||
|
recommendedNextProbe: "explain_evidence_basis"
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
|
||||||
|
policy_owner: "assistantMcpDiscoveryPilotExecutor",
|
||||||
|
pilot_status: "executed",
|
||||||
|
pilot_scope: valueFlowProfile.scope,
|
||||||
|
dry_run: dryRun,
|
||||||
|
mcp_execution_performed: executedPrimitives.length > 0,
|
||||||
|
executed_primitives: executedPrimitives,
|
||||||
|
skipped_primitives: skippedPrimitives,
|
||||||
|
probe_results: probeResults,
|
||||||
|
evidence,
|
||||||
|
source_rows_summary: sourceRowsSummary,
|
||||||
|
derived_metadata_surface: null,
|
||||||
|
derived_entity_resolution: null,
|
||||||
|
derived_activity_period: null,
|
||||||
|
derived_ranked_value_flow: derivedRankedValueFlow,
|
||||||
|
derived_value_flow: null,
|
||||||
|
derived_bidirectional_value_flow: null,
|
||||||
|
query_limitations: queryLimitations,
|
||||||
|
reason_codes: reasonCodes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const derivedValueFlow = deriveValueFlow(
|
const derivedValueFlow = deriveValueFlow(
|
||||||
queryResult,
|
queryResult,
|
||||||
counterparty,
|
counterparty,
|
||||||
|
|
@ -2733,6 +2942,7 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||||
derived_metadata_surface: null,
|
derived_metadata_surface: null,
|
||||||
derived_entity_resolution: null,
|
derived_entity_resolution: null,
|
||||||
derived_activity_period: null,
|
derived_activity_period: null,
|
||||||
|
derived_ranked_value_flow: null,
|
||||||
derived_value_flow: derivedValueFlow,
|
derived_value_flow: derivedValueFlow,
|
||||||
derived_bidirectional_value_flow: null,
|
derived_bidirectional_value_flow: null,
|
||||||
query_limitations: queryLimitations,
|
query_limitations: queryLimitations,
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
reviewAssistantMcpDiscoveryPlanAgainstCatalog,
|
reviewAssistantMcpDiscoveryPlanAgainstCatalog,
|
||||||
type AssistantMcpCatalogPlanReview
|
type AssistantMcpCatalogPlanReview
|
||||||
} from "./assistantMcpCatalogIndex";
|
} from "./assistantMcpCatalogIndex";
|
||||||
|
import type { AssistantMcpDiscoveryDataNeedGraphContract } from "./assistantMcpDiscoveryDataNeedGraph";
|
||||||
|
|
||||||
export const ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION = "assistant_mcp_discovery_planner_v1" as const;
|
export const ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION = "assistant_mcp_discovery_planner_v1" as const;
|
||||||
|
|
||||||
|
|
@ -17,6 +18,8 @@ export type AssistantMcpDiscoveryChainId =
|
||||||
| "metadata_inspection"
|
| "metadata_inspection"
|
||||||
| "metadata_lane_clarification"
|
| "metadata_lane_clarification"
|
||||||
| "value_flow"
|
| "value_flow"
|
||||||
|
| "value_flow_comparison"
|
||||||
|
| "value_flow_ranking"
|
||||||
| "lifecycle"
|
| "lifecycle"
|
||||||
| "movement_evidence"
|
| "movement_evidence"
|
||||||
| "document_evidence"
|
| "document_evidence"
|
||||||
|
|
@ -24,6 +27,7 @@ export type AssistantMcpDiscoveryChainId =
|
||||||
|
|
||||||
export interface AssistantMcpDiscoveryPlannerInput {
|
export interface AssistantMcpDiscoveryPlannerInput {
|
||||||
semanticDataNeed?: string | null;
|
semanticDataNeed?: string | null;
|
||||||
|
dataNeedGraph?: AssistantMcpDiscoveryDataNeedGraphContract | null;
|
||||||
turnMeaning?: AssistantMcpDiscoveryTurnMeaningRef | null;
|
turnMeaning?: AssistantMcpDiscoveryTurnMeaningRef | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,6 +36,7 @@ export interface AssistantMcpDiscoveryPlannerContract {
|
||||||
policy_owner: "assistantMcpDiscoveryPlanner";
|
policy_owner: "assistantMcpDiscoveryPlanner";
|
||||||
planner_status: AssistantMcpDiscoveryPlannerStatus;
|
planner_status: AssistantMcpDiscoveryPlannerStatus;
|
||||||
semantic_data_need: string | null;
|
semantic_data_need: string | null;
|
||||||
|
data_need_graph: AssistantMcpDiscoveryDataNeedGraphContract | null;
|
||||||
selected_chain_id: AssistantMcpDiscoveryChainId;
|
selected_chain_id: AssistantMcpDiscoveryChainId;
|
||||||
selected_chain_summary: string;
|
selected_chain_summary: string;
|
||||||
proposed_primitives: AssistantMcpDiscoveryPrimitive[];
|
proposed_primitives: AssistantMcpDiscoveryPrimitive[];
|
||||||
|
|
@ -93,6 +98,10 @@ function hasEntity(meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefin
|
||||||
return (meaning?.explicit_entity_candidates?.length ?? 0) > 0;
|
return (meaning?.explicit_entity_candidates?.length ?? 0) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasSubjectCandidates(graph: AssistantMcpDiscoveryDataNeedGraphContract | null | undefined): boolean {
|
||||||
|
return (graph?.subject_candidates.length ?? 0) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
function aggregationAxis(meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefined): string | null {
|
function aggregationAxis(meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefined): string | null {
|
||||||
return toNonEmptyString(meaning?.asked_aggregation_axis)?.toLowerCase() ?? null;
|
return toNonEmptyString(meaning?.asked_aggregation_axis)?.toLowerCase() ?? null;
|
||||||
}
|
}
|
||||||
|
|
@ -136,14 +145,150 @@ function budgetOverrideFor(input: AssistantMcpDiscoveryPlannerInput, recipe: Pla
|
||||||
|
|
||||||
function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
|
function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
|
||||||
const meaning = input.turnMeaning ?? null;
|
const meaning = input.turnMeaning ?? null;
|
||||||
|
const dataNeedGraph = input.dataNeedGraph ?? null;
|
||||||
const domain = lower(meaning?.asked_domain_family);
|
const domain = lower(meaning?.asked_domain_family);
|
||||||
const action = lower(meaning?.asked_action_family);
|
const action = lower(meaning?.asked_action_family);
|
||||||
const unsupported = lower(meaning?.unsupported_but_understood_family);
|
const unsupported = lower(meaning?.unsupported_but_understood_family);
|
||||||
|
const graphFactFamily = lower(dataNeedGraph?.business_fact_family);
|
||||||
|
const graphAction = lower(dataNeedGraph?.action_family);
|
||||||
|
const graphAggregation = lower(dataNeedGraph?.aggregation_need);
|
||||||
|
const graphClarificationGaps = (dataNeedGraph?.clarification_gaps ?? []).map((item) => lower(item));
|
||||||
const combined = `${domain} ${action} ${unsupported}`.trim();
|
const combined = `${domain} ${action} ${unsupported}`.trim();
|
||||||
const axes: string[] = [];
|
const axes: string[] = [];
|
||||||
const requestedAggregationAxis = aggregationAxis(meaning);
|
const requestedAggregationAxis = aggregationAxis(meaning);
|
||||||
addScopeAxes(axes, meaning);
|
addScopeAxes(axes, meaning);
|
||||||
|
|
||||||
|
if (graphClarificationGaps.includes("lane_family_choice")) {
|
||||||
|
pushUnique(axes, "lane_family_choice");
|
||||||
|
return {
|
||||||
|
semanticDataNeed: "metadata lane clarification",
|
||||||
|
chainId: "metadata_lane_clarification",
|
||||||
|
chainSummary: "Preserve the ambiguous metadata surface and ask the user to choose the next data lane before running MCP probes.",
|
||||||
|
primitives: [],
|
||||||
|
axes,
|
||||||
|
reason: "planner_selected_metadata_lane_clarification_from_data_need_graph"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (graphFactFamily === "value_flow") {
|
||||||
|
if (dataNeedGraph?.comparison_need === "incoming_vs_outgoing" && !hasSubjectCandidates(dataNeedGraph)) {
|
||||||
|
pushUnique(axes, "amount");
|
||||||
|
pushUnique(axes, "coverage_target");
|
||||||
|
if (requestedAggregationAxis === "month" || graphAggregation === "by_month") {
|
||||||
|
pushUnique(axes, "calendar_month");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
semanticDataNeed: "bidirectional value-flow comparison evidence",
|
||||||
|
chainId: "value_flow_comparison",
|
||||||
|
chainSummary:
|
||||||
|
"Query incoming and outgoing movements for the checked period and organization, compare the checked sides, and probe coverage before answering a bounded comparison.",
|
||||||
|
primitives: ["query_movements", "probe_coverage"],
|
||||||
|
axes,
|
||||||
|
reason: "planner_selected_bidirectional_value_flow_comparison_from_data_need_graph"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (dataNeedGraph?.ranking_need && !hasSubjectCandidates(dataNeedGraph)) {
|
||||||
|
pushUnique(axes, "aggregate_axis");
|
||||||
|
pushUnique(axes, "amount");
|
||||||
|
pushUnique(axes, "coverage_target");
|
||||||
|
return {
|
||||||
|
semanticDataNeed: "ranked value-flow evidence",
|
||||||
|
chainId: "value_flow_ranking",
|
||||||
|
chainSummary:
|
||||||
|
"Query scoped movements for the checked period and organization, aggregate checked amounts by counterparty, then probe coverage before answering a bounded ranking.",
|
||||||
|
primitives: ["query_movements", "aggregate_by_axis", "probe_coverage"],
|
||||||
|
axes,
|
||||||
|
reason:
|
||||||
|
dataNeedGraph.ranking_need === "bottom_asc"
|
||||||
|
? "planner_selected_bottom_ranked_value_flow_from_data_need_graph"
|
||||||
|
: "planner_selected_top_ranked_value_flow_from_data_need_graph"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
pushUnique(axes, "aggregate_axis");
|
||||||
|
pushUnique(axes, "amount");
|
||||||
|
pushUnique(axes, "coverage_target");
|
||||||
|
if (requestedAggregationAxis === "month" || graphAggregation === "by_month") {
|
||||||
|
pushUnique(axes, "calendar_month");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
semanticDataNeed: "counterparty value-flow evidence",
|
||||||
|
chainId: "value_flow",
|
||||||
|
chainSummary: "Resolve the business entity, query scoped movements, aggregate checked amounts, then probe coverage before answering.",
|
||||||
|
primitives: ["resolve_entity_reference", "query_movements", "aggregate_by_axis", "probe_coverage"],
|
||||||
|
axes,
|
||||||
|
reason:
|
||||||
|
requestedAggregationAxis === "month" || graphAggregation === "by_month"
|
||||||
|
? "planner_selected_monthly_value_flow_from_data_need_graph"
|
||||||
|
: "planner_selected_value_flow_from_data_need_graph"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (graphFactFamily === "activity_lifecycle") {
|
||||||
|
pushUnique(axes, "document_date");
|
||||||
|
pushUnique(axes, "coverage_target");
|
||||||
|
pushUnique(axes, "evidence_basis");
|
||||||
|
return {
|
||||||
|
semanticDataNeed: "counterparty lifecycle evidence",
|
||||||
|
chainId: "lifecycle",
|
||||||
|
chainSummary: "Resolve the business entity, query supporting documents, probe coverage, then explain the evidence basis for the inferred activity window.",
|
||||||
|
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage", "explain_evidence_basis"],
|
||||||
|
axes,
|
||||||
|
reason: "planner_selected_lifecycle_from_data_need_graph"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (graphFactFamily === "schema_surface") {
|
||||||
|
pushUnique(axes, "metadata_scope");
|
||||||
|
return {
|
||||||
|
semanticDataNeed: "1C metadata evidence",
|
||||||
|
chainId: "metadata_inspection",
|
||||||
|
chainSummary: "Inspect the 1C metadata surface first, then ground the next safe lane from confirmed schema evidence.",
|
||||||
|
primitives: ["inspect_1c_metadata"],
|
||||||
|
axes,
|
||||||
|
reason: "planner_selected_metadata_from_data_need_graph"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (graphFactFamily === "movement_evidence") {
|
||||||
|
pushUnique(axes, "coverage_target");
|
||||||
|
return {
|
||||||
|
semanticDataNeed: "movement evidence",
|
||||||
|
chainId: "movement_evidence",
|
||||||
|
chainSummary: "Resolve the business entity, fetch scoped movement rows, and probe coverage without pretending to have a full movement universe.",
|
||||||
|
primitives: ["resolve_entity_reference", "query_movements", "probe_coverage"],
|
||||||
|
axes,
|
||||||
|
reason: "planner_selected_movement_from_data_need_graph"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (graphFactFamily === "document_evidence") {
|
||||||
|
pushUnique(axes, "coverage_target");
|
||||||
|
return {
|
||||||
|
semanticDataNeed: "document evidence",
|
||||||
|
chainId: "document_evidence",
|
||||||
|
chainSummary: "Resolve the business entity, fetch scoped document rows, and probe coverage before stating the checked document evidence.",
|
||||||
|
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage"],
|
||||||
|
axes,
|
||||||
|
reason: "planner_selected_document_from_data_need_graph"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (graphFactFamily === "entity_grounding" || (!graphFactFamily && (dataNeedGraph?.subject_candidates.length ?? 0) > 0)) {
|
||||||
|
pushUnique(axes, "business_entity");
|
||||||
|
pushUnique(axes, "coverage_target");
|
||||||
|
return {
|
||||||
|
semanticDataNeed: "entity discovery evidence",
|
||||||
|
chainId: "entity_resolution",
|
||||||
|
chainSummary: "Search candidate business entities, resolve the most relevant 1C reference, and prove whether the entity grounding is stable enough for the next probe.",
|
||||||
|
primitives: ["search_business_entity", "resolve_entity_reference", "probe_coverage"],
|
||||||
|
axes,
|
||||||
|
reason:
|
||||||
|
graphAction === "search_business_entity"
|
||||||
|
? "planner_selected_entity_resolution_from_data_need_graph"
|
||||||
|
: "planner_selected_entity_resolution_recipe"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (includesAny(combined, ["metadata_lane_choice_clarification", "resolve_next_lane"])) {
|
if (includesAny(combined, ["metadata_lane_choice_clarification", "resolve_next_lane"])) {
|
||||||
pushUnique(axes, "lane_family_choice");
|
pushUnique(axes, "lane_family_choice");
|
||||||
return {
|
return {
|
||||||
|
|
@ -267,8 +412,12 @@ export function planAssistantMcpDiscovery(
|
||||||
const recipe = recipeFor(input);
|
const recipe = recipeFor(input);
|
||||||
const budgetOverride = budgetOverrideFor(input, recipe);
|
const budgetOverride = budgetOverrideFor(input, recipe);
|
||||||
const semanticDataNeed = toNonEmptyString(input.semanticDataNeed) ?? recipe.semanticDataNeed;
|
const semanticDataNeed = toNonEmptyString(input.semanticDataNeed) ?? recipe.semanticDataNeed;
|
||||||
|
const dataNeedGraph = input.dataNeedGraph ?? null;
|
||||||
const reasonCodes: string[] = [];
|
const reasonCodes: string[] = [];
|
||||||
pushReason(reasonCodes, recipe.reason);
|
pushReason(reasonCodes, recipe.reason);
|
||||||
|
if (dataNeedGraph) {
|
||||||
|
pushReason(reasonCodes, "planner_consumed_data_need_graph_v1");
|
||||||
|
}
|
||||||
if (budgetOverride.maxProbeCount) {
|
if (budgetOverride.maxProbeCount) {
|
||||||
pushReason(reasonCodes, "planner_enabled_chunked_coverage_probe_budget");
|
pushReason(reasonCodes, "planner_enabled_chunked_coverage_probe_budget");
|
||||||
}
|
}
|
||||||
|
|
@ -296,6 +445,7 @@ export function planAssistantMcpDiscovery(
|
||||||
policy_owner: "assistantMcpDiscoveryPlanner",
|
policy_owner: "assistantMcpDiscoveryPlanner",
|
||||||
planner_status: plannerStatus,
|
planner_status: plannerStatus,
|
||||||
semantic_data_need: semanticDataNeed,
|
semantic_data_need: semanticDataNeed,
|
||||||
|
data_need_graph: dataNeedGraph,
|
||||||
selected_chain_id: recipe.chainId,
|
selected_chain_id: recipe.chainId,
|
||||||
selected_chain_summary: recipe.chainSummary,
|
selected_chain_summary: recipe.chainSummary,
|
||||||
proposed_primitives: recipe.primitives,
|
proposed_primitives: recipe.primitives,
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,36 @@ function userFacingLines(values: string[]): string[] {
|
||||||
}
|
}
|
||||||
|
|
||||||
function localizeLine(value: string): string {
|
function localizeLine(value: string): string {
|
||||||
|
if (/^1C activity rows were found for the requested counterparty scope$/i.test(value)) {
|
||||||
|
return "В 1С найдены строки активности в запрошенном срезе.";
|
||||||
|
}
|
||||||
|
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
|
||||||
|
return "В 1С найдены строки входящих денежных поступлений в запрошенном срезе.";
|
||||||
|
}
|
||||||
|
if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
|
||||||
|
return "В 1С найдены строки исходящих платежей и списаний в запрошенном срезе.";
|
||||||
|
}
|
||||||
|
const openScopeBidirectionalMatch = value.match(
|
||||||
|
/^1C bidirectional value-flow rows were checked for the requested counterparty scope: incoming=(found|not_found), outgoing=(found|not_found)$/i
|
||||||
|
);
|
||||||
|
if (openScopeBidirectionalMatch) {
|
||||||
|
const incoming =
|
||||||
|
openScopeBidirectionalMatch[1] === "found"
|
||||||
|
? "входящие строки найдены"
|
||||||
|
: "входящие строки не найдены";
|
||||||
|
const outgoing =
|
||||||
|
openScopeBidirectionalMatch[2] === "found"
|
||||||
|
? "исходящие строки найдены"
|
||||||
|
: "исходящие строки не найдены";
|
||||||
|
return `В 1С проверены входящие и исходящие денежные строки в запрошенном срезе: ${incoming}, ${outgoing}.`;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
/^Requested period hit the MCP row limit, but the approved monthly recovery probe budget is smaller than the required subperiod count$/i.test(
|
||||||
|
value
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return "Запрошенный период уперся в лимит строк MCP; доступного бюджета помесячных дозапросов не хватило, чтобы покрыть все подпериоды.";
|
||||||
|
}
|
||||||
const counterpartyMatch = value.match(/^1C activity rows were found for counterparty\s+(.+)$/i);
|
const counterpartyMatch = value.match(/^1C activity rows were found for counterparty\s+(.+)$/i);
|
||||||
if (counterpartyMatch) {
|
if (counterpartyMatch) {
|
||||||
return `В 1С найдены строки активности по контрагенту ${counterpartyMatch[1]}.`;
|
return `В 1С найдены строки активности по контрагенту ${counterpartyMatch[1]}.`;
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
planAssistantMcpDiscovery,
|
planAssistantMcpDiscovery,
|
||||||
type AssistantMcpDiscoveryPlannerContract
|
type AssistantMcpDiscoveryPlannerContract
|
||||||
} from "./assistantMcpDiscoveryPlanner";
|
} from "./assistantMcpDiscoveryPlanner";
|
||||||
|
import type { AssistantMcpDiscoveryDataNeedGraphContract } from "./assistantMcpDiscoveryDataNeedGraph";
|
||||||
import type { AssistantMcpDiscoveryTurnMeaningRef } from "./assistantMcpDiscoveryPolicy";
|
import type { AssistantMcpDiscoveryTurnMeaningRef } from "./assistantMcpDiscoveryPolicy";
|
||||||
|
|
||||||
export const ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION =
|
export const ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION =
|
||||||
|
|
@ -25,6 +26,7 @@ export type AssistantMcpDiscoveryRuntimeBridgeStatus =
|
||||||
|
|
||||||
export interface AssistantMcpDiscoveryRuntimeBridgeInput {
|
export interface AssistantMcpDiscoveryRuntimeBridgeInput {
|
||||||
semanticDataNeed?: string | null;
|
semanticDataNeed?: string | null;
|
||||||
|
dataNeedGraph?: AssistantMcpDiscoveryDataNeedGraphContract | null;
|
||||||
turnMeaning?: AssistantMcpDiscoveryTurnMeaningRef | null;
|
turnMeaning?: AssistantMcpDiscoveryTurnMeaningRef | null;
|
||||||
deps?: AssistantMcpDiscoveryPilotExecutorDeps;
|
deps?: AssistantMcpDiscoveryPilotExecutorDeps;
|
||||||
}
|
}
|
||||||
|
|
@ -98,6 +100,7 @@ export async function runAssistantMcpDiscoveryRuntimeBridge(
|
||||||
): Promise<AssistantMcpDiscoveryRuntimeBridgeContract> {
|
): Promise<AssistantMcpDiscoveryRuntimeBridgeContract> {
|
||||||
const planner = planAssistantMcpDiscovery({
|
const planner = planAssistantMcpDiscovery({
|
||||||
semanticDataNeed: input.semanticDataNeed,
|
semanticDataNeed: input.semanticDataNeed,
|
||||||
|
dataNeedGraph: input.dataNeedGraph,
|
||||||
turnMeaning: input.turnMeaning
|
turnMeaning: input.turnMeaning
|
||||||
});
|
});
|
||||||
const pilot = await executeAssistantMcpDiscoveryPilot(planner, input.deps);
|
const pilot = await executeAssistantMcpDiscoveryPilot(planner, input.deps);
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,7 @@ export async function runAssistantMcpDiscoveryRuntimeEntryPoint(
|
||||||
|
|
||||||
const bridge = await runAssistantMcpDiscoveryRuntimeBridge({
|
const bridge = await runAssistantMcpDiscoveryRuntimeBridge({
|
||||||
semanticDataNeed: turnInput.semantic_data_need,
|
semanticDataNeed: turnInput.semantic_data_need,
|
||||||
|
dataNeedGraph: turnInput.data_need_graph,
|
||||||
turnMeaning: turnInput.turn_meaning_ref,
|
turnMeaning: turnInput.turn_meaning_ref,
|
||||||
deps: input.deps
|
deps: input.deps
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
import type { AssistantMcpDiscoveryTurnMeaningRef } from "./assistantMcpDiscoveryPolicy";
|
import type { AssistantMcpDiscoveryTurnMeaningRef } from "./assistantMcpDiscoveryPolicy";
|
||||||
|
import {
|
||||||
|
buildAssistantMcpDiscoveryDataNeedGraph,
|
||||||
|
type AssistantMcpDiscoveryDataNeedGraphContract
|
||||||
|
} from "./assistantMcpDiscoveryDataNeedGraph";
|
||||||
|
|
||||||
export const ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION =
|
export const ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION =
|
||||||
"assistant_mcp_discovery_turn_input_v1" as const;
|
"assistant_mcp_discovery_turn_input_v1" as const;
|
||||||
|
|
@ -25,6 +29,7 @@ export interface AssistantMcpDiscoveryTurnInputContract {
|
||||||
adapter_status: AssistantMcpDiscoveryTurnInputStatus;
|
adapter_status: AssistantMcpDiscoveryTurnInputStatus;
|
||||||
should_run_discovery: boolean;
|
should_run_discovery: boolean;
|
||||||
semantic_data_need: string | null;
|
semantic_data_need: string | null;
|
||||||
|
data_need_graph: AssistantMcpDiscoveryDataNeedGraphContract | null;
|
||||||
turn_meaning_ref: AssistantMcpDiscoveryTurnMeaningRef | null;
|
turn_meaning_ref: AssistantMcpDiscoveryTurnMeaningRef | null;
|
||||||
source_signal: AssistantMcpDiscoveryTurnInputSource;
|
source_signal: AssistantMcpDiscoveryTurnInputSource;
|
||||||
reason_codes: string[];
|
reason_codes: string[];
|
||||||
|
|
@ -114,6 +119,10 @@ function compactLower(value: unknown): string {
|
||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sameScopedName(left: string | null, right: string | null): boolean {
|
||||||
|
return Boolean(left && right && compactLower(left) === compactLower(right));
|
||||||
|
}
|
||||||
|
|
||||||
function candidateValue(value: unknown): string | null {
|
function candidateValue(value: unknown): string | null {
|
||||||
const direct = toNonEmptyString(value);
|
const direct = toNonEmptyString(value);
|
||||||
if (direct && direct !== "[object Object]") {
|
if (direct && direct !== "[object Object]") {
|
||||||
|
|
@ -407,6 +416,12 @@ function hasBidirectionalValueFlowSignal(text: string): boolean {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasValueRankingSignal(text: string): boolean {
|
||||||
|
return /(?:кто\s+больше\s+всего.*ден[её]г|больше\s+всего.*ден[её]г|прин[её]с.*ден[её]г|сам(?:ый|ая|ое|ые).*(?:доходн|прибыльн)|most.*money|highest\s+(?:revenue|payment))/iu.test(
|
||||||
|
text
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function hasMonthlyAggregationSignal(text: string): boolean {
|
function hasMonthlyAggregationSignal(text: string): boolean {
|
||||||
return /(?:\u043f\u043e\s+\u043c\u0435\u0441\u044f\u0446\u0430\u043c|\u043f\u043e\u043c\u0435\u0441\u044f\u0447\u043d\u043e|\u0435\u0436\u0435\u043c\u0435\u0441\u044f\u0447\u043d\u043e|month\s+by\s+month|by\s+month|monthly)/iu.test(
|
return /(?:\u043f\u043e\s+\u043c\u0435\u0441\u044f\u0446\u0430\u043c|\u043f\u043e\u043c\u0435\u0441\u044f\u0447\u043d\u043e|\u0435\u0436\u0435\u043c\u0435\u0441\u044f\u0447\u043d\u043e|month\s+by\s+month|by\s+month|monthly)/iu.test(
|
||||||
text
|
text
|
||||||
|
|
@ -741,7 +756,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
const rawLifecycleSignal = hasLifecycleSignal(rawText);
|
const rawLifecycleSignal = hasLifecycleSignal(rawText);
|
||||||
const rawBidirectionalValueFlowSignal = !rawLifecycleSignal && hasBidirectionalValueFlowSignal(rawText);
|
const rawBidirectionalValueFlowSignal = !rawLifecycleSignal && hasBidirectionalValueFlowSignal(rawText);
|
||||||
const rawValueFlowSignal =
|
const rawValueFlowSignal =
|
||||||
!rawLifecycleSignal && (hasValueFlowSignal(rawText) || rawBidirectionalValueFlowSignal);
|
!rawLifecycleSignal &&
|
||||||
|
(hasValueFlowSignal(rawText) || hasValueRankingSignal(rawText) || rawBidirectionalValueFlowSignal);
|
||||||
const rawMetadataSignal = !rawLifecycleSignal && !rawValueFlowSignal && hasMetadataSignal(rawText);
|
const rawMetadataSignal = !rawLifecycleSignal && !rawValueFlowSignal && hasMetadataSignal(rawText);
|
||||||
const rawEntityResolutionSignal =
|
const rawEntityResolutionSignal =
|
||||||
!rawLifecycleSignal && !rawValueFlowSignal && !rawMetadataSignal && hasEntityResolutionSignal(rawText);
|
!rawLifecycleSignal && !rawValueFlowSignal && !rawMetadataSignal && hasEntityResolutionSignal(rawText);
|
||||||
|
|
@ -773,6 +789,18 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate);
|
const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate);
|
||||||
const assistantTurnMeaningDateScope = toNonEmptyString(assistantTurnMeaning?.explicit_date_scope);
|
const assistantTurnMeaningDateScope = toNonEmptyString(assistantTurnMeaning?.explicit_date_scope);
|
||||||
const assistantTurnMeaningOrganizationScope = toNonEmptyString(assistantTurnMeaning?.explicit_organization_scope);
|
const assistantTurnMeaningOrganizationScope = toNonEmptyString(assistantTurnMeaning?.explicit_organization_scope);
|
||||||
|
const predecomposeOrganizationMirrorsCounterparty = sameScopedName(
|
||||||
|
predecomposeEntities.counterparty,
|
||||||
|
predecomposeEntities.organization
|
||||||
|
);
|
||||||
|
const organizationMirrorsPredecomposeCounterparty = Boolean(
|
||||||
|
(rawBidirectionalValueFlowSignal || hasValueRankingSignal(rawText)) &&
|
||||||
|
(sameScopedName(predecomposeEntities.counterparty, assistantTurnMeaningOrganizationScope) ||
|
||||||
|
predecomposeOrganizationMirrorsCounterparty)
|
||||||
|
);
|
||||||
|
const normalizedPredecomposeCounterparty = organizationMirrorsPredecomposeCounterparty
|
||||||
|
? null
|
||||||
|
: predecomposeEntities.counterparty;
|
||||||
const predecomposeDateScope = collectDateScope(predecomposeContract);
|
const predecomposeDateScope = collectDateScope(predecomposeContract);
|
||||||
const followupDiscoverySeedApplicable = Boolean(
|
const followupDiscoverySeedApplicable = Boolean(
|
||||||
followupSeed.domain &&
|
followupSeed.domain &&
|
||||||
|
|
@ -1038,7 +1066,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
for (const candidate of collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates)) {
|
for (const candidate of collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates)) {
|
||||||
pushNormalizedEntityResolutionCandidate(entityCandidates, candidate);
|
pushNormalizedEntityResolutionCandidate(entityCandidates, candidate);
|
||||||
}
|
}
|
||||||
pushNormalizedEntityResolutionCandidate(entityCandidates, predecomposeEntities.counterparty);
|
pushNormalizedEntityResolutionCandidate(entityCandidates, normalizedPredecomposeCounterparty);
|
||||||
pushNormalizedEntityResolutionCandidate(entityCandidates, followupSeed.counterparty);
|
pushNormalizedEntityResolutionCandidate(entityCandidates, followupSeed.counterparty);
|
||||||
} else {
|
} else {
|
||||||
if (groundedFollowupEntity) {
|
if (groundedFollowupEntity) {
|
||||||
|
|
@ -1047,7 +1075,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
for (const candidate of collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates)) {
|
for (const candidate of collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates)) {
|
||||||
pushScopedEntityCandidate(entityCandidates, candidate, groundedFollowupEntity);
|
pushScopedEntityCandidate(entityCandidates, candidate, groundedFollowupEntity);
|
||||||
}
|
}
|
||||||
pushScopedEntityCandidate(entityCandidates, predecomposeEntities.counterparty, groundedFollowupEntity);
|
pushScopedEntityCandidate(entityCandidates, normalizedPredecomposeCounterparty, groundedFollowupEntity);
|
||||||
if (!groundedFollowupEntity) {
|
if (!groundedFollowupEntity) {
|
||||||
pushScopedEntityCandidate(entityCandidates, followupSeed.counterparty, null);
|
pushScopedEntityCandidate(entityCandidates, followupSeed.counterparty, null);
|
||||||
pushScopedEntityCandidate(entityCandidates, followupSeed.discoveryEntity, null);
|
pushScopedEntityCandidate(entityCandidates, followupSeed.discoveryEntity, null);
|
||||||
|
|
@ -1058,14 +1086,26 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
pushUnique(entityCandidates, followupSeed.discoveryEntity);
|
pushUnique(entityCandidates, followupSeed.discoveryEntity);
|
||||||
pushUnique(entityCandidates, rawMetadataScopeHint);
|
pushUnique(entityCandidates, rawMetadataScopeHint);
|
||||||
}
|
}
|
||||||
if (valueFlowSignal && !predecomposeEntities.counterparty && !followupSeed.counterparty) {
|
const openScopeValueFlowWithoutCounterparty =
|
||||||
|
valueFlowSignal && !normalizedPredecomposeCounterparty && !followupSeed.counterparty;
|
||||||
|
const valueFlowOrganizationStaysScope =
|
||||||
|
openScopeValueFlowWithoutCounterparty &&
|
||||||
|
(bidirectionalValueFlowSignal || hasValueRankingSignal(rawText));
|
||||||
|
if (openScopeValueFlowWithoutCounterparty && !valueFlowOrganizationStaysScope) {
|
||||||
pushUnique(entityCandidates, predecomposeEntities.organization);
|
pushUnique(entityCandidates, predecomposeEntities.organization);
|
||||||
pushUnique(entityCandidates, followupSeed.organization);
|
pushUnique(entityCandidates, followupSeed.organization);
|
||||||
}
|
}
|
||||||
const explicitOrganizationScope =
|
const explicitOrganizationScope =
|
||||||
valueFlowSignal && !predecomposeEntities.counterparty && !followupSeed.counterparty
|
valueFlowOrganizationStaysScope || !openScopeValueFlowWithoutCounterparty
|
||||||
? null
|
? predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope ?? followupSeed.organization
|
||||||
: predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope ?? followupSeed.organization;
|
: null;
|
||||||
|
if (valueFlowOrganizationStaysScope && explicitOrganizationScope) {
|
||||||
|
for (let index = entityCandidates.length - 1; index >= 0; index -= 1) {
|
||||||
|
if (entityCandidates[index] === explicitOrganizationScope) {
|
||||||
|
entityCandidates.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const explicitDateScope = assistantTurnMeaningDateScope ?? predecomposeDateScope ?? rawDateScope ?? followupSeed.dateScope;
|
const explicitDateScope = assistantTurnMeaningDateScope ?? predecomposeDateScope ?? rawDateScope ?? followupSeed.dateScope;
|
||||||
|
|
||||||
const turnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {
|
const turnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {
|
||||||
|
|
@ -1312,7 +1352,10 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
if (unsupported) {
|
if (unsupported) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
|
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
|
||||||
}
|
}
|
||||||
if (predecomposeEntities.counterparty) {
|
if (
|
||||||
|
!(valueFlowOrganizationStaysScope && normalizedPredecomposeCounterparty === explicitOrganizationScope) &&
|
||||||
|
normalizedPredecomposeCounterparty
|
||||||
|
) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose");
|
pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose");
|
||||||
}
|
}
|
||||||
if (followupSeed.counterparty) {
|
if (followupSeed.counterparty) {
|
||||||
|
|
@ -1330,6 +1373,17 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
if (runDiscovery && !hasTurnMeaning) {
|
if (runDiscovery && !hasTurnMeaning) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_turn_meaning_missing");
|
pushReason(reasonCodes, "mcp_discovery_turn_meaning_missing");
|
||||||
}
|
}
|
||||||
|
const dataNeedGraph =
|
||||||
|
runDiscovery && hasTurnMeaning
|
||||||
|
? buildAssistantMcpDiscoveryDataNeedGraph({
|
||||||
|
semanticDataNeed,
|
||||||
|
rawUtterance: rawSignalSourceText,
|
||||||
|
turnMeaning: cleanTurnMeaning
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
if (dataNeedGraph) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_data_need_graph_built");
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
schema_version: ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION,
|
schema_version: ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION,
|
||||||
|
|
@ -1337,6 +1391,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
adapter_status: !runDiscovery ? "not_applicable" : hasTurnMeaning ? "ready" : "needs_more_context",
|
adapter_status: !runDiscovery ? "not_applicable" : hasTurnMeaning ? "ready" : "needs_more_context",
|
||||||
should_run_discovery: runDiscovery,
|
should_run_discovery: runDiscovery,
|
||||||
semantic_data_need: runDiscovery ? semanticDataNeed : null,
|
semantic_data_need: runDiscovery ? semanticDataNeed : null,
|
||||||
|
data_need_graph: dataNeedGraph,
|
||||||
turn_meaning_ref: runDiscovery && hasTurnMeaning ? cleanTurnMeaning : null,
|
turn_meaning_ref: runDiscovery && hasTurnMeaning ? cleanTurnMeaning : null,
|
||||||
source_signal: sourceSignal,
|
source_signal: sourceSignal,
|
||||||
reason_codes: reasonCodes
|
reason_codes: reasonCodes
|
||||||
|
|
|
||||||
|
|
@ -222,6 +222,74 @@ describe("assistant MCP discovery answer adapter", () => {
|
||||||
expect(draft.must_not_claim).toContain("Do not claim rows were checked when mcp_execution_performed=false.");
|
expect(draft.must_not_claim).toContain("Do not claim rows were checked when mcp_execution_performed=false.");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("asks for organization rather than counterparty when a ranked value-flow ask already has the period", async () => {
|
||||||
|
const planner = planAssistantMcpDiscovery({
|
||||||
|
dataNeedGraph: {
|
||||||
|
schema_version: "assistant_data_need_graph_v1",
|
||||||
|
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||||
|
subject_candidates: [],
|
||||||
|
business_fact_family: "value_flow",
|
||||||
|
action_family: "turnover",
|
||||||
|
aggregation_need: null,
|
||||||
|
time_scope_need: "explicit_period",
|
||||||
|
comparison_need: null,
|
||||||
|
ranking_need: "top_desc",
|
||||||
|
proof_expectation: "coverage_checked_fact",
|
||||||
|
clarification_gaps: [],
|
||||||
|
decomposition_candidates: ["collect_scoped_movements", "aggregate_ranked_axis_values", "probe_coverage"],
|
||||||
|
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
|
||||||
|
reason_codes: ["data_need_graph_built", "data_need_graph_ranking_top_desc"]
|
||||||
|
},
|
||||||
|
turnMeaning: {
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "turnover",
|
||||||
|
explicit_date_scope: "2020"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([]));
|
||||||
|
|
||||||
|
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||||
|
|
||||||
|
expect(draft.answer_mode).toBe("needs_clarification");
|
||||||
|
expect(draft.headline).toContain("ranking");
|
||||||
|
expect(draft.next_step_line).toContain("организацию");
|
||||||
|
expect(draft.next_step_line).not.toContain("Уточните контрагента");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("asks for organization rather than counterparty on open bidirectional comparison when only the period is known", async () => {
|
||||||
|
const planner = planAssistantMcpDiscovery({
|
||||||
|
dataNeedGraph: {
|
||||||
|
schema_version: "assistant_data_need_graph_v1",
|
||||||
|
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||||
|
subject_candidates: [],
|
||||||
|
business_fact_family: "value_flow",
|
||||||
|
action_family: "net_value_flow",
|
||||||
|
aggregation_need: null,
|
||||||
|
time_scope_need: "explicit_period",
|
||||||
|
comparison_need: "incoming_vs_outgoing",
|
||||||
|
ranking_need: null,
|
||||||
|
proof_expectation: "coverage_checked_fact",
|
||||||
|
clarification_gaps: [],
|
||||||
|
decomposition_candidates: ["collect_incoming_movements", "collect_outgoing_movements", "probe_coverage"],
|
||||||
|
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
|
||||||
|
reason_codes: ["data_need_graph_built", "data_need_graph_comparison_incoming_vs_outgoing"]
|
||||||
|
},
|
||||||
|
turnMeaning: {
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "net_value_flow",
|
||||||
|
explicit_date_scope: "2020"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([]));
|
||||||
|
|
||||||
|
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||||
|
|
||||||
|
expect(draft.answer_mode).toBe("needs_clarification");
|
||||||
|
expect(draft.headline).toContain("входящий и исходящий");
|
||||||
|
expect(draft.next_step_line).toContain("организацию");
|
||||||
|
expect(draft.next_step_line).not.toContain("Уточните контрагента");
|
||||||
|
});
|
||||||
|
|
||||||
it("asks for an explicit lane choice when mixed metadata ambiguity cannot continue on a neutral follow-up", async () => {
|
it("asks for an explicit lane choice when mixed metadata ambiguity cannot continue on a neutral follow-up", async () => {
|
||||||
const planner = planAssistantMcpDiscovery({
|
const planner = planAssistantMcpDiscovery({
|
||||||
turnMeaning: {
|
turnMeaning: {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { buildAssistantMcpDiscoveryDataNeedGraph } from "../src/services/assistantMcpDiscoveryDataNeedGraph";
|
||||||
|
|
||||||
|
describe("assistant MCP discovery data need graph", () => {
|
||||||
|
it("builds a monthly bidirectional value-flow graph from grounded turn meaning", () => {
|
||||||
|
const result = buildAssistantMcpDiscoveryDataNeedGraph({
|
||||||
|
semanticDataNeed: "counterparty value-flow evidence",
|
||||||
|
rawUtterance: "какое нетто по деньгам с SVK за 2020 год по месяцам",
|
||||||
|
turnMeaning: {
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "net_value_flow",
|
||||||
|
asked_aggregation_axis: "month",
|
||||||
|
explicit_entity_candidates: ["SVK"],
|
||||||
|
explicit_date_scope: "2020"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.business_fact_family).toBe("value_flow");
|
||||||
|
expect(result.action_family).toBe("net_value_flow");
|
||||||
|
expect(result.aggregation_need).toBe("by_month");
|
||||||
|
expect(result.time_scope_need).toBe("explicit_period");
|
||||||
|
expect(result.comparison_need).toBe("incoming_vs_outgoing");
|
||||||
|
expect(result.proof_expectation).toBe("coverage_checked_fact");
|
||||||
|
expect(result.clarification_gaps).toEqual([]);
|
||||||
|
expect(result.decomposition_candidates).toEqual([
|
||||||
|
"resolve_entity_reference",
|
||||||
|
"collect_incoming_movements",
|
||||||
|
"collect_outgoing_movements",
|
||||||
|
"aggregate_by_month",
|
||||||
|
"probe_coverage"
|
||||||
|
]);
|
||||||
|
expect(result.forbidden_overclaim_flags).toContain("no_unchecked_fact_totals");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("marks metadata lane choice as a clarification-required graph", () => {
|
||||||
|
const result = buildAssistantMcpDiscoveryDataNeedGraph({
|
||||||
|
semanticDataNeed: "metadata lane clarification",
|
||||||
|
rawUtterance: "давай дальше",
|
||||||
|
turnMeaning: {
|
||||||
|
asked_domain_family: "metadata",
|
||||||
|
asked_action_family: "resolve_next_lane",
|
||||||
|
explicit_entity_candidates: ["SVK"],
|
||||||
|
unsupported_but_understood_family: "metadata_lane_choice_clarification"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.business_fact_family).toBe("schema_surface");
|
||||||
|
expect(result.clarification_gaps).toEqual(["lane_family_choice"]);
|
||||||
|
expect(result.proof_expectation).toBe("clarification_required");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps entity search as an entity-grounding graph", () => {
|
||||||
|
const result = buildAssistantMcpDiscoveryDataNeedGraph({
|
||||||
|
semanticDataNeed: "entity discovery evidence",
|
||||||
|
rawUtterance: "найди в 1С контрагента Группа СВК",
|
||||||
|
turnMeaning: {
|
||||||
|
asked_domain_family: "entity_resolution",
|
||||||
|
asked_action_family: "search_business_entity",
|
||||||
|
explicit_entity_candidates: ["Группа СВК"]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.business_fact_family).toBe("entity_grounding");
|
||||||
|
expect(result.subject_candidates).toEqual(["Группа СВК"]);
|
||||||
|
expect(result.proof_expectation).toBe("entity_grounding");
|
||||||
|
expect(result.decomposition_candidates).toEqual([
|
||||||
|
"search_business_entity",
|
||||||
|
"resolve_entity_reference",
|
||||||
|
"probe_coverage"
|
||||||
|
]);
|
||||||
|
expect(result.forbidden_overclaim_flags).toContain("no_unresolved_entity_claim");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats top-value wording as a ranking ask rather than a missing-subject fact ask", () => {
|
||||||
|
const result = buildAssistantMcpDiscoveryDataNeedGraph({
|
||||||
|
semanticDataNeed: "counterparty value-flow evidence",
|
||||||
|
rawUtterance: "кто больше всего принес денег в 2020",
|
||||||
|
turnMeaning: {
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "turnover",
|
||||||
|
explicit_date_scope: "2020"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.business_fact_family).toBe("value_flow");
|
||||||
|
expect(result.ranking_need).toBe("top_desc");
|
||||||
|
expect(result.clarification_gaps).toEqual([]);
|
||||||
|
expect(result.decomposition_candidates).toEqual([
|
||||||
|
"collect_scoped_movements",
|
||||||
|
"aggregate_ranked_axis_values",
|
||||||
|
"probe_coverage"
|
||||||
|
]);
|
||||||
|
expect(result.reason_codes).toContain("data_need_graph_ranking_top_desc");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats incoming-vs-outgoing comparison as an open-scope value need rather than a missing-subject fact ask", () => {
|
||||||
|
const result = buildAssistantMcpDiscoveryDataNeedGraph({
|
||||||
|
semanticDataNeed: "counterparty value-flow evidence",
|
||||||
|
rawUtterance: "что больше: входящие или исходящие деньги за 2020 год?",
|
||||||
|
turnMeaning: {
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "net_value_flow",
|
||||||
|
explicit_date_scope: "2020"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.business_fact_family).toBe("value_flow");
|
||||||
|
expect(result.comparison_need).toBe("incoming_vs_outgoing");
|
||||||
|
expect(result.clarification_gaps).toEqual([]);
|
||||||
|
expect(result.decomposition_candidates).toEqual([
|
||||||
|
"collect_incoming_movements",
|
||||||
|
"collect_outgoing_movements",
|
||||||
|
"probe_coverage"
|
||||||
|
]);
|
||||||
|
expect(result.reason_codes).toContain("data_need_graph_comparison_incoming_vs_outgoing");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -4,6 +4,27 @@ import { planAssistantMcpDiscovery } from "../src/services/assistantMcpDiscovery
|
||||||
describe("assistant MCP discovery planner", () => {
|
describe("assistant MCP discovery planner", () => {
|
||||||
it("builds a catalog-compatible value-flow discovery plan from current turn meaning", () => {
|
it("builds a catalog-compatible value-flow discovery plan from current turn meaning", () => {
|
||||||
const result = planAssistantMcpDiscovery({
|
const result = planAssistantMcpDiscovery({
|
||||||
|
dataNeedGraph: {
|
||||||
|
schema_version: "assistant_data_need_graph_v1",
|
||||||
|
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||||
|
subject_candidates: ["SVK"],
|
||||||
|
business_fact_family: "value_flow",
|
||||||
|
action_family: "turnover",
|
||||||
|
aggregation_need: null,
|
||||||
|
time_scope_need: "explicit_period",
|
||||||
|
comparison_need: null,
|
||||||
|
ranking_need: null,
|
||||||
|
proof_expectation: "coverage_checked_fact",
|
||||||
|
clarification_gaps: [],
|
||||||
|
decomposition_candidates: [
|
||||||
|
"resolve_entity_reference",
|
||||||
|
"collect_scoped_movements",
|
||||||
|
"aggregate_checked_amounts",
|
||||||
|
"probe_coverage"
|
||||||
|
],
|
||||||
|
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
|
||||||
|
reason_codes: ["data_need_graph_built"]
|
||||||
|
},
|
||||||
turnMeaning: {
|
turnMeaning: {
|
||||||
asked_domain_family: "counterparty_value",
|
asked_domain_family: "counterparty_value",
|
||||||
asked_action_family: "turnover",
|
asked_action_family: "turnover",
|
||||||
|
|
@ -25,8 +46,10 @@ describe("assistant MCP discovery planner", () => {
|
||||||
expect(result.required_axes).toEqual(["counterparty", "period", "aggregate_axis", "amount", "coverage_target"]);
|
expect(result.required_axes).toEqual(["counterparty", "period", "aggregate_axis", "amount", "coverage_target"]);
|
||||||
expect(result.catalog_review.review_status).toBe("catalog_compatible");
|
expect(result.catalog_review.review_status).toBe("catalog_compatible");
|
||||||
expect(result.discovery_plan.answer_may_use_raw_model_claims).toBe(false);
|
expect(result.discovery_plan.answer_may_use_raw_model_claims).toBe(false);
|
||||||
|
expect(result.data_need_graph?.business_fact_family).toBe("value_flow");
|
||||||
expect(result.discovery_plan.execution_budget.max_probe_count).toBe(30);
|
expect(result.discovery_plan.execution_budget.max_probe_count).toBe(30);
|
||||||
expect(result.reason_codes).toContain("planner_enabled_chunked_coverage_probe_budget");
|
expect(result.reason_codes).toContain("planner_enabled_chunked_coverage_probe_budget");
|
||||||
|
expect(result.reason_codes).toContain("planner_consumed_data_need_graph_v1");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps a value-flow plan in clarification state when period axis is missing", () => {
|
it("keeps a value-flow plan in clarification state when period axis is missing", () => {
|
||||||
|
|
@ -91,6 +114,22 @@ describe("assistant MCP discovery planner", () => {
|
||||||
|
|
||||||
it("builds a movement discovery plan without aggregating value-flow totals", () => {
|
it("builds a movement discovery plan without aggregating value-flow totals", () => {
|
||||||
const result = planAssistantMcpDiscovery({
|
const result = planAssistantMcpDiscovery({
|
||||||
|
dataNeedGraph: {
|
||||||
|
schema_version: "assistant_data_need_graph_v1",
|
||||||
|
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||||
|
subject_candidates: ["SVK"],
|
||||||
|
business_fact_family: "movement_evidence",
|
||||||
|
action_family: "list_movements",
|
||||||
|
aggregation_need: null,
|
||||||
|
time_scope_need: "explicit_period",
|
||||||
|
comparison_need: null,
|
||||||
|
ranking_need: null,
|
||||||
|
proof_expectation: "coverage_checked_fact",
|
||||||
|
clarification_gaps: [],
|
||||||
|
decomposition_candidates: ["resolve_entity_reference", "fetch_scoped_movements", "probe_coverage"],
|
||||||
|
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
|
||||||
|
reason_codes: ["data_need_graph_built"]
|
||||||
|
},
|
||||||
turnMeaning: {
|
turnMeaning: {
|
||||||
asked_domain_family: "movements",
|
asked_domain_family: "movements",
|
||||||
asked_action_family: "list_movements",
|
asked_action_family: "list_movements",
|
||||||
|
|
@ -107,7 +146,192 @@ describe("assistant MCP discovery planner", () => {
|
||||||
expect(result.proposed_primitives).toEqual(["resolve_entity_reference", "query_movements", "probe_coverage"]);
|
expect(result.proposed_primitives).toEqual(["resolve_entity_reference", "query_movements", "probe_coverage"]);
|
||||||
expect(result.proposed_primitives).not.toContain("aggregate_by_axis");
|
expect(result.proposed_primitives).not.toContain("aggregate_by_axis");
|
||||||
expect(result.required_axes).toEqual(["counterparty", "period", "coverage_target"]);
|
expect(result.required_axes).toEqual(["counterparty", "period", "coverage_target"]);
|
||||||
expect(result.reason_codes).toContain("planner_selected_movement_recipe");
|
expect(result.reason_codes).toContain("planner_selected_movement_from_data_need_graph");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can select value-flow chain from data need graph even when turn meaning family is still under-specified", () => {
|
||||||
|
const result = planAssistantMcpDiscovery({
|
||||||
|
dataNeedGraph: {
|
||||||
|
schema_version: "assistant_data_need_graph_v1",
|
||||||
|
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||||
|
subject_candidates: ["SVK"],
|
||||||
|
business_fact_family: "value_flow",
|
||||||
|
action_family: "net_value_flow",
|
||||||
|
aggregation_need: "by_month",
|
||||||
|
time_scope_need: "explicit_period",
|
||||||
|
comparison_need: "incoming_vs_outgoing",
|
||||||
|
ranking_need: null,
|
||||||
|
proof_expectation: "coverage_checked_fact",
|
||||||
|
clarification_gaps: [],
|
||||||
|
decomposition_candidates: [
|
||||||
|
"resolve_entity_reference",
|
||||||
|
"collect_incoming_movements",
|
||||||
|
"collect_outgoing_movements",
|
||||||
|
"aggregate_by_month",
|
||||||
|
"probe_coverage"
|
||||||
|
],
|
||||||
|
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
|
||||||
|
reason_codes: ["data_need_graph_built"]
|
||||||
|
},
|
||||||
|
turnMeaning: {
|
||||||
|
explicit_entity_candidates: ["SVK"],
|
||||||
|
explicit_date_scope: "2020",
|
||||||
|
asked_aggregation_axis: "month"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.planner_status).toBe("ready_for_execution");
|
||||||
|
expect(result.selected_chain_id).toBe("value_flow");
|
||||||
|
expect(result.proposed_primitives).toEqual([
|
||||||
|
"resolve_entity_reference",
|
||||||
|
"query_movements",
|
||||||
|
"aggregate_by_axis",
|
||||||
|
"probe_coverage"
|
||||||
|
]);
|
||||||
|
expect(result.required_axes).toEqual([
|
||||||
|
"counterparty",
|
||||||
|
"period",
|
||||||
|
"aggregate_axis",
|
||||||
|
"amount",
|
||||||
|
"coverage_target",
|
||||||
|
"calendar_month"
|
||||||
|
]);
|
||||||
|
expect(result.reason_codes).toContain("planner_selected_monthly_value_flow_from_data_need_graph");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not collapse a ranking-shaped value graph into entity-resolution just because no subject is preselected", () => {
|
||||||
|
const result = planAssistantMcpDiscovery({
|
||||||
|
dataNeedGraph: {
|
||||||
|
schema_version: "assistant_data_need_graph_v1",
|
||||||
|
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||||
|
subject_candidates: [],
|
||||||
|
business_fact_family: "value_flow",
|
||||||
|
action_family: "turnover",
|
||||||
|
aggregation_need: null,
|
||||||
|
time_scope_need: "explicit_period",
|
||||||
|
comparison_need: null,
|
||||||
|
ranking_need: "top_desc",
|
||||||
|
proof_expectation: "coverage_checked_fact",
|
||||||
|
clarification_gaps: [],
|
||||||
|
decomposition_candidates: ["collect_scoped_movements", "aggregate_ranked_axis_values", "probe_coverage"],
|
||||||
|
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
|
||||||
|
reason_codes: ["data_need_graph_built", "data_need_graph_ranking_top_desc"]
|
||||||
|
},
|
||||||
|
turnMeaning: {
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "turnover",
|
||||||
|
explicit_date_scope: "2020"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.planner_status).toBe("needs_clarification");
|
||||||
|
expect(result.selected_chain_id).toBe("value_flow_ranking");
|
||||||
|
expect(result.proposed_primitives).toEqual(["query_movements", "aggregate_by_axis", "probe_coverage"]);
|
||||||
|
expect(result.required_axes).toEqual(["period", "aggregate_axis", "amount", "coverage_target"]);
|
||||||
|
expect(result.catalog_review.review_status).toBe("needs_more_axes");
|
||||||
|
expect(result.reason_codes).toContain("planner_selected_top_ranked_value_flow_from_data_need_graph");
|
||||||
|
expect(result.selected_chain_id).not.toBe("entity_resolution");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps ranked value-flow ready for execution once checked period and organization are known", () => {
|
||||||
|
const result = planAssistantMcpDiscovery({
|
||||||
|
dataNeedGraph: {
|
||||||
|
schema_version: "assistant_data_need_graph_v1",
|
||||||
|
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||||
|
subject_candidates: [],
|
||||||
|
business_fact_family: "value_flow",
|
||||||
|
action_family: "turnover",
|
||||||
|
aggregation_need: null,
|
||||||
|
time_scope_need: "explicit_period",
|
||||||
|
comparison_need: null,
|
||||||
|
ranking_need: "top_desc",
|
||||||
|
proof_expectation: "coverage_checked_fact",
|
||||||
|
clarification_gaps: [],
|
||||||
|
decomposition_candidates: ["collect_scoped_movements", "aggregate_ranked_axis_values", "probe_coverage"],
|
||||||
|
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
|
||||||
|
reason_codes: ["data_need_graph_built", "data_need_graph_ranking_top_desc"]
|
||||||
|
},
|
||||||
|
turnMeaning: {
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "turnover",
|
||||||
|
explicit_date_scope: "2020",
|
||||||
|
explicit_organization_scope: "ООО Альтернатива Плюс"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.planner_status).toBe("ready_for_execution");
|
||||||
|
expect(result.selected_chain_id).toBe("value_flow_ranking");
|
||||||
|
expect(result.proposed_primitives).toEqual(["query_movements", "aggregate_by_axis", "probe_coverage"]);
|
||||||
|
expect(result.required_axes).toEqual(["organization", "period", "aggregate_axis", "amount", "coverage_target"]);
|
||||||
|
expect(result.catalog_review.review_status).toBe("catalog_compatible");
|
||||||
|
expect(result.reason_codes).toContain("planner_selected_top_ranked_value_flow_from_data_need_graph");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not collapse incoming-vs-outgoing comparison into entity-resolution when no counterparty is preselected", () => {
|
||||||
|
const result = planAssistantMcpDiscovery({
|
||||||
|
dataNeedGraph: {
|
||||||
|
schema_version: "assistant_data_need_graph_v1",
|
||||||
|
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||||
|
subject_candidates: [],
|
||||||
|
business_fact_family: "value_flow",
|
||||||
|
action_family: "net_value_flow",
|
||||||
|
aggregation_need: null,
|
||||||
|
time_scope_need: "explicit_period",
|
||||||
|
comparison_need: "incoming_vs_outgoing",
|
||||||
|
ranking_need: null,
|
||||||
|
proof_expectation: "coverage_checked_fact",
|
||||||
|
clarification_gaps: [],
|
||||||
|
decomposition_candidates: ["collect_incoming_movements", "collect_outgoing_movements", "probe_coverage"],
|
||||||
|
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
|
||||||
|
reason_codes: ["data_need_graph_built", "data_need_graph_comparison_incoming_vs_outgoing"]
|
||||||
|
},
|
||||||
|
turnMeaning: {
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "net_value_flow",
|
||||||
|
explicit_date_scope: "2020"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.planner_status).toBe("needs_clarification");
|
||||||
|
expect(result.selected_chain_id).toBe("value_flow_comparison");
|
||||||
|
expect(result.proposed_primitives).toEqual(["query_movements", "probe_coverage"]);
|
||||||
|
expect(result.required_axes).toEqual(["period", "amount", "coverage_target"]);
|
||||||
|
expect(result.reason_codes).toContain("planner_selected_bidirectional_value_flow_comparison_from_data_need_graph");
|
||||||
|
expect(result.selected_chain_id).not.toBe("entity_resolution");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps bidirectional comparison ready for execution once checked period and organization are known", () => {
|
||||||
|
const result = planAssistantMcpDiscovery({
|
||||||
|
dataNeedGraph: {
|
||||||
|
schema_version: "assistant_data_need_graph_v1",
|
||||||
|
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||||
|
subject_candidates: [],
|
||||||
|
business_fact_family: "value_flow",
|
||||||
|
action_family: "net_value_flow",
|
||||||
|
aggregation_need: null,
|
||||||
|
time_scope_need: "explicit_period",
|
||||||
|
comparison_need: "incoming_vs_outgoing",
|
||||||
|
ranking_need: null,
|
||||||
|
proof_expectation: "coverage_checked_fact",
|
||||||
|
clarification_gaps: [],
|
||||||
|
decomposition_candidates: ["collect_incoming_movements", "collect_outgoing_movements", "probe_coverage"],
|
||||||
|
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
|
||||||
|
reason_codes: ["data_need_graph_built", "data_need_graph_comparison_incoming_vs_outgoing"]
|
||||||
|
},
|
||||||
|
turnMeaning: {
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "net_value_flow",
|
||||||
|
explicit_date_scope: "2020",
|
||||||
|
explicit_organization_scope: "ООО Альтернатива Плюс"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.planner_status).toBe("ready_for_execution");
|
||||||
|
expect(result.selected_chain_id).toBe("value_flow_comparison");
|
||||||
|
expect(result.proposed_primitives).toEqual(["query_movements", "probe_coverage"]);
|
||||||
|
expect(result.required_axes).toEqual(["organization", "period", "amount", "coverage_target"]);
|
||||||
|
expect(result.catalog_review.review_status).toBe("catalog_compatible");
|
||||||
|
expect(result.reason_codes).toContain("planner_selected_bidirectional_value_flow_comparison_from_data_need_graph");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds an inference-safe lifecycle plan with evidence explanation", () => {
|
it("builds an inference-safe lifecycle plan with evidence explanation", () => {
|
||||||
|
|
|
||||||
|
|
@ -397,4 +397,41 @@ describe("assistant MCP discovery response candidate", () => {
|
||||||
expect(candidate.reply_text).toBeNull();
|
expect(candidate.reply_text).toBeNull();
|
||||||
expect(candidate.eligible_for_future_hot_runtime).toBe(false);
|
expect(candidate.eligible_for_future_hot_runtime).toBe(false);
|
||||||
});
|
});
|
||||||
|
it("localizes open-scope bidirectional comparison scope and probe-limit wording without contour garbage", () => {
|
||||||
|
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||||
|
entryPoint({
|
||||||
|
bridge: {
|
||||||
|
bridge_status: "answer_draft_ready",
|
||||||
|
user_facing_response_allowed: true,
|
||||||
|
business_fact_answer_allowed: true,
|
||||||
|
requires_user_clarification: false,
|
||||||
|
answer_draft: {
|
||||||
|
answer_mode: "confirmed_with_bounded_inference",
|
||||||
|
headline:
|
||||||
|
"\u041f\u043e \u0434\u0430\u043d\u043d\u044b\u043c 1\u0421 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0441\u0442\u0440\u043e\u043a\u0438 \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0438 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0445 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0439; \u043d\u0435\u0442\u0442\u043e \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u0430\u043a \u0440\u0430\u0441\u0447\u0435\u0442 \u043f\u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u043c \u0441\u0442\u0440\u043e\u043a\u0430\u043c \u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u043d\u043d\u043e\u043c\u0443 \u043f\u0435\u0440\u0438\u043e\u0434\u0443.",
|
||||||
|
confirmed_lines: [
|
||||||
|
"1C bidirectional value-flow rows were checked for the requested counterparty scope: incoming=found, outgoing=found"
|
||||||
|
],
|
||||||
|
inference_lines: [],
|
||||||
|
unknown_lines: [],
|
||||||
|
limitation_lines: [
|
||||||
|
"Requested period hit the MCP row limit, but the approved monthly recovery probe budget is smaller than the required subperiod count"
|
||||||
|
],
|
||||||
|
next_step_line: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(candidate.reply_text).toContain(
|
||||||
|
"\u0412 1\u0421 \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u043d\u044b \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0438 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0435 \u0441\u0442\u0440\u043e\u043a\u0438 \u0432 \u0437\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043d\u043e\u043c \u0441\u0440\u0435\u0437\u0435"
|
||||||
|
);
|
||||||
|
expect(candidate.reply_text).toContain(
|
||||||
|
"\u0417\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043d\u044b\u0439 \u043f\u0435\u0440\u0438\u043e\u0434 \u0443\u043f\u0435\u0440\u0441\u044f \u0432 \u043b\u0438\u043c\u0438\u0442 \u0441\u0442\u0440\u043e\u043a MCP"
|
||||||
|
);
|
||||||
|
expect(candidate.reply_text).not.toContain(
|
||||||
|
"\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0441\u043a\u043e\u043c\u0443 \u043a\u043e\u043d\u0442\u0443\u0440\u0443"
|
||||||
|
);
|
||||||
|
expect(candidate.reply_text).not.toContain("Requested period hit the MCP row limit");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,30 @@ function buildDeps(rows: Array<Record<string, unknown>>, error: string | null =
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildBidirectionalDeps(
|
||||||
|
incomingRows: Array<Record<string, unknown>>,
|
||||||
|
outgoingRows: Array<Record<string, unknown>>
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
executeAddressMcpQuery: vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
fetched_rows: incomingRows.length,
|
||||||
|
matched_rows: incomingRows.length,
|
||||||
|
raw_rows: incomingRows,
|
||||||
|
rows: incomingRows,
|
||||||
|
error: null
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
fetched_rows: outgoingRows.length,
|
||||||
|
matched_rows: outgoingRows.length,
|
||||||
|
raw_rows: outgoingRows,
|
||||||
|
rows: outgoingRows,
|
||||||
|
error: null
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
describe("assistant MCP discovery runtime bridge", () => {
|
describe("assistant MCP discovery runtime bridge", () => {
|
||||||
it("composes planner, pilot executor, and answer draft without wiring the hot runtime", async () => {
|
it("composes planner, pilot executor, and answer draft without wiring the hot runtime", async () => {
|
||||||
const result = await runAssistantMcpDiscoveryRuntimeBridge({
|
const result = await runAssistantMcpDiscoveryRuntimeBridge({
|
||||||
|
|
@ -51,6 +75,166 @@ describe("assistant MCP discovery runtime bridge", () => {
|
||||||
expect(result.answer_draft.next_step_line).toContain("Уточните контрагента");
|
expect(result.answer_draft.next_step_line).toContain("Уточните контрагента");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps ranked value-flow in clarification without asking for a counterparty when only the period is known", async () => {
|
||||||
|
const result = await runAssistantMcpDiscoveryRuntimeBridge({
|
||||||
|
dataNeedGraph: {
|
||||||
|
schema_version: "assistant_data_need_graph_v1",
|
||||||
|
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||||
|
subject_candidates: [],
|
||||||
|
business_fact_family: "value_flow",
|
||||||
|
action_family: "turnover",
|
||||||
|
aggregation_need: null,
|
||||||
|
time_scope_need: "explicit_period",
|
||||||
|
comparison_need: null,
|
||||||
|
ranking_need: "top_desc",
|
||||||
|
proof_expectation: "coverage_checked_fact",
|
||||||
|
clarification_gaps: [],
|
||||||
|
decomposition_candidates: ["collect_scoped_movements", "aggregate_ranked_axis_values", "probe_coverage"],
|
||||||
|
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
|
||||||
|
reason_codes: ["data_need_graph_built", "data_need_graph_ranking_top_desc"]
|
||||||
|
},
|
||||||
|
turnMeaning: {
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "turnover",
|
||||||
|
explicit_date_scope: "2020"
|
||||||
|
},
|
||||||
|
deps: buildDeps([])
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.bridge_status).toBe("needs_clarification");
|
||||||
|
expect(result.requires_user_clarification).toBe(true);
|
||||||
|
expect(result.pilot.mcp_execution_performed).toBe(false);
|
||||||
|
expect(result.planner.selected_chain_id).toBe("value_flow_ranking");
|
||||||
|
expect(result.answer_draft.headline).toContain("ranking");
|
||||||
|
expect(result.answer_draft.next_step_line).toContain("организацию");
|
||||||
|
expect(result.answer_draft.next_step_line).not.toContain("Уточните контрагента");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("produces a bounded ranked value-flow answer when period and organization are known", async () => {
|
||||||
|
const result = await runAssistantMcpDiscoveryRuntimeBridge({
|
||||||
|
dataNeedGraph: {
|
||||||
|
schema_version: "assistant_data_need_graph_v1",
|
||||||
|
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||||
|
subject_candidates: [],
|
||||||
|
business_fact_family: "value_flow",
|
||||||
|
action_family: "turnover",
|
||||||
|
aggregation_need: null,
|
||||||
|
time_scope_need: "explicit_period",
|
||||||
|
comparison_need: null,
|
||||||
|
ranking_need: "top_desc",
|
||||||
|
proof_expectation: "coverage_checked_fact",
|
||||||
|
clarification_gaps: [],
|
||||||
|
decomposition_candidates: ["collect_scoped_movements", "aggregate_ranked_axis_values", "probe_coverage"],
|
||||||
|
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
|
||||||
|
reason_codes: ["data_need_graph_built", "data_need_graph_ranking_top_desc"]
|
||||||
|
},
|
||||||
|
turnMeaning: {
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "turnover",
|
||||||
|
explicit_date_scope: "2020",
|
||||||
|
explicit_organization_scope: "ООО Альтернатива Плюс"
|
||||||
|
},
|
||||||
|
deps: buildDeps([
|
||||||
|
{ Period: "2020-01-10T00:00:00", Amount: 1200, Counterparty: "СВК-А" },
|
||||||
|
{ Period: "2020-03-11T00:00:00", Amount: 800, Counterparty: "СВК-Б" },
|
||||||
|
{ Period: "2020-05-12T00:00:00", Amount: 900, Counterparty: "СВК-А" }
|
||||||
|
])
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.bridge_status).toBe("answer_draft_ready");
|
||||||
|
expect(result.business_fact_answer_allowed).toBe(true);
|
||||||
|
expect(result.planner.selected_chain_id).toBe("value_flow_ranking");
|
||||||
|
expect(result.pilot.derived_ranked_value_flow?.ranked_values[0]).toMatchObject({
|
||||||
|
axis_value: "СВК-А",
|
||||||
|
total_amount: 2100
|
||||||
|
});
|
||||||
|
expect(result.answer_draft.confirmed_lines.join("\n")).toContain("СВК-А");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps open bidirectional comparison in clarification without asking for a counterparty when only the period is known", async () => {
|
||||||
|
const result = await runAssistantMcpDiscoveryRuntimeBridge({
|
||||||
|
dataNeedGraph: {
|
||||||
|
schema_version: "assistant_data_need_graph_v1",
|
||||||
|
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||||
|
subject_candidates: [],
|
||||||
|
business_fact_family: "value_flow",
|
||||||
|
action_family: "net_value_flow",
|
||||||
|
aggregation_need: null,
|
||||||
|
time_scope_need: "explicit_period",
|
||||||
|
comparison_need: "incoming_vs_outgoing",
|
||||||
|
ranking_need: null,
|
||||||
|
proof_expectation: "coverage_checked_fact",
|
||||||
|
clarification_gaps: [],
|
||||||
|
decomposition_candidates: ["collect_incoming_movements", "collect_outgoing_movements", "probe_coverage"],
|
||||||
|
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
|
||||||
|
reason_codes: ["data_need_graph_built", "data_need_graph_comparison_incoming_vs_outgoing"]
|
||||||
|
},
|
||||||
|
turnMeaning: {
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "net_value_flow",
|
||||||
|
explicit_date_scope: "2020"
|
||||||
|
},
|
||||||
|
deps: buildDeps([])
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.bridge_status).toBe("needs_clarification");
|
||||||
|
expect(result.requires_user_clarification).toBe(true);
|
||||||
|
expect(result.pilot.mcp_execution_performed).toBe(false);
|
||||||
|
expect(result.planner.selected_chain_id).toBe("value_flow_comparison");
|
||||||
|
expect(result.answer_draft.headline).toContain("входящий и исходящий");
|
||||||
|
expect(result.answer_draft.next_step_line).toContain("организацию");
|
||||||
|
expect(result.answer_draft.next_step_line).not.toContain("Уточните контрагента");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("produces a bounded bidirectional comparison answer when period and organization are known", async () => {
|
||||||
|
const result = await runAssistantMcpDiscoveryRuntimeBridge({
|
||||||
|
dataNeedGraph: {
|
||||||
|
schema_version: "assistant_data_need_graph_v1",
|
||||||
|
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||||
|
subject_candidates: [],
|
||||||
|
business_fact_family: "value_flow",
|
||||||
|
action_family: "net_value_flow",
|
||||||
|
aggregation_need: null,
|
||||||
|
time_scope_need: "explicit_period",
|
||||||
|
comparison_need: "incoming_vs_outgoing",
|
||||||
|
ranking_need: null,
|
||||||
|
proof_expectation: "coverage_checked_fact",
|
||||||
|
clarification_gaps: [],
|
||||||
|
decomposition_candidates: ["collect_incoming_movements", "collect_outgoing_movements", "probe_coverage"],
|
||||||
|
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
|
||||||
|
reason_codes: ["data_need_graph_built", "data_need_graph_comparison_incoming_vs_outgoing"]
|
||||||
|
},
|
||||||
|
turnMeaning: {
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "net_value_flow",
|
||||||
|
explicit_date_scope: "2020",
|
||||||
|
explicit_organization_scope: "ООО Альтернатива Плюс"
|
||||||
|
},
|
||||||
|
deps: buildBidirectionalDeps(
|
||||||
|
[
|
||||||
|
{ Period: "2020-01-10T00:00:00", Amount: 3200, Counterparty: "СВК-А" },
|
||||||
|
{ Period: "2020-04-11T00:00:00", Amount: 1800, Counterparty: "СВК-Б" }
|
||||||
|
],
|
||||||
|
[{ Period: "2020-02-12T00:00:00", Amount: 1400, Counterparty: "СВК-А" }]
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.bridge_status).toBe("answer_draft_ready");
|
||||||
|
expect(result.business_fact_answer_allowed).toBe(true);
|
||||||
|
expect(result.planner.selected_chain_id).toBe("value_flow_comparison");
|
||||||
|
expect(result.pilot.derived_bidirectional_value_flow).toMatchObject({
|
||||||
|
period_scope: "2020",
|
||||||
|
incoming_customer_revenue: {
|
||||||
|
total_amount: 5000
|
||||||
|
},
|
||||||
|
outgoing_supplier_payout: {
|
||||||
|
total_amount: 1400
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(result.answer_draft.confirmed_lines.join("\n")).toContain("получили");
|
||||||
|
expect(result.answer_draft.confirmed_lines.join("\n")).toContain("заплатили");
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps document-ready plans bounded when the pilot finds no confirmed rows", async () => {
|
it("keeps document-ready plans bounded when the pilot finds no confirmed rows", async () => {
|
||||||
const result = await runAssistantMcpDiscoveryRuntimeBridge({
|
const result = await runAssistantMcpDiscoveryRuntimeBridge({
|
||||||
turnMeaning: {
|
turnMeaning: {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,30 @@ function buildDeps(rows: Array<Record<string, unknown>>, error: string | null =
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildBidirectionalDeps(
|
||||||
|
incomingRows: Array<Record<string, unknown>>,
|
||||||
|
outgoingRows: Array<Record<string, unknown>>
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
executeAddressMcpQuery: vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
fetched_rows: incomingRows.length,
|
||||||
|
matched_rows: incomingRows.length,
|
||||||
|
raw_rows: incomingRows,
|
||||||
|
rows: incomingRows,
|
||||||
|
error: null
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
fetched_rows: outgoingRows.length,
|
||||||
|
matched_rows: outgoingRows.length,
|
||||||
|
raw_rows: outgoingRows,
|
||||||
|
rows: outgoingRows,
|
||||||
|
error: null
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function buildMetadataDeps(rows: Array<Record<string, unknown>>, error: string | null = null) {
|
function buildMetadataDeps(rows: Array<Record<string, unknown>>, error: string | null = null) {
|
||||||
return {
|
return {
|
||||||
executeAddressMcpMetadata: vi.fn(async () => ({
|
executeAddressMcpMetadata: vi.fn(async () => ({
|
||||||
|
|
@ -240,4 +264,93 @@ describe("assistant MCP discovery runtime entry point", () => {
|
||||||
expect(result.bridge?.pilot.pilot_scope).toBe("counterparty_document_evidence_query_documents_v1");
|
expect(result.bridge?.pilot.pilot_scope).toBe("counterparty_document_evidence_query_documents_v1");
|
||||||
expect(result.bridge?.answer_draft.answer_mode).toBe("confirmed_with_bounded_inference");
|
expect(result.bridge?.answer_draft.answer_mode).toBe("confirmed_with_bounded_inference");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("runs raw incoming-vs-outgoing comparison as an open-scope value-flow chain without inventing a counterparty", async () => {
|
||||||
|
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
|
||||||
|
userMessage: "что больше: входящие или исходящие деньги за 2020 год по ООО Альтернатива Плюс?",
|
||||||
|
predecomposeContract: {
|
||||||
|
entities: { organization: "ООО Альтернатива Плюс" },
|
||||||
|
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
|
||||||
|
},
|
||||||
|
deps: buildBidirectionalDeps(
|
||||||
|
[
|
||||||
|
{ Period: "2020-01-15T00:00:00", Amount: 2500, Counterparty: "Клиент-А" },
|
||||||
|
{ Period: "2020-06-20T00:00:00", Amount: 1000, Counterparty: "Клиент-Б" }
|
||||||
|
],
|
||||||
|
[{ Period: "2020-02-18T00:00:00", Amount: 900, Counterparty: "Поставщик-А" }]
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.entry_status).toBe("bridge_executed");
|
||||||
|
expect(result.discovery_attempted).toBe(true);
|
||||||
|
expect(result.turn_input.data_need_graph?.comparison_need).toBe("incoming_vs_outgoing");
|
||||||
|
expect(result.turn_input.turn_meaning_ref).toMatchObject({
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "net_value_flow",
|
||||||
|
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||||
|
explicit_date_scope: "2020"
|
||||||
|
});
|
||||||
|
expect(result.turn_input.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
|
||||||
|
expect(result.bridge?.planner.selected_chain_id).toBe("value_flow_comparison");
|
||||||
|
expect(result.bridge?.pilot.pilot_scope).toBe("counterparty_bidirectional_value_flow_query_movements_v1");
|
||||||
|
expect(result.bridge?.answer_draft.confirmed_lines.join("\n")).toContain("получили");
|
||||||
|
expect(result.bridge?.answer_draft.confirmed_lines.join("\n")).toContain("заплатили");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skip("keeps mirrored predecompose organization and counterparty out of the subject lane for open comparison", async () => {
|
||||||
|
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
|
||||||
|
userMessage: "что больше: входящие или исходящие деньги за 2020 год по ООО Альтернатива Плюс?",
|
||||||
|
predecomposeContract: {
|
||||||
|
entities: {
|
||||||
|
counterparty: "ООО Альтернатива Плюс",
|
||||||
|
organization: "ООО Альтернатива Плюс"
|
||||||
|
},
|
||||||
|
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
|
||||||
|
},
|
||||||
|
deps: buildBidirectionalDeps(
|
||||||
|
[{ Period: "2020-01-15T00:00:00", Amount: 2500, Counterparty: "Клиент-А" }],
|
||||||
|
[{ Period: "2020-02-18T00:00:00", Amount: 900, Counterparty: "Поставщик-А" }]
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.entry_status).toBe("bridge_executed");
|
||||||
|
expect(result.turn_input.turn_meaning_ref).toMatchObject({
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "net_value_flow",
|
||||||
|
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||||
|
explicit_date_scope: "2020"
|
||||||
|
});
|
||||||
|
expect(result.turn_input.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
|
||||||
|
expect(result.turn_input.data_need_graph?.subject_candidates).toEqual([]);
|
||||||
|
expect(result.bridge?.planner.selected_chain_id).toBe("value_flow_comparison");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skip("keeps mirrored predecompose organization and counterparty out of the subject lane for open comparison (utf8-safe)", async () => {
|
||||||
|
const orgName = "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
||||||
|
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
|
||||||
|
userMessage: "что больше: входящие или исходящие деньги за 2020 год по ООО Альтернатива Плюс?",
|
||||||
|
predecomposeContract: {
|
||||||
|
entities: {
|
||||||
|
counterparty: orgName,
|
||||||
|
organization: orgName
|
||||||
|
},
|
||||||
|
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
|
||||||
|
},
|
||||||
|
deps: buildBidirectionalDeps(
|
||||||
|
[{ Period: "2020-01-15T00:00:00", Amount: 2500, Counterparty: "\u041a\u043b\u0438\u0435\u043d\u0442-\u0410" }],
|
||||||
|
[{ Period: "2020-02-18T00:00:00", Amount: 900, Counterparty: "\u041f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a-\u0410" }]
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.entry_status).toBe("bridge_executed");
|
||||||
|
expect(result.turn_input.turn_meaning_ref).toMatchObject({
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "net_value_flow",
|
||||||
|
explicit_organization_scope: orgName,
|
||||||
|
explicit_date_scope: "2020"
|
||||||
|
});
|
||||||
|
expect(result.turn_input.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
|
||||||
|
expect(result.turn_input.data_need_graph?.subject_candidates).toEqual([]);
|
||||||
|
expect(result.bridge?.planner.selected_chain_id).toBe("value_flow_comparison");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ describe("assistant MCP discovery turn input adapter", () => {
|
||||||
expect(result.adapter_status).toBe("ready");
|
expect(result.adapter_status).toBe("ready");
|
||||||
expect(result.should_run_discovery).toBe(true);
|
expect(result.should_run_discovery).toBe(true);
|
||||||
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
|
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
|
||||||
|
expect(result.data_need_graph?.business_fact_family).toBe("value_flow");
|
||||||
|
expect(result.data_need_graph?.time_scope_need).toBe("explicit_period");
|
||||||
expect(result.turn_meaning_ref?.explicit_entity_candidates).toEqual(["SVK", "Группа СВК"]);
|
expect(result.turn_meaning_ref?.explicit_entity_candidates).toEqual(["SVK", "Группа СВК"]);
|
||||||
expect(result.turn_meaning_ref?.explicit_organization_scope).toBe("Альтернатива");
|
expect(result.turn_meaning_ref?.explicit_organization_scope).toBe("Альтернатива");
|
||||||
expect(result.turn_meaning_ref?.explicit_date_scope).toBe("2020");
|
expect(result.turn_meaning_ref?.explicit_date_scope).toBe("2020");
|
||||||
|
|
@ -160,6 +162,8 @@ describe("assistant MCP discovery turn input adapter", () => {
|
||||||
expect(result.should_run_discovery).toBe(true);
|
expect(result.should_run_discovery).toBe(true);
|
||||||
expect(result.source_signal).toBe("raw_text");
|
expect(result.source_signal).toBe("raw_text");
|
||||||
expect(result.semantic_data_need).toBe("1C metadata evidence");
|
expect(result.semantic_data_need).toBe("1C metadata evidence");
|
||||||
|
expect(result.data_need_graph?.business_fact_family).toBe("schema_surface");
|
||||||
|
expect(result.data_need_graph?.decomposition_candidates).toEqual(["inspect_metadata_surface"]);
|
||||||
expect(result.turn_meaning_ref).toMatchObject({
|
expect(result.turn_meaning_ref).toMatchObject({
|
||||||
asked_domain_family: "metadata",
|
asked_domain_family: "metadata",
|
||||||
asked_action_family: "inspect_fields",
|
asked_action_family: "inspect_fields",
|
||||||
|
|
@ -198,6 +202,8 @@ describe("assistant MCP discovery turn input adapter", () => {
|
||||||
expect(result.should_run_discovery).toBe(true);
|
expect(result.should_run_discovery).toBe(true);
|
||||||
expect(result.source_signal).toBe("raw_text");
|
expect(result.source_signal).toBe("raw_text");
|
||||||
expect(result.semantic_data_need).toBe("entity discovery evidence");
|
expect(result.semantic_data_need).toBe("entity discovery evidence");
|
||||||
|
expect(result.data_need_graph?.business_fact_family).toBe("entity_grounding");
|
||||||
|
expect(result.data_need_graph?.subject_candidates).toEqual(["Группа СВК"]);
|
||||||
expect(result.turn_meaning_ref).toMatchObject({
|
expect(result.turn_meaning_ref).toMatchObject({
|
||||||
asked_domain_family: "entity_resolution",
|
asked_domain_family: "entity_resolution",
|
||||||
asked_action_family: "search_business_entity",
|
asked_action_family: "search_business_entity",
|
||||||
|
|
@ -1256,4 +1262,119 @@ describe("assistant MCP discovery turn input adapter", () => {
|
||||||
"\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a \u043d\u0430\u0439\u0442\u0438 \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430 \u0441 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c '\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a'"
|
"\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a \u043d\u0430\u0439\u0442\u0438 \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430 \u0441 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c '\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a'"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("marks top-value wording as a ranking data need without inventing a missing subject gap", () => {
|
||||||
|
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||||
|
userMessage: "кто больше всего принес денег в 2020"
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.adapter_status).toBe("ready");
|
||||||
|
expect(result.should_run_discovery).toBe(true);
|
||||||
|
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
|
||||||
|
expect(result.data_need_graph?.business_fact_family).toBe("value_flow");
|
||||||
|
expect(result.data_need_graph?.ranking_need).toBe("top_desc");
|
||||||
|
expect(result.data_need_graph?.clarification_gaps).toEqual([]);
|
||||||
|
expect(result.data_need_graph?.decomposition_candidates).toEqual([
|
||||||
|
"collect_scoped_movements",
|
||||||
|
"aggregate_ranked_axis_values",
|
||||||
|
"probe_coverage"
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it("keeps organization as scope for open bidirectional comparison wording instead of inventing a subject candidate", () => {
|
||||||
|
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||||
|
userMessage: "что больше: входящие или исходящие деньги за 2020 год по ООО Альтернатива Плюс?",
|
||||||
|
predecomposeContract: {
|
||||||
|
entities: { organization: "ООО Альтернатива Плюс" },
|
||||||
|
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.adapter_status).toBe("ready");
|
||||||
|
expect(result.should_run_discovery).toBe(true);
|
||||||
|
expect(result.turn_meaning_ref).toMatchObject({
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "net_value_flow",
|
||||||
|
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||||
|
explicit_date_scope: "2020"
|
||||||
|
});
|
||||||
|
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
|
||||||
|
expect(result.data_need_graph?.comparison_need).toBe("incoming_vs_outgoing");
|
||||||
|
expect(result.data_need_graph?.clarification_gaps).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("drops organization-shaped assistant-turn entity pollution when open comparison already has explicit organization scope", () => {
|
||||||
|
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||||
|
userMessage: "что больше: входящие или исходящие деньги за 2020 год по ООО Альтернатива Плюс?",
|
||||||
|
assistantTurnMeaning: {
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "net_value_flow",
|
||||||
|
explicit_entity_candidates: [{ value: "ООО Альтернатива Плюс" }],
|
||||||
|
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||||
|
explicit_date_scope: "2020",
|
||||||
|
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting"
|
||||||
|
},
|
||||||
|
predecomposeContract: {
|
||||||
|
entities: { counterparty: "ООО Альтернатива Плюс", organization: "ООО Альтернатива Плюс" },
|
||||||
|
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.turn_meaning_ref).toMatchObject({
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "net_value_flow",
|
||||||
|
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||||
|
explicit_date_scope: "2020"
|
||||||
|
});
|
||||||
|
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
|
||||||
|
expect(result.data_need_graph?.comparison_need).toBe("incoming_vs_outgoing");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skip("treats mirrored predecompose organization and counterparty as organization scope for open comparison", () => {
|
||||||
|
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||||
|
userMessage: "что больше: входящие или исходящие деньги за 2020 год по ООО Альтернатива Плюс?",
|
||||||
|
predecomposeContract: {
|
||||||
|
entities: {
|
||||||
|
counterparty: "ООО Альтернатива Плюс",
|
||||||
|
organization: "ООО Альтернатива Плюс"
|
||||||
|
},
|
||||||
|
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.turn_meaning_ref).toMatchObject({
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "net_value_flow",
|
||||||
|
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||||
|
explicit_date_scope: "2020"
|
||||||
|
});
|
||||||
|
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
|
||||||
|
expect(result.data_need_graph?.subject_candidates).toEqual([]);
|
||||||
|
expect(result.data_need_graph?.comparison_need).toBe("incoming_vs_outgoing");
|
||||||
|
expect(result.reason_codes).not.toContain("mcp_discovery_counterparty_from_predecompose");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skip("treats mirrored predecompose organization and counterparty as organization scope for open comparison (utf8-safe)", () => {
|
||||||
|
const orgName = "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
||||||
|
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||||
|
userMessage: "что больше: входящие или исходящие деньги за 2020 год по ООО Альтернатива Плюс?",
|
||||||
|
predecomposeContract: {
|
||||||
|
entities: {
|
||||||
|
counterparty: orgName,
|
||||||
|
organization: orgName
|
||||||
|
},
|
||||||
|
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.turn_meaning_ref).toMatchObject({
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "net_value_flow",
|
||||||
|
explicit_organization_scope: orgName,
|
||||||
|
explicit_date_scope: "2020"
|
||||||
|
});
|
||||||
|
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
|
||||||
|
expect(result.data_need_graph?.subject_candidates).toEqual([]);
|
||||||
|
expect(result.data_need_graph?.comparison_need).toBe("incoming_vs_outgoing");
|
||||||
|
expect(result.reason_codes).not.toContain("mcp_discovery_counterparty_from_predecompose");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue