Compare commits
23 Commits
99a568241d
...
dca49ef4e1
| Author | SHA1 | Date |
|---|---|---|
|
|
dca49ef4e1 | |
|
|
bd58ab490f | |
|
|
bc54cd9628 | |
|
|
acacada0f6 | |
|
|
1fd8062dc7 | |
|
|
ce48fa83a5 | |
|
|
007369a78a | |
|
|
40cf71d118 | |
|
|
c0b3296953 | |
|
|
c328c52c9b | |
|
|
d9454fcdef | |
|
|
f951dae9f0 | |
|
|
eac3709f2b | |
|
|
7ef788fa50 | |
|
|
6d9c1568c3 | |
|
|
d66e2bfb01 | |
|
|
561b4ea45c | |
|
|
bda7ca9cc1 | |
|
|
d323dcd509 | |
|
|
429bd3d8ec | |
|
|
b542b65b81 | |
|
|
cd3315e06d | |
|
|
4baa54fe81 |
|
|
@ -1261,6 +1261,106 @@ Module progress:
|
|||
|
||||
- Big Block 5 MCP Semantic Data Agent: `99%`.
|
||||
|
||||
## Progress Update - 2026-04-21 MCP Discovery Monthly Bidirectional Multi-Axis Aggregation
|
||||
|
||||
The nineteenth implementation slice of Big Block 5 closes the first explicit multi-axis aggregation gap for guarded MCP discovery.
|
||||
|
||||
Before this slice the assistant could answer:
|
||||
|
||||
- incoming value-flow total;
|
||||
- outgoing supplier-payout total;
|
||||
- composed bidirectional net total.
|
||||
|
||||
But it could not keep a user request like "по месяцам" as part of the machine-readable turn meaning, plan shape, and final guarded answer.
|
||||
|
||||
New behavior:
|
||||
|
||||
- monthly wording such as `по месяцам`, `помесячно`, `ежемесячно`, `monthly`, and `month by month` is captured as `asked_aggregation_axis=month`;
|
||||
- the discovery planner keeps the monthly request as an explicit `calendar_month` axis on top of the existing guarded value-flow recipe;
|
||||
- runtime derives `monthly_breakdown` for single-direction value-flow and for composed bidirectional net value-flow from the same confirmed 1C rows;
|
||||
- the guarded answer draft now adds month-by-month business lines instead of flattening a monthly question back into a single total;
|
||||
- the response-candidate layer keeps those monthly lines user-facing and localizes the monthly inference basis without leaking planner/runtime/pilot mechanics.
|
||||
|
||||
Replay result:
|
||||
|
||||
- `address_truth_harness_phase19_mcp_discovery_response_gate_live_rerun12` passed `8/8` after adding the new monthly step and exposed one presentation defect: duplicated punctuation in monthly amount lines;
|
||||
- `address_truth_harness_phase19_mcp_discovery_response_gate_live_rerun13` passed `8/8` after punctuation cleanup;
|
||||
- final status: `accepted`;
|
||||
- the new step `step_07_counterparty_bidirectional_monthly_net_flow_uses_guarded_discovery` answered through guarded MCP discovery with monthly lines from `янв 2020` through `дек 2020`;
|
||||
- user-facing text keeps the same honesty boundary as the total net answer: outgoing coverage still hits the `100`-row probe limit, so the monthly shape is a found-row calculation rather than a fully proven all-period saldo;
|
||||
- user-facing monthly output contains no `query_documents`, `query_movements`, `runtime_`, `planner_`, `catalog_`, `primitive`, or `pilot_` leak.
|
||||
|
||||
Validation:
|
||||
|
||||
- `npm test -- assistantMcpDiscoveryTurnInputAdapter.test.ts assistantMcpDiscoveryPlanner.test.ts assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts assistantMcpDiscoveryResponseCandidate.test.ts assistantMcpDiscoveryResponsePolicy.test.ts assistantMcpDiscoveryRuntimeEntryPoint.test.ts assistantMcpDiscoveryRuntimeBridge.test.ts assistantAddressLaneResponseRuntimeAdapter.test.ts assistantDeepTurnResponseRuntimeAdapter.test.ts assistantLivingChatRuntimeAdapter.test.ts assistantAddressOrchestrationRuntimeAdapter.test.ts assistantDebugPayloadAssembler.test.ts` passed `90/90`;
|
||||
- `npm run build` passed;
|
||||
- `python scripts/domain_truth_harness.py run-live --spec docs/orchestration/address_truth_harness_phase19_mcp_discovery_response_gate.json --output-dir artifacts/domain_runs/address_truth_harness_phase19_mcp_discovery_response_gate_live_rerun13 --timeout-seconds 180` passed `8/8`, final status `accepted`.
|
||||
|
||||
Known remaining boundary:
|
||||
|
||||
- this still does not make Qwen3 an unrestricted self-navigating 1C agent across arbitrary registers and schemas; follow-up drilldown over the discovered rows and broader self-navigation remain future work outside this first completed guarded multi-axis slice.
|
||||
|
||||
Module progress:
|
||||
|
||||
- Big Block 5 MCP Semantic Data Agent: `100%`.
|
||||
|
||||
## Progress Update - 2026-04-21 MCP Discovery Yearly Coverage Recovery Through Monthly Probes
|
||||
|
||||
The twentieth implementation slice of Big Block 5 closes the first yearly-coverage proof gap that remained inside guarded MCP discovery value-flow answers.
|
||||
|
||||
Before this slice the assistant could already:
|
||||
|
||||
- answer incoming counterparty turnover totals through guarded MCP discovery;
|
||||
- answer outgoing supplier payout totals through guarded MCP discovery;
|
||||
- compose bidirectional net totals and month-by-month net lines from the same evidence.
|
||||
|
||||
But the broad yearly value-flow probe still stopped at the first MCP row limit. For dense 2020 counterparty activity this meant the assistant answered honestly, yet underpowered: user-facing text still said the outgoing side hit `100 из 100`, even though the missing coverage was recoverable through bounded subperiod probes rather than impossible.
|
||||
|
||||
New behavior:
|
||||
|
||||
- the discovery planner now grants a larger but still bounded probe budget for explicit yearly value-flow and monthly-aggregation questions;
|
||||
- the pilot executor first runs the broad yearly value-flow probe and, only when that broad probe hits the row limit on an explicit year window, automatically recovers coverage through month-by-month 1C subprobes;
|
||||
- recovered subperiod rows are stitched into one coverage-aware result instead of being exposed as separate runtime fragments;
|
||||
- guarded answer drafting keeps the answer honest: it no longer claims a partial year when monthly recovery fully proves the requested period, but it still states the derivation basis and keeps "outside the checked period is not proven" boundaries intact.
|
||||
|
||||
Replay result:
|
||||
|
||||
- `address_truth_harness_phase19_mcp_discovery_response_gate_live_rerun14` passed `8/8`, final status `accepted`;
|
||||
- the supplier payout step now answers the 2020 `Группа СВК` question with `43 763 351,53 руб.`, based on `299 из 299` recovered rows from `2020-01-09` through `2020-12-25`;
|
||||
- the bidirectional net step now answers with `47 628 853,03 руб.` received, `43 763 351,53 руб.` paid, and `3 865 501,50 руб.` net in our favor for the requested 2020 window;
|
||||
- the monthly net step keeps the month-by-month shape from January through December 2020 and no longer inherits the old fake partiality caused by the first broad outgoing probe hitting the row cap;
|
||||
- user-facing text explains that coverage was restored through monthly 1C checks after the broad probe hit the row limit, without leaking `planner_`, `pilot_`, `runtime_`, `query_documents`, or `query_movements`.
|
||||
|
||||
Validation:
|
||||
|
||||
- `npm test -- assistantMcpDiscoveryPolicy.test.ts assistantMcpDiscoveryPlanner.test.ts assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts assistantMcpDiscoveryResponseCandidate.test.ts assistantMcpDiscoveryResponsePolicy.test.ts assistantMcpDiscoveryRuntimeEntryPoint.test.ts assistantMcpDiscoveryRuntimeBridge.test.ts assistantAddressLaneResponseRuntimeAdapter.test.ts assistantDeepTurnResponseRuntimeAdapter.test.ts assistantLivingChatRuntimeAdapter.test.ts assistantAddressOrchestrationRuntimeAdapter.test.ts assistantDebugPayloadAssembler.test.ts` passed `91/91`;
|
||||
- `npm run build` passed;
|
||||
- `python scripts/domain_truth_harness.py run-live --spec docs/orchestration/address_truth_harness_phase19_mcp_discovery_response_gate.json --output-dir artifacts/domain_runs/address_truth_harness_phase19_mcp_discovery_response_gate_live_rerun14 --timeout-seconds 240` passed `8/8`, final status `accepted`.
|
||||
|
||||
Known remaining boundary:
|
||||
|
||||
- this still does not make Qwen3 an unrestricted self-navigating 1C agent across arbitrary registers and schemas; richer follow-up drilldown over the recovered rows, broader self-navigation, and evidence-safe agentic exploration remain future work beyond this first completed guarded MCP semantic data block.
|
||||
|
||||
Module progress:
|
||||
|
||||
- Big Block 5 MCP Semantic Data Agent: `100%`.
|
||||
|
||||
## Reset Hand-Off - 2026-04-21
|
||||
|
||||
The progress updates above closed the first guarded MCP discovery pilot wave.
|
||||
|
||||
They do **not** mean the strategic autonomy target is complete.
|
||||
|
||||
From 2026-04-21 onward the mainline continues in:
|
||||
|
||||
- [15 - mcp_bounded_autonomy_reset_plan_2026-04-21.md](/x:/1C/NDC_1C/docs/ARCH/11%20-%20architecture_turnaround/15%20-%20mcp_bounded_autonomy_reset_plan_2026-04-21.md:1)
|
||||
|
||||
That reset freezes the continuity/authority stabilization as sufficient and returns the project to the primary trajectory:
|
||||
|
||||
- metadata-first self-navigation;
|
||||
- entity/schema grounding;
|
||||
- planner-selected MCP primitive chains instead of route-per-question hardcoding.
|
||||
|
||||
## Execution Rule
|
||||
|
||||
Do not implement this plan as:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,201 @@
|
|||
# 15 - MCP Bounded Autonomy Reset Plan (2026-04-21)
|
||||
|
||||
## Purpose
|
||||
|
||||
This note resets the execution focus after the stabilization wave inside turnaround `11`.
|
||||
|
||||
It does not cancel the continuity and authority work already done.
|
||||
|
||||
It clarifies that the main project trajectory is not:
|
||||
|
||||
- endless polishing of deterministic route arbitration;
|
||||
- route-per-question hardcoding;
|
||||
- a fake "agentic" mode that is actually another brittle prompt wrapper.
|
||||
|
||||
The real trajectory is:
|
||||
|
||||
- bounded assistant autonomy over reviewed MCP primitives;
|
||||
- proof-first discovery of 1C evidence;
|
||||
- gradual reduction of route hardcoding at the question level.
|
||||
|
||||
## Why The Reset Is Necessary
|
||||
|
||||
The previous wave repaired real defects:
|
||||
|
||||
- stale carryover stopped beating the current question on critical contours;
|
||||
- guarded MCP discovery answers stopped being overwritten by stale exact/lifecycle paths;
|
||||
- broad business evaluation no longer breaks the return into the data contour.
|
||||
|
||||
That work stays valid.
|
||||
|
||||
But it was support work around the edge of the main target.
|
||||
|
||||
The strategic target was always bigger:
|
||||
|
||||
- the assistant should not need one deterministic route per business wording;
|
||||
- the assistant should be able to orient itself inside 1C through a reviewed set of MCP primitives;
|
||||
- the planner should decide which safe primitive chain to use for the current data need;
|
||||
- the evidence gate should decide what may be stated to the user.
|
||||
|
||||
So the reset is not "we were wrong".
|
||||
|
||||
It is:
|
||||
|
||||
- stabilization is now frozen as sufficient;
|
||||
- the mainline returns to `MCP-first bounded autonomy`.
|
||||
|
||||
## What Is Frozen
|
||||
|
||||
The following is now baseline, not the mainline:
|
||||
|
||||
- current-turn meaning authority;
|
||||
- continuity subordinated to current explicit meaning;
|
||||
- guarded discovery response replacement;
|
||||
- broad-evaluation bridge that does not destroy the next data follow-up.
|
||||
|
||||
These seams may still receive bug fixes if they block MCP-first execution.
|
||||
|
||||
They are not the main feature track anymore.
|
||||
|
||||
## North Star
|
||||
|
||||
The target is not an unrestricted free agent.
|
||||
|
||||
The target is a bounded planner over a reviewed primitive catalog:
|
||||
|
||||
1. recognize the business data need;
|
||||
2. pick allowed MCP primitives;
|
||||
3. execute bounded probes against 1C;
|
||||
4. aggregate evidence;
|
||||
5. answer only within the evidence gate.
|
||||
|
||||
In short:
|
||||
|
||||
- move determinism from `route per user wording`
|
||||
- to `catalog of safe primitives + proof workflow`.
|
||||
|
||||
## Big Block A. Metadata-First Self-Navigation
|
||||
|
||||
### Goal
|
||||
|
||||
Teach the assistant to inspect the 1C schema surface before guessing a route.
|
||||
|
||||
This is the first real step from pilot hardcoding toward self-navigation.
|
||||
|
||||
### Scope
|
||||
|
||||
- live execution of `inspect_1c_metadata`;
|
||||
- metadata-aware planner path that cannot collapse into `query_documents`;
|
||||
- machine-readable metadata evidence:
|
||||
- available object sets;
|
||||
- matching objects;
|
||||
- available fields/sections when metadata returns them;
|
||||
- known limitations;
|
||||
- human-safe answer draft for metadata discovery.
|
||||
|
||||
### Why This Block Comes First
|
||||
|
||||
Without metadata-first inspection, every later autonomy step is blind:
|
||||
|
||||
- entity resolution is guesswork;
|
||||
- register/document choice is guesswork;
|
||||
- long-tail discovery turns back into hidden route hardcoding.
|
||||
|
||||
### Acceptance
|
||||
|
||||
- raw metadata wording can bootstrap discovery input;
|
||||
- planner keeps `inspect_1c_metadata` as the chosen primitive;
|
||||
- pilot executes live metadata inspection through MCP;
|
||||
- user-facing answer stays free of primitive/query/runtime garbage.
|
||||
|
||||
## Big Block B. Entity And Schema Grounding
|
||||
|
||||
### Goal
|
||||
|
||||
Move from "I found some metadata" to "I can ground the user ask onto the right 1C surface".
|
||||
|
||||
### Scope
|
||||
|
||||
- search and resolve candidate entities through MCP instead of local tails only;
|
||||
- bind metadata findings to probable document/register families;
|
||||
- preserve ambiguity honestly when multiple surfaces compete;
|
||||
- keep machine-readable grounding evidence for downstream probes.
|
||||
|
||||
### Acceptance
|
||||
|
||||
- assistant can say which schema surface it selected and why;
|
||||
- ambiguity is surfaced as a bounded clarification, not silent route drift;
|
||||
- chosen surface becomes reusable context for the next primitive.
|
||||
|
||||
## Big Block C. Planner-Selected Primitive Chains
|
||||
|
||||
### Goal
|
||||
|
||||
Replace one-off pilot scopes with planner-selected safe chains.
|
||||
|
||||
### Scope
|
||||
|
||||
- chain primitives such as:
|
||||
- `inspect_1c_metadata`
|
||||
- `search_business_entity`
|
||||
- `resolve_entity_reference`
|
||||
- `query_documents`
|
||||
- `query_movements`
|
||||
- `aggregate_by_axis`
|
||||
- `probe_coverage`
|
||||
- `explain_evidence_basis`
|
||||
- keep exact deterministic routes as fast-paths;
|
||||
- use discovery as the general path for understood long-tail questions.
|
||||
|
||||
### Acceptance
|
||||
|
||||
- new long-tail questions become answerable without adding a dedicated route for each wording;
|
||||
- the planner output explains the chosen primitive chain;
|
||||
- the answer gate still blocks overclaiming.
|
||||
|
||||
## Stage 1 Started Now
|
||||
|
||||
The first block is no longer just planned.
|
||||
|
||||
It has started in code in this pass.
|
||||
|
||||
Implemented in this stage:
|
||||
|
||||
- raw metadata wording now bootstraps discovery input;
|
||||
- metadata planning stays on `inspect_1c_metadata` and no longer falls into `query_documents`;
|
||||
- the pilot executor now has a live metadata inspection slice;
|
||||
- the answer adapter can produce a user-safe metadata surface answer.
|
||||
|
||||
This stage is intentionally narrow.
|
||||
|
||||
It does **not** yet mean:
|
||||
|
||||
- unrestricted Qwen3 navigation across arbitrary 1C contours;
|
||||
- automatic multi-step schema-to-entity-to-query chaining;
|
||||
- hot-runtime replacement of broad assistant behavior everywhere.
|
||||
|
||||
It means the architecture now has the first real self-navigation primitive in production code.
|
||||
|
||||
## Execution Rule
|
||||
|
||||
From this point the project should prefer:
|
||||
|
||||
- adding or strengthening reviewed MCP primitives;
|
||||
- planner-selected evidence workflows;
|
||||
- machine-readable grounding and proof contracts.
|
||||
|
||||
And should avoid:
|
||||
|
||||
- growing another layer of hidden route hardcoding for each new wording;
|
||||
- long stabilization detours unless they protect an MCP-first invariant;
|
||||
- fake autonomy that bypasses the evidence gate.
|
||||
|
||||
## Bottom Line
|
||||
|
||||
Turnaround `11` is no longer only about making the assistant feel less glitchy.
|
||||
|
||||
The next move is larger:
|
||||
|
||||
- make the assistant able to look into 1C through bounded MCP discovery,
|
||||
- choose its path through reviewed primitives,
|
||||
- and answer from proved evidence instead of memorized route scripts.
|
||||
|
|
@ -67,6 +67,8 @@
|
|||
"forbidden_answer_patterns": [
|
||||
"(?i)точный маршрут.*не подключ",
|
||||
"(?i)не буду подставлять",
|
||||
"(?i)100\\s+из\\s+100",
|
||||
"(?i)лимит.*строк",
|
||||
"(?i)query_documents",
|
||||
"(?i)query_movements",
|
||||
"(?i)runtime_",
|
||||
|
|
@ -95,6 +97,8 @@
|
|||
"forbidden_answer_patterns": [
|
||||
"(?i)точный маршрут.*не подключ",
|
||||
"(?i)не буду подставлять",
|
||||
"(?i)100\\s+из\\s+100",
|
||||
"(?i)лимит.*строк",
|
||||
"(?i)query_documents",
|
||||
"(?i)query_movements",
|
||||
"(?i)runtime_",
|
||||
|
|
@ -125,6 +129,8 @@
|
|||
"forbidden_answer_patterns": [
|
||||
"(?i)точный маршрут.*не подключ",
|
||||
"(?i)не буду подставлять",
|
||||
"(?i)100\\s+из\\s+100",
|
||||
"(?i)лимит.*строк",
|
||||
"(?i)query_documents",
|
||||
"(?i)query_movements",
|
||||
"(?i)runtime_",
|
||||
|
|
@ -173,7 +179,41 @@
|
|||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_07_off_domain_living_chat_not_hijacked",
|
||||
"step_id": "step_07_counterparty_bidirectional_monthly_net_flow_uses_guarded_discovery",
|
||||
"title": "Unsupported-but-understood counterparty monthly net cash-flow question keeps monthly structure from discovery",
|
||||
"question": "какое помесячное нетто по деньгам с Группа СВК за 2020 год: сколько получили и сколько заплатили по месяцам?",
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)свк",
|
||||
"(?i)1с|найден|строк|проверен",
|
||||
"(?i)получил|входящ|поступ",
|
||||
"(?i)заплат|исходящ|списан|плат[её]ж",
|
||||
"(?i)помесяч|по месяцам|янв|фев|мар|апр|май|июн|июл|авг|сен|окт|ноя|дек",
|
||||
"(?i)нетто|сальдо|разниц",
|
||||
"(?i)сумм|руб",
|
||||
"(?i)2020|период",
|
||||
"(?i)не подтвержд|проверенн|найденн"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)точный маршрут.*не подключ",
|
||||
"(?i)не буду подставлять",
|
||||
"(?i)query_documents",
|
||||
"(?i)query_movements",
|
||||
"(?i)runtime_",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_",
|
||||
"(?i)primitive",
|
||||
"(?i)pilot_"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"mcp_discovery_bidirectional_value_flow",
|
||||
"counterparty_monthly_net_cash_flow",
|
||||
"multi_axis_aggregation",
|
||||
"unsupported_current_turn_meaning_boundary"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_08_off_domain_living_chat_not_hijacked",
|
||||
"title": "Off-domain living chat remains human and is not hijacked by discovery carryover",
|
||||
"question": "а чем капибара отличается от утки?",
|
||||
"required_answer_patterns_any": [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,138 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase20_continuity_stabilization",
|
||||
"domain": "address_phase20_continuity_stabilization",
|
||||
"title": "Phase 20 continuity stabilization replay",
|
||||
"description": "Targeted AGENT replay for the continuity stabilization slice after assistant-stage1--I0x_DLqDb. The scenario validates that temporal tail words no longer turn into pseudo-counterparties, exact debt snapshots are not overwritten by discovery, and VAT/date follow-ups keep the previous period instead of drifting into unrelated carryover.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_top_client_all_time",
|
||||
"title": "All-time top client question stays in ranking semantics and does not invent counterparty time",
|
||||
"question": "кто у нас самый доходный клиент за все время?",
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)клиент|контрагент",
|
||||
"(?i)доходн|выручк|заработ"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)не найден контрагент.*время",
|
||||
"(?i)контрагент с названием\\s+\"?время\"?"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"value_flow_ranking",
|
||||
"temporal_tail_not_entity"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_top_year_all_time",
|
||||
"title": "Top year question stays in yearly ranking semantics and does not invent pseudo-entity year",
|
||||
"question": "какой у нас самый доходный год?",
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)20\\d{2}|19\\d{2}",
|
||||
"(?i)доходн|выручк|заработ"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)не найден контрагент.*год",
|
||||
"(?i)контрагент с названием\\s+\"?год\"?"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"value_flow_ranking",
|
||||
"year_tail_not_entity"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_receivables_as_of_may_2017",
|
||||
"title": "Receivables snapshot for May 2017 answers directly instead of being overwritten by discovery",
|
||||
"question": "кто нам должен денег на май 2017?",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation"
|
||||
],
|
||||
"required_direct_answer_patterns_any": [
|
||||
"(?i)долж",
|
||||
"(?i)дебитор",
|
||||
"(?i)задолж"
|
||||
],
|
||||
"forbidden_direct_answer_patterns": [
|
||||
"(?i)partial_coverage",
|
||||
"(?i)не удалось ответить",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"receivables_snapshot",
|
||||
"exact_not_overwritten"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_vat_for_same_period",
|
||||
"title": "VAT payable follow-up keeps the carried May 2017 period",
|
||||
"question": "а какой ндс мы должны примерно заплатить за этот период?",
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)ндс",
|
||||
"(?i)май|2017",
|
||||
"(?i)заплат|к уплате|уплат"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)текущ(ий|его) период",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"vat_followup",
|
||||
"period_carryover"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_payables_today",
|
||||
"title": "Today payables snapshot answers directly and keeps debt semantics",
|
||||
"question": "мы должны комуто денег на сегодня?",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation"
|
||||
],
|
||||
"required_direct_answer_patterns_any": [
|
||||
"(?i)должны",
|
||||
"(?i)кредитор",
|
||||
"(?i)задолж",
|
||||
"(?i)долг к оплате|к оплате"
|
||||
],
|
||||
"forbidden_direct_answer_patterns": [
|
||||
"(?i)partial_coverage",
|
||||
"(?i)не удалось ответить",
|
||||
"(?i)не найден контрагент"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"payables_snapshot",
|
||||
"exact_not_overwritten"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_06_receivables_pronoun_followup",
|
||||
"title": "Short follow-up a nam resolves to mirrored receivables instead of fake counterparty time",
|
||||
"question": "а нам?",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation"
|
||||
],
|
||||
"required_direct_answer_patterns_any": [
|
||||
"(?i)нам должны",
|
||||
"(?i)дебитор",
|
||||
"(?i)задолж"
|
||||
],
|
||||
"forbidden_direct_answer_patterns": [
|
||||
"(?i)не найден контрагент.*время",
|
||||
"(?i)контрагент с названием\\s+\"?время\"?",
|
||||
"(?i)partial_coverage"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"pronoun_followup",
|
||||
"garbage_anchor_forbidden"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase21_net_followup_after_broad_eval",
|
||||
"domain": "address_phase21_net_followup_after_broad_eval",
|
||||
"title": "Phase 21 net-flow follow-up after broad evaluation replay",
|
||||
"description": "Targeted AGENT replay for the assistant-stage1-LpuYsX0SRP regression where a net cash-flow question about Группа СВК inside an existing dialogue chain was wrongly kept on the counterparty lifecycle contour instead of applying the guarded MCP discovery answer.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_company_activity_lifecycle",
|
||||
"title": "Activity lifecycle answer seeds broad counterparty context",
|
||||
"question": "а по Альтернативе Плюс сколько лет активности в базе 1С?",
|
||||
"allowed_reply_types": [
|
||||
"partial_coverage",
|
||||
"factual",
|
||||
"factual_with_explanation"
|
||||
],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)лет",
|
||||
"(?i)активност",
|
||||
"(?i)1с",
|
||||
"(?i)не получил|не подтвержден|проверил доступный контур"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"company_activity_lifecycle",
|
||||
"context_seed"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_broad_company_evaluation",
|
||||
"title": "Broad evaluation sits between lifecycle and net-flow question",
|
||||
"question": "Как ты оценишь деятельность компании?",
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)активн",
|
||||
"(?i)заказчик|контрагент|деятельност|оценк"
|
||||
],
|
||||
"criticality": "warning",
|
||||
"semantic_tags": [
|
||||
"broad_evaluation_bridge"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_net_flow_after_broad_eval",
|
||||
"title": "Net-flow follow-up overrides stale lifecycle carryover and answers with inflow outflow and net",
|
||||
"question": "какое нетто по деньгам с Группа СВК за 2020 год: сколько получили и сколько заплатили?",
|
||||
"allowed_reply_types": [
|
||||
"partial_coverage",
|
||||
"factual_with_explanation"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)свк",
|
||||
"(?i)получил|входящ|поступ",
|
||||
"(?i)заплат|исходящ|списан|плат[её]ж",
|
||||
"(?i)нетто|сальдо|разниц",
|
||||
"(?i)2020|период",
|
||||
"(?i)руб"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)активных заказчиков",
|
||||
"(?i)лет в базе",
|
||||
"(?i)последняя активность"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"counterparty_net_cash_flow",
|
||||
"stale_lifecycle_override"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase22_broad_business_evaluation_bridge",
|
||||
"domain": "address_phase22_broad_business_evaluation_bridge",
|
||||
"title": "Phase 22 broad business evaluation bridge replay",
|
||||
"description": "Targeted AGENT replay for the broad business evaluation seam where a follow-up like 'Как ты оценишь деятельность компании?' must not replay stale lifecycle routing and should still preserve the chain for the next exact net-flow question.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_company_activity_lifecycle",
|
||||
"title": "Lifecycle answer seeds grounded organization context",
|
||||
"question": "а по Альтернативе Плюс сколько лет активности в базе 1С?",
|
||||
"allowed_reply_types": [
|
||||
"partial_coverage",
|
||||
"factual",
|
||||
"factual_with_explanation"
|
||||
],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)лет",
|
||||
"(?i)активност",
|
||||
"(?i)1с",
|
||||
"(?i)не получил|не подтвержден|проверил доступный контур"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"company_activity_lifecycle",
|
||||
"grounded_context_seed"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_broad_business_evaluation",
|
||||
"title": "Broad business evaluation becomes grounded summary instead of stale lifecycle dump",
|
||||
"question": "Как ты оценишь деятельность компании?",
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)коротко|оценк|частичн",
|
||||
"(?i)1с|подтвержд",
|
||||
"(?i)денежн|долг|ндс|контрагент|операц"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)активных заказчиков",
|
||||
"(?i)последняя активность",
|
||||
"(?i)^\\s*1\\."
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"broad_business_evaluation",
|
||||
"grounded_summary"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_net_flow_after_broad_eval",
|
||||
"title": "Exact net-flow follow-up still answers after the broad bridge",
|
||||
"question": "какое нетто по деньгам с Группа СВК за 2020 год: сколько получили и сколько заплатили?",
|
||||
"allowed_reply_types": [
|
||||
"partial_coverage",
|
||||
"factual_with_explanation"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)свк",
|
||||
"(?i)получил|входящ|поступ",
|
||||
"(?i)заплат|исходящ|списан|плат[её]ж",
|
||||
"(?i)нетто|сальдо|разниц",
|
||||
"(?i)2020|период",
|
||||
"(?i)руб"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)активных заказчиков",
|
||||
"(?i)лет в базе",
|
||||
"(?i)последняя активность"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"counterparty_net_cash_flow",
|
||||
"broad_eval_bridge_preserved"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase25_entity_resolution_chain",
|
||||
"domain": "address_phase25_entity_resolution_chain",
|
||||
"title": "Phase 25 entity-resolution grounding replay",
|
||||
"description": "Targeted AGENT replay for the first Big Block C slice where the assistant must use MCP discovery to ground a business entity in the checked 1C catalog and answer honestly without pretending that documents, movements, or value-flow evidence were already checked.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_resolve_counterparty_from_catalog",
|
||||
"title": "Raw counterparty search wording resolves a grounded 1C entity without leaking downstream business facts",
|
||||
"question": "найди в 1С контрагента Группа СВК",
|
||||
"allowed_reply_types": [
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)группа\\s+свк",
|
||||
"(?i)контрагент",
|
||||
"(?i)документ|движени|денежн"
|
||||
],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)каталог",
|
||||
"(?i)1с",
|
||||
"(?i)наш[её]л",
|
||||
"(?i)найден"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)получили",
|
||||
"(?i)заплатили",
|
||||
"(?i)нетто",
|
||||
"(?i)оборот",
|
||||
"(?i)выручк",
|
||||
"(?i)сумм(а|ы)"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"entity_resolution",
|
||||
"catalog_grounding",
|
||||
"bounded_autonomy"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase26_entity_followup_chain",
|
||||
"domain": "address_phase26_entity_followup_chain",
|
||||
"title": "Phase 26 resolved-entity follow-up chain replay",
|
||||
"description": "Targeted AGENT replay for the next Big Block C slice where an MCP-grounded counterparty must become a reusable dialog anchor for downstream document and movement evidence requests without forcing the user to repeat the resolved 1C name.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_resolve_counterparty_alias",
|
||||
"title": "Entity resolution grounds the checked 1C counterparty from a loose alias",
|
||||
"question": "найди в 1С контрагента СВК",
|
||||
"allowed_reply_types": [
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)свк",
|
||||
"(?i)контрагент"
|
||||
],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)группа\\s+свк",
|
||||
"(?i)каталог",
|
||||
"(?i)найден",
|
||||
"(?i)наиболее вероят"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)получили",
|
||||
"(?i)заплатили",
|
||||
"(?i)нетто",
|
||||
"(?i)оборот",
|
||||
"(?i)выручк",
|
||||
"(?i)сумм(а|ы)"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"entity_resolution",
|
||||
"alias_grounding",
|
||||
"followup_anchor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_documents_by_resolved_entity_followup",
|
||||
"title": "Short document follow-up reuses the resolved counterparty anchor",
|
||||
"question": "по нему документы за 2020 год",
|
||||
"allowed_reply_types": [
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)документ|счет|накладн|акт",
|
||||
"(?i)2020"
|
||||
],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)группа\\s+свк",
|
||||
"(?i)свк"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)уточните, какого контрагента",
|
||||
"(?i)по какому контрагенту"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"entity_resolution",
|
||||
"document_evidence",
|
||||
"followup_reuse"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_movements_by_resolved_entity_followup",
|
||||
"title": "Short movement follow-up keeps the same grounded counterparty anchor",
|
||||
"question": "а теперь по нему движения за 2020 год",
|
||||
"allowed_reply_types": [
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)движени|платеж|операц|проводк",
|
||||
"(?i)2020"
|
||||
],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)группа\\s+свк",
|
||||
"(?i)свк"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)уточните, какого контрагента",
|
||||
"(?i)по какому контрагенту"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"entity_resolution",
|
||||
"movement_evidence",
|
||||
"followup_reuse"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase27_entity_value_followup_chain",
|
||||
"domain": "address_phase27_entity_value_followup_chain",
|
||||
"title": "Phase 27 resolved-entity value-flow follow-up replay",
|
||||
"description": "Targeted AGENT replay for the next Big Block C slice where an MCP-grounded counterparty must become a reusable dialog anchor for downstream value-flow and net-flow questions without forcing the user to restate the resolved 1C name.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_resolve_counterparty_alias",
|
||||
"title": "Entity resolution grounds the checked 1C counterparty from a loose alias",
|
||||
"question": "найди в 1С контрагента СВК",
|
||||
"allowed_reply_types": [
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)свк",
|
||||
"(?i)контрагент"
|
||||
],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)группа\\s+свк",
|
||||
"(?i)каталог",
|
||||
"(?i)найден",
|
||||
"(?i)наиболее вероят"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)получили",
|
||||
"(?i)заплатили",
|
||||
"(?i)нетто",
|
||||
"(?i)оборот",
|
||||
"(?i)выручк",
|
||||
"(?i)сумм(а|ы)"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"entity_resolution",
|
||||
"alias_grounding",
|
||||
"followup_anchor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_value_flow_by_resolved_entity_followup",
|
||||
"title": "Short turnover follow-up reuses the resolved counterparty anchor",
|
||||
"question": "сколько получили по нему за 2020 год",
|
||||
"allowed_reply_types": [
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2020",
|
||||
"(?i)получил|входящ|поступ",
|
||||
"(?i)руб"
|
||||
],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)группа\\s+свк",
|
||||
"(?i)свк"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)уточните, какого контрагента",
|
||||
"(?i)по какому контрагенту"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"entity_resolution",
|
||||
"counterparty_value_flow",
|
||||
"followup_reuse"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_net_flow_by_resolved_entity_followup",
|
||||
"title": "Short net-flow follow-up keeps the same grounded counterparty anchor",
|
||||
"question": "а какое нетто по нему за 2020 год",
|
||||
"allowed_reply_types": [
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2020",
|
||||
"(?i)нетто|сальдо|разниц",
|
||||
"(?i)руб"
|
||||
],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)группа\\s+свк",
|
||||
"(?i)свк"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)уточните, какого контрагента",
|
||||
"(?i)по какому контрагенту"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"entity_resolution",
|
||||
"counterparty_net_value_flow",
|
||||
"followup_reuse"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase28_entity_value_retarget_chain",
|
||||
"domain": "address_phase28_entity_value_retarget_chain",
|
||||
"title": "Phase 28 grounded entity value-flow retarget replay",
|
||||
"description": "Targeted AGENT replay for Big Block C where a grounded 1C counterparty must survive side-switch and year-switch follow-ups across incoming, payout, and net value-flow questions without forcing the user to restate the resolved name.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_resolve_counterparty_alias",
|
||||
"title": "Entity resolution grounds the checked 1C counterparty from a loose alias",
|
||||
"question": "найди в 1С контрагента СВК",
|
||||
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)свк", "(?i)контрагент"],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)группа\\s+свк",
|
||||
"(?i)каталог",
|
||||
"(?i)найден",
|
||||
"(?i)наиболее вероят"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)получили",
|
||||
"(?i)заплатили",
|
||||
"(?i)нетто",
|
||||
"(?i)оборот",
|
||||
"(?i)выручк",
|
||||
"(?i)сумм(а|ы)"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["entity_resolution", "alias_grounding", "followup_anchor"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_incoming_by_resolved_entity",
|
||||
"title": "Incoming value-flow follow-up reuses the resolved counterparty anchor",
|
||||
"question": "сколько получили по нему за 2020 год",
|
||||
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)2020", "(?i)получил|входящ|поступ", "(?i)руб"],
|
||||
"required_answer_patterns_any": ["(?i)группа\\s+свк", "(?i)свк"],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)уточните, какого контрагента",
|
||||
"(?i)по какому контрагенту"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["entity_resolution", "incoming_value_flow", "followup_reuse"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_payout_switch_by_resolved_entity",
|
||||
"title": "Outgoing payment follow-up keeps the same grounded counterparty and checked year",
|
||||
"question": "а теперь сколько заплатили?",
|
||||
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)2020", "(?i)заплатил|исходящ|списан|платеж", "(?i)руб"],
|
||||
"required_answer_patterns_any": ["(?i)группа\\s+свк", "(?i)свк"],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)уточните, какого контрагента",
|
||||
"(?i)по какому контрагенту",
|
||||
"(?i)за какой год"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["entity_resolution", "payout_switch", "followup_reuse", "date_carryover"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_year_switch_on_payout",
|
||||
"title": "Short year switch keeps the payout contour and grounded counterparty",
|
||||
"question": "а за 2021?",
|
||||
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)2021", "(?i)заплатил|исходящ|списан|платеж", "(?i)руб"],
|
||||
"required_answer_patterns_any": ["(?i)группа\\s+свк", "(?i)свк"],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)уточните, какого контрагента",
|
||||
"(?i)по какому контрагенту"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["entity_resolution", "payout_year_switch", "followup_reuse"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_net_switch_after_payout",
|
||||
"title": "Net-flow follow-up keeps the same grounded counterparty and switched year",
|
||||
"question": "а какое нетто?",
|
||||
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)2021", "(?i)нетто|сальдо|разниц", "(?i)руб"],
|
||||
"required_answer_patterns_any": ["(?i)группа\\s+свк", "(?i)свк"],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)уточните, какого контрагента",
|
||||
"(?i)по какому контрагенту",
|
||||
"(?i)за какой год"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["entity_resolution", "net_switch", "followup_reuse", "date_carryover"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase29_value_flow_to_documents_chain",
|
||||
"domain": "address_phase29_value_flow_to_documents_chain",
|
||||
"title": "Phase 29 grounded value-flow to document evidence replay",
|
||||
"description": "Targeted AGENT replay for Big Block C where a grounded counterparty and carried period must survive a pivot from value-flow answers into document evidence without forcing the user to restate the resolved name.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_resolve_counterparty_alias",
|
||||
"title": "Entity resolution grounds the checked 1C counterparty from a loose alias",
|
||||
"question": "найди в 1С контрагента СВК",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)свк", "(?i)контрагент"],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)группа\\s+свк",
|
||||
"(?i)каталог",
|
||||
"(?i)найден",
|
||||
"(?i)наиболее вероятн"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)получили",
|
||||
"(?i)заплатили",
|
||||
"(?i)нетто",
|
||||
"(?i)оборот",
|
||||
"(?i)выручк",
|
||||
"(?i)сумм(а|ы)"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["entity_resolution", "alias_grounding", "followup_anchor"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_incoming_by_resolved_entity",
|
||||
"title": "Incoming value-flow follow-up reuses the resolved counterparty anchor",
|
||||
"question": "сколько получили по нему за 2020 год",
|
||||
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)2020", "(?i)получил|входящ|поступ", "(?i)руб"],
|
||||
"required_answer_patterns_any": ["(?i)группа\\s+свк", "(?i)свк"],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)уточните, какого контрагента",
|
||||
"(?i)по какому контрагенту"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["entity_resolution", "incoming_value_flow", "followup_reuse"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_payout_switch_by_resolved_entity",
|
||||
"title": "Outgoing payment follow-up keeps the same grounded counterparty and checked year",
|
||||
"question": "а теперь сколько заплатили?",
|
||||
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)2020", "(?i)заплатил|исходящ|списан|платеж", "(?i)руб"],
|
||||
"required_answer_patterns_any": ["(?i)группа\\s+свк", "(?i)свк"],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)уточните, какого контрагента",
|
||||
"(?i)по какому контрагенту",
|
||||
"(?i)за какой год"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["entity_resolution", "payout_switch", "followup_reuse", "date_carryover"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_year_switch_on_payout",
|
||||
"title": "Short year switch keeps the payout contour and grounded counterparty",
|
||||
"question": "а за 2021?",
|
||||
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)2021", "(?i)заплатил|исходящ|списан|платеж", "(?i)руб"],
|
||||
"required_answer_patterns_any": ["(?i)группа\\s+свк", "(?i)свк"],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)уточните, какого контрагента",
|
||||
"(?i)по какому контрагенту"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["entity_resolution", "payout_year_switch", "followup_reuse"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_documents_after_value_flow",
|
||||
"title": "Document evidence follow-up keeps the same grounded counterparty after the money answer",
|
||||
"question": "а по документам?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)документ|счет|накладн|акт"],
|
||||
"required_answer_patterns_any": ["(?i)группа\\s+свк", "(?i)свк", "(?i)2021"],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)уточните, какого контрагента",
|
||||
"(?i)по какому контрагенту",
|
||||
"(?i)сколько получили",
|
||||
"(?i)сколько заплатили",
|
||||
"(?i)нетто"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["entity_resolution", "document_evidence", "value_flow_pivot", "followup_reuse"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase32_planner_selected_chain_end_to_end",
|
||||
"domain": "address_phase32_planner_selected_chain_end_to_end",
|
||||
"title": "Phase 32 planner-selected chain end-to-end replay",
|
||||
"description": "Targeted AGENT replay for closing Big Block C: a grounded 1C counterparty must survive planner-selected pivots across incoming value-flow, outgoing payouts, net flow, document evidence, and movement evidence without forcing the user to restate the resolved name.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_resolve_counterparty_alias",
|
||||
"title": "Entity resolution grounds the checked 1C counterparty from a loose alias",
|
||||
"question": "найди в 1С контрагента СВК",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)свк", "(?i)контрагент"],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)группа\\s+свк",
|
||||
"(?i)каталог",
|
||||
"(?i)найден",
|
||||
"(?i)наиболее вероятн"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)получили",
|
||||
"(?i)заплатили",
|
||||
"(?i)нетто",
|
||||
"(?i)оборот",
|
||||
"(?i)выручк",
|
||||
"(?i)сумм(а|ы)"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["entity_resolution", "alias_grounding", "followup_anchor"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_incoming_by_resolved_entity",
|
||||
"title": "Incoming value-flow follow-up reuses the resolved counterparty anchor",
|
||||
"question": "сколько получили по нему за 2020 год",
|
||||
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)2020", "(?i)получил|входящ|поступ", "(?i)руб"],
|
||||
"required_answer_patterns_any": ["(?i)группа\\s+свк", "(?i)свк"],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)уточните, какого контрагента",
|
||||
"(?i)по какому контрагенту"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["entity_resolution", "incoming_value_flow", "followup_reuse"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_payout_switch_by_resolved_entity",
|
||||
"title": "Outgoing payment follow-up keeps the same grounded counterparty and checked year",
|
||||
"question": "а теперь сколько заплатили?",
|
||||
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)2020", "(?i)заплатил|исходящ|списан|платеж", "(?i)руб"],
|
||||
"required_answer_patterns_any": ["(?i)группа\\s+свк", "(?i)свк"],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)уточните, какого контрагента",
|
||||
"(?i)по какому контрагенту",
|
||||
"(?i)за какой год"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["entity_resolution", "payout_switch", "followup_reuse", "date_carryover"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_net_after_payout",
|
||||
"title": "Net-flow follow-up reuses the same grounded counterparty and checked year after payout",
|
||||
"question": "а какое нетто?",
|
||||
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)2020", "(?i)нетто|сальдо", "(?i)руб"],
|
||||
"required_answer_patterns_any": ["(?i)получ", "(?i)заплат", "(?i)группа\\s+свк", "(?i)свк"],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)уточните, какого контрагента",
|
||||
"(?i)по какому контрагенту"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["entity_resolution", "net_value_flow", "followup_reuse"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_documents_after_net",
|
||||
"title": "Document evidence follow-up keeps the grounded counterparty after the net answer",
|
||||
"question": "а по документам?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)документ|счет|накладн|акт"],
|
||||
"required_answer_patterns_any": ["(?i)группа\\s+свк", "(?i)свк", "(?i)2020"],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)уточните, какого контрагента",
|
||||
"(?i)по какому контрагенту",
|
||||
"(?i)сколько получили",
|
||||
"(?i)сколько заплатили",
|
||||
"(?i)нетто"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["entity_resolution", "document_evidence", "value_flow_pivot", "followup_reuse"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_06_movements_after_documents",
|
||||
"title": "Movement evidence follow-up keeps the grounded counterparty after the document answer",
|
||||
"question": "а по движениям?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": ["(?i)движени|операц|платеж|списан|поступ"],
|
||||
"required_answer_patterns_any": ["(?i)группа\\s+свк", "(?i)свк", "(?i)2020"],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)не найден контрагент",
|
||||
"(?i)уточните, какого контрагента",
|
||||
"(?i)по какому контрагенту",
|
||||
"(?i)сколько получили",
|
||||
"(?i)сколько заплатили",
|
||||
"(?i)нетто"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["entity_resolution", "movement_evidence", "document_pivot", "followup_reuse"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -225,6 +225,7 @@ function runAssistantAddressLaneResponseRuntime(input) {
|
|||
const mcpDiscoveryResponsePolicy = (0, assistantMcpDiscoveryResponsePolicy_1.applyAssistantMcpDiscoveryResponsePolicy)({
|
||||
currentReply: guardedResponse.assistantReply,
|
||||
currentReplySource: "address_query_runtime_v1",
|
||||
currentReplyType: guardedResponse.replyType,
|
||||
addressRuntimeMeta: debugWithResponseGuard
|
||||
});
|
||||
const finalAssistantReply = mcpDiscoveryResponsePolicy.applied
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ async function buildAssistantAddressOrchestrationRuntime(input) {
|
|||
};
|
||||
carryover = input.resolveAddressFollowupCarryoverContext(input.userMessage, input.sessionItems, addressInputMessage, addressPreDecompose, input.sessionAddressNavigationState);
|
||||
}
|
||||
const followupContext = carryover?.followupContext ?? null;
|
||||
const followupContext = toRecordObject(carryover?.followupContext);
|
||||
const routePolicyRuntime = (0, assistantRoutePolicyRuntimeAdapter_1.runAssistantRoutePolicyRuntime)({
|
||||
rawUserMessage: input.userMessage,
|
||||
effectiveAddressUserMessage: addressInputMessage,
|
||||
|
|
@ -159,7 +159,8 @@ async function buildAssistantAddressOrchestrationRuntime(input) {
|
|||
userMessage: input.userMessage,
|
||||
effectiveMessage: addressInputMessage,
|
||||
assistantTurnMeaning: toRecordObject(orchestrationContract?.assistant_turn_meaning),
|
||||
predecomposeContract
|
||||
predecomposeContract,
|
||||
followupContext
|
||||
}));
|
||||
}
|
||||
catch (error) {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,18 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.readAssistantMcpDiscoveryEntityResolutionStatus = readAssistantMcpDiscoveryEntityResolutionStatus;
|
||||
exports.readAssistantMcpDiscoveryEntityAmbiguityCandidates = readAssistantMcpDiscoveryEntityAmbiguityCandidates;
|
||||
exports.readAssistantMcpDiscoveryEntityCandidates = readAssistantMcpDiscoveryEntityCandidates;
|
||||
exports.readAssistantMcpDiscoveryPilotScope = readAssistantMcpDiscoveryPilotScope;
|
||||
exports.readAssistantMcpDiscoveryMetadataRouteFamily = readAssistantMcpDiscoveryMetadataRouteFamily;
|
||||
exports.readAssistantMcpDiscoveryMetadataSelectedEntitySet = readAssistantMcpDiscoveryMetadataSelectedEntitySet;
|
||||
exports.readAssistantMcpDiscoveryMetadataAmbiguityDetected = readAssistantMcpDiscoveryMetadataAmbiguityDetected;
|
||||
exports.readAssistantMcpDiscoveryMetadataAmbiguityEntitySets = readAssistantMcpDiscoveryMetadataAmbiguityEntitySets;
|
||||
exports.formatIsoDateForReply = formatIsoDateForReply;
|
||||
exports.readAddressDebugFilters = readAddressDebugFilters;
|
||||
exports.readAddressDebugItem = readAddressDebugItem;
|
||||
exports.readAddressDebugCounterparty = readAddressDebugCounterparty;
|
||||
exports.readAddressDebugIntent = readAddressDebugIntent;
|
||||
exports.readAddressDebugOrganization = readAddressDebugOrganization;
|
||||
exports.readAddressDebugScopedDate = readAddressDebugScopedDate;
|
||||
exports.readAddressDebugTemporalScope = readAddressDebugTemporalScope;
|
||||
|
|
@ -41,6 +51,219 @@ function toRecordObject(value) {
|
|||
}
|
||||
return value;
|
||||
}
|
||||
function candidateValue(value) {
|
||||
const direct = fallbackToNonEmptyString(value);
|
||||
if (direct && direct !== "[object Object]") {
|
||||
return direct;
|
||||
}
|
||||
const record = toRecordObject(value);
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
return (fallbackToNonEmptyString(record.value) ??
|
||||
fallbackToNonEmptyString(record.name) ??
|
||||
fallbackToNonEmptyString(record.ref) ??
|
||||
fallbackToNonEmptyString(record.text));
|
||||
}
|
||||
function readAssistantMcpDiscoveryEntry(debug) {
|
||||
const entry = toRecordObject(debug?.assistant_mcp_discovery_entry_point_v1);
|
||||
return fallbackToNonEmptyString(entry?.schema_version) === "assistant_mcp_discovery_runtime_entry_point_v1"
|
||||
? entry
|
||||
: null;
|
||||
}
|
||||
function readAssistantMcpDiscoveryTurnMeaning(debug) {
|
||||
const entry = readAssistantMcpDiscoveryEntry(debug);
|
||||
const turnInput = toRecordObject(entry?.turn_input);
|
||||
return toRecordObject(turnInput?.turn_meaning_ref);
|
||||
}
|
||||
function readAssistantMcpDiscoveryTurnMeaningMetadataAmbiguityEntitySets(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
const values = readAssistantMcpDiscoveryTurnMeaning(debug)?.metadata_ambiguity_entity_sets;
|
||||
if (!Array.isArray(values)) {
|
||||
return [];
|
||||
}
|
||||
return values.map((item) => toNonEmptyString(item)).filter((item) => Boolean(item));
|
||||
}
|
||||
function readAssistantMcpDiscoveryActionFamily(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
return toNonEmptyString(readAssistantMcpDiscoveryTurnMeaning(debug)?.asked_action_family);
|
||||
}
|
||||
function readAssistantMcpDiscoveryBridge(debug) {
|
||||
return toRecordObject(readAssistantMcpDiscoveryEntry(debug)?.bridge);
|
||||
}
|
||||
function readAssistantMcpDiscoveryDerivedMetadataSurface(debug) {
|
||||
const bridge = readAssistantMcpDiscoveryBridge(debug);
|
||||
const pilot = toRecordObject(bridge?.pilot);
|
||||
return toRecordObject(pilot?.derived_metadata_surface);
|
||||
}
|
||||
function readAssistantMcpDiscoveryDerivedEntityResolution(debug) {
|
||||
const bridge = readAssistantMcpDiscoveryBridge(debug);
|
||||
const pilot = toRecordObject(bridge?.pilot);
|
||||
return toRecordObject(pilot?.derived_entity_resolution);
|
||||
}
|
||||
function readAssistantMcpDiscoveryEntityResolutionStatus(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
return toNonEmptyString(readAssistantMcpDiscoveryDerivedEntityResolution(debug)?.resolution_status);
|
||||
}
|
||||
function readAssistantMcpDiscoveryEntityAmbiguityCandidates(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
const values = readAssistantMcpDiscoveryDerivedEntityResolution(debug)?.ambiguity_candidates;
|
||||
if (!Array.isArray(values)) {
|
||||
return [];
|
||||
}
|
||||
return values.map((item) => toNonEmptyString(item)).filter((item) => Boolean(item));
|
||||
}
|
||||
function collectAssistantMcpDiscoveryEntityCandidates(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
const result = [];
|
||||
const resolution = readAssistantMcpDiscoveryDerivedEntityResolution(debug);
|
||||
const pushCandidate = (value) => {
|
||||
const text = toNonEmptyString(value);
|
||||
if (text && !result.includes(text)) {
|
||||
result.push(text);
|
||||
}
|
||||
};
|
||||
pushCandidate(resolution?.resolved_entity);
|
||||
pushCandidate(resolution?.requested_entity);
|
||||
if (Array.isArray(resolution?.ambiguity_candidates)) {
|
||||
for (const candidate of resolution.ambiguity_candidates) {
|
||||
pushCandidate(candidate);
|
||||
}
|
||||
}
|
||||
const discoveryMeaning = readAssistantMcpDiscoveryTurnMeaning(debug);
|
||||
const explicitEntities = Array.isArray(discoveryMeaning?.explicit_entity_candidates)
|
||||
? discoveryMeaning.explicit_entity_candidates
|
||||
: [];
|
||||
for (const entity of explicitEntities) {
|
||||
pushCandidate(candidateValue(entity));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function readAssistantMcpDiscoveryEntityCandidates(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
return collectAssistantMcpDiscoveryEntityCandidates(debug, toNonEmptyString);
|
||||
}
|
||||
function readAssistantMcpDiscoveryPilotScope(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
const bridge = readAssistantMcpDiscoveryBridge(debug);
|
||||
const pilot = toRecordObject(bridge?.pilot);
|
||||
return toNonEmptyString(pilot?.pilot_scope);
|
||||
}
|
||||
function readAssistantMcpDiscoveryMetadataRouteFamily(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
return toNonEmptyString(readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.downstream_route_family);
|
||||
}
|
||||
function readAssistantMcpDiscoveryMetadataSelectedEntitySet(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
return toNonEmptyString(readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.selected_entity_set);
|
||||
}
|
||||
function readAssistantMcpDiscoveryMetadataAmbiguityDetected(debug) {
|
||||
return (readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.ambiguity_detected === true ||
|
||||
readAssistantMcpDiscoveryTurnMeaningMetadataAmbiguityEntitySets(debug).length > 0);
|
||||
}
|
||||
function readAssistantMcpDiscoveryMetadataAmbiguityEntitySets(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
const values = readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.ambiguity_entity_sets;
|
||||
if (Array.isArray(values)) {
|
||||
return values.map((item) => toNonEmptyString(item)).filter((item) => Boolean(item));
|
||||
}
|
||||
return readAssistantMcpDiscoveryTurnMeaningMetadataAmbiguityEntitySets(debug, toNonEmptyString);
|
||||
}
|
||||
function mapAssistantMcpDiscoveryPilotScopeToAddressIntent(pilotScope, actionFamily) {
|
||||
if (pilotScope === "counterparty_lifecycle_query_documents_v1") {
|
||||
return "counterparty_activity_lifecycle";
|
||||
}
|
||||
if (pilotScope === "counterparty_document_evidence_query_documents_v1") {
|
||||
return "list_documents_by_counterparty";
|
||||
}
|
||||
if (pilotScope === "counterparty_movement_evidence_query_movements_v1") {
|
||||
return "bank_operations_by_counterparty";
|
||||
}
|
||||
if (pilotScope === "counterparty_supplier_payout_query_movements_v1") {
|
||||
return "supplier_payouts_profile";
|
||||
}
|
||||
if (pilotScope === "counterparty_value_flow_query_movements_v1") {
|
||||
return "customer_revenue_and_payments";
|
||||
}
|
||||
if (pilotScope === "counterparty_bidirectional_value_flow_query_movements_v1") {
|
||||
return null;
|
||||
}
|
||||
if (actionFamily === "activity_duration") {
|
||||
return "counterparty_activity_lifecycle";
|
||||
}
|
||||
if (actionFamily === "payout") {
|
||||
return "supplier_payouts_profile";
|
||||
}
|
||||
if (actionFamily === "turnover") {
|
||||
return "customer_revenue_and_payments";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function readDiscoveryDateScopeFilters(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
const explicitDateScope = toNonEmptyString(readAssistantMcpDiscoveryTurnMeaning(debug)?.explicit_date_scope);
|
||||
if (!explicitDateScope) {
|
||||
return {
|
||||
asOfDate: null,
|
||||
periodFrom: null,
|
||||
periodTo: null
|
||||
};
|
||||
}
|
||||
const isoDateMatch = explicitDateScope.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
if (isoDateMatch) {
|
||||
return {
|
||||
asOfDate: explicitDateScope,
|
||||
periodFrom: null,
|
||||
periodTo: null
|
||||
};
|
||||
}
|
||||
const monthMatch = explicitDateScope.match(/^(\d{4})-(\d{2})$/);
|
||||
if (monthMatch) {
|
||||
const year = Number(monthMatch[1]);
|
||||
const month = Number(monthMatch[2]);
|
||||
if (Number.isFinite(year) && Number.isFinite(month) && month >= 1 && month <= 12) {
|
||||
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
||||
return {
|
||||
asOfDate: null,
|
||||
periodFrom: `${monthMatch[1]}-${monthMatch[2]}-01`,
|
||||
periodTo: `${monthMatch[1]}-${monthMatch[2]}-${String(lastDay).padStart(2, "0")}`
|
||||
};
|
||||
}
|
||||
}
|
||||
const yearMatch = explicitDateScope.match(/^(\d{4})$/);
|
||||
if (yearMatch) {
|
||||
return {
|
||||
asOfDate: null,
|
||||
periodFrom: `${yearMatch[1]}-01-01`,
|
||||
periodTo: `${yearMatch[1]}-12-31`
|
||||
};
|
||||
}
|
||||
const rangeMatch = explicitDateScope.match(/^(\d{4}-\d{2}-\d{2})\.\.(\d{4}-\d{2}-\d{2})$/);
|
||||
if (rangeMatch) {
|
||||
return {
|
||||
asOfDate: null,
|
||||
periodFrom: rangeMatch[1],
|
||||
periodTo: rangeMatch[2]
|
||||
};
|
||||
}
|
||||
return {
|
||||
asOfDate: null,
|
||||
periodFrom: null,
|
||||
periodTo: null
|
||||
};
|
||||
}
|
||||
function formatDiscoveryDateScopeForReply(value) {
|
||||
const text = fallbackToNonEmptyString(value);
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
return formatIsoDateForReply(text) ?? text;
|
||||
}
|
||||
function hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
if (!debug || debug.mcp_discovery_response_applied !== true) {
|
||||
return false;
|
||||
}
|
||||
const entry = readAssistantMcpDiscoveryEntry(debug);
|
||||
const bridge = readAssistantMcpDiscoveryBridge(debug);
|
||||
const bridgeStatus = toNonEmptyString(bridge?.bridge_status);
|
||||
const answerDraft = toRecordObject(bridge?.answer_draft);
|
||||
const answerMode = toNonEmptyString(answerDraft?.answer_mode);
|
||||
return Boolean(entry &&
|
||||
toNonEmptyString(entry.entry_status) === "bridge_executed" &&
|
||||
bridgeStatus === "answer_draft_ready" &&
|
||||
(bridge?.business_fact_answer_allowed === true ||
|
||||
answerMode === "confirmed_with_bounded_inference" ||
|
||||
answerMode === "bounded_inference_only"));
|
||||
}
|
||||
function mergeKnownOrganizationsDefault(values) {
|
||||
return (0, assistantOrganizationMatcher_1.mergeKnownOrganizations)(values);
|
||||
}
|
||||
|
|
@ -65,25 +288,62 @@ function readAddressDebugItem(debug, toNonEmptyString = fallbackToNonEmptyString
|
|||
? toNonEmptyString(debug?.anchor_value_resolved) ?? toNonEmptyString(debug?.anchor_value_raw)
|
||||
: null));
|
||||
}
|
||||
function readAddressDebugCounterparty(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
const extractedFilters = readAddressDebugFilters(debug);
|
||||
if (toNonEmptyString(extractedFilters?.counterparty)) {
|
||||
return toNonEmptyString(extractedFilters?.counterparty);
|
||||
}
|
||||
if (String(debug?.anchor_type ?? "") === "counterparty") {
|
||||
return toNonEmptyString(debug?.anchor_value_resolved) ?? toNonEmptyString(debug?.anchor_value_raw);
|
||||
}
|
||||
const discoveryEntities = collectAssistantMcpDiscoveryEntityCandidates(debug, toNonEmptyString);
|
||||
for (const entity of discoveryEntities) {
|
||||
const text = toNonEmptyString(entity);
|
||||
if (text) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function readAddressDebugIntent(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
const detectedIntent = toNonEmptyString(debug?.detected_intent);
|
||||
if (detectedIntent && detectedIntent !== "unknown") {
|
||||
return detectedIntent;
|
||||
}
|
||||
return mapAssistantMcpDiscoveryPilotScopeToAddressIntent(readAssistantMcpDiscoveryPilotScope(debug, toNonEmptyString), readAssistantMcpDiscoveryActionFamily(debug, toNonEmptyString));
|
||||
}
|
||||
function readAddressDebugOrganization(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
const extractedFilters = readAddressDebugFilters(debug);
|
||||
const rootFrameContext = toRecordObject(debug?.address_root_frame_context);
|
||||
return toNonEmptyString(extractedFilters?.organization) ?? toNonEmptyString(rootFrameContext?.organization);
|
||||
const discoveryMeaning = readAssistantMcpDiscoveryTurnMeaning(debug);
|
||||
return (toNonEmptyString(extractedFilters?.organization) ??
|
||||
toNonEmptyString(rootFrameContext?.organization) ??
|
||||
toNonEmptyString(discoveryMeaning?.explicit_organization_scope) ??
|
||||
toNonEmptyString(debug?.assistant_active_organization) ??
|
||||
toNonEmptyString(debug?.living_chat_selected_organization));
|
||||
}
|
||||
function readAddressDebugScopedDate(debug) {
|
||||
const extractedFilters = readAddressDebugFilters(debug);
|
||||
const rootFrameContext = toRecordObject(debug?.address_root_frame_context);
|
||||
return (formatIsoDateForReply(extractedFilters?.as_of_date) ??
|
||||
formatIsoDateForReply(rootFrameContext?.as_of_date) ??
|
||||
formatIsoDateForReply(extractedFilters?.period_to));
|
||||
formatIsoDateForReply(extractedFilters?.period_to) ??
|
||||
formatDiscoveryDateScopeForReply(readAssistantMcpDiscoveryTurnMeaning(debug)?.explicit_date_scope));
|
||||
}
|
||||
function readAddressDebugTemporalScope(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
const extractedFilters = readAddressDebugFilters(debug);
|
||||
const rootFrameContext = toRecordObject(debug?.address_root_frame_context);
|
||||
const discoveryDateScope = readDiscoveryDateScopeFilters(debug, toNonEmptyString);
|
||||
return {
|
||||
asOfDate: toNonEmptyString(extractedFilters?.as_of_date) ?? toNonEmptyString(rootFrameContext?.as_of_date),
|
||||
periodFrom: toNonEmptyString(extractedFilters?.period_from) ?? toNonEmptyString(rootFrameContext?.period_from),
|
||||
periodTo: toNonEmptyString(extractedFilters?.period_to) ?? toNonEmptyString(rootFrameContext?.period_to)
|
||||
asOfDate: toNonEmptyString(extractedFilters?.as_of_date) ??
|
||||
toNonEmptyString(rootFrameContext?.as_of_date) ??
|
||||
discoveryDateScope.asOfDate,
|
||||
periodFrom: toNonEmptyString(extractedFilters?.period_from) ??
|
||||
toNonEmptyString(rootFrameContext?.period_from) ??
|
||||
discoveryDateScope.periodFrom,
|
||||
periodTo: toNonEmptyString(extractedFilters?.period_to) ??
|
||||
toNonEmptyString(rootFrameContext?.period_to) ??
|
||||
discoveryDateScope.periodTo
|
||||
};
|
||||
}
|
||||
function resolveAddressDebugAnchorContext(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
|
|
@ -110,6 +370,13 @@ function resolveAddressDebugAnchorContext(debug, toNonEmptyString = fallbackToNo
|
|||
anchorValue: counterparty
|
||||
};
|
||||
}
|
||||
const discoveryCounterparty = readAddressDebugCounterparty(debug, toNonEmptyString);
|
||||
if (discoveryCounterparty) {
|
||||
return {
|
||||
anchorType: "counterparty",
|
||||
anchorValue: discoveryCounterparty
|
||||
};
|
||||
}
|
||||
const account = toNonEmptyString(extractedFilters?.account);
|
||||
if (account) {
|
||||
return {
|
||||
|
|
@ -149,6 +416,7 @@ function resolveNavigationSessionContextState(addressNavigationState, toNonEmpty
|
|||
function resolveAddressDebugContextFacts(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
return {
|
||||
item: readAddressDebugItem(debug, toNonEmptyString),
|
||||
counterparty: readAddressDebugCounterparty(debug, toNonEmptyString),
|
||||
organization: readAddressDebugOrganization(debug, toNonEmptyString),
|
||||
scopedDate: readAddressDebugScopedDate(debug)
|
||||
};
|
||||
|
|
@ -156,6 +424,31 @@ function resolveAddressDebugContextFacts(debug, toNonEmptyString = fallbackToNon
|
|||
function resolveAddressDebugCarryoverFilters(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
const extractedFilters = readAddressDebugFilters(debug);
|
||||
const nextFilters = extractedFilters ? { ...extractedFilters } : {};
|
||||
const discoveryDateScope = readDiscoveryDateScopeFilters(debug, toNonEmptyString);
|
||||
const preferGroundedDiscoveryDateScope = hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString) &&
|
||||
Boolean(discoveryDateScope.asOfDate || discoveryDateScope.periodFrom || discoveryDateScope.periodTo);
|
||||
const counterparty = readAddressDebugCounterparty(debug, toNonEmptyString);
|
||||
const organization = readAddressDebugOrganization(debug, toNonEmptyString);
|
||||
if (counterparty && !toNonEmptyString(nextFilters.counterparty)) {
|
||||
nextFilters.counterparty = counterparty;
|
||||
}
|
||||
if (organization && !toNonEmptyString(nextFilters.organization)) {
|
||||
nextFilters.organization = organization;
|
||||
}
|
||||
if (discoveryDateScope.asOfDate && (preferGroundedDiscoveryDateScope || !toNonEmptyString(nextFilters.as_of_date))) {
|
||||
nextFilters.as_of_date = discoveryDateScope.asOfDate;
|
||||
delete nextFilters.period_from;
|
||||
delete nextFilters.period_to;
|
||||
}
|
||||
if (discoveryDateScope.periodFrom &&
|
||||
(preferGroundedDiscoveryDateScope || !toNonEmptyString(nextFilters.period_from))) {
|
||||
nextFilters.period_from = discoveryDateScope.periodFrom;
|
||||
}
|
||||
if (discoveryDateScope.periodTo &&
|
||||
(preferGroundedDiscoveryDateScope || !toNonEmptyString(nextFilters.period_to))) {
|
||||
nextFilters.period_to = discoveryDateScope.periodTo;
|
||||
delete nextFilters.as_of_date;
|
||||
}
|
||||
const inventoryRootFrame = buildInventoryRootFrameFromAddressDebug(debug, toNonEmptyString);
|
||||
const rootFilters = inventoryRootFrame?.filters && typeof inventoryRootFrame.filters === "object"
|
||||
? inventoryRootFrame.filters
|
||||
|
|
@ -538,13 +831,12 @@ function isGroundedAddressDebug(debug, toNonEmptyString = fallbackToNonEmptyStri
|
|||
if (!debug || typeof debug !== "object") {
|
||||
return false;
|
||||
}
|
||||
const executionLane = toNonEmptyString(debug.execution_lane);
|
||||
if (executionLane !== "address_query") {
|
||||
return false;
|
||||
}
|
||||
const answerGroundingCheck = toRecordObject(debug.answer_grounding_check);
|
||||
const groundingStatus = toNonEmptyString(answerGroundingCheck?.status);
|
||||
return groundingStatus === "grounded";
|
||||
if (groundingStatus === "grounded" && toNonEmptyString(debug.execution_lane) === "address_query") {
|
||||
return true;
|
||||
}
|
||||
return hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString);
|
||||
}
|
||||
function isGroundedInventoryContextDebug(debug, toNonEmptyString) {
|
||||
if (!isGroundedAddressDebug(debug, toNonEmptyString)) {
|
||||
|
|
|
|||
|
|
@ -113,10 +113,26 @@ async function runAssistantLivingChatRuntime(input) {
|
|||
: "deterministic_data_scope_contract";
|
||||
}
|
||||
else if (unsupportedCurrentTurnMeaningBoundary) {
|
||||
chatText = buildUnsupportedCurrentTurnMeaningBoundaryReply({
|
||||
assistantTurnMeaning
|
||||
});
|
||||
livingChatSource = "deterministic_unsupported_current_turn_boundary";
|
||||
const unsupportedFamily = typeof assistantTurnMeaning?.unsupported_but_understood_family === "string"
|
||||
? assistantTurnMeaning.unsupported_but_understood_family
|
||||
: null;
|
||||
if (unsupportedFamily === "broad_business_evaluation") {
|
||||
const scopedOrganization = selectedOrganization ?? activeOrganization ?? continuityActiveOrganization ?? null;
|
||||
chatText = (0, assistantMemoryRecapPolicy_1.buildBroadBusinessEvaluationReply)({
|
||||
organization: scopedOrganization,
|
||||
addressDebug: continuitySnapshot.lastGroundedAddressDebug,
|
||||
sessionItems: input.sessionItems,
|
||||
toNonEmptyString: input.toNonEmptyString
|
||||
});
|
||||
activeOrganization = scopedOrganization ?? activeOrganization;
|
||||
livingChatSource = "deterministic_broad_business_evaluation_contract";
|
||||
}
|
||||
else {
|
||||
chatText = buildUnsupportedCurrentTurnMeaningBoundaryReply({
|
||||
assistantTurnMeaning
|
||||
});
|
||||
livingChatSource = "deterministic_unsupported_current_turn_boundary";
|
||||
}
|
||||
}
|
||||
else if ((selectedOrganization || activeOrganization) && input.hasOrganizationFactLookupSignal(userMessage)) {
|
||||
const scopedOrganization = selectedOrganization ?? activeOrganization ?? null;
|
||||
|
|
|
|||
|
|
@ -27,6 +27,12 @@ function uniqueStrings(values) {
|
|||
}
|
||||
return result;
|
||||
}
|
||||
function formatNamedChoiceList(values) {
|
||||
return uniqueStrings(values)
|
||||
.slice(0, 6)
|
||||
.map((value, index) => `${index + 1}. ${value}`)
|
||||
.join("; ");
|
||||
}
|
||||
function isInternalMechanicsLine(value) {
|
||||
const text = value.toLowerCase();
|
||||
return (text.includes("primitive") ||
|
||||
|
|
@ -51,6 +57,11 @@ function modeFor(pilot) {
|
|||
if (pilot.pilot_status === "skipped_needs_clarification") {
|
||||
return "needs_clarification";
|
||||
}
|
||||
if (pilot.pilot_scope === "entity_resolution_search_v1" &&
|
||||
(pilot.reason_codes.includes("pilot_entity_resolution_ambiguity_requires_clarification") ||
|
||||
pilot.derived_entity_resolution?.resolution_status === "ambiguous")) {
|
||||
return "needs_clarification";
|
||||
}
|
||||
if (pilot.evidence.answer_permission === "confirmed_answer") {
|
||||
return "confirmed_with_bounded_inference";
|
||||
}
|
||||
|
|
@ -64,7 +75,162 @@ function isValueFlowPilot(pilot) {
|
|||
pilot.pilot_scope === "counterparty_supplier_payout_query_movements_v1" ||
|
||||
pilot.pilot_scope === "counterparty_bidirectional_value_flow_query_movements_v1");
|
||||
}
|
||||
function isDocumentPilot(pilot) {
|
||||
return pilot.pilot_scope === "counterparty_document_evidence_query_documents_v1";
|
||||
}
|
||||
function isMovementPilot(pilot) {
|
||||
return pilot.pilot_scope === "counterparty_movement_evidence_query_movements_v1";
|
||||
}
|
||||
function isMetadataPilot(pilot) {
|
||||
return pilot.pilot_scope === "metadata_inspection_v1";
|
||||
}
|
||||
function isEntityResolutionPilot(pilot) {
|
||||
return pilot.pilot_scope === "entity_resolution_search_v1";
|
||||
}
|
||||
function isMetadataLaneChoiceClarification(pilot) {
|
||||
return (pilot.reason_codes.includes("planner_selected_metadata_lane_clarification_recipe") ||
|
||||
pilot.dry_run.reason_codes.includes("planner_selected_metadata_lane_clarification_recipe"));
|
||||
}
|
||||
function askedActionFamily(pilot) {
|
||||
const action = pilot.evidence.query_plan.turn_meaning_ref?.asked_action_family;
|
||||
if (typeof action !== "string") {
|
||||
return null;
|
||||
}
|
||||
const normalized = action.trim().toLowerCase();
|
||||
return normalized.length > 0 ? normalized : null;
|
||||
}
|
||||
function unsupportedFamily(pilot) {
|
||||
const unsupported = pilot.evidence.query_plan.turn_meaning_ref?.unsupported_but_understood_family;
|
||||
if (typeof unsupported !== "string") {
|
||||
return null;
|
||||
}
|
||||
const normalized = unsupported.trim().toLowerCase();
|
||||
return normalized.length > 0 ? normalized : null;
|
||||
}
|
||||
function firstEntityCandidate(pilot) {
|
||||
const values = Array.isArray(pilot.evidence.query_plan.turn_meaning_ref?.explicit_entity_candidates)
|
||||
? pilot.evidence.query_plan.turn_meaning_ref?.explicit_entity_candidates
|
||||
: [];
|
||||
for (const value of values) {
|
||||
const text = String(value ?? "").trim();
|
||||
if (text) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function explicitDateScope(pilot) {
|
||||
const value = pilot.evidence.query_plan.turn_meaning_ref?.explicit_date_scope;
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
const normalized = value.trim();
|
||||
return normalized.length > 0 ? normalized : null;
|
||||
}
|
||||
function documentOrMovementScopeRu(pilot) {
|
||||
const entity = firstEntityCandidate(pilot);
|
||||
const period = explicitDateScope(pilot);
|
||||
const entityPart = entity ? ` по контрагенту ${entity}` : "";
|
||||
const periodPart = period ? ` за ${period}` : " в проверенном окне";
|
||||
return `${entityPart}${periodPart}`;
|
||||
}
|
||||
function isMovementLaneClarification(pilot) {
|
||||
return (isMovementPilot(pilot) ||
|
||||
pilot.reason_codes.includes("planner_selected_movement_recipe") ||
|
||||
pilot.dry_run.reason_codes.includes("planner_selected_movement_recipe") ||
|
||||
askedActionFamily(pilot) === "list_movements" ||
|
||||
unsupportedFamily(pilot) === "movement_evidence");
|
||||
}
|
||||
function isDocumentLaneClarification(pilot) {
|
||||
return (isDocumentPilot(pilot) ||
|
||||
pilot.reason_codes.includes("planner_selected_document_recipe") ||
|
||||
pilot.dry_run.reason_codes.includes("planner_selected_document_recipe") ||
|
||||
askedActionFamily(pilot) === "list_documents" ||
|
||||
unsupportedFamily(pilot) === "document_evidence");
|
||||
}
|
||||
function laneScopeSuffix(pilot) {
|
||||
const entity = firstEntityCandidate(pilot);
|
||||
return entity ? ` по "${entity}"` : "";
|
||||
}
|
||||
function dryRunMissingAxis(pilot, axis) {
|
||||
return pilot.dry_run.execution_steps.some((step) => step.missing_axis_options.some((option) => option.includes(axis)));
|
||||
}
|
||||
function clarificationNeedRu(pilot) {
|
||||
const needsPeriod = dryRunMissingAxis(pilot, "period");
|
||||
const needsOrganization = dryRunMissingAxis(pilot, "organization");
|
||||
if (needsPeriod && needsOrganization) {
|
||||
return { subject: "проверяемый период и организацию", verb: "нужно" };
|
||||
}
|
||||
if (needsPeriod) {
|
||||
return { subject: "проверяемый период", verb: "нужен" };
|
||||
}
|
||||
if (needsOrganization) {
|
||||
return { subject: "организацию", verb: "нужно" };
|
||||
}
|
||||
return { subject: "контекст проверки", verb: "нужно" };
|
||||
}
|
||||
function clarificationNextStepLine(pilot, laneLabel) {
|
||||
const needsPeriod = dryRunMissingAxis(pilot, "period");
|
||||
const needsOrganization = dryRunMissingAxis(pilot, "organization");
|
||||
const scopeSuffix = laneScopeSuffix(pilot);
|
||||
if (needsPeriod && needsOrganization) {
|
||||
return `Уточните период и организацию, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
|
||||
}
|
||||
if (needsPeriod) {
|
||||
return `Уточните период, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
|
||||
}
|
||||
if (needsOrganization) {
|
||||
return `Уточните организацию, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
|
||||
}
|
||||
return `Уточните контекст проверки, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
|
||||
}
|
||||
function metadataRouteFamilyLabelRu(routeFamily) {
|
||||
if (routeFamily === "document_evidence") {
|
||||
return "контур документов";
|
||||
}
|
||||
if (routeFamily === "movement_evidence") {
|
||||
return "контур движений/регистров";
|
||||
}
|
||||
if (routeFamily === "catalog_drilldown") {
|
||||
return "контур справочников и связанных объектов";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function headlineFor(mode, pilot) {
|
||||
const askedMonthlyBreakdown = pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" ||
|
||||
pilot.derived_value_flow?.aggregation_axis === "month";
|
||||
if (isEntityResolutionPilot(pilot) && mode === "confirmed_with_bounded_inference") {
|
||||
return "По каталогу 1С найден вероятный контрагент; это заземление сущности для следующего шага, а не еще бизнес-ответ по данным.";
|
||||
}
|
||||
if (isEntityResolutionPilot(pilot) && mode === "needs_clarification") {
|
||||
return "По каталогу 1С нашлось несколько похожих контрагентов, и без уточнения нельзя честно выбрать правильную сущность.";
|
||||
}
|
||||
if (isEntityResolutionPilot(pilot) &&
|
||||
mode === "checked_sources_only" &&
|
||||
pilot.derived_entity_resolution?.resolution_status === "not_found") {
|
||||
return "По текущему каталожному поиску 1С точный контрагент пока не подтвержден.";
|
||||
}
|
||||
if (isMovementPilot(pilot) && mode === "confirmed_with_bounded_inference") {
|
||||
return `По движениям${documentOrMovementScopeRu(pilot)} в 1С найдены подтвержденные строки; ответ ограничен проверенным окном и найденными строками.`;
|
||||
}
|
||||
if (pilot.derived_metadata_surface && mode === "confirmed_with_bounded_inference") {
|
||||
if (pilot.derived_metadata_surface.ambiguity_detected) {
|
||||
return "По метаданным 1С найдены конкурирующие schema-поверхности; перед следующим шагом нужно удержать неоднозначность явно.";
|
||||
}
|
||||
if (pilot.derived_metadata_surface.downstream_route_family) {
|
||||
return "По метаданным 1С найдена схема и заземлена вероятная поверхность для следующего безопасного шага.";
|
||||
}
|
||||
return "По метаданным 1С найдена доступная схема для дальнейшего безопасного поиска.";
|
||||
}
|
||||
if (askedMonthlyBreakdown && pilot.derived_bidirectional_value_flow && mode === "confirmed_with_bounded_inference") {
|
||||
return "По данным 1С найдены строки входящих и исходящих денежных движений; нетто и помесячная раскладка могут называться только как расчет по найденным строкам и проверенному периоду.";
|
||||
}
|
||||
if (askedMonthlyBreakdown && pilot.derived_value_flow && mode === "confirmed_with_bounded_inference") {
|
||||
if (pilot.derived_value_flow.value_flow_direction === "outgoing_supplier_payout") {
|
||||
return "По данным 1С найдены строки исходящих платежей/списаний; сумму и помесячную раскладку можно называть только в рамках проверенного периода и найденных строк.";
|
||||
}
|
||||
return "По данным 1С найдены строки входящих денежных поступлений; сумму и помесячную раскладку можно называть только в рамках проверенного периода и найденных строк.";
|
||||
}
|
||||
if (pilot.derived_bidirectional_value_flow && mode === "confirmed_with_bounded_inference") {
|
||||
return "По данным 1С найдены строки входящих и исходящих денежных движений; нетто можно называть только как расчет по найденным строкам и проверенному периоду.";
|
||||
}
|
||||
|
|
@ -72,14 +238,37 @@ function headlineFor(mode, pilot) {
|
|||
if (pilot.derived_value_flow.value_flow_direction === "outgoing_supplier_payout") {
|
||||
return "По данным 1С найдены строки исходящих платежей/списаний; сумму можно называть только в рамках проверенного периода и найденных строк.";
|
||||
}
|
||||
return "По данным 1С найдены строки денежных движений; сумму можно называть только в рамках проверенного периода и найденных строк.";
|
||||
return "По данным 1С найдены строки входящих денежных поступлений; сумму можно называть только в рамках проверенного периода и найденных строк.";
|
||||
}
|
||||
if (isDocumentPilot(pilot) && mode === "confirmed_with_bounded_inference") {
|
||||
return `По документам${documentOrMovementScopeRu(pilot)} в 1С найдены подтвержденные строки; ответ ограничен проверенным окном и найденными строками.`;
|
||||
}
|
||||
if (isMovementPilot(pilot) && mode === "confirmed_with_bounded_inference") {
|
||||
return `По движениям${documentOrMovementScopeRu(pilot)} в 1С найдены подтвержденные строки; ответ ограничен проверенным окном и найденными строками.`;
|
||||
}
|
||||
if (mode === "confirmed_with_bounded_inference") {
|
||||
return "По данным 1С есть подтвержденная активность; длительность можно оценивать только как вывод из этих строк.";
|
||||
}
|
||||
if (isDocumentPilot(pilot) && mode === "bounded_inference_only") {
|
||||
return `По документам${documentOrMovementScopeRu(pilot)} полный срез не подтвержден; пока есть только ограниченная граница проверенного окна 1С.`;
|
||||
}
|
||||
if (isMovementPilot(pilot) && mode === "bounded_inference_only") {
|
||||
return `По движениям${documentOrMovementScopeRu(pilot)} полный срез не подтвержден; пока есть только ограниченная граница проверенного окна 1С.`;
|
||||
}
|
||||
if (mode === "bounded_inference_only") {
|
||||
return "Точный факт не подтвержден, но есть ограниченная оценка по найденной активности в 1С.";
|
||||
}
|
||||
if (mode === "needs_clarification" && isMetadataLaneChoiceClarification(pilot)) {
|
||||
return "По подтвержденной metadata-поверхности видно несколько конкурирующих data-lane, и без явного выбора дальше идти нельзя.";
|
||||
}
|
||||
if (mode === "needs_clarification" && isMovementLaneClarification(pilot)) {
|
||||
const need = clarificationNeedRu(pilot);
|
||||
return `Могу идти дальше по движениям/регистрам${laneScopeSuffix(pilot)}, но для запуска поиска в 1С ${need.verb} ${need.subject}.`;
|
||||
}
|
||||
if (mode === "needs_clarification" && isDocumentLaneClarification(pilot)) {
|
||||
const need = clarificationNeedRu(pilot);
|
||||
return `Могу идти дальше по документам${laneScopeSuffix(pilot)}, но для запуска поиска в 1С ${need.verb} ${need.subject}.`;
|
||||
}
|
||||
if (mode === "needs_clarification") {
|
||||
return "Нужно уточнить контекст перед поиском в 1С.";
|
||||
}
|
||||
|
|
@ -89,9 +278,43 @@ function headlineFor(mode, pilot) {
|
|||
return "Я проверил доступный контур, но подтвержденного факта для ответа не получил.";
|
||||
}
|
||||
function nextStepFor(mode, pilot) {
|
||||
if (isEntityResolutionPilot(pilot) && mode === "needs_clarification") {
|
||||
const ambiguityCandidates = pilot.derived_entity_resolution?.ambiguity_candidates ?? [];
|
||||
if (ambiguityCandidates.length > 0) {
|
||||
return `Уточните, какой именно контрагент нужен: ${formatNamedChoiceList(ambiguityCandidates)}. Можно ответить названием или номером варианта.`;
|
||||
}
|
||||
return "Уточните точное название контрагента или добавьте ИНН, и я продолжу уже по нужной сущности в 1С.";
|
||||
}
|
||||
if (isEntityResolutionPilot(pilot) && mode === "confirmed_with_bounded_inference") {
|
||||
return "Теперь могу продолжить уже по найденному контрагенту и искать документы, движения или денежный поток.";
|
||||
}
|
||||
if (isEntityResolutionPilot(pilot) &&
|
||||
mode === "checked_sources_only" &&
|
||||
pilot.derived_entity_resolution?.resolution_status === "not_found") {
|
||||
return "Дайте точное название или ИНН, и я повторю поиск по каталогу 1С более прицельно.";
|
||||
}
|
||||
if (mode === "needs_clarification" && isMetadataLaneChoiceClarification(pilot)) {
|
||||
return "Уточните, в какой контур идти дальше: по документам или по движениям/регистрам.";
|
||||
}
|
||||
if (mode === "needs_clarification" && isMovementLaneClarification(pilot)) {
|
||||
return clarificationNextStepLine(pilot, "движениям/регистрам");
|
||||
}
|
||||
if (mode === "needs_clarification" && isDocumentLaneClarification(pilot)) {
|
||||
return clarificationNextStepLine(pilot, "документам");
|
||||
}
|
||||
if (mode === "needs_clarification") {
|
||||
return "Уточните контрагента, период или организацию, и я смогу выполнить проверку по 1С.";
|
||||
}
|
||||
if (mode === "confirmed_with_bounded_inference" && pilot.derived_metadata_surface) {
|
||||
const surface = pilot.derived_metadata_surface;
|
||||
if (surface.ambiguity_detected && surface.ambiguity_entity_sets.length > 0) {
|
||||
return `Следующим шагом лучше сузить surface до одного семейства: ${surface.ambiguity_entity_sets.join(", ")}.`;
|
||||
}
|
||||
const routeLabel = metadataRouteFamilyLabelRu(surface.downstream_route_family);
|
||||
if (surface.selected_entity_set && routeLabel) {
|
||||
return `Следующим шагом могу пойти в ${routeLabel} по surface «${surface.selected_entity_set}» и уже искать подтвержденные данные, а не только схему.`;
|
||||
}
|
||||
}
|
||||
if (mode === "checked_sources_only" && pilot.query_limitations.length > 0) {
|
||||
return "Можно повторить проверку после восстановления MCP-доступа или сузить вопрос до конкретного контрагента/периода.";
|
||||
}
|
||||
|
|
@ -113,11 +336,61 @@ function buildMustNotClaim(pilot) {
|
|||
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.");
|
||||
}
|
||||
if (isDocumentPilot(pilot)) {
|
||||
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.");
|
||||
}
|
||||
if (isMovementPilot(pilot)) {
|
||||
claims.push("Do not claim full movement history outside the checked period.");
|
||||
claims.push("Do not present the confirmed movement rows as a complete movement universe.");
|
||||
}
|
||||
if (isMetadataPilot(pilot)) {
|
||||
claims.push("Do not present metadata surface as confirmed business data rows.");
|
||||
claims.push("Do not claim a document/register exists outside the checked metadata probe results.");
|
||||
claims.push("Do not present the inferred next checked lane as already executed data retrieval.");
|
||||
}
|
||||
if (isEntityResolutionPilot(pilot)) {
|
||||
claims.push("Do not present catalog grounding as confirmed business activity, turnover, or document evidence.");
|
||||
claims.push("Do not claim legal identity uniqueness when several catalog candidates are still plausible.");
|
||||
claims.push("Do not imply that the resolved entity has already been used in a downstream data probe.");
|
||||
}
|
||||
if (pilot.evidence.confirmed_facts.length === 0) {
|
||||
claims.push("Do not claim a confirmed business fact when confirmed_facts is empty.");
|
||||
}
|
||||
return claims;
|
||||
}
|
||||
const RU_MONTH_LABELS_SHORT = [
|
||||
"янв",
|
||||
"фев",
|
||||
"мар",
|
||||
"апр",
|
||||
"май",
|
||||
"июн",
|
||||
"июл",
|
||||
"авг",
|
||||
"сен",
|
||||
"окт",
|
||||
"ноя",
|
||||
"дек"
|
||||
];
|
||||
function monthLabelRu(monthBucket) {
|
||||
const match = monthBucket.match(/^(\d{4})-(\d{2})$/);
|
||||
if (!match) {
|
||||
return monthBucket;
|
||||
}
|
||||
const monthIndex = Number(match[2]) - 1;
|
||||
const label = RU_MONTH_LABELS_SHORT[monthIndex] ?? match[2];
|
||||
return `${label} ${match[1]}`;
|
||||
}
|
||||
function netLabelRu(netDirection) {
|
||||
if (netDirection === "net_incoming") {
|
||||
return "нетто в нашу сторону";
|
||||
}
|
||||
if (netDirection === "net_outgoing") {
|
||||
return "нетто исходящее";
|
||||
}
|
||||
return "нетто нулевое";
|
||||
}
|
||||
function derivedActivityInferenceLine(pilot) {
|
||||
const period = pilot.derived_activity_period;
|
||||
if (!period) {
|
||||
|
|
@ -129,6 +402,67 @@ function derivedActivityInferenceLine(pilot) {
|
|||
"Это вывод по данным 1С, а не юридически подтвержденный возраст регистрации."
|
||||
].join(" ");
|
||||
}
|
||||
function derivedMetadataConfirmedLine(pilot) {
|
||||
const surface = pilot.derived_metadata_surface;
|
||||
if (!surface) {
|
||||
return null;
|
||||
}
|
||||
const scope = surface.metadata_scope ? ` по области "${surface.metadata_scope}"` : "";
|
||||
const entitySets = surface.available_entity_sets.length > 0
|
||||
? ` Типы объектов: ${surface.available_entity_sets.join(", ")}.`
|
||||
: "";
|
||||
const objects = surface.matched_objects.length > 0
|
||||
? ` Найденные объекты: ${surface.matched_objects.slice(0, 8).join(", ")}.`
|
||||
: "";
|
||||
const selectedEntitySet = surface.selected_entity_set ? ` Выбранное family: ${surface.selected_entity_set}.` : "";
|
||||
const selectedObjects = surface.selected_surface_objects.length > 0
|
||||
? ` Выбранные surface-объекты: ${surface.selected_surface_objects.slice(0, 6).join(", ")}.`
|
||||
: "";
|
||||
const fields = surface.available_fields.length > 0
|
||||
? ` Доступные поля/секции: ${surface.available_fields.slice(0, 12).join(", ")}.`
|
||||
: "";
|
||||
return `Подтвержденная metadata-поверхность 1С${scope}: ${surface.matched_rows} строк metadata-ответа.${entitySets}${objects}${selectedEntitySet}${selectedObjects}${fields}`.replace(/\s+/g, " ").trim();
|
||||
}
|
||||
function derivedMetadataInferenceLine(pilot) {
|
||||
const surface = pilot.derived_metadata_surface;
|
||||
if (!surface) {
|
||||
return null;
|
||||
}
|
||||
if (surface.ambiguity_detected && surface.ambiguity_entity_sets.length > 0) {
|
||||
return `По подтвержденной metadata-поверхности видно несколько конкурирующих family: ${surface.ambiguity_entity_sets.join(", ")}. Следующий data-lane пока нельзя выбрать без явного сужения.`;
|
||||
}
|
||||
const routeLabel = metadataRouteFamilyLabelRu(surface.downstream_route_family);
|
||||
if (!surface.selected_entity_set || !routeLabel) {
|
||||
return null;
|
||||
}
|
||||
return `По подтвержденной metadata-поверхности следующий проверяемый шаг можно ограниченно оценить как ${routeLabel} через family «${surface.selected_entity_set}». Это еще не выполненный data-fetch, а только grounded выбор следующего контура.`;
|
||||
}
|
||||
function derivedEntityResolutionConfirmedLine(pilot) {
|
||||
const resolution = pilot.derived_entity_resolution;
|
||||
if (!resolution || resolution.resolution_status !== "resolved" || !resolution.resolved_entity) {
|
||||
return null;
|
||||
}
|
||||
const requested = resolution.requested_entity ? ` по запросу "${resolution.requested_entity}"` : "";
|
||||
const confidence = resolution.confidence === "high"
|
||||
? " Точность совпадения выглядит высокой."
|
||||
: resolution.confidence === "medium"
|
||||
? " Совпадение выглядит достаточно сильным, но это все еще catalog grounding."
|
||||
: " Совпадение выглядит вероятным, но его лучше считать рабочим заземлением сущности.";
|
||||
return `В текущем каталожном срезе 1С${requested} найден контрагент "${resolution.resolved_entity}".${confidence}`;
|
||||
}
|
||||
function derivedEntityResolutionInferenceLine(pilot) {
|
||||
const resolution = pilot.derived_entity_resolution;
|
||||
if (!resolution) {
|
||||
return null;
|
||||
}
|
||||
if (resolution.resolution_status === "resolved") {
|
||||
return "Сейчас подтверждено только заземление сущности по каталогу 1С; документы, движения и денежные показатели по ней еще не проверялись.";
|
||||
}
|
||||
if (resolution.resolution_status === "ambiguous" && resolution.ambiguity_candidates.length > 0) {
|
||||
return `В каталоге 1С нашлось несколько близких кандидатов: ${formatNamedChoiceList(resolution.ambiguity_candidates)}. Без уточнения нельзя честно выбрать одного контрагента для следующего шага.`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function derivedValueFlowConfirmedLine(pilot) {
|
||||
const flow = pilot.derived_value_flow;
|
||||
if (!flow) {
|
||||
|
|
@ -136,13 +470,15 @@ function derivedValueFlowConfirmedLine(pilot) {
|
|||
}
|
||||
const counterparty = flow.counterparty ? ` по контрагенту ${flow.counterparty}` : "";
|
||||
const period = flow.period_scope ? ` за период ${flow.period_scope}` : " в проверенном окне";
|
||||
const movementLabel = flow.value_flow_direction === "outgoing_supplier_payout" ? "исходящих платежей/списаний" : "денежных движений";
|
||||
const movementLabel = flow.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "исходящих платежей/списаний"
|
||||
: "входящих денежных поступлений";
|
||||
const totalLabel = flow.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "сумма исходящих платежей/списаний составляет"
|
||||
: "сумма составляет";
|
||||
: "сумма входящих денежных поступлений составляет";
|
||||
const caveat = flow.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "Это расчет по найденным строкам 1С, а не подтверждение полного объема платежей вне проверенного окна."
|
||||
: "Это расчет по найденным строкам 1С, а не подтверждение полного оборота вне проверенного окна.";
|
||||
: "Это расчет по найденным строкам 1С, а не подтверждение полного объема поступлений вне проверенного окна.";
|
||||
const dates = flow.first_movement_date && flow.latest_movement_date
|
||||
? ` Первая найденная дата движения: ${flow.first_movement_date}; последняя: ${flow.latest_movement_date}.`
|
||||
: "";
|
||||
|
|
@ -151,6 +487,19 @@ function derivedValueFlowConfirmedLine(pilot) {
|
|||
: "";
|
||||
return `По найденным строкам ${movementLabel} в 1С${counterparty}${period} ${totalLabel} ${flow.total_amount_human_ru} Учтено строк с суммой: ${flow.rows_with_amount} из ${flow.rows_matched}.${dates}${limitCaveat} ${caveat}`;
|
||||
}
|
||||
function derivedValueFlowMonthlyLines(pilot) {
|
||||
const flow = pilot.derived_value_flow;
|
||||
if (!flow || flow.aggregation_axis !== "month" || flow.monthly_breakdown.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return flow.monthly_breakdown.map((bucket) => {
|
||||
const monthLabel = monthLabelRu(bucket.month_bucket);
|
||||
if (flow.value_flow_direction === "outgoing_supplier_payout") {
|
||||
return `Помесячно: ${monthLabel} — заплатили ${bucket.total_amount_human_ru} по ${bucket.rows_with_amount} строкам с суммой`;
|
||||
}
|
||||
return `Помесячно: ${monthLabel} — получили ${bucket.total_amount_human_ru} по ${bucket.rows_with_amount} строкам с суммой`;
|
||||
});
|
||||
}
|
||||
function sideDateRange(first, latest) {
|
||||
if (first && latest) {
|
||||
return ` первая дата ${first}, последняя ${latest}`;
|
||||
|
|
@ -185,6 +534,13 @@ function derivedBidirectionalValueFlowConfirmedLine(pilot) {
|
|||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
function derivedBidirectionalValueFlowMonthlyLines(pilot) {
|
||||
const flow = pilot.derived_bidirectional_value_flow;
|
||||
if (!flow || flow.aggregation_axis !== "month" || flow.monthly_breakdown.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return flow.monthly_breakdown.map((bucket) => `Помесячно: ${monthLabelRu(bucket.month_bucket)} — получили ${bucket.incoming_total_amount_human_ru}, заплатили ${bucket.outgoing_total_amount_human_ru}, ${netLabelRu(bucket.net_direction)} ${bucket.net_amount_human_ru}`);
|
||||
}
|
||||
function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
|
||||
const mode = modeFor(pilot);
|
||||
const reasonCodes = [...pilot.reason_codes, ...pilot.evidence.reason_codes];
|
||||
|
|
@ -195,14 +551,28 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
|
|||
if (pilot.evidence.inferred_facts.length > 0) {
|
||||
pushReason(reasonCodes, "answer_contains_bounded_inference");
|
||||
}
|
||||
const derivedInferenceLine = derivedActivityInferenceLine(pilot);
|
||||
const derivedInferenceLine = derivedActivityInferenceLine(pilot) ??
|
||||
derivedMetadataInferenceLine(pilot) ??
|
||||
derivedEntityResolutionInferenceLine(pilot);
|
||||
const inferenceLines = derivedInferenceLine
|
||||
? [derivedInferenceLine]
|
||||
: pilot.evidence.inferred_facts;
|
||||
const derivedMetadataLine = derivedMetadataConfirmedLine(pilot);
|
||||
const derivedEntityResolutionLine = derivedEntityResolutionConfirmedLine(pilot);
|
||||
const derivedValueLine = derivedBidirectionalValueFlowConfirmedLine(pilot) ?? derivedValueFlowConfirmedLine(pilot);
|
||||
const monthlyConfirmedLines = derivedBidirectionalValueFlowMonthlyLines(pilot).length > 0
|
||||
? derivedBidirectionalValueFlowMonthlyLines(pilot)
|
||||
: derivedValueFlowMonthlyLines(pilot);
|
||||
if (monthlyConfirmedLines.length > 0) {
|
||||
pushReason(reasonCodes, "answer_contains_monthly_breakdown");
|
||||
}
|
||||
const confirmedLines = derivedValueLine
|
||||
? [...pilot.evidence.confirmed_facts, derivedValueLine]
|
||||
: pilot.evidence.confirmed_facts;
|
||||
? [...pilot.evidence.confirmed_facts, derivedValueLine, ...monthlyConfirmedLines]
|
||||
: derivedEntityResolutionLine
|
||||
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
|
||||
: derivedMetadataLine
|
||||
? [...pilot.evidence.confirmed_facts, derivedMetadataLine]
|
||||
: pilot.evidence.confirmed_facts;
|
||||
return {
|
||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryAnswerAdapter",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -38,6 +38,9 @@ function pushUnique(target, value) {
|
|||
function hasEntity(meaning) {
|
||||
return (meaning?.explicit_entity_candidates?.length ?? 0) > 0;
|
||||
}
|
||||
function aggregationAxis(meaning) {
|
||||
return toNonEmptyString(meaning?.asked_aggregation_axis)?.toLowerCase() ?? null;
|
||||
}
|
||||
function addScopeAxes(axes, meaning) {
|
||||
if (hasEntity(meaning)) {
|
||||
pushUnique(axes, "counterparty");
|
||||
|
|
@ -52,6 +55,24 @@ function addScopeAxes(axes, meaning) {
|
|||
function includesAny(text, tokens) {
|
||||
return tokens.some((token) => text.includes(token));
|
||||
}
|
||||
function isYearDateScope(meaning) {
|
||||
return /^\d{4}$/.test(toNonEmptyString(meaning?.explicit_date_scope) ?? "");
|
||||
}
|
||||
function budgetOverrideFor(input, recipe) {
|
||||
const meaning = input.turnMeaning ?? null;
|
||||
const requestedAggregationAxis = aggregationAxis(meaning);
|
||||
const isValueFlowRecipe = recipe.semanticDataNeed === "counterparty value-flow evidence" &&
|
||||
recipe.primitives.includes("query_movements");
|
||||
if (!isValueFlowRecipe) {
|
||||
return {};
|
||||
}
|
||||
if (requestedAggregationAxis === "month" || isYearDateScope(meaning)) {
|
||||
return {
|
||||
maxProbeCount: 30
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
function recipeFor(input) {
|
||||
const meaning = input.turnMeaning ?? null;
|
||||
const domain = lower(meaning?.asked_domain_family);
|
||||
|
|
@ -59,25 +80,35 @@ function recipeFor(input) {
|
|||
const unsupported = lower(meaning?.unsupported_but_understood_family);
|
||||
const combined = `${domain} ${action} ${unsupported}`.trim();
|
||||
const axes = [];
|
||||
const requestedAggregationAxis = aggregationAxis(meaning);
|
||||
addScopeAxes(axes, meaning);
|
||||
if (includesAny(combined, ["metadata_lane_choice_clarification", "resolve_next_lane"])) {
|
||||
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_recipe"
|
||||
};
|
||||
}
|
||||
if (includesAny(combined, ["turnover", "revenue", "payment", "payout", "value", "net", "netting", "balance", "cashflow"])) {
|
||||
pushUnique(axes, "aggregate_axis");
|
||||
pushUnique(axes, "amount");
|
||||
pushUnique(axes, "coverage_target");
|
||||
if (requestedAggregationAxis === "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: "planner_selected_value_flow_recipe"
|
||||
};
|
||||
}
|
||||
if (includesAny(combined, ["document", "documents"])) {
|
||||
pushUnique(axes, "coverage_target");
|
||||
return {
|
||||
semanticDataNeed: "document evidence",
|
||||
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage"],
|
||||
axes,
|
||||
reason: "planner_selected_document_recipe"
|
||||
reason: requestedAggregationAxis === "month"
|
||||
? "planner_selected_monthly_value_flow_recipe"
|
||||
: "planner_selected_value_flow_recipe"
|
||||
};
|
||||
}
|
||||
if (includesAny(combined, ["lifecycle", "activity", "duration", "age"])) {
|
||||
|
|
@ -86,6 +117,8 @@ function recipeFor(input) {
|
|||
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_recipe"
|
||||
|
|
@ -95,15 +128,42 @@ function recipeFor(input) {
|
|||
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_recipe"
|
||||
};
|
||||
}
|
||||
if (includesAny(combined, ["movement", "movements", "bank_operations", "movement_evidence", "list_movements"])) {
|
||||
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_recipe"
|
||||
};
|
||||
}
|
||||
if (includesAny(combined, ["document", "documents"])) {
|
||||
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_recipe"
|
||||
};
|
||||
}
|
||||
if (hasEntity(meaning)) {
|
||||
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: "planner_selected_entity_resolution_recipe"
|
||||
|
|
@ -111,6 +171,8 @@ function recipeFor(input) {
|
|||
}
|
||||
return {
|
||||
semanticDataNeed: "unclassified 1C discovery need",
|
||||
chainId: "metadata_inspection",
|
||||
chainSummary: "Start with metadata inspection instead of guessing a deeper fact route when the business need is still under-specified.",
|
||||
primitives: ["inspect_1c_metadata"],
|
||||
axes,
|
||||
reason: "planner_selected_clarification_recipe"
|
||||
|
|
@ -127,14 +189,19 @@ function statusFrom(plan, review) {
|
|||
}
|
||||
function planAssistantMcpDiscovery(input) {
|
||||
const recipe = recipeFor(input);
|
||||
const budgetOverride = budgetOverrideFor(input, recipe);
|
||||
const semanticDataNeed = toNonEmptyString(input.semanticDataNeed) ?? recipe.semanticDataNeed;
|
||||
const reasonCodes = [];
|
||||
pushReason(reasonCodes, recipe.reason);
|
||||
if (budgetOverride.maxProbeCount) {
|
||||
pushReason(reasonCodes, "planner_enabled_chunked_coverage_probe_budget");
|
||||
}
|
||||
const plan = (0, assistantMcpDiscoveryPolicy_1.buildAssistantMcpDiscoveryPlan)({
|
||||
semanticDataNeed,
|
||||
turnMeaning: input.turnMeaning,
|
||||
proposedPrimitives: recipe.primitives,
|
||||
requiredAxes: recipe.axes
|
||||
requiredAxes: recipe.axes,
|
||||
maxProbeCount: budgetOverride.maxProbeCount
|
||||
});
|
||||
const review = (0, assistantMcpCatalogIndex_1.reviewAssistantMcpDiscoveryPlanAgainstCatalog)(plan);
|
||||
const plannerStatus = statusFrom(plan, review);
|
||||
|
|
@ -152,6 +219,8 @@ function planAssistantMcpDiscovery(input) {
|
|||
policy_owner: "assistantMcpDiscoveryPlanner",
|
||||
planner_status: plannerStatus,
|
||||
semantic_data_need: semanticDataNeed,
|
||||
selected_chain_id: recipe.chainId,
|
||||
selected_chain_summary: recipe.chainSummary,
|
||||
proposed_primitives: recipe.primitives,
|
||||
required_axes: recipe.axes,
|
||||
discovery_plan: plan,
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ const DEFAULT_DISCOVERY_BUDGET = {
|
|||
max_probe_count: 3,
|
||||
max_rows_per_probe: 100
|
||||
};
|
||||
const MAX_PROBE_COUNT = 6;
|
||||
const MAX_PROBE_COUNT = 36;
|
||||
const MAX_ROWS_PER_PROBE = 500;
|
||||
const ALLOWED_PRIMITIVE_SET = new Set(exports.ASSISTANT_MCP_DISCOVERY_PRIMITIVES);
|
||||
function toNonEmptyString(value) {
|
||||
|
|
@ -74,19 +74,27 @@ function normalizeTurnMeaning(value) {
|
|||
const result = {};
|
||||
const domain = toNonEmptyString(value.asked_domain_family);
|
||||
const action = toNonEmptyString(value.asked_action_family);
|
||||
const aggregationAxis = toNonEmptyString(value.asked_aggregation_axis);
|
||||
const organization = toNonEmptyString(value.explicit_organization_scope);
|
||||
const dateScope = toNonEmptyString(value.explicit_date_scope);
|
||||
const unsupported = toNonEmptyString(value.unsupported_but_understood_family);
|
||||
const entities = toStringList(value.explicit_entity_candidates);
|
||||
const metadataAmbiguityEntitySets = toStringList(value.metadata_ambiguity_entity_sets);
|
||||
if (domain) {
|
||||
result.asked_domain_family = domain;
|
||||
}
|
||||
if (action) {
|
||||
result.asked_action_family = action;
|
||||
}
|
||||
if (aggregationAxis) {
|
||||
result.asked_aggregation_axis = aggregationAxis;
|
||||
}
|
||||
if (entities.length > 0) {
|
||||
result.explicit_entity_candidates = entities;
|
||||
}
|
||||
if (metadataAmbiguityEntitySets.length > 0) {
|
||||
result.metadata_ambiguity_entity_sets = metadataAmbiguityEntitySets;
|
||||
}
|
||||
if (organization) {
|
||||
result.explicit_organization_scope = organization;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,10 +70,24 @@ function localizeLine(value) {
|
|||
}
|
||||
const valueFlowMatch = value.match(/^1C value-flow rows were found for counterparty\s+(.+)$/i);
|
||||
if (valueFlowMatch) {
|
||||
return `В 1С найдены строки денежных движений по контрагенту ${valueFlowMatch[1]}.`;
|
||||
return `В 1С найдены строки входящих денежных поступлений по контрагенту ${valueFlowMatch[1]}.`;
|
||||
}
|
||||
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
|
||||
return "В 1С найдены строки денежных движений по запрошенному контрагентскому контуру.";
|
||||
return "В 1С найдены строки входящих денежных поступлений по запрошенному контрагентскому контуру.";
|
||||
}
|
||||
const documentRowsMatch = value.match(/^1C document rows were found for counterparty\s+(.+)$/i);
|
||||
if (documentRowsMatch) {
|
||||
return `В 1С найдены строки документов по контрагенту ${documentRowsMatch[1]}.`;
|
||||
}
|
||||
if (/^1C document rows were found for the requested scope$/i.test(value)) {
|
||||
return "В 1С найдены строки документов по запрошенному контуру.";
|
||||
}
|
||||
const movementRowsMatch = value.match(/^1C movement rows were found for counterparty\s+(.+)$/i);
|
||||
if (movementRowsMatch) {
|
||||
return `В 1С найдены строки движений по контрагенту ${movementRowsMatch[1]}.`;
|
||||
}
|
||||
if (/^1C movement rows were found for the requested scope$/i.test(value)) {
|
||||
return "В 1С найдены строки движений по запрошенному контуру.";
|
||||
}
|
||||
const supplierPayoutMatch = value.match(/^1C supplier-payout rows were found for counterparty\s+(.+)$/i);
|
||||
if (supplierPayoutMatch) {
|
||||
|
|
@ -97,8 +111,17 @@ function localizeLine(value) {
|
|||
if (/^Business activity duration may be inferred from first and latest confirmed 1C activity rows$/i.test(value)) {
|
||||
return "Длительность деловой активности можно оценивать только как вывод по первой и последней подтвержденной строке активности в 1С.";
|
||||
}
|
||||
if (/^Counterparty document evidence is limited to confirmed 1C document rows in the checked scope$/i.test(value)) {
|
||||
return "Срез документов ограничен только подтвержденными строками документов в проверенном окне.";
|
||||
}
|
||||
if (/^Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope$/i.test(value)) {
|
||||
return "Срез движений ограничен только подтвержденными строками движений в проверенном окне.";
|
||||
}
|
||||
if (/^Counterparty value-flow total was calculated from confirmed 1C movement rows$/i.test(value)) {
|
||||
return "Сумма рассчитана только по подтвержденным строкам денежных движений в 1С.";
|
||||
return "Сумма входящих поступлений рассчитана только по подтвержденным строкам поступлений в 1С.";
|
||||
}
|
||||
if (/^Counterparty monthly value-flow breakdown was grouped by month over confirmed 1C movement rows$/i.test(value)) {
|
||||
return "Помесячная раскладка входящих поступлений построена только по подтвержденным строкам поступлений в 1С.";
|
||||
}
|
||||
if (/^Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows$/i.test(value)) {
|
||||
return "Сумма исходящих платежей рассчитана только по подтвержденным строкам списаний в 1С.";
|
||||
|
|
@ -106,6 +129,53 @@ function localizeLine(value) {
|
|||
if (/^Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows$/i.test(value)) {
|
||||
return "Нетто денежного потока рассчитано только как входящие подтвержденные строки 1С минус исходящие подтвержденные строки 1С.";
|
||||
}
|
||||
if (/^Counterparty monthly net value-flow breakdown was grouped by month over confirmed incoming and outgoing 1C rows$/i.test(value)) {
|
||||
return "Помесячная нетто-раскладка сгруппирована только по подтвержденным входящим и исходящим строкам 1С.";
|
||||
}
|
||||
const metadataSurfaceMatch = value.match(/^Confirmed 1C metadata surface(?: for scope "([^"]+)")?: (\d+) rows and (\d+) matching objects$/i);
|
||||
if (metadataSurfaceMatch) {
|
||||
const scopePart = metadataSurfaceMatch[1] ? ` по области "${metadataSurfaceMatch[1]}"` : "";
|
||||
return `В 1С подтверждена metadata-поверхность${scopePart}: ${metadataSurfaceMatch[2]} строк metadata-ответа и ${metadataSurfaceMatch[3]} совпавших объекта(ов).`;
|
||||
}
|
||||
const metadataObjectSetsMatch = value.match(/^Available metadata object sets: (.+)$/i);
|
||||
if (metadataObjectSetsMatch) {
|
||||
return `Доступные типы metadata-объектов: ${metadataObjectSetsMatch[1]}.`;
|
||||
}
|
||||
const selectedMetadataEntitySetMatch = value.match(/^Selected metadata entity set: (.+)$/i);
|
||||
if (selectedMetadataEntitySetMatch) {
|
||||
return `Выбранное семейство metadata-объектов: ${selectedMetadataEntitySetMatch[1]}.`;
|
||||
}
|
||||
const selectedMetadataObjectsMatch = value.match(/^Selected metadata objects: (.+)$/i);
|
||||
if (selectedMetadataObjectsMatch) {
|
||||
return `Выбранные metadata-объекты для следующего шага: ${selectedMetadataObjectsMatch[1]}.`;
|
||||
}
|
||||
const metadataFieldsMatch = value.match(/^Available metadata fields\/sections: (.+)$/i);
|
||||
if (metadataFieldsMatch) {
|
||||
return `Доступные metadata-поля/секции: ${metadataFieldsMatch[1]}.`;
|
||||
}
|
||||
const metadataLaneInferenceMatch = value.match(/^A likely next checked lane may be inferred as (document_evidence|movement_evidence|catalog_drilldown) from the confirmed metadata surface$/i);
|
||||
if (metadataLaneInferenceMatch) {
|
||||
const routeLabel = metadataLaneInferenceMatch[1] === "document_evidence"
|
||||
? "контур документов"
|
||||
: metadataLaneInferenceMatch[1] === "movement_evidence"
|
||||
? "контур движений/регистров"
|
||||
: "контур справочников и связанных объектов";
|
||||
return `Следующий проверяемый контур по этой metadata-поверхности можно ограниченно оценить как ${routeLabel}.`;
|
||||
}
|
||||
if (/^Detailed metadata fields were not returned by this MCP metadata probe$/i.test(value)) {
|
||||
return "Эта MCP-проверка metadata не вернула детальный список полей.";
|
||||
}
|
||||
const metadataAmbiguityMatch = value.match(/^Exact downstream metadata surface remains ambiguous across: (.+)$/i);
|
||||
if (metadataAmbiguityMatch) {
|
||||
return `Точная downstream metadata-поверхность пока неоднозначна между family: ${metadataAmbiguityMatch[1]}.`;
|
||||
}
|
||||
const noMatchingMetadataScopeMatch = value.match(/^No matching 1C metadata objects were confirmed for scope "([^"]+)"$/i);
|
||||
if (noMatchingMetadataScopeMatch) {
|
||||
return `В 1С не подтверждены metadata-объекты по области "${noMatchingMetadataScopeMatch[1]}".`;
|
||||
}
|
||||
if (/^No matching 1C metadata objects were confirmed by this MCP metadata probe$/i.test(value)) {
|
||||
return "В 1С эта MCP-проверка не подтвердила подходящих metadata-объектов.";
|
||||
}
|
||||
if (/^Legal registration date is not proven by this MCP discovery pilot$/i.test(value)) {
|
||||
return "Юридическая дата регистрации этим поиском не подтверждена.";
|
||||
}
|
||||
|
|
@ -116,10 +186,22 @@ function localizeLine(value) {
|
|||
return "Полное покрытие запрошенного периода по двустороннему денежному потоку не подтверждено: хотя бы одна сторона проверки достигла лимита найденных строк.";
|
||||
}
|
||||
if (/^Full turnover outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
|
||||
return "Полный оборот вне проверенного периода этим поиском не подтвержден.";
|
||||
return "Полный объем входящих поступлений вне проверенного периода этим поиском не подтвержден.";
|
||||
}
|
||||
if (/^Full all-time turnover is not proven without an explicit checked period$/i.test(value)) {
|
||||
return "Полный оборот за все время без явно проверенного периода не подтвержден.";
|
||||
return "Полный объем входящих поступлений за все время без явно проверенного периода не подтвержден.";
|
||||
}
|
||||
if (/^Full document history outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
|
||||
return "Полный исторический срез документов вне проверенного периода этим поиском не подтвержден.";
|
||||
}
|
||||
if (/^Full document history is not proven without an explicit checked period$/i.test(value)) {
|
||||
return "Полный срез документов без явно проверенного периода не подтвержден.";
|
||||
}
|
||||
if (/^Full movement history outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
|
||||
return "Полный исторический срез движений вне проверенного периода этим поиском не подтвержден.";
|
||||
}
|
||||
if (/^Full movement history is not proven without an explicit checked period$/i.test(value)) {
|
||||
return "Полный срез движений без явно проверенного периода не подтвержден.";
|
||||
}
|
||||
if (/^Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
|
||||
return "Полный объем исходящих платежей вне проверенного периода этим поиском не подтвержден.";
|
||||
|
|
@ -133,6 +215,12 @@ function localizeLine(value) {
|
|||
if (/^Full all-time bidirectional value-flow is not proven without an explicit checked period$/i.test(value)) {
|
||||
return "Полный двусторонний денежный поток за все время без явно проверенного периода не подтвержден.";
|
||||
}
|
||||
if (/^Requested period coverage was recovered through monthly 1C value-flow probes after the broad probe hit the row limit$/i.test(value)) {
|
||||
return "Покрытие запрошенного периода восстановлено помесячными проверками 1С после того, как общая выборка уперлась в лимит строк.";
|
||||
}
|
||||
if (/^Requested period coverage for bidirectional value-flow was recovered through monthly 1C side probes after a broad probe hit the row limit$/i.test(value)) {
|
||||
return "Покрытие запрошенного периода по двустороннему денежному потоку восстановлено помесячными проверками 1С после того, как общая выборка уперлась в лимит строк хотя бы по одной стороне.";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function section(title, lines) {
|
||||
|
|
|
|||
|
|
@ -66,6 +66,10 @@ function isUnsupportedCurrentTurnBoundary(input) {
|
|||
input.livingChatSource === "deterministic_unsupported_current_turn_boundary" ||
|
||||
input.currentReplySource === "deterministic_unsupported_current_turn_boundary");
|
||||
}
|
||||
function isDeterministicBroadBusinessEvaluationReply(input) {
|
||||
return (input.livingChatSource === "deterministic_broad_business_evaluation_contract" ||
|
||||
input.currentReplySource === "deterministic_broad_business_evaluation_contract");
|
||||
}
|
||||
function isDiscoveryReadyChatCandidate(input, entryPoint) {
|
||||
const turnInput = toRecordObject(entryPoint?.turn_input);
|
||||
return (entryPoint?.entry_status === "bridge_executed" &&
|
||||
|
|
@ -89,6 +93,167 @@ function isDiscoveryReadyAddressCandidate(input, entryPoint) {
|
|||
turnInput?.should_run_discovery === true &&
|
||||
(source === "address_lane" || source === "address_exact" || source === "address_query_runtime_v1"));
|
||||
}
|
||||
function isDetectedIntentAlignedWithTurnMeaning(detectedIntent, turnMeaning) {
|
||||
const normalizedIntent = String(detectedIntent ?? "").trim().toLowerCase();
|
||||
if (!normalizedIntent) {
|
||||
return false;
|
||||
}
|
||||
const askedDomain = String(toNonEmptyString(turnMeaning?.asked_domain_family) ?? "").trim().toLowerCase();
|
||||
const askedAction = String(toNonEmptyString(turnMeaning?.asked_action_family) ?? "").trim().toLowerCase();
|
||||
if (normalizedIntent === "counterparty_activity_lifecycle") {
|
||||
return (askedDomain === "counterparty_lifecycle" ||
|
||||
askedAction === "activity_duration" ||
|
||||
askedAction === "age_or_activity_duration");
|
||||
}
|
||||
if (normalizedIntent === "supplier_payouts_profile") {
|
||||
return askedDomain === "counterparty_value" && askedAction === "payout";
|
||||
}
|
||||
if (normalizedIntent === "customer_revenue_and_payments") {
|
||||
return askedDomain === "counterparty_value" && (askedAction === "turnover" || askedAction === "counterparty_value_or_turnover");
|
||||
}
|
||||
if (normalizedIntent === "receivables_confirmed_as_of_date") {
|
||||
return askedDomain === "receivables" || askedAction === "confirmed_snapshot";
|
||||
}
|
||||
if (normalizedIntent === "payables_confirmed_as_of_date") {
|
||||
return askedDomain === "payables" || askedAction === "confirmed_snapshot";
|
||||
}
|
||||
if (normalizedIntent === "vat_liability_confirmed_for_tax_period") {
|
||||
return askedDomain === "vat" && askedAction === "confirmed_tax_period";
|
||||
}
|
||||
if (normalizedIntent === "vat_payable_confirmed_as_of_date") {
|
||||
return askedDomain === "vat" && askedAction === "confirmed_snapshot";
|
||||
}
|
||||
if (normalizedIntent === "vat_payable_forecast") {
|
||||
return askedDomain === "vat" && askedAction === "forecast";
|
||||
}
|
||||
if (normalizedIntent === "list_documents_by_counterparty") {
|
||||
return askedAction === "list_documents" || askedDomain === "counterparty_documents" || askedDomain === "counterparty";
|
||||
}
|
||||
if (normalizedIntent === "inventory_on_hand_as_of_date" || normalizedIntent === "inventory_aging_by_purchase_date") {
|
||||
return askedDomain === "inventory" && askedAction === "confirmed_snapshot";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function readDiscoveryTurnMeaning(entryPoint) {
|
||||
const turnInput = toRecordObject(entryPoint?.turn_input);
|
||||
return toRecordObject(turnInput?.turn_meaning_ref);
|
||||
}
|
||||
function readTruthAnswerShape(input) {
|
||||
const directShape = toRecordObject(input.addressRuntimeMeta?.answer_shape_contract);
|
||||
if (directShape) {
|
||||
return directShape;
|
||||
}
|
||||
const truthAnswerPolicy = toRecordObject(input.addressRuntimeMeta?.assistant_truth_answer_policy_v1);
|
||||
return toRecordObject(truthAnswerPolicy?.answer_shape);
|
||||
}
|
||||
function hasEffectivelyFactualAddressReply(input) {
|
||||
if (toNonEmptyString(input.currentReplyType) === "factual") {
|
||||
return true;
|
||||
}
|
||||
const truthAnswerShape = readTruthAnswerShape(input);
|
||||
return toNonEmptyString(truthAnswerShape?.reply_type) === "factual";
|
||||
}
|
||||
function readStateTransitionReasonCodes(input) {
|
||||
const directTransition = toRecordObject(input.addressRuntimeMeta?.assistant_state_transition_v1);
|
||||
const fallbackTransition = toRecordObject(input.addressRuntimeMeta?.state_transition_contract);
|
||||
const stateTransition = directTransition ?? fallbackTransition;
|
||||
if (!stateTransition || !Array.isArray(stateTransition.reason_codes)) {
|
||||
return [];
|
||||
}
|
||||
return stateTransition.reason_codes
|
||||
.map((item) => toNonEmptyString(item))
|
||||
.filter((item) => Boolean(item));
|
||||
}
|
||||
function hasRuntimeAdjustedExactReply(input, entryPoint) {
|
||||
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
if (!hasEffectivelyFactualAddressReply(input)) {
|
||||
return false;
|
||||
}
|
||||
const truthGateStatus = toNonEmptyString(input.addressRuntimeMeta?.truth_gate_contract_status);
|
||||
const truthAnswerPolicy = toRecordObject(input.addressRuntimeMeta?.assistant_truth_answer_policy_v1);
|
||||
const truthGate = toRecordObject(truthAnswerPolicy?.truth_gate);
|
||||
const sourceTruthGateStatus = toNonEmptyString(truthGate?.source_truth_gate_status);
|
||||
const coverageStatus = toNonEmptyString(truthGate?.coverage_status);
|
||||
const groundingStatus = toNonEmptyString(truthGate?.grounding_status);
|
||||
const hasFullConfirmedTruth = truthGateStatus === "full_confirmed" ||
|
||||
sourceTruthGateStatus === "full_confirmed" ||
|
||||
(coverageStatus === "full" && groundingStatus === "grounded");
|
||||
if (!hasFullConfirmedTruth) {
|
||||
return false;
|
||||
}
|
||||
const truthAnswerShape = readTruthAnswerShape(input);
|
||||
const capabilityContractId = toNonEmptyString(truthAnswerShape?.capability_contract_id);
|
||||
if (!capabilityContractId) {
|
||||
return false;
|
||||
}
|
||||
return readStateTransitionReasonCodes(input).some((reason) => /^intent_adjusted_to_.+_followup_context$/i.test(reason));
|
||||
}
|
||||
function hasAlignedFactualAddressReply(input, entryPoint) {
|
||||
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
if (!hasEffectivelyFactualAddressReply(input)) {
|
||||
return false;
|
||||
}
|
||||
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||
return isDetectedIntentAlignedWithTurnMeaning(detectedIntent, readDiscoveryTurnMeaning(entryPoint));
|
||||
}
|
||||
function hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint) {
|
||||
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
if (!hasEffectivelyFactualAddressReply(input)) {
|
||||
return false;
|
||||
}
|
||||
if (hasRuntimeAdjustedExactReply(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
|
||||
const askedDomain = toNonEmptyString(turnMeaning?.asked_domain_family);
|
||||
const askedAction = toNonEmptyString(turnMeaning?.asked_action_family);
|
||||
const unsupportedFamily = toNonEmptyString(turnMeaning?.unsupported_but_understood_family);
|
||||
if (!detectedIntent || (!askedDomain && !askedAction && !unsupportedFamily)) {
|
||||
return false;
|
||||
}
|
||||
return !isDetectedIntentAlignedWithTurnMeaning(detectedIntent, turnMeaning);
|
||||
}
|
||||
function hasMatchedFactualAddressContinuationTarget(input, entryPoint) {
|
||||
if (!hasEffectivelyFactualAddressReply(input)) {
|
||||
return false;
|
||||
}
|
||||
if (hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||
const dialogContinuationContract = toRecordObject(input.addressRuntimeMeta?.dialogContinuationContract) ??
|
||||
toRecordObject(input.addressRuntimeMeta?.dialog_continuation_contract_v2);
|
||||
const targetIntent = toNonEmptyString(dialogContinuationContract?.target_intent);
|
||||
return Boolean(detectedIntent && targetIntent && detectedIntent === targetIntent);
|
||||
}
|
||||
function hasFullConfirmedFactualAddressReply(input, entryPoint) {
|
||||
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
if (!hasEffectivelyFactualAddressReply(input)) {
|
||||
return false;
|
||||
}
|
||||
if (hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
const truthGateStatus = toNonEmptyString(input.addressRuntimeMeta?.truth_gate_contract_status);
|
||||
if (truthGateStatus === "full_confirmed") {
|
||||
return true;
|
||||
}
|
||||
const truthAnswerPolicy = toRecordObject(input.addressRuntimeMeta?.assistant_truth_answer_policy_v1);
|
||||
const truthGate = toRecordObject(truthAnswerPolicy?.truth_gate);
|
||||
const sourceTruthGateStatus = toNonEmptyString(truthGate?.source_truth_gate_status);
|
||||
const coverageStatus = toNonEmptyString(truthGate?.coverage_status);
|
||||
const groundingStatus = toNonEmptyString(truthGate?.grounding_status);
|
||||
return sourceTruthGateStatus === "full_confirmed" || (coverageStatus === "full" && groundingStatus === "grounded");
|
||||
}
|
||||
function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
||||
const currentReply = String(input.currentReply ?? "");
|
||||
const currentReplySource = toNonEmptyString(input.currentReplySource) ?? toNonEmptyString(input.livingChatSource) ?? "unknown";
|
||||
|
|
@ -96,9 +261,15 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
|||
const candidate = (0, assistantMcpDiscoveryResponseCandidate_1.buildAssistantMcpDiscoveryResponseCandidate)(entryPoint);
|
||||
const reasonCodes = [...candidate.reason_codes];
|
||||
const unsupportedBoundary = isUnsupportedCurrentTurnBoundary(input);
|
||||
const deterministicBroadBusinessEvaluationReply = isDeterministicBroadBusinessEvaluationReply(input);
|
||||
const discoveryReadyChatCandidate = isDiscoveryReadyChatCandidate(input, entryPoint);
|
||||
const discoveryReadyDeepCandidate = isDiscoveryReadyDeepCandidate(input, entryPoint);
|
||||
const discoveryReadyAddressCandidate = isDiscoveryReadyAddressCandidate(input, entryPoint);
|
||||
const alignedFactualAddressReply = hasAlignedFactualAddressReply(input, entryPoint);
|
||||
const semanticConflictWithDiscoveryTurnMeaning = hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint);
|
||||
const matchedFactualAddressContinuationTarget = hasMatchedFactualAddressContinuationTarget(input, entryPoint);
|
||||
const fullConfirmedFactualAddressReply = hasFullConfirmedFactualAddressReply(input, entryPoint);
|
||||
const runtimeAdjustedExactReply = hasRuntimeAdjustedExactReply(input, entryPoint);
|
||||
if (!entryPoint) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point");
|
||||
}
|
||||
|
|
@ -114,6 +285,24 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
|||
if (!discoveryReadyAddressCandidate) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_not_discovery_ready_address_candidate");
|
||||
}
|
||||
if (alignedFactualAddressReply) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_aligned_factual_address_reply");
|
||||
}
|
||||
if (semanticConflictWithDiscoveryTurnMeaning) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_semantic_conflict_allows_candidate_override");
|
||||
}
|
||||
if (matchedFactualAddressContinuationTarget) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_factual_address_continuation_target");
|
||||
}
|
||||
if (fullConfirmedFactualAddressReply) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply");
|
||||
}
|
||||
if (runtimeAdjustedExactReply) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_runtime_adjusted_exact_reply_over_stale_discovery_turn_meaning");
|
||||
}
|
||||
if (deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_broad_business_summary_over_clarification_candidate");
|
||||
}
|
||||
if (!ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status)) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_status_not_allowed");
|
||||
}
|
||||
|
|
@ -128,6 +317,11 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
|||
}
|
||||
const canApply = Boolean(entryPoint) &&
|
||||
(unsupportedBoundary || discoveryReadyChatCandidate || discoveryReadyDeepCandidate || discoveryReadyAddressCandidate) &&
|
||||
!alignedFactualAddressReply &&
|
||||
!matchedFactualAddressContinuationTarget &&
|
||||
!fullConfirmedFactualAddressReply &&
|
||||
!runtimeAdjustedExactReply &&
|
||||
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
|
||||
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
|
||||
candidate.eligible_for_future_hot_runtime &&
|
||||
Boolean(toNonEmptyString(candidate.reply_text)) &&
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -3,6 +3,7 @@
|
|||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.buildInventoryHistoryCapabilityFollowupReply = buildInventoryHistoryCapabilityFollowupReply;
|
||||
exports.buildAddressMemoryRecapReply = buildAddressMemoryRecapReply;
|
||||
exports.buildBroadBusinessEvaluationReply = buildBroadBusinessEvaluationReply;
|
||||
exports.buildSelectedObjectAnswerInspectionReply = buildSelectedObjectAnswerInspectionReply;
|
||||
exports.resolveAssistantLivingChatMemoryContext = resolveAssistantLivingChatMemoryContext;
|
||||
exports.createAssistantMemoryRecapPolicy = createAssistantMemoryRecapPolicy;
|
||||
|
|
@ -14,6 +15,119 @@ function toNonEmptyString(value) {
|
|||
const text = String(value).trim();
|
||||
return text.length > 0 ? text : null;
|
||||
}
|
||||
function toRecordObject(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function ensureSentence(value) {
|
||||
const text = String(value ?? "").trim();
|
||||
if (!text) {
|
||||
return "";
|
||||
}
|
||||
return /[.!?]$/.test(text) ? text : `${text}.`;
|
||||
}
|
||||
function periodPartForRecap(scopedDate) {
|
||||
if (!scopedDate) {
|
||||
return "";
|
||||
}
|
||||
return /^\d{2}\.\d{2}\.\d{4}$/.test(scopedDate) ? ` на ${scopedDate}` : ` за период ${scopedDate}`;
|
||||
}
|
||||
function readDiscoveryMetadataScope(debug) {
|
||||
const discoveryEntry = toRecordObject(debug?.assistant_mcp_discovery_entry_point_v1);
|
||||
const bridge = toRecordObject(discoveryEntry?.bridge);
|
||||
const pilot = toRecordObject(bridge?.pilot);
|
||||
const surface = toRecordObject(pilot?.derived_metadata_surface);
|
||||
const surfaceScope = toNonEmptyString(surface?.metadata_scope);
|
||||
if (surfaceScope) {
|
||||
return surfaceScope;
|
||||
}
|
||||
const turnInput = toRecordObject(discoveryEntry?.turn_input);
|
||||
const turnMeaningRef = toRecordObject(turnInput?.turn_meaning_ref);
|
||||
const entityCandidates = Array.isArray(turnMeaningRef?.explicit_entity_candidates)
|
||||
? turnMeaningRef.explicit_entity_candidates
|
||||
: [];
|
||||
for (const candidate of entityCandidates) {
|
||||
const text = toNonEmptyString(candidate);
|
||||
if (text) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function buildDiscoveryRecapFactLine(input) {
|
||||
if (!input.debug) {
|
||||
return null;
|
||||
}
|
||||
const pilotScope = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryPilotScope)(input.debug, toNonEmptyString);
|
||||
const discoveryEntry = toRecordObject(input.debug.assistant_mcp_discovery_entry_point_v1);
|
||||
const bridge = toRecordObject(discoveryEntry?.bridge);
|
||||
const pilot = toRecordObject(bridge?.pilot);
|
||||
const periodPart = periodPartForRecap(input.scopedDate);
|
||||
if (pilotScope === "metadata_inspection_v1") {
|
||||
const metadataScope = readDiscoveryMetadataScope(input.debug);
|
||||
const surface = toRecordObject(pilot?.derived_metadata_surface);
|
||||
const entitySets = Array.isArray(surface?.available_entity_sets)
|
||||
? surface.available_entity_sets
|
||||
.map((item) => toNonEmptyString(item))
|
||||
.filter((item) => Boolean(item))
|
||||
: [];
|
||||
const fields = Array.isArray(surface?.available_fields)
|
||||
? surface.available_fields
|
||||
.map((item) => toNonEmptyString(item))
|
||||
.filter((item) => Boolean(item))
|
||||
: [];
|
||||
const objects = Array.isArray(surface?.matched_objects)
|
||||
? surface.matched_objects
|
||||
.map((item) => toNonEmptyString(item))
|
||||
.filter((item) => Boolean(item))
|
||||
: [];
|
||||
const rows = Number(surface?.matched_rows ?? 0);
|
||||
const scopePart = metadataScope ? ` по области «${metadataScope}»` : "";
|
||||
const objectsPart = objects.length > 0 ? `, нашли объекты ${objects.slice(0, 4).join(", ")}` : "";
|
||||
const entitySetsPart = entitySets.length > 0 ? `, видны типы ${entitySets.slice(0, 4).join(", ")}` : "";
|
||||
const fieldsPart = fields.length > 0 ? `, доступны поля/секции ${fields.slice(0, 5).join(", ")}` : "";
|
||||
return `смотрели metadata-поверхность 1С${scopePart}${periodPart}: ${rows} подтвержденных строк${objectsPart}${entitySetsPart}${fieldsPart}`.trim();
|
||||
}
|
||||
if (!input.counterparty) {
|
||||
return null;
|
||||
}
|
||||
if (pilotScope === "counterparty_lifecycle_query_documents_v1") {
|
||||
const activityPeriod = toRecordObject(pilot?.derived_activity_period);
|
||||
const duration = toNonEmptyString(activityPeriod?.duration_human_ru);
|
||||
return duration
|
||||
? `смотрели подтвержденную активность по контрагенту «${input.counterparty}»${periodPart} и оценили период взаимодействия примерно как ${duration}`
|
||||
: `смотрели подтвержденную активность по контрагенту «${input.counterparty}»${periodPart}`;
|
||||
}
|
||||
if (pilotScope === "counterparty_supplier_payout_query_movements_v1") {
|
||||
const flow = toRecordObject(pilot?.derived_value_flow);
|
||||
const amount = toNonEmptyString(flow?.total_amount_human_ru);
|
||||
return amount
|
||||
? `считали исходящие платежи/списания по контрагенту «${input.counterparty}»${periodPart}: ${amount}`
|
||||
: `считали исходящие платежи/списания по контрагенту «${input.counterparty}»${periodPart}`;
|
||||
}
|
||||
if (pilotScope === "counterparty_value_flow_query_movements_v1") {
|
||||
const flow = toRecordObject(pilot?.derived_value_flow);
|
||||
const amount = toNonEmptyString(flow?.total_amount_human_ru);
|
||||
return amount
|
||||
? `смотрели денежный поток по контрагенту «${input.counterparty}»${periodPart}: ${amount}`
|
||||
: `смотрели денежный поток по контрагенту «${input.counterparty}»${periodPart}`;
|
||||
}
|
||||
if (pilotScope === "counterparty_bidirectional_value_flow_query_movements_v1") {
|
||||
const flow = toRecordObject(pilot?.derived_bidirectional_value_flow);
|
||||
const incoming = toRecordObject(flow?.incoming_customer_revenue);
|
||||
const outgoing = toRecordObject(flow?.outgoing_supplier_payout);
|
||||
const incomingAmount = toNonEmptyString(incoming?.total_amount_human_ru);
|
||||
const outgoingAmount = toNonEmptyString(outgoing?.total_amount_human_ru);
|
||||
const netAmount = toNonEmptyString(flow?.net_amount_human_ru);
|
||||
if (incomingAmount && outgoingAmount && netAmount) {
|
||||
return `считали нетто по деньгам с контрагентом «${input.counterparty}»${periodPart}: получили ${incomingAmount}, заплатили ${outgoingAmount}, расчетное нетто ${netAmount}`;
|
||||
}
|
||||
return `считали нетто по деньгам с контрагентом «${input.counterparty}»${periodPart}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function collectMessageSamples(input) {
|
||||
const values = [
|
||||
input.rawUserMessage,
|
||||
|
|
@ -60,7 +174,16 @@ function normalizeRecapIdentity(value) {
|
|||
function buildRecapFactLine(input) {
|
||||
const detectedIntent = String(input.debug?.detected_intent ?? "");
|
||||
const scopedDate = (0, assistantContinuityPolicy_1.resolveAddressDebugContextFacts)(input.debug).scopedDate;
|
||||
const discoveryFact = buildDiscoveryRecapFactLine({
|
||||
debug: input.debug,
|
||||
counterparty: input.counterparty,
|
||||
scopedDate
|
||||
});
|
||||
if (discoveryFact) {
|
||||
return discoveryFact;
|
||||
}
|
||||
const itemPart = input.item ? `по позиции «${input.item}»` : null;
|
||||
const counterpartyPart = input.counterparty ? `по контрагенту «${input.counterparty}»` : null;
|
||||
const organizationPart = input.organization ? `по компании «${input.organization}»` : null;
|
||||
const datePart = scopedDate ? ` на ${scopedDate}` : "";
|
||||
if (detectedIntent === "inventory_on_hand_as_of_date") {
|
||||
|
|
@ -87,6 +210,9 @@ function buildRecapFactLine(input) {
|
|||
if (detectedIntent === "counterparty_activity_lifecycle" && organizationPart) {
|
||||
return `смотрели активность в базе 1С ${organizationPart}`.trim();
|
||||
}
|
||||
if (detectedIntent === "list_documents_by_counterparty" && counterpartyPart) {
|
||||
return `поднимали документы ${counterpartyPart}${datePart}`.trim();
|
||||
}
|
||||
if (detectedIntent === "list_documents_by_counterparty" && organizationPart) {
|
||||
return `поднимали документы ${organizationPart}${datePart}`.trim();
|
||||
}
|
||||
|
|
@ -125,6 +251,7 @@ function collectRecentRecapFacts(input) {
|
|||
const fact = buildRecapFactLine({
|
||||
debug: item.debug,
|
||||
item: debugItem,
|
||||
counterparty: debugContext.counterparty,
|
||||
organization: debugOrganization
|
||||
});
|
||||
if (!fact || seen.has(fact)) {
|
||||
|
|
@ -141,6 +268,7 @@ function collectRecentRecapFacts(input) {
|
|||
function buildAddressMemoryRecapReply(input) {
|
||||
const contextFacts = (0, assistantContinuityPolicy_1.resolveAddressDebugContextFacts)(input.addressDebug, input.toNonEmptyString);
|
||||
const item = contextFacts.item;
|
||||
const counterparty = contextFacts.counterparty;
|
||||
const organization = input.organization ?? contextFacts.organization;
|
||||
const scopedDate = contextFacts.scopedDate;
|
||||
const recapFacts = collectRecentRecapFacts({
|
||||
|
|
@ -166,6 +294,21 @@ function buildAddressMemoryRecapReply(input) {
|
|||
"Могу продолжить по ней без переписывания сущности: кто поставил, когда купили, по каким документам или кому продали."
|
||||
].join(" ");
|
||||
}
|
||||
if (counterparty) {
|
||||
const organizationPart = organization ? ` по компании «${organization}»` : "";
|
||||
const periodPart = periodPartForRecap(scopedDate);
|
||||
if (recapFacts.length > 0) {
|
||||
return [
|
||||
`Да, помню. По контрагенту «${counterparty}»${organizationPart}${periodPart} мы уже выяснили:`,
|
||||
...recapFacts.map((fact) => `- ${fact}.`),
|
||||
"Могу сразу продолжить по нему: поступления, платежи, нетто, помесячную раскладку или границы подтверждения."
|
||||
].join("\n");
|
||||
}
|
||||
return [
|
||||
`Да, помню. Мы уже смотрели контур по контрагенту «${counterparty}»${organizationPart}${periodPart}.`,
|
||||
"Могу продолжить по нему без переписывания контекста: поступления, платежи, нетто, документы или пояснение границ ответа."
|
||||
].join(" ");
|
||||
}
|
||||
if (organization || scopedDate) {
|
||||
const organizationPart = organization ? ` по компании «${organization}»` : "";
|
||||
const datePart = scopedDate ? ` на ${scopedDate}` : "";
|
||||
|
|
@ -176,10 +319,62 @@ function buildAddressMemoryRecapReply(input) {
|
|||
}
|
||||
return "Да, помню предыдущий адресный контур. Могу кратко напомнить, что мы уже подтвердили, или сразу продолжить следующий шаг.";
|
||||
}
|
||||
function buildBroadBusinessEvaluationReply(input) {
|
||||
const contextFacts = (0, assistantContinuityPolicy_1.resolveAddressDebugContextFacts)(input.addressDebug, input.toNonEmptyString);
|
||||
const organization = input.organization ?? contextFacts.organization;
|
||||
const recapFacts = collectRecentRecapFacts({
|
||||
sessionItems: input.sessionItems,
|
||||
item: null,
|
||||
organization,
|
||||
toNonEmptyString: input.toNonEmptyString
|
||||
});
|
||||
const organizationPart = organization ? ` по компании «${organization}»` : "";
|
||||
if (recapFacts.length > 0) {
|
||||
return [
|
||||
`Коротко: по тому, что мы уже подтвердили в 1С${organizationPart}, компания выглядит операционно живой, но это пока только частичная оценка бизнеса.`,
|
||||
"Сейчас я опираюсь на такие подтвержденные факты:",
|
||||
...recapFacts.map((fact) => `- ${ensureSentence(fact)}`),
|
||||
"Это еще не полная диагностика всего бизнеса и не вывод о прибыли: я честно суммирую только те контуры, которые мы уже проверили в диалоге.",
|
||||
"Если хочешь, следующим шагом могу сузить оценку до денежного потока, долгов, НДС или ключевых контрагентов."
|
||||
].join("\n");
|
||||
}
|
||||
return [
|
||||
`Коротко: по нынешнему контексту 1С${organizationPart} я вижу признаки операционной активности, но для содержательной оценки бизнеса нужно еще несколько опорных срезов.`,
|
||||
"Если хочешь, я быстро доберу основу для такой оценки: денежный поток, дебиторка/кредиторка, НДС или ключевые контрагенты."
|
||||
].join(" ");
|
||||
}
|
||||
function buildSelectedObjectAnswerInspectionReply(input) {
|
||||
const contextFacts = (0, assistantContinuityPolicy_1.resolveAddressDebugContextFacts)(input.addressDebug, input.toNonEmptyString);
|
||||
const itemLabel = contextFacts.item ?? "эта позиция";
|
||||
const counterpartyLabel = contextFacts.counterparty;
|
||||
const detectedIntent = String(input.addressDebug?.detected_intent ?? "");
|
||||
const pilotScope = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryPilotScope)(input.addressDebug, input.toNonEmptyString);
|
||||
const periodPart = periodPartForRecap(contextFacts.scopedDate);
|
||||
if (counterpartyLabel && pilotScope === "counterparty_bidirectional_value_flow_query_movements_v1") {
|
||||
return [
|
||||
`Да, в предыдущем ответе речь шла о двустороннем денежном потоке с контрагентом «${counterpartyLabel}»${periodPart}.`,
|
||||
"Нетто там означало разницу между тем, что получили, и тем, что заплатили по найденным строкам 1С.",
|
||||
"Это расчет по проверенному периоду и подтвержденным строкам, а не заявление про весь оборот вне этого окна."
|
||||
].join(" ");
|
||||
}
|
||||
if (counterpartyLabel && pilotScope === "counterparty_supplier_payout_query_movements_v1") {
|
||||
return [
|
||||
`Да, в предыдущем ответе речь шла об исходящих платежах/списаниях по контрагенту «${counterpartyLabel}»${periodPart}.`,
|
||||
"Это сумма по найденным строкам 1С за проверенный период, а не обещание, что за пределами этого окна больше движений не было."
|
||||
].join(" ");
|
||||
}
|
||||
if (counterpartyLabel && pilotScope === "counterparty_value_flow_query_movements_v1") {
|
||||
return [
|
||||
`Да, в предыдущем ответе речь шла о денежном потоке по контрагенту «${counterpartyLabel}»${periodPart}.`,
|
||||
"Это расчет по найденным движениям 1С за проверенный период, а не безусловный итог по всем временам."
|
||||
].join(" ");
|
||||
}
|
||||
if (counterpartyLabel && pilotScope === "counterparty_lifecycle_query_documents_v1") {
|
||||
return [
|
||||
`Да, в предыдущем ответе речь шла об активности контрагента «${counterpartyLabel}»${periodPart}.`,
|
||||
"Это оценка по подтвержденным строкам 1С, а не юридически подтвержденная дата регистрации."
|
||||
].join(" ");
|
||||
}
|
||||
if (detectedIntent === "inventory_sale_trace_for_item") {
|
||||
return [
|
||||
`Да, если так прозвучало, это ошибка чтения ответа. «${itemLabel}» здесь не контрагент, а сама позиция, по которой мы смотрели продажу.`,
|
||||
|
|
|
|||
|
|
@ -2607,6 +2607,12 @@ function hasAddressFollowupContextSignal(userMessage) {
|
|||
if (ultraShortFollowup && hasAny(/^(?:давай|показывай|показывыай|ещ[её]|also|again|go|ok|okay)(?=$|[\s,.;:!?])/iu)) {
|
||||
return true;
|
||||
}
|
||||
const shortValueFlowRetargetCue = shortFollowup &&
|
||||
(hasMarker() || hasPointer() || hasAny(/^(?:Р°|a|Рё|i|also|then|now)(?=$|[\s,.;:!?])/iu)) &&
|
||||
hasAny(/(?:нетто|сальдо|разниц|получил|заплатил|поступ|входящ|исходящ|оборот|выручк|денеж)/iu);
|
||||
if (shortValueFlowRetargetCue) {
|
||||
return true;
|
||||
}
|
||||
if (hasStandaloneAddressTopicSignal(rawText || repairedText)) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -3865,11 +3871,7 @@ function findLastGroundedAddressAnswerDebug(items) {
|
|||
continue;
|
||||
}
|
||||
const debug = item.debug;
|
||||
if (debug.execution_lane !== "address_query") {
|
||||
continue;
|
||||
}
|
||||
const groundingStatus = toNonEmptyString(debug.answer_grounding_check?.status);
|
||||
if (groundingStatus === "grounded") {
|
||||
if ((0, assistantContinuityPolicy_1.isGroundedAddressDebug)(debug, toNonEmptyString)) {
|
||||
return debug;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,18 +121,22 @@ function createAssistantTransitionPolicy(deps) {
|
|||
return false;
|
||||
}
|
||||
const executionLane = deps.toNonEmptyString(debug.execution_lane);
|
||||
const detectedIntent = deps.toNonEmptyString(debug.detected_intent);
|
||||
const detectedIntent = (0, assistantContinuityPolicy_1.readAddressDebugIntent)(debug, deps.toNonEmptyString);
|
||||
const selectedRecipe = deps.toNonEmptyString(debug.selected_recipe);
|
||||
const answerGroundingCheck = debug.answer_grounding_check && typeof debug.answer_grounding_check === "object"
|
||||
? debug.answer_grounding_check
|
||||
: null;
|
||||
const groundingStatus = deps.toNonEmptyString(answerGroundingCheck?.status);
|
||||
const discoveryPilotScope = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryPilotScope)(debug, deps.toNonEmptyString);
|
||||
if (groundingStatus === "grounded") {
|
||||
return true;
|
||||
}
|
||||
if (selectedRecipe) {
|
||||
return true;
|
||||
}
|
||||
if (debug.mcp_discovery_response_applied === true && discoveryPilotScope) {
|
||||
return true;
|
||||
}
|
||||
return executionLane === "address_query" && Boolean(detectedIntent && detectedIntent !== "unknown");
|
||||
}
|
||||
function findRecentUsableAddressAssistantItem(items) {
|
||||
|
|
@ -237,6 +241,23 @@ function createAssistantTransitionPolicy(deps) {
|
|||
];
|
||||
return sameDatePhrases.some((phrase) => normalized.includes(phrase));
|
||||
}
|
||||
function hasShortValueFlowRetargetCue(text) {
|
||||
const normalized = normalizeFollowupText(text);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const tokenCount = deps.countTokens(normalized);
|
||||
if (!Number.isFinite(tokenCount) || tokenCount > 8) {
|
||||
return false;
|
||||
}
|
||||
const hasLeadCue = deps.hasFollowupMarker(text) ||
|
||||
deps.hasReferentialPointer(text) ||
|
||||
/^(?:\u0430|\u0438|also|then|now)(?=$|[\s,.;:!?])/iu.test(normalized);
|
||||
if (!hasLeadCue) {
|
||||
return false;
|
||||
}
|
||||
return /(?:\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|\u0440\u0430\u0437\u043d\u0438\u0446|\u043f\u043e\u043b\u0443\u0447|\u0437\u0430\u043f\u043b\u0430\u0442|\u043f\u043e\u0441\u0442\u0443\u043f|\u0432\u0445\u043e\u0434\u044f\u0449|\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\u043a|\u0434\u0435\u043d\u0435\u0436)/iu.test(normalized);
|
||||
}
|
||||
function shouldKeepPreviousIntentForShortCounterpartyRetarget(userMessage, sourceIntent) {
|
||||
const normalized = deps.compactWhitespace(deps.repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
|
||||
if (!normalized || deps.countTokens(normalized) > 4) {
|
||||
|
|
@ -337,7 +358,12 @@ function createAssistantTransitionPolicy(deps) {
|
|||
(deps.toNonEmptyString(alternateMessage)
|
||||
? deps.isImplicitAddressContinuationByLlm(alternateMessage, llmPreDecomposeMeta)
|
||||
: false));
|
||||
const sourceIntentHint = deps.toNonEmptyString(carryoverSourceDebug?.detected_intent);
|
||||
const sourceIntentHint = (0, assistantContinuityPolicy_1.readAddressDebugIntent)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryPilotScopeHint = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryPilotScope)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const hasValueFlowCarryoverSourceHint = sourceIntentHint === "customer_revenue_and_payments" ||
|
||||
sourceDiscoveryPilotScopeHint === "counterparty_value_flow_query_movements_v1" ||
|
||||
sourceDiscoveryPilotScopeHint === "counterparty_supplier_payout_query_movements_v1" ||
|
||||
sourceDiscoveryPilotScopeHint === "counterparty_bidirectional_value_flow_query_movements_v1";
|
||||
const navigationSessionState = (0, assistantContinuityPolicy_1.resolveNavigationSessionContextState)(addressNavigationState, deps.toNonEmptyString, deps.normalizeOrganizationScopeValue);
|
||||
const navigationFocusObjectHint = navigationSessionState.focusObject;
|
||||
const hasNavigationInventoryItemFocusHint = Boolean(deps.toNonEmptyString(navigationFocusObjectHint?.label) &&
|
||||
|
|
@ -359,13 +385,19 @@ function createAssistantTransitionPolicy(deps) {
|
|||
? deps.resolveDebtRoleSwapFollowupIntent(String(alternateMessage ?? ""), sourceIntentHint)
|
||||
: null;
|
||||
const debtRoleSwapIntent = debtRoleSwapPrimary ?? debtRoleSwapAlternate ?? null;
|
||||
const shortValueFlowRetargetPrimary = hasValueFlowCarryoverSourceHint && hasShortValueFlowRetargetCue(userMessage);
|
||||
const shortValueFlowRetargetAlternate = hasValueFlowCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
|
||||
? hasShortValueFlowRetargetCue(String(alternateMessage ?? ""))
|
||||
: false;
|
||||
let hasPrimaryFollowupSignal = deps.hasAddressFollowupContextSignal(userMessage) ||
|
||||
Boolean(debtRoleSwapPrimary) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryPurchaseDateVatBridge;
|
||||
let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
||||
Boolean(debtRoleSwapAlternate) ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
inventoryPurchaseDateVatBridge
|
||||
: false;
|
||||
|
|
@ -399,6 +431,8 @@ function createAssistantTransitionPolicy(deps) {
|
|||
hasInventoryRootRestatementAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
Boolean(debtRoleSwapIntent) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
|
|
@ -416,6 +450,8 @@ function createAssistantTransitionPolicy(deps) {
|
|||
hasInventoryRootRestatementAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
Boolean(debtRoleSwapIntent) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
|
|
@ -438,6 +474,8 @@ function createAssistantTransitionPolicy(deps) {
|
|||
!hasInventoryRootTemporalFollowupAlternate &&
|
||||
!hasInventoryRootRestatementPrimary &&
|
||||
!hasInventoryRootRestatementAlternate &&
|
||||
!shortValueFlowRetargetPrimary &&
|
||||
!shortValueFlowRetargetAlternate &&
|
||||
!hasImplicitContinuationSignal &&
|
||||
!hasOrganizationClarificationContinuation &&
|
||||
!hasIndexReferenceSignal) {
|
||||
|
|
@ -449,6 +487,8 @@ function createAssistantTransitionPolicy(deps) {
|
|||
!hasInventoryRootTemporalFollowupAlternate &&
|
||||
!hasInventoryRootRestatementPrimary &&
|
||||
!hasInventoryRootRestatementAlternate &&
|
||||
!shortValueFlowRetargetPrimary &&
|
||||
!shortValueFlowRetargetAlternate &&
|
||||
!hasImplicitContinuationSignal &&
|
||||
!hasOrganizationClarificationContinuation &&
|
||||
!hasIndexReferenceSignal) {
|
||||
|
|
@ -457,7 +497,15 @@ function createAssistantTransitionPolicy(deps) {
|
|||
if (!carryoverSourceDebug) {
|
||||
return null;
|
||||
}
|
||||
const sourceIntent = deps.toNonEmptyString(carryoverSourceDebug.detected_intent);
|
||||
const sourceIntent = (0, assistantContinuityPolicy_1.readAddressDebugIntent)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryPilotScope = sourceDiscoveryPilotScopeHint;
|
||||
const sourceDiscoveryMetadataRouteFamily = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryMetadataRouteFamily)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryMetadataSelectedEntitySet = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryMetadataSelectedEntitySet)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryMetadataAmbiguityDetected = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryMetadataAmbiguityDetected)(carryoverSourceDebug);
|
||||
const sourceDiscoveryMetadataAmbiguityEntitySets = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryMetadataAmbiguityEntitySets)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryEntityResolutionStatus = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryEntityResolutionStatus)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryEntityCandidates = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryEntityCandidates)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryEntityAmbiguityCandidates = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryEntityAmbiguityCandidates)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const llmExplicitIntent = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
|
||||
const llmSelectedObjectScopeDetected = llmPreDecomposeMeta?.predecomposeContract?.semantics?.selected_object_scope_detected === true;
|
||||
const resolvedPrimaryIntent = deps.resolveAddressIntent(deps.repairAddressMojibake(String(userMessage ?? ""))).intent;
|
||||
|
|
@ -546,12 +594,14 @@ function createAssistantTransitionPolicy(deps) {
|
|||
hasPrimaryFollowupSignal =
|
||||
deps.hasAddressFollowupContextSignal(userMessage) ||
|
||||
Boolean(debtRoleSwapPrimary) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
hasInventoryRootTemporalFollowupPrimary;
|
||||
hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
||||
Boolean(debtRoleSwapAlternate) ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
hasInventoryRootTemporalFollowupAlternate
|
||||
|
|
@ -567,6 +617,8 @@ function createAssistantTransitionPolicy(deps) {
|
|||
hasInventoryRootTemporalFollowupAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
Boolean(debtRoleSwapIntent) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
|
|
@ -687,6 +739,16 @@ function createAssistantTransitionPolicy(deps) {
|
|||
previous_filters: previousFilters,
|
||||
previous_anchor_type: previousAnchorType ?? undefined,
|
||||
previous_anchor_value: previousAnchor,
|
||||
previous_discovery_pilot_scope: sourceDiscoveryPilotScope ?? undefined,
|
||||
previous_discovery_entity_resolution_status: sourceDiscoveryEntityResolutionStatus ?? undefined,
|
||||
previous_discovery_entity_candidates: sourceDiscoveryEntityCandidates.length > 0 ? sourceDiscoveryEntityCandidates : undefined,
|
||||
previous_discovery_entity_ambiguity_candidates: sourceDiscoveryEntityAmbiguityCandidates.length > 0
|
||||
? sourceDiscoveryEntityAmbiguityCandidates
|
||||
: undefined,
|
||||
previous_discovery_metadata_route_family: sourceDiscoveryMetadataRouteFamily ?? undefined,
|
||||
previous_discovery_metadata_selected_entity_set: sourceDiscoveryMetadataSelectedEntitySet ?? undefined,
|
||||
previous_discovery_metadata_ambiguity_detected: sourceDiscoveryMetadataAmbiguityDetected || undefined,
|
||||
previous_discovery_metadata_ambiguity_entity_sets: sourceDiscoveryMetadataAmbiguityEntitySets.length > 0 ? sourceDiscoveryMetadataAmbiguityEntitySets : undefined,
|
||||
resolved_counterparty_from_display: resolvedCounterpartyFromDisplay || undefined,
|
||||
root_context_only: rootScopedPivot || undefined,
|
||||
root_intent: shouldAttachInventoryRootFrame ? inventoryRootFrame?.intent ?? undefined : undefined,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,10 @@ const SUPPORTED_ADDRESS_INTENTS = new Set([
|
|||
"payables_confirmed_as_of_date",
|
||||
"list_documents_by_counterparty",
|
||||
"customer_revenue_and_payments",
|
||||
"inventory_on_hand_as_of_date"
|
||||
"inventory_on_hand_as_of_date",
|
||||
"vat_liability_confirmed_for_tax_period",
|
||||
"vat_payable_confirmed_as_of_date",
|
||||
"vat_payable_forecast"
|
||||
]);
|
||||
function fallbackCompactWhitespace(value) {
|
||||
return String(value ?? "").replace(/\s+/g, " ").trim();
|
||||
|
|
@ -85,8 +88,23 @@ function detectCounterpartyTurnoverFamily(text) {
|
|||
"\u0434\u043e\u0445\u043e\u0434",
|
||||
"\u0431\u044b\u043b",
|
||||
"\u0431\u044b\u043b\u0430",
|
||||
"\u0432\u0440\u0435\u043c\u044f",
|
||||
"\u0432\u0440\u0435\u043c\u0435\u043d\u0438",
|
||||
"\u0433\u043e\u0434",
|
||||
"\u0433\u043e\u0434\u0430",
|
||||
"\u043f\u0435\u0440\u0438\u043e\u0434",
|
||||
"\u043f\u0435\u0440\u0438\u043e\u0434\u0430",
|
||||
"\u043c\u0435\u0441\u044f\u0446",
|
||||
"\u043c\u0435\u0441\u044f\u0446\u0430",
|
||||
"\u043a\u0432\u0430\u0440\u0442\u0430\u043b",
|
||||
"\u043a\u0432\u0430\u0440\u0442\u0430\u043b\u0430",
|
||||
"turnover",
|
||||
"revenue"
|
||||
"revenue",
|
||||
"time",
|
||||
"year",
|
||||
"period",
|
||||
"month",
|
||||
"quarter"
|
||||
]);
|
||||
const entity = rawEntity && !ignored.has(rawEntity) ? rawEntity : null;
|
||||
return {
|
||||
|
|
@ -94,6 +112,18 @@ function detectCounterpartyTurnoverFamily(text) {
|
|||
entity
|
||||
};
|
||||
}
|
||||
function detectBroadBusinessEvaluation(text) {
|
||||
const normalized = String(text ?? "");
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
if (/(?:как\s+ты\s+оценишь\s+деятельност[ьи]\s+компан|оценк[аи]?\s+деятельност[ьи]\s+компан|что\s+у\s+нас\s+вообще\s+происход|где\s+главн(?:ые|ый)\s+риски|как\s+у\s+нас\s+дела\s+по\s+компан)/iu.test(normalized)) {
|
||||
return {
|
||||
family: "broad_business_evaluation"
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function buildEntityCandidates(counterpartyTurnover) {
|
||||
if (!counterpartyTurnover?.entity) {
|
||||
return [];
|
||||
|
|
@ -115,9 +145,16 @@ function createAssistantTurnMeaningPolicy(deps = {}) {
|
|||
const joinedText = fallbackCompactWhitespace(`${rawText} ${effectiveText}`);
|
||||
const supportedIntent = detectSupportedIntent(joinedText, deps);
|
||||
const counterpartyTurnover = detectCounterpartyTurnoverFamily(joinedText);
|
||||
const broadBusinessEvaluation = detectBroadBusinessEvaluation(joinedText);
|
||||
const llmIntent = toNonEmptyString(input?.llmPreDecomposeMeta?.predecomposeContract?.intent, deps);
|
||||
const explicitIntentCandidate = supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null);
|
||||
const unsupportedFamily = !explicitIntentCandidate && counterpartyTurnover?.family ? counterpartyTurnover.family : null;
|
||||
const explicitIntentCandidate = broadBusinessEvaluation?.family
|
||||
? null
|
||||
: supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null);
|
||||
const unsupportedFamily = broadBusinessEvaluation?.family
|
||||
? broadBusinessEvaluation.family
|
||||
: !explicitIntentCandidate && counterpartyTurnover?.family
|
||||
? counterpartyTurnover.family
|
||||
: null;
|
||||
const reasonCodes = [];
|
||||
if (supportedIntent?.reason) {
|
||||
reasonCodes.push(supportedIntent.reason);
|
||||
|
|
@ -125,6 +162,9 @@ function createAssistantTurnMeaningPolicy(deps = {}) {
|
|||
if (counterpartyTurnover?.family) {
|
||||
reasonCodes.push("counterparty_turnover_current_turn_signal");
|
||||
}
|
||||
if (broadBusinessEvaluation?.family) {
|
||||
reasonCodes.push("broad_business_evaluation_current_turn_signal");
|
||||
}
|
||||
if (rawText !== normalizeTurnText(rawMessage, { ...deps, repairAddressMojibake: (value) => String(value ?? "") })) {
|
||||
reasonCodes.push("mojibake_repair_applied");
|
||||
}
|
||||
|
|
@ -136,23 +176,35 @@ function createAssistantTurnMeaningPolicy(deps = {}) {
|
|||
? "receivables"
|
||||
: explicitIntentCandidate?.startsWith("payables_")
|
||||
? "payables"
|
||||
: explicitIntentCandidate?.startsWith("inventory_")
|
||||
? "inventory"
|
||||
: explicitIntentCandidate?.includes("counterparty")
|
||||
? "counterparty"
|
||||
: counterpartyTurnover?.family
|
||||
? "counterparty"
|
||||
: null;
|
||||
: explicitIntentCandidate?.startsWith("vat_")
|
||||
? "vat"
|
||||
: explicitIntentCandidate?.startsWith("inventory_")
|
||||
? "inventory"
|
||||
: broadBusinessEvaluation?.family
|
||||
? "business_summary"
|
||||
: explicitIntentCandidate?.includes("counterparty")
|
||||
? "counterparty"
|
||||
: counterpartyTurnover?.family
|
||||
? "counterparty"
|
||||
: null;
|
||||
const askedActionFamily = explicitIntentCandidate === "receivables_confirmed_as_of_date" ||
|
||||
explicitIntentCandidate === "payables_confirmed_as_of_date" ||
|
||||
explicitIntentCandidate === "inventory_on_hand_as_of_date"
|
||||
? "confirmed_snapshot"
|
||||
: explicitIntentCandidate === "list_documents_by_counterparty"
|
||||
? "list_documents"
|
||||
: counterpartyTurnover?.family
|
||||
? "counterparty_value_or_turnover"
|
||||
: null;
|
||||
const staleReplayForbidden = Boolean(unsupportedFamily || (counterpartyTurnover?.entity && !explicitIntentCandidate));
|
||||
: broadBusinessEvaluation?.family
|
||||
? "broad_evaluation"
|
||||
: explicitIntentCandidate === "vat_liability_confirmed_for_tax_period"
|
||||
? "confirmed_tax_period"
|
||||
: explicitIntentCandidate === "vat_payable_confirmed_as_of_date"
|
||||
? "confirmed_snapshot"
|
||||
: explicitIntentCandidate === "vat_payable_forecast"
|
||||
? "forecast"
|
||||
: explicitIntentCandidate === "list_documents_by_counterparty"
|
||||
? "list_documents"
|
||||
: counterpartyTurnover?.family
|
||||
? "counterparty_value_or_turnover"
|
||||
: null;
|
||||
const staleReplayForbidden = Boolean(unsupportedFamily || broadBusinessEvaluation?.family || (counterpartyTurnover?.entity && !explicitIntentCandidate));
|
||||
return {
|
||||
schema_version: "assistant_turn_meaning_v1",
|
||||
raw_message: rawMessage,
|
||||
|
|
@ -163,7 +215,9 @@ function createAssistantTurnMeaningPolicy(deps = {}) {
|
|||
asked_action_family: askedActionFamily,
|
||||
explicit_intent_candidate: explicitIntentCandidate,
|
||||
explicit_entity_candidates: buildEntityCandidates(counterpartyTurnover),
|
||||
meaning_confidence: supportedIntent?.confidence ?? (counterpartyTurnover?.family ? "medium" : "low"),
|
||||
meaning_confidence: broadBusinessEvaluation?.family
|
||||
? "medium"
|
||||
: supportedIntent?.confidence ?? (counterpartyTurnover?.family ? "medium" : "low"),
|
||||
intent_override_strength: explicitIntentCandidate
|
||||
? "explicit_current_turn_intent"
|
||||
: staleReplayForbidden
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ export interface AddressFollowupContext {
|
|||
| "unknown"
|
||||
| null;
|
||||
previous_anchor_value?: string | null;
|
||||
previous_discovery_entity_resolution_status?: "resolved" | "ambiguous" | "not_found" | null;
|
||||
previous_discovery_entity_ambiguity_candidates?: string[];
|
||||
resolved_counterparty_from_display?: boolean;
|
||||
root_intent?: AddressIntent;
|
||||
root_filters?: AddressFilterSet;
|
||||
|
|
|
|||
|
|
@ -284,6 +284,7 @@ export function runAssistantAddressLaneResponseRuntime<ResponseType = AssistantM
|
|||
const mcpDiscoveryResponsePolicy = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply: guardedResponse.assistantReply,
|
||||
currentReplySource: "address_query_runtime_v1",
|
||||
currentReplyType: guardedResponse.replyType,
|
||||
addressRuntimeMeta: debugWithResponseGuard
|
||||
});
|
||||
const finalAssistantReply = mcpDiscoveryResponsePolicy.applied
|
||||
|
|
|
|||
|
|
@ -285,7 +285,7 @@ export async function buildAssistantAddressOrchestrationRuntime(
|
|||
);
|
||||
}
|
||||
|
||||
const followupContext = carryover?.followupContext ?? null;
|
||||
const followupContext = toRecordObject(carryover?.followupContext);
|
||||
const routePolicyRuntime = runAssistantRoutePolicyRuntime({
|
||||
rawUserMessage: input.userMessage,
|
||||
effectiveAddressUserMessage: addressInputMessage,
|
||||
|
|
@ -313,7 +313,8 @@ export async function buildAssistantAddressOrchestrationRuntime(
|
|||
userMessage: input.userMessage,
|
||||
effectiveMessage: addressInputMessage,
|
||||
assistantTurnMeaning: toRecordObject(orchestrationContract?.assistant_turn_meaning),
|
||||
predecomposeContract
|
||||
predecomposeContract,
|
||||
followupContext
|
||||
})) as Record<string, unknown>;
|
||||
} catch (error) {
|
||||
mcpDiscoveryRuntimeEntryPointError = String(error instanceof Error ? error.message : error ?? "unknown_error").slice(0, 280);
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export interface AssistantContinuitySnapshot {
|
|||
|
||||
export interface AssistantAddressDebugContextFacts {
|
||||
item: string | null;
|
||||
counterparty: string | null;
|
||||
organization: string | null;
|
||||
scopedDate: string | null;
|
||||
}
|
||||
|
|
@ -107,6 +108,304 @@ function toRecordObject(value: unknown): Record<string, unknown> | null {
|
|||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function candidateValue(value: unknown): string | null {
|
||||
const direct = fallbackToNonEmptyString(value);
|
||||
if (direct && direct !== "[object Object]") {
|
||||
return direct;
|
||||
}
|
||||
const record = toRecordObject(value);
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
fallbackToNonEmptyString(record.value) ??
|
||||
fallbackToNonEmptyString(record.name) ??
|
||||
fallbackToNonEmptyString(record.ref) ??
|
||||
fallbackToNonEmptyString(record.text)
|
||||
);
|
||||
}
|
||||
|
||||
function readAssistantMcpDiscoveryEntry(
|
||||
debug: Record<string, unknown> | null
|
||||
): Record<string, unknown> | null {
|
||||
const entry = toRecordObject(debug?.assistant_mcp_discovery_entry_point_v1);
|
||||
return fallbackToNonEmptyString(entry?.schema_version) === "assistant_mcp_discovery_runtime_entry_point_v1"
|
||||
? entry
|
||||
: null;
|
||||
}
|
||||
|
||||
function readAssistantMcpDiscoveryTurnMeaning(
|
||||
debug: Record<string, unknown> | null
|
||||
): Record<string, unknown> | null {
|
||||
const entry = readAssistantMcpDiscoveryEntry(debug);
|
||||
const turnInput = toRecordObject(entry?.turn_input);
|
||||
return toRecordObject(turnInput?.turn_meaning_ref);
|
||||
}
|
||||
|
||||
function readAssistantMcpDiscoveryTurnMeaningMetadataAmbiguityEntitySets(
|
||||
debug: Record<string, unknown> | null,
|
||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||
): string[] {
|
||||
const values = readAssistantMcpDiscoveryTurnMeaning(debug)?.metadata_ambiguity_entity_sets;
|
||||
if (!Array.isArray(values)) {
|
||||
return [];
|
||||
}
|
||||
return values.map((item) => toNonEmptyString(item)).filter((item): item is string => Boolean(item));
|
||||
}
|
||||
|
||||
function readAssistantMcpDiscoveryActionFamily(
|
||||
debug: Record<string, unknown> | null,
|
||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||
): string | null {
|
||||
return toNonEmptyString(readAssistantMcpDiscoveryTurnMeaning(debug)?.asked_action_family);
|
||||
}
|
||||
|
||||
function readAssistantMcpDiscoveryBridge(
|
||||
debug: Record<string, unknown> | null
|
||||
): Record<string, unknown> | null {
|
||||
return toRecordObject(readAssistantMcpDiscoveryEntry(debug)?.bridge);
|
||||
}
|
||||
|
||||
function readAssistantMcpDiscoveryDerivedMetadataSurface(
|
||||
debug: Record<string, unknown> | null
|
||||
): Record<string, unknown> | null {
|
||||
const bridge = readAssistantMcpDiscoveryBridge(debug);
|
||||
const pilot = toRecordObject(bridge?.pilot);
|
||||
return toRecordObject(pilot?.derived_metadata_surface);
|
||||
}
|
||||
|
||||
function readAssistantMcpDiscoveryDerivedEntityResolution(
|
||||
debug: Record<string, unknown> | null
|
||||
): Record<string, unknown> | null {
|
||||
const bridge = readAssistantMcpDiscoveryBridge(debug);
|
||||
const pilot = toRecordObject(bridge?.pilot);
|
||||
return toRecordObject(pilot?.derived_entity_resolution);
|
||||
}
|
||||
|
||||
export function readAssistantMcpDiscoveryEntityResolutionStatus(
|
||||
debug: Record<string, unknown> | null,
|
||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||
): string | null {
|
||||
return toNonEmptyString(readAssistantMcpDiscoveryDerivedEntityResolution(debug)?.resolution_status);
|
||||
}
|
||||
|
||||
export function readAssistantMcpDiscoveryEntityAmbiguityCandidates(
|
||||
debug: Record<string, unknown> | null,
|
||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||
): string[] {
|
||||
const values = readAssistantMcpDiscoveryDerivedEntityResolution(debug)?.ambiguity_candidates;
|
||||
if (!Array.isArray(values)) {
|
||||
return [];
|
||||
}
|
||||
return values.map((item) => toNonEmptyString(item)).filter((item): item is string => Boolean(item));
|
||||
}
|
||||
|
||||
function collectAssistantMcpDiscoveryEntityCandidates(
|
||||
debug: Record<string, unknown> | null,
|
||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||
): string[] {
|
||||
const result: string[] = [];
|
||||
const resolution = readAssistantMcpDiscoveryDerivedEntityResolution(debug);
|
||||
const pushCandidate = (value: unknown): void => {
|
||||
const text = toNonEmptyString(value);
|
||||
if (text && !result.includes(text)) {
|
||||
result.push(text);
|
||||
}
|
||||
};
|
||||
|
||||
pushCandidate(resolution?.resolved_entity);
|
||||
pushCandidate(resolution?.requested_entity);
|
||||
if (Array.isArray(resolution?.ambiguity_candidates)) {
|
||||
for (const candidate of resolution.ambiguity_candidates) {
|
||||
pushCandidate(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
const discoveryMeaning = readAssistantMcpDiscoveryTurnMeaning(debug);
|
||||
const explicitEntities = Array.isArray(discoveryMeaning?.explicit_entity_candidates)
|
||||
? discoveryMeaning.explicit_entity_candidates
|
||||
: [];
|
||||
for (const entity of explicitEntities) {
|
||||
pushCandidate(candidateValue(entity));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function readAssistantMcpDiscoveryEntityCandidates(
|
||||
debug: Record<string, unknown> | null,
|
||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||
): string[] {
|
||||
return collectAssistantMcpDiscoveryEntityCandidates(debug, toNonEmptyString);
|
||||
}
|
||||
|
||||
export function readAssistantMcpDiscoveryPilotScope(
|
||||
debug: Record<string, unknown> | null,
|
||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||
): string | null {
|
||||
const bridge = readAssistantMcpDiscoveryBridge(debug);
|
||||
const pilot = toRecordObject(bridge?.pilot);
|
||||
return toNonEmptyString(pilot?.pilot_scope);
|
||||
}
|
||||
|
||||
export function readAssistantMcpDiscoveryMetadataRouteFamily(
|
||||
debug: Record<string, unknown> | null,
|
||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||
): string | null {
|
||||
return toNonEmptyString(readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.downstream_route_family);
|
||||
}
|
||||
|
||||
export function readAssistantMcpDiscoveryMetadataSelectedEntitySet(
|
||||
debug: Record<string, unknown> | null,
|
||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||
): string | null {
|
||||
return toNonEmptyString(readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.selected_entity_set);
|
||||
}
|
||||
|
||||
export function readAssistantMcpDiscoveryMetadataAmbiguityDetected(
|
||||
debug: Record<string, unknown> | null
|
||||
): boolean {
|
||||
return (
|
||||
readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.ambiguity_detected === true ||
|
||||
readAssistantMcpDiscoveryTurnMeaningMetadataAmbiguityEntitySets(debug).length > 0
|
||||
);
|
||||
}
|
||||
|
||||
export function readAssistantMcpDiscoveryMetadataAmbiguityEntitySets(
|
||||
debug: Record<string, unknown> | null,
|
||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||
): string[] {
|
||||
const values = readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.ambiguity_entity_sets;
|
||||
if (Array.isArray(values)) {
|
||||
return values.map((item) => toNonEmptyString(item)).filter((item): item is string => Boolean(item));
|
||||
}
|
||||
return readAssistantMcpDiscoveryTurnMeaningMetadataAmbiguityEntitySets(debug, toNonEmptyString);
|
||||
}
|
||||
|
||||
function mapAssistantMcpDiscoveryPilotScopeToAddressIntent(
|
||||
pilotScope: string | null,
|
||||
actionFamily: string | null
|
||||
): string | null {
|
||||
if (pilotScope === "counterparty_lifecycle_query_documents_v1") {
|
||||
return "counterparty_activity_lifecycle";
|
||||
}
|
||||
if (pilotScope === "counterparty_document_evidence_query_documents_v1") {
|
||||
return "list_documents_by_counterparty";
|
||||
}
|
||||
if (pilotScope === "counterparty_movement_evidence_query_movements_v1") {
|
||||
return "bank_operations_by_counterparty";
|
||||
}
|
||||
if (pilotScope === "counterparty_supplier_payout_query_movements_v1") {
|
||||
return "supplier_payouts_profile";
|
||||
}
|
||||
if (pilotScope === "counterparty_value_flow_query_movements_v1") {
|
||||
return "customer_revenue_and_payments";
|
||||
}
|
||||
if (pilotScope === "counterparty_bidirectional_value_flow_query_movements_v1") {
|
||||
return null;
|
||||
}
|
||||
if (actionFamily === "activity_duration") {
|
||||
return "counterparty_activity_lifecycle";
|
||||
}
|
||||
if (actionFamily === "payout") {
|
||||
return "supplier_payouts_profile";
|
||||
}
|
||||
if (actionFamily === "turnover") {
|
||||
return "customer_revenue_and_payments";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function readDiscoveryDateScopeFilters(
|
||||
debug: Record<string, unknown> | null,
|
||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||
): {
|
||||
asOfDate: string | null;
|
||||
periodFrom: string | null;
|
||||
periodTo: string | null;
|
||||
} {
|
||||
const explicitDateScope = toNonEmptyString(readAssistantMcpDiscoveryTurnMeaning(debug)?.explicit_date_scope);
|
||||
if (!explicitDateScope) {
|
||||
return {
|
||||
asOfDate: null,
|
||||
periodFrom: null,
|
||||
periodTo: null
|
||||
};
|
||||
}
|
||||
const isoDateMatch = explicitDateScope.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
if (isoDateMatch) {
|
||||
return {
|
||||
asOfDate: explicitDateScope,
|
||||
periodFrom: null,
|
||||
periodTo: null
|
||||
};
|
||||
}
|
||||
const monthMatch = explicitDateScope.match(/^(\d{4})-(\d{2})$/);
|
||||
if (monthMatch) {
|
||||
const year = Number(monthMatch[1]);
|
||||
const month = Number(monthMatch[2]);
|
||||
if (Number.isFinite(year) && Number.isFinite(month) && month >= 1 && month <= 12) {
|
||||
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
||||
return {
|
||||
asOfDate: null,
|
||||
periodFrom: `${monthMatch[1]}-${monthMatch[2]}-01`,
|
||||
periodTo: `${monthMatch[1]}-${monthMatch[2]}-${String(lastDay).padStart(2, "0")}`
|
||||
};
|
||||
}
|
||||
}
|
||||
const yearMatch = explicitDateScope.match(/^(\d{4})$/);
|
||||
if (yearMatch) {
|
||||
return {
|
||||
asOfDate: null,
|
||||
periodFrom: `${yearMatch[1]}-01-01`,
|
||||
periodTo: `${yearMatch[1]}-12-31`
|
||||
};
|
||||
}
|
||||
const rangeMatch = explicitDateScope.match(/^(\d{4}-\d{2}-\d{2})\.\.(\d{4}-\d{2}-\d{2})$/);
|
||||
if (rangeMatch) {
|
||||
return {
|
||||
asOfDate: null,
|
||||
periodFrom: rangeMatch[1],
|
||||
periodTo: rangeMatch[2]
|
||||
};
|
||||
}
|
||||
return {
|
||||
asOfDate: null,
|
||||
periodFrom: null,
|
||||
periodTo: null
|
||||
};
|
||||
}
|
||||
|
||||
function formatDiscoveryDateScopeForReply(value: unknown): string | null {
|
||||
const text = fallbackToNonEmptyString(value);
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
return formatIsoDateForReply(text) ?? text;
|
||||
}
|
||||
|
||||
function hasGroundedDiscoveryBusinessAnswer(
|
||||
debug: Record<string, unknown> | null,
|
||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||
): boolean {
|
||||
if (!debug || debug.mcp_discovery_response_applied !== true) {
|
||||
return false;
|
||||
}
|
||||
const entry = readAssistantMcpDiscoveryEntry(debug);
|
||||
const bridge = readAssistantMcpDiscoveryBridge(debug);
|
||||
const bridgeStatus = toNonEmptyString(bridge?.bridge_status);
|
||||
const answerDraft = toRecordObject(bridge?.answer_draft);
|
||||
const answerMode = toNonEmptyString(answerDraft?.answer_mode);
|
||||
return Boolean(
|
||||
entry &&
|
||||
toNonEmptyString(entry.entry_status) === "bridge_executed" &&
|
||||
bridgeStatus === "answer_draft_ready" &&
|
||||
(bridge?.business_fact_answer_allowed === true ||
|
||||
answerMode === "confirmed_with_bounded_inference" ||
|
||||
answerMode === "bounded_inference_only")
|
||||
);
|
||||
}
|
||||
|
||||
function mergeKnownOrganizationsDefault(values: unknown[]): string[] {
|
||||
return mergeKnownOrganizationsFromMatcher(values);
|
||||
}
|
||||
|
|
@ -141,13 +440,55 @@ export function readAddressDebugItem(
|
|||
);
|
||||
}
|
||||
|
||||
export function readAddressDebugCounterparty(
|
||||
debug: Record<string, unknown> | null,
|
||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||
): string | null {
|
||||
const extractedFilters = readAddressDebugFilters(debug);
|
||||
if (toNonEmptyString(extractedFilters?.counterparty)) {
|
||||
return toNonEmptyString(extractedFilters?.counterparty);
|
||||
}
|
||||
if (String(debug?.anchor_type ?? "") === "counterparty") {
|
||||
return toNonEmptyString(debug?.anchor_value_resolved) ?? toNonEmptyString(debug?.anchor_value_raw);
|
||||
}
|
||||
const discoveryEntities = collectAssistantMcpDiscoveryEntityCandidates(debug, toNonEmptyString);
|
||||
for (const entity of discoveryEntities) {
|
||||
const text = toNonEmptyString(entity);
|
||||
if (text) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function readAddressDebugIntent(
|
||||
debug: Record<string, unknown> | null,
|
||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||
): string | null {
|
||||
const detectedIntent = toNonEmptyString(debug?.detected_intent);
|
||||
if (detectedIntent && detectedIntent !== "unknown") {
|
||||
return detectedIntent;
|
||||
}
|
||||
return mapAssistantMcpDiscoveryPilotScopeToAddressIntent(
|
||||
readAssistantMcpDiscoveryPilotScope(debug, toNonEmptyString),
|
||||
readAssistantMcpDiscoveryActionFamily(debug, toNonEmptyString)
|
||||
);
|
||||
}
|
||||
|
||||
export function readAddressDebugOrganization(
|
||||
debug: Record<string, unknown> | null,
|
||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||
): string | null {
|
||||
const extractedFilters = readAddressDebugFilters(debug);
|
||||
const rootFrameContext = toRecordObject(debug?.address_root_frame_context);
|
||||
return toNonEmptyString(extractedFilters?.organization) ?? toNonEmptyString(rootFrameContext?.organization);
|
||||
const discoveryMeaning = readAssistantMcpDiscoveryTurnMeaning(debug);
|
||||
return (
|
||||
toNonEmptyString(extractedFilters?.organization) ??
|
||||
toNonEmptyString(rootFrameContext?.organization) ??
|
||||
toNonEmptyString(discoveryMeaning?.explicit_organization_scope) ??
|
||||
toNonEmptyString(debug?.assistant_active_organization) ??
|
||||
toNonEmptyString(debug?.living_chat_selected_organization)
|
||||
);
|
||||
}
|
||||
|
||||
export function readAddressDebugScopedDate(debug: Record<string, unknown> | null): string | null {
|
||||
|
|
@ -156,7 +497,8 @@ export function readAddressDebugScopedDate(debug: Record<string, unknown> | null
|
|||
return (
|
||||
formatIsoDateForReply(extractedFilters?.as_of_date) ??
|
||||
formatIsoDateForReply(rootFrameContext?.as_of_date) ??
|
||||
formatIsoDateForReply(extractedFilters?.period_to)
|
||||
formatIsoDateForReply(extractedFilters?.period_to) ??
|
||||
formatDiscoveryDateScopeForReply(readAssistantMcpDiscoveryTurnMeaning(debug)?.explicit_date_scope)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -166,10 +508,20 @@ export function readAddressDebugTemporalScope(
|
|||
): AssistantAddressDebugTemporalScope {
|
||||
const extractedFilters = readAddressDebugFilters(debug);
|
||||
const rootFrameContext = toRecordObject(debug?.address_root_frame_context);
|
||||
const discoveryDateScope = readDiscoveryDateScopeFilters(debug, toNonEmptyString);
|
||||
return {
|
||||
asOfDate: toNonEmptyString(extractedFilters?.as_of_date) ?? toNonEmptyString(rootFrameContext?.as_of_date),
|
||||
periodFrom: toNonEmptyString(extractedFilters?.period_from) ?? toNonEmptyString(rootFrameContext?.period_from),
|
||||
periodTo: toNonEmptyString(extractedFilters?.period_to) ?? toNonEmptyString(rootFrameContext?.period_to)
|
||||
asOfDate:
|
||||
toNonEmptyString(extractedFilters?.as_of_date) ??
|
||||
toNonEmptyString(rootFrameContext?.as_of_date) ??
|
||||
discoveryDateScope.asOfDate,
|
||||
periodFrom:
|
||||
toNonEmptyString(extractedFilters?.period_from) ??
|
||||
toNonEmptyString(rootFrameContext?.period_from) ??
|
||||
discoveryDateScope.periodFrom,
|
||||
periodTo:
|
||||
toNonEmptyString(extractedFilters?.period_to) ??
|
||||
toNonEmptyString(rootFrameContext?.period_to) ??
|
||||
discoveryDateScope.periodTo
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -202,6 +554,13 @@ export function resolveAddressDebugAnchorContext(
|
|||
anchorValue: counterparty
|
||||
};
|
||||
}
|
||||
const discoveryCounterparty = readAddressDebugCounterparty(debug, toNonEmptyString);
|
||||
if (discoveryCounterparty) {
|
||||
return {
|
||||
anchorType: "counterparty",
|
||||
anchorValue: discoveryCounterparty
|
||||
};
|
||||
}
|
||||
const account = toNonEmptyString(extractedFilters?.account);
|
||||
if (account) {
|
||||
return {
|
||||
|
|
@ -250,6 +609,7 @@ export function resolveAddressDebugContextFacts(
|
|||
): AssistantAddressDebugContextFacts {
|
||||
return {
|
||||
item: readAddressDebugItem(debug, toNonEmptyString),
|
||||
counterparty: readAddressDebugCounterparty(debug, toNonEmptyString),
|
||||
organization: readAddressDebugOrganization(debug, toNonEmptyString),
|
||||
scopedDate: readAddressDebugScopedDate(debug)
|
||||
};
|
||||
|
|
@ -261,6 +621,36 @@ export function resolveAddressDebugCarryoverFilters(
|
|||
): Record<string, unknown> {
|
||||
const extractedFilters = readAddressDebugFilters(debug);
|
||||
const nextFilters = extractedFilters ? { ...extractedFilters } : {};
|
||||
const discoveryDateScope = readDiscoveryDateScopeFilters(debug, toNonEmptyString);
|
||||
const preferGroundedDiscoveryDateScope =
|
||||
hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString) &&
|
||||
Boolean(discoveryDateScope.asOfDate || discoveryDateScope.periodFrom || discoveryDateScope.periodTo);
|
||||
const counterparty = readAddressDebugCounterparty(debug, toNonEmptyString);
|
||||
const organization = readAddressDebugOrganization(debug, toNonEmptyString);
|
||||
if (counterparty && !toNonEmptyString(nextFilters.counterparty)) {
|
||||
nextFilters.counterparty = counterparty;
|
||||
}
|
||||
if (organization && !toNonEmptyString(nextFilters.organization)) {
|
||||
nextFilters.organization = organization;
|
||||
}
|
||||
if (discoveryDateScope.asOfDate && (preferGroundedDiscoveryDateScope || !toNonEmptyString(nextFilters.as_of_date))) {
|
||||
nextFilters.as_of_date = discoveryDateScope.asOfDate;
|
||||
delete nextFilters.period_from;
|
||||
delete nextFilters.period_to;
|
||||
}
|
||||
if (
|
||||
discoveryDateScope.periodFrom &&
|
||||
(preferGroundedDiscoveryDateScope || !toNonEmptyString(nextFilters.period_from))
|
||||
) {
|
||||
nextFilters.period_from = discoveryDateScope.periodFrom;
|
||||
}
|
||||
if (
|
||||
discoveryDateScope.periodTo &&
|
||||
(preferGroundedDiscoveryDateScope || !toNonEmptyString(nextFilters.period_to))
|
||||
) {
|
||||
nextFilters.period_to = discoveryDateScope.periodTo;
|
||||
delete nextFilters.as_of_date;
|
||||
}
|
||||
const inventoryRootFrame = buildInventoryRootFrameFromAddressDebug(debug, toNonEmptyString);
|
||||
const rootFilters =
|
||||
inventoryRootFrame?.filters && typeof inventoryRootFrame.filters === "object"
|
||||
|
|
@ -897,13 +1287,12 @@ export function isGroundedAddressDebug(
|
|||
if (!debug || typeof debug !== "object") {
|
||||
return false;
|
||||
}
|
||||
const executionLane = toNonEmptyString(debug.execution_lane);
|
||||
if (executionLane !== "address_query") {
|
||||
return false;
|
||||
}
|
||||
const answerGroundingCheck = toRecordObject(debug.answer_grounding_check);
|
||||
const groundingStatus = toNonEmptyString(answerGroundingCheck?.status);
|
||||
return groundingStatus === "grounded";
|
||||
if (groundingStatus === "grounded" && toNonEmptyString(debug.execution_lane) === "address_query") {
|
||||
return true;
|
||||
}
|
||||
return hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString);
|
||||
}
|
||||
|
||||
function isGroundedInventoryContextDebug(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
buildAddressMemoryRecapReply as buildAddressMemoryRecapReplyFromPolicy,
|
||||
buildBroadBusinessEvaluationReply as buildBroadBusinessEvaluationReplyFromPolicy,
|
||||
buildSelectedObjectAnswerInspectionReply as buildSelectedObjectAnswerInspectionReplyFromPolicy,
|
||||
buildInventoryHistoryCapabilityFollowupReply as buildInventoryHistoryCapabilityFollowupReplyFromPolicy,
|
||||
resolveAssistantLivingChatMemoryContext
|
||||
|
|
@ -191,10 +192,26 @@ export async function runAssistantLivingChatRuntime(
|
|||
? "deterministic_data_scope_contract_live"
|
||||
: "deterministic_data_scope_contract";
|
||||
} else if (unsupportedCurrentTurnMeaningBoundary) {
|
||||
chatText = buildUnsupportedCurrentTurnMeaningBoundaryReply({
|
||||
assistantTurnMeaning
|
||||
});
|
||||
livingChatSource = "deterministic_unsupported_current_turn_boundary";
|
||||
const unsupportedFamily =
|
||||
typeof assistantTurnMeaning?.unsupported_but_understood_family === "string"
|
||||
? assistantTurnMeaning.unsupported_but_understood_family
|
||||
: null;
|
||||
if (unsupportedFamily === "broad_business_evaluation") {
|
||||
const scopedOrganization = selectedOrganization ?? activeOrganization ?? continuityActiveOrganization ?? null;
|
||||
chatText = buildBroadBusinessEvaluationReplyFromPolicy({
|
||||
organization: scopedOrganization,
|
||||
addressDebug: continuitySnapshot.lastGroundedAddressDebug,
|
||||
sessionItems: input.sessionItems,
|
||||
toNonEmptyString: input.toNonEmptyString
|
||||
});
|
||||
activeOrganization = scopedOrganization ?? activeOrganization;
|
||||
livingChatSource = "deterministic_broad_business_evaluation_contract";
|
||||
} else {
|
||||
chatText = buildUnsupportedCurrentTurnMeaningBoundaryReply({
|
||||
assistantTurnMeaning
|
||||
});
|
||||
livingChatSource = "deterministic_unsupported_current_turn_boundary";
|
||||
}
|
||||
} else if ((selectedOrganization || activeOrganization) && input.hasOrganizationFactLookupSignal(userMessage)) {
|
||||
const scopedOrganization = selectedOrganization ?? activeOrganization ?? null;
|
||||
chatText = input.buildAssistantOrganizationFactBoundaryReply(scopedOrganization);
|
||||
|
|
|
|||
|
|
@ -52,6 +52,13 @@ function uniqueStrings(values: string[]): string[] {
|
|||
return result;
|
||||
}
|
||||
|
||||
function formatNamedChoiceList(values: string[]): string {
|
||||
return uniqueStrings(values)
|
||||
.slice(0, 6)
|
||||
.map((value, index) => `${index + 1}. ${value}`)
|
||||
.join("; ");
|
||||
}
|
||||
|
||||
function isInternalMechanicsLine(value: string): boolean {
|
||||
const text = value.toLowerCase();
|
||||
return (
|
||||
|
|
@ -80,6 +87,13 @@ function modeFor(pilot: AssistantMcpDiscoveryPilotExecutionContract): AssistantM
|
|||
if (pilot.pilot_status === "skipped_needs_clarification") {
|
||||
return "needs_clarification";
|
||||
}
|
||||
if (
|
||||
pilot.pilot_scope === "entity_resolution_search_v1" &&
|
||||
(pilot.reason_codes.includes("pilot_entity_resolution_ambiguity_requires_clarification") ||
|
||||
pilot.derived_entity_resolution?.resolution_status === "ambiguous")
|
||||
) {
|
||||
return "needs_clarification";
|
||||
}
|
||||
if (pilot.evidence.answer_permission === "confirmed_answer") {
|
||||
return "confirmed_with_bounded_inference";
|
||||
}
|
||||
|
|
@ -97,7 +111,197 @@ function isValueFlowPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): b
|
|||
);
|
||||
}
|
||||
|
||||
function isDocumentPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||||
return pilot.pilot_scope === "counterparty_document_evidence_query_documents_v1";
|
||||
}
|
||||
|
||||
function isMovementPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||||
return pilot.pilot_scope === "counterparty_movement_evidence_query_movements_v1";
|
||||
}
|
||||
|
||||
function isMetadataPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||||
return pilot.pilot_scope === "metadata_inspection_v1";
|
||||
}
|
||||
|
||||
function isEntityResolutionPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||||
return pilot.pilot_scope === "entity_resolution_search_v1";
|
||||
}
|
||||
|
||||
function isMetadataLaneChoiceClarification(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||||
return (
|
||||
pilot.reason_codes.includes("planner_selected_metadata_lane_clarification_recipe") ||
|
||||
pilot.dry_run.reason_codes.includes("planner_selected_metadata_lane_clarification_recipe")
|
||||
);
|
||||
}
|
||||
|
||||
function askedActionFamily(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||||
const action = pilot.evidence.query_plan.turn_meaning_ref?.asked_action_family;
|
||||
if (typeof action !== "string") {
|
||||
return null;
|
||||
}
|
||||
const normalized = action.trim().toLowerCase();
|
||||
return normalized.length > 0 ? normalized : null;
|
||||
}
|
||||
|
||||
function unsupportedFamily(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||||
const unsupported = pilot.evidence.query_plan.turn_meaning_ref?.unsupported_but_understood_family;
|
||||
if (typeof unsupported !== "string") {
|
||||
return null;
|
||||
}
|
||||
const normalized = unsupported.trim().toLowerCase();
|
||||
return normalized.length > 0 ? normalized : null;
|
||||
}
|
||||
|
||||
function firstEntityCandidate(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||||
const values = Array.isArray(pilot.evidence.query_plan.turn_meaning_ref?.explicit_entity_candidates)
|
||||
? pilot.evidence.query_plan.turn_meaning_ref?.explicit_entity_candidates
|
||||
: [];
|
||||
for (const value of values) {
|
||||
const text = String(value ?? "").trim();
|
||||
if (text) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function explicitDateScope(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||||
const value = pilot.evidence.query_plan.turn_meaning_ref?.explicit_date_scope;
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
const normalized = value.trim();
|
||||
return normalized.length > 0 ? normalized : null;
|
||||
}
|
||||
|
||||
function documentOrMovementScopeRu(pilot: AssistantMcpDiscoveryPilotExecutionContract): string {
|
||||
const entity = firstEntityCandidate(pilot);
|
||||
const period = explicitDateScope(pilot);
|
||||
const entityPart = entity ? ` по контрагенту ${entity}` : "";
|
||||
const periodPart = period ? ` за ${period}` : " в проверенном окне";
|
||||
return `${entityPart}${periodPart}`;
|
||||
}
|
||||
|
||||
function isMovementLaneClarification(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||||
return (
|
||||
isMovementPilot(pilot) ||
|
||||
pilot.reason_codes.includes("planner_selected_movement_recipe") ||
|
||||
pilot.dry_run.reason_codes.includes("planner_selected_movement_recipe") ||
|
||||
askedActionFamily(pilot) === "list_movements" ||
|
||||
unsupportedFamily(pilot) === "movement_evidence"
|
||||
);
|
||||
}
|
||||
|
||||
function isDocumentLaneClarification(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||||
return (
|
||||
isDocumentPilot(pilot) ||
|
||||
pilot.reason_codes.includes("planner_selected_document_recipe") ||
|
||||
pilot.dry_run.reason_codes.includes("planner_selected_document_recipe") ||
|
||||
askedActionFamily(pilot) === "list_documents" ||
|
||||
unsupportedFamily(pilot) === "document_evidence"
|
||||
);
|
||||
}
|
||||
|
||||
function laneScopeSuffix(pilot: AssistantMcpDiscoveryPilotExecutionContract): string {
|
||||
const entity = firstEntityCandidate(pilot);
|
||||
return entity ? ` по "${entity}"` : "";
|
||||
}
|
||||
|
||||
function dryRunMissingAxis(pilot: AssistantMcpDiscoveryPilotExecutionContract, axis: string): boolean {
|
||||
return pilot.dry_run.execution_steps.some((step) =>
|
||||
step.missing_axis_options.some((option) => option.includes(axis))
|
||||
);
|
||||
}
|
||||
|
||||
function clarificationNeedRu(
|
||||
pilot: AssistantMcpDiscoveryPilotExecutionContract
|
||||
): { subject: string; verb: string } {
|
||||
const needsPeriod = dryRunMissingAxis(pilot, "period");
|
||||
const needsOrganization = dryRunMissingAxis(pilot, "organization");
|
||||
if (needsPeriod && needsOrganization) {
|
||||
return { subject: "проверяемый период и организацию", verb: "нужно" };
|
||||
}
|
||||
if (needsPeriod) {
|
||||
return { subject: "проверяемый период", verb: "нужен" };
|
||||
}
|
||||
if (needsOrganization) {
|
||||
return { subject: "организацию", verb: "нужно" };
|
||||
}
|
||||
return { subject: "контекст проверки", verb: "нужно" };
|
||||
}
|
||||
|
||||
function clarificationNextStepLine(
|
||||
pilot: AssistantMcpDiscoveryPilotExecutionContract,
|
||||
laneLabel: string
|
||||
): string {
|
||||
const needsPeriod = dryRunMissingAxis(pilot, "period");
|
||||
const needsOrganization = dryRunMissingAxis(pilot, "organization");
|
||||
const scopeSuffix = laneScopeSuffix(pilot);
|
||||
if (needsPeriod && needsOrganization) {
|
||||
return `Уточните период и организацию, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
|
||||
}
|
||||
if (needsPeriod) {
|
||||
return `Уточните период, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
|
||||
}
|
||||
if (needsOrganization) {
|
||||
return `Уточните организацию, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
|
||||
}
|
||||
return `Уточните контекст проверки, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
|
||||
}
|
||||
|
||||
function metadataRouteFamilyLabelRu(
|
||||
routeFamily: "document_evidence" | "movement_evidence" | "catalog_drilldown" | null
|
||||
): string | null {
|
||||
if (routeFamily === "document_evidence") {
|
||||
return "контур документов";
|
||||
}
|
||||
if (routeFamily === "movement_evidence") {
|
||||
return "контур движений/регистров";
|
||||
}
|
||||
if (routeFamily === "catalog_drilldown") {
|
||||
return "контур справочников и связанных объектов";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string {
|
||||
const askedMonthlyBreakdown =
|
||||
pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" ||
|
||||
pilot.derived_value_flow?.aggregation_axis === "month";
|
||||
if (isEntityResolutionPilot(pilot) && mode === "confirmed_with_bounded_inference") {
|
||||
return "По каталогу 1С найден вероятный контрагент; это заземление сущности для следующего шага, а не еще бизнес-ответ по данным.";
|
||||
}
|
||||
if (isEntityResolutionPilot(pilot) && mode === "needs_clarification") {
|
||||
return "По каталогу 1С нашлось несколько похожих контрагентов, и без уточнения нельзя честно выбрать правильную сущность.";
|
||||
}
|
||||
if (
|
||||
isEntityResolutionPilot(pilot) &&
|
||||
mode === "checked_sources_only" &&
|
||||
pilot.derived_entity_resolution?.resolution_status === "not_found"
|
||||
) {
|
||||
return "По текущему каталожному поиску 1С точный контрагент пока не подтвержден.";
|
||||
}
|
||||
if (isMovementPilot(pilot) && mode === "confirmed_with_bounded_inference") {
|
||||
return `По движениям${documentOrMovementScopeRu(pilot)} в 1С найдены подтвержденные строки; ответ ограничен проверенным окном и найденными строками.`;
|
||||
}
|
||||
if (pilot.derived_metadata_surface && mode === "confirmed_with_bounded_inference") {
|
||||
if (pilot.derived_metadata_surface.ambiguity_detected) {
|
||||
return "По метаданным 1С найдены конкурирующие schema-поверхности; перед следующим шагом нужно удержать неоднозначность явно.";
|
||||
}
|
||||
if (pilot.derived_metadata_surface.downstream_route_family) {
|
||||
return "По метаданным 1С найдена схема и заземлена вероятная поверхность для следующего безопасного шага.";
|
||||
}
|
||||
return "По метаданным 1С найдена доступная схема для дальнейшего безопасного поиска.";
|
||||
}
|
||||
if (askedMonthlyBreakdown && pilot.derived_bidirectional_value_flow && mode === "confirmed_with_bounded_inference") {
|
||||
return "По данным 1С найдены строки входящих и исходящих денежных движений; нетто и помесячная раскладка могут называться только как расчет по найденным строкам и проверенному периоду.";
|
||||
}
|
||||
if (askedMonthlyBreakdown && pilot.derived_value_flow && mode === "confirmed_with_bounded_inference") {
|
||||
if (pilot.derived_value_flow.value_flow_direction === "outgoing_supplier_payout") {
|
||||
return "По данным 1С найдены строки исходящих платежей/списаний; сумму и помесячную раскладку можно называть только в рамках проверенного периода и найденных строк.";
|
||||
}
|
||||
return "По данным 1С найдены строки входящих денежных поступлений; сумму и помесячную раскладку можно называть только в рамках проверенного периода и найденных строк.";
|
||||
}
|
||||
if (pilot.derived_bidirectional_value_flow && mode === "confirmed_with_bounded_inference") {
|
||||
return "По данным 1С найдены строки входящих и исходящих денежных движений; нетто можно называть только как расчет по найденным строкам и проверенному периоду.";
|
||||
}
|
||||
|
|
@ -105,14 +309,37 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
|||
if (pilot.derived_value_flow.value_flow_direction === "outgoing_supplier_payout") {
|
||||
return "По данным 1С найдены строки исходящих платежей/списаний; сумму можно называть только в рамках проверенного периода и найденных строк.";
|
||||
}
|
||||
return "По данным 1С найдены строки денежных движений; сумму можно называть только в рамках проверенного периода и найденных строк.";
|
||||
return "По данным 1С найдены строки входящих денежных поступлений; сумму можно называть только в рамках проверенного периода и найденных строк.";
|
||||
}
|
||||
if (isDocumentPilot(pilot) && mode === "confirmed_with_bounded_inference") {
|
||||
return `По документам${documentOrMovementScopeRu(pilot)} в 1С найдены подтвержденные строки; ответ ограничен проверенным окном и найденными строками.`;
|
||||
}
|
||||
if (isMovementPilot(pilot) && mode === "confirmed_with_bounded_inference") {
|
||||
return `По движениям${documentOrMovementScopeRu(pilot)} в 1С найдены подтвержденные строки; ответ ограничен проверенным окном и найденными строками.`;
|
||||
}
|
||||
if (mode === "confirmed_with_bounded_inference") {
|
||||
return "По данным 1С есть подтвержденная активность; длительность можно оценивать только как вывод из этих строк.";
|
||||
}
|
||||
if (isDocumentPilot(pilot) && mode === "bounded_inference_only") {
|
||||
return `По документам${documentOrMovementScopeRu(pilot)} полный срез не подтвержден; пока есть только ограниченная граница проверенного окна 1С.`;
|
||||
}
|
||||
if (isMovementPilot(pilot) && mode === "bounded_inference_only") {
|
||||
return `По движениям${documentOrMovementScopeRu(pilot)} полный срез не подтвержден; пока есть только ограниченная граница проверенного окна 1С.`;
|
||||
}
|
||||
if (mode === "bounded_inference_only") {
|
||||
return "Точный факт не подтвержден, но есть ограниченная оценка по найденной активности в 1С.";
|
||||
}
|
||||
if (mode === "needs_clarification" && isMetadataLaneChoiceClarification(pilot)) {
|
||||
return "По подтвержденной metadata-поверхности видно несколько конкурирующих data-lane, и без явного выбора дальше идти нельзя.";
|
||||
}
|
||||
if (mode === "needs_clarification" && isMovementLaneClarification(pilot)) {
|
||||
const need = clarificationNeedRu(pilot);
|
||||
return `Могу идти дальше по движениям/регистрам${laneScopeSuffix(pilot)}, но для запуска поиска в 1С ${need.verb} ${need.subject}.`;
|
||||
}
|
||||
if (mode === "needs_clarification" && isDocumentLaneClarification(pilot)) {
|
||||
const need = clarificationNeedRu(pilot);
|
||||
return `Могу идти дальше по документам${laneScopeSuffix(pilot)}, но для запуска поиска в 1С ${need.verb} ${need.subject}.`;
|
||||
}
|
||||
if (mode === "needs_clarification") {
|
||||
return "Нужно уточнить контекст перед поиском в 1С.";
|
||||
}
|
||||
|
|
@ -123,9 +350,47 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
|||
}
|
||||
|
||||
function nextStepFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||||
if (isEntityResolutionPilot(pilot) && mode === "needs_clarification") {
|
||||
const ambiguityCandidates = pilot.derived_entity_resolution?.ambiguity_candidates ?? [];
|
||||
if (ambiguityCandidates.length > 0) {
|
||||
return `Уточните, какой именно контрагент нужен: ${formatNamedChoiceList(
|
||||
ambiguityCandidates
|
||||
)}. Можно ответить названием или номером варианта.`;
|
||||
}
|
||||
return "Уточните точное название контрагента или добавьте ИНН, и я продолжу уже по нужной сущности в 1С.";
|
||||
}
|
||||
if (isEntityResolutionPilot(pilot) && mode === "confirmed_with_bounded_inference") {
|
||||
return "Теперь могу продолжить уже по найденному контрагенту и искать документы, движения или денежный поток.";
|
||||
}
|
||||
if (
|
||||
isEntityResolutionPilot(pilot) &&
|
||||
mode === "checked_sources_only" &&
|
||||
pilot.derived_entity_resolution?.resolution_status === "not_found"
|
||||
) {
|
||||
return "Дайте точное название или ИНН, и я повторю поиск по каталогу 1С более прицельно.";
|
||||
}
|
||||
if (mode === "needs_clarification" && isMetadataLaneChoiceClarification(pilot)) {
|
||||
return "Уточните, в какой контур идти дальше: по документам или по движениям/регистрам.";
|
||||
}
|
||||
if (mode === "needs_clarification" && isMovementLaneClarification(pilot)) {
|
||||
return clarificationNextStepLine(pilot, "движениям/регистрам");
|
||||
}
|
||||
if (mode === "needs_clarification" && isDocumentLaneClarification(pilot)) {
|
||||
return clarificationNextStepLine(pilot, "документам");
|
||||
}
|
||||
if (mode === "needs_clarification") {
|
||||
return "Уточните контрагента, период или организацию, и я смогу выполнить проверку по 1С.";
|
||||
}
|
||||
if (mode === "confirmed_with_bounded_inference" && pilot.derived_metadata_surface) {
|
||||
const surface = pilot.derived_metadata_surface;
|
||||
if (surface.ambiguity_detected && surface.ambiguity_entity_sets.length > 0) {
|
||||
return `Следующим шагом лучше сузить surface до одного семейства: ${surface.ambiguity_entity_sets.join(", ")}.`;
|
||||
}
|
||||
const routeLabel = metadataRouteFamilyLabelRu(surface.downstream_route_family);
|
||||
if (surface.selected_entity_set && routeLabel) {
|
||||
return `Следующим шагом могу пойти в ${routeLabel} по surface «${surface.selected_entity_set}» и уже искать подтвержденные данные, а не только схему.`;
|
||||
}
|
||||
}
|
||||
if (mode === "checked_sources_only" && pilot.query_limitations.length > 0) {
|
||||
return "Можно повторить проверку после восстановления MCP-доступа или сузить вопрос до конкретного контрагента/периода.";
|
||||
}
|
||||
|
|
@ -148,12 +413,65 @@ 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 present a derived sum as a legal/accounting final total outside the checked 1C rows.");
|
||||
}
|
||||
if (isDocumentPilot(pilot)) {
|
||||
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.");
|
||||
}
|
||||
if (isMovementPilot(pilot)) {
|
||||
claims.push("Do not claim full movement history outside the checked period.");
|
||||
claims.push("Do not present the confirmed movement rows as a complete movement universe.");
|
||||
}
|
||||
if (isMetadataPilot(pilot)) {
|
||||
claims.push("Do not present metadata surface as confirmed business data rows.");
|
||||
claims.push("Do not claim a document/register exists outside the checked metadata probe results.");
|
||||
claims.push("Do not present the inferred next checked lane as already executed data retrieval.");
|
||||
}
|
||||
if (isEntityResolutionPilot(pilot)) {
|
||||
claims.push("Do not present catalog grounding as confirmed business activity, turnover, or document evidence.");
|
||||
claims.push("Do not claim legal identity uniqueness when several catalog candidates are still plausible.");
|
||||
claims.push("Do not imply that the resolved entity has already been used in a downstream data probe.");
|
||||
}
|
||||
if (pilot.evidence.confirmed_facts.length === 0) {
|
||||
claims.push("Do not claim a confirmed business fact when confirmed_facts is empty.");
|
||||
}
|
||||
return claims;
|
||||
}
|
||||
|
||||
const RU_MONTH_LABELS_SHORT = [
|
||||
"янв",
|
||||
"фев",
|
||||
"мар",
|
||||
"апр",
|
||||
"май",
|
||||
"июн",
|
||||
"июл",
|
||||
"авг",
|
||||
"сен",
|
||||
"окт",
|
||||
"ноя",
|
||||
"дек"
|
||||
] as const;
|
||||
|
||||
function monthLabelRu(monthBucket: string): string {
|
||||
const match = monthBucket.match(/^(\d{4})-(\d{2})$/);
|
||||
if (!match) {
|
||||
return monthBucket;
|
||||
}
|
||||
const monthIndex = Number(match[2]) - 1;
|
||||
const label = RU_MONTH_LABELS_SHORT[monthIndex] ?? match[2];
|
||||
return `${label} ${match[1]}`;
|
||||
}
|
||||
|
||||
function netLabelRu(netDirection: "net_incoming" | "net_outgoing" | "balanced"): string {
|
||||
if (netDirection === "net_incoming") {
|
||||
return "нетто в нашу сторону";
|
||||
}
|
||||
if (netDirection === "net_outgoing") {
|
||||
return "нетто исходящее";
|
||||
}
|
||||
return "нетто нулевое";
|
||||
}
|
||||
|
||||
function derivedActivityInferenceLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||||
const period = pilot.derived_activity_period;
|
||||
if (!period) {
|
||||
|
|
@ -166,6 +484,78 @@ function derivedActivityInferenceLine(pilot: AssistantMcpDiscoveryPilotExecution
|
|||
].join(" ");
|
||||
}
|
||||
|
||||
function derivedMetadataConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||||
const surface = pilot.derived_metadata_surface;
|
||||
if (!surface) {
|
||||
return null;
|
||||
}
|
||||
const scope = surface.metadata_scope ? ` по области "${surface.metadata_scope}"` : "";
|
||||
const entitySets =
|
||||
surface.available_entity_sets.length > 0
|
||||
? ` Типы объектов: ${surface.available_entity_sets.join(", ")}.`
|
||||
: "";
|
||||
const objects =
|
||||
surface.matched_objects.length > 0
|
||||
? ` Найденные объекты: ${surface.matched_objects.slice(0, 8).join(", ")}.`
|
||||
: "";
|
||||
const selectedEntitySet = surface.selected_entity_set ? ` Выбранное family: ${surface.selected_entity_set}.` : "";
|
||||
const selectedObjects =
|
||||
surface.selected_surface_objects.length > 0
|
||||
? ` Выбранные surface-объекты: ${surface.selected_surface_objects.slice(0, 6).join(", ")}.`
|
||||
: "";
|
||||
const fields =
|
||||
surface.available_fields.length > 0
|
||||
? ` Доступные поля/секции: ${surface.available_fields.slice(0, 12).join(", ")}.`
|
||||
: "";
|
||||
return `Подтвержденная metadata-поверхность 1С${scope}: ${surface.matched_rows} строк metadata-ответа.${entitySets}${objects}${selectedEntitySet}${selectedObjects}${fields}`.replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function derivedMetadataInferenceLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||||
const surface = pilot.derived_metadata_surface;
|
||||
if (!surface) {
|
||||
return null;
|
||||
}
|
||||
if (surface.ambiguity_detected && surface.ambiguity_entity_sets.length > 0) {
|
||||
return `По подтвержденной metadata-поверхности видно несколько конкурирующих family: ${surface.ambiguity_entity_sets.join(", ")}. Следующий data-lane пока нельзя выбрать без явного сужения.`;
|
||||
}
|
||||
const routeLabel = metadataRouteFamilyLabelRu(surface.downstream_route_family);
|
||||
if (!surface.selected_entity_set || !routeLabel) {
|
||||
return null;
|
||||
}
|
||||
return `По подтвержденной metadata-поверхности следующий проверяемый шаг можно ограниченно оценить как ${routeLabel} через family «${surface.selected_entity_set}». Это еще не выполненный data-fetch, а только grounded выбор следующего контура.`;
|
||||
}
|
||||
|
||||
function derivedEntityResolutionConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||||
const resolution = pilot.derived_entity_resolution;
|
||||
if (!resolution || resolution.resolution_status !== "resolved" || !resolution.resolved_entity) {
|
||||
return null;
|
||||
}
|
||||
const requested = resolution.requested_entity ? ` по запросу "${resolution.requested_entity}"` : "";
|
||||
const confidence =
|
||||
resolution.confidence === "high"
|
||||
? " Точность совпадения выглядит высокой."
|
||||
: resolution.confidence === "medium"
|
||||
? " Совпадение выглядит достаточно сильным, но это все еще catalog grounding."
|
||||
: " Совпадение выглядит вероятным, но его лучше считать рабочим заземлением сущности.";
|
||||
return `В текущем каталожном срезе 1С${requested} найден контрагент "${resolution.resolved_entity}".${confidence}`;
|
||||
}
|
||||
|
||||
function derivedEntityResolutionInferenceLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||||
const resolution = pilot.derived_entity_resolution;
|
||||
if (!resolution) {
|
||||
return null;
|
||||
}
|
||||
if (resolution.resolution_status === "resolved") {
|
||||
return "Сейчас подтверждено только заземление сущности по каталогу 1С; документы, движения и денежные показатели по ней еще не проверялись.";
|
||||
}
|
||||
if (resolution.resolution_status === "ambiguous" && resolution.ambiguity_candidates.length > 0) {
|
||||
return `В каталоге 1С нашлось несколько близких кандидатов: ${formatNamedChoiceList(
|
||||
resolution.ambiguity_candidates
|
||||
)}. Без уточнения нельзя честно выбрать одного контрагента для следующего шага.`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function derivedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||||
const flow = pilot.derived_value_flow;
|
||||
if (!flow) {
|
||||
|
|
@ -174,15 +564,17 @@ function derivedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutio
|
|||
const counterparty = flow.counterparty ? ` по контрагенту ${flow.counterparty}` : "";
|
||||
const period = flow.period_scope ? ` за период ${flow.period_scope}` : " в проверенном окне";
|
||||
const movementLabel =
|
||||
flow.value_flow_direction === "outgoing_supplier_payout" ? "исходящих платежей/списаний" : "денежных движений";
|
||||
flow.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "исходящих платежей/списаний"
|
||||
: "входящих денежных поступлений";
|
||||
const totalLabel =
|
||||
flow.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "сумма исходящих платежей/списаний составляет"
|
||||
: "сумма составляет";
|
||||
: "сумма входящих денежных поступлений составляет";
|
||||
const caveat =
|
||||
flow.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "Это расчет по найденным строкам 1С, а не подтверждение полного объема платежей вне проверенного окна."
|
||||
: "Это расчет по найденным строкам 1С, а не подтверждение полного оборота вне проверенного окна.";
|
||||
: "Это расчет по найденным строкам 1С, а не подтверждение полного объема поступлений вне проверенного окна.";
|
||||
const dates =
|
||||
flow.first_movement_date && flow.latest_movement_date
|
||||
? ` Первая найденная дата движения: ${flow.first_movement_date}; последняя: ${flow.latest_movement_date}.`
|
||||
|
|
@ -193,6 +585,20 @@ function derivedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutio
|
|||
return `По найденным строкам ${movementLabel} в 1С${counterparty}${period} ${totalLabel} ${flow.total_amount_human_ru} Учтено строк с суммой: ${flow.rows_with_amount} из ${flow.rows_matched}.${dates}${limitCaveat} ${caveat}`;
|
||||
}
|
||||
|
||||
function derivedValueFlowMonthlyLines(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] {
|
||||
const flow = pilot.derived_value_flow;
|
||||
if (!flow || flow.aggregation_axis !== "month" || flow.monthly_breakdown.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return flow.monthly_breakdown.map((bucket) => {
|
||||
const monthLabel = monthLabelRu(bucket.month_bucket);
|
||||
if (flow.value_flow_direction === "outgoing_supplier_payout") {
|
||||
return `Помесячно: ${monthLabel} — заплатили ${bucket.total_amount_human_ru} по ${bucket.rows_with_amount} строкам с суммой`;
|
||||
}
|
||||
return `Помесячно: ${monthLabel} — получили ${bucket.total_amount_human_ru} по ${bucket.rows_with_amount} строкам с суммой`;
|
||||
});
|
||||
}
|
||||
|
||||
function sideDateRange(first: string | null, latest: string | null): string {
|
||||
if (first && latest) {
|
||||
return ` первая дата ${first}, последняя ${latest}`;
|
||||
|
|
@ -230,6 +636,17 @@ function derivedBidirectionalValueFlowConfirmedLine(pilot: AssistantMcpDiscovery
|
|||
.trim();
|
||||
}
|
||||
|
||||
function derivedBidirectionalValueFlowMonthlyLines(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] {
|
||||
const flow = pilot.derived_bidirectional_value_flow;
|
||||
if (!flow || flow.aggregation_axis !== "month" || flow.monthly_breakdown.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return flow.monthly_breakdown.map(
|
||||
(bucket) =>
|
||||
`Помесячно: ${monthLabelRu(bucket.month_bucket)} — получили ${bucket.incoming_total_amount_human_ru}, заплатили ${bucket.outgoing_total_amount_human_ru}, ${netLabelRu(bucket.net_direction)} ${bucket.net_amount_human_ru}`
|
||||
);
|
||||
}
|
||||
|
||||
export function buildAssistantMcpDiscoveryAnswerDraft(
|
||||
pilot: AssistantMcpDiscoveryPilotExecutionContract
|
||||
): AssistantMcpDiscoveryAnswerDraftContract {
|
||||
|
|
@ -242,14 +659,30 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
|
|||
if (pilot.evidence.inferred_facts.length > 0) {
|
||||
pushReason(reasonCodes, "answer_contains_bounded_inference");
|
||||
}
|
||||
const derivedInferenceLine = derivedActivityInferenceLine(pilot);
|
||||
const derivedInferenceLine =
|
||||
derivedActivityInferenceLine(pilot) ??
|
||||
derivedMetadataInferenceLine(pilot) ??
|
||||
derivedEntityResolutionInferenceLine(pilot);
|
||||
const inferenceLines = derivedInferenceLine
|
||||
? [derivedInferenceLine]
|
||||
: pilot.evidence.inferred_facts;
|
||||
const derivedMetadataLine = derivedMetadataConfirmedLine(pilot);
|
||||
const derivedEntityResolutionLine = derivedEntityResolutionConfirmedLine(pilot);
|
||||
const derivedValueLine = derivedBidirectionalValueFlowConfirmedLine(pilot) ?? derivedValueFlowConfirmedLine(pilot);
|
||||
const monthlyConfirmedLines =
|
||||
derivedBidirectionalValueFlowMonthlyLines(pilot).length > 0
|
||||
? derivedBidirectionalValueFlowMonthlyLines(pilot)
|
||||
: derivedValueFlowMonthlyLines(pilot);
|
||||
if (monthlyConfirmedLines.length > 0) {
|
||||
pushReason(reasonCodes, "answer_contains_monthly_breakdown");
|
||||
}
|
||||
const confirmedLines = derivedValueLine
|
||||
? [...pilot.evidence.confirmed_facts, derivedValueLine]
|
||||
: pilot.evidence.confirmed_facts;
|
||||
? [...pilot.evidence.confirmed_facts, derivedValueLine, ...monthlyConfirmedLines]
|
||||
: derivedEntityResolutionLine
|
||||
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
|
||||
: derivedMetadataLine
|
||||
? [...pilot.evidence.confirmed_facts, derivedMetadataLine]
|
||||
: pilot.evidence.confirmed_facts;
|
||||
|
||||
return {
|
||||
schema_version: ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -13,6 +13,15 @@ export const ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION = "assistant_mcp_dis
|
|||
|
||||
export type AssistantMcpDiscoveryPlannerStatus = "ready_for_execution" | "needs_clarification" | "blocked";
|
||||
|
||||
export type AssistantMcpDiscoveryChainId =
|
||||
| "metadata_inspection"
|
||||
| "metadata_lane_clarification"
|
||||
| "value_flow"
|
||||
| "lifecycle"
|
||||
| "movement_evidence"
|
||||
| "document_evidence"
|
||||
| "entity_resolution";
|
||||
|
||||
export interface AssistantMcpDiscoveryPlannerInput {
|
||||
semanticDataNeed?: string | null;
|
||||
turnMeaning?: AssistantMcpDiscoveryTurnMeaningRef | null;
|
||||
|
|
@ -23,6 +32,8 @@ export interface AssistantMcpDiscoveryPlannerContract {
|
|||
policy_owner: "assistantMcpDiscoveryPlanner";
|
||||
planner_status: AssistantMcpDiscoveryPlannerStatus;
|
||||
semantic_data_need: string | null;
|
||||
selected_chain_id: AssistantMcpDiscoveryChainId;
|
||||
selected_chain_summary: string;
|
||||
proposed_primitives: AssistantMcpDiscoveryPrimitive[];
|
||||
required_axes: string[];
|
||||
discovery_plan: AssistantMcpDiscoveryPlanContract;
|
||||
|
|
@ -32,11 +43,17 @@ export interface AssistantMcpDiscoveryPlannerContract {
|
|||
|
||||
interface PlannerRecipe {
|
||||
semanticDataNeed: string;
|
||||
chainId: AssistantMcpDiscoveryChainId;
|
||||
chainSummary: string;
|
||||
primitives: AssistantMcpDiscoveryPrimitive[];
|
||||
axes: string[];
|
||||
reason: string;
|
||||
}
|
||||
|
||||
interface PlannerBudgetOverride {
|
||||
maxProbeCount?: number;
|
||||
}
|
||||
|
||||
function toNonEmptyString(value: unknown): string | null {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
|
|
@ -76,6 +93,10 @@ function hasEntity(meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefin
|
|||
return (meaning?.explicit_entity_candidates?.length ?? 0) > 0;
|
||||
}
|
||||
|
||||
function aggregationAxis(meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefined): string | null {
|
||||
return toNonEmptyString(meaning?.asked_aggregation_axis)?.toLowerCase() ?? null;
|
||||
}
|
||||
|
||||
function addScopeAxes(axes: string[], meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefined): void {
|
||||
if (hasEntity(meaning)) {
|
||||
pushUnique(axes, "counterparty");
|
||||
|
|
@ -92,6 +113,27 @@ function includesAny(text: string, tokens: string[]): boolean {
|
|||
return tokens.some((token) => text.includes(token));
|
||||
}
|
||||
|
||||
function isYearDateScope(meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefined): boolean {
|
||||
return /^\d{4}$/.test(toNonEmptyString(meaning?.explicit_date_scope) ?? "");
|
||||
}
|
||||
|
||||
function budgetOverrideFor(input: AssistantMcpDiscoveryPlannerInput, recipe: PlannerRecipe): PlannerBudgetOverride {
|
||||
const meaning = input.turnMeaning ?? null;
|
||||
const requestedAggregationAxis = aggregationAxis(meaning);
|
||||
const isValueFlowRecipe =
|
||||
recipe.semanticDataNeed === "counterparty value-flow evidence" &&
|
||||
recipe.primitives.includes("query_movements");
|
||||
if (!isValueFlowRecipe) {
|
||||
return {};
|
||||
}
|
||||
if (requestedAggregationAxis === "month" || isYearDateScope(meaning)) {
|
||||
return {
|
||||
maxProbeCount: 30
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
|
||||
const meaning = input.turnMeaning ?? null;
|
||||
const domain = lower(meaning?.asked_domain_family);
|
||||
|
|
@ -99,27 +141,37 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
|
|||
const unsupported = lower(meaning?.unsupported_but_understood_family);
|
||||
const combined = `${domain} ${action} ${unsupported}`.trim();
|
||||
const axes: string[] = [];
|
||||
const requestedAggregationAxis = aggregationAxis(meaning);
|
||||
addScopeAxes(axes, meaning);
|
||||
|
||||
if (includesAny(combined, ["metadata_lane_choice_clarification", "resolve_next_lane"])) {
|
||||
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_recipe"
|
||||
};
|
||||
}
|
||||
|
||||
if (includesAny(combined, ["turnover", "revenue", "payment", "payout", "value", "net", "netting", "balance", "cashflow"])) {
|
||||
pushUnique(axes, "aggregate_axis");
|
||||
pushUnique(axes, "amount");
|
||||
pushUnique(axes, "coverage_target");
|
||||
if (requestedAggregationAxis === "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: "planner_selected_value_flow_recipe"
|
||||
};
|
||||
}
|
||||
|
||||
if (includesAny(combined, ["document", "documents"])) {
|
||||
pushUnique(axes, "coverage_target");
|
||||
return {
|
||||
semanticDataNeed: "document evidence",
|
||||
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage"],
|
||||
axes,
|
||||
reason: "planner_selected_document_recipe"
|
||||
reason: requestedAggregationAxis === "month"
|
||||
? "planner_selected_monthly_value_flow_recipe"
|
||||
: "planner_selected_value_flow_recipe"
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -129,6 +181,8 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
|
|||
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_recipe"
|
||||
|
|
@ -139,16 +193,45 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
|
|||
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_recipe"
|
||||
};
|
||||
}
|
||||
|
||||
if (includesAny(combined, ["movement", "movements", "bank_operations", "movement_evidence", "list_movements"])) {
|
||||
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_recipe"
|
||||
};
|
||||
}
|
||||
|
||||
if (includesAny(combined, ["document", "documents"])) {
|
||||
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_recipe"
|
||||
};
|
||||
}
|
||||
|
||||
if (hasEntity(meaning)) {
|
||||
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: "planner_selected_entity_resolution_recipe"
|
||||
|
|
@ -157,6 +240,8 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
|
|||
|
||||
return {
|
||||
semanticDataNeed: "unclassified 1C discovery need",
|
||||
chainId: "metadata_inspection",
|
||||
chainSummary: "Start with metadata inspection instead of guessing a deeper fact route when the business need is still under-specified.",
|
||||
primitives: ["inspect_1c_metadata"],
|
||||
axes,
|
||||
reason: "planner_selected_clarification_recipe"
|
||||
|
|
@ -180,15 +265,20 @@ export function planAssistantMcpDiscovery(
|
|||
input: AssistantMcpDiscoveryPlannerInput
|
||||
): AssistantMcpDiscoveryPlannerContract {
|
||||
const recipe = recipeFor(input);
|
||||
const budgetOverride = budgetOverrideFor(input, recipe);
|
||||
const semanticDataNeed = toNonEmptyString(input.semanticDataNeed) ?? recipe.semanticDataNeed;
|
||||
const reasonCodes: string[] = [];
|
||||
pushReason(reasonCodes, recipe.reason);
|
||||
if (budgetOverride.maxProbeCount) {
|
||||
pushReason(reasonCodes, "planner_enabled_chunked_coverage_probe_budget");
|
||||
}
|
||||
|
||||
const plan = buildAssistantMcpDiscoveryPlan({
|
||||
semanticDataNeed,
|
||||
turnMeaning: input.turnMeaning,
|
||||
proposedPrimitives: recipe.primitives,
|
||||
requiredAxes: recipe.axes
|
||||
requiredAxes: recipe.axes,
|
||||
maxProbeCount: budgetOverride.maxProbeCount
|
||||
});
|
||||
const review = reviewAssistantMcpDiscoveryPlanAgainstCatalog(plan);
|
||||
const plannerStatus = statusFrom(plan, review);
|
||||
|
|
@ -206,6 +296,8 @@ export function planAssistantMcpDiscovery(
|
|||
policy_owner: "assistantMcpDiscoveryPlanner",
|
||||
planner_status: plannerStatus,
|
||||
semantic_data_need: semanticDataNeed,
|
||||
selected_chain_id: recipe.chainId,
|
||||
selected_chain_summary: recipe.chainSummary,
|
||||
proposed_primitives: recipe.primitives,
|
||||
required_axes: recipe.axes,
|
||||
discovery_plan: plan,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ export type AssistantMcpDiscoveryAnswerPermission = "confirmed_answer" | "bounde
|
|||
export interface AssistantMcpDiscoveryTurnMeaningRef {
|
||||
asked_domain_family?: string | null;
|
||||
asked_action_family?: string | null;
|
||||
asked_aggregation_axis?: string | null;
|
||||
explicit_entity_candidates?: string[];
|
||||
metadata_ambiguity_entity_sets?: string[];
|
||||
explicit_organization_scope?: string | null;
|
||||
explicit_date_scope?: string | null;
|
||||
meaning_confidence?: number | null;
|
||||
|
|
@ -101,7 +103,7 @@ const DEFAULT_DISCOVERY_BUDGET: AssistantMcpDiscoveryExecutionBudget = {
|
|||
max_rows_per_probe: 100
|
||||
};
|
||||
|
||||
const MAX_PROBE_COUNT = 6;
|
||||
const MAX_PROBE_COUNT = 36;
|
||||
const MAX_ROWS_PER_PROBE = 500;
|
||||
|
||||
const ALLOWED_PRIMITIVE_SET = new Set<string>(ASSISTANT_MCP_DISCOVERY_PRIMITIVES);
|
||||
|
|
@ -164,19 +166,27 @@ function normalizeTurnMeaning(
|
|||
const result: AssistantMcpDiscoveryTurnMeaningRef = {};
|
||||
const domain = toNonEmptyString(value.asked_domain_family);
|
||||
const action = toNonEmptyString(value.asked_action_family);
|
||||
const aggregationAxis = toNonEmptyString(value.asked_aggregation_axis);
|
||||
const organization = toNonEmptyString(value.explicit_organization_scope);
|
||||
const dateScope = toNonEmptyString(value.explicit_date_scope);
|
||||
const unsupported = toNonEmptyString(value.unsupported_but_understood_family);
|
||||
const entities = toStringList(value.explicit_entity_candidates);
|
||||
const metadataAmbiguityEntitySets = toStringList(value.metadata_ambiguity_entity_sets);
|
||||
if (domain) {
|
||||
result.asked_domain_family = domain;
|
||||
}
|
||||
if (action) {
|
||||
result.asked_action_family = action;
|
||||
}
|
||||
if (aggregationAxis) {
|
||||
result.asked_aggregation_axis = aggregationAxis;
|
||||
}
|
||||
if (entities.length > 0) {
|
||||
result.explicit_entity_candidates = entities;
|
||||
}
|
||||
if (metadataAmbiguityEntitySets.length > 0) {
|
||||
result.metadata_ambiguity_entity_sets = metadataAmbiguityEntitySets;
|
||||
}
|
||||
if (organization) {
|
||||
result.explicit_organization_scope = organization;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,10 +100,24 @@ function localizeLine(value: string): string {
|
|||
}
|
||||
const valueFlowMatch = value.match(/^1C value-flow rows were found for counterparty\s+(.+)$/i);
|
||||
if (valueFlowMatch) {
|
||||
return `В 1С найдены строки денежных движений по контрагенту ${valueFlowMatch[1]}.`;
|
||||
return `В 1С найдены строки входящих денежных поступлений по контрагенту ${valueFlowMatch[1]}.`;
|
||||
}
|
||||
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
|
||||
return "В 1С найдены строки денежных движений по запрошенному контрагентскому контуру.";
|
||||
return "В 1С найдены строки входящих денежных поступлений по запрошенному контрагентскому контуру.";
|
||||
}
|
||||
const documentRowsMatch = value.match(/^1C document rows were found for counterparty\s+(.+)$/i);
|
||||
if (documentRowsMatch) {
|
||||
return `В 1С найдены строки документов по контрагенту ${documentRowsMatch[1]}.`;
|
||||
}
|
||||
if (/^1C document rows were found for the requested scope$/i.test(value)) {
|
||||
return "В 1С найдены строки документов по запрошенному контуру.";
|
||||
}
|
||||
const movementRowsMatch = value.match(/^1C movement rows were found for counterparty\s+(.+)$/i);
|
||||
if (movementRowsMatch) {
|
||||
return `В 1С найдены строки движений по контрагенту ${movementRowsMatch[1]}.`;
|
||||
}
|
||||
if (/^1C movement rows were found for the requested scope$/i.test(value)) {
|
||||
return "В 1С найдены строки движений по запрошенному контуру.";
|
||||
}
|
||||
const supplierPayoutMatch = value.match(/^1C supplier-payout rows were found for counterparty\s+(.+)$/i);
|
||||
if (supplierPayoutMatch) {
|
||||
|
|
@ -131,8 +145,17 @@ function localizeLine(value: string): string {
|
|||
if (/^Business activity duration may be inferred from first and latest confirmed 1C activity rows$/i.test(value)) {
|
||||
return "Длительность деловой активности можно оценивать только как вывод по первой и последней подтвержденной строке активности в 1С.";
|
||||
}
|
||||
if (/^Counterparty document evidence is limited to confirmed 1C document rows in the checked scope$/i.test(value)) {
|
||||
return "Срез документов ограничен только подтвержденными строками документов в проверенном окне.";
|
||||
}
|
||||
if (/^Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope$/i.test(value)) {
|
||||
return "Срез движений ограничен только подтвержденными строками движений в проверенном окне.";
|
||||
}
|
||||
if (/^Counterparty value-flow total was calculated from confirmed 1C movement rows$/i.test(value)) {
|
||||
return "Сумма рассчитана только по подтвержденным строкам денежных движений в 1С.";
|
||||
return "Сумма входящих поступлений рассчитана только по подтвержденным строкам поступлений в 1С.";
|
||||
}
|
||||
if (/^Counterparty monthly value-flow breakdown was grouped by month over confirmed 1C movement rows$/i.test(value)) {
|
||||
return "Помесячная раскладка входящих поступлений построена только по подтвержденным строкам поступлений в 1С.";
|
||||
}
|
||||
if (/^Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows$/i.test(value)) {
|
||||
return "Сумма исходящих платежей рассчитана только по подтвержденным строкам списаний в 1С.";
|
||||
|
|
@ -140,6 +163,58 @@ function localizeLine(value: string): string {
|
|||
if (/^Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows$/i.test(value)) {
|
||||
return "Нетто денежного потока рассчитано только как входящие подтвержденные строки 1С минус исходящие подтвержденные строки 1С.";
|
||||
}
|
||||
if (/^Counterparty monthly net value-flow breakdown was grouped by month over confirmed incoming and outgoing 1C rows$/i.test(value)) {
|
||||
return "Помесячная нетто-раскладка сгруппирована только по подтвержденным входящим и исходящим строкам 1С.";
|
||||
}
|
||||
const metadataSurfaceMatch = value.match(
|
||||
/^Confirmed 1C metadata surface(?: for scope "([^"]+)")?: (\d+) rows and (\d+) matching objects$/i
|
||||
);
|
||||
if (metadataSurfaceMatch) {
|
||||
const scopePart = metadataSurfaceMatch[1] ? ` по области "${metadataSurfaceMatch[1]}"` : "";
|
||||
return `В 1С подтверждена metadata-поверхность${scopePart}: ${metadataSurfaceMatch[2]} строк metadata-ответа и ${metadataSurfaceMatch[3]} совпавших объекта(ов).`;
|
||||
}
|
||||
const metadataObjectSetsMatch = value.match(/^Available metadata object sets: (.+)$/i);
|
||||
if (metadataObjectSetsMatch) {
|
||||
return `Доступные типы metadata-объектов: ${metadataObjectSetsMatch[1]}.`;
|
||||
}
|
||||
const selectedMetadataEntitySetMatch = value.match(/^Selected metadata entity set: (.+)$/i);
|
||||
if (selectedMetadataEntitySetMatch) {
|
||||
return `Выбранное семейство metadata-объектов: ${selectedMetadataEntitySetMatch[1]}.`;
|
||||
}
|
||||
const selectedMetadataObjectsMatch = value.match(/^Selected metadata objects: (.+)$/i);
|
||||
if (selectedMetadataObjectsMatch) {
|
||||
return `Выбранные metadata-объекты для следующего шага: ${selectedMetadataObjectsMatch[1]}.`;
|
||||
}
|
||||
const metadataFieldsMatch = value.match(/^Available metadata fields\/sections: (.+)$/i);
|
||||
if (metadataFieldsMatch) {
|
||||
return `Доступные metadata-поля/секции: ${metadataFieldsMatch[1]}.`;
|
||||
}
|
||||
const metadataLaneInferenceMatch = value.match(
|
||||
/^A likely next checked lane may be inferred as (document_evidence|movement_evidence|catalog_drilldown) from the confirmed metadata surface$/i
|
||||
);
|
||||
if (metadataLaneInferenceMatch) {
|
||||
const routeLabel =
|
||||
metadataLaneInferenceMatch[1] === "document_evidence"
|
||||
? "контур документов"
|
||||
: metadataLaneInferenceMatch[1] === "movement_evidence"
|
||||
? "контур движений/регистров"
|
||||
: "контур справочников и связанных объектов";
|
||||
return `Следующий проверяемый контур по этой metadata-поверхности можно ограниченно оценить как ${routeLabel}.`;
|
||||
}
|
||||
if (/^Detailed metadata fields were not returned by this MCP metadata probe$/i.test(value)) {
|
||||
return "Эта MCP-проверка metadata не вернула детальный список полей.";
|
||||
}
|
||||
const metadataAmbiguityMatch = value.match(/^Exact downstream metadata surface remains ambiguous across: (.+)$/i);
|
||||
if (metadataAmbiguityMatch) {
|
||||
return `Точная downstream metadata-поверхность пока неоднозначна между family: ${metadataAmbiguityMatch[1]}.`;
|
||||
}
|
||||
const noMatchingMetadataScopeMatch = value.match(/^No matching 1C metadata objects were confirmed for scope "([^"]+)"$/i);
|
||||
if (noMatchingMetadataScopeMatch) {
|
||||
return `В 1С не подтверждены metadata-объекты по области "${noMatchingMetadataScopeMatch[1]}".`;
|
||||
}
|
||||
if (/^No matching 1C metadata objects were confirmed by this MCP metadata probe$/i.test(value)) {
|
||||
return "В 1С эта MCP-проверка не подтвердила подходящих metadata-объектов.";
|
||||
}
|
||||
if (/^Legal registration date is not proven by this MCP discovery pilot$/i.test(value)) {
|
||||
return "Юридическая дата регистрации этим поиском не подтверждена.";
|
||||
}
|
||||
|
|
@ -150,10 +225,22 @@ function localizeLine(value: string): string {
|
|||
return "Полное покрытие запрошенного периода по двустороннему денежному потоку не подтверждено: хотя бы одна сторона проверки достигла лимита найденных строк.";
|
||||
}
|
||||
if (/^Full turnover outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
|
||||
return "Полный оборот вне проверенного периода этим поиском не подтвержден.";
|
||||
return "Полный объем входящих поступлений вне проверенного периода этим поиском не подтвержден.";
|
||||
}
|
||||
if (/^Full all-time turnover is not proven without an explicit checked period$/i.test(value)) {
|
||||
return "Полный оборот за все время без явно проверенного периода не подтвержден.";
|
||||
return "Полный объем входящих поступлений за все время без явно проверенного периода не подтвержден.";
|
||||
}
|
||||
if (/^Full document history outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
|
||||
return "Полный исторический срез документов вне проверенного периода этим поиском не подтвержден.";
|
||||
}
|
||||
if (/^Full document history is not proven without an explicit checked period$/i.test(value)) {
|
||||
return "Полный срез документов без явно проверенного периода не подтвержден.";
|
||||
}
|
||||
if (/^Full movement history outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
|
||||
return "Полный исторический срез движений вне проверенного периода этим поиском не подтвержден.";
|
||||
}
|
||||
if (/^Full movement history is not proven without an explicit checked period$/i.test(value)) {
|
||||
return "Полный срез движений без явно проверенного периода не подтвержден.";
|
||||
}
|
||||
if (/^Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
|
||||
return "Полный объем исходящих платежей вне проверенного периода этим поиском не подтвержден.";
|
||||
|
|
@ -167,6 +254,20 @@ function localizeLine(value: string): string {
|
|||
if (/^Full all-time bidirectional value-flow is not proven without an explicit checked period$/i.test(value)) {
|
||||
return "Полный двусторонний денежный поток за все время без явно проверенного периода не подтвержден.";
|
||||
}
|
||||
if (
|
||||
/^Requested period coverage was recovered through monthly 1C value-flow probes after the broad probe hit the row limit$/i.test(
|
||||
value
|
||||
)
|
||||
) {
|
||||
return "Покрытие запрошенного периода восстановлено помесячными проверками 1С после того, как общая выборка уперлась в лимит строк.";
|
||||
}
|
||||
if (
|
||||
/^Requested period coverage for bidirectional value-flow was recovered through monthly 1C side probes after a broad probe hit the row limit$/i.test(
|
||||
value
|
||||
)
|
||||
) {
|
||||
return "Покрытие запрошенного периода по двустороннему денежному потоку восстановлено помесячными проверками 1С после того, как общая выборка уперлась в лимит строк хотя бы по одной стороне.";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export type AssistantMcpDiscoveryResponsePolicyDecision = "apply_candidate" | "k
|
|||
export interface ApplyAssistantMcpDiscoveryResponsePolicyInput {
|
||||
currentReply: string;
|
||||
currentReplySource?: string | null;
|
||||
currentReplyType?: string | null;
|
||||
livingChatSource?: string | null;
|
||||
modeDecisionReason?: string | null;
|
||||
addressRuntimeMeta?: Record<string, unknown> | null;
|
||||
|
|
@ -110,6 +111,13 @@ function isUnsupportedCurrentTurnBoundary(input: ApplyAssistantMcpDiscoveryRespo
|
|||
);
|
||||
}
|
||||
|
||||
function isDeterministicBroadBusinessEvaluationReply(input: ApplyAssistantMcpDiscoveryResponsePolicyInput): boolean {
|
||||
return (
|
||||
input.livingChatSource === "deterministic_broad_business_evaluation_contract" ||
|
||||
input.currentReplySource === "deterministic_broad_business_evaluation_contract"
|
||||
);
|
||||
}
|
||||
|
||||
function isDiscoveryReadyChatCandidate(
|
||||
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||
|
|
@ -151,6 +159,204 @@ function isDiscoveryReadyAddressCandidate(
|
|||
);
|
||||
}
|
||||
|
||||
function isDetectedIntentAlignedWithTurnMeaning(
|
||||
detectedIntent: string | null,
|
||||
turnMeaning: Record<string, unknown> | null
|
||||
): boolean {
|
||||
const normalizedIntent = String(detectedIntent ?? "").trim().toLowerCase();
|
||||
if (!normalizedIntent) {
|
||||
return false;
|
||||
}
|
||||
const askedDomain = String(toNonEmptyString(turnMeaning?.asked_domain_family) ?? "").trim().toLowerCase();
|
||||
const askedAction = String(toNonEmptyString(turnMeaning?.asked_action_family) ?? "").trim().toLowerCase();
|
||||
|
||||
if (normalizedIntent === "counterparty_activity_lifecycle") {
|
||||
return (
|
||||
askedDomain === "counterparty_lifecycle" ||
|
||||
askedAction === "activity_duration" ||
|
||||
askedAction === "age_or_activity_duration"
|
||||
);
|
||||
}
|
||||
if (normalizedIntent === "supplier_payouts_profile") {
|
||||
return askedDomain === "counterparty_value" && askedAction === "payout";
|
||||
}
|
||||
if (normalizedIntent === "customer_revenue_and_payments") {
|
||||
return askedDomain === "counterparty_value" && (askedAction === "turnover" || askedAction === "counterparty_value_or_turnover");
|
||||
}
|
||||
if (normalizedIntent === "receivables_confirmed_as_of_date") {
|
||||
return askedDomain === "receivables" || askedAction === "confirmed_snapshot";
|
||||
}
|
||||
if (normalizedIntent === "payables_confirmed_as_of_date") {
|
||||
return askedDomain === "payables" || askedAction === "confirmed_snapshot";
|
||||
}
|
||||
if (normalizedIntent === "vat_liability_confirmed_for_tax_period") {
|
||||
return askedDomain === "vat" && askedAction === "confirmed_tax_period";
|
||||
}
|
||||
if (normalizedIntent === "vat_payable_confirmed_as_of_date") {
|
||||
return askedDomain === "vat" && askedAction === "confirmed_snapshot";
|
||||
}
|
||||
if (normalizedIntent === "vat_payable_forecast") {
|
||||
return askedDomain === "vat" && askedAction === "forecast";
|
||||
}
|
||||
if (normalizedIntent === "list_documents_by_counterparty") {
|
||||
return askedAction === "list_documents" || askedDomain === "counterparty_documents" || askedDomain === "counterparty";
|
||||
}
|
||||
if (normalizedIntent === "inventory_on_hand_as_of_date" || normalizedIntent === "inventory_aging_by_purchase_date") {
|
||||
return askedDomain === "inventory" && askedAction === "confirmed_snapshot";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function readDiscoveryTurnMeaning(
|
||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||
): Record<string, unknown> | null {
|
||||
const turnInput = toRecordObject(entryPoint?.turn_input);
|
||||
return toRecordObject(turnInput?.turn_meaning_ref);
|
||||
}
|
||||
|
||||
function readTruthAnswerShape(input: ApplyAssistantMcpDiscoveryResponsePolicyInput): Record<string, unknown> | null {
|
||||
const directShape = toRecordObject(input.addressRuntimeMeta?.answer_shape_contract);
|
||||
if (directShape) {
|
||||
return directShape;
|
||||
}
|
||||
const truthAnswerPolicy = toRecordObject(input.addressRuntimeMeta?.assistant_truth_answer_policy_v1);
|
||||
return toRecordObject(truthAnswerPolicy?.answer_shape);
|
||||
}
|
||||
|
||||
function hasEffectivelyFactualAddressReply(input: ApplyAssistantMcpDiscoveryResponsePolicyInput): boolean {
|
||||
if (toNonEmptyString(input.currentReplyType) === "factual") {
|
||||
return true;
|
||||
}
|
||||
const truthAnswerShape = readTruthAnswerShape(input);
|
||||
return toNonEmptyString(truthAnswerShape?.reply_type) === "factual";
|
||||
}
|
||||
|
||||
function readStateTransitionReasonCodes(input: ApplyAssistantMcpDiscoveryResponsePolicyInput): string[] {
|
||||
const directTransition = toRecordObject(input.addressRuntimeMeta?.assistant_state_transition_v1);
|
||||
const fallbackTransition = toRecordObject(input.addressRuntimeMeta?.state_transition_contract);
|
||||
const stateTransition = directTransition ?? fallbackTransition;
|
||||
if (!stateTransition || !Array.isArray(stateTransition.reason_codes)) {
|
||||
return [];
|
||||
}
|
||||
return stateTransition.reason_codes
|
||||
.map((item) => toNonEmptyString(item))
|
||||
.filter((item): item is string => Boolean(item));
|
||||
}
|
||||
|
||||
function hasRuntimeAdjustedExactReply(
|
||||
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||
): boolean {
|
||||
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
if (!hasEffectivelyFactualAddressReply(input)) {
|
||||
return false;
|
||||
}
|
||||
const truthGateStatus = toNonEmptyString(input.addressRuntimeMeta?.truth_gate_contract_status);
|
||||
const truthAnswerPolicy = toRecordObject(input.addressRuntimeMeta?.assistant_truth_answer_policy_v1);
|
||||
const truthGate = toRecordObject(truthAnswerPolicy?.truth_gate);
|
||||
const sourceTruthGateStatus = toNonEmptyString(truthGate?.source_truth_gate_status);
|
||||
const coverageStatus = toNonEmptyString(truthGate?.coverage_status);
|
||||
const groundingStatus = toNonEmptyString(truthGate?.grounding_status);
|
||||
const hasFullConfirmedTruth =
|
||||
truthGateStatus === "full_confirmed" ||
|
||||
sourceTruthGateStatus === "full_confirmed" ||
|
||||
(coverageStatus === "full" && groundingStatus === "grounded");
|
||||
if (!hasFullConfirmedTruth) {
|
||||
return false;
|
||||
}
|
||||
const truthAnswerShape = readTruthAnswerShape(input);
|
||||
const capabilityContractId = toNonEmptyString(truthAnswerShape?.capability_contract_id);
|
||||
if (!capabilityContractId) {
|
||||
return false;
|
||||
}
|
||||
return readStateTransitionReasonCodes(input).some(
|
||||
(reason) => /^intent_adjusted_to_.+_followup_context$/i.test(reason)
|
||||
);
|
||||
}
|
||||
|
||||
function hasAlignedFactualAddressReply(
|
||||
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||
): boolean {
|
||||
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
if (!hasEffectivelyFactualAddressReply(input)) {
|
||||
return false;
|
||||
}
|
||||
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||
return isDetectedIntentAlignedWithTurnMeaning(detectedIntent, readDiscoveryTurnMeaning(entryPoint));
|
||||
}
|
||||
|
||||
function hasSemanticConflictWithDiscoveryTurnMeaning(
|
||||
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||
): boolean {
|
||||
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
if (!hasEffectivelyFactualAddressReply(input)) {
|
||||
return false;
|
||||
}
|
||||
if (hasRuntimeAdjustedExactReply(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
|
||||
const askedDomain = toNonEmptyString(turnMeaning?.asked_domain_family);
|
||||
const askedAction = toNonEmptyString(turnMeaning?.asked_action_family);
|
||||
const unsupportedFamily = toNonEmptyString(turnMeaning?.unsupported_but_understood_family);
|
||||
if (!detectedIntent || (!askedDomain && !askedAction && !unsupportedFamily)) {
|
||||
return false;
|
||||
}
|
||||
return !isDetectedIntentAlignedWithTurnMeaning(detectedIntent, turnMeaning);
|
||||
}
|
||||
|
||||
function hasMatchedFactualAddressContinuationTarget(
|
||||
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||
): boolean {
|
||||
if (!hasEffectivelyFactualAddressReply(input)) {
|
||||
return false;
|
||||
}
|
||||
if (hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||
const dialogContinuationContract =
|
||||
toRecordObject(input.addressRuntimeMeta?.dialogContinuationContract) ??
|
||||
toRecordObject(input.addressRuntimeMeta?.dialog_continuation_contract_v2);
|
||||
const targetIntent = toNonEmptyString(dialogContinuationContract?.target_intent);
|
||||
return Boolean(detectedIntent && targetIntent && detectedIntent === targetIntent);
|
||||
}
|
||||
|
||||
function hasFullConfirmedFactualAddressReply(
|
||||
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||
): boolean {
|
||||
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
if (!hasEffectivelyFactualAddressReply(input)) {
|
||||
return false;
|
||||
}
|
||||
if (hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
const truthGateStatus = toNonEmptyString(input.addressRuntimeMeta?.truth_gate_contract_status);
|
||||
if (truthGateStatus === "full_confirmed") {
|
||||
return true;
|
||||
}
|
||||
const truthAnswerPolicy = toRecordObject(input.addressRuntimeMeta?.assistant_truth_answer_policy_v1);
|
||||
const truthGate = toRecordObject(truthAnswerPolicy?.truth_gate);
|
||||
const sourceTruthGateStatus = toNonEmptyString(truthGate?.source_truth_gate_status);
|
||||
const coverageStatus = toNonEmptyString(truthGate?.coverage_status);
|
||||
const groundingStatus = toNonEmptyString(truthGate?.grounding_status);
|
||||
return sourceTruthGateStatus === "full_confirmed" || (coverageStatus === "full" && groundingStatus === "grounded");
|
||||
}
|
||||
|
||||
export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||
input: ApplyAssistantMcpDiscoveryResponsePolicyInput
|
||||
): AssistantMcpDiscoveryResponsePolicyResult {
|
||||
|
|
@ -161,9 +367,15 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
|||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(entryPoint);
|
||||
const reasonCodes = [...candidate.reason_codes];
|
||||
const unsupportedBoundary = isUnsupportedCurrentTurnBoundary(input);
|
||||
const deterministicBroadBusinessEvaluationReply = isDeterministicBroadBusinessEvaluationReply(input);
|
||||
const discoveryReadyChatCandidate = isDiscoveryReadyChatCandidate(input, entryPoint);
|
||||
const discoveryReadyDeepCandidate = isDiscoveryReadyDeepCandidate(input, entryPoint);
|
||||
const discoveryReadyAddressCandidate = isDiscoveryReadyAddressCandidate(input, entryPoint);
|
||||
const alignedFactualAddressReply = hasAlignedFactualAddressReply(input, entryPoint);
|
||||
const semanticConflictWithDiscoveryTurnMeaning = hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint);
|
||||
const matchedFactualAddressContinuationTarget = hasMatchedFactualAddressContinuationTarget(input, entryPoint);
|
||||
const fullConfirmedFactualAddressReply = hasFullConfirmedFactualAddressReply(input, entryPoint);
|
||||
const runtimeAdjustedExactReply = hasRuntimeAdjustedExactReply(input, entryPoint);
|
||||
|
||||
if (!entryPoint) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point");
|
||||
|
|
@ -180,6 +392,30 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
|||
if (!discoveryReadyAddressCandidate) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_not_discovery_ready_address_candidate");
|
||||
}
|
||||
if (alignedFactualAddressReply) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_aligned_factual_address_reply");
|
||||
}
|
||||
if (semanticConflictWithDiscoveryTurnMeaning) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_semantic_conflict_allows_candidate_override");
|
||||
}
|
||||
if (matchedFactualAddressContinuationTarget) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_factual_address_continuation_target");
|
||||
}
|
||||
if (fullConfirmedFactualAddressReply) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply");
|
||||
}
|
||||
if (runtimeAdjustedExactReply) {
|
||||
pushReason(
|
||||
reasonCodes,
|
||||
"mcp_discovery_response_policy_keep_runtime_adjusted_exact_reply_over_stale_discovery_turn_meaning"
|
||||
);
|
||||
}
|
||||
if (deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") {
|
||||
pushReason(
|
||||
reasonCodes,
|
||||
"mcp_discovery_response_policy_keep_broad_business_summary_over_clarification_candidate"
|
||||
);
|
||||
}
|
||||
if (!ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status)) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_status_not_allowed");
|
||||
}
|
||||
|
|
@ -196,6 +432,11 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
|||
const canApply =
|
||||
Boolean(entryPoint) &&
|
||||
(unsupportedBoundary || discoveryReadyChatCandidate || discoveryReadyDeepCandidate || discoveryReadyAddressCandidate) &&
|
||||
!alignedFactualAddressReply &&
|
||||
!matchedFactualAddressContinuationTarget &&
|
||||
!fullConfirmedFactualAddressReply &&
|
||||
!runtimeAdjustedExactReply &&
|
||||
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
|
||||
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
|
||||
candidate.eligible_for_future_hot_runtime &&
|
||||
Boolean(toNonEmptyString(candidate.reply_text)) &&
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import {
|
||||
isGroundedAddressDebug,
|
||||
readAssistantMcpDiscoveryPilotScope,
|
||||
resolveAddressDebugContextFacts,
|
||||
resolveAssistantContinuitySnapshot
|
||||
} from "./assistantContinuityPolicy";
|
||||
|
|
@ -54,6 +55,130 @@ function toNonEmptyString(value: unknown): string | null {
|
|||
return text.length > 0 ? text : null;
|
||||
}
|
||||
|
||||
function toRecordObject(value: unknown): Record<string, unknown> | null {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return null;
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function ensureSentence(value: string): string {
|
||||
const text = String(value ?? "").trim();
|
||||
if (!text) {
|
||||
return "";
|
||||
}
|
||||
return /[.!?]$/.test(text) ? text : `${text}.`;
|
||||
}
|
||||
|
||||
function periodPartForRecap(scopedDate: string | null): string {
|
||||
if (!scopedDate) {
|
||||
return "";
|
||||
}
|
||||
return /^\d{2}\.\d{2}\.\d{4}$/.test(scopedDate) ? ` на ${scopedDate}` : ` за период ${scopedDate}`;
|
||||
}
|
||||
|
||||
function readDiscoveryMetadataScope(debug: Record<string, unknown> | null): string | null {
|
||||
const discoveryEntry = toRecordObject(debug?.assistant_mcp_discovery_entry_point_v1);
|
||||
const bridge = toRecordObject(discoveryEntry?.bridge);
|
||||
const pilot = toRecordObject(bridge?.pilot);
|
||||
const surface = toRecordObject(pilot?.derived_metadata_surface);
|
||||
const surfaceScope = toNonEmptyString(surface?.metadata_scope);
|
||||
if (surfaceScope) {
|
||||
return surfaceScope;
|
||||
}
|
||||
const turnInput = toRecordObject(discoveryEntry?.turn_input);
|
||||
const turnMeaningRef = toRecordObject(turnInput?.turn_meaning_ref);
|
||||
const entityCandidates = Array.isArray(turnMeaningRef?.explicit_entity_candidates)
|
||||
? turnMeaningRef.explicit_entity_candidates
|
||||
: [];
|
||||
for (const candidate of entityCandidates) {
|
||||
const text = toNonEmptyString(candidate);
|
||||
if (text) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildDiscoveryRecapFactLine(input: {
|
||||
debug: Record<string, unknown> | null;
|
||||
counterparty: string | null;
|
||||
scopedDate: string | null;
|
||||
}): string | null {
|
||||
if (!input.debug) {
|
||||
return null;
|
||||
}
|
||||
const pilotScope = readAssistantMcpDiscoveryPilotScope(input.debug, toNonEmptyString);
|
||||
const discoveryEntry = toRecordObject(input.debug.assistant_mcp_discovery_entry_point_v1);
|
||||
const bridge = toRecordObject(discoveryEntry?.bridge);
|
||||
const pilot = toRecordObject(bridge?.pilot);
|
||||
const periodPart = periodPartForRecap(input.scopedDate);
|
||||
if (pilotScope === "metadata_inspection_v1") {
|
||||
const metadataScope = readDiscoveryMetadataScope(input.debug);
|
||||
const surface = toRecordObject(pilot?.derived_metadata_surface);
|
||||
const entitySets = Array.isArray(surface?.available_entity_sets)
|
||||
? surface.available_entity_sets
|
||||
.map((item) => toNonEmptyString(item))
|
||||
.filter((item): item is string => Boolean(item))
|
||||
: [];
|
||||
const fields = Array.isArray(surface?.available_fields)
|
||||
? surface.available_fields
|
||||
.map((item) => toNonEmptyString(item))
|
||||
.filter((item): item is string => Boolean(item))
|
||||
: [];
|
||||
const objects = Array.isArray(surface?.matched_objects)
|
||||
? surface.matched_objects
|
||||
.map((item) => toNonEmptyString(item))
|
||||
.filter((item): item is string => Boolean(item))
|
||||
: [];
|
||||
const rows = Number(surface?.matched_rows ?? 0);
|
||||
const scopePart = metadataScope ? ` по области «${metadataScope}»` : "";
|
||||
const objectsPart = objects.length > 0 ? `, нашли объекты ${objects.slice(0, 4).join(", ")}` : "";
|
||||
const entitySetsPart =
|
||||
entitySets.length > 0 ? `, видны типы ${entitySets.slice(0, 4).join(", ")}` : "";
|
||||
const fieldsPart =
|
||||
fields.length > 0 ? `, доступны поля/секции ${fields.slice(0, 5).join(", ")}` : "";
|
||||
return `смотрели metadata-поверхность 1С${scopePart}${periodPart}: ${rows} подтвержденных строк${objectsPart}${entitySetsPart}${fieldsPart}`.trim();
|
||||
}
|
||||
if (!input.counterparty) {
|
||||
return null;
|
||||
}
|
||||
if (pilotScope === "counterparty_lifecycle_query_documents_v1") {
|
||||
const activityPeriod = toRecordObject(pilot?.derived_activity_period);
|
||||
const duration = toNonEmptyString(activityPeriod?.duration_human_ru);
|
||||
return duration
|
||||
? `смотрели подтвержденную активность по контрагенту «${input.counterparty}»${periodPart} и оценили период взаимодействия примерно как ${duration}`
|
||||
: `смотрели подтвержденную активность по контрагенту «${input.counterparty}»${periodPart}`;
|
||||
}
|
||||
if (pilotScope === "counterparty_supplier_payout_query_movements_v1") {
|
||||
const flow = toRecordObject(pilot?.derived_value_flow);
|
||||
const amount = toNonEmptyString(flow?.total_amount_human_ru);
|
||||
return amount
|
||||
? `считали исходящие платежи/списания по контрагенту «${input.counterparty}»${periodPart}: ${amount}`
|
||||
: `считали исходящие платежи/списания по контрагенту «${input.counterparty}»${periodPart}`;
|
||||
}
|
||||
if (pilotScope === "counterparty_value_flow_query_movements_v1") {
|
||||
const flow = toRecordObject(pilot?.derived_value_flow);
|
||||
const amount = toNonEmptyString(flow?.total_amount_human_ru);
|
||||
return amount
|
||||
? `смотрели денежный поток по контрагенту «${input.counterparty}»${periodPart}: ${amount}`
|
||||
: `смотрели денежный поток по контрагенту «${input.counterparty}»${periodPart}`;
|
||||
}
|
||||
if (pilotScope === "counterparty_bidirectional_value_flow_query_movements_v1") {
|
||||
const flow = toRecordObject(pilot?.derived_bidirectional_value_flow);
|
||||
const incoming = toRecordObject(flow?.incoming_customer_revenue);
|
||||
const outgoing = toRecordObject(flow?.outgoing_supplier_payout);
|
||||
const incomingAmount = toNonEmptyString(incoming?.total_amount_human_ru);
|
||||
const outgoingAmount = toNonEmptyString(outgoing?.total_amount_human_ru);
|
||||
const netAmount = toNonEmptyString(flow?.net_amount_human_ru);
|
||||
if (incomingAmount && outgoingAmount && netAmount) {
|
||||
return `считали нетто по деньгам с контрагентом «${input.counterparty}»${periodPart}: получили ${incomingAmount}, заплатили ${outgoingAmount}, расчетное нетто ${netAmount}`;
|
||||
}
|
||||
return `считали нетто по деньгам с контрагентом «${input.counterparty}»${periodPart}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function collectMessageSamples(input: ResolveAssistantRouteMemorySignalsInput): string[] {
|
||||
const values = [
|
||||
input.rawUserMessage,
|
||||
|
|
@ -120,11 +245,21 @@ function normalizeRecapIdentity(value: unknown): string {
|
|||
function buildRecapFactLine(input: {
|
||||
debug: Record<string, unknown> | null;
|
||||
item: string | null;
|
||||
counterparty: string | null;
|
||||
organization: string | null;
|
||||
}): string | null {
|
||||
const detectedIntent = String(input.debug?.detected_intent ?? "");
|
||||
const scopedDate = resolveAddressDebugContextFacts(input.debug).scopedDate;
|
||||
const discoveryFact = buildDiscoveryRecapFactLine({
|
||||
debug: input.debug,
|
||||
counterparty: input.counterparty,
|
||||
scopedDate
|
||||
});
|
||||
if (discoveryFact) {
|
||||
return discoveryFact;
|
||||
}
|
||||
const itemPart = input.item ? `по позиции «${input.item}»` : null;
|
||||
const counterpartyPart = input.counterparty ? `по контрагенту «${input.counterparty}»` : null;
|
||||
const organizationPart = input.organization ? `по компании «${input.organization}»` : null;
|
||||
const datePart = scopedDate ? ` на ${scopedDate}` : "";
|
||||
if (detectedIntent === "inventory_on_hand_as_of_date") {
|
||||
|
|
@ -151,6 +286,9 @@ function buildRecapFactLine(input: {
|
|||
if (detectedIntent === "counterparty_activity_lifecycle" && organizationPart) {
|
||||
return `смотрели активность в базе 1С ${organizationPart}`.trim();
|
||||
}
|
||||
if (detectedIntent === "list_documents_by_counterparty" && counterpartyPart) {
|
||||
return `поднимали документы ${counterpartyPart}${datePart}`.trim();
|
||||
}
|
||||
if (detectedIntent === "list_documents_by_counterparty" && organizationPart) {
|
||||
return `поднимали документы ${organizationPart}${datePart}`.trim();
|
||||
}
|
||||
|
|
@ -196,6 +334,7 @@ function collectRecentRecapFacts(input: {
|
|||
const fact = buildRecapFactLine({
|
||||
debug: item.debug,
|
||||
item: debugItem,
|
||||
counterparty: debugContext.counterparty,
|
||||
organization: debugOrganization
|
||||
});
|
||||
if (!fact || seen.has(fact)) {
|
||||
|
|
@ -219,6 +358,7 @@ export function buildAddressMemoryRecapReply(input: {
|
|||
}): string {
|
||||
const contextFacts = resolveAddressDebugContextFacts(input.addressDebug, input.toNonEmptyString);
|
||||
const item = contextFacts.item;
|
||||
const counterparty = contextFacts.counterparty;
|
||||
const organization = input.organization ?? contextFacts.organization;
|
||||
const scopedDate = contextFacts.scopedDate;
|
||||
const recapFacts = collectRecentRecapFacts({
|
||||
|
|
@ -246,6 +386,22 @@ export function buildAddressMemoryRecapReply(input: {
|
|||
].join(" ");
|
||||
}
|
||||
|
||||
if (counterparty) {
|
||||
const organizationPart = organization ? ` по компании «${organization}»` : "";
|
||||
const periodPart = periodPartForRecap(scopedDate);
|
||||
if (recapFacts.length > 0) {
|
||||
return [
|
||||
`Да, помню. По контрагенту «${counterparty}»${organizationPart}${periodPart} мы уже выяснили:`,
|
||||
...recapFacts.map((fact) => `- ${fact}.`),
|
||||
"Могу сразу продолжить по нему: поступления, платежи, нетто, помесячную раскладку или границы подтверждения."
|
||||
].join("\n");
|
||||
}
|
||||
return [
|
||||
`Да, помню. Мы уже смотрели контур по контрагенту «${counterparty}»${organizationPart}${periodPart}.`,
|
||||
"Могу продолжить по нему без переписывания контекста: поступления, платежи, нетто, документы или пояснение границ ответа."
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
if (organization || scopedDate) {
|
||||
const organizationPart = organization ? ` по компании «${organization}»` : "";
|
||||
const datePart = scopedDate ? ` на ${scopedDate}` : "";
|
||||
|
|
@ -258,13 +414,77 @@ export function buildAddressMemoryRecapReply(input: {
|
|||
return "Да, помню предыдущий адресный контур. Могу кратко напомнить, что мы уже подтвердили, или сразу продолжить следующий шаг.";
|
||||
}
|
||||
|
||||
export function buildBroadBusinessEvaluationReply(input: {
|
||||
organization: string | null;
|
||||
addressDebug: Record<string, unknown> | null;
|
||||
sessionItems?: unknown[];
|
||||
toNonEmptyString: (value: unknown) => string | null;
|
||||
}): string {
|
||||
const contextFacts = resolveAddressDebugContextFacts(input.addressDebug, input.toNonEmptyString);
|
||||
const organization = input.organization ?? contextFacts.organization;
|
||||
const recapFacts = collectRecentRecapFacts({
|
||||
sessionItems: input.sessionItems,
|
||||
item: null,
|
||||
organization,
|
||||
toNonEmptyString: input.toNonEmptyString
|
||||
});
|
||||
const organizationPart = organization ? ` по компании «${organization}»` : "";
|
||||
|
||||
if (recapFacts.length > 0) {
|
||||
return [
|
||||
`Коротко: по тому, что мы уже подтвердили в 1С${organizationPart}, компания выглядит операционно живой, но это пока только частичная оценка бизнеса.`,
|
||||
"Сейчас я опираюсь на такие подтвержденные факты:",
|
||||
...recapFacts.map((fact) => `- ${ensureSentence(fact)}`),
|
||||
"Это еще не полная диагностика всего бизнеса и не вывод о прибыли: я честно суммирую только те контуры, которые мы уже проверили в диалоге.",
|
||||
"Если хочешь, следующим шагом могу сузить оценку до денежного потока, долгов, НДС или ключевых контрагентов."
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
return [
|
||||
`Коротко: по нынешнему контексту 1С${organizationPart} я вижу признаки операционной активности, но для содержательной оценки бизнеса нужно еще несколько опорных срезов.`,
|
||||
"Если хочешь, я быстро доберу основу для такой оценки: денежный поток, дебиторка/кредиторка, НДС или ключевые контрагенты."
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
export function buildSelectedObjectAnswerInspectionReply(input: {
|
||||
addressDebug: Record<string, unknown> | null;
|
||||
toNonEmptyString: (value: unknown) => string | null;
|
||||
}): string {
|
||||
const contextFacts = resolveAddressDebugContextFacts(input.addressDebug, input.toNonEmptyString);
|
||||
const itemLabel = contextFacts.item ?? "эта позиция";
|
||||
const counterpartyLabel = contextFacts.counterparty;
|
||||
const detectedIntent = String(input.addressDebug?.detected_intent ?? "");
|
||||
const pilotScope = readAssistantMcpDiscoveryPilotScope(input.addressDebug, input.toNonEmptyString);
|
||||
const periodPart = periodPartForRecap(contextFacts.scopedDate);
|
||||
|
||||
if (counterpartyLabel && pilotScope === "counterparty_bidirectional_value_flow_query_movements_v1") {
|
||||
return [
|
||||
`Да, в предыдущем ответе речь шла о двустороннем денежном потоке с контрагентом «${counterpartyLabel}»${periodPart}.`,
|
||||
"Нетто там означало разницу между тем, что получили, и тем, что заплатили по найденным строкам 1С.",
|
||||
"Это расчет по проверенному периоду и подтвержденным строкам, а не заявление про весь оборот вне этого окна."
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
if (counterpartyLabel && pilotScope === "counterparty_supplier_payout_query_movements_v1") {
|
||||
return [
|
||||
`Да, в предыдущем ответе речь шла об исходящих платежах/списаниях по контрагенту «${counterpartyLabel}»${periodPart}.`,
|
||||
"Это сумма по найденным строкам 1С за проверенный период, а не обещание, что за пределами этого окна больше движений не было."
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
if (counterpartyLabel && pilotScope === "counterparty_value_flow_query_movements_v1") {
|
||||
return [
|
||||
`Да, в предыдущем ответе речь шла о денежном потоке по контрагенту «${counterpartyLabel}»${periodPart}.`,
|
||||
"Это расчет по найденным движениям 1С за проверенный период, а не безусловный итог по всем временам."
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
if (counterpartyLabel && pilotScope === "counterparty_lifecycle_query_documents_v1") {
|
||||
return [
|
||||
`Да, в предыдущем ответе речь шла об активности контрагента «${counterpartyLabel}»${periodPart}.`,
|
||||
"Это оценка по подтвержденным строкам 1С, а не юридически подтвержденная дата регистрации."
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
if (detectedIntent === "inventory_sale_trace_for_item") {
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -2563,6 +2563,12 @@ function hasAddressFollowupContextSignal(userMessage) {
|
|||
if (ultraShortFollowup && hasAny(/^(?:давай|показывай|показывыай|ещ[её]|also|again|go|ok|okay)(?=$|[\s,.;:!?])/iu)) {
|
||||
return true;
|
||||
}
|
||||
const shortValueFlowRetargetCue = shortFollowup &&
|
||||
(hasMarker() || hasPointer() || hasAny(/^(?:Р°|a|Рё|i|also|then|now)(?=$|[\s,.;:!?])/iu)) &&
|
||||
hasAny(/(?:нетто|сальдо|разниц|получил|заплатил|поступ|входящ|исходящ|оборот|выручк|денеж)/iu);
|
||||
if (shortValueFlowRetargetCue) {
|
||||
return true;
|
||||
}
|
||||
if (hasStandaloneAddressTopicSignal(rawText || repairedText)) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -3822,11 +3828,7 @@ function findLastGroundedAddressAnswerDebug(items) {
|
|||
continue;
|
||||
}
|
||||
const debug = item.debug;
|
||||
if (debug.execution_lane !== "address_query") {
|
||||
continue;
|
||||
}
|
||||
const groundingStatus = toNonEmptyString(debug.answer_grounding_check?.status);
|
||||
if (groundingStatus === "grounded") {
|
||||
if ((0, assistantContinuityPolicy_1.isGroundedAddressDebug)(debug, toNonEmptyString)) {
|
||||
return debug;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,18 @@ import {
|
|||
buildRootScopedCarryoverFilters,
|
||||
buildInventoryRootFrameFromAddressDebug,
|
||||
hydrateInventoryRootFrameState,
|
||||
readAddressDebugIntent,
|
||||
readAddressDebugFilters,
|
||||
readAddressDebugItem,
|
||||
readAssistantMcpDiscoveryMetadataAmbiguityDetected,
|
||||
readAssistantMcpDiscoveryMetadataAmbiguityEntitySets,
|
||||
readAssistantMcpDiscoveryEntityCandidates,
|
||||
readAssistantMcpDiscoveryEntityAmbiguityCandidates,
|
||||
readAssistantMcpDiscoveryEntityResolutionStatus,
|
||||
readAssistantMcpDiscoveryMetadataRouteFamily,
|
||||
readAssistantMcpDiscoveryMetadataSelectedEntitySet,
|
||||
readAddressDebugTemporalScope,
|
||||
readAssistantMcpDiscoveryPilotScope,
|
||||
resolveOrganizationClarificationContinuation,
|
||||
resolveNavigationSessionContextState,
|
||||
resolveAddressDebugCarryoverFilters,
|
||||
|
|
@ -171,19 +180,23 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
return false;
|
||||
}
|
||||
const executionLane = deps.toNonEmptyString(debug.execution_lane);
|
||||
const detectedIntent = deps.toNonEmptyString(debug.detected_intent);
|
||||
const detectedIntent = readAddressDebugIntent(debug, deps.toNonEmptyString);
|
||||
const selectedRecipe = deps.toNonEmptyString(debug.selected_recipe);
|
||||
const answerGroundingCheck =
|
||||
debug.answer_grounding_check && typeof debug.answer_grounding_check === "object"
|
||||
? debug.answer_grounding_check
|
||||
: null;
|
||||
const groundingStatus = deps.toNonEmptyString(answerGroundingCheck?.status);
|
||||
const discoveryPilotScope = readAssistantMcpDiscoveryPilotScope(debug, deps.toNonEmptyString);
|
||||
if (groundingStatus === "grounded") {
|
||||
return true;
|
||||
}
|
||||
if (selectedRecipe) {
|
||||
return true;
|
||||
}
|
||||
if (debug.mcp_discovery_response_applied === true && discoveryPilotScope) {
|
||||
return true;
|
||||
}
|
||||
return executionLane === "address_query" && Boolean(detectedIntent && detectedIntent !== "unknown");
|
||||
}
|
||||
|
||||
|
|
@ -310,6 +323,27 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
return sameDatePhrases.some((phrase) => normalized.includes(phrase));
|
||||
}
|
||||
|
||||
function hasShortValueFlowRetargetCue(text) {
|
||||
const normalized = normalizeFollowupText(text);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const tokenCount = deps.countTokens(normalized);
|
||||
if (!Number.isFinite(tokenCount) || tokenCount > 8) {
|
||||
return false;
|
||||
}
|
||||
const hasLeadCue =
|
||||
deps.hasFollowupMarker(text) ||
|
||||
deps.hasReferentialPointer(text) ||
|
||||
/^(?:\u0430|\u0438|also|then|now)(?=$|[\s,.;:!?])/iu.test(normalized);
|
||||
if (!hasLeadCue) {
|
||||
return false;
|
||||
}
|
||||
return /(?:\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|\u0440\u0430\u0437\u043d\u0438\u0446|\u043f\u043e\u043b\u0443\u0447|\u0437\u0430\u043f\u043b\u0430\u0442|\u043f\u043e\u0441\u0442\u0443\u043f|\u0432\u0445\u043e\u0434\u044f\u0449|\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\u043a|\u0434\u0435\u043d\u0435\u0436)/iu.test(
|
||||
normalized
|
||||
);
|
||||
}
|
||||
|
||||
function shouldKeepPreviousIntentForShortCounterpartyRetarget(userMessage, sourceIntent) {
|
||||
const normalized = deps.compactWhitespace(
|
||||
deps.repairAddressMojibake(String(userMessage ?? "")).toLowerCase()
|
||||
|
|
@ -438,7 +472,16 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
(deps.toNonEmptyString(alternateMessage)
|
||||
? deps.isImplicitAddressContinuationByLlm(alternateMessage, llmPreDecomposeMeta)
|
||||
: false));
|
||||
const sourceIntentHint = deps.toNonEmptyString(carryoverSourceDebug?.detected_intent);
|
||||
const sourceIntentHint = readAddressDebugIntent(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryPilotScopeHint = readAssistantMcpDiscoveryPilotScope(
|
||||
carryoverSourceDebug,
|
||||
deps.toNonEmptyString
|
||||
);
|
||||
const hasValueFlowCarryoverSourceHint =
|
||||
sourceIntentHint === "customer_revenue_and_payments" ||
|
||||
sourceDiscoveryPilotScopeHint === "counterparty_value_flow_query_movements_v1" ||
|
||||
sourceDiscoveryPilotScopeHint === "counterparty_supplier_payout_query_movements_v1" ||
|
||||
sourceDiscoveryPilotScopeHint === "counterparty_bidirectional_value_flow_query_movements_v1";
|
||||
const navigationSessionState = resolveNavigationSessionContextState(
|
||||
addressNavigationState,
|
||||
deps.toNonEmptyString,
|
||||
|
|
@ -474,14 +517,22 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
? deps.resolveDebtRoleSwapFollowupIntent(String(alternateMessage ?? ""), sourceIntentHint)
|
||||
: null;
|
||||
const debtRoleSwapIntent = debtRoleSwapPrimary ?? debtRoleSwapAlternate ?? null;
|
||||
const shortValueFlowRetargetPrimary =
|
||||
hasValueFlowCarryoverSourceHint && hasShortValueFlowRetargetCue(userMessage);
|
||||
const shortValueFlowRetargetAlternate =
|
||||
hasValueFlowCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
|
||||
? hasShortValueFlowRetargetCue(String(alternateMessage ?? ""))
|
||||
: false;
|
||||
let hasPrimaryFollowupSignal =
|
||||
deps.hasAddressFollowupContextSignal(userMessage) ||
|
||||
Boolean(debtRoleSwapPrimary) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryPurchaseDateVatBridge;
|
||||
let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
||||
Boolean(debtRoleSwapAlternate) ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
inventoryPurchaseDateVatBridge
|
||||
: false;
|
||||
|
|
@ -532,6 +583,8 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
hasInventoryRootRestatementAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
Boolean(debtRoleSwapIntent) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
|
|
@ -550,6 +603,8 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
hasInventoryRootRestatementAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
Boolean(debtRoleSwapIntent) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
|
|
@ -577,6 +632,8 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
!hasInventoryRootTemporalFollowupAlternate &&
|
||||
!hasInventoryRootRestatementPrimary &&
|
||||
!hasInventoryRootRestatementAlternate &&
|
||||
!shortValueFlowRetargetPrimary &&
|
||||
!shortValueFlowRetargetAlternate &&
|
||||
!hasImplicitContinuationSignal &&
|
||||
!hasOrganizationClarificationContinuation &&
|
||||
!hasIndexReferenceSignal
|
||||
|
|
@ -590,6 +647,8 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
!hasInventoryRootTemporalFollowupAlternate &&
|
||||
!hasInventoryRootRestatementPrimary &&
|
||||
!hasInventoryRootRestatementAlternate &&
|
||||
!shortValueFlowRetargetPrimary &&
|
||||
!shortValueFlowRetargetAlternate &&
|
||||
!hasImplicitContinuationSignal &&
|
||||
!hasOrganizationClarificationContinuation &&
|
||||
!hasIndexReferenceSignal
|
||||
|
|
@ -599,7 +658,35 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
if (!carryoverSourceDebug) {
|
||||
return null;
|
||||
}
|
||||
const sourceIntent = deps.toNonEmptyString(carryoverSourceDebug.detected_intent);
|
||||
const sourceIntent = readAddressDebugIntent(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryPilotScope = sourceDiscoveryPilotScopeHint;
|
||||
const sourceDiscoveryMetadataRouteFamily = readAssistantMcpDiscoveryMetadataRouteFamily(
|
||||
carryoverSourceDebug,
|
||||
deps.toNonEmptyString
|
||||
);
|
||||
const sourceDiscoveryMetadataSelectedEntitySet = readAssistantMcpDiscoveryMetadataSelectedEntitySet(
|
||||
carryoverSourceDebug,
|
||||
deps.toNonEmptyString
|
||||
);
|
||||
const sourceDiscoveryMetadataAmbiguityDetected = readAssistantMcpDiscoveryMetadataAmbiguityDetected(
|
||||
carryoverSourceDebug
|
||||
);
|
||||
const sourceDiscoveryMetadataAmbiguityEntitySets = readAssistantMcpDiscoveryMetadataAmbiguityEntitySets(
|
||||
carryoverSourceDebug,
|
||||
deps.toNonEmptyString
|
||||
);
|
||||
const sourceDiscoveryEntityResolutionStatus = readAssistantMcpDiscoveryEntityResolutionStatus(
|
||||
carryoverSourceDebug,
|
||||
deps.toNonEmptyString
|
||||
);
|
||||
const sourceDiscoveryEntityCandidates = readAssistantMcpDiscoveryEntityCandidates(
|
||||
carryoverSourceDebug,
|
||||
deps.toNonEmptyString
|
||||
);
|
||||
const sourceDiscoveryEntityAmbiguityCandidates = readAssistantMcpDiscoveryEntityAmbiguityCandidates(
|
||||
carryoverSourceDebug,
|
||||
deps.toNonEmptyString
|
||||
);
|
||||
const llmExplicitIntent = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
|
||||
const llmSelectedObjectScopeDetected =
|
||||
llmPreDecomposeMeta?.predecomposeContract?.semantics?.selected_object_scope_detected === true;
|
||||
|
|
@ -699,12 +786,14 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
hasPrimaryFollowupSignal =
|
||||
deps.hasAddressFollowupContextSignal(userMessage) ||
|
||||
Boolean(debtRoleSwapPrimary) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
hasInventoryRootTemporalFollowupPrimary;
|
||||
hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
||||
Boolean(debtRoleSwapAlternate) ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
hasInventoryRootTemporalFollowupAlternate
|
||||
|
|
@ -720,6 +809,8 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
hasInventoryRootTemporalFollowupAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
Boolean(debtRoleSwapIntent) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
|
|
@ -931,6 +1022,19 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
previous_filters: previousFilters,
|
||||
previous_anchor_type: previousAnchorType ?? undefined,
|
||||
previous_anchor_value: previousAnchor,
|
||||
previous_discovery_pilot_scope: sourceDiscoveryPilotScope ?? undefined,
|
||||
previous_discovery_entity_resolution_status: sourceDiscoveryEntityResolutionStatus ?? undefined,
|
||||
previous_discovery_entity_candidates:
|
||||
sourceDiscoveryEntityCandidates.length > 0 ? sourceDiscoveryEntityCandidates : undefined,
|
||||
previous_discovery_entity_ambiguity_candidates:
|
||||
sourceDiscoveryEntityAmbiguityCandidates.length > 0
|
||||
? sourceDiscoveryEntityAmbiguityCandidates
|
||||
: undefined,
|
||||
previous_discovery_metadata_route_family: sourceDiscoveryMetadataRouteFamily ?? undefined,
|
||||
previous_discovery_metadata_selected_entity_set: sourceDiscoveryMetadataSelectedEntitySet ?? undefined,
|
||||
previous_discovery_metadata_ambiguity_detected: sourceDiscoveryMetadataAmbiguityDetected || undefined,
|
||||
previous_discovery_metadata_ambiguity_entity_sets:
|
||||
sourceDiscoveryMetadataAmbiguityEntitySets.length > 0 ? sourceDiscoveryMetadataAmbiguityEntitySets : undefined,
|
||||
resolved_counterparty_from_display: resolvedCounterpartyFromDisplay || undefined,
|
||||
root_context_only: rootScopedPivot || undefined,
|
||||
root_intent: shouldAttachInventoryRootFrame ? inventoryRootFrame?.intent ?? undefined : undefined,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ const SUPPORTED_ADDRESS_INTENTS = new Set([
|
|||
"payables_confirmed_as_of_date",
|
||||
"list_documents_by_counterparty",
|
||||
"customer_revenue_and_payments",
|
||||
"inventory_on_hand_as_of_date"
|
||||
"inventory_on_hand_as_of_date",
|
||||
"vat_liability_confirmed_for_tax_period",
|
||||
"vat_payable_confirmed_as_of_date",
|
||||
"vat_payable_forecast"
|
||||
]);
|
||||
|
||||
function fallbackCompactWhitespace(value) {
|
||||
|
|
@ -89,8 +92,23 @@ function detectCounterpartyTurnoverFamily(text) {
|
|||
"\u0434\u043e\u0445\u043e\u0434",
|
||||
"\u0431\u044b\u043b",
|
||||
"\u0431\u044b\u043b\u0430",
|
||||
"\u0432\u0440\u0435\u043c\u044f",
|
||||
"\u0432\u0440\u0435\u043c\u0435\u043d\u0438",
|
||||
"\u0433\u043e\u0434",
|
||||
"\u0433\u043e\u0434\u0430",
|
||||
"\u043f\u0435\u0440\u0438\u043e\u0434",
|
||||
"\u043f\u0435\u0440\u0438\u043e\u0434\u0430",
|
||||
"\u043c\u0435\u0441\u044f\u0446",
|
||||
"\u043c\u0435\u0441\u044f\u0446\u0430",
|
||||
"\u043a\u0432\u0430\u0440\u0442\u0430\u043b",
|
||||
"\u043a\u0432\u0430\u0440\u0442\u0430\u043b\u0430",
|
||||
"turnover",
|
||||
"revenue"
|
||||
"revenue",
|
||||
"time",
|
||||
"year",
|
||||
"period",
|
||||
"month",
|
||||
"quarter"
|
||||
]);
|
||||
const entity = rawEntity && !ignored.has(rawEntity) ? rawEntity : null;
|
||||
return {
|
||||
|
|
@ -99,6 +117,23 @@ function detectCounterpartyTurnoverFamily(text) {
|
|||
};
|
||||
}
|
||||
|
||||
function detectBroadBusinessEvaluation(text) {
|
||||
const normalized = String(text ?? "");
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
/(?:как\s+ты\s+оценишь\s+деятельност[ьи]\s+компан|оценк[аи]?\s+деятельност[ьи]\s+компан|что\s+у\s+нас\s+вообще\s+происход|где\s+главн(?:ые|ый)\s+риски|как\s+у\s+нас\s+дела\s+по\s+компан)/iu.test(
|
||||
normalized
|
||||
)
|
||||
) {
|
||||
return {
|
||||
family: "broad_business_evaluation"
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildEntityCandidates(counterpartyTurnover) {
|
||||
if (!counterpartyTurnover?.entity) {
|
||||
return [];
|
||||
|
|
@ -121,10 +156,17 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
|
|||
const joinedText = fallbackCompactWhitespace(`${rawText} ${effectiveText}`);
|
||||
const supportedIntent = detectSupportedIntent(joinedText, deps);
|
||||
const counterpartyTurnover = detectCounterpartyTurnoverFamily(joinedText);
|
||||
const broadBusinessEvaluation = detectBroadBusinessEvaluation(joinedText);
|
||||
const llmIntent = toNonEmptyString(input?.llmPreDecomposeMeta?.predecomposeContract?.intent, deps);
|
||||
const explicitIntentCandidate =
|
||||
supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null);
|
||||
const unsupportedFamily = !explicitIntentCandidate && counterpartyTurnover?.family ? counterpartyTurnover.family : null;
|
||||
broadBusinessEvaluation?.family
|
||||
? null
|
||||
: supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null);
|
||||
const unsupportedFamily = broadBusinessEvaluation?.family
|
||||
? broadBusinessEvaluation.family
|
||||
: !explicitIntentCandidate && counterpartyTurnover?.family
|
||||
? counterpartyTurnover.family
|
||||
: null;
|
||||
const reasonCodes = [];
|
||||
if (supportedIntent?.reason) {
|
||||
reasonCodes.push(supportedIntent.reason);
|
||||
|
|
@ -132,6 +174,9 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
|
|||
if (counterpartyTurnover?.family) {
|
||||
reasonCodes.push("counterparty_turnover_current_turn_signal");
|
||||
}
|
||||
if (broadBusinessEvaluation?.family) {
|
||||
reasonCodes.push("broad_business_evaluation_current_turn_signal");
|
||||
}
|
||||
if (rawText !== normalizeTurnText(rawMessage, { ...deps, repairAddressMojibake: (value) => String(value ?? "") })) {
|
||||
reasonCodes.push("mojibake_repair_applied");
|
||||
}
|
||||
|
|
@ -146,8 +191,12 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
|
|||
? "receivables"
|
||||
: explicitIntentCandidate?.startsWith("payables_")
|
||||
? "payables"
|
||||
: explicitIntentCandidate?.startsWith("vat_")
|
||||
? "vat"
|
||||
: explicitIntentCandidate?.startsWith("inventory_")
|
||||
? "inventory"
|
||||
: broadBusinessEvaluation?.family
|
||||
? "business_summary"
|
||||
: explicitIntentCandidate?.includes("counterparty")
|
||||
? "counterparty"
|
||||
: counterpartyTurnover?.family
|
||||
|
|
@ -158,12 +207,22 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
|
|||
explicitIntentCandidate === "payables_confirmed_as_of_date" ||
|
||||
explicitIntentCandidate === "inventory_on_hand_as_of_date"
|
||||
? "confirmed_snapshot"
|
||||
: broadBusinessEvaluation?.family
|
||||
? "broad_evaluation"
|
||||
: explicitIntentCandidate === "vat_liability_confirmed_for_tax_period"
|
||||
? "confirmed_tax_period"
|
||||
: explicitIntentCandidate === "vat_payable_confirmed_as_of_date"
|
||||
? "confirmed_snapshot"
|
||||
: explicitIntentCandidate === "vat_payable_forecast"
|
||||
? "forecast"
|
||||
: explicitIntentCandidate === "list_documents_by_counterparty"
|
||||
? "list_documents"
|
||||
: counterpartyTurnover?.family
|
||||
? "counterparty_value_or_turnover"
|
||||
: null;
|
||||
const staleReplayForbidden = Boolean(unsupportedFamily || (counterpartyTurnover?.entity && !explicitIntentCandidate));
|
||||
const staleReplayForbidden = Boolean(
|
||||
unsupportedFamily || broadBusinessEvaluation?.family || (counterpartyTurnover?.entity && !explicitIntentCandidate)
|
||||
);
|
||||
return {
|
||||
schema_version: "assistant_turn_meaning_v1",
|
||||
raw_message: rawMessage,
|
||||
|
|
@ -174,7 +233,9 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
|
|||
asked_action_family: askedActionFamily,
|
||||
explicit_intent_candidate: explicitIntentCandidate,
|
||||
explicit_entity_candidates: buildEntityCandidates(counterpartyTurnover),
|
||||
meaning_confidence: supportedIntent?.confidence ?? (counterpartyTurnover?.family ? "medium" : "low"),
|
||||
meaning_confidence: broadBusinessEvaluation?.family
|
||||
? "medium"
|
||||
: supportedIntent?.confidence ?? (counterpartyTurnover?.family ? "medium" : "low"),
|
||||
intent_override_strength: explicitIntentCandidate
|
||||
? "explicit_current_turn_intent"
|
||||
: staleReplayForbidden
|
||||
|
|
|
|||
|
|
@ -3141,5 +3141,113 @@ describe("assistant address follow-up carryover", () => {
|
|||
expect(calls[0].options?.followupContext?.root_filters?.counterparty).toBeUndefined();
|
||||
expect(normalizerService.normalize).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.skip("passes grounded MCP discovery payout context into a short year-switch follow-up", async () => {
|
||||
const followupMessage = "а теперь за 2021?";
|
||||
const calls: Array<{ message: string; options?: any }> = [];
|
||||
|
||||
const addressQueryService = {
|
||||
tryHandle: vi.fn(async (message: string, options?: any) => {
|
||||
calls.push({ message, options });
|
||||
if (message === followupMessage && options?.followupContext) {
|
||||
return buildAddressLaneResult({
|
||||
reply_text: "Подтверждены исходящие платежи по Группа СВК за 2021 год.",
|
||||
debug: {
|
||||
...buildAddressLaneResult().debug,
|
||||
detected_intent: "supplier_payouts_profile",
|
||||
selected_recipe: "address_supplier_payouts_profile_v1",
|
||||
extracted_filters: {
|
||||
counterparty: "Группа СВК",
|
||||
organization: "ООО Альтернатива Плюс",
|
||||
period_from: "2021-01-01",
|
||||
period_to: "2021-12-31"
|
||||
},
|
||||
reasons: ["address_action_detected", "address_entity_detected", "address_followup_context_applied"]
|
||||
}
|
||||
});
|
||||
}
|
||||
return null;
|
||||
})
|
||||
} as any;
|
||||
|
||||
const normalizerService = {
|
||||
normalize: vi.fn(async () => ({
|
||||
assistant_reply: "normalizer_fallback_should_not_be_used",
|
||||
reply_type: "partial_coverage",
|
||||
debug: {}
|
||||
}))
|
||||
} as any;
|
||||
|
||||
const sessions = new AssistantSessionStore();
|
||||
const service = new AssistantService(
|
||||
normalizerService,
|
||||
sessions as any,
|
||||
{} as any,
|
||||
{ persistSession: vi.fn() } as any,
|
||||
addressQueryService
|
||||
);
|
||||
|
||||
const sessionId = `asst-discovery-followup-year-switch-${Date.now()}`;
|
||||
sessions.appendItem(sessionId, {
|
||||
message_id: "msg-discovery-payout-seed",
|
||||
session_id: sessionId,
|
||||
role: "assistant",
|
||||
text: "Подтверждены исходящие платежи по Группа СВК за 2020 год.",
|
||||
reply_type: "partial_coverage",
|
||||
created_at: "2026-04-20T10:00:00.000Z",
|
||||
trace_id: "living-discovery-seed",
|
||||
debug: {
|
||||
execution_lane: "living_chat",
|
||||
mcp_discovery_response_applied: true,
|
||||
assistant_active_organization: "ООО Альтернатива Плюс",
|
||||
assistant_mcp_discovery_entry_point_v1: {
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
entry_status: "bridge_executed",
|
||||
turn_input: {
|
||||
turn_meaning_ref: {
|
||||
asked_action_family: "payout",
|
||||
explicit_entity_candidates: ["Группа СВК"],
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||
explicit_date_scope: "2020"
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
business_fact_answer_allowed: true,
|
||||
pilot: {
|
||||
pilot_scope: "counterparty_supplier_payout_query_movements_v1"
|
||||
},
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} as any);
|
||||
|
||||
const response = await service.handleMessage({
|
||||
session_id: sessionId,
|
||||
user_message: followupMessage,
|
||||
useMock: true
|
||||
} as any);
|
||||
|
||||
expect(response.ok).toBe(true);
|
||||
expect(response.reply_type).toBe("factual");
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0].message).toBe(followupMessage);
|
||||
expect(calls[0].options?.followupContext?.previous_intent).toBe("supplier_payouts_profile");
|
||||
expect(calls[0].options?.followupContext?.previous_discovery_pilot_scope).toBe(
|
||||
"counterparty_supplier_payout_query_movements_v1"
|
||||
);
|
||||
expect(calls[0].options?.followupContext?.previous_anchor_type).toBe("counterparty");
|
||||
expect(calls[0].options?.followupContext?.previous_anchor_value).toBe("Группа СВК");
|
||||
expect(calls[0].options?.followupContext?.previous_filters).toMatchObject({
|
||||
counterparty: "Группа СВК",
|
||||
organization: "ООО Альтернатива Плюс",
|
||||
period_from: "2020-01-01",
|
||||
period_to: "2020-12-31"
|
||||
});
|
||||
expect(normalizerService.normalize).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -154,6 +154,97 @@ describe("assistant address orchestration runtime adapter", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("passes grounded discovery follow-up carryover into MCP discovery entry point for a short year switch", async () => {
|
||||
const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
|
||||
entry_status: "bridge_executed",
|
||||
hot_runtime_wired: false,
|
||||
discovery_attempted: true
|
||||
}));
|
||||
const input = buildInput({
|
||||
userMessage: "а теперь за 2021?",
|
||||
runAddressLlmPreDecompose: vi.fn(async () => ({
|
||||
attempted: true,
|
||||
applied: false,
|
||||
effectiveMessage: "а теперь за 2021?",
|
||||
reason: "raw_kept",
|
||||
predecomposeContract: {
|
||||
mode: "unsupported",
|
||||
intent: "unknown",
|
||||
period: {
|
||||
scope: "year",
|
||||
period_from: "2021-01-01",
|
||||
period_to: "2021-12-31",
|
||||
has_explicit_period: true
|
||||
}
|
||||
}
|
||||
})),
|
||||
resolveAddressFollowupCarryoverContext: vi.fn(() => ({
|
||||
followupContext: {
|
||||
previous_intent: "supplier_payouts_profile",
|
||||
target_intent: "supplier_payouts_profile",
|
||||
previous_discovery_pilot_scope: "counterparty_supplier_payout_query_movements_v1",
|
||||
previous_anchor_type: "counterparty",
|
||||
previous_anchor_value: "Группа СВК",
|
||||
previous_filters: {
|
||||
counterparty: "Группа СВК",
|
||||
organization: "ООО Альтернатива Плюс",
|
||||
period_from: "2020-01-01",
|
||||
period_to: "2020-12-31"
|
||||
}
|
||||
}
|
||||
})),
|
||||
resolveAssistantOrchestrationDecision: vi.fn(() => ({
|
||||
runAddressLane: true,
|
||||
livingMode: "address_data",
|
||||
livingReason: "address_lane_triggered",
|
||||
toolGateDecision: "run_address_lane",
|
||||
toolGateReason: "followup_context_detected",
|
||||
orchestrationContract: {
|
||||
schema_version: "assistant_orchestration_contract_v1",
|
||||
assistant_turn_meaning: {
|
||||
schema_version: "assistant_turn_meaning_v1",
|
||||
raw_message: "а теперь за 2021?",
|
||||
effective_message: "а теперь за 2021?",
|
||||
explicit_entity_candidates: []
|
||||
}
|
||||
}
|
||||
})),
|
||||
runMcpDiscoveryRuntimeEntryPoint
|
||||
});
|
||||
|
||||
const output = await buildAssistantAddressOrchestrationRuntime(input);
|
||||
|
||||
expect(output.orchestrationDecision.runAddressLane).toBe(true);
|
||||
expect(runMcpDiscoveryRuntimeEntryPoint).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
userMessage: "а теперь за 2021?",
|
||||
effectiveMessage: "а теперь за 2021?",
|
||||
followupContext: expect.objectContaining({
|
||||
previous_intent: "supplier_payouts_profile",
|
||||
target_intent: "supplier_payouts_profile",
|
||||
previous_discovery_pilot_scope: "counterparty_supplier_payout_query_movements_v1",
|
||||
previous_anchor_type: "counterparty",
|
||||
previous_anchor_value: "Группа СВК",
|
||||
previous_filters: expect.objectContaining({
|
||||
counterparty: "Группа СВК",
|
||||
organization: "ООО Альтернатива Плюс",
|
||||
period_from: "2020-01-01",
|
||||
period_to: "2020-12-31"
|
||||
})
|
||||
})
|
||||
})
|
||||
);
|
||||
expect(output.addressRuntimeMeta.mcpDiscoveryRuntimeEntryPoint).toEqual(
|
||||
expect.objectContaining({
|
||||
entry_status: "bridge_executed",
|
||||
discovery_attempted: true,
|
||||
hot_runtime_wired: false
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps address orchestration alive when MCP discovery entry point fails", async () => {
|
||||
const input = buildInput({
|
||||
runMcpDiscoveryRuntimeEntryPoint: vi.fn(async () => {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import {
|
|||
applyTemporalCarryoverFilters,
|
||||
buildRootScopedCarryoverFilters,
|
||||
hydrateInventoryRootFrameState,
|
||||
readAddressDebugCounterparty,
|
||||
readAddressDebugIntent,
|
||||
readAddressDebugTemporalScope,
|
||||
resolveNavigationSessionContextState,
|
||||
resolveAddressDebugCarryoverFilters,
|
||||
|
|
@ -108,11 +110,169 @@ describe("assistantContinuityPolicy organization authority", () => {
|
|||
|
||||
expect(facts).toEqual({
|
||||
item: "Рабочая станция",
|
||||
counterparty: null,
|
||||
organization: 'ООО "Альтернатива Плюс"',
|
||||
scopedDate: "31.03.2020"
|
||||
});
|
||||
});
|
||||
|
||||
it("reads counterparty, organization and period from grounded MCP discovery fallback", () => {
|
||||
const facts = resolveAddressDebugContextFacts({
|
||||
execution_lane: "living_chat",
|
||||
mcp_discovery_response_applied: true,
|
||||
assistant_active_organization: "ООО Альтернатива Плюс",
|
||||
assistant_mcp_discovery_entry_point_v1: {
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
entry_status: "bridge_executed",
|
||||
turn_input: {
|
||||
turn_meaning_ref: {
|
||||
explicit_entity_candidates: ["Группа СВК"],
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||
explicit_date_scope: "2020"
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
business_fact_answer_allowed: true,
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference"
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(facts).toEqual({
|
||||
item: null,
|
||||
counterparty: "Группа СВК",
|
||||
organization: "ООО Альтернатива Плюс",
|
||||
scopedDate: "2020"
|
||||
});
|
||||
});
|
||||
|
||||
it("hydrates intent and carryover filters from grounded MCP discovery payout scope", () => {
|
||||
const debug = {
|
||||
execution_lane: "living_chat",
|
||||
mcp_discovery_response_applied: true,
|
||||
assistant_active_organization: "ООО Альтернатива Плюс",
|
||||
assistant_mcp_discovery_entry_point_v1: {
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
entry_status: "bridge_executed",
|
||||
turn_input: {
|
||||
turn_meaning_ref: {
|
||||
asked_action_family: "payout",
|
||||
explicit_entity_candidates: ["Группа СВК"],
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||
explicit_date_scope: "2020"
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
business_fact_answer_allowed: true,
|
||||
pilot: {
|
||||
pilot_scope: "counterparty_supplier_payout_query_movements_v1"
|
||||
},
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
expect(readAddressDebugIntent(debug)).toBe("supplier_payouts_profile");
|
||||
expect(readAddressDebugTemporalScope(debug)).toEqual({
|
||||
asOfDate: null,
|
||||
periodFrom: "2020-01-01",
|
||||
periodTo: "2020-12-31"
|
||||
});
|
||||
expect(resolveAddressDebugCarryoverFilters(debug)).toEqual({
|
||||
counterparty: "Группа СВК",
|
||||
organization: "ООО Альтернатива Плюс",
|
||||
period_from: "2020-01-01",
|
||||
period_to: "2020-12-31"
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers grounded discovery date scope over stale exact-route date filters in carryover", () => {
|
||||
const debug = {
|
||||
execution_lane: "address_query",
|
||||
extracted_filters: {
|
||||
counterparty: "Группа СВК",
|
||||
period_to: "2026-04-22"
|
||||
},
|
||||
mcp_discovery_response_applied: true,
|
||||
assistant_mcp_discovery_entry_point_v1: {
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
entry_status: "bridge_executed",
|
||||
turn_input: {
|
||||
turn_meaning_ref: {
|
||||
asked_action_family: "payout",
|
||||
explicit_entity_candidates: ["Группа СВК"],
|
||||
explicit_date_scope: "2020"
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
business_fact_answer_allowed: true,
|
||||
pilot: {
|
||||
pilot_scope: "counterparty_supplier_payout_query_movements_v1"
|
||||
},
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
expect(resolveAddressDebugCarryoverFilters(debug)).toEqual({
|
||||
counterparty: "Группа СВК",
|
||||
period_from: "2020-01-01",
|
||||
period_to: "2020-12-31"
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers the resolved entity from grounded entity-resolution discovery for counterparty carryover", () => {
|
||||
const debug = {
|
||||
execution_lane: "living_chat",
|
||||
mcp_discovery_response_applied: true,
|
||||
assistant_mcp_discovery_entry_point_v1: {
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
entry_status: "bridge_executed",
|
||||
turn_input: {
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "entity_resolution",
|
||||
asked_action_family: "search_business_entity",
|
||||
explicit_entity_candidates: ["СВК"]
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
business_fact_answer_allowed: true,
|
||||
pilot: {
|
||||
pilot_scope: "entity_resolution_search_v1",
|
||||
derived_entity_resolution: {
|
||||
requested_entity: "СВК",
|
||||
resolution_status: "resolved",
|
||||
resolved_entity: "Группа СВК",
|
||||
ambiguity_candidates: []
|
||||
}
|
||||
},
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
expect(readAddressDebugCounterparty(debug)).toBe("Группа СВК");
|
||||
expect(resolveAddressDebugCarryoverFilters(debug)).toEqual({
|
||||
counterparty: "Группа СВК"
|
||||
});
|
||||
expect(resolveAddressDebugAnchorContext(debug)).toEqual({
|
||||
anchorType: "counterparty",
|
||||
anchorValue: "Группа СВК"
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves navigation session context through one shared helper", () => {
|
||||
const state = resolveNavigationSessionContextState({
|
||||
session_context: {
|
||||
|
|
|
|||
|
|
@ -133,6 +133,91 @@ describe("assistant living chat runtime adapter", () => {
|
|||
expect(executeLlmChat).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("builds deterministic broad business evaluation summary from grounded continuity instead of replaying lifecycle noise", async () => {
|
||||
const executeLlmChat = vi.fn(async () => "raw-llm");
|
||||
const input = buildRuntimeInput({
|
||||
userMessage: "Как ты оценишь деятельность компании?",
|
||||
modeDecision: { mode: "chat", reason: "unsupported_current_turn_meaning_boundary" },
|
||||
sessionScope: {
|
||||
knownOrganizations: ["ООО Альтернатива Плюс"],
|
||||
selectedOrganization: null,
|
||||
activeOrganization: "ООО Альтернатива Плюс"
|
||||
},
|
||||
sessionItems: [
|
||||
{
|
||||
role: "assistant",
|
||||
debug: {
|
||||
execution_lane: "address_query",
|
||||
answer_grounding_check: {
|
||||
status: "grounded"
|
||||
},
|
||||
detected_intent: "counterparty_activity_lifecycle",
|
||||
extracted_filters: {
|
||||
organization: "ООО Альтернатива Плюс"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
debug: {
|
||||
execution_lane: "living_chat",
|
||||
mcp_discovery_response_applied: true,
|
||||
assistant_mcp_discovery_entry_point_v1: {
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
entry_status: "bridge_executed",
|
||||
turn_input: {
|
||||
turn_meaning_ref: {
|
||||
explicit_entity_candidates: ["Группа СВК"],
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||
explicit_date_scope: "2020"
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
business_fact_answer_allowed: true,
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference"
|
||||
},
|
||||
pilot: {
|
||||
pilot_scope: "counterparty_bidirectional_value_flow_query_movements_v1",
|
||||
derived_bidirectional_value_flow: {
|
||||
net_amount_human_ru: "3 865 501,50 руб.",
|
||||
incoming_customer_revenue: {
|
||||
total_amount_human_ru: "47 628 853,03 руб."
|
||||
},
|
||||
outgoing_supplier_payout: {
|
||||
total_amount_human_ru: "43 763 351,53 руб."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
addressRuntimeMeta: {
|
||||
toolGateReason: "unsupported_current_turn_meaning_boundary",
|
||||
orchestrationContract: {
|
||||
unsupported_current_turn_meaning_boundary: true,
|
||||
assistant_turn_meaning: {
|
||||
unsupported_but_understood_family: "broad_business_evaluation"
|
||||
}
|
||||
}
|
||||
},
|
||||
executeLlmChat
|
||||
});
|
||||
|
||||
const output = await runAssistantLivingChatRuntime(input);
|
||||
|
||||
expect(output.handled).toBe(true);
|
||||
expect(output.chatText.toLowerCase()).toContain("оценка бизнеса");
|
||||
expect(output.chatText).toContain("ООО Альтернатива Плюс");
|
||||
expect(output.chatText).toContain("Группа СВК");
|
||||
expect(output.chatText).toContain("нетто");
|
||||
expect(output.debug?.living_chat_response_source).toBe("deterministic_broad_business_evaluation_contract");
|
||||
expect(executeLlmChat).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("builds deterministic boundary for unsupported current-turn business meaning", async () => {
|
||||
const executeLlmChat = vi.fn(async () => "raw-llm");
|
||||
const input = buildRuntimeInput({
|
||||
|
|
@ -366,6 +451,56 @@ describe("assistant living chat runtime adapter", () => {
|
|||
expect(executeLlmChat).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("builds deterministic memory recap for prior grounded MCP discovery counterparty context", async () => {
|
||||
const executeLlmChat = vi.fn(async () => "raw-llm");
|
||||
const input = buildRuntimeInput({
|
||||
userMessage: "а ты помнишь, что мы выяснили по свк?",
|
||||
modeDecision: { mode: "chat", reason: "memory_recap_followup_detected" },
|
||||
sessionItems: [
|
||||
{
|
||||
role: "assistant",
|
||||
debug: {
|
||||
execution_lane: "living_chat",
|
||||
mcp_discovery_response_applied: true,
|
||||
assistant_mcp_discovery_entry_point_v1: {
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
entry_status: "bridge_executed",
|
||||
turn_input: {
|
||||
turn_meaning_ref: {
|
||||
explicit_entity_candidates: ["Группа СВК"],
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||
explicit_date_scope: "2020"
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
business_fact_answer_allowed: true,
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference"
|
||||
},
|
||||
pilot: {
|
||||
pilot_scope: "counterparty_supplier_payout_query_movements_v1",
|
||||
derived_value_flow: {
|
||||
total_amount_human_ru: "43 763 351,53 руб."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
executeLlmChat
|
||||
});
|
||||
|
||||
const output = await runAssistantLivingChatRuntime(input);
|
||||
|
||||
expect(output.handled).toBe(true);
|
||||
expect(output.chatText).toContain("Группа СВК");
|
||||
expect(output.chatText).toContain("43 763 351,53 руб.");
|
||||
expect(output.debug?.living_chat_response_source).toBe("deterministic_memory_recap_contract");
|
||||
expect(executeLlmChat).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses continuity-backed active organization for organization-fact boundary even when session scope is empty", async () => {
|
||||
const executeLlmChat = vi.fn(async () => "raw-llm");
|
||||
const input = buildRuntimeInput({
|
||||
|
|
@ -434,4 +569,50 @@ describe("assistant living chat runtime adapter", () => {
|
|||
expect(output.debug?.living_chat_response_source).toBe("deterministic_answer_inspection_contract");
|
||||
expect(executeLlmChat).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("builds deterministic answer inspection reply over grounded MCP discovery net answer", async () => {
|
||||
const executeLlmChat = vi.fn(async () => "raw-llm");
|
||||
const input = buildRuntimeInput({
|
||||
userMessage: "что ты имел в виду под нетто по свк?",
|
||||
modeDecision: { mode: "chat", reason: "answer_inspection_followup_detected" },
|
||||
sessionItems: [
|
||||
{
|
||||
role: "assistant",
|
||||
debug: {
|
||||
execution_lane: "living_chat",
|
||||
mcp_discovery_response_applied: true,
|
||||
assistant_mcp_discovery_entry_point_v1: {
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
entry_status: "bridge_executed",
|
||||
turn_input: {
|
||||
turn_meaning_ref: {
|
||||
explicit_entity_candidates: ["Группа СВК"],
|
||||
explicit_date_scope: "2020"
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
business_fact_answer_allowed: true,
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference"
|
||||
},
|
||||
pilot: {
|
||||
pilot_scope: "counterparty_bidirectional_value_flow_query_movements_v1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
executeLlmChat
|
||||
});
|
||||
|
||||
const output = await runAssistantLivingChatRuntime(input);
|
||||
|
||||
expect(output.handled).toBe(true);
|
||||
expect(output.chatText).toContain("Группа СВК");
|
||||
expect(output.chatText).toContain("Нетто");
|
||||
expect(output.debug?.living_chat_response_source).toBe("deterministic_answer_inspection_contract");
|
||||
expect(executeLlmChat).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -31,6 +31,35 @@ function buildSequentialDeps(results: Array<{ rows: Array<Record<string, unknown
|
|||
return { executeAddressMcpQuery };
|
||||
}
|
||||
|
||||
function buildCustomQueryDeps(result: {
|
||||
fetched_rows: number;
|
||||
matched_rows: number;
|
||||
rows: Array<Record<string, unknown>>;
|
||||
raw_rows?: Array<Record<string, unknown>>;
|
||||
error?: string | null;
|
||||
}) {
|
||||
return {
|
||||
executeAddressMcpQuery: vi.fn(async () => ({
|
||||
fetched_rows: result.fetched_rows,
|
||||
matched_rows: result.matched_rows,
|
||||
rows: result.rows,
|
||||
raw_rows: result.raw_rows ?? result.rows,
|
||||
error: result.error ?? null
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
function buildMetadataDeps(rows: Array<Record<string, unknown>>, error: string | null = null) {
|
||||
return {
|
||||
executeAddressMcpMetadata: vi.fn(async () => ({
|
||||
fetched_rows: error ? 0 : rows.length,
|
||||
raw_rows: error ? [] : rows,
|
||||
rows: error ? [] : rows,
|
||||
error
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
describe("assistant MCP discovery answer adapter", () => {
|
||||
it("turns confirmed lifecycle evidence into a human-safe bounded answer draft", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
|
|
@ -78,6 +107,103 @@ describe("assistant MCP discovery answer adapter", () => {
|
|||
expect(draft.must_not_claim).toContain("Do not claim a confirmed business fact when confirmed_facts is empty.");
|
||||
});
|
||||
|
||||
it("turns generic document evidence into a bounded document answer draft", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "documents",
|
||||
asked_action_family: "list_documents",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "document_evidence"
|
||||
}
|
||||
});
|
||||
const pilot = await executeAssistantMcpDiscoveryPilot(
|
||||
planner,
|
||||
buildDeps([{ Period: "2020-01-15T00:00:00", Counterparty: "SVK", Registrar: "Doc1" }])
|
||||
);
|
||||
|
||||
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||
|
||||
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
|
||||
expect(draft.headline).toContain("документ");
|
||||
expect(draft.headline).toContain("2020");
|
||||
expect(draft.headline).toContain("SVK");
|
||||
expect(draft.confirmed_lines).toContain("В 1С найдены строки документов по контрагенту SVK за 2020.");
|
||||
expect(draft.inference_lines).toContain(
|
||||
"Срез документов по контрагенту SVK за 2020 ограничен только подтвержденными строками документов, найденными этим поиском."
|
||||
);
|
||||
expect(draft.unknown_lines).toContain(
|
||||
"Полный исторический срез документов по контрагенту SVK вне периода 2020 этим поиском не подтвержден."
|
||||
);
|
||||
expect(draft.must_not_claim).toContain("Do not claim full document history outside the checked period.");
|
||||
});
|
||||
|
||||
it("turns generic movement evidence into a bounded movement answer draft", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "movements",
|
||||
asked_action_family: "list_movements",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "movement_evidence"
|
||||
}
|
||||
});
|
||||
const pilot = await executeAssistantMcpDiscoveryPilot(
|
||||
planner,
|
||||
buildDeps([{ Period: "2020-01-15T00:00:00", Counterparty: "SVK", Registrar: "Move1" }])
|
||||
);
|
||||
|
||||
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||
|
||||
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
|
||||
expect(draft.headline).toContain("движени");
|
||||
expect(draft.headline).toContain("2020");
|
||||
expect(draft.headline).toContain("SVK");
|
||||
expect(draft.confirmed_lines).toContain("В 1С найдены строки движений по контрагенту SVK за 2020.");
|
||||
expect(draft.inference_lines).toContain(
|
||||
"Срез движений по контрагенту SVK за 2020 ограничен только подтвержденными строками движений, найденными этим поиском."
|
||||
);
|
||||
expect(draft.unknown_lines).toContain(
|
||||
"Полный исторический срез движений по контрагенту SVK вне периода 2020 этим поиском не подтвержден."
|
||||
);
|
||||
expect(draft.must_not_claim).toContain("Do not claim full movement history outside the checked period.");
|
||||
expect(draft.must_not_claim).toContain("Do not present the confirmed movement rows as a complete movement universe.");
|
||||
});
|
||||
|
||||
it("keeps bounded-only movement answers tied to the resolved entity and checked period", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "movements",
|
||||
asked_action_family: "list_movements",
|
||||
explicit_entity_candidates: ["Группа СВК"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "movement_evidence"
|
||||
}
|
||||
});
|
||||
const pilot = await executeAssistantMcpDiscoveryPilot(
|
||||
planner,
|
||||
buildCustomQueryDeps({
|
||||
fetched_rows: 100,
|
||||
matched_rows: 0,
|
||||
rows: [],
|
||||
raw_rows: [{ Period: "2020-06-30T00:00:00", Counterparty: "Группа СВК", Registrar: "Move1" }]
|
||||
})
|
||||
);
|
||||
|
||||
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||
|
||||
expect(draft.answer_mode).toBe("bounded_inference_only");
|
||||
expect(draft.headline).toContain("движени");
|
||||
expect(draft.headline).toContain("Группа СВК");
|
||||
expect(draft.headline).toContain("2020");
|
||||
expect(draft.inference_lines).toContain(
|
||||
"По движениям по контрагенту Группа СВК за 2020 удалось проверить только ограниченный срез 1С; подтвержденных строк движений этим поиском не найдено."
|
||||
);
|
||||
expect(draft.unknown_lines).toContain(
|
||||
"Полный исторический срез движений по контрагенту Группа СВК вне периода 2020 этим поиском не подтвержден."
|
||||
);
|
||||
});
|
||||
|
||||
it("asks for clarification when discovery did not execute due to missing scope", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
|
|
@ -96,6 +222,200 @@ describe("assistant MCP discovery answer adapter", () => {
|
|||
expect(draft.must_not_claim).toContain("Do not claim rows were checked when mcp_execution_performed=false.");
|
||||
});
|
||||
|
||||
it("asks for an explicit lane choice when mixed metadata ambiguity cannot continue on a neutral follow-up", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "metadata",
|
||||
asked_action_family: "resolve_next_lane",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "metadata_lane_choice_clarification"
|
||||
}
|
||||
});
|
||||
const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([]));
|
||||
|
||||
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||
|
||||
expect(draft.answer_mode).toBe("needs_clarification");
|
||||
expect(draft.headline).toContain("data-lane");
|
||||
expect(draft.next_step_line).toContain("по документам");
|
||||
expect(draft.next_step_line).toContain("по движениям/регистрам");
|
||||
expect(draft.must_not_claim).toContain("Do not claim rows were checked when mcp_execution_performed=false.");
|
||||
});
|
||||
|
||||
it("keeps movement clarification anchored to the chosen lane after metadata ambiguity was resolved", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "movements",
|
||||
asked_action_family: "list_movements",
|
||||
explicit_entity_candidates: ["НДС"],
|
||||
unsupported_but_understood_family: "movement_evidence"
|
||||
}
|
||||
});
|
||||
const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([]));
|
||||
|
||||
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||
|
||||
expect(draft.answer_mode).toBe("needs_clarification");
|
||||
expect(draft.headline).toContain("движениям/регистрам");
|
||||
expect(draft.headline).toContain("НДС");
|
||||
expect(draft.headline).toContain("период");
|
||||
expect(draft.next_step_line).toContain("движениям/регистрам");
|
||||
expect(draft.next_step_line).toContain("НДС");
|
||||
expect(draft.next_step_line).toContain("период");
|
||||
});
|
||||
|
||||
it("turns resolved entity grounding into a human-safe entity search answer draft", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "entity_resolution",
|
||||
asked_action_family: "search_business_entity",
|
||||
explicit_entity_candidates: ["Группа СВК"],
|
||||
unsupported_but_understood_family: "entity_resolution"
|
||||
}
|
||||
});
|
||||
const pilot = await executeAssistantMcpDiscoveryPilot(
|
||||
planner,
|
||||
buildDeps([
|
||||
{ Counterparty: "Группа СВК", CounterpartyRef: "Ref-1" },
|
||||
{ Counterparty: "СВК Логистика", CounterpartyRef: "Ref-2" }
|
||||
])
|
||||
);
|
||||
|
||||
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||
|
||||
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
|
||||
expect(draft.headline).toContain("вероятный контрагент");
|
||||
expect(draft.confirmed_lines.join("\n")).toContain("Группа СВК");
|
||||
expect(draft.inference_lines.join("\n")).toContain("заземление сущности");
|
||||
expect(draft.next_step_line).toContain("искать документы, движения или денежный поток");
|
||||
expect(draft.must_not_claim).toContain(
|
||||
"Do not present catalog grounding as confirmed business activity, turnover, or document evidence."
|
||||
);
|
||||
});
|
||||
|
||||
it("asks for clarification when entity grounding stays ambiguous", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "entity_resolution",
|
||||
asked_action_family: "search_business_entity",
|
||||
explicit_entity_candidates: ["СВК"],
|
||||
unsupported_but_understood_family: "entity_resolution"
|
||||
}
|
||||
});
|
||||
const pilot = await executeAssistantMcpDiscoveryPilot(
|
||||
planner,
|
||||
buildDeps([
|
||||
{ Counterparty: "СВК-А", CounterpartyRef: "Ref-1" },
|
||||
{ Counterparty: "СВК-Б", CounterpartyRef: "Ref-2" }
|
||||
])
|
||||
);
|
||||
|
||||
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||
|
||||
expect(draft.answer_mode).toBe("needs_clarification");
|
||||
expect(draft.headline).toContain("несколько похожих контрагентов");
|
||||
expect(draft.inference_lines.join("\n")).toContain("СВК-А");
|
||||
expect(draft.inference_lines.join("\n")).toContain("1. СВК-А");
|
||||
expect(draft.inference_lines.join("\n")).toContain("2. СВК-Б");
|
||||
expect(draft.next_step_line).toContain("какой именно контрагент нужен");
|
||||
expect(draft.next_step_line).toContain("1. СВК-А");
|
||||
expect(draft.next_step_line).toContain("2. СВК-Б");
|
||||
expect(draft.next_step_line).toContain("номером варианта");
|
||||
});
|
||||
|
||||
it.skip("keeps entity search honest when no catalog candidate was confirmed", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "entity_resolution",
|
||||
asked_action_family: "search_business_entity",
|
||||
explicit_entity_candidates: ["Несуществующий Контрагент"],
|
||||
unsupported_but_understood_family: "entity_resolution"
|
||||
}
|
||||
});
|
||||
const pilot = await executeAssistantMcpDiscoveryPilot(
|
||||
planner,
|
||||
buildDeps([{ Counterparty: "Группа СВК", CounterpartyRef: "Ref-1" }])
|
||||
);
|
||||
|
||||
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||
|
||||
expect(draft.answer_mode).toBe("checked_sources_only");
|
||||
expect(draft.headline).toContain("точный контрагент пока не подтвержден");
|
||||
expect(draft.unknown_lines).toContain(
|
||||
'No counterparty matching "Несуществующий Контрагент" was confirmed in the checked 1C catalog slice'
|
||||
);
|
||||
expect(draft.next_step_line).toContain("Дайте точное название или ИНН");
|
||||
});
|
||||
|
||||
it("turns metadata surface evidence into a human-safe metadata answer draft", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "metadata",
|
||||
asked_action_family: "inspect_documents",
|
||||
explicit_entity_candidates: ["НДС"]
|
||||
}
|
||||
});
|
||||
const pilot = await executeAssistantMcpDiscoveryPilot(
|
||||
planner,
|
||||
buildMetadataDeps([
|
||||
{
|
||||
FullName: "Документ.СчетФактураВыданный",
|
||||
MetaType: "Документ",
|
||||
attributes: [{ Name: "Дата" }, { Name: "Организация" }]
|
||||
}
|
||||
])
|
||||
);
|
||||
|
||||
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||
const confirmedText = draft.confirmed_lines.join("\n");
|
||||
|
||||
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
|
||||
expect(draft.headline).toContain("заземлена вероятная поверхность");
|
||||
expect(confirmedText).toContain("Подтвержденная metadata-поверхность 1С");
|
||||
expect(confirmedText).toContain("Документ.СчетФактураВыданный");
|
||||
expect(confirmedText).toContain("Выбранное family: Документ");
|
||||
expect(confirmedText).toContain("Дата");
|
||||
expect(draft.inference_lines.join("\n")).toContain("контур документов");
|
||||
expect(draft.next_step_line).toContain("surface «Документ»");
|
||||
expect(draft.must_not_claim).toContain("Do not present metadata surface as confirmed business data rows.");
|
||||
expect(draft.must_not_claim).toContain("Do not present the inferred next checked lane as already executed data retrieval.");
|
||||
});
|
||||
|
||||
it("keeps metadata answer honest when schema surface stays ambiguous", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "metadata",
|
||||
asked_action_family: "inspect_fields",
|
||||
explicit_entity_candidates: ["НДС"]
|
||||
}
|
||||
});
|
||||
const pilot = await executeAssistantMcpDiscoveryPilot(
|
||||
planner,
|
||||
buildMetadataDeps([
|
||||
{
|
||||
FullName: "Документ.СчетФактураВыданный",
|
||||
MetaType: "Документ",
|
||||
attributes: [{ Name: "Дата" }]
|
||||
},
|
||||
{
|
||||
FullName: "РегистрНакопления.НДСПокупок",
|
||||
MetaType: "РегистрНакопления",
|
||||
resources: [{ Name: "СуммаНДС" }]
|
||||
}
|
||||
])
|
||||
);
|
||||
|
||||
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||
|
||||
expect(draft.headline).toContain("конкурирующие schema-поверхности");
|
||||
expect(draft.inference_lines.join("\n")).toContain("несколько конкурирующих family");
|
||||
expect(draft.unknown_lines).toContain(
|
||||
"Exact downstream metadata surface remains ambiguous across: Документ, РегистрНакопления"
|
||||
);
|
||||
expect(draft.next_step_line).toContain("Документ, РегистрНакопления");
|
||||
});
|
||||
|
||||
it("turns value-flow evidence into a bounded turnover answer draft", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
|
|
@ -117,8 +437,9 @@ describe("assistant MCP discovery answer adapter", () => {
|
|||
const confirmedText = draft.confirmed_lines.join("\n");
|
||||
|
||||
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
|
||||
expect(draft.headline).toContain("денежных движений");
|
||||
expect(draft.headline).toContain("входящих денежных поступлений");
|
||||
expect(confirmedText).toContain("3 750,50 руб.");
|
||||
expect(confirmedText).toContain("входящих денежных поступлений");
|
||||
expect(confirmedText).toContain("2020-01-15");
|
||||
expect(confirmedText).toContain("2020-02-20");
|
||||
expect(draft.unknown_lines).toContain("Full turnover outside the checked period is not proven by this MCP discovery pilot");
|
||||
|
|
@ -191,6 +512,87 @@ describe("assistant MCP discovery answer adapter", () => {
|
|||
expect(draft.must_not_claim).toContain("Do not present a derived sum as a legal/accounting final total outside the checked 1C rows.");
|
||||
});
|
||||
|
||||
it("renders monthly bidirectional breakdown lines when the turn explicitly asked by month", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "net_value_flow",
|
||||
asked_aggregation_axis: "month",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting"
|
||||
}
|
||||
});
|
||||
const pilot = await executeAssistantMcpDiscoveryPilot(
|
||||
planner,
|
||||
buildSequentialDeps([
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-01-15T00:00:00", Amount: 10000, Counterparty: "SVK" },
|
||||
{ Period: "2020-02-20T00:00:00", Amount: "2 500,50", Counterparty: "SVK" }
|
||||
]
|
||||
},
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-01-10T00:00:00", Amount: 4000, Counterparty: "SVK" },
|
||||
{ Period: "2020-02-11T00:00:00", Amount: 1000, Counterparty: "SVK" }
|
||||
]
|
||||
}
|
||||
])
|
||||
);
|
||||
|
||||
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||
const confirmedText = draft.confirmed_lines.join("\n");
|
||||
|
||||
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
|
||||
expect(draft.headline).toContain("помесяч");
|
||||
expect(confirmedText).toContain("Помесячно: янв 2020");
|
||||
expect(confirmedText).toContain("получили 10 000 руб.");
|
||||
expect(confirmedText).toContain("заплатили 4 000 руб.");
|
||||
expect(confirmedText).toContain("Помесячно: фев 2020");
|
||||
expect(confirmedText).toContain("нетто в нашу сторону 1 500,50 руб.");
|
||||
expect(draft.reason_codes).toContain("answer_contains_monthly_breakdown");
|
||||
});
|
||||
|
||||
it("keeps recovered yearly coverage out of the unknown block and explains the recovery as bounded inference", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "payout",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "counterparty_payouts_or_outflow"
|
||||
}
|
||||
});
|
||||
const broadRows = Array.from({ length: 100 }, (_, index) => ({
|
||||
Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`,
|
||||
Amount: 10,
|
||||
Counterparty: "SVK"
|
||||
}));
|
||||
const monthlyResults = Array.from({ length: 12 }, (_, index) => ({
|
||||
rows: [
|
||||
{
|
||||
Period: `2020-${String(index + 1).padStart(2, "0")}-05T00:00:00`,
|
||||
Amount: (index + 1) * 100,
|
||||
Counterparty: "SVK"
|
||||
}
|
||||
]
|
||||
}));
|
||||
const pilot = await executeAssistantMcpDiscoveryPilot(
|
||||
planner,
|
||||
buildSequentialDeps([{ rows: broadRows }, ...monthlyResults])
|
||||
);
|
||||
|
||||
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||
|
||||
expect(draft.inference_lines).toContain(
|
||||
"Requested period coverage was recovered through monthly 1C value-flow probes after the broad probe hit the row limit"
|
||||
);
|
||||
expect(draft.unknown_lines).not.toContain(
|
||||
"Complete requested-period coverage is not proven because the MCP discovery probe row limit was reached"
|
||||
);
|
||||
});
|
||||
|
||||
it("does not leak primitive names or query text into user-facing lines", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
|
|
@ -245,4 +647,27 @@ describe("assistant MCP discovery answer adapter", () => {
|
|||
expect(inferenceText).toContain("не юридически подтвержденный возраст регистрации");
|
||||
expect(draft.reason_codes).toContain("pilot_derived_activity_period_from_confirmed_rows");
|
||||
});
|
||||
|
||||
it("keeps not-found entity search user-facing lines in Russian", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "entity_resolution",
|
||||
asked_action_family: "search_business_entity",
|
||||
explicit_entity_candidates: ["\u041d\u0435\u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u041a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442"],
|
||||
unsupported_but_understood_family: "entity_resolution"
|
||||
}
|
||||
});
|
||||
const pilot = await executeAssistantMcpDiscoveryPilot(
|
||||
planner,
|
||||
buildDeps([{ Counterparty: "\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a", CounterpartyRef: "Ref-1" }])
|
||||
);
|
||||
|
||||
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||
const unknownText = draft.unknown_lines.join("\n");
|
||||
|
||||
expect(draft.answer_mode).toBe("checked_sources_only");
|
||||
expect(unknownText).toContain("\u043d\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442");
|
||||
expect(unknownText).toContain("\u041d\u0435\u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u041a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442");
|
||||
expect(unknownText).toContain("\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b, \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f \u0438 \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u0435\u043b\u0438");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -30,6 +30,17 @@ function buildSequentialDeps(results: Array<{ rows: Array<Record<string, unknown
|
|||
return { executeAddressMcpQuery };
|
||||
}
|
||||
|
||||
function buildMetadataDeps(rows: Array<Record<string, unknown>>, error: string | null = null) {
|
||||
return {
|
||||
executeAddressMcpMetadata: vi.fn(async () => ({
|
||||
fetched_rows: error ? 0 : rows.length,
|
||||
raw_rows: error ? [] : rows,
|
||||
rows: error ? [] : rows,
|
||||
error
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
describe("assistant MCP discovery pilot executor", () => {
|
||||
it("executes only the lifecycle query_documents primitive through injected MCP deps", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
|
|
@ -92,6 +103,320 @@ describe("assistant MCP discovery pilot executor", () => {
|
|||
expect(deps.executeAddressMcpQuery).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses the explicit selected chain id when choosing the movement pilot scope", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "movements",
|
||||
asked_action_family: "list_movements",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "movement_evidence"
|
||||
}
|
||||
});
|
||||
const deps = buildDeps([{ Period: "2020-01-15T00:00:00", Amount: 1250, Counterparty: "SVK", Registrar: "Move1" }]);
|
||||
|
||||
const result = await executeAssistantMcpDiscoveryPilot(
|
||||
{
|
||||
...planner,
|
||||
reason_codes: planner.reason_codes.filter((code) => !code.startsWith("planner_selected_"))
|
||||
},
|
||||
deps
|
||||
);
|
||||
|
||||
expect(result.pilot_status).toBe("executed");
|
||||
expect(result.pilot_scope).toBe("counterparty_movement_evidence_query_movements_v1");
|
||||
expect(result.executed_primitives).toEqual(["query_movements"]);
|
||||
});
|
||||
|
||||
it("executes generic document evidence through query_documents", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "documents",
|
||||
asked_action_family: "list_documents",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "document_evidence"
|
||||
}
|
||||
});
|
||||
const deps = buildDeps([
|
||||
{ Period: "2020-01-15T00:00:00", Counterparty: "SVK", Registrar: "Doc1" },
|
||||
{ Period: "2020-03-20T00:00:00", Counterparty: "SVK", Registrar: "Doc2" }
|
||||
]);
|
||||
|
||||
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
|
||||
|
||||
expect(result.pilot_status).toBe("executed");
|
||||
expect(result.pilot_scope).toBe("counterparty_document_evidence_query_documents_v1");
|
||||
expect(result.executed_primitives).toEqual(["query_documents"]);
|
||||
expect(result.evidence.confirmed_facts).toContain("В 1С найдены строки документов по контрагенту SVK за 2020.");
|
||||
expect(result.evidence.inferred_facts).toContain(
|
||||
"Срез документов по контрагенту SVK за 2020 ограничен только подтвержденными строками документов, найденными этим поиском."
|
||||
);
|
||||
expect(result.evidence.unknown_facts).toContain(
|
||||
"Полный исторический срез документов по контрагенту SVK вне периода 2020 этим поиском не подтвержден."
|
||||
);
|
||||
expect(result.source_rows_summary).toBe("2 MCP document rows fetched, 2 matched document scope");
|
||||
});
|
||||
|
||||
it("executes generic movement evidence through query_movements without deriving turnover totals", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "movements",
|
||||
asked_action_family: "list_movements",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "movement_evidence"
|
||||
}
|
||||
});
|
||||
const deps = buildDeps([
|
||||
{ Period: "2020-01-15T00:00:00", Amount: 1250, Counterparty: "SVK", Registrar: "Move1" },
|
||||
{ Period: "2020-03-20T00:00:00", Amount: "900,25", Counterparty: "SVK", Registrar: "Move2" }
|
||||
]);
|
||||
|
||||
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
|
||||
|
||||
expect(result.pilot_status).toBe("executed");
|
||||
expect(result.pilot_scope).toBe("counterparty_movement_evidence_query_movements_v1");
|
||||
expect(result.executed_primitives).toEqual(["query_movements"]);
|
||||
expect(result.derived_value_flow).toBeNull();
|
||||
expect(result.derived_bidirectional_value_flow).toBeNull();
|
||||
expect(result.evidence.confirmed_facts).toContain("В 1С найдены строки движений по контрагенту SVK за 2020.");
|
||||
expect(result.evidence.inferred_facts).toContain(
|
||||
"Срез движений по контрагенту SVK за 2020 ограничен только подтвержденными строками движений, найденными этим поиском."
|
||||
);
|
||||
expect(result.evidence.unknown_facts).toContain(
|
||||
"Полный исторический срез движений по контрагенту SVK вне периода 2020 этим поиском не подтвержден."
|
||||
);
|
||||
expect(result.source_rows_summary).toBe("2 MCP movement rows fetched, 2 matched movement scope");
|
||||
|
||||
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(1);
|
||||
const call = deps.executeAddressMcpQuery.mock.calls[0]?.[0];
|
||||
expect(String(call?.query ?? "")).toContain("Документ.СписаниеСРасчетногоСчета");
|
||||
expect(String(call?.query ?? "")).toContain("Документ.ПоступлениеНаРасчетныйСчет");
|
||||
expect(call?.limit).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("executes inspect_1c_metadata and derives a confirmed metadata surface", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "metadata",
|
||||
asked_action_family: "inspect_documents",
|
||||
explicit_entity_candidates: ["НДС"]
|
||||
}
|
||||
});
|
||||
const deps = buildMetadataDeps([
|
||||
{
|
||||
FullName: "Документ.СчетФактураВыданный",
|
||||
MetaType: "Документ",
|
||||
attributes: [{ Name: "Дата" }, { Name: "Организация" }]
|
||||
},
|
||||
{
|
||||
FullName: "Документ.СчетФактураПолученный",
|
||||
MetaType: "Документ",
|
||||
attributes: [{ Name: "Контрагент" }]
|
||||
}
|
||||
]);
|
||||
|
||||
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
|
||||
|
||||
expect(result.pilot_status).toBe("executed");
|
||||
expect(result.pilot_scope).toBe("metadata_inspection_v1");
|
||||
expect(result.mcp_execution_performed).toBe(true);
|
||||
expect(result.executed_primitives).toEqual(["inspect_1c_metadata"]);
|
||||
expect(result.evidence.evidence_status).toBe("confirmed");
|
||||
expect(result.source_rows_summary).toBe("2 MCP metadata rows fetched");
|
||||
expect(result.derived_metadata_surface).toMatchObject({
|
||||
metadata_scope: "НДС",
|
||||
requested_meta_types: ["Документ"],
|
||||
matched_rows: 2,
|
||||
available_entity_sets: ["Документ"],
|
||||
matched_objects: ["Документ.СчетФактураВыданный", "Документ.СчетФактураПолученный"],
|
||||
selected_entity_set: "Документ",
|
||||
selected_surface_objects: ["Документ.СчетФактураВыданный", "Документ.СчетФактураПолученный"],
|
||||
downstream_route_family: "document_evidence",
|
||||
recommended_next_primitive: "query_documents",
|
||||
ambiguity_detected: false,
|
||||
ambiguity_entity_sets: [],
|
||||
available_fields: ["Дата", "Организация", "Контрагент"],
|
||||
inference_basis: "confirmed_1c_metadata_surface_rows"
|
||||
});
|
||||
expect(result.reason_codes).toContain("pilot_inspect_1c_metadata_mcp_executed");
|
||||
expect(result.reason_codes).toContain("pilot_derived_metadata_surface_from_confirmed_rows");
|
||||
expect(deps.executeAddressMcpMetadata).toHaveBeenCalledTimes(1);
|
||||
expect(deps.executeAddressMcpMetadata.mock.calls[0]?.[0]).toMatchObject({
|
||||
meta_type: ["Документ"],
|
||||
name_mask: "НДС"
|
||||
});
|
||||
});
|
||||
|
||||
it("executes the full entity-resolution chain through the checked counterparty catalog slice", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "entity_resolution",
|
||||
asked_action_family: "search_business_entity",
|
||||
explicit_entity_candidates: ["Группа СВК"],
|
||||
unsupported_but_understood_family: "entity_resolution"
|
||||
}
|
||||
});
|
||||
const deps = buildDeps([
|
||||
{ Counterparty: "Группа СВК", CounterpartyRef: "Ref-1" },
|
||||
{ Counterparty: "СВК Логистика", CounterpartyRef: "Ref-2" }
|
||||
]);
|
||||
|
||||
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
|
||||
|
||||
expect(result.pilot_status).toBe("executed");
|
||||
expect(result.pilot_scope).toBe("entity_resolution_search_v1");
|
||||
expect(result.mcp_execution_performed).toBe(true);
|
||||
expect(result.executed_primitives).toEqual([
|
||||
"search_business_entity",
|
||||
"resolve_entity_reference",
|
||||
"probe_coverage"
|
||||
]);
|
||||
expect(result.skipped_primitives).toEqual([]);
|
||||
expect(result.derived_entity_resolution).toMatchObject({
|
||||
requested_entity: "Группа СВК",
|
||||
resolution_status: "resolved",
|
||||
resolved_entity: "Группа СВК",
|
||||
resolved_reference: "Ref-1",
|
||||
confidence: "high",
|
||||
inference_basis: "catalog_counterparty_search_rows"
|
||||
});
|
||||
expect(result.evidence.confirmed_facts).toContain(
|
||||
"В проверенном каталожном срезе 1С найден контрагент: Группа СВК"
|
||||
);
|
||||
expect(result.evidence.inferred_facts).toContain(
|
||||
"Пока проверено только заземление сущности по каталогу 1С; документы, движения и денежные показатели еще не проверялись"
|
||||
);
|
||||
expect(result.evidence.unknown_facts).toContain(
|
||||
"Документы, движения и денежные показатели по этому контрагенту еще не проверялись; пока был только каталожный поиск"
|
||||
);
|
||||
expect(result.reason_codes).toContain("pilot_search_business_entity_mcp_executed");
|
||||
expect(result.reason_codes).toContain("pilot_resolve_entity_reference_from_catalog_rows");
|
||||
expect(result.reason_codes).toContain("pilot_probe_coverage_executed_for_entity_resolution");
|
||||
expect(result.reason_codes).toContain("pilot_entity_resolution_grounding_stable_for_downstream_probe");
|
||||
expect(result.reason_codes).toContain("pilot_derived_entity_resolution_from_catalog_rows");
|
||||
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("keeps entity-resolution honest when several catalog candidates remain ambiguous", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "entity_resolution",
|
||||
asked_action_family: "search_business_entity",
|
||||
explicit_entity_candidates: ["СВК"],
|
||||
unsupported_but_understood_family: "entity_resolution"
|
||||
}
|
||||
});
|
||||
const deps = buildDeps([
|
||||
{ Counterparty: "СВК-А", CounterpartyRef: "Ref-1" },
|
||||
{ Counterparty: "СВК-Б", CounterpartyRef: "Ref-2" }
|
||||
]);
|
||||
|
||||
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
|
||||
|
||||
expect(result.pilot_status).toBe("executed");
|
||||
expect(result.pilot_scope).toBe("entity_resolution_search_v1");
|
||||
expect(result.executed_primitives).toEqual([
|
||||
"search_business_entity",
|
||||
"resolve_entity_reference",
|
||||
"probe_coverage"
|
||||
]);
|
||||
expect(result.skipped_primitives).toEqual([]);
|
||||
expect(result.derived_entity_resolution).toMatchObject({
|
||||
requested_entity: "СВК",
|
||||
resolution_status: "ambiguous",
|
||||
resolved_entity: null,
|
||||
ambiguity_candidates: ["СВК-А", "СВК-Б"],
|
||||
confidence: "low"
|
||||
});
|
||||
expect(result.evidence.confirmed_facts).toEqual([]);
|
||||
expect(result.evidence.unknown_facts).toContain(
|
||||
"Точное заземление контрагента в 1С остается неоднозначным между вариантами: СВК-А, СВК-Б"
|
||||
);
|
||||
expect(result.reason_codes).toContain("pilot_resolve_entity_reference_requires_clarification");
|
||||
expect(result.reason_codes).toContain("pilot_probe_coverage_executed_for_entity_resolution");
|
||||
expect(result.reason_codes).toContain("pilot_entity_resolution_ambiguity_requires_clarification");
|
||||
});
|
||||
|
||||
it("keeps metadata grounding ambiguous when several surface families compete", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "metadata",
|
||||
asked_action_family: "inspect_fields",
|
||||
explicit_entity_candidates: ["НДС"]
|
||||
}
|
||||
});
|
||||
const deps = buildMetadataDeps([
|
||||
{
|
||||
FullName: "Документ.СчетФактураВыданный",
|
||||
MetaType: "Документ",
|
||||
attributes: [{ Name: "Дата" }]
|
||||
},
|
||||
{
|
||||
FullName: "РегистрНакопления.НДСПокупок",
|
||||
MetaType: "РегистрНакопления",
|
||||
resources: [{ Name: "СуммаНДС" }]
|
||||
}
|
||||
]);
|
||||
|
||||
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
|
||||
|
||||
expect(result.pilot_status).toBe("executed");
|
||||
expect(result.derived_metadata_surface).toMatchObject({
|
||||
metadata_scope: "НДС",
|
||||
available_entity_sets: ["Документ", "РегистрНакопления"],
|
||||
selected_entity_set: null,
|
||||
downstream_route_family: null,
|
||||
recommended_next_primitive: null,
|
||||
ambiguity_detected: true,
|
||||
ambiguity_entity_sets: ["Документ", "РегистрНакопления"]
|
||||
});
|
||||
expect(result.evidence.inferred_facts).toEqual([]);
|
||||
expect(result.evidence.unknown_facts).toContain(
|
||||
"Exact downstream metadata surface remains ambiguous across: Документ, РегистрНакопления"
|
||||
);
|
||||
});
|
||||
|
||||
it("infers metadata entity-set families from object names when meta type columns are absent", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "metadata",
|
||||
asked_action_family: "inspect_surface",
|
||||
explicit_entity_candidates: ["НДС"]
|
||||
}
|
||||
});
|
||||
const deps = buildMetadataDeps([
|
||||
{
|
||||
FullName: "Документ.СчетФактураВыданный",
|
||||
attributes: [{ Name: "Дата" }]
|
||||
},
|
||||
{
|
||||
FullName: "РегистрНакопления.НДСПокупок",
|
||||
resources: [{ Name: "СуммаНДС" }]
|
||||
},
|
||||
{
|
||||
FullName: "Справочник.КодыОперацийНДС"
|
||||
}
|
||||
]);
|
||||
|
||||
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
|
||||
|
||||
expect(result.pilot_status).toBe("executed");
|
||||
expect(result.derived_metadata_surface).toMatchObject({
|
||||
metadata_scope: "НДС",
|
||||
available_entity_sets: ["Документ", "РегистрНакопления", "Справочник"],
|
||||
selected_entity_set: null,
|
||||
downstream_route_family: null,
|
||||
recommended_next_primitive: null,
|
||||
ambiguity_detected: true,
|
||||
ambiguity_entity_sets: ["Документ", "РегистрНакопления", "Справочник"]
|
||||
});
|
||||
expect(result.evidence.unknown_facts).toContain(
|
||||
"Exact downstream metadata surface remains ambiguous across: Документ, РегистрНакопления, Справочник"
|
||||
);
|
||||
});
|
||||
|
||||
it("executes value-flow query_movements and derives a guarded turnover sum", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
|
|
@ -199,6 +524,56 @@ describe("assistant MCP discovery pilot executor", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("recovers yearly value-flow coverage by splitting a limited broad probe into monthly subprobes", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "payout",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "counterparty_payouts_or_outflow"
|
||||
}
|
||||
});
|
||||
const broadRows = Array.from({ length: 100 }, (_, index) => ({
|
||||
Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`,
|
||||
Amount: 10,
|
||||
Counterparty: "SVK"
|
||||
}));
|
||||
const monthlyResults = Array.from({ length: 12 }, (_, index) => ({
|
||||
rows: [
|
||||
{
|
||||
Period: `2020-${String(index + 1).padStart(2, "0")}-05T00:00:00`,
|
||||
Amount: (index + 1) * 100,
|
||||
Counterparty: "SVK"
|
||||
}
|
||||
]
|
||||
}));
|
||||
|
||||
const result = await executeAssistantMcpDiscoveryPilot(
|
||||
planner,
|
||||
buildSequentialDeps([{ rows: broadRows }, ...monthlyResults])
|
||||
);
|
||||
|
||||
expect(result.derived_value_flow).toMatchObject({
|
||||
value_flow_direction: "outgoing_supplier_payout",
|
||||
coverage_limited_by_probe_limit: false,
|
||||
coverage_recovered_by_period_chunking: true,
|
||||
period_chunking_granularity: "month",
|
||||
rows_matched: 12,
|
||||
rows_with_amount: 12,
|
||||
total_amount: 7800,
|
||||
first_movement_date: "2020-01-05",
|
||||
latest_movement_date: "2020-12-05"
|
||||
});
|
||||
expect(result.evidence.inferred_facts).toContain(
|
||||
"Requested period coverage was recovered through monthly 1C value-flow probes after the broad probe hit the row limit"
|
||||
);
|
||||
expect(result.evidence.unknown_facts).not.toContain(
|
||||
"Complete requested-period coverage is not proven because the MCP discovery probe row limit was reached"
|
||||
);
|
||||
expect(result.reason_codes).toContain("pilot_monthly_period_chunking_recovered_coverage");
|
||||
});
|
||||
|
||||
it("executes bidirectional value-flow queries and derives guarded net cash flow", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
|
|
@ -260,7 +635,129 @@ describe("assistant MCP discovery pilot executor", () => {
|
|||
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("keeps non-lifecycle ready plans unsupported until a dedicated pilot exists", async () => {
|
||||
it("derives monthly bidirectional value-flow breakdown when the turn explicitly asks by month", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "net_value_flow",
|
||||
asked_aggregation_axis: "month",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting"
|
||||
}
|
||||
});
|
||||
const deps = buildSequentialDeps([
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-01-15T00:00:00", Amount: 10000, Counterparty: "SVK" },
|
||||
{ Period: "2020-02-20T00:00:00", Amount: "2 500,50", Counterparty: "SVK" }
|
||||
]
|
||||
},
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-01-10T00:00:00", Amount: 4000, Counterparty: "SVK" },
|
||||
{ Period: "2020-02-11T00:00:00", Amount: 1000, Counterparty: "SVK" }
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
|
||||
|
||||
expect(result.derived_bidirectional_value_flow?.aggregation_axis).toBe("month");
|
||||
expect(result.derived_bidirectional_value_flow?.monthly_breakdown).toMatchObject([
|
||||
{
|
||||
month_bucket: "2020-01",
|
||||
incoming_total_amount: 10000,
|
||||
incoming_rows_with_amount: 1,
|
||||
outgoing_total_amount: 4000,
|
||||
outgoing_rows_with_amount: 1,
|
||||
net_amount: 6000,
|
||||
net_direction: "net_incoming"
|
||||
},
|
||||
{
|
||||
month_bucket: "2020-02",
|
||||
incoming_total_amount: 2500.5,
|
||||
incoming_rows_with_amount: 1,
|
||||
outgoing_total_amount: 1000,
|
||||
outgoing_rows_with_amount: 1,
|
||||
net_amount: 1500.5,
|
||||
net_direction: "net_incoming"
|
||||
}
|
||||
]);
|
||||
expect(result.derived_bidirectional_value_flow?.monthly_breakdown[0]?.incoming_total_amount_human_ru).toContain("10 000");
|
||||
expect(result.derived_bidirectional_value_flow?.monthly_breakdown[0]?.net_amount_human_ru).toContain("6 000");
|
||||
expect(result.derived_bidirectional_value_flow?.monthly_breakdown[1]?.incoming_total_amount_human_ru).toContain("2 500,50");
|
||||
expect(result.derived_bidirectional_value_flow?.monthly_breakdown[1]?.net_amount_human_ru).toContain("1 500,50");
|
||||
expect(result.evidence.inferred_facts).toContain(
|
||||
"Counterparty monthly net value-flow breakdown was grouped by month over confirmed incoming and outgoing 1C rows"
|
||||
);
|
||||
expect(result.reason_codes).toContain("pilot_derived_bidirectional_monthly_breakdown_from_confirmed_rows");
|
||||
});
|
||||
|
||||
it("recovers bidirectional yearly coverage when one side is rebuilt from monthly subprobes", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "net_value_flow",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting"
|
||||
}
|
||||
});
|
||||
const outgoingBroadRows = Array.from({ length: 100 }, (_, index) => ({
|
||||
Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`,
|
||||
Amount: 10,
|
||||
Counterparty: "SVK"
|
||||
}));
|
||||
const outgoingMonthlyResults = Array.from({ length: 12 }, (_, index) => ({
|
||||
rows: [
|
||||
{
|
||||
Period: `2020-${String(index + 1).padStart(2, "0")}-10T00:00:00`,
|
||||
Amount: (index + 1) * 50,
|
||||
Counterparty: "SVK"
|
||||
}
|
||||
]
|
||||
}));
|
||||
const deps = buildSequentialDeps([
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-01-15T00:00:00", Amount: 10000, Counterparty: "SVK" },
|
||||
{ Period: "2020-02-20T00:00:00", Amount: 10000, Counterparty: "SVK" }
|
||||
]
|
||||
},
|
||||
{ rows: outgoingBroadRows },
|
||||
...outgoingMonthlyResults
|
||||
]);
|
||||
|
||||
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
|
||||
|
||||
expect(result.derived_bidirectional_value_flow).toMatchObject({
|
||||
coverage_limited_by_probe_limit: false,
|
||||
coverage_recovered_by_period_chunking: true,
|
||||
period_chunking_granularity: "month",
|
||||
net_amount: 16100,
|
||||
incoming_customer_revenue: {
|
||||
total_amount: 20000,
|
||||
coverage_limited_by_probe_limit: false
|
||||
},
|
||||
outgoing_supplier_payout: {
|
||||
total_amount: 3900,
|
||||
coverage_limited_by_probe_limit: false,
|
||||
coverage_recovered_by_period_chunking: true,
|
||||
period_chunking_granularity: "month"
|
||||
}
|
||||
});
|
||||
expect(result.evidence.inferred_facts).toContain(
|
||||
"Requested period coverage for bidirectional value-flow was recovered through monthly 1C side probes after a broad probe hit the row limit"
|
||||
);
|
||||
expect(result.evidence.unknown_facts).not.toContain(
|
||||
"Complete requested-period coverage for bidirectional value-flow is not proven because at least one MCP discovery probe row limit was reached"
|
||||
);
|
||||
expect(result.reason_codes).toContain("pilot_bidirectional_outgoing_monthly_period_chunking_recovered_coverage");
|
||||
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(14);
|
||||
});
|
||||
|
||||
it("executes document-ready plans through the dedicated document pilot", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_documents",
|
||||
|
|
@ -272,11 +769,62 @@ describe("assistant MCP discovery pilot executor", () => {
|
|||
|
||||
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
|
||||
|
||||
expect(result.pilot_status).toBe("unsupported");
|
||||
expect(result.mcp_execution_performed).toBe(false);
|
||||
expect(result.skipped_primitives).toEqual(["resolve_entity_reference", "query_documents", "probe_coverage"]);
|
||||
expect(result.reason_codes).toContain("pilot_scope_unsupported_for_live_execution");
|
||||
expect(deps.executeAddressMcpQuery).not.toHaveBeenCalled();
|
||||
expect(result.pilot_status).toBe("executed");
|
||||
expect(result.pilot_scope).toBe("counterparty_document_evidence_query_documents_v1");
|
||||
expect(result.mcp_execution_performed).toBe(true);
|
||||
expect(result.executed_primitives).toEqual(["query_documents"]);
|
||||
expect(result.reason_codes).toContain("pilot_query_documents_mcp_executed");
|
||||
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("keeps document and movement evidence scoped to the resolved entity and checked period", async () => {
|
||||
const documentPlanner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "documents",
|
||||
asked_action_family: "list_documents",
|
||||
explicit_entity_candidates: ["Группа СВК"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "document_evidence"
|
||||
}
|
||||
});
|
||||
const documentResult = await executeAssistantMcpDiscoveryPilot(
|
||||
documentPlanner,
|
||||
buildDeps([{ Period: "2020-01-15T00:00:00", Counterparty: "Группа СВК", Registrar: "Doc1" }])
|
||||
);
|
||||
|
||||
expect(documentResult.evidence.confirmed_facts).toContain(
|
||||
"В 1С найдены строки документов по контрагенту Группа СВК за 2020."
|
||||
);
|
||||
expect(documentResult.evidence.inferred_facts).toContain(
|
||||
"Срез документов по контрагенту Группа СВК за 2020 ограничен только подтвержденными строками документов, найденными этим поиском."
|
||||
);
|
||||
expect(documentResult.evidence.unknown_facts).toContain(
|
||||
"Полный исторический срез документов по контрагенту Группа СВК вне периода 2020 этим поиском не подтвержден."
|
||||
);
|
||||
|
||||
const movementPlanner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "movements",
|
||||
asked_action_family: "list_movements",
|
||||
explicit_entity_candidates: ["Группа СВК"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "movement_evidence"
|
||||
}
|
||||
});
|
||||
const movementResult = await executeAssistantMcpDiscoveryPilot(
|
||||
movementPlanner,
|
||||
buildDeps([{ Period: "2020-01-15T00:00:00", Counterparty: "Группа СВК", Registrar: "Move1" }])
|
||||
);
|
||||
|
||||
expect(movementResult.evidence.confirmed_facts).toContain(
|
||||
"В 1С найдены строки движений по контрагенту Группа СВК за 2020."
|
||||
);
|
||||
expect(movementResult.evidence.inferred_facts).toContain(
|
||||
"Срез движений по контрагенту Группа СВК за 2020 ограничен только подтвержденными строками движений, найденными этим поиском."
|
||||
);
|
||||
expect(movementResult.evidence.unknown_facts).toContain(
|
||||
"Полный исторический срез движений по контрагенту Группа СВК вне периода 2020 этим поиском не подтвержден."
|
||||
);
|
||||
});
|
||||
|
||||
it("records MCP errors as limitations without converting them into facts", async () => {
|
||||
|
|
@ -299,4 +847,54 @@ describe("assistant MCP discovery pilot executor", () => {
|
|||
expect(result.query_limitations).toContain("MCP fetch failed: timeout");
|
||||
expect(result.reason_codes).toContain("pilot_query_documents_mcp_error");
|
||||
});
|
||||
|
||||
it("emits Russian confirmed and bounded facts for resolved entity grounding", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "entity_resolution",
|
||||
asked_action_family: "search_business_entity",
|
||||
explicit_entity_candidates: ["\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a"],
|
||||
unsupported_but_understood_family: "entity_resolution"
|
||||
}
|
||||
});
|
||||
const deps = buildDeps([
|
||||
{ Counterparty: "\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a", CounterpartyRef: "Ref-1" },
|
||||
{ Counterparty: "\u0421\u0412\u041a \u041b\u043e\u0433\u0438\u0441\u0442\u0438\u043a\u0430", CounterpartyRef: "Ref-2" }
|
||||
]);
|
||||
|
||||
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
|
||||
|
||||
expect(result.evidence.confirmed_facts.join("\n")).toContain("\u043d\u0430\u0439\u0434\u0435\u043d \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442");
|
||||
expect(result.evidence.confirmed_facts.join("\n")).toContain("\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a");
|
||||
expect(result.evidence.inferred_facts.join("\n")).toContain(
|
||||
"\u041f\u043e\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u043d\u043e \u0442\u043e\u043b\u044c\u043a\u043e \u0437\u0430\u0437\u0435\u043c\u043b\u0435\u043d\u0438\u0435 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0438 \u043f\u043e \u043a\u0430\u0442\u0430\u043b\u043e\u0433\u0443 1\u0421"
|
||||
);
|
||||
expect(result.evidence.unknown_facts.join("\n")).toContain(
|
||||
"\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b, \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f \u0438 \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u0435\u043b\u0438"
|
||||
);
|
||||
});
|
||||
|
||||
it("emits Russian ambiguity boundaries for entity grounding", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "entity_resolution",
|
||||
asked_action_family: "search_business_entity",
|
||||
explicit_entity_candidates: ["\u0421\u0412\u041a"],
|
||||
unsupported_but_understood_family: "entity_resolution"
|
||||
}
|
||||
});
|
||||
const deps = buildDeps([
|
||||
{ Counterparty: "\u0421\u0412\u041a-\u0410", CounterpartyRef: "Ref-1" },
|
||||
{ Counterparty: "\u0421\u0412\u041a-\u0411", CounterpartyRef: "Ref-2" }
|
||||
]);
|
||||
|
||||
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
|
||||
|
||||
expect(result.evidence.confirmed_facts).toEqual([]);
|
||||
expect(result.evidence.unknown_facts.join("\n")).toContain(
|
||||
"\u0422\u043e\u0447\u043d\u043e\u0435 \u0437\u0430\u0437\u0435\u043c\u043b\u0435\u043d\u0438\u0435 \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430 \u0432 1\u0421 \u043e\u0441\u0442\u0430\u0435\u0442\u0441\u044f \u043d\u0435\u043e\u0434\u043d\u043e\u0437\u043d\u0430\u0447\u043d\u044b\u043c"
|
||||
);
|
||||
expect(result.evidence.unknown_facts.join("\n")).toContain("\u0421\u0412\u041a-\u0410");
|
||||
expect(result.evidence.unknown_facts.join("\n")).toContain("\u0421\u0412\u041a-\u0411");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ describe("assistant MCP discovery planner", () => {
|
|||
|
||||
expect(result.planner_status).toBe("ready_for_execution");
|
||||
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
|
||||
expect(result.selected_chain_id).toBe("value_flow");
|
||||
expect(result.selected_chain_summary).toContain("query scoped movements");
|
||||
expect(result.proposed_primitives).toEqual([
|
||||
"resolve_entity_reference",
|
||||
"query_movements",
|
||||
|
|
@ -23,6 +25,8 @@ describe("assistant MCP discovery planner", () => {
|
|||
expect(result.required_axes).toEqual(["counterparty", "period", "aggregate_axis", "amount", "coverage_target"]);
|
||||
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.execution_budget.max_probe_count).toBe(30);
|
||||
expect(result.reason_codes).toContain("planner_enabled_chunked_coverage_probe_budget");
|
||||
});
|
||||
|
||||
it("keeps a value-flow plan in clarification state when period axis is missing", () => {
|
||||
|
|
@ -40,6 +44,36 @@ describe("assistant MCP discovery planner", () => {
|
|||
expect(result.reason_codes).toContain("planner_needs_more_user_or_scope_context");
|
||||
});
|
||||
|
||||
it("keeps requested monthly aggregation as an explicit planning axis for value-flow discovery", () => {
|
||||
const result = planAssistantMcpDiscovery({
|
||||
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.planner_status).toBe("ready_for_execution");
|
||||
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_recipe");
|
||||
expect(result.discovery_plan.execution_budget.max_probe_count).toBe(30);
|
||||
});
|
||||
|
||||
it("builds a document discovery plan without falling back to movement primitives", () => {
|
||||
const result = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
|
|
@ -55,6 +89,27 @@ describe("assistant MCP discovery planner", () => {
|
|||
expect(result.required_axes).toEqual(["counterparty", "coverage_target"]);
|
||||
});
|
||||
|
||||
it("builds a movement discovery plan without aggregating value-flow totals", () => {
|
||||
const result = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "movements",
|
||||
asked_action_family: "list_movements",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "movement_evidence"
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.planner_status).toBe("ready_for_execution");
|
||||
expect(result.semantic_data_need).toBe("movement evidence");
|
||||
expect(result.selected_chain_id).toBe("movement_evidence");
|
||||
expect(result.selected_chain_summary).toContain("movement rows");
|
||||
expect(result.proposed_primitives).toEqual(["resolve_entity_reference", "query_movements", "probe_coverage"]);
|
||||
expect(result.proposed_primitives).not.toContain("aggregate_by_axis");
|
||||
expect(result.required_axes).toEqual(["counterparty", "period", "coverage_target"]);
|
||||
expect(result.reason_codes).toContain("planner_selected_movement_recipe");
|
||||
});
|
||||
|
||||
it("builds an inference-safe lifecycle plan with evidence explanation", () => {
|
||||
const result = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
|
|
@ -88,6 +143,58 @@ describe("assistant MCP discovery planner", () => {
|
|||
expect(result.catalog_review.evidence_floors.inspect_1c_metadata).toBe("source_summary");
|
||||
});
|
||||
|
||||
it("keeps broad metadata surface inspection on inspect_1c_metadata", () => {
|
||||
const result = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "metadata",
|
||||
asked_action_family: "inspect_surface",
|
||||
explicit_entity_candidates: ["НДС"]
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.planner_status).toBe("ready_for_execution");
|
||||
expect(result.semantic_data_need).toBe("1C metadata evidence");
|
||||
expect(result.proposed_primitives).toEqual(["inspect_1c_metadata"]);
|
||||
expect(result.proposed_primitives).not.toContain("query_documents");
|
||||
expect(result.proposed_primitives).not.toContain("query_movements");
|
||||
expect(result.reason_codes).toContain("planner_selected_metadata_recipe");
|
||||
});
|
||||
|
||||
it("keeps metadata document inspection on inspect_1c_metadata instead of query_documents", () => {
|
||||
const result = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "metadata",
|
||||
asked_action_family: "inspect_documents"
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.planner_status).toBe("ready_for_execution");
|
||||
expect(result.proposed_primitives).toEqual(["inspect_1c_metadata"]);
|
||||
expect(result.proposed_primitives).not.toContain("query_documents");
|
||||
});
|
||||
|
||||
it("keeps metadata lane-choice clarification in needs_clarification without launching MCP primitives", () => {
|
||||
const result = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "metadata",
|
||||
asked_action_family: "resolve_next_lane",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "metadata_lane_choice_clarification"
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.planner_status).toBe("needs_clarification");
|
||||
expect(result.semantic_data_need).toBe("metadata lane clarification");
|
||||
expect(result.selected_chain_id).toBe("metadata_lane_clarification");
|
||||
expect(result.selected_chain_summary).toContain("choose the next data lane");
|
||||
expect(result.proposed_primitives).toEqual([]);
|
||||
expect(result.required_axes).toEqual(["counterparty", "period", "lane_family_choice"]);
|
||||
expect(result.discovery_plan.plan_status).toBe("needs_clarification");
|
||||
expect(result.reason_codes).toContain("planner_selected_metadata_lane_clarification_recipe");
|
||||
expect(result.reason_codes).toContain("planner_needs_more_user_or_scope_context");
|
||||
});
|
||||
|
||||
it("does not mark an unclassified turn as executable without turn meaning context", () => {
|
||||
const result = planAssistantMcpDiscovery({});
|
||||
|
||||
|
|
@ -95,4 +202,18 @@ describe("assistant MCP discovery planner", () => {
|
|||
expect(result.discovery_plan.plan_status).toBe("needs_clarification");
|
||||
expect(result.reason_codes).toContain("planner_needs_more_user_or_scope_context");
|
||||
});
|
||||
|
||||
it("exposes an explicit entity-resolution chain instead of silently collapsing into a lifecycle lane", () => {
|
||||
const result = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
explicit_entity_candidates: ["SVK"]
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.planner_status).toBe("ready_for_execution");
|
||||
expect(result.semantic_data_need).toBe("entity discovery evidence");
|
||||
expect(result.selected_chain_id).toBe("entity_resolution");
|
||||
expect(result.selected_chain_summary).toContain("resolve the most relevant 1C reference");
|
||||
expect(result.proposed_primitives).toEqual(["search_business_entity", "resolve_entity_reference", "probe_coverage"]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ describe("assistant MCP discovery policy", () => {
|
|||
expect(plan.rejected_primitives).toEqual(["drop_database"]);
|
||||
expect(plan.requires_evidence_gate).toBe(true);
|
||||
expect(plan.answer_may_use_raw_model_claims).toBe(false);
|
||||
expect(plan.execution_budget).toEqual({ max_probe_count: 6, max_rows_per_probe: 500 });
|
||||
expect(plan.execution_budget).toEqual({ max_probe_count: 36, max_rows_per_probe: 500 });
|
||||
expect(plan.reason_codes).toContain("model_proposed_unregistered_mcp_primitive");
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -54,10 +54,10 @@ describe("assistant MCP discovery response candidate", () => {
|
|||
requires_user_clarification: false,
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference",
|
||||
headline: "По данным 1С найдены строки денежных движений; сумму можно называть только в рамках проверенного периода и найденных строк.",
|
||||
headline: "По данным 1С найдены строки входящих денежных поступлений; сумму можно называть только в рамках проверенного периода и найденных строк.",
|
||||
confirmed_lines: [
|
||||
"1C value-flow rows were found for counterparty SVK",
|
||||
"По найденным строкам денежных движений в 1С по контрагенту SVK за период 2020 сумма составляет 3 750 руб."
|
||||
"По найденным строкам входящих денежных поступлений в 1С по контрагенту SVK за период 2020 сумма входящих денежных поступлений составляет 3 750 руб."
|
||||
],
|
||||
inference_lines: ["Counterparty value-flow total was calculated from confirmed 1C movement rows"],
|
||||
unknown_lines: ["Full turnover outside the checked period is not proven by this MCP discovery pilot"],
|
||||
|
|
@ -69,9 +69,9 @@ describe("assistant MCP discovery response candidate", () => {
|
|||
);
|
||||
|
||||
expect(candidate.candidate_status).toBe("ready_for_guarded_use");
|
||||
expect(candidate.reply_text).toContain("В 1С найдены строки денежных движений по контрагенту SVK.");
|
||||
expect(candidate.reply_text).toContain("В 1С найдены строки входящих денежных поступлений по контрагенту SVK.");
|
||||
expect(candidate.reply_text).toContain("3 750 руб.");
|
||||
expect(candidate.reply_text).toContain("Полный оборот вне проверенного периода этим поиском не подтвержден.");
|
||||
expect(candidate.reply_text).toContain("Полный объем входящих поступлений вне проверенного периода этим поиском не подтвержден.");
|
||||
expect(candidate.reply_text).not.toContain("pilot_");
|
||||
expect(candidate.reply_text).not.toContain("query_movements");
|
||||
});
|
||||
|
|
@ -151,6 +151,162 @@ describe("assistant MCP discovery response candidate", () => {
|
|||
expect(candidate.reply_text).not.toContain("query_movements");
|
||||
});
|
||||
|
||||
it("keeps monthly breakdown lines user-facing and localizes monthly inference basis", () => {
|
||||
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: "По данным 1С найдены строки входящих и исходящих денежных движений; нетто и помесячная раскладка могут называться только как расчет по найденным строкам и проверенному периоду.",
|
||||
confirmed_lines: [
|
||||
"1C bidirectional value-flow rows were checked for counterparty SVK: incoming=found, outgoing=found",
|
||||
"Помесячно: янв 2020 — получили 10 000 руб., заплатили 4 000 руб., нетто в нашу сторону 6 000 руб."
|
||||
],
|
||||
inference_lines: [
|
||||
"Counterparty monthly net value-flow breakdown was grouped by month over confirmed incoming and outgoing 1C rows"
|
||||
],
|
||||
unknown_lines: [],
|
||||
limitation_lines: [],
|
||||
next_step_line: null
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
expect(candidate.reply_text).toContain("Помесячно: янв 2020");
|
||||
expect(candidate.reply_text).toContain("Помесячная нетто-раскладка сгруппирована только по подтвержденным входящим и исходящим строкам 1С.");
|
||||
expect(candidate.reply_text).not.toContain("Counterparty monthly net value-flow breakdown");
|
||||
});
|
||||
|
||||
it("localizes recovered coverage facts without leaking broad-probe wording", () => {
|
||||
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: "По данным 1С найдены строки исходящих платежей/списаний; сумму можно называть только в рамках проверенного периода и найденных строк.",
|
||||
confirmed_lines: ["1C supplier-payout rows were found for counterparty SVK"],
|
||||
inference_lines: [
|
||||
"Requested period coverage was recovered through monthly 1C value-flow probes after the broad probe hit the row limit"
|
||||
],
|
||||
unknown_lines: [],
|
||||
limitation_lines: [],
|
||||
next_step_line: null
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
expect(candidate.reply_text).toContain("Покрытие запрошенного периода восстановлено помесячными проверками 1С");
|
||||
expect(candidate.reply_text).not.toContain("broad probe hit the row limit");
|
||||
});
|
||||
|
||||
it("localizes document evidence without leaking raw English facts", () => {
|
||||
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: "По данным 1С найдены строки документов; ответ ограничен проверенным периодом и найденными строками.",
|
||||
confirmed_lines: ["1C document rows were found for counterparty SVK"],
|
||||
inference_lines: ["Counterparty document evidence is limited to confirmed 1C document rows in the checked scope"],
|
||||
unknown_lines: ["Full document history outside the checked period is not proven by this MCP discovery pilot"],
|
||||
limitation_lines: [],
|
||||
next_step_line: null
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
expect(candidate.reply_text).toContain("В 1С найдены строки документов по контрагенту SVK.");
|
||||
expect(candidate.reply_text).toContain("Срез документов ограничен только подтвержденными строками документов");
|
||||
expect(candidate.reply_text).toContain("Полный исторический срез документов вне проверенного периода этим поиском не подтвержден.");
|
||||
expect(candidate.reply_text).not.toContain("1C document rows were found");
|
||||
});
|
||||
|
||||
it("localizes movement evidence without leaking raw English facts", () => {
|
||||
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: "По данным 1С найдены строки движений; ответ ограничен проверенным периодом и найденными строками.",
|
||||
confirmed_lines: ["1C movement rows were found for counterparty SVK"],
|
||||
inference_lines: ["Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope"],
|
||||
unknown_lines: ["Full movement history outside the checked period is not proven by this MCP discovery pilot"],
|
||||
limitation_lines: [],
|
||||
next_step_line: null
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
expect(candidate.reply_text).toContain("В 1С найдены строки движений по контрагенту SVK.");
|
||||
expect(candidate.reply_text).toContain("Срез движений ограничен только подтвержденными строками движений");
|
||||
expect(candidate.reply_text).toContain("Полный исторический срез движений вне проверенного периода этим поиском не подтвержден.");
|
||||
expect(candidate.reply_text).not.toContain("1C movement rows were found");
|
||||
});
|
||||
|
||||
it("localizes metadata evidence without leaking raw MCP wording", () => {
|
||||
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: "По данным 1С найдена подтвержденная metadata-поверхность.",
|
||||
confirmed_lines: [
|
||||
'Confirmed 1C metadata surface for scope "НДС": 7 rows and 3 matching objects',
|
||||
"Available metadata object sets: accumulation_register, document",
|
||||
"Selected metadata entity set: Документ",
|
||||
"Selected metadata objects: Документ.СчетФактураВыданный",
|
||||
"Available metadata fields/sections: amount, vat_rate, organization"
|
||||
],
|
||||
inference_lines: [
|
||||
"A likely next checked lane may be inferred as document_evidence from the confirmed metadata surface"
|
||||
],
|
||||
unknown_lines: [
|
||||
'No matching 1C metadata objects were confirmed for scope "Прибыль"',
|
||||
"Exact downstream metadata surface remains ambiguous across: Документ, РегистрНакопления"
|
||||
],
|
||||
limitation_lines: ["Detailed metadata fields were not returned by this MCP metadata probe"],
|
||||
next_step_line: null
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
expect(candidate.reply_text).toContain('В 1С подтверждена metadata-поверхность по области "НДС"');
|
||||
expect(candidate.reply_text).toContain("Доступные типы metadata-объектов");
|
||||
expect(candidate.reply_text).toContain("Выбранное семейство metadata-объектов: Документ");
|
||||
expect(candidate.reply_text).toContain("Выбранные metadata-объекты для следующего шага");
|
||||
expect(candidate.reply_text).toContain("Доступные metadata-поля/секции");
|
||||
expect(candidate.reply_text).toContain("контур документов");
|
||||
expect(candidate.reply_text).toContain('В 1С не подтверждены metadata-объекты по области "Прибыль"');
|
||||
expect(candidate.reply_text).toContain("неоднозначна между family");
|
||||
expect(candidate.reply_text).toContain("Эта MCP-проверка metadata не вернула детальный список полей");
|
||||
expect(candidate.reply_text).not.toContain("Confirmed 1C metadata surface");
|
||||
});
|
||||
|
||||
it("returns not applicable when discovery was skipped for an exact supported route", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate({
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
|
|
@ -195,6 +351,34 @@ describe("assistant MCP discovery response candidate", () => {
|
|||
expect(candidate.eligible_for_future_hot_runtime).toBe(true);
|
||||
});
|
||||
|
||||
it("surfaces metadata lane-choice clarification as a user-facing clarification candidate", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
bridge: {
|
||||
bridge_status: "needs_clarification",
|
||||
user_facing_response_allowed: true,
|
||||
business_fact_answer_allowed: false,
|
||||
requires_user_clarification: true,
|
||||
answer_draft: {
|
||||
answer_mode: "needs_clarification",
|
||||
headline: "По подтвержденной metadata-поверхности видно несколько конкурирующих data-lane, и без явного выбора дальше идти нельзя.",
|
||||
confirmed_lines: [],
|
||||
inference_lines: [],
|
||||
unknown_lines: [],
|
||||
limitation_lines: [],
|
||||
next_step_line: "Уточните, в какой контур идти дальше: по документам или по движениям/регистрам."
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
expect(candidate.candidate_status).toBe("clarification_candidate");
|
||||
expect(candidate.reply_type).toBe("clarification_required");
|
||||
expect(candidate.reply_text).toContain("data-lane");
|
||||
expect(candidate.reply_text).toContain("по документам");
|
||||
expect(candidate.reply_text).toContain("по движениям/регистрам");
|
||||
});
|
||||
|
||||
it("does not expose unsupported bridge output as a future hot candidate", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
|
|
|
|||
|
|
@ -113,11 +113,17 @@ describe("assistant MCP discovery response policy", () => {
|
|||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply: "stale exact route answer",
|
||||
currentReplySource: "address_query_runtime_v1",
|
||||
currentReplyType: "factual",
|
||||
addressRuntimeMeta: {
|
||||
detected_intent: "list_documents_by_counterparty",
|
||||
assistant_mcp_discovery_entry_point_v1: entryPoint({
|
||||
turn_input: {
|
||||
adapter_status: "ready",
|
||||
should_run_discovery: true
|
||||
should_run_discovery: true,
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "payout"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -129,6 +135,221 @@ describe("assistant MCP discovery response policy", () => {
|
|||
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_not_discovery_ready_address_candidate");
|
||||
});
|
||||
|
||||
it("keeps aligned factual address lane answers when the exact lane already matched the same semantic intent", () => {
|
||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply: "ИП Калинин Н.М. | сумма: 216600 | операций: 2",
|
||||
currentReplySource: "address_query_runtime_v1",
|
||||
currentReplyType: "factual",
|
||||
addressRuntimeMeta: {
|
||||
detected_intent: "customer_revenue_and_payments",
|
||||
assistant_mcp_discovery_entry_point_v1: entryPoint({
|
||||
turn_input: {
|
||||
adapter_status: "ready",
|
||||
should_run_discovery: true,
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "turnover"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.applied).toBe(false);
|
||||
expect(result.decision).toBe("keep_current_reply");
|
||||
expect(result.reply_text).toBe("ИП Калинин Н.М. | сумма: 216600 | операций: 2");
|
||||
expect(result.reason_codes).toContain("mcp_discovery_response_policy_keep_aligned_factual_address_reply");
|
||||
});
|
||||
|
||||
it("keeps factual address follow-up replies when they already match the continuation target intent", () => {
|
||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply: "ИП Калинин Н.М. | сумма: 216600 | операций: 2",
|
||||
currentReplySource: "address_query_runtime_v1",
|
||||
currentReplyType: "factual",
|
||||
addressRuntimeMeta: {
|
||||
detected_intent: "customer_revenue_and_payments",
|
||||
dialogContinuationContract: {
|
||||
target_intent: "customer_revenue_and_payments"
|
||||
},
|
||||
assistant_mcp_discovery_entry_point_v1: entryPoint({
|
||||
turn_input: {
|
||||
adapter_status: "ready",
|
||||
should_run_discovery: true,
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "turnover"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.applied).toBe(false);
|
||||
expect(result.decision).toBe("keep_current_reply");
|
||||
expect(result.reason_codes).toContain("mcp_discovery_response_policy_keep_factual_address_continuation_target");
|
||||
});
|
||||
|
||||
it("keeps full-confirmed factual address replies even when discovery has a guarded candidate", () => {
|
||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply: "ООО Ромашка | сумма: 128000 | операций: 3",
|
||||
currentReplySource: "address_query_runtime_v1",
|
||||
currentReplyType: "factual",
|
||||
addressRuntimeMeta: {
|
||||
detected_intent: "receivables_confirmed_as_of_date",
|
||||
truth_gate_contract_status: "full_confirmed",
|
||||
assistant_truth_answer_policy_v1: {
|
||||
truth_gate: {
|
||||
coverage_status: "full",
|
||||
grounding_status: "grounded",
|
||||
source_truth_gate_status: "full_confirmed"
|
||||
}
|
||||
},
|
||||
assistant_mcp_discovery_entry_point_v1: entryPoint({
|
||||
turn_input: {
|
||||
adapter_status: "ready",
|
||||
should_run_discovery: true,
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "receivables",
|
||||
asked_action_family: "confirmed_snapshot"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.applied).toBe(false);
|
||||
expect(result.decision).toBe("keep_current_reply");
|
||||
expect(result.reply_text).toBe("ООО Ромашка | сумма: 128000 | операций: 3");
|
||||
expect(result.reason_codes).toContain("mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply");
|
||||
});
|
||||
|
||||
it("overrides a stale full-confirmed lifecycle reply when discovery proves a different net-flow question", () => {
|
||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply: "Коротко: активных заказчиков в 2020 году — 1.",
|
||||
currentReplySource: "address_query_runtime_v1",
|
||||
currentReplyType: "factual",
|
||||
addressRuntimeMeta: {
|
||||
detected_intent: "counterparty_activity_lifecycle",
|
||||
truth_gate_contract_status: "full_confirmed",
|
||||
assistant_truth_answer_policy_v1: {
|
||||
truth_gate: {
|
||||
coverage_status: "full",
|
||||
grounding_status: "grounded",
|
||||
source_truth_gate_status: "full_confirmed"
|
||||
}
|
||||
},
|
||||
dialog_continuation_contract_v2: {
|
||||
target_intent: "counterparty_activity_lifecycle"
|
||||
},
|
||||
assistant_mcp_discovery_entry_point_v1: entryPoint({
|
||||
turn_input: {
|
||||
adapter_status: "ready",
|
||||
should_run_discovery: true,
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "net_value_flow",
|
||||
explicit_entity_candidates: ["Группа СВК"],
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting"
|
||||
}
|
||||
},
|
||||
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: "По данным 1С найдены строки входящих и исходящих денежных движений.",
|
||||
confirmed_lines: ["Получили 47 628 853,03 руб.; заплатили 43 763 351,53 руб.; нетто 3 865 501,50 руб."],
|
||||
inference_lines: [],
|
||||
unknown_lines: ["Полное сальдо вне проверенного окна не подтверждено."],
|
||||
limitation_lines: [],
|
||||
next_step_line: null
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.applied).toBe(true);
|
||||
expect(result.decision).toBe("apply_candidate");
|
||||
expect(result.reply_source).toBe("mcp_discovery_response_candidate_guarded");
|
||||
expect(result.reply_text).toContain("47 628 853,03");
|
||||
expect(result.reason_codes).toContain("mcp_discovery_response_policy_semantic_conflict_allows_candidate_override");
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply");
|
||||
});
|
||||
|
||||
it("keeps runtime-adjusted exact VAT follow-up replies over stale discovery turn meaning", () => {
|
||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply: "Коротко: подтвержденный НДС к уплате за май 2017 — 0,00 руб.",
|
||||
currentReplySource: "address_query_runtime_v1",
|
||||
currentReplyType: "factual",
|
||||
addressRuntimeMeta: {
|
||||
detected_intent: "vat_liability_confirmed_for_tax_period",
|
||||
truth_gate_contract_status: "full_confirmed",
|
||||
assistant_truth_answer_policy_v1: {
|
||||
truth_gate: {
|
||||
coverage_status: "full",
|
||||
grounding_status: "grounded",
|
||||
source_truth_gate_status: "full_confirmed"
|
||||
},
|
||||
answer_shape: {
|
||||
reply_type: "factual",
|
||||
capability_contract_id: "confirmed_vat_liability_for_tax_period"
|
||||
}
|
||||
},
|
||||
assistant_state_transition_v1: {
|
||||
reason_codes: [
|
||||
"root_followup_continue_previous",
|
||||
"intent_adjusted_to_vat_followup_context",
|
||||
"period_from_from_followup_context",
|
||||
"period_to_from_followup_context",
|
||||
"confirmed_balance_exact_vat_tax_period_intent"
|
||||
]
|
||||
},
|
||||
assistant_mcp_discovery_entry_point_v1: entryPoint({
|
||||
turn_input: {
|
||||
adapter_status: "ready",
|
||||
should_run_discovery: true,
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "confirmed_snapshot",
|
||||
explicit_entity_candidates: ["ООО Альтернатива Плюс"],
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||
explicit_date_scope: "2017-05-31",
|
||||
unsupported_but_understood_family: "counterparty_value_or_turnover"
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "checked_sources_only",
|
||||
user_facing_response_allowed: true,
|
||||
business_fact_answer_allowed: false,
|
||||
requires_user_clarification: false,
|
||||
answer_draft: {
|
||||
answer_mode: "checked_sources_only",
|
||||
headline: "Я проверил доступный контур, но подтвержденного факта для ответа не получил.",
|
||||
confirmed_lines: [],
|
||||
inference_lines: [],
|
||||
unknown_lines: ["Полный объем входящих поступлений вне проверенного периода этим поиском не подтвержден."],
|
||||
limitation_lines: [],
|
||||
next_step_line: null
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.applied).toBe(false);
|
||||
expect(result.decision).toBe("keep_current_reply");
|
||||
expect(result.reply_text).toContain("подтвержденный НДС к уплате");
|
||||
expect(result.reason_codes).toContain(
|
||||
"mcp_discovery_response_policy_keep_runtime_adjusted_exact_reply_over_stale_discovery_turn_meaning"
|
||||
);
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_semantic_conflict_allows_candidate_override");
|
||||
});
|
||||
|
||||
it("keeps address lane answers when discovery was not requested for the current turn", () => {
|
||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply: "supported exact route answer",
|
||||
|
|
@ -171,4 +392,50 @@ describe("assistant MCP discovery response policy", () => {
|
|||
expect(result.reason_codes).toContain("mcp_discovery_response_policy_candidate_not_eligible");
|
||||
expect(result.reason_codes).toContain("mcp_discovery_response_policy_kept_current_reply");
|
||||
});
|
||||
|
||||
it("keeps deterministic broad business evaluation summary instead of replacing it with a clarification candidate", () => {
|
||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply: "Коротко: по уже подтвержденным данным в 1С компания выглядит живой операционно.",
|
||||
currentReplySource: "deterministic_broad_business_evaluation_contract",
|
||||
livingChatSource: "deterministic_broad_business_evaluation_contract",
|
||||
modeDecisionReason: "unsupported_current_turn_meaning_boundary",
|
||||
addressRuntimeMeta: {
|
||||
assistant_mcp_discovery_entry_point_v1: entryPoint({
|
||||
turn_input: {
|
||||
adapter_status: "ready",
|
||||
should_run_discovery: true,
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "business_summary",
|
||||
asked_action_family: "broad_evaluation",
|
||||
unsupported_but_understood_family: "broad_business_evaluation",
|
||||
stale_replay_forbidden: true
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "needs_clarification",
|
||||
user_facing_response_allowed: true,
|
||||
business_fact_answer_allowed: false,
|
||||
requires_user_clarification: true,
|
||||
answer_draft: {
|
||||
answer_mode: "needs_clarification",
|
||||
headline: "Нужно уточнить контекст перед поиском в 1С.",
|
||||
confirmed_lines: [],
|
||||
inference_lines: [],
|
||||
unknown_lines: ["MCP discovery pilot needs more scope before execution"],
|
||||
limitation_lines: ["MCP discovery pilot needs more scope before execution"],
|
||||
next_step_line: "Уточните контрагента, период или организацию."
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.applied).toBe(false);
|
||||
expect(result.decision).toBe("keep_current_reply");
|
||||
expect(result.reply_source).toBe("deterministic_broad_business_evaluation_contract");
|
||||
expect(result.reply_text).toContain("компания выглядит живой операционно");
|
||||
expect(result.reason_codes).toContain(
|
||||
"mcp_discovery_response_policy_keep_broad_business_summary_over_clarification_candidate"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ describe("assistant MCP discovery runtime bridge", () => {
|
|||
expect(result.answer_draft.next_step_line).toContain("Уточните контрагента");
|
||||
});
|
||||
|
||||
it("keeps unsupported ready plans outside the hot answer path", async () => {
|
||||
it("keeps document-ready plans bounded when the pilot finds no confirmed rows", async () => {
|
||||
const result = await runAssistantMcpDiscoveryRuntimeBridge({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "document",
|
||||
|
|
@ -61,11 +61,12 @@ describe("assistant MCP discovery runtime bridge", () => {
|
|||
deps: buildDeps([])
|
||||
});
|
||||
|
||||
expect(result.bridge_status).toBe("unsupported");
|
||||
expect(result.bridge_status).toBe("checked_sources_only");
|
||||
expect(result.hot_runtime_wired).toBe(false);
|
||||
expect(result.pilot.mcp_execution_performed).toBe(false);
|
||||
expect(result.pilot.pilot_scope).toBe("counterparty_document_evidence_query_documents_v1");
|
||||
expect(result.pilot.mcp_execution_performed).toBe(true);
|
||||
expect(result.business_fact_answer_allowed).toBe(false);
|
||||
expect(result.reason_codes).toContain("runtime_bridge_status_unsupported");
|
||||
expect(result.reason_codes).toContain("runtime_bridge_status_checked_sources_only");
|
||||
});
|
||||
|
||||
it("preserves the answer adapter boundary against internal mechanics leakage", async () => {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,17 @@ function buildDeps(rows: Array<Record<string, unknown>>, error: string | null =
|
|||
};
|
||||
}
|
||||
|
||||
function buildMetadataDeps(rows: Array<Record<string, unknown>>, error: string | null = null) {
|
||||
return {
|
||||
executeAddressMcpMetadata: vi.fn(async () => ({
|
||||
fetched_rows: error ? 0 : rows.length,
|
||||
raw_rows: error ? [] : rows,
|
||||
rows: error ? [] : rows,
|
||||
error
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
describe("assistant MCP discovery runtime entry point", () => {
|
||||
it("runs the bridge for discovery-eligible lifecycle turn context", async () => {
|
||||
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
|
||||
|
|
@ -78,4 +89,155 @@ describe("assistant MCP discovery runtime entry point", () => {
|
|||
expect(result.bridge?.hot_runtime_wired).toBe(false);
|
||||
expect(result.reason_codes).toContain("mcp_discovery_unsupported_but_understood_turn");
|
||||
});
|
||||
|
||||
it("runs the bridge for raw metadata wording without an exact route owner", async () => {
|
||||
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
|
||||
userMessage: "какие документы и поля есть в 1С по НДС?",
|
||||
deps: buildMetadataDeps([
|
||||
{
|
||||
FullName: "Документ.СчетФактураВыданный",
|
||||
MetaType: "Документ",
|
||||
attributes: [{ Name: "Дата" }]
|
||||
}
|
||||
])
|
||||
});
|
||||
|
||||
expect(result.entry_status).toBe("bridge_executed");
|
||||
expect(result.discovery_attempted).toBe(true);
|
||||
expect(result.turn_input.semantic_data_need).toBe("1C metadata evidence");
|
||||
expect(result.turn_input.turn_meaning_ref).toMatchObject({
|
||||
asked_domain_family: "metadata",
|
||||
asked_action_family: "inspect_fields"
|
||||
});
|
||||
expect(result.bridge?.pilot.pilot_scope).toBe("metadata_inspection_v1");
|
||||
expect(result.bridge?.answer_draft.headline).toContain("метаданным 1С");
|
||||
});
|
||||
|
||||
it("runs the bridge for raw entity-resolution wording and executes the full grounding chain", async () => {
|
||||
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
|
||||
userMessage: "найди в 1С контрагента Группа СВК",
|
||||
deps: buildDeps([
|
||||
{ Counterparty: "Группа СВК", CounterpartyRef: "Ref-1" },
|
||||
{ Counterparty: "СВК Логистика", CounterpartyRef: "Ref-2" }
|
||||
])
|
||||
});
|
||||
|
||||
expect(result.entry_status).toBe("bridge_executed");
|
||||
expect(result.discovery_attempted).toBe(true);
|
||||
expect(result.turn_input.semantic_data_need).toBe("entity discovery evidence");
|
||||
expect(result.turn_input.turn_meaning_ref).toMatchObject({
|
||||
asked_domain_family: "entity_resolution",
|
||||
asked_action_family: "search_business_entity",
|
||||
explicit_entity_candidates: ["Группа СВК"]
|
||||
});
|
||||
expect(result.bridge?.bridge_status).toBe("answer_draft_ready");
|
||||
expect(result.bridge?.pilot.pilot_scope).toBe("entity_resolution_search_v1");
|
||||
expect(result.bridge?.pilot.executed_primitives).toEqual([
|
||||
"search_business_entity",
|
||||
"resolve_entity_reference",
|
||||
"probe_coverage"
|
||||
]);
|
||||
expect(result.bridge?.pilot.derived_entity_resolution).toMatchObject({
|
||||
resolution_status: "resolved",
|
||||
resolved_entity: "Группа СВК",
|
||||
resolved_reference: "Ref-1"
|
||||
});
|
||||
expect(result.bridge?.answer_draft.answer_mode).toBe("confirmed_with_bounded_inference");
|
||||
});
|
||||
|
||||
it("runs the bridge again when the user clarifies one ambiguous entity-resolution candidate", async () => {
|
||||
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
|
||||
userMessage: "СВК-А",
|
||||
followupContext: {
|
||||
previous_discovery_pilot_scope: "entity_resolution_search_v1",
|
||||
previous_discovery_entity_resolution_status: "ambiguous",
|
||||
previous_discovery_entity_candidates: ["СВК", "СВК-А", "СВК-Б"],
|
||||
previous_discovery_entity_ambiguity_candidates: ["СВК-А", "СВК-Б"]
|
||||
},
|
||||
deps: buildDeps([
|
||||
{ Counterparty: "СВК-А", CounterpartyRef: "Ref-1" },
|
||||
{ Counterparty: "СВК-Б", CounterpartyRef: "Ref-2" }
|
||||
])
|
||||
});
|
||||
|
||||
expect(result.entry_status).toBe("bridge_executed");
|
||||
expect(result.discovery_attempted).toBe(true);
|
||||
expect(result.turn_input.source_signal).toBe("followup_context");
|
||||
expect(result.turn_input.semantic_data_need).toBe("entity discovery evidence");
|
||||
expect(result.turn_input.turn_meaning_ref).toMatchObject({
|
||||
asked_domain_family: "entity_resolution",
|
||||
asked_action_family: "search_business_entity",
|
||||
explicit_entity_candidates: ["СВК-А"]
|
||||
});
|
||||
expect(result.bridge?.pilot.pilot_scope).toBe("entity_resolution_search_v1");
|
||||
expect(result.bridge?.pilot.executed_primitives).toEqual([
|
||||
"search_business_entity",
|
||||
"resolve_entity_reference",
|
||||
"probe_coverage"
|
||||
]);
|
||||
expect(result.bridge?.pilot.derived_entity_resolution).toMatchObject({
|
||||
resolution_status: "resolved",
|
||||
resolved_entity: "СВК-А",
|
||||
resolved_reference: "Ref-1"
|
||||
});
|
||||
});
|
||||
|
||||
it("chains an ordinal ambiguity clarification directly into value-flow execution", async () => {
|
||||
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
|
||||
userMessage: "второй вариант, сколько получили за 2020 год",
|
||||
followupContext: {
|
||||
previous_discovery_pilot_scope: "entity_resolution_search_v1",
|
||||
previous_discovery_entity_resolution_status: "ambiguous",
|
||||
previous_discovery_entity_candidates: ["СВК", "СВК-А", "СВК-Б"],
|
||||
previous_discovery_entity_ambiguity_candidates: ["СВК-А", "СВК-Б"]
|
||||
},
|
||||
deps: buildDeps([
|
||||
{ Period: "2020-01-15T00:00:00", Amount: 1200, Counterparty: "СВК-Б" },
|
||||
{ Period: "2020-02-20T00:00:00", Amount: 800, Counterparty: "СВК-Б" }
|
||||
])
|
||||
});
|
||||
|
||||
expect(result.entry_status).toBe("bridge_executed");
|
||||
expect(result.discovery_attempted).toBe(true);
|
||||
expect(result.turn_input.source_signal).toBe("followup_context");
|
||||
expect(result.turn_input.semantic_data_need).toBe("counterparty value-flow evidence");
|
||||
expect(result.turn_input.turn_meaning_ref).toMatchObject({
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "turnover",
|
||||
explicit_entity_candidates: ["СВК-Б"],
|
||||
explicit_date_scope: "2020"
|
||||
});
|
||||
expect(result.bridge?.pilot.pilot_scope).toBe("counterparty_value_flow_query_movements_v1");
|
||||
expect(result.bridge?.pilot.derived_value_flow).toMatchObject({
|
||||
counterparty: "СВК-Б",
|
||||
period_scope: "2020",
|
||||
total_amount: 2000
|
||||
});
|
||||
});
|
||||
|
||||
it("chains an ordinal ambiguity clarification directly into document evidence execution", async () => {
|
||||
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
|
||||
userMessage: "первый вариант, покажи документы за 2020 год",
|
||||
followupContext: {
|
||||
previous_discovery_pilot_scope: "entity_resolution_search_v1",
|
||||
previous_discovery_entity_resolution_status: "ambiguous",
|
||||
previous_discovery_entity_candidates: ["СВК", "СВК-А", "СВК-Б"],
|
||||
previous_discovery_entity_ambiguity_candidates: ["СВК-А", "СВК-Б"]
|
||||
},
|
||||
deps: buildDeps([{ Period: "2020-03-12T00:00:00", Counterparty: "СВК-А", Registrar: "Doc-1" }])
|
||||
});
|
||||
|
||||
expect(result.entry_status).toBe("bridge_executed");
|
||||
expect(result.discovery_attempted).toBe(true);
|
||||
expect(result.turn_input.source_signal).toBe("followup_context");
|
||||
expect(result.turn_input.semantic_data_need).toBe("document evidence");
|
||||
expect(result.turn_input.turn_meaning_ref).toMatchObject({
|
||||
asked_domain_family: "documents",
|
||||
asked_action_family: "list_documents",
|
||||
explicit_entity_candidates: ["СВК-А"],
|
||||
explicit_date_scope: "2020"
|
||||
});
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,6 +1,7 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildAddressMemoryRecapReply,
|
||||
buildBroadBusinessEvaluationReply,
|
||||
buildSelectedObjectAnswerInspectionReply,
|
||||
createAssistantMemoryRecapPolicy,
|
||||
resolveAssistantLivingChatMemoryContext
|
||||
|
|
@ -133,6 +134,50 @@ describe("assistantMemoryRecapPolicy", () => {
|
|||
expect(signals.contextualMemoryRecapFollowupDetected).toBe(false);
|
||||
});
|
||||
|
||||
it("detects contextual memory recap over prior grounded MCP discovery answer", () => {
|
||||
const signals = policy.resolveRouteMemorySignals({
|
||||
rawUserMessage: "а ты помнишь, что мы уже выяснили по свк?",
|
||||
repairedRawUserMessage: "",
|
||||
effectiveAddressUserMessage: "",
|
||||
repairedEffectiveAddressUserMessage: "",
|
||||
dataScopeMetaQuery: false,
|
||||
capabilityMetaQuery: false,
|
||||
dataRetrievalSignal: false,
|
||||
strongDataSignal: false,
|
||||
aggregateBusinessAnalyticsSignal: false,
|
||||
lastGroundedAddressDebug: null,
|
||||
hasPriorAddressDebug: true,
|
||||
sessionItems: [
|
||||
{
|
||||
role: "assistant",
|
||||
debug: {
|
||||
execution_lane: "living_chat",
|
||||
mcp_discovery_response_applied: true,
|
||||
assistant_mcp_discovery_entry_point_v1: {
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
entry_status: "bridge_executed",
|
||||
turn_input: {
|
||||
turn_meaning_ref: {
|
||||
explicit_entity_candidates: ["Группа СВК"],
|
||||
explicit_date_scope: "2020"
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
business_fact_answer_allowed: true,
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
expect(signals.contextualMemoryRecapFollowupDetected).toBe(true);
|
||||
});
|
||||
|
||||
it("builds deterministic recap summary from recent selected-object facts", () => {
|
||||
const context = resolveAssistantLivingChatMemoryContext({
|
||||
modeDecisionReason: "memory_recap_followup_detected",
|
||||
|
|
@ -278,4 +323,241 @@ describe("assistantMemoryRecapPolicy", () => {
|
|||
expect(reply).toContain("Рабочая станция");
|
||||
expect(reply).toContain("Покупатель");
|
||||
});
|
||||
|
||||
it("builds deterministic recap summary from grounded MCP discovery counterparty context", () => {
|
||||
const sessionItems = [
|
||||
{
|
||||
role: "assistant",
|
||||
debug: {
|
||||
execution_lane: "living_chat",
|
||||
mcp_discovery_response_applied: true,
|
||||
assistant_mcp_discovery_entry_point_v1: {
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
entry_status: "bridge_executed",
|
||||
turn_input: {
|
||||
turn_meaning_ref: {
|
||||
explicit_entity_candidates: ["Группа СВК"],
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||
explicit_date_scope: "2020"
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
business_fact_answer_allowed: true,
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference"
|
||||
},
|
||||
pilot: {
|
||||
pilot_scope: "counterparty_bidirectional_value_flow_query_movements_v1",
|
||||
derived_bidirectional_value_flow: {
|
||||
net_amount_human_ru: "3 865 501,50 руб.",
|
||||
incoming_customer_revenue: {
|
||||
total_amount_human_ru: "47 628 853,03 руб."
|
||||
},
|
||||
outgoing_supplier_payout: {
|
||||
total_amount_human_ru: "43 763 351,53 руб."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
const context = resolveAssistantLivingChatMemoryContext({
|
||||
modeDecisionReason: "memory_recap_followup_detected",
|
||||
sessionItems
|
||||
});
|
||||
|
||||
const reply = buildAddressMemoryRecapReply({
|
||||
organization: null,
|
||||
addressDebug: context.lastMemoryAddressDebug,
|
||||
sessionItems,
|
||||
toNonEmptyString: (value: unknown) => {
|
||||
const text = String(value ?? "").trim();
|
||||
return text.length > 0 ? text : null;
|
||||
}
|
||||
});
|
||||
|
||||
expect(context.contextualMemoryRecapFollowup).toBe(true);
|
||||
expect(reply).toContain("Группа СВК");
|
||||
expect(reply).toContain("нетто");
|
||||
expect(reply).toContain("47 628 853,03 руб.");
|
||||
expect(reply).toContain("43 763 351,53 руб.");
|
||||
});
|
||||
|
||||
it("builds deterministic recap summary from grounded MCP metadata discovery context", () => {
|
||||
const sessionItems = [
|
||||
{
|
||||
role: "assistant",
|
||||
debug: {
|
||||
execution_lane: "living_chat",
|
||||
mcp_discovery_response_applied: true,
|
||||
assistant_mcp_discovery_entry_point_v1: {
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
entry_status: "bridge_executed",
|
||||
turn_input: {
|
||||
turn_meaning_ref: {
|
||||
explicit_entity_candidates: ["НДС"]
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
business_fact_answer_allowed: true,
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference"
|
||||
},
|
||||
pilot: {
|
||||
pilot_scope: "metadata_inspection_v1",
|
||||
derived_metadata_surface: {
|
||||
metadata_scope: "НДС",
|
||||
matched_rows: 7,
|
||||
matched_objects: ["РегистрНакопления.НДСПокупок"],
|
||||
available_entity_sets: ["accumulation_register", "document"],
|
||||
available_fields: ["amount", "vat_rate", "organization"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
const context = resolveAssistantLivingChatMemoryContext({
|
||||
modeDecisionReason: "memory_recap_followup_detected",
|
||||
sessionItems
|
||||
});
|
||||
|
||||
const reply = buildAddressMemoryRecapReply({
|
||||
organization: null,
|
||||
addressDebug: context.lastMemoryAddressDebug,
|
||||
sessionItems,
|
||||
toNonEmptyString: (value: unknown) => {
|
||||
const text = String(value ?? "").trim();
|
||||
return text.length > 0 ? text : null;
|
||||
}
|
||||
});
|
||||
|
||||
expect(context.contextualMemoryRecapFollowup).toBe(true);
|
||||
expect(reply).toContain("НДС");
|
||||
expect(reply).toContain("metadata-поверхность 1С");
|
||||
expect(reply).toContain("amount");
|
||||
expect(reply).toContain("accumulation_register");
|
||||
});
|
||||
|
||||
it("builds deterministic broad business evaluation summary from recent grounded organization facts", () => {
|
||||
const sessionItems = [
|
||||
{
|
||||
role: "assistant",
|
||||
debug: {
|
||||
execution_lane: "address_query",
|
||||
answer_grounding_check: {
|
||||
status: "grounded"
|
||||
},
|
||||
detected_intent: "counterparty_activity_lifecycle",
|
||||
extracted_filters: {
|
||||
organization: "ООО Альтернатива Плюс"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
debug: {
|
||||
execution_lane: "living_chat",
|
||||
mcp_discovery_response_applied: true,
|
||||
assistant_mcp_discovery_entry_point_v1: {
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
entry_status: "bridge_executed",
|
||||
turn_input: {
|
||||
turn_meaning_ref: {
|
||||
explicit_entity_candidates: ["Группа СВК"],
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||
explicit_date_scope: "2020"
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
business_fact_answer_allowed: true,
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference"
|
||||
},
|
||||
pilot: {
|
||||
pilot_scope: "counterparty_bidirectional_value_flow_query_movements_v1",
|
||||
derived_bidirectional_value_flow: {
|
||||
net_amount_human_ru: "3 865 501,50 руб.",
|
||||
incoming_customer_revenue: {
|
||||
total_amount_human_ru: "47 628 853,03 руб."
|
||||
},
|
||||
outgoing_supplier_payout: {
|
||||
total_amount_human_ru: "43 763 351,53 руб."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const reply = buildBroadBusinessEvaluationReply({
|
||||
organization: "ООО Альтернатива Плюс",
|
||||
addressDebug: sessionItems[1].debug as any,
|
||||
sessionItems,
|
||||
toNonEmptyString: (value: unknown) => {
|
||||
const text = String(value ?? "").trim();
|
||||
return text.length > 0 ? text : null;
|
||||
}
|
||||
});
|
||||
|
||||
expect(reply.toLowerCase()).toContain("оценка бизнеса");
|
||||
expect(reply).toContain("ООО Альтернатива Плюс");
|
||||
expect(reply).toContain("47 628 853,03");
|
||||
});
|
||||
|
||||
it("builds grounded answer inspection reply for MCP discovery net answer", () => {
|
||||
const context = resolveAssistantLivingChatMemoryContext({
|
||||
modeDecisionReason: "answer_inspection_followup_detected",
|
||||
sessionItems: [
|
||||
{
|
||||
role: "assistant",
|
||||
debug: {
|
||||
execution_lane: "living_chat",
|
||||
mcp_discovery_response_applied: true,
|
||||
assistant_mcp_discovery_entry_point_v1: {
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
entry_status: "bridge_executed",
|
||||
turn_input: {
|
||||
turn_meaning_ref: {
|
||||
explicit_entity_candidates: ["Группа СВК"],
|
||||
explicit_date_scope: "2020"
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
business_fact_answer_allowed: true,
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference"
|
||||
},
|
||||
pilot: {
|
||||
pilot_scope: "counterparty_bidirectional_value_flow_query_movements_v1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const reply = buildSelectedObjectAnswerInspectionReply({
|
||||
addressDebug: context.lastAnswerInspectionAddressDebug,
|
||||
toNonEmptyString: (value: unknown) => {
|
||||
const text = String(value ?? "").trim();
|
||||
return text.length > 0 ? text : null;
|
||||
}
|
||||
});
|
||||
|
||||
expect(context.contextualAnswerInspectionFollowup).toBe(true);
|
||||
expect(reply).toContain("Группа СВК");
|
||||
expect(reply).toContain("Нетто");
|
||||
expect(reply).toContain("проверенному периоду");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -606,7 +606,7 @@ describe("assistantRoutePolicy", () => {
|
|||
expect(decision.orchestrationContract?.organization_scope_switch_detected).not.toBe(true);
|
||||
});
|
||||
|
||||
it("keeps company activity assessment follow-up in address lane when lifecycle intent is resolved from grounded continuity", () => {
|
||||
it("routes broad business evaluation follow-up to chat instead of replaying lifecycle address intent", () => {
|
||||
const policy = buildPolicy({
|
||||
resolveAddressIntent: () => ({ intent: "counterparty_activity_lifecycle", confidence: "high" }),
|
||||
findLastGroundedAddressAnswerDebug: () => ({
|
||||
|
|
@ -618,6 +618,15 @@ describe("assistantRoutePolicy", () => {
|
|||
period_to: "2026-04-18"
|
||||
}
|
||||
}),
|
||||
resolveAssistantTurnMeaning: () => ({
|
||||
schema_version: "assistant_turn_meaning_v1",
|
||||
asked_domain_family: "business_summary",
|
||||
asked_action_family: "broad_evaluation",
|
||||
explicit_intent_candidate: null,
|
||||
unsupported_but_understood_family: "broad_business_evaluation",
|
||||
stale_replay_forbidden: true,
|
||||
reason_codes: ["broad_business_evaluation_current_turn_signal"]
|
||||
}),
|
||||
resolveAddressToolGateDecision: () => ({
|
||||
runAddressLane: false,
|
||||
decision: "skip_address_lane",
|
||||
|
|
@ -666,9 +675,12 @@ describe("assistantRoutePolicy", () => {
|
|||
useMock: false
|
||||
});
|
||||
|
||||
expect(decision.runAddressLane).toBe(true);
|
||||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||||
expect(decision.livingMode).toBe("address_data");
|
||||
expect(decision.runAddressLane).toBe(false);
|
||||
expect(decision.toolGateDecision).toBe("skip_address_lane");
|
||||
expect(decision.toolGateReason).toBe("unsupported_current_turn_meaning_boundary");
|
||||
expect(decision.livingMode).toBe("chat");
|
||||
expect(decision.livingReason).toBe("unsupported_current_turn_meaning_boundary");
|
||||
expect(decision.orchestrationContract?.unsupported_current_turn_family).toBe("broad_business_evaluation");
|
||||
});
|
||||
|
||||
it("recovers an address route from current-turn meaning when L0 resolver is noisy", () => {
|
||||
|
|
|
|||
|
|
@ -796,6 +796,7 @@ describe("assistantTransitionPolicy", () => {
|
|||
|
||||
it("retargets selected-object provenance follow-up from inventory root when semantic scope is already detected", () => {
|
||||
const policy = buildPolicy({
|
||||
hasAddressFollowupContextSignal: () => true,
|
||||
findLastAddressAssistantItem: () => ({
|
||||
text: "На 31.03.2016 на складе подтверждено 2 позиции.",
|
||||
debug: {
|
||||
|
|
@ -810,8 +811,7 @@ describe("assistantTransitionPolicy", () => {
|
|||
anchor_type: "organization",
|
||||
anchor_value_resolved: 'ООО "Альтернатива Плюс"'
|
||||
}
|
||||
}),
|
||||
hasAddressFollowupContextSignal: () => true
|
||||
})
|
||||
});
|
||||
|
||||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||||
|
|
@ -1013,4 +1013,490 @@ describe("assistantTransitionPolicy", () => {
|
|||
|
||||
expect(carryover).toBeNull();
|
||||
});
|
||||
|
||||
it("drops carryover for broad business evaluation so lifecycle context does not stick to the new question", () => {
|
||||
const policy = buildPolicy({
|
||||
findLastAddressAssistantItem: () => ({
|
||||
text: "Lifecycle answer",
|
||||
debug: {
|
||||
execution_lane: "address_query",
|
||||
answer_grounding_check: { status: "grounded" },
|
||||
detected_intent: "counterparty_activity_lifecycle",
|
||||
extracted_filters: {
|
||||
organization: 'ООО "Альтернатива Плюс"',
|
||||
period_to: "2020-12-31"
|
||||
},
|
||||
anchor_type: "organization",
|
||||
anchor_value_resolved: 'ООО "Альтернатива Плюс"'
|
||||
}
|
||||
}),
|
||||
hasAddressFollowupContextSignal: () => true,
|
||||
resolveAssistantTurnMeaning: () => ({
|
||||
schema_version: "assistant_turn_meaning_v1",
|
||||
asked_domain_family: "business_summary",
|
||||
asked_action_family: "broad_evaluation",
|
||||
explicit_intent_candidate: null,
|
||||
unsupported_but_understood_family: "broad_business_evaluation",
|
||||
explicit_entity_candidates: [],
|
||||
stale_replay_forbidden: true
|
||||
})
|
||||
});
|
||||
|
||||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||||
"Как ты оценишь деятельность компании?",
|
||||
[],
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
expect(carryover).toBeNull();
|
||||
});
|
||||
|
||||
it("reuses grounded MCP discovery payout context for a short year-switch follow-up", () => {
|
||||
const policy = buildPolicy({
|
||||
findLastAddressAssistantItem: () => null,
|
||||
hasAddressFollowupContextSignal: () => true
|
||||
});
|
||||
|
||||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||||
"а теперь за 2021?",
|
||||
[
|
||||
{
|
||||
role: "assistant",
|
||||
text: "Подтверждены исходящие платежи по Группа СВК за 2020 год.",
|
||||
debug: {
|
||||
execution_lane: "living_chat",
|
||||
mcp_discovery_response_applied: true,
|
||||
assistant_active_organization: "ООО Альтернатива Плюс",
|
||||
assistant_mcp_discovery_entry_point_v1: {
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
entry_status: "bridge_executed",
|
||||
turn_input: {
|
||||
turn_meaning_ref: {
|
||||
asked_action_family: "payout",
|
||||
explicit_entity_candidates: ["Группа СВК"],
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||
explicit_date_scope: "2020"
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
business_fact_answer_allowed: true,
|
||||
pilot: {
|
||||
pilot_scope: "counterparty_supplier_payout_query_movements_v1"
|
||||
},
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
expect(carryover?.followupSelectionMode).toBe("carry_previous_intent");
|
||||
expect(carryover?.followupContext?.previous_intent).toBe("supplier_payouts_profile");
|
||||
expect(carryover?.followupContext?.target_intent).toBe("supplier_payouts_profile");
|
||||
expect(carryover?.followupContext?.previous_discovery_pilot_scope).toBe(
|
||||
"counterparty_supplier_payout_query_movements_v1"
|
||||
);
|
||||
expect(carryover?.followupContext?.previous_anchor_type).toBe("counterparty");
|
||||
expect(carryover?.followupContext?.previous_anchor_value).toBe("Группа СВК");
|
||||
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||||
counterparty: "Группа СВК",
|
||||
organization: "ООО Альтернатива Плюс",
|
||||
period_from: "2020-01-01",
|
||||
period_to: "2020-12-31"
|
||||
});
|
||||
});
|
||||
it("carries resolved entity candidates from grounded entity-resolution discovery into followup context", () => {
|
||||
const policy = buildPolicy({
|
||||
findLastAddressAssistantItem: () => null,
|
||||
hasAddressFollowupContextSignal: () => true
|
||||
});
|
||||
|
||||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||||
"по нему документы за 2020 год",
|
||||
[
|
||||
{
|
||||
role: "assistant",
|
||||
text: "В текущем каталожном срезе 1С по запросу \"СВК\" найден контрагент \"Группа СВК\".",
|
||||
debug: {
|
||||
execution_lane: "living_chat",
|
||||
mcp_discovery_response_applied: true,
|
||||
assistant_mcp_discovery_entry_point_v1: {
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
entry_status: "bridge_executed",
|
||||
turn_input: {
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "entity_resolution",
|
||||
asked_action_family: "search_business_entity",
|
||||
explicit_entity_candidates: ["СВК"]
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
business_fact_answer_allowed: true,
|
||||
pilot: {
|
||||
pilot_scope: "entity_resolution_search_v1",
|
||||
derived_entity_resolution: {
|
||||
requested_entity: "СВК",
|
||||
resolution_status: "resolved",
|
||||
resolved_entity: "Группа СВК",
|
||||
ambiguity_candidates: []
|
||||
}
|
||||
},
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
expect(carryover?.followupContext?.previous_discovery_pilot_scope).toBe("entity_resolution_search_v1");
|
||||
expect(carryover?.followupContext?.previous_discovery_entity_candidates).toEqual(["Группа СВК", "СВК"]);
|
||||
expect(carryover?.followupContext?.previous_anchor_type).toBe("counterparty");
|
||||
expect(carryover?.followupContext?.previous_anchor_value).toBe("Группа СВК");
|
||||
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||||
counterparty: "Группа СВК"
|
||||
});
|
||||
});
|
||||
|
||||
it("carries ambiguity candidates from entity-resolution discovery into followup context", () => {
|
||||
const policy = buildPolicy({
|
||||
findLastAddressAssistantItem: () => null,
|
||||
hasAddressFollowupContextSignal: () => true
|
||||
});
|
||||
|
||||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||||
"СВК-А",
|
||||
[
|
||||
{
|
||||
role: "assistant",
|
||||
text: "По каталогу 1С нашлось несколько похожих контрагентов: СВК-А, СВК-Б.",
|
||||
debug: {
|
||||
execution_lane: "living_chat",
|
||||
mcp_discovery_response_applied: true,
|
||||
assistant_mcp_discovery_entry_point_v1: {
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
entry_status: "bridge_executed",
|
||||
turn_input: {
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "entity_resolution",
|
||||
asked_action_family: "search_business_entity",
|
||||
explicit_entity_candidates: ["СВК"]
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
business_fact_answer_allowed: false,
|
||||
pilot: {
|
||||
pilot_scope: "entity_resolution_search_v1",
|
||||
derived_entity_resolution: {
|
||||
requested_entity: "СВК",
|
||||
resolution_status: "ambiguous",
|
||||
resolved_entity: null,
|
||||
ambiguity_candidates: ["СВК-А", "СВК-Б"]
|
||||
}
|
||||
},
|
||||
answer_draft: {
|
||||
answer_mode: "needs_clarification"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
expect(carryover?.followupContext?.previous_discovery_pilot_scope).toBe("entity_resolution_search_v1");
|
||||
expect(carryover?.followupContext?.previous_discovery_entity_resolution_status).toBe("ambiguous");
|
||||
expect(carryover?.followupContext?.previous_discovery_entity_candidates).toEqual(["СВК", "СВК-А", "СВК-Б"]);
|
||||
expect(carryover?.followupContext?.previous_discovery_entity_ambiguity_candidates).toEqual([
|
||||
"СВК-А",
|
||||
"СВК-Б"
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps exact payout carryover for a short net follow-up without restating counterparty or year", () => {
|
||||
const policy = buildPolicy({
|
||||
findLastAddressAssistantItem: () => ({
|
||||
role: "assistant",
|
||||
text: "Платежи по Группа СВК за 2021",
|
||||
debug: {
|
||||
execution_lane: "address_query",
|
||||
answer_grounding_check: { status: "grounded" },
|
||||
detected_intent: "customer_revenue_and_payments",
|
||||
selected_recipe: "address_customer_revenue_and_payments_v1",
|
||||
extracted_filters: {
|
||||
counterparty: "Группа СВК",
|
||||
period_from: "2021-01-01",
|
||||
period_to: "2021-12-31"
|
||||
},
|
||||
anchor_type: "counterparty",
|
||||
anchor_value_resolved: "Группа СВК"
|
||||
}
|
||||
}),
|
||||
hasAddressFollowupContextSignal: () => true
|
||||
});
|
||||
|
||||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||||
"а какое нетто?",
|
||||
[],
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
expect(carryover?.followupSelectionMode).toBe("carry_previous_intent");
|
||||
expect(carryover?.followupContext?.previous_intent).toBe("customer_revenue_and_payments");
|
||||
expect(carryover?.followupContext?.target_intent).toBe("customer_revenue_and_payments");
|
||||
expect(carryover?.followupContext?.previous_anchor_type).toBe("counterparty");
|
||||
expect(carryover?.followupContext?.previous_anchor_value).toBe("Группа СВК");
|
||||
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||||
counterparty: "Группа СВК",
|
||||
period_from: "2021-01-01",
|
||||
period_to: "2021-12-31"
|
||||
});
|
||||
});
|
||||
|
||||
it("carries grounded metadata downstream route hints into followup context", () => {
|
||||
const policy = buildPolicy({
|
||||
findLastAddressAssistantItem: () => null,
|
||||
hasAddressFollowupContextSignal: () => true
|
||||
});
|
||||
|
||||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||||
"then documents",
|
||||
[
|
||||
{
|
||||
role: "assistant",
|
||||
text: "Metadata surface confirmed.",
|
||||
debug: {
|
||||
execution_lane: "living_chat",
|
||||
mcp_discovery_response_applied: true,
|
||||
assistant_mcp_discovery_entry_point_v1: {
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
entry_status: "bridge_executed",
|
||||
turn_input: {
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "metadata",
|
||||
asked_action_family: "inspect_documents",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
explicit_date_scope: "2020"
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
business_fact_answer_allowed: true,
|
||||
pilot: {
|
||||
pilot_scope: "metadata_inspection_v1",
|
||||
derived_metadata_surface: {
|
||||
selected_entity_set: "Документ",
|
||||
downstream_route_family: "document_evidence",
|
||||
ambiguity_detected: false
|
||||
}
|
||||
},
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
expect(carryover?.followupContext?.previous_discovery_pilot_scope).toBe("metadata_inspection_v1");
|
||||
expect(carryover?.followupContext?.previous_discovery_metadata_route_family).toBe("document_evidence");
|
||||
expect(carryover?.followupContext?.previous_discovery_metadata_selected_entity_set).toBe("Документ");
|
||||
expect(carryover?.followupContext?.previous_discovery_metadata_ambiguity_detected).toBeUndefined();
|
||||
});
|
||||
it("carries metadata ambiguity entity sets into follow-up context for downstream lane arbitration", () => {
|
||||
const policy = buildPolicy({
|
||||
findLastAddressAssistantItem: () => ({
|
||||
text: "metadata ambiguity",
|
||||
debug: {
|
||||
execution_lane: "living_chat",
|
||||
mcp_discovery_response_applied: true,
|
||||
assistant_mcp_discovery_entry_point_v1: {
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
entry_status: "bridge_executed",
|
||||
turn_input: {
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "metadata",
|
||||
asked_action_family: "inspect_documents",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
explicit_date_scope: "2020"
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
business_fact_answer_allowed: true,
|
||||
pilot: {
|
||||
pilot_scope: "metadata_inspection_v1",
|
||||
derived_metadata_surface: {
|
||||
selected_entity_set: null,
|
||||
downstream_route_family: null,
|
||||
ambiguity_detected: true,
|
||||
ambiguity_entity_sets: ["Документ", "РегистрНакопления"]
|
||||
}
|
||||
},
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
hasAddressFollowupContextSignal: () => true,
|
||||
hasReferentialPointer: () => false,
|
||||
resolveAddressIntent: () => ({ intent: "unknown" }),
|
||||
resolveAddressIntentFamily: () => null,
|
||||
resolveAssistantTurnMeaning: () => null
|
||||
});
|
||||
|
||||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||||
"по документам",
|
||||
[{ kind: "assistant", text: "metadata ambiguity" }],
|
||||
"по документам",
|
||||
{ predecomposeContract: { intent: "unknown" } },
|
||||
null
|
||||
);
|
||||
|
||||
expect(carryover?.followupContext?.previous_discovery_metadata_ambiguity_detected).toBe(true);
|
||||
expect(carryover?.followupContext?.previous_discovery_metadata_ambiguity_entity_sets).toEqual([
|
||||
"Документ",
|
||||
"РегистрНакопления"
|
||||
]);
|
||||
});
|
||||
it("preserves metadata ambiguity choice sets through a clarification assistant turn", () => {
|
||||
const policy = buildPolicy({
|
||||
findLastAddressAssistantItem: () => ({
|
||||
text: "уточните: по документам или по движениям?",
|
||||
debug: {
|
||||
execution_lane: "living_chat",
|
||||
mcp_discovery_response_applied: true,
|
||||
assistant_mcp_discovery_entry_point_v1: {
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
entry_status: "bridge_executed",
|
||||
turn_input: {
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "metadata",
|
||||
asked_action_family: "resolve_next_lane",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
metadata_ambiguity_entity_sets: ["Документ", "РегистрНакопления"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "metadata_lane_choice_clarification"
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "needs_clarification",
|
||||
business_fact_answer_allowed: false,
|
||||
pilot: {
|
||||
pilot_scope: "metadata_inspection_v1"
|
||||
},
|
||||
answer_draft: {
|
||||
answer_mode: "needs_clarification"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
hasAddressFollowupContextSignal: () => true,
|
||||
hasReferentialPointer: () => false,
|
||||
resolveAddressIntent: () => ({ intent: "unknown" }),
|
||||
resolveAddressIntentFamily: () => null,
|
||||
resolveAssistantTurnMeaning: () => null
|
||||
});
|
||||
|
||||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||||
"по движениям",
|
||||
[{ kind: "assistant", text: "уточните: по документам или по движениям?" }],
|
||||
"по движениям",
|
||||
{ predecomposeContract: { intent: "unknown" } },
|
||||
null
|
||||
);
|
||||
|
||||
expect(carryover?.followupContext?.previous_discovery_pilot_scope).toBe("metadata_inspection_v1");
|
||||
expect(carryover?.followupContext?.previous_discovery_metadata_ambiguity_detected).toBe(true);
|
||||
expect(carryover?.followupContext?.previous_discovery_metadata_ambiguity_entity_sets).toEqual([
|
||||
"Документ",
|
||||
"РегистрНакопления"
|
||||
]);
|
||||
});
|
||||
it("switches to VAT tax-period intent while preserving carried period filters", () => {
|
||||
const policy = buildPolicy({
|
||||
findLastAddressAssistantItem: () => ({
|
||||
text: "Подтвержденная дебиторская задолженность на 31.05.2017 собрана.",
|
||||
debug: {
|
||||
detected_intent: "receivables_confirmed_as_of_date",
|
||||
extracted_filters: {
|
||||
organization: 'ООО "Альтернатива Плюс"',
|
||||
as_of_date: "2017-05-31",
|
||||
period_from: "2017-05-01",
|
||||
period_to: "2017-05-31"
|
||||
},
|
||||
anchor_type: "organization",
|
||||
anchor_value_resolved: 'ООО "Альтернатива Плюс"'
|
||||
}
|
||||
}),
|
||||
hasAddressFollowupContextSignal: () => true,
|
||||
hasReferentialPointer: (value: unknown) => /этот период/i.test(String(value ?? "")),
|
||||
resolveAddressIntent: () => ({ intent: "unknown" }),
|
||||
resolveAddressIntentFamily: (intent: unknown) => {
|
||||
if (String(intent ?? "").startsWith("receivables_")) return "receivables";
|
||||
if (String(intent ?? "").startsWith("vat_")) return "vat";
|
||||
return null;
|
||||
},
|
||||
resolveAssistantTurnMeaning: () => ({
|
||||
schema_version: "assistant_turn_meaning_v1",
|
||||
asked_domain_family: "vat",
|
||||
asked_action_family: "confirmed_tax_period",
|
||||
explicit_intent_candidate: "vat_liability_confirmed_for_tax_period",
|
||||
explicit_entity_candidates: [],
|
||||
intent_override_strength: "explicit_current_turn_intent",
|
||||
stale_replay_forbidden: false
|
||||
})
|
||||
});
|
||||
|
||||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||||
"а какой ндс мы должны примерно заплатить за этот период?",
|
||||
[],
|
||||
"Какой НДС должен быть уплачен за текущий период?",
|
||||
{
|
||||
predecomposeContract: {
|
||||
intent: "unknown"
|
||||
}
|
||||
},
|
||||
null
|
||||
);
|
||||
|
||||
expect(carryover?.followupSelectionMode).toBe("carry_previous_intent");
|
||||
expect(carryover?.followupContext?.previous_intent).toBe("receivables_confirmed_as_of_date");
|
||||
expect(carryover?.followupContext?.target_intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||||
organization: 'ООО "Альтернатива Плюс"',
|
||||
as_of_date: "2017-05-31",
|
||||
period_from: "2017-05-01",
|
||||
period_to: "2017-05-31"
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
|
|||
import { createAssistantTurnMeaningPolicy } from "../src/services/assistantTurnMeaningPolicy";
|
||||
import { resolveAddressIntent } from "../src/services/addressIntentResolver";
|
||||
|
||||
function buildPolicy() {
|
||||
function buildPolicy(overrides: Record<string, unknown> = {}) {
|
||||
return createAssistantTurnMeaningPolicy({
|
||||
compactWhitespace: (value: string) => String(value ?? "").replace(/\s+/g, " ").trim(),
|
||||
repairAddressMojibake: (value: string) => value,
|
||||
|
|
@ -13,7 +13,8 @@ function buildPolicy() {
|
|||
}
|
||||
const text = String(value).trim();
|
||||
return text.length > 0 ? text : null;
|
||||
}
|
||||
},
|
||||
...overrides
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -55,4 +56,58 @@ describe("assistantTurnMeaningPolicy", () => {
|
|||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("ignores temporal tail words in all-time revenue ranking questions", () => {
|
||||
const policy = buildPolicy({
|
||||
resolveAddressIntent: (text: string) =>
|
||||
text.includes("\u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0439")
|
||||
? { intent: "customer_revenue_and_payments", confidence: "high" }
|
||||
: resolveAddressIntent(text)
|
||||
});
|
||||
|
||||
const meaning = policy.resolveAssistantTurnMeaning({
|
||||
rawUserMessage:
|
||||
"\u043a\u0442\u043e \u0443 \u043d\u0430\u0441 \u0441\u0430\u043c\u044b\u0439 \u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0439 \u043a\u043b\u0438\u0435\u043d\u0442 \u0437\u0430 \u0432\u0441\u0435 \u0432\u0440\u0435\u043c\u044f"
|
||||
});
|
||||
|
||||
expect(meaning.explicit_intent_candidate).toBe("customer_revenue_and_payments");
|
||||
expect(meaning.explicit_entity_candidates).toEqual([]);
|
||||
expect(meaning.stale_replay_forbidden).toBe(false);
|
||||
});
|
||||
|
||||
it("treats VAT period questions as supported current-turn intent", () => {
|
||||
const policy = buildPolicy({
|
||||
resolveAddressIntent: (text: string) =>
|
||||
text.includes("\u043d\u0434\u0441")
|
||||
? { intent: "vat_liability_confirmed_for_tax_period", confidence: "high" }
|
||||
: resolveAddressIntent(text)
|
||||
});
|
||||
|
||||
const meaning = policy.resolveAssistantTurnMeaning({
|
||||
rawUserMessage:
|
||||
"\u0430 \u043a\u0430\u043a\u043e\u0439 \u043d\u0434\u0441 \u043c\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c \u0437\u0430 \u044d\u0442\u043e\u0442 \u043f\u0435\u0440\u0438\u043e\u0434"
|
||||
});
|
||||
|
||||
expect(meaning.explicit_intent_candidate).toBe("vat_liability_confirmed_for_tax_period");
|
||||
expect(meaning.asked_domain_family).toBe("vat");
|
||||
expect(meaning.asked_action_family).toBe("confirmed_tax_period");
|
||||
expect(meaning.stale_replay_forbidden).toBe(false);
|
||||
});
|
||||
|
||||
it("marks broad business evaluation as unsupported-but-understood instead of stale lifecycle replay", () => {
|
||||
const policy = buildPolicy({
|
||||
resolveAddressIntent: () => ({ intent: "counterparty_activity_lifecycle", confidence: "high" })
|
||||
});
|
||||
|
||||
const meaning = policy.resolveAssistantTurnMeaning({
|
||||
rawUserMessage: "Как ты оценишь деятельность компании?"
|
||||
});
|
||||
|
||||
expect(meaning.explicit_intent_candidate).toBeNull();
|
||||
expect(meaning.asked_domain_family).toBe("business_summary");
|
||||
expect(meaning.asked_action_family).toBe("broad_evaluation");
|
||||
expect(meaning.unsupported_but_understood_family).toBe("broad_business_evaluation");
|
||||
expect(meaning.stale_replay_forbidden).toBe(true);
|
||||
expect(meaning.reason_codes).toContain("broad_business_evaluation_current_turn_signal");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue